Contents
12. Open and close
|
How to program a text adventure in C
by Ruud Helderman
<r.helderman@hccnet.nl>
Licensed under
MIT License
12. Open and close
In the previous chapter,
we used ‘condition’ functions to make objects disappear.
But of course, there is a much simpler way to achieve the same:
just clear the object’s location attribute!
The cave entrance was a typical example
where condition functions work particularly well.
This is because the entrance is affected by attributes in other objects
(the guard and the silver coin);
the functions make it possible to keep all the logic together.
Let’s take a more straightforward example.
Suppose the cave has a door leading to a backroom.
Just a simple doorway, which the player can open and close.
Just like in the previous chapter, we will use two objects for the passage;
one to represent the open door, and another for when it is closed.
- backroom
description "a backroom"
tags "backroom"
details "The room is dusty and messy.\n"
- openDoorToBackroom
description "an open door to the south"
tags "south", "door", "doorway"
destination backroom
details "The door is open.\n"
textGo "You walk through the door into the backroom.\n"
- closedDoorToBackroom
description "a closed door to the south"
tags "south", "door", "doorway"
location cave
prospect backroom
details "The door is closed.\n"
textGo "The door is closed.\n"
|
Naturally, the door should be accessible from the other side as well.
- openDoorToCave
description "an open door to the north"
tags "north", "door", "doorway"
destination cave
details "The door is open.\n"
textGo "You walk through the door into the cave.\n"
- closedDoorToCave
description "a closed door to the north"
tags "north", "door", "doorway"
location backroom
prospect cave
details "The door is closed.\n"
textGo "The door is closed.\n"
|
Notice I only gave the closed doorways a location; the open ones have none.
So initially, the door is closed
(hence the dashed arrows between cave and backroom
in the generated map you see on the right).
To open the door, all we have to do is swap the locations.
openDoorToBackroom->location = cave;
closedDoorToBackroom->location = NULL;
openDoorToCave->location = backroom;
closedDoorToCave->location = NULL;
|
Let’s create a helper function to accommodate this.
void swapLocations(OBJECT *obj1, OBJECT *obj2)
{
OBJECT *tmp = obj1->location;
obj1->location = obj2->location;
obj2->location = tmp;
}
|
Now the following statements can be used to open the door;
and once it is open, the same statements will close it again.
swapLocations(openDoorToBackroom, closedDoorToBackroom);
swapLocations(openDoorToCave, closedDoorToCave);
|
The helper function is particularly convenient
when the object in question is movable.
For example, a box can be opened and closed,
but it is also an item that can be picked up and moved elsewhere.
In other words, its location is not fixed.
Function swapLocations does not rely on a fixed location,
since it passes the current location back and forth between two objects.
Of course, a box is not a passage;
the player is always on the outside, so a single pair of objects will suffice,
and so will a single call to swapLocations.
swapLocations(openBox, closedBox);
|
This is more or less all we need
to implement some new commands open and close.
Below is a simple implementation of open;
the implementation of close is similar.
OBJECT *obj = parseObject(noun);
if (obj == closedDoorToBackRoom || obj == closedDoorToCave)
{
swapLocations(openDoorToBackroom, closedDoorToBackroom);
swapLocations(openDoorToCave, closedDoorToCave);
printf("OK.\n");
}
else if (obj == closedBox)
{
swapLocations(openBox, closedBox);
printf("OK.\n");
}
else if (obj == openDoorToBackRoom || obj == openDoorToCave || obj == openBox)
{
printf("That is already open.\n");
}
else
{
printf("That cannot be opened.\n");
}
|
To make things slightly more complicated,
we can put a lock on the door or on the box.
This requires (at least) three mutually exclusive objects;
one for each of the possible states:
open, closed and locked.
But we can still use the same function to swap locations of the objects.
For example, here’s how to unlock a locked box; and vice versa.
swapLocations(closedBox, lockedBox);
|
There is some overhead involved in the other commands.
Our implementation of command open must be expanded
to handle the new object lockedBox:
...
else if (obj == lockedBox)
{
printf("You can't, it is locked.\n");
}
...
|
It may be clear that the number of lines of code
is proportional to the number of doors in the game
(and boxes and other objects that can be opened).
So if your game has more than just a handful of doors,
then it is a good idea to go for a more generic solution.
By the way, this is something that goes for every command:
when it concerns a good many objects, try to write generic code;
but when you are dealing with one or two special cases,
just stick to straightforward, specialized code.
Generic code typically comes with a data-driven approach.
In other words,
we need to add one or more attributes to our object structure.
In this particular case, we will be adding a function pointer
for each of the commands we wish to support:
open, close, lock and unlock.
object.txt |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "toggle.h"
- typedef struct object {
- bool (*condition)(void);
- const char *description;
- const char **tags;
- struct object *location;
- struct object *destination;
- struct object *prospect;
- const char *details;
- const char *contents;
- const char *textGo;
- int weight;
- int capacity;
- int health;
- void (*open)(void);
- void (*close)(void);
- void (*lock)(void);
- void (*unlock)(void);
- } OBJECT;
- extern OBJECT objs[];
- - field
- description "an open field"
- tags "field"
- details "The field is a nice and quiet place under a clear blue sky."
- capacity 9999
- - cave
- description "a little cave"
- tags "cave"
- details "The cave is just a cold, damp, rocky chamber."
- capacity 9999
- - silver
- description "a silver coin"
- tags "silver", "coin", "silver coin"
- location field
- details "The coin has an eagle on the obverse."
- weight 1
- - gold
- description "a gold coin"
- tags "gold", "coin", "gold coin"
- location openBox
- details "The shiny coin seems to be a rare and priceless artefact."
- weight 1
- - guard
- description "a burly guard"
- tags "guard", "burly guard"
- location field
- details "The guard is a really big fellow."
- contents "He has"
- health 100
- capacity 20
- - player
- description "yourself"
- tags "yourself"
- location field
- details "You would need a mirror to look at yourself."
- contents "You have"
- health 100
- capacity 20
- - intoCave
- condition { return guard->health == 0 || silver->location == guard; }
- description "a cave entrance to the east"
- tags "east", "entrance"
- location field
- destination cave
- details "The entrance is just a narrow opening in a small outcrop."
- textGo "You walk into the cave."
- open isAlreadyOpen
- - intoCaveBlocked
- condition { return guard->health > 0 && silver->location != guard; }
- description "a cave entrance to the east"
- tags "east", "entrance"
- location field
- prospect cave
- details "The entrance is just a narrow opening in a small outcrop."
- textGo "The guard stops you from walking into the cave."
- open isAlreadyOpen
- - exitCave
- description "an exit to the west"
- tags "west", "exit"
- location cave
- destination field
- details "Sunlight pours in through an opening in the cave's wall."
- textGo "You walk out of the cave."
- open isAlreadyOpen
- - wallField
- description "dense forest all around"
- tags "west", "north", "south", "forest"
- location field
- details "The field is surrounded by trees and undergrowth."
- textGo "Dense forest is blocking the way."
- - wallCave
- description "solid rock all around"
- tags "east", "north", "rock"
- location cave
- details "Carved in stone is a secret password 'abccb'."
- textGo "Solid rock is blocking the way."
- - backroom
- description "a backroom"
- tags "backroom"
- details "The room is dusty and messy."
- capacity 9999
- - wallBackroom
- description "solid rock all around"
- tags "east", "west", "south", "rock"
- location backroom
- details "Trendy wallpaper covers the rock walls."
- textGo "Solid rock is blocking the way."
- - openDoorToBackroom
- description "an open door to the south"
- tags "south", "door", "doorway"
- destination backroom
- details "The door is open."
- textGo "You walk through the door into a backroom."
- open isAlreadyOpen
- close toggleDoorToBackroom
- - closedDoorToBackroom
- description "a closed door to the south"
- tags "south", "door", "doorway"
- location cave
- prospect backroom
- details "The door is closed."
- textGo "The door is closed."
- open toggleDoorToBackroom
- close isAlreadyClosed
- - openDoorToCave
- description "an open door to the north"
- tags "north", "door", "doorway"
- destination cave
- details "The door is open."
- textGo "You walk through the door into the cave."
- open isAlreadyOpen
- close toggleDoorToCave
- - closedDoorToCave
- description "a closed door to the north"
- tags "north", "door", "doorway"
- location backroom
- prospect cave
- details "The door is closed."
- textGo "The door is closed."
- open toggleDoorToCave
- close isAlreadyClosed
- - openBox
- description "a wooden box"
- tags "box", "wooden box"
- details "The box is open."
- weight 5
- capacity 10
- open isAlreadyOpen
- close toggleBox
- lock isStillOpen
- unlock isAlreadyOpen
- - closedBox
- description "a wooden box"
- tags "box", "wooden box"
- details "The box is closed."
- weight 5
- open toggleBox
- close isAlreadyClosed
- lock toggleBoxLock
- unlock isAlreadyUnlocked
- - lockedBox
- description "a wooden box"
- tags "box", "wooden box"
- location backroom
- details "The box is closed."
- weight 5
- open isStillLocked
- close isAlreadyClosed
- lock isAlreadyLocked
- unlock toggleBoxLock
- - keyForBox
- description "a tiny key"
- tags "key", "tiny key"
- location cave
- details "The key is really small and shiny."
- weight 1
|
Explanation:
- Line 89:
at first sight, isAlreadyOpen may seem inappropriate here;
technically, intoCaveBlocked is a closed passage.
But storywise, it is still an opening.
- Line 169, 180, 191:
If you prefer a heavy treasure chest instead of a box,
then all you have to do is increase the weight
(and adjust the relevant text and tags accordingly).
To avoid
duplicate code,
I deliberately did not use anonymous functions this time.
Instead, we will implement the necessary logic in a separate module.
Function swapLocations is in there too,
but in a slightly extended version,
which will also output feedback to the user.
toggle.h |
- extern void cannotBeOpened(void);
- extern void cannotBeClosed(void);
- extern void cannotBeLocked(void);
- extern void cannotBeUnlocked(void);
- extern void isAlreadyOpen(void);
- extern void isAlreadyClosed(void);
- extern void isAlreadyLocked(void);
- extern void isAlreadyUnlocked(void);
- extern void isStillOpen(void);
- extern void isStillLocked(void);
- extern void toggleDoorToBackroom(void);
- extern void toggleDoorToCave(void);
- extern void toggleBox(void);
- extern void toggleBoxLock(void);
|
toggle.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- static void swapLocations(const char *verb1, OBJECT *obj1,
- const char *verb2, OBJECT *obj2)
- {
- OBJECT *tmp = obj1->location;
- OBJECT *obj = tmp != NULL ? obj1 : obj2;
- const char *verb = tmp != NULL ? verb1 : verb2;
- obj1->location = obj2->location;
- obj2->location = tmp;
- if (verb != NULL) printf("You %s %s.\n", verb, obj->description);
- }
- void cannotBeOpened(void) { printf("That cannot be opened.\n"); }
- void cannotBeClosed(void) { printf("That cannot be closed.\n"); }
- void cannotBeLocked(void) { printf("That cannot be locked.\n"); }
- void cannotBeUnlocked(void) { printf("That cannot be unlocked.\n"); }
- void isAlreadyOpen(void) { printf("That is already open.\n"); }
- void isAlreadyClosed(void) { printf("That is already closed.\n"); }
- void isAlreadyLocked(void) { printf("That is already locked.\n"); }
- void isAlreadyUnlocked(void) { printf("That is already unlocked.\n"); }
- void isStillOpen(void) { printf("That is still open.\n"); }
- void isStillLocked(void) { printf("That is locked.\n"); }
- void toggleDoorToBackroom(void)
- {
- swapLocations(NULL, openDoorToCave, NULL, closedDoorToCave);
- swapLocations("close", openDoorToBackroom, "open", closedDoorToBackroom);
- }
- void toggleDoorToCave(void)
- {
- swapLocations(NULL, openDoorToBackroom, NULL, closedDoorToBackroom);
- swapLocations("close", openDoorToCave, "open", closedDoorToCave);
- }
- void toggleBox(void)
- {
- swapLocations("close", openBox, "open", closedBox);
- }
- void toggleBoxLock(void)
- {
- if (keyForBox->location == player)
- {
- swapLocations("lock", closedBox, "unlock", lockedBox);
- }
- else
- {
- printf("You don't have a key.\n");
- }
- }
|
As announced earlier, the implementations of the four commands
open, close, lock and unlock
are totally generic.
openclose.h |
- extern void executeOpen(const char *noun);
- extern void executeClose(const char *noun);
- extern void executeLock(const char *noun);
- extern void executeUnlock(const char *noun);
|
openclose.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "reach.h"
- void executeOpen(const char *noun)
- {
- OBJECT *obj = reachableObject("what you want to open", noun);
- if (obj != NULL) (*obj->open)();
- }
- void executeClose(const char *noun)
- {
- OBJECT *obj = reachableObject("what you want to close", noun);
- if (obj != NULL) (*obj->close)();
- }
- void executeLock(const char *noun)
- {
- OBJECT *obj = reachableObject("what you want to lock", noun);
- if (obj != NULL) (*obj->lock)();
- }
- void executeUnlock(const char *noun)
- {
- OBJECT *obj = reachableObject("what you want to unlock", noun);
- if (obj != NULL) (*obj->unlock)();
- }
|
Above, I used a generic function reachableObject
to handle objects that are not here;
see below for its implementation.
This way, we don’t have to write the same code four times
(once for every execute function).
More commands will be added in chapter 15;
these will benefit from the same function.
reach.h |
- extern OBJECT *reachableObject(const char *intention, const char *noun);
|
reach.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "misc.h"
- #include "noun.h"
- OBJECT *reachableObject(const char *intention, const char *noun)
- {
- OBJECT *obj = getVisible(intention, noun);
- switch (getDistance(player, obj))
- {
- case distSelf:
- printf("You should not be doing that to yourself.\n");
- break;
- case distHeldContained:
- case distHereContained:
- printf("You would have to get it from %s first.\n",
- obj->location->description);
- break;
- case distOverthere:
- printf("Too far away, move closer please.\n");
- break;
- case distNotHere:
- case distUnknownObject:
- // already handled by getVisible
- break;
- default:
- return obj;
- }
- return NULL;
- }
|
The necessary modifications to object.awk are very straightforward:
object.awk |
- BEGIN {
- count = 0;
- obj = "";
- if (pass == "c2") {
- print "\nstatic bool alwaysTrue(void) { return true; }";
- print "\nOBJECT objs[] = {";
- }
- }
- /^- / {
- outputRecord(",");
- obj = $2;
- prop["condition"] = "alwaysTrue";
- prop["description"] = "NULL";
- prop["tags"] = "";
- prop["location"] = "NULL";
- prop["destination"] = "NULL";
- prop["prospect"] = "";
- prop["details"] = "\"You see nothing special.\"";
- prop["contents"] = "\"You see\"";
- prop["textGo"] = "\"You can't get much closer than this.\"";
- prop["weight"] = "99";
- prop["capacity"] = "0";
- prop["health"] = "0";
- prop["open"] = "cannotBeOpened";
- prop["close"] = "cannotBeClosed";
- prop["lock"] = "cannotBeLocked";
- prop["unlock"] = "cannotBeUnlocked";
- }
- obj && /^[ \t]+[a-z]/ {
- name = $1;
- $1 = "";
- if (name in prop) {
- prop[name] = $0;
- if (/^[ \t]*\{/) {
- prop[name] = name count;
- if (pass == "c1") print "static bool " prop[name] "(void) " $0;
- }
- }
- else if (pass == "c2") {
- print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\"";
- }
- }
- !obj && pass == (/^#include/ ? "c1" : "h") {
- print;
- }
- END {
- outputRecord("\n};");
- if (pass == "h") {
- print "\n#define endOfObjs\t(objs + " count ")";
- print "\n#define validObject(obj)\t" \
- "((obj) != NULL && (*(obj)->condition)())";
- }
- }
- function outputRecord(separator)
- {
- if (obj) {
- if (pass == "h") {
- print "#define " obj "\t(objs + " count ")";
- }
- else if (pass == "c1") {
- print "static const char *tags" count "[] = {" prop["tags"] ", NULL};";
- }
- else if (pass == "c2") {
- print "\t{\t/* " count " = " obj " */";
- print "\t\t" prop["condition"] ",";
- print "\t\t" prop["description"] ",";
- print "\t\ttags" count ",";
- print "\t\t" prop["location"] ",";
- print "\t\t" prop["destination"] ",";
- print "\t\t" prop[prop["prospect"] ? "prospect" : "destination"] ",";
- print "\t\t" prop["details"] ",";
- print "\t\t" prop["contents"] ",";
- print "\t\t" prop["textGo"] ",";
- print "\t\t" prop["weight"] ",";
- print "\t\t" prop["capacity"] ",";
- print "\t\t" prop["health"] ",";
- print "\t\t" prop["open"] ",";
- print "\t\t" prop["close"] ",";
- print "\t\t" prop["lock"] ",";
- print "\t\t" prop["unlock"];
- print "\t}" separator;
- delete prop;
- }
- count++;
- }
- }
|
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
--> get coin
You pick up a silver coin.
--> give coin
You give a silver coin to a burly guard.
--> go cave
You walk into the cave.
You are in a little cave.
You see:
an exit to the west
solid rock all around
a closed door to the south
a tiny key
--> get key
You pick up a tiny key.
--> go south
The door is closed.
--> open door
You open a closed door to the south.
--> go south
You walk through the door into a backroom.
You are in a backroom.
You see:
solid rock all around
an open door to the north
a wooden box
--> unlock box
You unlock a wooden box.
--> open box
You open a wooden box.
--> look box
The box is open.
You see:
a gold coin
--> get gold
You get a gold coin from a wooden box.
--> quit
Bye!
|
The additions to parsexec.c are equally straightforward.
parsexec.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <string.h>
- #include "location.h"
- #include "inventory.h"
- #include "openclose.h"
- bool parseAndExecute(char *input)
- {
- char *verb = strtok(input, " \n");
- char *noun = strtok(NULL, "\n");
- if (verb != NULL)
- {
- if (strcmp(verb, "quit") == 0)
- {
- return false;
- }
- else if (strcmp(verb, "look") == 0)
- {
- executeLook(noun);
- }
- else if (strcmp(verb, "go") == 0)
- {
- executeGo(noun);
- }
- else if (strcmp(verb, "get") == 0)
- {
- executeGet(noun);
- }
- else if (strcmp(verb, "drop") == 0)
- {
- executeDrop(noun);
- }
- else if (strcmp(verb, "give") == 0)
- {
- executeGive(noun);
- }
- else if (strcmp(verb, "ask") == 0)
- {
- executeAsk(noun);
- }
- else if (strcmp(verb, "inventory") == 0)
- {
- executeInventory();
- }
- else if (strcmp(verb, "open") == 0)
- {
- executeOpen(noun);
- }
- else if (strcmp(verb, "close") == 0)
- {
- executeClose(noun);
- }
- else if (strcmp(verb, "lock") == 0)
- {
- executeLock(noun);
- }
- else if (strcmp(verb, "unlock") == 0)
- {
- executeUnlock(noun);
- }
- else
- {
- printf("I don't know how to '%s'.\n", verb);
- }
- }
- return true;
- }
|
Notes:
- You may have noticed that object.txt has almost doubled in size
in this chapter.
I can already promise you, this is only the beginning.
object.txt is our main source of game data;
once we seriously start adding locations, items and actors,
the number of lines could easily grow to many thousands.
- Function swapLocation
can be used for many other things than doors and locks.
In chapter 15, it will be used again, this time to turn the light on and off.
- As you can see in the sample output,
the player can get the gold from the box,
but he is unable to put it back in again!
Our parser cannot handle ‘complex’ commands like
put coin in box.
So in the next chapter, we will write a whole new parser;
the current two-line implementation badly needs replacement!
Next chapter: 13. The parser