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

10. More attributes

There are many possible reasons for expanding the ‘object’ structure.

When we introduced objects in chapter 4, they had only three attributes. In chapter 6, we added a fourth. This is more or less the absolute minimum. To put more detail into our adventure, we need some more attributes. Here are a few examples.

  1. The command look around gives a global description of the player’s location, including a list of items, actors and other objects present there. Many adventures require the player to examine these objects, either to reveal certain clues that are needed to make progress in the game, or simply to enhance the game’s atmosphere. We will add an attribute details that holds a detailed description of each object, plus an attribute contents that is used with objects that contain other objects.
  2. When the player follows a passage, the response is invariably “OK” followed by a description of the new location. This is a bit dull; it would be so much nicer to give each passage its own custom message. We will add an attribute textGo to hold this message.
  3. Some passages have a ‘twist’; they do not go where the player expects them to go. For example, a forest path may be hiding a pitfall. While the passage appears to be leading from location A to location B, in reality the end point is location C, i.e. the bottom of a pit. More common ‘twists’ are passages that are ‘blocked’: a closed grating, a broken bridge, a narrow crack. Suppose our cave entrance is blocked by the guard. Any attempt to enter the cave will fail; instead the player will stay at his original location, i.e. the field. We could simply change the passage’s destination to field (or NULL), but that would result in an undesirable response to commands like go cave and look cave: “You don’t see any cave here.” We need separate attributes for the actual and the apparent end point of a passage. We will introduce an attribute prospect to represent the latter; the attribute destination, introduced in chapter 6, still holds the actual end point. In most cases, the two will be equal, so we will have object.awk generate an appropriate default; prospect only needs to be specified in object.txt when it differs from destination.
  4. In many adventures, the player, as well as other actors in the game, are limited in how much they can carry. Give each item a weight; the combined weight of all items in an actor’s inventory should not exceed that actor’s capacity. Give an object a very high weight to make it immovable (a tree, a house, a mountain).
  5. RPG-style adventure games will need a whole range of attributes for actors (both player and non-player), for example health. Objects with zero health are either dead, or they are not an actor at all.

We define seven new attributes in object.txt:

object.txt
  1. #include <stdio.h>
  2. #include "object.h"
  3. typedef struct object {
  4. const char *description;
  5. const char **tags;
  6. struct object *location;
  7. struct object *destination;
  8. struct object *prospect;
  9. const char *details;
  10. const char *contents;
  11. const char *textGo;
  12. int weight;
  13. int capacity;
  14. int health;
  15. } OBJECT;
  16. extern OBJECT objs[];
  17. - field
  18. description "an open field"
  19. tags "field"
  20. details "The field is a nice and quiet place under a clear blue sky."
  21. capacity 9999
  22. - cave
  23. description "a little cave"
  24. tags "cave"
  25. details "The cave is just a cold, damp, rocky chamber."
  26. capacity 9999
  27. - silver
  28. description "a silver coin"
  29. tags "silver", "coin", "silver coin"
  30. location field
  31. details "The coin has an eagle on the obverse."
  32. weight 1
  33. - gold
  34. description "a gold coin"
  35. tags "gold", "coin", "gold coin"
  36. location cave
  37. details "The shiny coin seems to be a rare and priceless artefact."
  38. weight 1
  39. - guard
  40. description "a burly guard"
  41. tags "guard", "burly guard"
  42. location field
  43. details "The guard is a really big fellow."
  44. contents "He has"
  45. health 100
  46. capacity 20
  47. - player
  48. description "yourself"
  49. tags "yourself"
  50. location field
  51. details "You would need a mirror to look at yourself."
  52. contents "You have"
  53. health 100
  54. capacity 20
  55. - intoCave
  56. description "a cave entrance to the east"
  57. tags "east", "entrance"
  58. location field
  59. prospect cave
  60. details "The entrance is just a narrow opening in a small outcrop."
  61. textGo "The guard stops you from walking into the cave."
  62. - exitCave
  63. description "an exit to the west"
  64. tags "west", "exit"
  65. location cave
  66. destination field
  67. details "Sunlight pours in through an opening in the cave's wall."
  68. textGo "You walk out of the cave."
  69. - wallField
  70. description "dense forest all around"
  71. tags "west", "north", "south", "forest"
  72. location field
  73. details "The field is surrounded by trees and undergrowth."
  74. textGo "Dense forest is blocking the way."
  75. - wallCave
  76. description "solid rock all around"
  77. tags "east", "north", "south", "rock"
  78. location cave
  79. details "Carved in stone is a secret password 'abccb'."
  80. textGo "Solid rock is blocking the way."

