CONTENTS
1. Introduction
2. The main loop
3. Locations
4. Objects
5. Inventory
6. Passages
7. Distance
8. North, east, south, west
9. Code generation
10. More attributes
11. Conditions
12. Open and close
13. The parser

How to program a text adventure in C

12. Open and close

In the previous chapter, we used ‘condition’ functions to make objects disappear. But of course, there is a much simpler way to achieve the same: just clear the object's location attribute!

The cave entrance was a typical example where condition functions work particularly well. The entrance is affected by attributes in other objects (the guard and the silver coin), but the functions make it possible to keep all the logic together.

Let's take a more straightforward example. Suppose the cave has a door leading to a backroom. Just a simple doorway, which the player can open and close. Just like in the previous chapter, we will use two objects for the passage; one to represent the open door, and another for when it is closed.

- backroom description "a backroom" tags "backroom" details "The room is dusty and messy.\n" - openDoorToBackroom description "an open door to the south" tags "south", "door", "doorway" destination backroom details "The door is open.\n" textGo "You walk through the door into the backroom.\n" - closedDoorToBackroom description "a closed door to the south" tags "south", "door", "doorway" location cave prospect backroom details "The door is closed.\n" textGo "The door is closed.\n"

Naturally, the door should be accessible from the other side as well.

- openDoorToCave description "an open door to the north" tags "north", "door", "doorway" destination cave details "The door is open.\n" textGo "You walk through the door into the cave.\n" - closedDoorToCave description "a closed door to the north" tags "north", "door", "doorway" location backroom prospect cave details "The door is closed.\n" textGo "The door is closed.\n"

Notice I only gave the closed doorways a location; the open ones have none. So initially, the door is closed (hence the dashed arrows between cave and backroom in the generated map you see on the right). To open the door, all we have to do is swap the locations.

openDoorToBackroom->location = cave; closedDoorToBackroom->location = NULL; openDoorToCave->location = backroom; closedDoorToCave->location = NULL;

Let's create a helper function to accommodate this.

void swapLocations(OBJECT *obj1, OBJECT *obj2) { OBJECT *tmp = obj1->location; obj1->location = obj2->location; obj2->location = tmp; }

Now the following statements can be used to open the door; and once it is open, the same statements will close it again.

swapLocations(openDoorToBackroom, closedDoorToBackroom); swapLocations(openDoorToCave, closedDoorToCave);

The helper function is particularly convenient when the object in question is movable. For example, a box can be opened and closed, but it is also an item that can be picked up and moved elsewhere. In other words, its location is not fixed. Function swapLocations does not rely on a fixed location, since it passes the current location back and forth between two objects.

Of course, a box is not a passage; the player is always on the outside, so a single pair of objects will suffice, and so will a single call to swapLocations.

swapLocations(openBox, closedBox);

This is more or less all we need to implement some new commands open and close. Below is a simple implementation of open; the implementation of close is similar.

OBJECT *obj = parseObject(noun); if (obj == closedDoorToBackRoom || obj == closedDoorToCave) { swapLocations(openDoorToBackroom, closedDoorToBackroom); swapLocations(openDoorToCave, closedDoorToCave); printf("OK.\n"); } else if (obj == closedBox) { swapLocations(openBox, closedBox); printf("OK.\n"); } else if (obj == openDoorToBackRoom || obj == openDoorToCave || obj == openBox) { printf("That is already open.\n"); } else { printf("That cannot be opened.\n"); }

To make things slightly more complicated, we can put a lock on the door or on the box. This requires (at least) three mutually exclusive objects; one for each of the possible states: open, closed and locked. But we can still use the same function to swap locations of the objects. For example, here's how to unlock a locked box; and vice versa.

swapLocations(closedBox, lockedBox);

There is some overhead involved in the other commands. Our implementation of command open must be expanded to handle the new object lockedBox:

... else if (obj == lockedBox) { printf("You can't, it is locked.\n"); } ...

It may be clear that the number of lines of code is proportional to the number of doors in the game (and boxes and other objects that can be opened). So if your game has more than just a handful of doors, then it is a good idea to go for a more generic solution. By the way, this is something that goes for every command: when it concerns a good many objects, try to write generic code; but when you are dealing with one or two special cases, just stick to straightforward, specialized code.

Generic code typically comes with a data-driven approach. In other words, we need to add one or more attributes to our object structure. In this particular case, we will be adding a function pointer for each of the commands we wish to support: open, close, lock and unlock.

