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

19. Conversations

One of the most difficult parts of writing an adventure is to come up with challenging puzzles. Puzzles must be difficult, but without being illogical. If your game demands the player to unlock a door with a towel, fine. But then at least give the player a clue. It’s no fun brute-forcing your way through a game, having to try every combination of object(s) and verb until you find something that matches the author’s obscure sense of logic.

Clues can be hidden in descriptive text. This text may come out unannounced, for example as you enter a location (“A hollow voice says PLUGH”) or run into an actor. But it also makes sense to let the player actively search for clues by examining objects and talking to non-player characters. Obvious advantages are:

Conversation with non-player characters can add a lot of depth to a game. Clues given may help you or hinder you. Some examples:

Conversations may go a lot further than just asking for information. Some suggestions:

Of course, these more advanced forms of interacting with actors demand a pretty sophisticated conversation engine. A recommended read is Dynamic Conversation Engine Concept by Luke Bergeron. I found this to be incredibly inspiring. The article has been removed from Scribd, but fortunately the Wayback Machine still has a copy:
https://web.archive.org/web/20100409073709/http://www.scribd.com/doc/17682546/Dynamic-Conversation-Engine-Concept

Talk

In our little adventure game, we will try to keep it simple. We will implement a very straightforward ‘talk’ verb. It allows the player to talk with an actor about some topic. The topic of discussion can be any object: an item, a location, even another actor. In its most elaborate form, you could give every actor their own opinion about certain topics. That requires a matrix of possible reactions; one for every combination of actor and object.

Tour guide Guard
Cave “A unique experience for you to see!” “Sorry, we’re closed.”
Silver coin “You found a coin? That brings good luck!” “Maybe we can arrange something for you...”
Gold coin “Rumor has it there’s treasure in the caves.” “You will have to find a key first...”
Tour guide “Sorry, I just can’t stop talking!” “Too slick, if you ask me.”
Guard “When the price is right, he’s your man!” “I’m not in the mood to talk about myself.”

In a big adventure, such a matrix could be huge, making it extremely time-consuming for the author to come up with a witty response on every attempt from the player to engage in conversation. Furthermore, a huge number of combinations could easily turn into just another brute-force puzzle, where the player feels forced to interrogate every actor about every possible topic.

To keep it maintainable, you may want to cut down on the number of combinations. Here are a few ways to do so.

TODO: properties 'loyalty' and 'confidentiality'. Only spill gossip if object's confidentiality < actor's loyalty.

To keep things simple, we will stick to a single response per object, given by any actor the player will talk to about that object.

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
a lamp

--> talk with guard
I don't understand what you want to talk about.

--> talk about coin
You hear a burly guard say: 'Money makes the world go round...'

--> talk with guard about forest
You hear a burly guard say: 'You cannot go there, it is impenetrable.'

--> talk about cave
You hear a burly guard say: 'It's dark in there; bring a lamp!'

--> talk about east
You hear a burly guard say: 'It's just a compass direction.'

--> talk about yourself
You hear a burly guard say: 'You're not from around here, are you?'

--> talk about guard
You hear a burly guard say: 'I don't want to talk about myself.'

--> talk about hamburgers
I don't understand what you want to talk about.

--> talk
I don't understand what you want to talk about.

--> quit

