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
14. Multiple nouns
15. Light and dark
16. Savegame
17. Test automation
18. Abbreviations
19. Conversations
20. Combat
21. Multi-player
22. Client-server
23. Database
24. Speech
25. JavaScript

How to program a text adventure in C

by Ruud Helderman <r.helderman@hccnet.nl>

Licensed under MIT License

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

Explanation:

To avoid duplicate code, I deliberately did not use anonymous functions this time. Instead, we will implement the necessary logic in a separate module. Function swapLocations is in there too, but in a slightly extended version, which will also output feedback to the user.

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

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

openclose.h
  1. extern void executeOpen(const char *noun);
  2. extern void executeClose(const char *noun);
  3. extern void executeLock(const char *noun);
  4. extern void executeUnlock(const char *noun);
openclose.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "reach.h"
  5. void executeOpen(const char *noun)
  6. {
  7. OBJECT *obj = reachableObject("what you want to open", noun);
  8. if (obj != NULL) (*obj->open)();
  9. }
  10. void executeClose(const char *noun)
  11. {
  12. OBJECT *obj = reachableObject("what you want to close", noun);
  13. if (obj != NULL) (*obj->close)();
  14. }
  15. void executeLock(const char *noun)
  16. {
  17. OBJECT *obj = reachableObject("what you want to lock", noun);
  18. if (obj != NULL) (*obj->lock)();
  19. }
  20. void executeUnlock(const char *noun)
  21. {
  22. OBJECT *obj = reachableObject("what you want to unlock", noun);
  23. if (obj != NULL) (*obj->unlock)();
  24. }

Above, I used a generic function reachableObject to handle objects that are not here; see below for its implementation. This way, we don’t have to write the same code four times (once for every execute function). More commands will be added in chapter 15; these will benefit from the same function.

reach.h
  1. extern OBJECT *reachableObject(const char *intention, const char *noun);
reach.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "misc.h"
  5. #include "noun.h"
  6. OBJECT *reachableObject(const char *intention, const char *noun)
  7. {
  8. OBJECT *obj = getVisible(intention, noun);
  9. switch (getDistance(player, obj))
  10. {
  11. case distSelf:
  12. printf("You should not be doing that to yourself.\n");
  13. break;
  14. case distHeldContained:
  15. case distHereContained:
  16. printf("You would have to get it from %s first.\n",
  17. obj->location->description);
  18. break;
  19. case distOverthere:
  20. printf("Too far away, move closer please.\n");
  21. break;
  22. case distNotHere:
  23. case distUnknownObject:
  24. // already handled by getVisible
  25. break;
  26. default:
  27. return obj;
  28. }
  29. return NULL;
  30. }

The necessary modifications to object.awk are very straightforward:

