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

4. Objects

Before we go on, let me make it perfectly clear I am using the term ‘object’ in a philosophical sense here. It has nothing to do with object-oriented programming, nor does it have anything in common with the ‘Object’ type pre-defined in programming languages like Java, C# and Python. Below, I will define a new data type named object; any other name will do equally well if you find object to be confusing, or if it gives a namespace conflict in the programming environment you are using. Very well, moving on...

Most puzzles in adventure games revolve around items and/or actors. Examples:

Naturally, the guard might as well be a dog, troll, dragon or robot. In this sense, non-player character is a common term, but I don’t want to make a distinction between player and non-player characters (the guard might well be another player in a multi-player game). And just ‘character’ is easily confused with a character data type, so I will stick to ‘actor’.

To represent items and actors, we can use a struct like this:

Basic map with objects

struct object { const char *description; const char *tag; struct location *location; } objs[] = { {"a silver coin", "silver", &locs[0]}, {"a gold coin" , "gold" , &locs[1]}, {"a burly guard", "guard" , &locs[0]} };

Notice this data structure is very similar to the array of locations we made in the previous chapter. In fact, the two are so similar we can merge them into a single big list containing locations, items and actors, and simply refer to all of them as objects.

struct object { const char *description; const char *tag; struct object *location; } objs[] = { {"an open field", "field" , NULL}, {"a little cave", "cave" , NULL}, {"a silver coin", "silver", &objs[0]}, {"a gold coin" , "gold" , &objs[1]}, {"a burly guard", "guard" , &objs[0]} };

Now that there is no separation between objects and locations, the struct object contains a pointer to itself. This is nothing exceptional in C: a linked list works in a similar way, so don’t be alarmed.

To make it easier to reference individual objects, we will define symbolic names for pointers to each element in the array.

#define field (objs + 0) #define cave (objs + 1) #define silver (objs + 2) #define gold (objs + 3) #define guard (objs + 4)

Here are a few examples of how to use these pointers. The first one is an adaption of a code sample from the previous chapter, displaying the text “You are in an open field.”

printf("You are in %s.\n", field->description);

The following piece of code will list all items and actors present in the cave.

struct object *obj; for (obj = objs; obj < objs + 5; obj++) { if (obj->location == cave) { printf("%s\n", obj->description); } }

So what is the benefit of having a single big list of objects? Our code becomes simpler, as many functions (like the one above) only need to scan through a single list of objects, rather than three lists. One might argue that this is irrelevant, since each command applies to one type of object only:

But this separation is hardly realistic, for three reasons:

  1. Some commands apply to more than one type of object, in particular examine.
  2. Besides, an adventure that responds to “eat guard” with “You can’t”, is just plain boring. Verbs should not discriminate on the type of object. The perfect adventure is one that returns an imaginative response for every combination of verb and noun within the game’s vocabulary.
  3. Some objects may have more than one role in the game. Examples:

With all objects together in one big list, it is tempting to add some enum attribute named ‘type’ to struct object to help us distinguish between the different types of objects. However, objects typically have other characteristics that work equally well:

There is one more object we will add to the array: the player himself. In the next chapter, we will see the real benefits of this choice. For now, the only difference is in the way the player’s current location is stored. In the previous chapter, there was a separate variable locationOfPlayer. We will drop it, and use the location attribute of the player object instead. For example, this statement will move the player into the cave:

player->location = cave;

And this expression returns the description of the player’s current location:

player->location->description

Time to put it all together. We start with a whole new module for the array of objects.

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

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

--> go field
OK.
You are in an open field.
You see:
a silver coin
a burly guard

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

--> look around
You are in an open field.
You see:
a silver coin
a burly guard

--> quit

Bye!
object.h
  1. typedef struct object {
  2. const char *description;
  3. const char *tag;
  4. struct object *location;
  5. } OBJECT;
  6. extern OBJECT objs[];
  7. #define field (objs + 0)
  8. #define cave (objs + 1)
  9. #define silver (objs + 2)
  10. #define gold (objs + 3)
  11. #define guard (objs + 4)
  12. #define player (objs + 5)
  13. #define endOfObjs (objs + 6)
