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

14. Multiple nouns

Time to prove the new parser is really capable of interpreting more complex commands.

So far, our parser only accepted simple verb-noun commands. Let’s try and parse some more complex commands like:

Adding patterns to parsexec.c seems straightforward:

But as explained in the previous chapter, similar commands (like ‘get A from B’ and the already existing ‘get A’) must be placed in the correct order. If ‘get A’ would appear first, then it would consume any command starting with ‘get’, including all those that were supposed to be matched by the new pattern. In short, ‘get A from B’ must come before ‘get A’.

Some commands (for example put) do not have a single-noun variant. Nevertheless, we will add a pattern with a single nonterminal (‘put A’). We need this to correctly capture a misspelled noun. The full pattern ‘put A in B’ will not match a command like ‘put koin in box’. As explained in the previous chapter, only a nonterminal at the end of a pattern is able to capture a misspelled or otherwise unrecognized noun.

The same applies to commands with more than two nouns. With n nouns, you need n patterns to handle all possible mismatches. An example with three parameters:

  1. “paint A on B with C”
  2. “paint A on B”
  3. “paint A”

Again, the order of the patterns is vital: if “paint A” would be on top (meaning it will be tried first), then it would consume every paint command, including those that were meant to be caught by 1 and 2.

For now, we do not need patterns with more than two nonterminals.

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 to guard
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 from box
You get a gold coin from a wooden box.

--> inventory
You have:
a gold coin
a tiny key

--> put gold in box
You put a gold coin in a wooden box.

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

--> quit

Bye!
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 "inventory2.h"
  10. #include "openclose.h"
  11. typedef struct
  12. {
  13. const char *pattern;
  14. bool (*function)(void);
  15. } COMMAND;
  16. static bool executeQuit(void)
  17. {
  18. return false;
  19. }
  20. static bool executeNoMatch(void)
  21. {
  22. const char *src = *params;
  23. int len;
  24. for (len = 0; src[len] != '\0' && !isspace(src[len]); len++);
  25. if (len > 0) printf("I don't know how to '%.*s'.\n", len, src);
  26. return true;
  27. }
  28. bool parseAndExecute(const char *input)
  29. {
  30. static const COMMAND commands[] =
  31. {
  32. { "quit" , executeQuit },
  33. { "look" , executeLookAround },
  34. { "look around" , executeLookAround },
  35. { "look at A" , executeLook },
  36. { "look A" , executeLook },
  37. { "examine A" , executeLook },
  38. { "go to A" , executeGo },
  39. { "go A" , executeGo },
  40. { "get A from B" , executeGetFrom },
  41. { "get A" , executeGet },
  42. { "put A in B" , executePutIn },
  43. { "drop A in B" , executePutIn },
  44. { "drop A" , executeDrop },
  45. { "ask A from B" , executeAskFrom },
  46. { "ask A" , executeAsk },
  47. { "give A to B" , executeGiveTo },
  48. { "give A" , executeGive },
  49. { "inventory" , executeInventory },
  50. { "open A" , executeOpen },
  51. { "close A" , executeClose },
  52. { "lock A" , executeLock },
  53. { "unlock A" , executeUnlock },
  54. { "A" , executeNoMatch }
  55. };
  56. const COMMAND *cmd;
  57. for (cmd = commands; !matchCommand(input, cmd->pattern); cmd++);
  58. return (*cmd->function)();
  59. }

In a new module inventory2.c, we implement the new multi-noun commands get, drop/put, ask and give. Now we can finally put the gold coin back in the box!

inventory2.h
  1. extern bool executeGetFrom(void);
  2. extern bool executePutIn(void);
  3. extern bool executeAskFrom(void);
  4. extern bool executeGiveTo(void);
