Adventure style games are very popular in computing circles, and I'm going to develop a small one here. Because of space I will have to leave out many of the advanced features of most adventure games, but it will give you an idea of how it looks in ABC. And of course it will be obvious how the bells and whistles can quickly be added on.
As I'm sure you know, a (textual) adventure program works by describing a scene. You then give instructions on where to go, or what to do, and it responds by telling you what happened as a result. For instance, if it says
You are standing by a building at the end of a road.
A spring flows from the building.
and you reply
> enter buildingit might reply
You are inside a building, a well house for a spring.
There is a bottle here.
There are some keys here.
after which the dialogue might proceed as follows:
> take keys
> leave the building
Please use 1 or 2 word sentences.
> leave
You are outside the building.
> go west
You are standing by a stream.
> go south
You are at a small slit that the stream runs down.
A dry river bed carries on ahead.
> go down
You don't seem to be able to go that way.
> south
You have found a metal grate fixed into the ground.
> down
Sorry, you can't do that.
> open grate
The grate is open.
You are at a hole in the ground.
There is a metal grate lying on the ground.
> down
You are in a dim chamber.
A hole in the ceiling shows the sky above.
and so on.
The main program making up this adventure looks like this:
HOW TO ADVENTURE:
START
GET command
WHILE command <> "quit":
OBEY command
GET command
FINISH
START will initialise some variables, like the place where the player
is, and what the player is holding. FINISH will print out the score
and so on. GET will print the prompt, read a line, strip off spaces,
and reduce it to lower-case:
HOW TO GET command:
GET LINE
WHILE command = "": GET LINE
GET LINE:
WRITE "> "
READ command RAW
PUT lower stripped command IN command
OBEY has to split a command into its constituent words, and then decide what
action needs to be taken for that command:
HOW TO OBEY command:
SPLIT command INTO verb AND object
SELECT:
verb = "": PASS
special command: TRY TO MOVE command
verb = "move": TRY TO MOVE object
verb = "take": TRY TO TAKE object
verb = "drop": TRY TO DROP object
verb = "kill": TRY TO KILL object
verb = "what": INVENTORY
ELSE: CAN'T DO verb, object
SPLIT does what its name suggests: splits the command into its constituent
words, and makes sure it only consists of one or two words:
HOW TO SPLIT command INTO verb AND object:
PUT split command IN words
SELECT:
#words = 1: PUT words item 1, "" IN verb, object
#words = 2: PUT words item 1, words item 2 IN verb, object
ELSE:
WRITE "Please use 1 or 2 word sentences." /
PUT "", "" IN verb, object
A nice feature is to allow synonyms for commands, to allow "go west" and
"proceed west" and "move west" all to mean the same thing. We can do that
by having a table of synonyms:
>>> WRITE synonyms["move"]
{"go"; "proceed"}
and then adding in SPLIT:
SHARE synonyms
...
IF SOME word IN keys synonyms HAS verb in synonyms[word]:
PUT word IN verb
In this adventure, each place has a name, which is a short description you get each time you visit it after the first time ("You are x", such as "outside the building" above). Then, each place has a long description used for describing it the first time you go there. Such a description is stored as a table of lines, for instance
>>> WRITE description["inside large hall"]
{[1]: "This is a large hall."; [2]: "There is an exit to the west."}
To display such a message neatly, we can define the following command:
HOW TO DISPLAY message:
FOR line IN message:
WRITE line /
>>> DISPLAY description["inside large hall"]
This is a large hall.
There is an exit to the west.
Then there is a map of all locations, which gives for each location a table of
directions that the player can go in, and where that direction leads to.
>>> WRITE map["inside the building"]
{["out"]: "outside the building"}
>>> WRITE map["outside the building"]
{["in"]: "inside the building"; ["south"]: "standing by the stream";
["west"]: "in the forest"}
We can play a nasty trick on the player:
>>> WRITE map["in the forest"]
{["east"]: "in the forest"; ["north"]: "in the forest";
["south"]: "in the forest"; ["west"]: "standing by the stream"}
Moving is attempted by means of the command TRY TO MOVE. All commands beginning
TRY TO first check that the conditions for the action are acceptable, and only
then do the action. The current location is held in place: TRY TO MOVE
checks that the direction asked for is in the map for the current place:
HOW TO TRY TO MOVE direction:
SHARE map, place
SELECT:
direction = "":
WRITE "Where to?" /
direction in keys map[place]:
MOVE TO map[place][direction]
ELSE:
WRITE "You don't seem to be able to go that way" /
MOVE TO does the actual moving. For now here is a simple version, but it will
get more involved later.
HOW TO MOVE TO there:
SHARE place
PUT there IN place
DESCRIBE place
(DESCRIBE describes a place and the objects to be found there; you'll see it
shortly.)
In OBEY, you will have noticed the lines
SELECT:
special command: TRY TO MOVE command
This is to allow the player to say south instead of go south,
by seeing if the command is already in the map for the current place:
HOW TO REPORT special command:
SHARE map, place
REPORT command in keys map[place]
Notice that it also allows you to use commands instead of directions in the
map. For instance, when at the grate, you can open the grate by having two
places, an open grate and a closed grate:
>>> WRITE map["at closed grate"]
{["north"]: "at slit"; ["open grate"]: "at open grate"}
Different objects are left lying about at various places. These are recorded in a table objects. Just as with places, each object has a simple name, to be used when the player wants to know what is being carried, and a longer description when an object is first found.
>>> WRITE objects["inside the building"]
{"bottle"; "keys"}
>>> WRITE description["keys"]
There are some keys here.
Now I can show you DESCRIBE. It remembers which places have already been
described (and therefore visited), and only gives the long description the
first time:
HOW TO DESCRIBE place:
SHARE description, objects, visited
SELECT:
place in visited:
WRITE "You are ", place /
ELSE:
DISPLAY description[place]
INSERT place IN visited
FOR object IN objects for place:
DISPLAY description[object]
Notice here the line "FOR object IN objects for place:". Not every place may
be recorded in the objects table, so it is a shorthand to save repeated checks
to see if it is:
HOW TO RETURN property for thing:
SELECT:
thing in keys property: RETURN property[thing]
ELSE: RETURN {}
You'll find it used again later on.
Then there is a list of what the player is carrying, called holding, which is initially empty. To find out what is being carried, the player can ask for an inventory:
HOW TO INVENTORY:
SHARE holding
SELECT:
holding = {}:
WRITE "You aren't carrying anything" /
ELSE:
WRITE "You are carrying: "
LIST holding
This uses a useful command to neatly print a list of objects:
HOW TO LIST things:
PUT "" IN separator
FOR object IN things:
WRITE separator, object
PUT ", " IN separator
WRITE /
>>> LIST objects["inside the building"]
bottle, keys
Another useful tool is a test to see if an object is currently being carried:
HOW TO REPORT carrying object:
SHARE holding
REPORT object in holding
and another to test if an object is present:
HOW TO REPORT present object:
SHARE objects, place
REPORT object in objects for place
TRY TO TAKE can now check that the object is present, that it's not already
being carried and so on, before actually taking it:
HOW TO TRY TO TAKE object:
SHARE holding
SELECT:
object = "":
WRITE "Which object?" /
carrying object:
WRITE "You're already carrying it!" /
NOT present object:
WRITE "I see no `object`." /
#holding > 6:
WRITE "You can't carry any more." /
ELSE:
TAKE object
TAKE looks like this, again a simple version for now:
HOW TO TAKE object:
SHARE holding, objects, place
REMOVE object FROM objects[place]
INSERT object IN holding
TRY TO DROP is similar:
HOW TO TRY TO DROP object:
SELECT:
object = "": WRITE "Which object?" /
NOT carrying object: WRITE "You're not holding it!" /
ELSE: DROP object
HOW TO DROP object:
SHARE holding, objects, place
REMOVE object FROM holding
INCLUDE object IN objects FOR place
The command INCLUDE adds an item to a table:
HOW TO INCLUDE object IN property FOR thing:
IF thing not.in keys property:
PUT {} IN property[thing]
INSERT object IN property[thing]
One of the tricks of adventure games is that certain actions are not possible unless you are at a certain place, or you are carrying a certain thing, and some actions have unexpected side-effects.
For instance, you shouldn't be able to open the grate if you aren't carrying the keys. So we can alter MOVE TO to check for this:
HOW TO MOVE TO there:
SHARE place
SELECT:
opening.grate AND NOT carrying "keys":
WRITE "I don't seem able to open the grate" /
ELSE:
PUT there IN place
DESCRIBE place
opening.grate:
REPORT (place, there) = ("at closed grate", "at open grate")
Similarly, somewhere in the cave there is a bird, but you can only catch it if
you're carrying the cage. Furthermore, the jangling of the keys frightens it.
So we can alter TAKE to do this:
HOW TO TAKE object:
SHARE holding, objects, place
SELECT:
object = "bird" AND carrying "keys":
WRITE "The bird flutters off in fright." /
object = "bird" AND NOT carrying "cage":
WRITE "You don't seem able to catch the bird." /
ELSE:
REMOVE object FROM objects[place]
INSERT object IN holding
An example of a side-effect is that dropping the bird is the only way to scare
off the snake (should you meet it):
HOW TO DROP object:
SHARE holding, objects, place
IF object = "bird" AND present "snake":
WRITE "With a great flurry the bird attacks the snake." /
WRITE "The snake flees into the darkness." /
REMOVE "snake" FROM objects[place]
REMOVE object FROM holding
INCLUDE object IN objects FOR place
(Obviously, TAKE should also be changed to prevent you from trying to take the
snake.)
HOW TO TRY TO KILL object:
SELECT:
object = "":
WRITE "Which object?" /
(NOT present object) AND (NOT carrying object):
WRITE "I see no `object`" /
ELSE:
KILL object
HOW TO KILL object:
SHARE holding, objects, place
SELECT:
object = "bird":
WRITE "How cruel! The poor bird dies with a mournful peep." /
ELIMINATE
INCLUDE "dead bird" IN objects FOR place
object = "snake":
WRITE "Attacking the snake is both dangerous and ineffective." /
ELSE: \ It's not a living creature
WRITE "It's already dead!" /
ELIMINATE:
SELECT:
carrying object: REMOVE object FROM holding
present object: REMOVE object FROM objects[place]
Well, that's the body of the adventure. Of course, lots of extra places, objects, beings and commands must be added, but that's just a case of more of the same.
In OBEY, if it can't obey your command, it invokes CAN'T DO. As a nicety this prints funny remarks for certain commands. For instance, if you're at the stream, you might try "swim":
>>> DISPLAY funny["swim"]
The water would get into my circuits.
HOW TO CAN'T DO verb:
SHARE funny
SELECT:
verb in keys funny: DISPLAY funny[verb]
ELSE:
WRITE "Sorry, you can't do that" /
As a final touch, you might want to add the commands "save" and "restore"
to OBEY, so you can save a game, and come back later to it (or so you can try
something, and if it fails restore it and try something else).
This is remarkably easy. Since the state of the game is reflected by a small number of variables, you can just put them in another variable:
HOW TO SAVE:
SHARE saved, holding, objects, place
PUT holding, objects, place IN saved
HOW TO RESTORE:
SHARE saved, holding, objects, place
PUT saved IN holding, objects, place
DESCRIBE place
Copyright © Steven Pemberton, CWI, Amsterdam, 1991