Scott Adams Adventure Interpreter
Coding all of an adventure game's logic directly can take a long time and a lot of RAM. Writing an interpreter that reads from a game database solves both of those problems, because a few bytes of data can represent a whole series of actions, and you only have to write the interpreter once.
Most of what is described on this page applies generally to all Scott Adams adventure interpreters. Ideally all platforms would work the same way, but there may be subtle differences between them. Certain details may be specific to the Apple II implementation. In theory, the graphical "SAGA" versions are the same as the text-only versions, but there might be differences there as well.
I wasn't able to find a thorough, definitive source of information about the game database contents. The best pieces of documentation are the Definition file from the ScottFree interpreter by Alan Cox, and the Manual for the Scott Adams Adventure Compiler (SAC). The Definition file was written for the TRS-80 data format, so it's not a perfect match, but the various elements are all present.
Overview
The game database defines verbs, nouns, rooms, items, and actions. The interpreter parses what the player types on the command line into a verb and optional noun, and then lets the game's action list drive what happens next. There are a few built-in concepts, like a light source with limited fuel and a room where treasures are stored, but most of what happens is defined by the database.
For example, if you type "SAVE" to save the game, you might expect the interpreter to intercept the command. It does not. If the game database does not include an explicit handler for SAVE, there will be no way to save the game. This allows the game to enhance or override things that seem to be built-in commands, e.g. Ghost Town provides special handling for the SCORE command to award bonus points.
The only exception to this is basic movement. The interpreter handles moving in the six directions (north south east west up down) internally, based on a list of exits defined for each room.
Verbs and Nouns
The database specifies the number of significant characters in a word, usually 3, 4, or 5. When looking for a match, only that many letters are considered, so "TAKE TABL" and "TAKESHI TABLEAU" will have the same effect if the word length is 4. Words may have aliases, so e.g. GET and TAKE do exactly the same thing. These are defined by prefixing the alias with an asterisk.
The original database format interleaved verbs and nouns, so every game has exactly the same number of each. On the Apple II they're split into two separate lists, one of which may be padded out.
A few verbs are hard-coded into the interpreter. Verb #1 is GO, verb #10 is TAKE, and verb #18 is DROP. Nouns 1-6 are also hard-coded: NORTH, SOUTH, EAST, WEST, UP, and DOWN. Verb 0 ("AUTO") has a special meaning internally, as does noun 0 ("ANY").
Rooms
The database defines a collection of rooms, each of which has a text description and a set of six exits to other rooms. The last room defined is special: if the player dies, they get sent there, and can be in a recoverable "limbo" room or just plain game over.
Room #0 is "nowhere", and holds items that are not currently in the game.
The database specifies the player's starting room, and in which room treasures must be placed (if any). The interpreter keeps track of which room the player is in, and provides various mechanisms for moving the player around. There is a "default" alternate room that the player can swap between, as well as a mechanism for swapping between multiple locations that seems to be broken on the Apple II.
Items
Everything you can interact with in the game is in the "item" list, including not only tools and treasures but also kings and palaces. Treasures start and end with an asterisk. Anything you can interact with will also have an entry in the noun table, but there's no direct connection between the two tables.
The item name strings may end with a word that starts and ends with
slashes, e.g. "Shovel/SHOV/",$1f
. This tells the interpreter
to define a simple action for taking or dropping the item.
(More on this later.)
Every item is located in a room. Room zero is used for items that don't exist, and room -1 means it's in the player's inventory.
There is only one hard-coded item: object 9 is the light source (torch, lamp, etc).
Globals
In addition to tracking the room locations for each item, the game manages a set of 32 boolean flags and a set of nine 16-bit counters. Counter #8 holds the amount of fuel remaining in the light source object, which is initialized to a game-specific value, and updated automatically as the player moves. Flag #15 is set by the game when the room is dark, and flag #16 is set by the interpreter when the lamp runs out of fuel.
If the room is dark, and the light source object is not held by the player or sitting in the room, the room description is replaced with a "too dark to see" message, and moving in a direction without an exit is fatal.
Curiously, the light source object always provides light, regardless of fuel level. It's up to the game to notice when the lamp runs out and swap item #9 ("lit lamp") for some other item ("unlit lamp").
It's worth noting that the counters don't work the way you might want (and the way some documentation claims). Ideally the interpreter would allow you to select a counter and modify or test it. In practice it works like a register that can be swapped with RAM addresses. If you want to examine the contents of a counter, you have to swap it into the register, examine it, and swap it back out after.
Actions
The heart of the game is the "actions" system. There are two kinds of action, one that responds to a specific verb or verb/noun pair, and one that has a percent chance of firing. The latter are referred to as "occurrences".
Each action has a series of boolean conditions, all of which must be true for the action to be accepted. For example, if you want to unlock a door, you must be in the same room as the "locked door" item and be holding the key. If you're in the wrong room, or don't have the key, or the door has been replaced by the "unlocked door" item, then the action won't fire.
When the user types a command, the interpreter starts by checking to see if it matches one of the built-in movement commands. If it doesn't, it scans the list of actions, looking for entries that have a matching verb or verb/noun pair. When it finds a match, it tests the conditions. If they match, the action's operations are performed, and the scan ends. If they don't match, the scan continues with the next action. Since only the first successful action is executed, it's possible to have a series of specific tests followed by a generic handler.
Once actions are processed, the interpreter does a second pass for occurrences. These are not tied to the command typed, and every occurrence with successful conditions is executed, not just the first. This is how many of the game mechanics are implemented, e.g. there's a percent chance that your scorpion stings may turn into festering scorpion stings on every turn.
Each action/occurrence can have up to five conditions and four operations. If an action is more complex, there is a "continue" operation that causes processing to continue into the next action slot.
As mentioned earlier, the item name syntax defines a mechanism for
defining implicit TAKE and DROP actions. Items the player
can carry have a word appended to the end between slashes, e.g.
"Shovel/SHOV/",$1f
.
If the player issues a TAKE or DROP action, and the noun is recognized but
there's no matching action for it, the game searches the item name table for
a matching word between slashes. If it finds a match, it performs the action on
the item number specified at the end of the string ($1f in this example).
Conditions
Names come from the SAC manual.
Opcode | Name | Arg | Description |
---|---|---|---|
$00 | (no-op) | Used for empty entries. | |
$01 | carried | item# | True if the player is carrying the specified item. |
$02 | here | item# | True if the specified item is in the current room. |
$03 | accessible | item# | True if the item is being carried by the player, or is in the current room (carried OR here). |
$04 | at | room# | True if the player is in the specified room. |
$05 | !here | item# | True if the item is NOT in the player's current room. |
$06 | !carried | item# | True if the player is NOT carrying the item. |
$07 | !at | room# | True if the player is NOT in the specified room. |
$08 | flag | flag# | True if the specified flag is true. |
$09 | !flag | flag# | True if the specified flag is NOT true. |
$0a | loaded | - | True if the player is carrying at least one item. |
$0b | !loaded | - | True if the player is not carrying any items. |
$0c | !accessible | item# | True if the item is NOT being carried by the player and is NOT in the current room (!carried AND !here). |
$0d | exists | item# | True if the item is in the game (not in room zero). |
$0e | !exists | item# | True if the item is NOT in the game (is in room zero). |
$0f | counter_le | value | True if the current counter's value is <= the argument. |
$10 | counter_gt | value | True if the current counter's value is > the argument. (This is incorrectly listed as _ge in SAC manual.) |
$11 | !moved | item# | True if the item is in its original location. |
$12 | moved | item# | True if the item is NOT in its original location. |
$13 | counter_eq | value | True if the current counter's value is equal to the argument. |
Opcodes and arguments are encoded in a single 16-bit value, where the opcode is (value % 20), and the argument is (value / 20).
Operations
Names come from the SAC manual (which calls these "results").
Opcode | Name | Arg | Description |
---|---|---|---|
$00 | (no-op) | - | Does nothing. |
$01-33 | msg | - | Prints message N-1 (0-50). |
$34 | get | item# | Picks up the item and puts it in the player's inventory, unless the inventory is full. |
$35 | drop | item# | Puts the specified item in the player's current location. In addition to dropping items from the player's inventory, this is also commonly used to move items from "nowhere" (room 0) to the current room. ("put_current" might be a better name.) |
$36 | moveto | room# | Moves player to the specified room, and redraws the room description. |
$37 | destroy | item# | Moves the item to room 0, effectively removing it from the game. |
$38 | set_dark | - | Sets the darkness flag (#15). |
$39 | clear_dark | - | Clears the darkness flag (#15). |
$3a | set_flag | flag# | Sets the specified flag to True. |
$3b | destroy2 | item# | (same as $37) |
$3c | clear_flag | flag# | Sets the specified flag to False. |
$3d | die | - | Announce death, clear darkness flag, and move to last defined ("limbo") room. |
$3e | put | item#, room# | Puts the specified item in the specified room. |
$3f | game_over | - | Ends game, giving option to restart or exit. |
$40 | look | - | Redisplays description of current room, obvious exits, and visible items. |
$41 | score | - | Prints the current score as N/100, based on the number of treasures currently stored in the treasure collection room. If all treasures have been collected, announces victory and ends the game. |
$42 | inventory | - | Displays player's inventory. |
$43 | set_0 | - | Sets flag 0 to True. |
$44 | clear_0 | - | Sets flag 0 to False. |
$45 | refill_lamp | - | Refills the lightsource item, returning it to its original full capacity. |
$46 | clear_screen | - | Clears the screen. (Does nothing on Apple II.) |
$47 | save_game | - | Initiates save-game dialog. |
$48 | swap | item#, item# | Swaps the locations of the two items. Handy when an item changes state, e.g. a locked door becomes an unlocked door. |
$49 | continue | - | Indicates that the current action continues in the next slot; used for complex actions. |
$4a | superget | item# | Picks up the specified item, regardless of inventory count. Useful for status effects, e.g. festering scorpion bites. |
$4b | put_with | item#, item# | Puts the first item into the same location as the second item. |
$4c | look2 | - | (same as $40) |
$4d | decrease_counter | - | Decrements the current counter. (Most of the docs and interpreter implementations say it will not reduce the value below zero. In practice it decrements freely, but the comparison operators treat the value as unsigned.) |
$4e | print_counter | - | Prints the value of the current counter (0-99). |
$4f | set_counter | value | Sets the current counter's value to the argument. |
$50 | swap_loc_default | - | Swaps the player's location with the "default" alternate location. The alternate location is initially undefined. |
$51 | select_counter | counter# | (This is misnamed, should be "swap_counter".) Swaps the current counter's value with the value in the specified storage slot (0-8). |
$52 | add_counter | value | Increases the counter by the specified value. |
$53 | subtract_counter | value | Decreases the counter by the specified value. |
$54 | print_noun | - | Prints the noun that the player typed. |
$55 | print_noun_nl | - | Prints the noun the player typed, and adds a newline. |
$56 | nl | - | Prints a newline. |
$57 | swap_loc | loc# | Swaps the player's location with the specified alternate location. (I think this is broken on the Apple II.) |
$58 | pause | - | Waits for 0.5 seconds. This seems to vary by implementation. Some docs say it pauses for 2 seconds. |
$59 | draw | pic# | For SAGA games: draws picture N. |
$5a-65 | - | - | Unused. |
$66-96 | msg | - | Prints message N-51 (51-99). |
The opcodes are stored in tables. The arguments are actually stored in the condition table, as arguments to conditions with opcode $00.
Saved Games
The Apple II interpreter saves game state in two files, named
AREC1?
and AREC2?
, where the '?' is replaced
with a character whose ASCII value is the adventure number.
Game data is copied to a 256-byte buffer, which is written to disk or tape.
The first file holds the various bits of global state: counters, flags, and
current/alternate rooms. The second file just holds the
room location data for all items.