object.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 #include <stdio.h> #include "object.h" #include "toggle.h" typedef struct object { int (*condition)(struct object *); const char *description; const char **tags; struct object *location; struct object *destination; struct object *prospect; const char *details; const char *contents; const char *textGo; int weight; int capacity; int health; const char *(*open)(struct object *); const char *(*close)(struct object *); const char *(*lock)(struct object *); const char *(*unlock)(struct object *); } OBJECT; extern OBJECT objs[]; - field description "an open field" tags "field" details "The field is a nice and quiet place under a clear blue sky.\n" - cave description "a little cave" tags "cave" details "The cave is just a cold, damp, rocky chamber.\n" - silver description "a silver coin" tags "silver", "coin", "silver coin" location field details "The coin has an eagle on the obverse.\n" weight 1 - gold description "a gold coin" tags "gold", "coin", "gold coin" location openBox details "The shiny coin seems to be a rare and priceless artefact.\n" weight 1 - guard description "a burly guard" tags "guard", "burly guard" location field details "The guard is a really big fellow.\n" contents "He has" health 100 capacity 20 - player description "yourself" tags "yourself" location field details "You would need a mirror to look at yourself.\n" contents "You have" health 100 capacity 20 - intoCave condition { return guard->health == 0 || silver->location == guard; } description "a cave entrance to the east" tags "east", "entrance" location field destination cave details "The entrance is just a narrow opening in a small outcrop.\n" textGo "You walk into the cave.\n" open isAlreadyOpen - intoCaveBlocked condition { return guard->health > 0 && silver->location != guard; } description "a cave entrance to the east" tags "east", "entrance" location field prospect cave details "The entrance is just a narrow opening in a small outcrop.\n" textGo "The guard stops you from walking into the cave.\n" open isAlreadyOpen - exitCave description "a way out to the west" tags "west", "out" location cave destination field details "Sunlight pours in through an opening in the cave's wall.\n" textGo "You walk out of the cave.\n" open isAlreadyOpen - wallField description "dense forest all around" tags "west", "north", "south", "forest" location field details "The field is surrounded by trees and undergrowth.\n" textGo "Dense forest is blocking the way.\n" - wallCave description "solid rock all around" tags "east", "north", "rock" location cave details "Carved in stone is a secret password 'abccb'.\n" textGo "Solid rock is blocking the way.\n" - backroom description "a backroom" tags "backroom" details "The room is dusty and messy.\n" - wallBackroom description "solid rock all around" tags "east", "west", "south", "rock" location backroom details "Trendy wallpaper covers the rock walls.\n" textGo "Solid rock is blocking the way.\n" - openDoorToBackroom description "an open door to the south" tags "south", "door", "doorway" destination backroom details "The door is open.\n" textGo "You walk through the door into the backroom.\n" open isAlreadyOpen close toggleBackdoor - closedDoorToBackroom description "a closed door to the south" tags "south", "door", "doorway" location cave prospect backroom details "The door is closed.\n" textGo "The door is closed.\n" open toggleBackdoor close isAlreadyClosed - openDoorToCave description "an open door to the north" tags "north", "door", "doorway" destination cave details "The door is open.\n" textGo "You walk through the door into the cave.\n" open isAlreadyOpen close toggleBackdoor - closedDoorToCave description "a closed door to the north" tags "north", "door", "doorway" location backroom prospect cave details "The door is closed.\n" textGo "The door is closed.\n" open toggleBackdoor close isAlreadyClosed - openBox description "a wooden box" tags "box", "wooden box" details "The box is open.\n" weight 5 capacity 10 open isAlreadyOpen close toggleBox lock isStillOpen unlock isAlreadyOpen - closedBox description "a wooden box" tags "box", "wooden box" details "The box is closed.\n" weight 5 open toggleBox close isAlreadyClosed lock toggleBoxLock unlock isAlreadyUnlocked - lockedBox description "a wooden box" tags "box", "wooden box" location backroom details "The box is closed.\n" weight 5 open isStillLocked close isAlreadyClosed lock isAlreadyLocked unlock toggleBoxLock - keyForBox description "a tiny key" tags "key", "tiny key" location cave details "The key is really small and shiny.\n" weight 1

To keep the code maintainable, I deliberately did not use anonymous functions this time. Instead, we will implement the necessary functionality in a separate module.

