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

7. Distance

A typical adventure contains many puzzles. Infocom adventures were notoriously difficult to complete; solving every puzzle might require weeks, even months of trial and error. Don't be surprised if the majority of replies from the game are ‘errors’: you can't do this; you can't go there; you died.

So far, our error handling has been very basic. For example, command get just returns “You can't” in all possible situations that prevent the player from picking up something. Such a reply is dull and not very helpful. It neglects an important aspect of any computer game; in fact, an essential part of life itself: the player has to learn from his mistakes. It is OK for an adventure to be difficult, even frustratingly difficult. But when the player has the feeling he is not making any progress at all, or when the only way to make progress is a brute-force attack on all verb-noun combinations, then even the most hardened player will lose interest and eventually give up. The least an adventure game should do, is explain why the player's command cannot be completed: “You can't do that, because...” This helps to make the virtual world more convincing, the story more credible, and the game more enjoyable.

Most commands operate on one or more objects, for example:

The first thing to check (after the obvious typing errors caught by the parser) is for the presence of these objects; failure should result in something like “There is no ... here” or “You don't see any ...” In this chapter, we will build a generic function that can be used by every command to find out if an object is within reach of the player.

You may think we only need to distinguish two cases: either the object is here, or it is not. But many commands require more gradients than just ‘here’ and ‘not here’. Examples:

This is because the term 'here' can mean a lot of things:

distPlayer The object is the player object == player
distHeld The player is holding the object object->location == player
distHeldContained The player is holding another object (for example a bag) containing the object object->location != NULL && object->location->location == player
distLocation The object is the player's location object == player->location
distHere The object is present at the player's location object->location == player->location
distHereContained Another object (either a person or a ‘container’), present at the player's location, is holding (but not hiding) the object object->location != NULL && object->location->location == player->location
distOverthere The object is a nearby location getPassageTo(object) != NULL

The first case (object is player) may seem trivial, but it is important nonetheless. For example, the command "examine yourself" should not return "There is no yourself here."

I tried to follow a logical order: nearby things are at the top, further down below they become more distant. We can continue the list, to cover objects that are even further away:

distNotHere The object is (or appears to be) not here  
distUnknownObject The parser did not recognize the noun entered (we will use this in a later chapter) object == NULL
distNoObjectSpecified The player entered a command with no noun object == NULL

Notice we have seven different cases of 'here', but only one for 'not here'. This is because typically, the game only needs to provide information about things that can be perceived by the player. If it's not here, then there's nothing more to tell.

In the leftmost column, I proposed a symbolic name for each case. We will gather these names in an enum named DISTANCE.

typedef enum { distPlayer, distHeld, distHeldContained, distLocation, distHere, distHereContained, distOverthere, distNotHere, distUnknownObject, distNoObjectSpecified } DISTANCE;

And in the rightmost column, I proposed a condition for each case to satisfy. With a little reshuffling, we can easily turn this into a function that calculates the ‘distance’ of an object (as seen from the player's point of view):

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; }

That's all! We can call this function and do a comparison on its return value, for example:

DISTANCE distance = distanceTo(gold); if (distance <= distHeldContained) { printf("Gold in your possession makes you feel rich.\n"); } else if (distance < distNotHere) { printf("The sight of gold makes you feel greedy.\n"); }

Time to put things into place. The definitions of enum DISTANCE and function distanceTo are added to misc.h and misc.c, since we will be using them in more than one module.

Sample output
Welcome to Little Cave Adventure. You are in an open field. You see: a silver coin a burly guard a cave entrance --> go guard You can't get any closer than this. --> give silver You have no silver. --> ask silver Sorry, a burly guard has no silver. --> get silver OK. --> get gold You don't see any gold here. --> give silver OK. --> go cave OK. You are in a little cave. You see: a gold coin a way out --> get gold OK. --> give gold There is nobody here to give that to. --> quit Bye!
misc.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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);
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 #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->destination == 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; } OBJECT *parseObject(const char *noun) { OBJECT *obj, *found = NULL; for (obj = objs; obj < endOfObjs; obj++) { if (noun != NULL && strcmp(noun, obj->tag) == 0) { found = obj; } } return found; } OBJECT *personHere(void) { OBJECT *obj; for (obj = objs; obj < endOfObjs; obj++) { if (distanceTo(obj) == distHere && obj == guard) { 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("You see:\n"); } printf("%s\n", obj->description); } } return count; }

Explanation:

In function executeGo, we can replace all if conditions by checks on distance. This immediately helps us replace the meaningless “You can't go there” message by two new replies:

location.h
1 2 extern void executeLook(const char *noun); extern void executeGo(const char *noun);
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 #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 { printf("I don't understand what you want to see.\n"); } } 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) { printf("OK.\n"); player->location = obj; executeLook("around"); } else if (distance == distHere && obj->destination != NULL) { printf("OK.\n"); player->location = obj->destination; executeLook("around"); } 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); } }

Module inventory.c had an equally meaningless “You can't” reply; we will split it up into six separate messages. That may sound like a lot; however, these messages cover four different commands (get, drop, give, ask).

inventory.h
1 2 3 4 5 extern void executeGet(const char *noun); extern void executeDrop(const char *noun); extern void executeGive(const char *noun); extern void executeAsk(const char *noun); extern void executeInventory(void);
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 #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 { 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"); } }

Explanation:

The other modules (main.c and object.*) remain unchanged, you can see them in previous chapters.

In this chapter, the concept of distance was used to improve the quality of replies returned by the game. But the benefits of distance are not reserved to the output side; it can be used equally well to make improvements on the input side. In the next chapter, we will use distance to improve the recognition of nouns.


Next chapter: 8. North, east, south, west