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 |
Traditional text adventures use compass directions to navigate.
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.
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:
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