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

11. Conditions

So far, all the objects' attributes were data: text, numbers. But attributes may just as well be code.

In the previous chapter, we limited the player's freedom of movement by closing the cave entrance (passage intoCave). This already makes the game a lot more challenging, but it does not make much of a puzzle, unless we offer a tiny possibility for the player to open the passage. The real challenge should be for the player to find the condition under which the passage opens.

Let's take a simple example. To get past the guard and enter the cave, the player has to either kill or bribe the guard (or both, for what it's worth). In other words:

Opening a closed passage (in this case intoCave) involves changing a number of attribute values:

There are a number of ways to accomplish this. Here, I will discuss an approach that is simple, maintainable and versatile.

First of all, we define two separate passages: one that represents the open passage, and the other representing the closed passage. The passages are identical in every attribute except for the ones listed above. (In the generated map you see below, notice the two arrows leading into the cave; one solid, one dashed.)

Next, we introduce a new attribute named condition that determines whether or not a certain object exists. The two passages will be given mutually exclusive conditions, so that only one of them can exist at any given time.

Each condition will be implemented as a boolean-valued function; true means the object exists, false means it does not.

int intoCaveIsOpen(OBJECT *obj) { return guard->health == 0 || silver->location == guard; } int intoCaveIsClosed(OBJECT *obj) { return guard->health > 0 && silver->location != guard; }

The parameter obj is unused, but it may prove its use in more generic functions that can be used for more than one object; so I prefer to keep it there.

The new attribute condition is a pointer to such a function:

int (*condition)(OBJECT *);

After some small modifications to object.awk, similar to those made in the previous chapter, we can immediately start assigning functions to the new attribute in object.txt.

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 --> go entrance The guard stops you from walking into the cave. --> get coin OK. --> give coin OK. --> go entrance You walk into the cave. You are in a little cave. You see: a gold coin a way out to the west solid rock all around --> quit Bye!
- intoCave condition intoCaveIsOpen description "a cave entrance to the east" tags "east", "entrance" location field destination cave detail "The entrance is just a narrow opening in a small outcrop.\n" textGo "You walk into the cave.\n" - intoCaveBlocked condition intoCaveIsClosed description "a cave entrance to the east" tags "east", "entrance" location field prospect cave detail "The entrance is just a narrow opening in a small outcrop.\n" textGo "The guard stops you from walking into the cave.\n"

The two ‘condition’ functions are so specific, each of them is used just this once. Now, wouldn't it be nice to define the functions right where we need them? Many programming languages support anonymous functions; something like this:

- intoCave condition { return guard->health == 0 || silver->location == guard; } ... - intoCaveBlocked condition { return guard->health > 0 && silver->location != guard; } ...

Plain C does not allow this, but since object.txt is a product of our own domain-specific language (see chapter 9), we can do anything we like! That is, if we can make the code generator turn it into something the C compiler will swallow. The following adjustments to object.awk will do just that.

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 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"; } 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}" separator; delete prop; } count++; } }

Explanation:

So now we can add the extra passage and the conditions to object.txt, as explained earlier.

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 #include <stdio.h> #include "object.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; } 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 cave 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" - 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" - 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" - 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", "south", "rock" location cave details "Carved in stone is a secret password 'abccb'.\n" textGo "Solid rock is blocking the way.\n"

To make the conditions work, we need to adjust function distanceTo, plus every loop that scans through objects.

misc.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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 #include <stdio.h> #include <string.h> #include "object.h" #include "misc.h" OBJECT *getPassageTo(OBJECT *targetLocation) { OBJECT *obj; forEachObject(obj) { if (obj->location == player->location && obj->prospect == targetLocation) { return obj; } } return NULL; } DISTANCE distanceTo(OBJECT *obj) { return !validObject(obj) ? distUnknownObject : obj == player ? distPlayer : obj == player->location ? distLocation : obj->location == player ? distHeld : obj->location == player->location ? distHere : getPassageTo(obj) != NULL ? distOverthere : !validObject(obj->location) ? distNotHere : obj->location->location == player ? distHeldContained : obj->location->location == player->location ? distHereContained : distNotHere; } static int nounIsInTags(const char *noun, const char **tags) { while (*tags != NULL) { if (strcmp(noun, *tags++) == 0) return 1; } return 0; } OBJECT *parseObject(const char *noun) { OBJECT *obj, *found = NULL; forEachObject(obj) { if (noun != NULL && nounIsInTags(noun, obj->tags) && distanceTo(obj) < distanceTo(found)) { found = obj; } } return found; } OBJECT *personHere(void) { OBJECT *obj; forEachObject(obj) { if (distanceTo(obj) == distHere && obj->health > 0) { return obj; } } return NULL; } int listObjectsAtLocation(OBJECT *location) { int count = 0; OBJECT *obj; forEachObject(obj) { if (obj != player && obj->location == location) { if (count++ == 0) { printf("%s:\n", location->contents); } printf("%s\n", obj->description); } } return count; } int weightOfContents(OBJECT *container) { int sum = 0; OBJECT *obj; forEachObject(obj) { if (obj->location == container) sum += obj->weight; } return sum; }

Notes:


Next chapter: 12. Open and close