inventory2.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "match.h"
  5. #include "noun.h"
  6. #include "move.h"
  7. #include "reach.h"
  8. bool executeGetFrom(void)
  9. {
  10. OBJECT *from = reachableObject("where to get that from", params[1]);
  11. if (from != NULL && getVisible("what you want to get", params[0]) != NULL)
  12. {
  13. if (from->health > 0)
  14. {
  15. printf("You should ask %s nicely.\n", from->description);
  16. }
  17. else
  18. {
  19. moveObject(getPossession(from, "get", params[0]), player);
  20. }
  21. }
  22. return true;
  23. }
  24. bool executePutIn(void)
  25. {
  26. OBJECT *obj = getPossession(player, "put", params[0]);
  27. if (obj != NULL)
  28. {
  29. OBJECT *to = reachableObject("where to put that in", params[1]);
  30. if (to != NULL)
  31. {
  32. if (to->health > 0)
  33. {
  34. printf("You should offer that nicely to %s.\n", to->description);
  35. }
  36. else
  37. {
  38. moveObject(obj, to);
  39. }
  40. }
  41. }
  42. return true;
  43. }
  44. bool executeAskFrom(void)
  45. {
  46. OBJECT *from = reachableObject("who to ask that", params[1]);
  47. if (from != NULL)
  48. {
  49. if (from->health > 0)
  50. {
  51. if (getVisible("what you want to ask", params[0]) != NULL)
  52. {
  53. moveObject(getPossession(from, "ask", params[0]), player);
  54. }
  55. }
  56. else
  57. {
  58. printf("There is no response from %s.\n", from->description);
  59. }
  60. }
  61. return true;
  62. }
  63. bool executeGiveTo(void)
  64. {
  65. OBJECT *obj = getPossession(player, "give", params[0]);
  66. if (obj != NULL)
  67. {
  68. OBJECT *to = reachableObject("who to give that to", params[1]);
  69. if (to != NULL)
  70. {
  71. if (to->health > 0)
  72. {
  73. moveObject(obj, to);
  74. }
  75. else
  76. {
  77. printf("No eagerness is shown by %s.\n", to->description);
  78. }
  79. }
  80. }
  81. return true;
  82. }

You may notice that commands like ‘put coin in forest’ and ‘put coin in box’ (when the box is closed) get the following strange reply: “That is way too heavy.” That is because most objects (including the closed box, and scenary like the forest) have zero capacity to hold other objects. Rightfully so, but the message is quite inappropriate. To avoid that, we will introduce a separate message for objects that have zero capacity: “It doesn’t seem to fit in.”

However, that particular message is totally misleading when it comes as a response to ‘put key in box’. So we will make a special exception for that particular combination of objects.

move.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "misc.h"
  5. static int weightOfContents(OBJECT *container)
  6. {
  7. int sum = 0;
  8. OBJECT *obj;
  9. for (obj = objs; obj < endOfObjs; obj++)
  10. {
  11. if (isHolding(container, obj)) sum += obj->weight;
  12. }
  13. return sum;
  14. }
  15. static void describeMove(OBJECT *obj, OBJECT *to)
  16. {
  17. if (to == player->location)
  18. {
  19. printf("You drop %s.\n", obj->description);
  20. }
  21. else if (to != player)
  22. {
  23. printf(to->health > 0 ? "You give %s to %s.\n" : "You put %s in %s.\n",
  24. obj->description, to->description);
  25. }
  26. else if (obj->location == player->location)
  27. {
  28. printf("You pick up %s.\n", obj->description);
  29. }
  30. else
  31. {
  32. printf("You get %s from %s.\n",
  33. obj->description, obj->location->description);
  34. }
  35. }
  36. void moveObject(OBJECT *obj, OBJECT *to)
  37. {
  38. if (obj == NULL)
  39. {
  40. // already handled by getVisible or getPossession
  41. }
  42. else if (to == NULL)
  43. {
  44. printf("There is nobody here to give that to.\n");
  45. }
  46. else if (to->capacity == 0)
  47. {
  48. printf(obj == keyForBox && (to == closedBox || to == lockedBox) ?
  49. "The key seems to fit the lock.\n" :
  50. "It doesn't seem to fit in.\n");
  51. }
  52. else if (obj->weight > to->capacity)
  53. {
  54. printf("That is way too heavy.\n");
  55. }
  56. else if (obj->weight + weightOfContents(to) > to->capacity)
  57. {
  58. printf("That would become too heavy.\n");
  59. }
  60. else
  61. {
  62. describeMove(obj, to);
  63. obj->location = to;
  64. }
  65. }

I guess the more complex the commands we accept, the more effort we need to put into making convincing replies.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 15. Light and dark