Bye!
object.txt
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "toggle.h"
  5. typedef struct object {
  6. bool (*condition)(void);
  7. const char *description;
  8. const char **tags;
  9. struct object *location;
  10. struct object *destination;
  11. struct object *prospect;
  12. const char *details;
  13. const char *contents;
  14. const char *textGo;
  15. const char *gossip;
  16. int weight;
  17. int capacity;
  18. int health;
  19. int light;
  20. void (*open)(void);
  21. void (*close)(void);
  22. void (*lock)(void);
  23. void (*unlock)(void);
  24. } OBJECT;
  25. extern OBJECT objs[];
  26. - gossipEWNS
  27. tags "east", "west", "north", "south"
  28. gossip "It's just a compass direction."
  29. - field
  30. description "an open field"
  31. tags "field"
  32. details "The field is a nice and quiet place under a clear blue sky."
  33. gossip "A lot of tourists go there."
  34. capacity 9999
  35. light 100
  36. - cave
  37. description "a little cave"
  38. tags "cave"
  39. details "The cave is just a cold, damp, rocky chamber."
  40. gossip "It's dark in there; bring a lamp!"
  41. capacity 9999
  42. - silver
  43. description "a silver coin"
  44. tags "silver", "coin", "silver coin"
  45. location field
  46. details "The coin has an eagle on the obverse."
  47. gossip "Money makes the world go round..."
  48. weight 1
  49. - gold
  50. description "a gold coin"
  51. tags "gold", "coin", "gold coin"
  52. location openBox
  53. details "The shiny coin seems to be a rare and priceless artefact."
  54. gossip "Money makes the world go round..."
  55. weight 1
  56. - guard
  57. description "a burly guard"
  58. tags "guard", "burly guard"
  59. location field
  60. details "The guard is a really big fellow."
  61. gossip "Easy to bribe..."
  62. contents "He has"
  63. health 100
  64. capacity 20
  65. - player
  66. description "yourself"
  67. tags "yourself"
  68. location field
  69. details "You would need a mirror to look at yourself."
  70. gossip "You're not from around here, are you?"
  71. contents "You have"
  72. health 100
  73. capacity 20
  74. - intoCave
  75. condition { return guard->health == 0 || silver->location == guard; }
  76. description "a cave entrance to the east"
  77. tags "east", "entrance"
  78. location field
  79. destination cave
  80. details "The entrance is just a narrow opening in a small outcrop."
  81. textGo "You walk into the cave."
  82. open isAlreadyOpen
  83. - intoCaveBlocked
  84. condition { return guard->health > 0 && silver->location != guard; }
  85. description "a cave entrance to the east"
  86. tags "east", "entrance"
  87. location field
  88. prospect cave
  89. details "The entrance is just a narrow opening in a small outcrop."
  90. textGo "The guard stops you from walking into the cave."
  91. open isAlreadyOpen
  92. - exitCave
  93. description "an exit to the west"
  94. tags "west", "exit"
  95. location cave
  96. destination field
  97. details "Sunlight pours in through an opening in the cave's wall."
  98. textGo "You walk out of the cave."
  99. open isAlreadyOpen
  100. - wallField
  101. description "dense forest all around"
  102. tags "west", "north", "south", "forest"
  103. location field
  104. details "The field is surrounded by trees and undergrowth."
  105. textGo "Dense forest is blocking the way."
  106. gossip "You cannot go there, it is impenetrable."
  107. - wallCave
  108. description "solid rock all around"
  109. tags "east", "north", "rock"
  110. location cave
  111. details "Carved in stone is a secret password 'abccb'."
  112. textGo "Solid rock is blocking the way."
  113. - backroom
  114. description "a backroom"
  115. tags "backroom"
  116. details "The room is dusty and messy."
  117. gossip "There is something of value to be found there."
  118. capacity 9999
  119. - wallBackroom
  120. description "solid rock all around"
  121. tags "east", "west", "south", "rock"
  122. location backroom
  123. details "Trendy wallpaper covers the rock walls."
  124. textGo "Solid rock is blocking the way."
  125. - openDoorToBackroom
  126. description "an open door to the south"
  127. tags "south", "door", "doorway"
  128. destination backroom
  129. details "The door is open."
  130. textGo "You walk through the door into a backroom."
  131. open isAlreadyOpen
  132. close toggleDoorToBackroom
  133. - closedDoorToBackroom
  134. description "a closed door to the south"
  135. tags "south", "door", "doorway"
  136. location cave
  137. prospect backroom
  138. details "The door is closed."
  139. textGo "The door is closed."
  140. open toggleDoorToBackroom
  141. close isAlreadyClosed
  142. - openDoorToCave
  143. description "an open door to the north"
  144. tags "north", "door", "doorway"
  145. destination cave
  146. details "The door is open."
  147. textGo "You walk through the door into the cave."
  148. open isAlreadyOpen
  149. close toggleDoorToCave
  150. - closedDoorToCave
  151. description "a closed door to the north"
  152. tags "north", "door", "doorway"
  153. location backroom
  154. prospect cave
  155. details "The door is closed."
  156. textGo "The door is closed."
  157. open toggleDoorToCave
  158. close isAlreadyClosed
  159. - openBox
  160. description "a wooden box"
  161. tags "box", "wooden box"
  162. details "The box is open."
  163. gossip "You need a key to open it."
  164. weight 5
  165. capacity 10
  166. open isAlreadyOpen
  167. close toggleBox
  168. lock isStillOpen
  169. unlock isAlreadyOpen
  170. - closedBox
  171. description "a wooden box"
  172. tags "box", "wooden box"
  173. details "The box is closed."
  174. weight 5
  175. open toggleBox
  176. close isAlreadyClosed
  177. lock toggleBoxLock
  178. unlock isAlreadyUnlocked
  179. - lockedBox
  180. description "a wooden box"
  181. tags "box", "wooden box"
  182. location backroom
  183. details "The box is closed."
  184. weight 5
  185. open isStillLocked
  186. close isAlreadyClosed
  187. lock isAlreadyLocked
  188. unlock toggleBoxLock
  189. - keyForBox
  190. description "a tiny key"
  191. tags "key", "tiny key"
  192. location cave
  193. details "The key is really small and shiny."
  194. gossip "A small key opens a small lock."
  195. weight 1
  196. - lampOff
  197. description "a lamp"
  198. tags "lamp"
  199. location field
  200. details "The lamp is off."
  201. gossip "Essential in dark areas."
  202. weight 5
  203. - lampOn
  204. description "a lamp"
  205. tags "lamp"
  206. details "The lamp is on."
  207. weight 5
  208. light 100

