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

13. The parser

Every text adventure has a parser. But there are parsers and parsers. A simple ‘verb-noun’ parser (like the one we have been using ever since chapter 2) could be sufficient for a carefully designed adventure game. However, Infocom has proven that a more advanced parser really helps to make an enjoyable game. It does not have to pass the Turing test; remember, it’s only a game. But the parser should enable the player to express his intentions in a more or less natural way.

Our current parser mainly consists of two lines of code, tucked away in parsexec.c:

char *verb = strtok(input, " \n"); char *noun = strtok(NULL, "\n");

Alright, plus the sequence of strcmp calls to map verbs to commands, and the functions in noun.c to map nouns to objects. But that’s it. This system has served us well for the past 11 chapters, but it has its flaws.

Writing a good parser is no trivial task, but here I will give you a relatively simple approach. We will define a grammar that consists of a list of patterns, similar to (but much simpler than) regular expressions. Examples of patterns:

look around Matches just that; double spaces, leading spaces, trailing spaces and case differences are ignored
go A Matches the word go followed by the tag of an object (see chapters 4 and 8 for an explanation of ‘tags’)
put A in B Matches the word put followed by a tag, the word in, and another tag

To parse the user’s input, we will traverse the list of patterns from top to bottom, trying to match the user’s input with each pattern in turn. We will stop at the first match found. For the sake of simplicity, we will not make use of backtracking, though this could be added later.

Uppercase letters are the nonterminal symbols in our grammar; they match any tag (of any object). Matching is greedy; when the parser has a choice between two different tags (for example ‘silver coin’ and ‘silver’), the longer tag will take precedence.

The matching tag can then be passed as parameter noun to one of the execute functions. For commands with more than one noun (introduced in the next chapter), parameter passing becomes a bit unpractical. For simplicity, I will use a global variable instead of parameters (despite the bad reputation of global variables). The variable will be an array of string pointers.

const char *params[26];

The array has 26 elements; one for each (uppercase) letter in the alphabet. That is sufficient for upto 26 different nonterminals in a single pattern. For every nonterminal in a (matching) pattern, a matching tag will be ‘captured’ by filling the nonterminal’s array element with a pointer to that particular tag. params[0] (the first array element) captures nonterminal ‘A’, params[1] captures ‘B’, and so forth. A simple macro can be used to find the array element that belongs to a given nonterminal.

#define paramByLetter(letter) (params + (letter) - 'A')

Note: you may find the array length of 26 a bit over the top, but it does save me the trouble of writing some bounds checking code to prevent a buffer overflow in case of a malformed pattern.

Now we have to think of a way to handle missing or unrecognized nouns. Suppose the user makes a typo and enters “go kave”. Question is, should this command match the pattern “go A” or not? If we don’t, then the command will fail to match any pattern and end up in a generic error handler, which will probably reply with something like “I don’t know how to go kave”. This takes away every opportunity to improve these ‘negative responses’; a reply “I don’t understand where you want to go” already feels a lot more natural. It would be best to maintain all replies concerning command go inside function executeGo.

There are a few ways to achieve this, but the easiest one is to allow nonterminals to match anything; so not just a valid tag, but also total gibberish, blanks or just nothing. This ‘invalid’ input will be captured as an empty string (“”).

Having such a ‘loose’ nonterminal in the middle of a pattern does complicate the pattern matching process; it would require backtracking to properly align the word ‘in’ when matching input “put foo in in box” with pattern “put A in B”.

To keep things simple, we will only enable this loose matching for nonterminals that occur at the very end of a pattern. To be able to properly match the example above, we need two separate patterns:

The order of the patterns is vital here: if “put A” would be on top (meaning it will be tried first), then it would consume every put command; a valid “put coin in box” would be considered invalid because there is no tag “coin in box.”

The same goes for commands that appear in multiple forms, such as look.

The first two patterns can be in any order, but the third one must come last.

Time to put this into action. We will discard the existing contents of module parsexec.c, and replace it by a new implementation of function parseAndExecute using a list of patterns that should be able to match every command we have implemented so far. Each pattern is tied to a function that executes the appropriate command.

parsexec.h
  1. extern bool parseAndExecute(const char *input);
parsexec.c
  1. #include <ctype.h>
  2. #include <stdbool.h>
  3. #include <stdio.h>
  4. #include "object.h"
  5. #include "misc.h"
  6. #include "match.h"
  7. #include "location.h"
  8. #include "inventory.h"
  9. #include "openclose.h"
  10. typedef struct
  11. {
  12. const char *pattern;
  13. bool (*function)(void);
  14. } COMMAND;
  15. static bool executeQuit(void)
  16. {
  17. return false;
  18. }
  19. static bool executeNoMatch(void)
  20. {
  21. const char *src = *params;
  22. int len;
  23. for (len = 0; src[len] != '\0' && !isspace(src[len]); len++);
  24. if (len > 0) printf("I don't know how to '%.*s'.\n", len, src);
  25. return true;
  26. }
  27. bool parseAndExecute(const char *input)
  28. {
  29. static const COMMAND commands[] =
  30. {
  31. { "quit" , executeQuit },
  32. { "look" , executeLookAround },
  33. { "look around" , executeLookAround },
  34. { "look at A" , executeLook },
  35. { "look A" , executeLook },
  36. { "examine A" , executeLook },
  37. { "go to A" , executeGo },
  38. { "go A" , executeGo },
  39. { "get A" , executeGet },
  40. { "drop A" , executeDrop },
  41. { "ask A" , executeAsk },
  42. { "give A" , executeGive },
  43. { "inventory" , executeInventory },
  44. { "open A" , executeOpen },
  45. { "close A" , executeClose },
  46. { "lock A" , executeLock },
  47. { "unlock A" , executeUnlock },
  48. { "A" , executeNoMatch }
  49. };
  50. const COMMAND *cmd;
  51. for (cmd = commands; !matchCommand(input, cmd->pattern); cmd++);
  52. return (*cmd->function)();
  53. }

