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 FINISHSTART 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 commandOBEY 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, objectSPLIT 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, objectA 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 commandThis 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 holdingThis 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, keysAnother useful tool is a test to see if an object is currently being carried:
HOW TO REPORT carrying object: SHARE holding REPORT object in holdingand another to test if an object is present:
HOW TO REPORT present object: SHARE objects, place REPORT object in objects for placeTRY 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 objectTAKE 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 holdingTRY 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 placeThe 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 holdingAn 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 placeCopyright © Steven Pemberton, CWI, Amsterdam, 1991