How to program a text adventure in C
by Ruud Helderman
<r.helderman@hccnet.nl>
Licensed under
MIT License
6. Passages
It’s time to draw a map - and implement it!
The best tools for drawing a map will always be: a pencil and a piece of paper.
A basic map consists of locations (the rectangles),
connected by passages (the arrows).
We already created locations in chapter 3,
now we will start adding the passages.
In the virtual world,
a ‘passage’ may be anything connecting two locations:
a road, a door, a stretch of sand in a desert.
Basically, a passage has the following properties:
- A starting point (location).
- A destination (location).
- The narrative description, for example “a forest path”.
- The tag by which the passage is referred to in the go command.
Considering these properties, it may not come as a surprise that
the struct object defined in chapter 4
is very suitable to store a passage.
In fact, a passage is not that different from an item or actor;
it is present at a certain location as a ‘visible exit’
(this location is the starting point).
It just behaves differently to certain commands.
In particular the command ‘go’:
applied to a passage, go will change the player’s location.
The target location (the ‘destination’)
can be stored in the struct object as a new attribute.
struct object {
const char *description;
const char *tag;
struct object *location;
struct object *destination;
};
|
Notes:
- Obviously, destination is unused in most other objects
(items, actors), but I consider this to be a minor waste of space.
- A passage always runs in one direction;
to connect two locations bi-directionally,
we always have to create two separate passages.
This may seem clumsy at first, but it does give us great flexibility
in refining the behavior of command ‘go’;
see chapter 10 and 11.
- On a large map, you may find it tedious to create all passages by hand.
Then I strongly advise you to
generate the more repetitive parts of your map using custom tooling.
This will not be covered here,
but you may find some inspiration in chapter 9,
where we discuss code generation.
So we expand 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
a cave entrance
--> go entrance
OK.
You are in a little cave.
You see:
a gold coin
an exit
--> go exit
OK.
You are in an open field.
You see:
a silver coin
a burly guard
a cave entrance
--> go cave
OK.
You are in a little cave.
You see:
a gold coin
an exit
--> quit
Bye!
|
object.h |
- typedef struct object {
- const char *description;
- const char *tag;
- struct object *location;
- struct object *destination;
- } OBJECT;
- extern OBJECT objs[];
- #define field (objs + 0)
- #define cave (objs + 1)
- #define silver (objs + 2)
- #define gold (objs + 3)
- #define guard (objs + 4)
- #define player (objs + 5)
- #define intoCave (objs + 6)
- #define exitCave (objs + 7)
- #define endOfObjs (objs + 8)
|
object.c |
- #include <stdio.h>
- #include "object.h"
- OBJECT objs[] = {
- {"an open field" , "field" , NULL , NULL },
- {"a little cave" , "cave" , NULL , NULL },
- {"a silver coin" , "silver" , field, NULL },
- {"a gold coin" , "gold" , cave , NULL },
- {"a burly guard" , "guard" , field, NULL },
- {"yourself" , "yourself", field, NULL },
- {"a cave entrance", "entrance", field, cave },
- {"an exit" , "exit" , cave , field }
- };
|
We will add a little helper function to misc.c
to determine whether a passage exists between two given locations.
misc.h |
- extern OBJECT *getPassage(OBJECT *from, OBJECT *to);
- extern OBJECT *actorHere(void);
- extern int listObjectsAtLocation(OBJECT *location);
|
misc.c |
- #include <stdio.h>
- #include "object.h"
- OBJECT *getPassage(OBJECT *from, OBJECT *to)
- {
- if (from != NULL && to != NULL)
- {
- OBJECT *obj;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (obj->location == from && obj->destination == to)
- {
- return obj;
- }
- }
- }
- return NULL;
- }
- OBJECT *actorHere(void)
- {
- OBJECT *obj;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (obj->location == player->location && obj == guard)
- {
- return obj;
- }
- }
- return NULL;
- }
- int listObjectsAtLocation(OBJECT *location)
- {
- int count = 0;
- OBJECT *obj;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (obj != player && obj->location == location)
- {
- if (count++ == 0)
- {
- printf("You see:\n");
- }
- printf("%s\n", obj->description);
- }
- }
- return count;
- }
|
Explanation:
- Lines 9 and 11:
we scan through all objects until we encounter a passage running
from location ‘from’ to location ‘to’.
- Line 13:
if a matching passage is found, we return a pointer to the passage object.
- Line 17:
if no such passage exists, we return NULL.
We will use the new function getPassage
in the implementation of command ‘go’
to determine whether or not a passage exists
that can take the player to the desired location.
location.h |
- extern void executeLook(const char *noun);
- extern void executeGo(const char *noun);
|
location.c |
- #include <stdio.h>
- #include <string.h>
- #include "object.h"
- #include "misc.h"
- #include "noun.h"
- void executeLook(const char *noun)
- {
- if (noun != NULL && strcmp(noun, "around") == 0)
- {
- printf("You are in %s.\n", player->location->description);
- listObjectsAtLocation(player->location);
- }
- else
- {
- printf("I don't understand what you want to see.\n");
- }
- }
- void executeGo(const char *noun)
- {
- OBJECT *obj = getVisible("where you want to go", noun);
- if (obj == NULL)
- {
- // already handled by getVisible
- }
- else if (getPassage(player->location, obj) != NULL)
- {
- printf("OK.\n");
- player->location = obj;
- executeLook("around");
- }
- else if (obj->location != player->location)
- {
- printf("You don't see any %s here.\n", noun);
- }
- else if (obj->destination != NULL)
- {
- printf("OK.\n");
- player->location = obj->destination;
- executeLook("around");
- }
- else
- {
- printf("You can't get much closer than this.\n");
- }
- }
|
Explanation:
- Line 27:
as of now, command go <location> will only be accepted
if a passage exists to take us there.
- Line 37-42:
besides go <location> (lines 27-32),
we now offer an alternative way for the player to move:
go <passage>.
For example, when in the field,
go cave and go entrance will have the same effect.
We will also use the new function getPassage to determine whether
a certain location is visible from where the player is standing.
Locations that are not connected to the current location by a passage,
are not considered visible.
noun.h |
- extern OBJECT *getVisible(const char *intention, const char *noun);
- extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
|
noun.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <string.h>
- #include "object.h"
- #include "misc.h"
- static bool objectHasTag(OBJECT *obj, const char *noun)
- {
- return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0;
- }
- static OBJECT *getObject(const char *noun)
- {
- OBJECT *obj, *res = NULL;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (objectHasTag(obj, noun))
- {
- res = obj;
- }
- }
- return res;
- }
- OBJECT *getVisible(const char *intention, const char *noun)
- {
- OBJECT *obj = getObject(noun);
- if (obj == NULL)
- {
- printf("I don't understand %s.\n", intention);
- }
- 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))))
- {
- printf("You don't see any %s here.\n", noun);
- obj = NULL;
- }
- return obj;
- }
- OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
- {
- OBJECT *obj = NULL;
- if (from == NULL)
- {
- printf("I don't understand who you want to %s.\n", verb);
- }
- else if ((obj = getObject(noun)) == NULL)
- {
- printf("I don't understand what you want to %s.\n", verb);
- }
- else if (obj == from)
- {
- printf("You should not be doing that to %s.\n", obj->description);
- obj = NULL;
- }
- else if (obj->location != from)
- {
- if (from == player)
- {
- printf("You are not holding any %s.\n", noun);
- }
- else
- {
- printf("There appears to be no %s you can get from %s.\n",
- noun, from->description);
- }
- obj = NULL;
- }
- return obj;
- }
|
Explanation:
- Line 36:
originally, every location was visible from every location.
We will now use function getPassage
to check whether or not two locations are adjacent.
This also prevents other objects from being mistaken for locations.
Originally, every object without a location, was considered to be a location.
This could have caused trouble in chapter 12,
where we will introduce some non-location objects
that (temporarily) do not have a location.
- Line 37:
the adjustment in line 36 does not relieve us from
a NULL check on obj->location.
Without this check, ‘go cave’ could cause
the last two rules (lines 38 and 39) to give a segmentation fault.
The other modules
(main.c, parsexec.c, inventory.c, move.c)
remain unchanged, you can see them in the previous chapters.
Obviously, the map in this sample is trivial:
there are only two locations, and they are connected in both directions.
A third location will be added in chapter 12.
Homework assignment: draw a more elaborate map
and turn it into a list of objects (locations and passages).
Next chapter: 7. Distance