Explanation:

The hardest part is the implementation of function matchCommand. But as you can see below, that too can be done in less than 100 lines of code.

match.h
  1. #define MAX_PARAMS 26
  2. extern const char *params[];
  3. #define paramByLetter(letter) (params + (letter) - 'A')
  4. extern bool matchCommand(const char *src, const char *pattern);
match.c
  1. #include <ctype.h>
  2. #include <stdbool.h>
  3. #include <string.h>
  4. #include "object.h"
  5. #include "misc.h"
  6. #include "match.h"
  7. const char *params[MAX_PARAMS];
  8. static const char *skipSpaces(const char *src)
  9. {
  10. while (isspace(*src)) src++;
  11. return src;
  12. }
  13. static const char *matchSpaces(const char *src)
  14. {
  15. return *src == '\0' || isspace(*src) ? skipSpaces(src) : NULL;
  16. }
  17. static const char *matchTerminal(const char *src, char terminal)
  18. {
  19. return terminal == ' ' ? matchSpaces(src) :
  20. tolower(*src) == tolower(terminal) ? src + 1 : NULL;
  21. }
  22. static const char *matchTag(const char *src, const char *tag, bool atEnd)
  23. {
  24. while (src != NULL && *tag != '\0')
  25. {
  26. src = matchTerminal(src, *tag++);
  27. }
  28. return atEnd && src != NULL && *skipSpaces(src) != '\0' ? NULL : src;
  29. }
  30. static const char *matchParam(const char *src, const char **par, bool loose)
  31. {
  32. const char *restOfSrc = loose ? src + strlen(src) : NULL;
  33. OBJECT *obj;
  34. for (obj = objs; obj < endOfObjs; obj++)
  35. {
  36. const char **tag;
  37. for (tag = obj->tags; *tag != NULL; tag++)
  38. {
  39. const char *behindMatch = matchTag(src, *tag, loose);
  40. if (behindMatch != NULL && strlen(*tag) > strlen(*par))
  41. {
  42. *par = *tag;
  43. restOfSrc = behindMatch;
  44. }
  45. }
  46. }
  47. if (**par == '\0')
  48. {
  49. *par = src;
  50. }
  51. return restOfSrc;
  52. }
  53. bool matchCommand(const char *src, const char *pattern)
  54. {
  55. const char **par;
  56. for (par = params; par < params + MAX_PARAMS; par++)
  57. {
  58. *par = "";
  59. }
  60. for (src = skipSpaces(src); src != NULL && *pattern != '\0'; pattern++)
  61. {
  62. src = isupper(*pattern)
  63. ? matchParam(src, paramByLetter(*pattern), pattern[1] == '\0')
  64. : matchTerminal(src, *pattern);
  65. }
  66. return src != NULL && *skipSpaces(src) == '\0';
  67. }

Explanation:

We adjust the implementations of the various commands to make use of the new array params.

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.

--> give coin
You give a silver coin to a burly guard.

--> go cave
You walk into the cave.

You are in a little cave.
You see:
an exit to the west
solid rock all around
a closed door to the south
a tiny key

--> get key
You pick up a tiny key.

--> go south
The door is closed.

--> open door
You open a closed door to the south.

--> go south
You walk through the door into a backroom.

You are in a backroom.
You see:
solid rock all around
an open door to the north
a wooden box

--> unlock box
You unlock a wooden box.

--> open box
You open a wooden box.

--> examine box
The box is open.
You see:
a gold coin

--> get gold
You get a gold coin from a wooden box.

--> quit

Bye!
inventory.h
  1. extern bool executeGet(void);
  2. extern bool executeDrop(void);
  3. extern bool executeAsk(void);
  4. extern bool executeGive(void);
  5. extern bool executeInventory(void);
