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
14. Multiple nouns
15. Light and dark
16. Savegame
17. Test automation
18. Abbreviations
19. Conversations
20. Combat
21. Multi-player
22. Client-server
23. Database
24. Speech
25. JavaScript

How to program a text adventure in C

by Ruud Helderman <r.helderman@hccnet.nl>

Licensed under MIT License

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. So far, tags were globally unique in our game; there were no duplicates. This will radically change.

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

--> get coin
You pick up a silver coin.

--> go west
You can't get much closer than this.

--> go east
OK.
You are in a little cave.
You see:
a gold coin
an exit to the west
solid rock all around

--> go west
OK.
You are in an open field.
You see:
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
an exit to the west
solid rock all around

--> drop coin
You drop a silver coin.

--> get coin
Please be specific about which coin you mean.

--> get gold coin
You pick up a gold coin.

--> quit

Bye!
object.h
  1. typedef struct object {
  2. const char *description;
  3. const char **tags;
  4. struct object *location;
  5. struct object *destination;
  6. } OBJECT;
  7. extern OBJECT objs[];
  8. #define field (objs + 0)
  9. #define cave (objs + 1)
  10. #define silver (objs + 2)
  11. #define gold (objs + 3)
  12. #define guard (objs + 4)
  13. #define player (objs + 5)
  14. #define intoCave (objs + 6)
  15. #define exitCave (objs + 7)
  16. #define wallField (objs + 8)
  17. #define wallCave (objs + 9)
  18. #define endOfObjs (objs + 10)
object.c
  1. #include <stdio.h>
  2. #include "object.h"
  3. static const char *tags0[] = {"field", NULL};
  4. static const char *tags1[] = {"cave", NULL};
  5. static const char *tags2[] = {"silver", "coin", "silver coin", NULL};
  6. static const char *tags3[] = {"gold", "coin", "gold coin", NULL};
  7. static const char *tags4[] = {"guard", "burly guard", NULL};
  8. static const char *tags5[] = {"yourself", NULL};
  9. static const char *tags6[] = {"east", "entrance", NULL};
  10. static const char *tags7[] = {"west", "exit", NULL};
  11. static const char *tags8[] = {"west", "north", "south", "forest", NULL};
  12. static const char *tags9[] = {"east", "north", "south", "rock", NULL};
  13. OBJECT objs[] = {
  14. {"an open field" , tags0, NULL , NULL },
  15. {"a little cave" , tags1, NULL , NULL },
  16. {"a silver coin" , tags2, field, NULL },
  17. {"a gold coin" , tags3, cave , NULL },
  18. {"a burly guard" , tags4, field, NULL },
  19. {"yourself" , tags5, field, NULL },
  20. {"a cave entrance to the east", tags6, field, cave },
  21. {"an exit to the west" , tags7, cave , field },
  22. {"dense forest all around" , tags8, field, NULL },
  23. {"solid rock all around" , tags9, cave , NULL }
  24. };

Explanation:

Of course, for this change to take effect, we also need to adjust function objectHasTag in noun.c.

In the same module, we can also fix problem #2. Partially, this problem was already solved in the previous chapter. The distance check introduced there, already makes it less likely to find more than one matching object. A tag like ‘east’ would always match a passage originating from the current location, and never conflict with eastbound exits in other locations. But the possibility is still there; the silver and gold coin might end up in the same room. So how to choose between them, based on their mutual tag ‘coin’? The answer is, we cannot, and so we should not. So instead of randomly picking either object, we will let functions getVisible and getPossession inform the player he has to be more specific.

noun.h
  1. extern OBJECT *getVisible(const char *intention, const char *noun);
  2. extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
