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

7. Distance

A typical adventure contains many puzzles. Infocom adventures were notoriously difficult to complete; solving every puzzle might require weeks, even months of trial and error. Don’t be surprised if the majority of replies from the game are ‘errors’: you can’t do this; you can’t go there; you died. I hate adventures that just return “You can’t” in response to every attempt I make to solve one of the game’s puzzles. Such a reply is dull and not very helpful. It neglects an important aspect of any computer game; in fact, an essential part of life itself: the player has to learn from his mistakes. It is OK for an adventure to be difficult, even frustratingly difficult. But when the player has the feeling he is not making any progress at all, or when the only way to make progress is a brute-force attack on all verb-noun combinations, then even the most hardened player will lose interest and eventually give up. The least an adventure game should do, is explain why the player’s command cannot be completed: “You can’t do that, because...” This helps to make the virtual world more convincing, the story more credible, and the game more enjoyable.

I already put quite some effort in having the game explain why certain commands are ineffective. Just take a look at the many printf calls in noun.c, inventory.c, location.c, move.c. But as the game is getting increasingly complex, this is becoming quite a burden. I need a more structural way to detect and handle error conditions. That is what we will be working on in this chapter.

Most commands operate on one or more objects, for example:

The first thing to check (after the obvious typos caught by the parser) is for the presence of these objects; failure should result in something like “There is no ... here” or “You don’t see any ...” In this chapter, we will build a generic function that can be used by every command to find out if an object is within reach of the player.

You may think we only need to distinguish two cases: either the object is here, or it is not. But many commands require more gradients than just ‘here’ and ‘not here’. Examples:

It all boils down to the fact that there are different notions of ‘here’:

distSelf The object is the player object == player
distHeld The player is holding the object object->location == player
distHeldContained The player is holding another object (for example a bag) containing the object object->location != NULL && object->location->location == player
distLocation The object is the player’s location object == player->location
distHere The object is present at the player’s location object->location == player->location
distHereContained Another object (either an actor or a ‘container’), present at the player’s location, is holding (but not hiding) the object object->location != NULL && object->location->location == player->location
distOverthere The object is a nearby location getPassage(player->location, object) != NULL

The first case (object is player) may seem trivial, but it is important nonetheless. For example, the command "examine yourself" should not return "There is no yourself here."

I tried to follow a logical order: nearby things are at the top, further down below they become more distant. We can continue the list, to cover objects that are even further away:

distNotHere The object is (or appears to be) not here  
distUnknownObject The parser did not recognize the noun entered object == NULL

Notice we have seven different cases of ‘here’, but only one for ‘not here’. This is because typically, the game only needs to provide information about things that can be perceived by the player. If it’s not here, then there’s nothing more to say.

In the leftmost column, I proposed a symbolic name for each case. We will gather these names in an enum named DISTANCE.

typedef enum { distSelf, distHeld, distHeldContained, distLocation, distHere, distHereContained, distOverthere, distNotHere, distUnknownObject } DISTANCE;

And in the rightmost column, I proposed a condition for each case to satisfy. With a little reshuffling, we can easily turn this into a function that calculates the ‘distance’ of an object (as seen from the player’s point of view):

DISTANCE getDistance(OBJECT *from, OBJECT *to) { return to == NULL ? distUnknownObject : to == from ? distSelf : to->location == from ? distHeld : to == from->location ? distLocation : to->location == from->location ? distHere : getPassage(from->location, to) != NULL ? distOverthere : to->location == NULL ? distNotHere : to->location->location == from ? distHeldContained : to->location->location == from->location ? distHereContained : distNotHere; }

That’s all! We can call this function and do a comparison on its return value. For example, we had the following piece of code in noun.c:

else if (!(obj == player || obj == player->location || obj->location == player || obj->location == player->location || getPassage(player->location, obj) != NULL || (obj->location != NULL && (obj->location->location == player || obj->location->location == player->location))))

We can now replace each sub-condition by an appropriate distance check:

else if (!(getDistance(player, obj) == distSelf || getDistance(player, obj) == distLocation || getDistance(player, obj) == distHeld || getDistance(player, obj) == distHere || getDistance(player, obj) == distOverthere || getDistance(player, obj) == distHeldContained || getDistance(player, obj) == distHereContained)

This can be reduced to:

else if (getDistance(player, obj) >= distNotHere)

This was just an example to give you an idea of the concept; the actual implementation of noun.c you will find below, looks slightly different.

Time to put things into place. The definitions of enum DISTANCE and function getDistance are added to misc.h and misc.c, since we will be using them in more than one module.

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->destination == 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 == guard)
  42. {
  43. return obj;
  44. }
  45. }
  46. return NULL;
  47. }
  48. int listObjectsAtLocation(OBJECT *location)
  49. {
  50. int count = 0;
  51. OBJECT *obj;
  52. for (obj = objs; obj < endOfObjs; obj++)
  53. {
  54. if (obj != player && isHolding(location, obj))
  55. {
  56. if (count++ == 0)
  57. {
  58. printf("You see:\n");
  59. }
  60. printf("%s\n", obj->description);
  61. }
  62. }
  63. return count;
  64. }