inventory.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "misc.h"
  5. #include "match.h"
  6. #include "noun.h"
  7. #include "move.h"
  8. bool executeGet(void)
  9. {
  10. OBJECT *obj = getVisible("what you want to get", params[0]);
  11. switch (getDistance(player, obj))
  12. {
  13. case distSelf:
  14. printf("You should not be doing that to yourself.\n");
  15. break;
  16. case distHeld:
  17. printf("You already have %s.\n", obj->description);
  18. break;
  19. case distOverthere:
  20. printf("Too far away, move closer please.\n");
  21. break;
  22. case distUnknownObject:
  23. // already handled by getVisible
  24. break;
  25. default:
  26. if (obj->location != NULL && obj->location->health > 0)
  27. {
  28. printf("You should ask %s nicely.\n", obj->location->description);
  29. }
  30. else
  31. {
  32. moveObject(obj, player);
  33. }
  34. }
  35. return true;
  36. }
  37. bool executeDrop(void)
  38. {
  39. moveObject(getPossession(player, "drop", params[0]), player->location);
  40. return true;
  41. }
  42. bool executeAsk(void)
  43. {
  44. moveObject(getPossession(actorHere(), "ask", params[0]), player);
  45. return true;
  46. }
  47. bool executeGive(void)
  48. {
  49. moveObject(getPossession(player, "give", params[0]), actorHere());
  50. return true;
  51. }
  52. bool executeInventory(void)
  53. {
  54. if (listObjectsAtLocation(player) == 0)
  55. {
  56. printf("You are empty-handed.\n");
  57. }
  58. return true;
  59. }

Same thing for the module we added in the previous chapter.

openclose.h
  1. extern bool executeOpen(void);
  2. extern bool executeClose(void);
  3. extern bool executeLock(void);
  4. extern bool executeUnlock(void);
openclose.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "match.h"
  5. #include "reach.h"
  6. bool executeOpen(void)
  7. {
  8. OBJECT *obj = reachableObject("what you want to open", params[0]);
  9. if (obj != NULL) (*obj->open)();
  10. return true;
  11. }
  12. bool executeClose(void)
  13. {
  14. OBJECT *obj = reachableObject("what you want to close", params[0]);
  15. if (obj != NULL) (*obj->close)();
  16. return true;
  17. }
  18. bool executeLock(void)
  19. {
  20. OBJECT *obj = reachableObject("what you want to lock", params[0]);
  21. if (obj != NULL) (*obj->lock)();
  22. return true;
  23. }
  24. bool executeUnlock(void)
  25. {
  26. OBJECT *obj = reachableObject("what you want to unlock", params[0]);
  27. if (obj != NULL) (*obj->unlock)();
  28. return true;
  29. }

In location.c, the command look around is given its own function, separate from the look command to inspect specific objects (see lines 7-12).

location.h
  1. extern bool executeLookAround(void);
  2. extern bool executeLook(void);
  3. extern bool executeGo(void);
location.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "misc.h"
  5. #include "match.h"
  6. #include "noun.h"
  7. bool executeLookAround(void)
  8. {
  9. printf("You are in %s.\n", player->location->description);
  10. listObjectsAtLocation(player->location);
  11. return true;
  12. }
  13. bool executeLook(void)
  14. {
  15. OBJECT *obj = getVisible("what you want to look at", params[0]);
  16. switch (getDistance(player, obj))
  17. {
  18. case distHereContained:
  19. printf("Hard to see, try to get it first.\n");
  20. break;
  21. case distOverthere:
  22. printf("Too far away, move closer please.\n");
  23. break;
  24. case distNotHere:
  25. printf("You don't see any %s here.\n", params[0]);
  26. break;
  27. case distUnknownObject:
  28. // already handled by getVisible
  29. break;
  30. default:
  31. printf("%s\n", obj->details);
  32. listObjectsAtLocation(obj);
  33. }
  34. return true;
  35. }
  36. static void movePlayer(OBJECT *passage)
  37. {
  38. printf("%s\n", passage->textGo);
  39. if (passage->destination != NULL)
  40. {
  41. player->location = passage->destination;
  42. printf("\n");
  43. executeLookAround();
  44. }
  45. }
  46. bool executeGo(void)
  47. {
  48. OBJECT *obj = getVisible("where you want to go", params[0]);
  49. switch (getDistance(player, obj))
  50. {
  51. case distOverthere:
  52. movePlayer(getPassage(player->location, obj));
  53. break;
  54. case distNotHere:
  55. printf("You don't see any %s here.\n", params[0]);
  56. break;
  57. case distUnknownObject:
  58. // already handled by getVisible
  59. break;
  60. default:
  61. movePlayer(obj);
  62. }
  63. return true;
  64. }

Our game still accepts simple verb-noun commands only, but the new parser does have the potential to accept commands with more than one noun like ‘put coin in box’; this will be demonstrated in the next chapter.

The new parser is a huge improvement over the original one, but by today’s standards, it is still far from perfect. For example, there is no structural way to manipulate multiple objects with a single command (‘get coin, key and lamp’) or execute two or more commands in a row (‘get key then go east’).

In the true sense of the word, my parser is not even a parser. It is just a simple pattern matcher. IMHO, it does its job sufficiently well for a simple adventure game. Some of its flaws can be mitigated by adding more patterns. But eventually, you will run into its limitations, and you may want to move on to something more mature. In that case, I would recommend a decent parser generator (e.g. Yacc). Please be aware that this is not an easy thing to do. For now, this is outside the scope of this tutorial.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 14. Multiple nouns