object.c
  1. #include <stdio.h>
  2. #include "object.h"
  3. OBJECT objs[] = {
  4. {"an open field", "field" , NULL },
  5. {"a little cave", "cave" , NULL },
  6. {"a silver coin", "silver" , field },
  7. {"a gold coin" , "gold" , cave },
  8. {"a burly guard", "guard" , field },
  9. {"yourself" , "yourself", field }
  10. };

Note: to compile this module, the compiler must support constant folding. This rules out some of the more primitive compilers like Z88DK. Which is a shame, since that particular compiler might otherwise be used to port our 1980s-style game to a 1980s-style computer.

For most commands (go, to begin with), the following module is going to help us find the object that matches the noun specified.

noun.h
  1. extern OBJECT *getVisible(const char *intention, const char *noun);
noun.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include "object.h"
  5. static bool objectHasTag(OBJECT *obj, const char *noun)
  6. {
  7. return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0;
  8. }
  9. static OBJECT *getObject(const char *noun)
  10. {
  11. OBJECT *obj, *res = NULL;
  12. for (obj = objs; obj < endOfObjs; obj++)
  13. {
  14. if (objectHasTag(obj, noun))
  15. {
  16. res = obj;
  17. }
  18. }
  19. return res;
  20. }
  21. OBJECT *getVisible(const char *intention, const char *noun)
  22. {
  23. OBJECT *obj = getObject(noun);
  24. if (obj == NULL)
  25. {
  26. printf("I don't understand %s.\n", intention);
  27. }
  28. else if (!(obj == player ||
  29. obj == player->location ||
  30. obj->location == player ||
  31. obj->location == player->location ||
  32. obj->location == NULL ||
  33. obj->location->location == player ||
  34. obj->location->location == player->location))
  35. {
  36. printf("You don't see any %s here.\n", noun);
  37. obj = NULL;
  38. }
  39. return obj;
  40. }

Explanation:

Here is another helper function. It prints a list of objects (items, actors) present at a specific location. It is going to be used in function executeLook, and in the next chapter we will introduce another command that needs it.

misc.h
  1. extern int listObjectsAtLocation(OBJECT *location);
misc.c
  1. #include <stdio.h>
  2. #include "object.h"
  3. int listObjectsAtLocation(OBJECT *location)
  4. {
  5. int count = 0;
  6. OBJECT *obj;
  7. for (obj = objs; obj < endOfObjs; obj++)
  8. {
  9. if (obj != player && obj->location == location)
  10. {
  11. if (count++ == 0)
  12. {
  13. printf("You see:\n");
  14. }
  15. printf("%s\n", obj->description);
  16. }
  17. }
  18. return count;
  19. }

Explanation:

In location.c, the implementation of commands look around and go is adjusted to the new data structure. The old array of locations is removed, and so is the variable locationOfPlayer.

location.h
  1. extern void executeLook(const char *noun);
  2. extern void executeGo(const char *noun);
location.c
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include "object.h"
  4. #include "misc.h"
  5. #include "noun.h"
  6. void executeLook(const char *noun)
  7. {
  8. if (noun != NULL && strcmp(noun, "around") == 0)
  9. {
  10. printf("You are in %s.\n", player->location->description);
  11. listObjectsAtLocation(player->location);
  12. }
  13. else
  14. {
  15. printf("I don't understand what you want to see.\n");
  16. }
  17. }
  18. void executeGo(const char *noun)
  19. {
  20. OBJECT *obj = getVisible("where you want to go", noun);
  21. if (obj == NULL)
  22. {
  23. // already handled by getVisible
  24. }
  25. else if (obj->location == NULL && obj != player->location)
  26. {
  27. printf("OK.\n");
  28. player->location = obj;
  29. executeLook("around");
  30. }
  31. else
  32. {
  33. printf("You can't get much closer than this.\n");
  34. }
  35. }

Explanation:

The modules main.c and parsexec.c remain unchanged; you can see them in chapters 2 and 3, respectively.

Again, feel free to experiment by adding more objects to the array in object.c. Do not forget to increase endOfObjs in object.h accordingly, or the additional objects will be ignored.

It is nice to have some items to enrich our virtual environment, but right now, there is not much we can do with them. Silver and gold is scattered across the floor, but we cannot even pick it up! Let’s fix that in the next chapter.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 5. Inventory