Explanation:

New attributes also require an adjustment in the code generator.

object.awk
  1. BEGIN {
  2. count = 0;
  3. obj = "";
  4. if (pass == "c2") {
  5. print "\nOBJECT objs[] = {";
  6. }
  7. }
  8. /^- / {
  9. outputRecord(",");
  10. obj = $2;
  11. prop["description"] = "NULL";
  12. prop["tags"] = "";
  13. prop["location"] = "NULL";
  14. prop["destination"] = "NULL";
  15. prop["prospect"] = "";
  16. prop["details"] = "\"You see nothing special.\"";
  17. prop["contents"] = "\"You see\"";
  18. prop["textGo"] = "\"You can't get much closer than this.\"";
  19. prop["weight"] = "99";
  20. prop["capacity"] = "0";
  21. prop["health"] = "0";
  22. }
  23. obj && /^[ \t]+[a-z]/ {
  24. name = $1;
  25. $1 = "";
  26. if (name in prop) {
  27. prop[name] = $0;
  28. }
  29. else if (pass == "c2") {
  30. print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\"";
  31. }
  32. }
  33. !obj && pass == (/^#include/ ? "c1" : "h") {
  34. print;
  35. }
  36. END {
  37. outputRecord("\n};");
  38. if (pass == "h") {
  39. print "\n#define endOfObjs\t(objs + " count ")";
  40. }
  41. }
  42. function outputRecord(separator)
  43. {
  44. if (obj) {
  45. if (pass == "h") {
  46. print "#define " obj "\t(objs + " count ")";
  47. }
  48. else if (pass == "c1") {
  49. print "static const char *tags" count "[] = {" prop["tags"] ", NULL};";
  50. }
  51. else if (pass == "c2") {
  52. print "\t{\t/* " count " = " obj " */";
  53. print "\t\t" prop["description"] ",";
  54. print "\t\ttags" count ",";
  55. print "\t\t" prop["location"] ",";
  56. print "\t\t" prop["destination"] ",";
  57. print "\t\t" prop[prop["prospect"] ? "prospect" : "destination"] ",";
  58. print "\t\t" prop["details"] ",";
  59. print "\t\t" prop["contents"] ",";
  60. print "\t\t" prop["textGo"] ",";
  61. print "\t\t" prop["weight"] ",";
  62. print "\t\t" prop["capacity"] ",";
  63. print "\t\t" prop["health"];
  64. print "\t}" separator;
  65. delete prop;
  66. }
  67. count++;
  68. }
  69. }

Now we’re all set to start using the new attributes! details is used in a newly recognized command look <object>, and textGo replaces the fixed text “OK” in our implementation of command go.

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

--> look guard
The guard is a really big fellow.

--> get guard
That is way too heavy.

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

--> inventory
You have:
a silver coin

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

--> look guard
The guard is a really big fellow.
He has:
a silver coin

--> go cave
The guard stops you from walking into the cave.

--> go north
Dense forest is blocking the way.

--> quit

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

Attributes weight and capacity together become a possible reason for not being able to move certain objects around. And a health check replaces the hard-coded whitelist of actors.

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 (obj->weight > to->capacity)
  47. {
  48. printf("That is way too heavy.\n");
  49. }
  50. else if (obj->weight + weightOfContents(to) > to->capacity)
  51. {
  52. printf("That would become too heavy.\n");
  53. }
  54. else
  55. {
  56. describeMove(obj, to);
  57. obj->location = to;
  58. }
  59. }