toggle.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 extern const char *cannotBeOpened(OBJECT *obj); extern const char *cannotBeClosed(OBJECT *obj); extern const char *cannotBeLocked(OBJECT *obj); extern const char *cannotBeUnlocked(OBJECT *obj); extern const char *isAlreadyOpen(OBJECT *obj); extern const char *isAlreadyClosed(OBJECT *obj); extern const char *isAlreadyLocked(OBJECT *obj); extern const char *isAlreadyUnlocked(OBJECT *obj); extern const char *isStillOpen(OBJECT *obj); extern const char *isStillLocked(OBJECT *obj); extern const char *toggleBackdoor(OBJECT *obj); extern const char *toggleBox(OBJECT *obj); extern const char *toggleBoxLock(OBJECT *obj);
toggle.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include "object.h" static void swapLocations(OBJECT *obj1, OBJECT *obj2) { OBJECT *tmp = obj1->location; obj1->location = obj2->location; obj2->location = tmp; } const char *cannotBeOpened(OBJECT *obj) { return "That cannot be opened.\n"; } const char *cannotBeClosed(OBJECT *obj) { return "That cannot be closed.\n"; } const char *cannotBeLocked(OBJECT *obj) { return "That cannot be locked.\n"; } const char *cannotBeUnlocked(OBJECT *obj) { return "That cannot be unlocked.\n"; } const char *isAlreadyOpen(OBJECT *obj) { return "That is already open.\n"; } const char *isAlreadyClosed(OBJECT *obj) { return "That is already closed.\n"; } const char *isAlreadyLocked(OBJECT *obj) { return "That is already locked.\n"; } const char *isAlreadyUnlocked(OBJECT *obj) { return "That is already unlocked.\n"; } const char *isStillOpen(OBJECT *obj) { return "That is still open.\n"; } const char *isStillLocked(OBJECT *obj) { return "That is still locked.\n"; } const char *toggleBackdoor(OBJECT *obj) { swapLocations(openDoorToBackroom, closedDoorToBackroom); swapLocations(openDoorToCave, closedDoorToCave); return "OK.\n"; } const char *toggleBox(OBJECT *obj) { swapLocations(openBox, closedBox); return "OK.\n"; } const char *toggleBoxLock(OBJECT *obj) { if (keyForBox->location == player) { swapLocations(closedBox, lockedBox); return "OK.\n"; } else { return "You don't have a key.\n"; } }

As announced earlier, the implementations of the four commands open, close, lock and unlock are totally generic.

execute.h
1 2 3 4 extern void executeOpen(const char *noun); extern void executeClose(const char *noun); extern void executeLock(const char *noun); extern void executeUnlock(const char *noun);
execute.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <stdio.h> #include "object.h" #include "misc.h" static int objectWithinReach(const char *verb, OBJECT *obj, const char *noun) { int ok = 0; DISTANCE distance = distanceTo(obj); if (distance > distNotHere) { printf("I don't understand what you want to %s.\n", verb); } else if (distance == distNotHere) { printf("You don't see any %s here.\n", noun); } else if (distance >= distHereContained) { printf("That is out of reach.\n"); } else { ok = 1; } return ok; } void executeOpen(const char *noun) { OBJECT *obj = parseObject(noun); if (objectWithinReach("open", obj, noun)) { printf("%s", (*obj->open)(obj)); } } void executeClose(const char *noun) { OBJECT *obj = parseObject(noun); if (objectWithinReach("close", obj, noun)) { printf("%s", (*obj->close)(obj)); } } void executeLock(const char *noun) { OBJECT *obj = parseObject(noun); if (objectWithinReach("lock", obj, noun)) { printf("%s", (*obj->lock)(obj)); } } void executeUnlock(const char *noun) { OBJECT *obj = parseObject(noun); if (objectWithinReach("unlock", obj, noun)) { printf("%s", (*obj->unlock)(obj)); } }

Explanation:

The necessary modifications to object.awk are very straightforward:

object.awk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 BEGIN { count = 0; obj = ""; if (pass == "c2") { print "\nstatic int alwaysTrue(OBJECT *obj) { return 1; }"; print "\nOBJECT objs[] = {"; } } /^- / { outputRecord(","); obj = $2; prop["condition"] = "alwaysTrue"; prop["description"] = "NULL"; prop["tags"] = ""; prop["location"] = "NULL"; prop["destination"] = "NULL"; prop["prospect"] = ""; prop["details"] = "\"You see nothing special.\\n\""; prop["contents"] = "\"You see\""; prop["textGo"] = "\"You can't get any closer than this.\\n\""; prop["weight"] = "99"; prop["capacity"] = "9999"; prop["health"] = "0"; prop["open"] = "cannotBeOpened"; prop["close"] = "cannotBeClosed"; prop["lock"] = "cannotBeLocked"; prop["unlock"] = "cannotBeUnlocked"; } obj && /^[ \t]+[a-z]/ { name = $1; $1 = ""; if (name in prop) { prop[name] = $0; if (/^[ \t]*\{/) { prop[name] = name count; if (pass == "c1") print "static int " prop[name] "(OBJECT *obj) " $0; } } else if (pass == "c2") { print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\""; } } !obj && pass == (/^#include/ ? "c1" : "h") { print; } END { outputRecord("\n};"); if (pass == "h") { print "\n#define endOfObjs\t(objs + " count ")"; print "\n#define validObject(obj)\t" \ "((obj) != NULL && (*(obj)->condition)((obj)))"; print "\n#define forEachObject(obj)\t" \ "for (obj = objs; obj < endOfObjs; obj++) if (validObject(obj))"; } } function outputRecord(separator) { if (obj) { if (pass == "h") { print "#define " obj "\t(objs + " count ")"; } else if (pass == "c1") { print "static const char *tags" count "[] = {" prop["tags"] ", NULL};"; } else if (pass == "c2") { print "\t{\t/* " count " = " obj " */"; print "\t\t" prop["condition"] ","; print "\t\t" prop["description"] ","; print "\t\ttags" count ","; print "\t\t" prop["location"] ","; print "\t\t" prop["destination"] ","; print "\t\t" prop[prop["prospect"] ? "prospect" : "destination"] ","; print "\t\t" prop["details"] ","; print "\t\t" prop["contents"] ","; print "\t\t" prop["textGo"] ","; print "\t\t" prop["weight"] ","; print "\t\t" prop["capacity"] ","; print "\t\t" prop["health"] ","; print "\t\t" prop["open"] ","; print "\t\t" prop["close"] ","; print "\t\t" prop["lock"] ","; print "\t\t" prop["unlock"]; print "\t}" separator; delete prop; } count++; } }
Sample output
Welcome to Little Cave Adventure. You are in an open field. You see: a silver coin a burly guard a cave entrance to the east dense forest all around --> get coin OK. --> give coin OK. --> go cave You walk into the cave. You are in a little cave. You see: a way out to the west solid rock all around a closed door to the south a tiny key --> get key OK. --> go south The door is closed. --> open door OK. --> go south You walk through the door into the backroom. You are in a backroom. You see: solid rock all around an open door to the north a wooden box --> unlock box OK. --> open box OK. --> look box The box is open. You see: a gold coin --> get gold Sorry, a wooden box is holding it. --> quit Bye!

The additions to main.c are equally straightforward.

main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include <stdio.h> #include <string.h> #include "location.h" #include "inventory.h" #include "execute.h" static char input[100]; static int getInput() { printf("\n--> "); return fgets(input, sizeof(input), stdin) != NULL; } static int parseAndExecute() { char *verb = strtok(input, " \n"); char *noun = strtok(NULL, "\n"); if (verb != NULL) { if (strcmp(verb, "quit") == 0) { return 0; } else if (strcmp(verb, "look") == 0) { executeLook(noun); } else if (strcmp(verb, "go") == 0) { executeGo(noun); } else if (strcmp(verb, "get") == 0) { executeGet(noun); } else if (strcmp(verb, "drop") == 0) { executeDrop(noun); } else if (strcmp(verb, "give") == 0) { executeGive(noun); } else if (strcmp(verb, "ask") == 0) { executeAsk(noun); } else if (strcmp(verb, "inventory") == 0) { executeInventory(); } else if (strcmp(verb, "open") == 0) { executeOpen(noun); } else if (strcmp(verb, "close") == 0) { executeClose(noun); } else if (strcmp(verb, "lock") == 0) { executeLock(noun); } else if (strcmp(verb, "unlock") == 0) { executeUnlock(noun); } else { printf("I don't know how to '%s'.\n", verb); } } return 1; } int main() { printf("Welcome to Little Cave Adventure.\n"); executeLook("around"); while (getInput() && parseAndExecute()); printf("\nBye!\n"); return 0; }

Notes:


Next chapter: 13. The parser