noun.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include "object.h"
  5. #include "misc.h"
  6. static bool objectHasTag(OBJECT *obj, const char *noun)
  7. {
  8. if (noun != NULL && *noun != '\0')
  9. {
  10. const char **tag;
  11. for (tag = obj->tags; *tag != NULL; tag++)
  12. {
  13. if (strcmp(*tag, noun) == 0) return true;
  14. }
  15. }
  16. return false;
  17. }
  18. static OBJECT ambiguousNoun;
  19. static OBJECT *getObject(const char *noun, OBJECT *from, DISTANCE maxDistance)
  20. {
  21. OBJECT *obj, *res = NULL;
  22. for (obj = objs; obj < endOfObjs; obj++)
  23. {
  24. if (objectHasTag(obj, noun) && getDistance(from, obj) <= maxDistance)
  25. {
  26. res = res == NULL ? obj : &ambiguousNoun;
  27. }
  28. }
  29. return res;
  30. }
  31. OBJECT *getVisible(const char *intention, const char *noun)
  32. {
  33. OBJECT *obj = getObject(noun, player, distOverthere);
  34. if (obj == NULL)
  35. {
  36. if (getObject(noun, player, distNotHere) == NULL)
  37. {
  38. printf("I don't understand %s.\n", intention);
  39. }
  40. else
  41. {
  42. printf("You don't see any %s here.\n", noun);
  43. }
  44. }
  45. else if (obj == &ambiguousNoun)
  46. {
  47. printf("Please be specific about which %s you mean.\n", noun);
  48. obj = NULL;
  49. }
  50. return obj;
  51. }
  52. OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
  53. {
  54. OBJECT *obj = NULL;
  55. if (from == NULL)
  56. {
  57. printf("I don't understand who you want to %s.\n", verb);
  58. }
  59. else if ((obj = getObject(noun, from, distHeldContained)) == NULL)
  60. {
  61. if (getObject(noun, player, distNotHere) == NULL)
  62. {
  63. printf("I don't understand what you want to %s.\n", verb);
  64. }
  65. else if (from == player)
  66. {
  67. printf("You are not holding any %s.\n", noun);
  68. }
  69. else
  70. {
  71. printf("There appears to be no %s you can get from %s.\n",
  72. noun, from->description);
  73. }
  74. }
  75. else if (obj == &ambiguousNoun)
  76. {
  77. printf("Please be specific about which %s you want to %s.\n",
  78. noun, verb);
  79. obj = NULL;
  80. }
  81. else if (obj == from)
  82. {
  83. printf("You should not be doing that to %s.\n", obj->description);
  84. obj = NULL;
  85. }
  86. return obj;
  87. }

Explanation:

Problem #3 can be fixed by simply removing a single space character from function parseAndExecute (line 10 below). 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.

parsexec.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include "location.h"
  5. #include "inventory.h"
  6. bool parseAndExecute(char *input)
  7. {
  8. char *verb = strtok(input, " \n");
  9. char *noun = strtok(NULL, "\n");
  10. if (verb != NULL)
  11. {
  12. if (strcmp(verb, "quit") == 0)
  13. {
  14. return false;
  15. }
  16. else if (strcmp(verb, "look") == 0)
  17. {
  18. executeLook(noun);
  19. }
  20. else if (strcmp(verb, "go") == 0)
  21. {
  22. executeGo(noun);
  23. }
  24. else if (strcmp(verb, "get") == 0)
  25. {
  26. executeGet(noun);
  27. }
  28. else if (strcmp(verb, "drop") == 0)
  29. {
  30. executeDrop(noun);
  31. }
  32. else if (strcmp(verb, "give") == 0)
  33. {
  34. executeGive(noun);
  35. }
  36. else if (strcmp(verb, "ask") == 0)
  37. {
  38. executeAsk(noun);
  39. }
  40. else if (strcmp(verb, "inventory") == 0)
  41. {
  42. executeInventory();
  43. }
  44. else
  45. {
  46. printf("I don't know how to '%s'.\n", verb);
  47. }
  48. }
  49. return true;
  50. }

Modules main.c, inventory.c, location.c, move.c and misc.c remain unchanged, you can see them in the previous chapters.

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.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 9. Code generation