Explanation:

In function executeGo, we can replace most of the if conditions by a check on distance.

Sample output
Welcome to Little Cave Adventure.
You are in an open field.
You see:
a silver coin
a burly guard
a cave entrance

--> go guard
You can't get much closer than this.

--> give silver
You are not holding any silver.

--> ask silver
There appears to be no silver you can get from a burly guard.

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

--> get gold
You don't see any gold here.

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

--> go cave
OK.
You are in a little cave.
You see:
a gold coin
an exit

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

--> give gold
There is nobody here to give that to.

--> quit

Bye!
location.h
  1. extern void executeLook(const char *noun);
  2. extern void executeGo(const char *noun);
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. printf("I don't understand what you want to see.\n");
  17. }
  18. }
  19. void executeGo(const char *noun)
  20. {
  21. OBJECT *obj = getVisible("where you want to go", noun);
  22. switch (getDistance(player, obj))
  23. {
  24. case distOverthere:
  25. printf("OK.\n");
  26. player->location = obj;
  27. executeLook("around");
  28. break;
  29. case distNotHere:
  30. printf("You don't see any %s here.\n", noun);
  31. break;
  32. case distUnknownObject:
  33. // already handled by getVisible
  34. break;
  35. default:
  36. if (obj->destination != NULL)
  37. {
  38. printf("OK.\n");
  39. player->location = obj->destination;
  40. executeLook("around");
  41. }
  42. else
  43. {
  44. printf("You can't get much closer than this.\n");
  45. }
  46. }
  47. }

The same goes for function executeGet.

inventory.h
  1. extern void executeGet(const char *noun);
  2. extern void executeDrop(const char *noun);
  3. extern void executeAsk(const char *noun);
  4. extern void executeGive(const char *noun);
  5. extern void executeInventory(void);
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 == guard)
  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. }

And finally, we will adjust the constraints in noun.c. I am adding two parameters to function getObject that make it possible to find a match for a specific noun, while at the same time ignore any objects that are considered absent. This will really pay off in the next chapter, where we will introduce different objects having the same tag.

noun.h
  1. extern OBJECT *getVisible(const char *intention, const char *noun);
  2. extern OBJECT *getPossession(OBJECT *from, const char *verb, 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. return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0;
  9. }
  10. static OBJECT *getObject(const char *noun, OBJECT *from, DISTANCE maxDistance)
  11. {
  12. OBJECT *obj, *res = NULL;
  13. for (obj = objs; obj < endOfObjs; obj++)
  14. {
  15. if (objectHasTag(obj, noun) && getDistance(from, obj) <= maxDistance)
  16. {
  17. res = obj;
  18. }
  19. }
  20. return res;
  21. }
  22. OBJECT *getVisible(const char *intention, const char *noun)
  23. {
  24. OBJECT *obj = getObject(noun, player, distOverthere);
  25. if (obj == NULL)
  26. {
  27. if (getObject(noun, player, distNotHere) == NULL)
  28. {
  29. printf("I don't understand %s.\n", intention);
  30. }
  31. else
  32. {
  33. printf("You don't see any %s here.\n", noun);
  34. }
  35. }
  36. return obj;
  37. }
  38. OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
  39. {
  40. OBJECT *obj = NULL;
  41. if (from == NULL)
  42. {
  43. printf("I don't understand who you want to %s.\n", verb);
  44. }
  45. else if ((obj = getObject(noun, from, distHeldContained)) == NULL)
  46. {
  47. if (getObject(noun, player, distNotHere) == NULL)
  48. {
  49. printf("I don't understand what you want to %s.\n", verb);
  50. }
  51. else if (from == player)
  52. {
  53. printf("You are not holding any %s.\n", noun);
  54. }
  55. else
  56. {
  57. printf("There appears to be no %s you can get from %s.\n",
  58. noun, from->description);
  59. }
  60. }
  61. else if (obj == from)
  62. {
  63. printf("You should not be doing that to %s.\n", obj->description);
  64. obj = NULL;
  65. }
  66. return obj;
  67. }

Explanation:

The other modules (main.c, parsexec.c, move.c, object.c) remain unchanged, you can see them in previous chapters.

In this chapter, the concept of distance was mainly used to choose between different responses the game can give to the user. But the benefits of distance are not reserved to the output side; it can be used equally well to make improvements on the input side. In the next chapter, we will use distance to improve the recognition of nouns entered by the user.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 8. North, east, south, west