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

10. More attributes

There are many possible reasons for expanding the ‘object’ structure.

When we introduced objects in chapter 4, they had only three attributes. In chapter 6, we added a fourth. This is more or less the absolute minimum. To put more detail into our adventure, we need some more attributes. Here are a few examples.

  1. The command look around gives a global description of the player's location, including a list of items, persons and other objects present there. Many adventures require the player to examine these objects, either to reveal certain clues that are needed to make progress in the game, or simply to enhance the game's atmosphere. We will add an attribute details that holds a detailed description of each object, plus an attribute contents that is used with objects that contain other objects.
  2. When the player follows a passage, the response is invariably “OK” followed by a description of the new location. This is a bit dull; it would be so much nicer to give each passage its own custom message. We will add an attribute textGo to hold this message.
  3. Some passage have a ‘twist’; they do not go where the player expects them to go. For example, a forest path may be hiding a pitfall. While the passage appears to be leading from location A to location B, in reality the end point is location C, i.e. the bottom of a pit. More common ‘twists’ are passages that are ‘blocked’: a closed grating, a broken bridge, a narrow crack. Suppose our cave entrance is blocked by the guard. Any attempt to enter the cave will fail; instead the player will stay at his original location, i.e. the field. We could simply change the passage's destination to field (or NULL), but that would result in an undesirable response to commands like go cave and look cave: “You don't see any cave here.” We need separate attributes for the actual and the apparent end point of a passage. We will introduce an attribute prospect to represent the latter; the attribute destination, introduced in chapter 6, still holds the actual end point. In most cases, the two will be equal, so we will have object.awk generate an appropriate default; prospect only needs to be specified in object.txt when it differs from destination.
  4. In many adventures, the player, as well as other persons in the game, are limited in how much they can carry. Give each item a weight; the combined weight of all items in a person's inventory should not exceed that person's capacity. Give an object a very high weight to make it immovable (a tree, a house, a mountain).
  5. RPG-style adventure games will need a whole range of attributes for persons (both player and non-player), for example health. Objects with zero health are either dead, or they are not a person at all.

We define seven new attributes in object.txt:

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 #include <stdio.h> #include "object.h" typedef 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 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"

Explanation:

New attributes also require an adjustment in the code generator.

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 BEGIN { count = 0; obj = ""; if (pass == "c2") { print "\nOBJECT objs[] = {"; } } /^- / { outputRecord(","); obj = $2; 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; } 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 ")"; } } 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["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++; } }

Now we're all set to start using the new attributes! details is used in a newly recognized command look <object>, and textGo replaces the fixed text “OK” in our implementation of command go.

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 --> look guard The guard is a really big fellow. --> get guard That is way to heavy. --> get coin OK. --> inventory You have: a silver coin --> give coin OK. --> look guard The guard is a really big fellow. He has: a silver coin --> go cave The guard stops you from walking into the cave. --> quit Bye!
location.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 #include <stdio.h> #include <string.h> #include "object.h" #include "misc.h" void executeLook(const char *noun) { if (noun != NULL && strcmp(noun, "around") == 0) { printf("You are in %s.\n", player->location->description); listObjectsAtLocation(player->location); } else { OBJECT *obj = parseObject(noun); DISTANCE distance = distanceTo(obj); if (distance >= distUnknownObject) { printf("I don't understand what you want to see.\n"); } else if (distance == distNotHere) { printf("You don't see any %s here.\n", noun); } else if (distance == distOverthere) { printf("Too far away, move closer please.\n"); } else if (distance == distHereContained) { printf("Hard to see, try to get it first.\n"); } else { printf("%s", obj->details); listObjectsAtLocation(obj); } } } static void movePlayer(OBJECT *passage) { printf("%s", passage->textGo); if (passage->destination != NULL) { player->location = passage->destination; printf("\n"); executeLook("around"); } } void executeGo(const char *noun) { OBJECT *obj = parseObject(noun); DISTANCE distance = distanceTo(obj); if (distance >= distUnknownObject) { printf("I don't understand where you want to go.\n"); } else if (distance == distLocation) { printf("You are already there.\n"); } else if (distance == distOverthere) { movePlayer(getPassageTo(obj)); } else if (distance == distHere) { movePlayer(obj); } else if (distance < distNotHere) { printf("You can't get any closer than this.\n"); } else { printf("You don't see any %s here.\n", noun); } }

Attributes weight and capacity together become a possible reason for not being able to move certain objects around.

inventory.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 #include <stdio.h> #include "object.h" #include "misc.h" static void moveObject(const char *noun, OBJECT *from, OBJECT *to) { OBJECT *obj = parseObject(noun); if (obj == NULL) { printf("I don't understand what item you mean.\n"); } else if (from != obj->location) { switch (distanceTo(obj)) { case distPlayer: printf("You should not be doing that to yourself.\n"); break; case distHeld: printf("You already have %s.\n", obj->description); break; case distLocation: case distOverthere: printf("That's not an item.\n"); break; case distHere: if (from == player) { printf("You have no %s.\n", noun); } else { printf("Sorry, %s has no %s.\n", from->description, noun); } break; case distHeldContained: case distHereContained: printf("Sorry, %s is holding it.\n", obj->location->description); break; default: printf("You don't see any %s here.\n", noun); } } else if (to == NULL) { printf("There is nobody here to give that to.\n"); } else if (obj->weight > to->capacity) { printf("That is way too heavy.\n"); } else if (obj->weight + weightOfContents(to) > to->capacity) { printf("That would become too heavy.\n"); } else { obj->location = to; printf("OK.\n"); } } void executeGet(const char *noun) { moveObject(noun, player->location, player); } void executeDrop(const char *noun) { moveObject(noun, player, player->location); } void executeGive(const char *noun) { moveObject(noun, player, personHere()); } void executeAsk(const char *noun) { moveObject(noun, personHere(), player); } void executeInventory(void) { if (listObjectsAtLocation(player) == 0) { printf("You are empty-handed.\n"); } }

The weight check makes use of a new function weightOfContents; it will be implemented in misc.c. In the same module, we also make modifications to some of the existing functions, to support the last few attributes.

Attribute health will be used to tell persons apart from ‘dead’ objects. And contents replaces the fixed text “You see”. The original text was already a bit odd when listing the player's inventory, but now that function listObjectsAtLocation is used to display contents of any possible object (see function executeLook above), we really need something more flexible.

By replacing attribute destination by prospect in function getPassageTo, we are improving responses to all commands (not just go and look) applied to a location that is seen lying on the other end of a ‘passage with a twist.’

misc.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 typedef enum { distPlayer, distHeld, distHeldContained, distLocation, distHere, distHereContained, distOverthere, distNotHere, distUnknownObject, distNoObjectSpecified } DISTANCE; extern OBJECT *getPassageTo(OBJECT *targetLocation); extern DISTANCE distanceTo(OBJECT *obj); extern OBJECT *parseObject(const char *noun); extern OBJECT *personHere(void); extern int listObjectsAtLocation(OBJECT *location); extern int weightOfContents(OBJECT *container);
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; for (obj = objs; obj < endOfObjs; obj++) { if (obj->location == player->location && obj->prospect == targetLocation) { return obj; } } return NULL; } DISTANCE distanceTo(OBJECT *obj) { return obj == NULL ? distUnknownObject : obj == player ? distPlayer : obj == player->location ? distLocation : obj->location == player ? distHeld : obj->location == player->location ? distHere : getPassageTo(obj) != NULL ? distOverthere : obj->location == NULL ? 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; for (obj = objs; obj < endOfObjs; obj++) { if (noun != NULL && nounIsInTags(noun, obj->tags) && distanceTo(obj) < distanceTo(found)) { found = obj; } } return found; } OBJECT *personHere(void) { OBJECT *obj; for (obj = objs; obj < endOfObjs; obj++) { if (distanceTo(obj) == distHere && obj->health > 0) { return obj; } } return NULL; } int listObjectsAtLocation(OBJECT *location) { int count = 0; OBJECT *obj; for (obj = objs; obj < endOfObjs; 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; for (obj = objs; obj < endOfObjs; obj++) { if (obj->location == container) sum += obj->weight; } return sum; }

To make the whole picture complete, it would be nice to expand the generated map from the previous chapter with dashed lines for the ‘apparent’ passages (leading towards a prospect).

map.awk
1 2 3 4 5 6 7 8 9 10 11 12 BEGIN { print "digraph map {"; } /^- / { outputEdge(); location = destination = prospect = ""; } $1 == "location" { location = $2; } $1 == "destination" { destination = $2; } $1 == "prospect" { prospect = $2; } END { outputEdge(); print "}"; } function outputEdge() { if (location && destination) print "\t" location " -> " destination; if (location && prospect) print "\t" location " -> " prospect " [style=dashed]"; }

Notes:


Next chapter: 11. Conditions