Explanation:

As always, we also need to make object.awk aware of the new property, and specify a default value.

object.awk
  1. BEGIN {
  2. count = 0;
  3. obj = "";
  4. if (pass == "c2") {
  5. print "\nstatic bool alwaysTrue(void) { return true; }";
  6. print "\nOBJECT objs[] = {";
  7. }
  8. }
  9. /^- / {
  10. outputRecord(",");
  11. obj = $2;
  12. prop["condition"] = "alwaysTrue";
  13. prop["description"] = "NULL";
  14. prop["tags"] = "";
  15. prop["location"] = "NULL";
  16. prop["destination"] = "NULL";
  17. prop["prospect"] = "";
  18. prop["details"] = "\"You see nothing special.\"";
  19. prop["contents"] = "\"You see\"";
  20. prop["textGo"] = "\"You can't get much closer than this.\"";
  21. prop["gossip"] = "\"I know nothing about that.\"";
  22. prop["weight"] = "99";
  23. prop["capacity"] = "0";
  24. prop["health"] = "0";
  25. prop["light"] = "0";
  26. prop["open"] = "cannotBeOpened";
  27. prop["close"] = "cannotBeClosed";
  28. prop["lock"] = "cannotBeLocked";
  29. prop["unlock"] = "cannotBeUnlocked";
  30. }
  31. obj && /^[ \t]+[a-z]/ {
  32. name = $1;
  33. $1 = "";
  34. if (name in prop) {
  35. prop[name] = $0;
  36. if (/^[ \t]*\{/) {
  37. prop[name] = name count;
  38. if (pass == "c1") print "static bool " prop[name] "(void) " $0;
  39. }
  40. }
  41. else if (pass == "c2") {
  42. print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\"";
  43. }
  44. }
  45. !obj && pass == (/^#include/ ? "c1" : "h") {
  46. print;
  47. }
  48. END {
  49. outputRecord("\n};");
  50. if (pass == "h") {
  51. print "\n#define endOfObjs\t(objs + " count ")";
  52. print "\n#define validObject(obj)\t" \
  53. "((obj) != NULL && (*(obj)->condition)())";
  54. }
  55. }
  56. function outputRecord(separator)
  57. {
  58. if (obj) {
  59. if (pass == "h") {
  60. print "#define " obj "\t(objs + " count ")";
  61. }
  62. else if (pass == "c1") {
  63. print "static const char *tags" count "[] = {" prop["tags"] ", NULL};";
  64. }
  65. else if (pass == "c2") {
  66. print "\t{\t/* " count " = " obj " */";
  67. print "\t\t" prop["condition"] ",";
  68. print "\t\t" prop["description"] ",";
  69. print "\t\ttags" count ",";
  70. print "\t\t" prop["location"] ",";
  71. print "\t\t" prop["destination"] ",";
  72. print "\t\t" prop[prop["prospect"] ? "prospect" : "destination"] ",";
  73. print "\t\t" prop["details"] ",";
  74. print "\t\t" prop["contents"] ",";
  75. print "\t\t" prop["textGo"] ",";
  76. print "\t\t" prop["gossip"] ",";
  77. print "\t\t" prop["weight"] ",";
  78. print "\t\t" prop["capacity"] ",";
  79. print "\t\t" prop["health"] ",";
  80. print "\t\t" prop["light"] ",";
  81. print "\t\t" prop["open"] ",";
  82. print "\t\t" prop["close"] ",";
  83. print "\t\t" prop["lock"] ",";
  84. print "\t\t" prop["unlock"];
  85. print "\t}" separator;
  86. delete prop;
  87. }
  88. count++;
  89. }
  90. }

Adding a new command ‘talk’ to the parser:

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

Explanation:

As many times before, we introduce a new module to implement the new command:

talk.h
  1. extern bool executeTalk(void);
  2. extern bool executeTalkTo(void);
talk.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 "reach.h"
  8. static void talk(const char *about, OBJECT *to)
  9. {
  10. OBJECT *topic = getTopic(about);
  11. if (topic == NULL)
  12. {
  13. printf("I don't understand what you want to talk about.\n");
  14. }
  15. else
  16. {
  17. printf("You hear %s say: '%s'\n",
  18. to->description,
  19. topic == to ? "I don't want to talk about myself."
  20. : topic->gossip);
  21. }
  22. }
  23. bool executeTalk(void)
  24. {
  25. OBJECT *to = actorHere();
  26. if (to != NULL)
  27. {
  28. talk(params[0], to);
  29. }
  30. else
  31. {
  32. printf("There is nobody here to talk to.\n");
  33. }
  34. return true;
  35. }
  36. bool executeTalkTo(void)
  37. {
  38. OBJECT *to = reachableObject("who to talk to", params[1]);
  39. if (to != NULL)
  40. {
  41. if (to->health > 0)
  42. {
  43. talk(params[0], to);
  44. }
  45. else
  46. {
  47. printf("There is no response from %s.\n", to->description);
  48. }
  49. }
  50. return true;
  51. }

Function getTopic is implemented in one of the existing modules:

noun.h
  1. extern OBJECT *getVisible(const char *intention, const char *noun);
  2. extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
  3. extern OBJECT *getTopic(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 if (isLit(player->location))
  41. {
  42. printf("You don't see any %s here.\n", noun);
  43. }
  44. else
  45. {
  46. printf("It's too dark.\n");
  47. }
  48. }
  49. else if (obj == &ambiguousNoun)
  50. {
  51. printf("Please be specific about which %s you mean.\n", noun);
  52. obj = NULL;
  53. }
  54. return obj;
  55. }
  56. OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
  57. {
  58. OBJECT *obj = NULL;
  59. if (from == NULL)
  60. {
  61. printf("I don't understand who you want to %s.\n", verb);
  62. }
  63. else if ((obj = getObject(noun, from, distHeldContained)) == NULL)
  64. {
  65. if (getObject(noun, player, distNotHere) == NULL)
  66. {
  67. printf("I don't understand what you want to %s.\n", verb);
  68. }
  69. else if (from == player)
  70. {
  71. printf("You are not holding any %s.\n", noun);
  72. }
  73. else
  74. {
  75. printf("There appears to be no %s you can get from %s.\n",
  76. noun, from->description);
  77. }
  78. }
  79. else if (obj == &ambiguousNoun)
  80. {
  81. printf("Please be specific about which %s you want to %s.\n",
  82. noun, verb);
  83. obj = NULL;
  84. }
  85. else if (obj == from)
  86. {
  87. printf("You should not be doing that to %s.\n", obj->description);
  88. obj = NULL;
  89. }
  90. return obj;
  91. }
  92. OBJECT *getTopic(const char *noun)
  93. {
  94. OBJECT *obj;
  95. for (obj = objs; obj < endOfObjs; obj++)
  96. {
  97. if (objectHasTag(obj, noun)) return obj;
  98. }
  99. return NULL;
  100. }

Explanation:

And if you cannot talk your way out of a difficult situation, then there is always the option to use physical force. More on that in the following chapter.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 20. Combat