object.awk
  1. BEGIN {
  2. count = 0;
  3. obj = "";
  4. if (pass == "c2") {
  5. print "\nstatic bool alwaysTrue(void) { return true; }";
  6. print "\nOBJECT objs[] = {";
  7. }
  8. }
  9. /^- / {
  10. outputRecord(",");
  11. obj = $2;
  12. prop["condition"] = "alwaysTrue";
  13. prop["description"] = "NULL";
  14. prop["tags"] = "";
  15. prop["location"] = "NULL";
  16. prop["destination"] = "NULL";
  17. prop["prospect"] = "";
  18. prop["details"] = "\"You see nothing special.\"";
  19. prop["contents"] = "\"You see\"";
  20. prop["textGo"] = "\"You can't get much closer than this.\"";
  21. prop["weight"] = "99";
  22. prop["capacity"] = "0";
  23. prop["health"] = "0";
  24. prop["open"] = "cannotBeOpened";
  25. prop["close"] = "cannotBeClosed";
  26. prop["lock"] = "cannotBeLocked";
  27. prop["unlock"] = "cannotBeUnlocked";
  28. }
  29. obj && /^[ \t]+[a-z]/ {
  30. name = $1;
  31. $1 = "";
  32. if (name in prop) {
  33. prop[name] = $0;
  34. if (/^[ \t]*\{/) {
  35. prop[name] = name count;
  36. if (pass == "c1") print "static bool " prop[name] "(void) " $0;
  37. }
  38. }
  39. else if (pass == "c2") {
  40. print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\"";
  41. }
  42. }
  43. !obj && pass == (/^#include/ ? "c1" : "h") {
  44. print;
  45. }
  46. END {
  47. outputRecord("\n};");
  48. if (pass == "h") {
  49. print "\n#define endOfObjs\t(objs + " count ")";
  50. print "\n#define validObject(obj)\t" \
  51. "((obj) != NULL && (*(obj)->condition)())";
  52. }
  53. }
  54. function outputRecord(separator)
  55. {
  56. if (obj) {
  57. if (pass == "h") {
  58. print "#define " obj "\t(objs + " count ")";
  59. }
  60. else if (pass == "c1") {
  61. print "static const char *tags" count "[] = {" prop["tags"] ", NULL};";
  62. }
  63. else if (pass == "c2") {
  64. print "\t{\t/* " count " = " obj " */";
  65. print "\t\t" prop["condition"] ",";
  66. print "\t\t" prop["description"] ",";
  67. print "\t\ttags" count ",";
  68. print "\t\t" prop["location"] ",";
  69. print "\t\t" prop["destination"] ",";
  70. print "\t\t" prop[prop["prospect"] ? "prospect" : "destination"] ",";
  71. print "\t\t" prop["details"] ",";
  72. print "\t\t" prop["contents"] ",";
  73. print "\t\t" prop["textGo"] ",";
  74. print "\t\t" prop["weight"] ",";
  75. print "\t\t" prop["capacity"] ",";
  76. print "\t\t" prop["health"] ",";
  77. print "\t\t" prop["open"] ",";
  78. print "\t\t" prop["close"] ",";
  79. print "\t\t" prop["lock"] ",";
  80. print "\t\t" prop["unlock"];
  81. print "\t}" separator;
  82. delete prop;
  83. }
  84. count++;
  85. }
  86. }
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
You pick up a silver coin.

--> give coin
You give a silver coin to a burly guard.

--> go cave
You walk into the cave.

You are in a little cave.
You see:
an exit to the west
solid rock all around
a closed door to the south
a tiny key

--> get key
You pick up a tiny key.

--> go south
The door is closed.

--> open door
You open a closed door to the south.

--> go south
You walk through the door into a backroom.

You are in a backroom.
You see:
solid rock all around
an open door to the north
a wooden box

--> unlock box
You unlock a wooden box.

--> open box
You open a wooden box.

--> look box
The box is open.
You see:
a gold coin

--> get gold
You get a gold coin from a wooden box.

--> quit

Bye!

The additions to parsexec.c are equally straightforward.

parsexec.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include "location.h"
  5. #include "inventory.h"
  6. #include "openclose.h"
  7. bool parseAndExecute(char *input)
  8. {
  9. char *verb = strtok(input, " \n");
  10. char *noun = strtok(NULL, "\n");
  11. if (verb != NULL)
  12. {
  13. if (strcmp(verb, "quit") == 0)
  14. {
  15. return false;
  16. }
  17. else if (strcmp(verb, "look") == 0)
  18. {
  19. executeLook(noun);
  20. }
  21. else if (strcmp(verb, "go") == 0)
  22. {
  23. executeGo(noun);
  24. }
  25. else if (strcmp(verb, "get") == 0)
  26. {
  27. executeGet(noun);
  28. }
  29. else if (strcmp(verb, "drop") == 0)
  30. {
  31. executeDrop(noun);
  32. }
  33. else if (strcmp(verb, "give") == 0)
  34. {
  35. executeGive(noun);
  36. }
  37. else if (strcmp(verb, "ask") == 0)
  38. {
  39. executeAsk(noun);
  40. }
  41. else if (strcmp(verb, "inventory") == 0)
  42. {
  43. executeInventory();
  44. }
  45. else if (strcmp(verb, "open") == 0)
  46. {
  47. executeOpen(noun);
  48. }
  49. else if (strcmp(verb, "close") == 0)
  50. {
  51. executeClose(noun);
  52. }
  53. else if (strcmp(verb, "lock") == 0)
  54. {
  55. executeLock(noun);
  56. }
  57. else if (strcmp(verb, "unlock") == 0)
  58. {
  59. executeUnlock(noun);
  60. }
  61. else
  62. {
  63. printf("I don't know how to '%s'.\n", verb);
  64. }
  65. }
  66. return true;
  67. }

Notes:


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 13. The parser