Here is one more module that can use health to identify actors.

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

The weight check makes use of a new function weightOfContents; it will be implemented in misc.c. In the same module, we also make modifications to some of the existing functions, to support the last few attributes.

Attribute contents replaces the fixed text “You see”. The original text was already a bit odd when listing the player’s inventory, but now that function listObjectsAtLocation is used to display contents of any possible object (see function executeLook above), we really need something more flexible.

By replacing attribute destination by prospect in function getPassage, we are improving responses to all commands (not just go and look) applied to a location that is seen lying on the other end of a ‘passage with a twist.’

misc.h
  1. typedef enum {
  2. distSelf,
  3. distHeld,
  4. distHeldContained,
  5. distLocation,
  6. distHere,
  7. distHereContained,
  8. distOverthere,
  9. distNotHere,
  10. distUnknownObject
  11. } DISTANCE;
  12. extern bool isHolding(OBJECT *container, OBJECT *obj);
  13. extern OBJECT *getPassage(OBJECT *from, OBJECT *to);
  14. extern DISTANCE getDistance(OBJECT *from, OBJECT *to);
  15. extern OBJECT *actorHere(void);
  16. extern int listObjectsAtLocation(OBJECT *location);
misc.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "misc.h"
  5. bool isHolding(OBJECT *container, OBJECT *obj)
  6. {
  7. return obj != NULL && obj->location == container;
  8. }
  9. OBJECT *getPassage(OBJECT *from, OBJECT *to)
  10. {
  11. if (from != NULL && to != NULL)
  12. {
  13. OBJECT *obj;
  14. for (obj = objs; obj < endOfObjs; obj++)
  15. {
  16. if (isHolding(from, obj) && obj->prospect == to)
  17. {
  18. return obj;
  19. }
  20. }
  21. }
  22. return NULL;
  23. }
  24. DISTANCE getDistance(OBJECT *from, OBJECT *to)
  25. {
  26. return to == NULL ? distUnknownObject :
  27. to == from ? distSelf :
  28. isHolding(from, to) ? distHeld :
  29. isHolding(to, from) ? distLocation :
  30. isHolding(from->location, to) ? distHere :
  31. isHolding(from, to->location) ? distHeldContained :
  32. isHolding(from->location, to->location) ? distHereContained :
  33. getPassage(from->location, to) != NULL ? distOverthere :
  34. distNotHere;
  35. }
  36. OBJECT *actorHere(void)
  37. {
  38. OBJECT *obj;
  39. for (obj = objs; obj < endOfObjs; obj++)
  40. {
  41. if (isHolding(player->location, obj) && obj != player &&
  42. obj->health > 0)
  43. {
  44. return obj;
  45. }
  46. }
  47. return NULL;
  48. }
  49. int listObjectsAtLocation(OBJECT *location)
  50. {
  51. int count = 0;
  52. OBJECT *obj;
  53. for (obj = objs; obj < endOfObjs; obj++)
  54. {
  55. if (obj != player && isHolding(location, obj))
  56. {
  57. if (count++ == 0)
  58. {
  59. printf("%s:\n", location->contents);
  60. }
  61. printf("%s\n", obj->description);
  62. }
  63. }
  64. return count;
  65. }

To make the whole picture complete, it would be nice to expand the generated map from the previous chapter with dashed lines for the ‘apparent’ passages (leading towards a prospect).

map.awk
  1. BEGIN { print "digraph map {"; }
  2. /^- / { outputEdges(); delete a; }
  3. /^[ \t]/ { a[$1] = $2; }
  4. END { outputEdges(); print "}"; }
  5. function outputEdges()
  6. {
  7. outputEdge(a["location"], a["destination"], "");
  8. outputEdge(a["location"], a["prospect"], " [style=dashed]");
  9. }
  10. function outputEdge(from, to, style)
  11. {
  12. if (from && to) print "\t" from " -> " to style;
  13. }

Notes:


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 11. Conditions