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

11. Conditions

So far, all the objects’ attributes were data: text, numbers. But attributes may just as well be code.

In the previous chapter, we limited the player’s freedom of movement by closing the cave entrance (passage intoCave). This already makes the game a lot more challenging, but it does not make much of a puzzle, unless we offer a tiny possibility for the player to open the passage. The real challenge should be for the player to find the condition under which the passage opens.

Let’s take a simple example. To get past the guard and enter the cave, the player has to either kill or bribe the guard (or both, for what it’s worth). In other words:

Opening a closed passage (in this case intoCave) involves changing a number of attribute values:

There are a number of ways to accomplish this. Here, I will discuss an approach that is simple, maintainable and versatile.

First of all, we define two separate passages: one that represents the open passage, and the other representing the closed passage. The passages are identical in every attribute except for the ones listed above. (In the generated map you see below, notice the two arrows leading into the cave; one solid, one dashed.)

Next, we introduce a new attribute named condition that determines whether or not a certain object exists. The two passages will be given mutually exclusive conditions, so that only one of them can exist at any given time.

Each condition will be implemented as a boolean-valued function; true means the object exists, false means it does not.

bool intoCaveIsOpen(void) { return guard->health == 0 || silver->location == guard; } bool intoCaveIsClosed(void) { return guard->health > 0 && silver->location != guard; }

The new attribute condition is a pointer to such a function:

bool (*condition)(void);

After some small modifications to object.awk, similar to those made in the previous chapter, we can immediately start assigning functions to the new attribute in object.txt.

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

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

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

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

--> go entrance
You walk into the cave.

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

--> quit

Bye!
- intoCave condition intoCaveIsOpen description "a cave entrance to the east" tags "east", "entrance" location field destination cave detail "The entrance is just a narrow opening in a small outcrop.\n" textGo "You walk into the cave.\n" - intoCaveBlocked condition intoCaveIsClosed description "a cave entrance to the east" tags "east", "entrance" location field prospect cave detail "The entrance is just a narrow opening in a small outcrop.\n" textGo "The guard stops you from walking into the cave.\n"

The two ‘condition’ functions are so specific, each of them is used just this once. Now, wouldn’t it be nice to define the functions right where we need them? Many programming languages support anonymous functions; something like this:

- intoCave condition { return guard->health == 0 || silver->location == guard; } ... - intoCaveBlocked condition { return guard->health > 0 && silver->location != guard; } ...

Plain C does not allow this, but since object.txt is a product of our own domain-specific language (see chapter 9), we can do anything we like! That is, if we can make the code generator turn it into something the C compiler will swallow. The following adjustments to object.awk will do just that.

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["weight"] = "99";
  22. prop["capacity"] = "0";
  23. prop["health"] = "0";
  24. }
  25. obj && /^[ \t]+[a-z]/ {
  26. name = $1;
  27. $1 = "";
  28. if (name in prop) {
  29. prop[name] = $0;
  30. if (/^[ \t]*\{/) {
  31. prop[name] = name count;
  32. if (pass == "c1") print "static bool " prop[name] "(void) " $0;
  33. }
  34. }
  35. else if (pass == "c2") {
  36. print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\"";
  37. }
  38. }
  39. !obj && pass == (/^#include/ ? "c1" : "h") {
  40. print;
  41. }
  42. END {
  43. outputRecord("\n};");
  44. if (pass == "h") {
  45. print "\n#define endOfObjs\t(objs + " count ")";
  46. print "\n#define validObject(obj)\t" \
  47. "((obj) != NULL && (*(obj)->condition)())";
  48. }
  49. }
  50. function outputRecord(separator)
  51. {
  52. if (obj) {
  53. if (pass == "h") {
  54. print "#define " obj "\t(objs + " count ")";
  55. }
  56. else if (pass == "c1") {
  57. print "static const char *tags" count "[] = {" prop["tags"] ", NULL};";
  58. }
  59. else if (pass == "c2") {
  60. print "\t{\t/* " count " = " obj " */";
  61. print "\t\t" prop["condition"] ",";
  62. print "\t\t" prop["description"] ",";
  63. print "\t\ttags" count ",";
  64. print "\t\t" prop["location"] ",";
  65. print "\t\t" prop["destination"] ",";
  66. print "\t\t" prop[prop["prospect"] ? "prospect" : "destination"] ",";
  67. print "\t\t" prop["details"] ",";
  68. print "\t\t" prop["contents"] ",";
  69. print "\t\t" prop["textGo"] ",";
  70. print "\t\t" prop["weight"] ",";
  71. print "\t\t" prop["capacity"] ",";
  72. print "\t\t" prop["health"];
  73. print "\t}" separator;
  74. delete prop;
  75. }
  76. count++;
  77. }
  78. }

Explanation:

So now we can add the extra passage and the conditions to object.txt, as explained earlier.

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

To make the conditions work, we need to adjust functions isHolding and getDistance.

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 validObject(obj) && 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. !validObject(to) ? distNotHere :
  28. to == from ? distSelf :
  29. isHolding(from, to) ? distHeld :
  30. isHolding(to, from) ? distLocation :
  31. isHolding(from->location, to) ? distHere :
  32. isHolding(from, to->location) ? distHeldContained :
  33. isHolding(from->location, to->location) ? distHereContained :
  34. getPassage(from->location, to) != NULL ? distOverthere :
  35. distNotHere;
  36. }
  37. OBJECT *actorHere(void)
  38. {
  39. OBJECT *obj;
  40. for (obj = objs; obj < endOfObjs; obj++)
  41. {
  42. if (isHolding(player->location, obj) && obj != player &&
  43. obj->health > 0)
  44. {
  45. return obj;
  46. }
  47. }
  48. return NULL;
  49. }
  50. int listObjectsAtLocation(OBJECT *location)
  51. {
  52. int count = 0;
  53. OBJECT *obj;
  54. for (obj = objs; obj < endOfObjs; obj++)
  55. {
  56. if (obj != player && isHolding(location, obj))
  57. {
  58. if (count++ == 0)
  59. {
  60. printf("%s:\n", location->contents);
  61. }
  62. printf("%s\n", obj->description);
  63. }
  64. }
  65. return count;
  66. }

Notes:


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 12. Open and close