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

8. North, east, south, west

Traditional text adventures use compass directions to navigate.

Basic map with compass directions

For example, on the map I drew in chapter 6, the player might want to enter go east to move from the field to the cave. We can implement this by giving passage intoCave the tag "east". However, there are two problems that we need to solve first.

  1. We may still want to refer to the passage as "entrance" as well as "east". But right now, an object can have one tag only.
  2. On a bigger map, with more locations and passages, the tag "east" will be defined many times. Currently, our parser cannot handle this: tags are assumed to be unique (see function parseObject in module misc.c).

These problems apply to other objects as well, not just passages. In our adventure, we have a silver coin and a gold coin. On the one hand, it would be silly not to accept get coin in a location where only one of the coins is present. On the other hand, it should be possible to use get silver coin instead in case both coins are present at the same location.

This immediately brings us to a third problem with our parser:

  1. A tag can only be a single word; "silver coin" would never be recognized.

All three problems will be solved in this chapter, starting with problem #1. It is resolved by giving each object a list of tags, instead of just a single tag.

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 west You can't get any closer than this. --> go east OK. You are in a little cave. You see: a gold coin a way out to the west solid rock all around --> go west OK. 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 OK. You are in a little cave. You see: a gold coin a way out to the west solid rock all around --> quit Bye!
object.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct object { const char *description; const char **tags; struct object *location; struct object *destination; } OBJECT; extern OBJECT objs[]; #define field (objs + 0) #define cave (objs + 1) #define silver (objs + 2) #define gold (objs + 3) #define guard (objs + 4) #define player (objs + 5) #define intoCave (objs + 6) #define exitCave (objs + 7) #define wallField (objs + 8) #define wallCave (objs + 9) #define endOfObjs (objs + 10)
object.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 #include <stdio.h> #include "object.h" static const char *tags0[] = {"field", NULL}; static const char *tags1[] = {"cave", NULL}; static const char *tags2[] = {"silver", "coin", "silver coin", NULL}; static const char *tags3[] = {"gold", "coin", "gold coin", NULL}; static const char *tags4[] = {"guard", "burly guard", NULL}; static const char *tags5[] = {"yourself", NULL}; static const char *tags6[] = {"east", "entrance", NULL}; static const char *tags7[] = {"west", "out", NULL}; static const char *tags8[] = {"west", "north", "south", "forest", NULL}; static const char *tags9[] = {"east", "north", "south", "rock", NULL}; OBJECT objs[] = { {"an open field" , tags0, NULL , NULL }, {"a little cave" , tags1, NULL , NULL }, {"a silver coin" , tags2, field, NULL }, {"a gold coin" , tags3, cave , NULL }, {"a burly guard" , tags4, field, NULL }, {"yourself" , tags5, field, NULL }, {"a cave entrance to the east", tags6, field, cave }, {"a way out to the west" , tags7, cave , field }, {"dense forest all around" , tags8, field, NULL }, {"solid rock all around" , tags9, cave , NULL } };

Explanation:

Of course, for this change to take effect, we also need to adjust function parseObject. In the same function, we can also fix problem #2: by giving precedence to objects that are located near the player. The function distanceTo, introduced in the previous chapter, really pays off now!

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 #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; } 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 == 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:

Problem #3 can be fixed by simply removing a single space character from function parseAndExecute. This solution is far from perfect (a double space between ‘silver’ and ‘coin’ is not accepted), but it will do until we make ourselves a better parser in chapter 13.

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 #include <stdio.h> #include <string.h> #include "location.h" #include "inventory.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 { 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; }

Modules location.* and inventory.* remain unchanged, you can see them in the previous chapter.

Now that the array of objects (object.c) starts to grow in multiple dimensions (in particular with the introduction of multiple tags), we need a way to make it more maintainable. We will do so in the next chapter.


Next chapter: 9. Code generation