The shape of it
The Hobbit is a small program driving a large database. Of the 40000 bytes, most are data: a dictionary, 59 sentence patterns, 61 object and character records, 80 room records, 177 messages, 22 line-drawn pictures and the characters' scripts. The code between them is a parser, an interpreter for each of those formats, and one routine that carries out any action for anyone.
That last point is the game's central idea. The player and the other characters are the same kind of thing -- a record in
OBJECT_INDEX -- and they act through the same code. When Thorin opens a door, his script puts the action code for OPEN and the door's number into
ACTION and
TARGET, exactly where the parser puts them when you type OPEN DOOR, and
DO_ACTION does the rest. Doors, keys, rivers and ropes behave the same whoever uses them, because there is only one copy of the rules.
A turn, in outline:
- Read a line (READ_LINE), and turn its words into tokens (TOKENISE).
- Parse it into one or more commands, each in a 24-byte frame (PARSE_COMMAND).
- For each command, find the sentence pattern it fits, which gives the action code (MATCH_PATTERN), and find the objects its names mean, trying each candidate until one works (MATCH_AND_TRY).
- Tell it and do it: narrate the action (NARRATE_ACTION) and carry it out (DO_ACTION).
- The world's turn (END_OF_TURN): has the player won; every other character acts (CHARACTERS_ACT); the timers count down.
OBEY is where steps 3 to 5 happen, once for every command on the line.
Reading a line
READ_LINE prints the prompt and takes keys into
INPUT_LINE: letters, space, quote, comma and full stop, with backspace to correct. Three things are more than a line editor:
- The game types for you. GET_KEY counts down while it waits, and if nothing is pressed in time it types WAIT and presses ENTER itself -- which is why WAIT appears on screen when nobody typed it, and why the world goes on without you. The time allowed, PATIENCE, is 3000 keyboard scans at every prompt -- about 23 seconds -- and each key leaves what was left plus 500 scans, never more than 3000; pause mid-line for longer than that and the half-typed line is thrown away for the WAIT.
- One-key moves. On an empty line the cursor keys 5, 6, 7 and 8 are W, S, N and E, and each is a whole command (ONE_KEY_MOVES).
- @ on an empty line runs the last command again (REPEAT_LAST).
The first turn is not typed at all: the main loop copies LOOK into the line from
LOOK_COMMAND, which is how the game opens with a description.
Words
The dictionary at
WORD_LIST packs each word one letter to a byte, as a 5-bit code -- A is 1, which is the low five bits of ASCII 'A', so MATCH_WORD gets the code with a single AND $1F. Bit 7 marks the last letter. Bits 5 and 6 of the first two bytes together are the word's class: verb, noun, adjective, adverb, preposition, article, AND, THEN and so on. A 26-entry index at
WORD_INDEX gives the first word for each initial letter, and a word ends its letter's run only by having a different initial -- the list is grouped, not sorted.
TOKENISE turns each typed word into a two-byte token: its class in the top nibble and its 12-bit offset in the dictionary below. Synonyms come out as the word they stand for, so GET SWORD is TAKE SWORD before anything else sees it. Words may be shortened -- EXAM is EXAMINE, INV is INVENTORY -- and lengthened a little: SWORDS is SWORD, but only for entries of four letters or more, so INT is INTO and not IN. A word not in the dictionary stops the line with "i do not know the word ...".
The words the game prints are a second list,
SECOND_LIST, packed the same way but reached by no index: a message names a word by its 12-bit offset from
WORD_INDEX, so nothing ever holds the list's own address. The parser's list and the printer's list divide by direction -- the one you type into, the one the story is written from.
Parsing a sentence
The parser is a state machine over the tokens. The class of each token picks a handler from
PARSER_CLASSES, through JP (HL), and register E carries the state: whether a verb may come next, an adverb, an article, whether the first or second noun phrase is still empty. A word where it is not allowed gets "what ?" or the like (
NOT_ALLOWED_HERE).
What it builds is a
frame of 24 bytes (
COMMAND_FRAME): the verb, an adverb or direction, and two noun phrases of ten bytes each -- two prepositions, the noun and two adjectives -- every word as its two-byte reference, articles dropped. VICIOUSLY ATTACK THE TROLL WITH THE SWORD becomes ATTACK, VICIOUSLY, TROLL, and WITH SWORD. And a noun phrase is laid out exactly as an object's name is in its record, so deciding what the player means is a byte comparison (
NAME_MATCHES): the noun must match, the adjectives are optional and may come in either order, but a wrong one rules the object out.
A line can hold several commands, each in its own frame below the first (
FRAMES):
- THEN and a full stop end one command and start the next.
- AND joins noun phrases, but if a verb follows it, it becomes THEN: TAKE THE MAP AND DROP IT is two commands. PARSE_AND keeps a checkpoint (AND_TOKENS) to go back to when that happens.
- ALL sets a bit on the verb, and ALL ... EXCEPT puts the exceptions in frames of their own that the command skips (WORD_ALL, WORD_EXCEPT).
- IT is the last target's name, kept at IT_NAME, put back as if it had been typed.
- Quotes make an order: SAY TO THORIN "GO EAST" parses GO EAST in frames marked as said to someone (IS_ORDER), which DO_TALK later hands to the character.
- Special words -- SAVE, LOAD, QUIT, PAUSE, HELP, SCORE, PRINT, NOPRINT -- are handled on the spot by PARSE_SPECIAL from SPECIAL_WORDS, and never become actions.
When the game has to ask "which ... ?", it keeps the unfinished sentence (
KEEP_QUESTION), and the next line's answer is fitted into it rather than parsed as a new command.
From a sentence to an action
The game has no table of verbs. It has
ACTION_PATTERNS: 59 sentence shapes of eight bytes each, a verb, a particle and a preposition as word references plus flags -- TAKE, TAKE OFF, TAKE OUT OF, PUT IN, PUT ON, GIVE TO.
An action code is simply a pattern's place in that list, counting from 1, and that code is the key to everything else:
ACTION_TABLE, the objects' own handlers, the characters' reactions and their scripts. The first ten are the directions, each with GO as its last word, so NORTH and GO NORTH are one action.
MATCH_PATTERN gathers a probe of four words -- the verb, then a particle and a preposition picked from the noun phrases -- and searches the patterns with the same name comparison the objects use, so word order does not matter. A verb that fits no pattern just passes the time: "you ... . time passes...".
Which phrase is the target and which the instrument is decided by the prepositions, not by the order they were typed in (
ASSIGN_PHRASES): the phrase that has the preposition the pattern expects is the one it belongs to, wherever it came in the sentence. The pattern's flags then set its options (
PATTERN_OPTIONS): whether the action needs light, and whether an object is a place rather than a thing, as for ENTER.
Finding the objects: try, and see what works
Names are not resolved to objects up front.
MATCH_AND_TRY walks every object whose name fits the target -- within the actor's reach (
IN_REACH) -- and for each, every object that fits the instrument, and
tries the action on each pair until one would work. When two things share a name, the one used is the first the action works on.
Trying is a real run of the action with one flag down.
DOING_IT clear means "only a test": every handler checks the flag before it changes anything, and instead answers in
SUCCEEDED whether it would have worked (
FOR_REAL). So there is one copy of each rule, used both to decide and to do.
If nothing works,
TARGET_TROUBLE decides what to say. One thing fitted: it is done for real, and its own refusal is printed -- "the door is locked.". Several fitted and none worked: "which ... ?" (
ASK_WHICH). None fitted: the name is looked for everywhere, to say either "i do not see the ... here" or why the thing that is somewhere else will not do. A refused command costs no time:
OBEY goes on to the next command without the world's turn.
Telling the story
The game never stores a finished sentence about an action.
NARRATE_ACTION builds one from the pattern: who, "cannot" if it was refused, the verb, the first object after its particle and the second after its preposition. The same code tells you what you did and what Thorin did, which is why the other characters' doings read so uniformly.
Everything else it says is a message in a small bytecode run by
RUN_MESSAGE: a byte with bit 7 set starts a two-byte word reference, $60-$7F is one of 32 common words, $20-$5F a literal character, and below $20 a control code (
CONTROL_CODES). The control codes print the actor's name, IS or ARE, HIS or YOUR, the target, the instrument -- so one message serves the whole cast: "you are not carrying it." and "Gandalf is not carrying it." are the same bytes.
Verbs agree with their subject. A word reference carries flags, and
PRINT_WORD adds a word's ending when the sentence is about someone other than the player --
ACTING is zero for the player -- from
ENDINGS: -s, -es, -ies, and a backspace then -ies, which is how CARRY becomes CARRIES.
What other characters do is only printed when the player can see it. In the dark, the first thing each turn is heard instead: "you hear a noise.".
The world's turn
After every command that was carried out,
END_OF_TURN runs the rest of the world:
- Has the player won? (CHECK_WON) -- the valuable treasure in the wooden chest.
- Every other character acts (CHARACTERS_ACT).
- The timers count down (TIMERS). Ten of them, each with a length, a count, a routine for when it runs out, and a warning routine for the turns before: the bog that swallows you if you stay in it, the web that smothers you, the wine that wears off, the magic door that opens and shuts, the hole in the mountain's side. Only one timer fires in a turn; a second is held back a turn, so two events never land at once.
Many timers are started by arriving somewhere:
ARRIVAL_HOOKS is a table of routines keyed by location that
MOVE runs when the player gets there. That is how a place can be deadly only after a few turns in it.
How the characters decide what to do
Each character has a slot in
CHARACTERS: who it is, where its script has got to, its
script table, how many of its scripts it wanders among, and how many orders it will take. The scripts themselves -- decoded step by step on the
Characters page -- are a small instruction set (
CHARACTERS_ACT):
- An action, with or without objects: TAKE the chest, GO NORTH, ATTACK the player, RUN. The step is tried exactly as a typed command would be -- tested, then done (ACTOR_TRIES).
- A routine instead of an action, run once as a test and again for real if it would work (SCRIPT_DO).
- A pause: nothing this turn.
- Go to another step, or switch at random to one of the character's ordinary scripts (SCRIPT_RANDOM). This is how Gandalf and Thorin wander: their ordinary scripts are one list of moves entered at different places, and a random switch now and then sends them off somewhere new.
Every step can carry a
fallback: where to go on if it is refused. A step that fails moves on, a step that succeeds ends the character's turn, and six failures in a row end it too -- so a character keeps trying things until something works, one success per turn.
Reactions. A script table's entries keyed 0 are the ordinary scripts; the rest are keyed by action codes. When something is done to a character -- given something, attacked, captured --
REACT looks the action up in its table and, if it has a script for it, switches to it. So giving Thorin something, or hitting him, changes what he does next.
Orders. SAY TO THORIN "GO EAST" is parsed into frames of its own and handed over by
DO_TALK, which decides how many sentences the character will accept: a random number up to its limit -- Thorin 6, Gandalf and Elrond 5, Gollum 3, the wood elf and the trolls 1, the warg and the goblins never. The orders wait in
ORDERS, and each turn one replaces the character's script step, unless the step it is on is marked as one an order cannot interrupt. That, together with the random limit, is why Thorin sometimes does what he is told and sometimes does not.
Captivity. A character held by something closed can do nothing; held by something open, it tries to CLIMB OUT OF it (
CAPTIVE).
Leaving the story. A character that is killed gives its slot up (
KILL), and its script never runs again. (A step can also be marked to be used only once: when it works,
SCRIPT_DO zeroes the step's first byte. Only Thorin's remark about the small curious key is marked so; he goes on as before.) Three slots start empty -- the butler's, Bard's and the dragon's -- and are filled by arrival hooks when the story reaches them.
Moving about
MOVE moves whoever is acting, the player included. A room's record is a 10-byte head -- lit or dark, visited, capacity, name, description -- followed by its exits, three bytes each: a direction, an object the way goes through (a door, a gate, a river), and where it leads (
FIND_EXIT).
A way through an object is shut unless the object is open or broken; the actor, with everything it carries, must fit through it; and the room it goes into must have space (
CAN_PASS). Nearly every room has no limit, but some are small.
The first time the player reaches a place, it is described in full with its picture, and the score goes up if the place is worth points (
VISIT_SCORES, 750 between fourteen places). Coming back gives only the name and what is there (
DESCRIBE_BRIEFLY).
One road in five is shut at the start of each game (
HIDDEN_ROADS); it opens again when Elrond reads the curious map and tells you the way.
Light and dark
Twenty-six rooms are dark: the trolls' cave, the goblins' dungeon and its passages, Gollum's lake, the Elvenking's halls and the passage into the mountain. Only the player is ever in the dark -- the characters see everywhere (
TOO_DARK). The one light in the game is the short strong sword, which glows from the start: within reach -- carried, or just lying in the same room -- it lights every dark place. The torch has the same flags, but only the sword is tested. And a player shut inside something closed is in the dark whatever the room: close the barrel over yourself without the sword and it is dark inside, and OPEN, which needs light, is refused.
In the dark, a move goes in a random direction, and a way that is not there is a fall: each halves the player's strength, and the seventh is fatal. Actions that need light are refused, the picture is blacked out, and the other characters are heard rather than seen.
Fighting and breaking things
Every object and character has a strength and a defence, bytes 5 and 6 of its record.
DO_ATTACK sets the attacker's strength, plus the weapon's -- bare hands are a FIST -- against the target's defence, each jostled by
JOSTLE. That was meant to be a random -10 to +10, but in practice it is a random 0 to +10, and about one time in 25 the blow or the guard comes out as 0 instead: see
Bugs. A blow no stronger than the defence is wasted. One more than 16 stronger kills outright -- so a guard of 0 loses to any blow over 16. Anything between picks a message from a table of wounds by how much stronger it was (
WOUNDS) and wears the target's strength and defence down, so a long fight goes to whoever keeps landing blows. The wearing down is less even than that sounds: see
Bugs.
Characters have sides (byte 4), and no one attacks their own -- except the player, who can turn on a friend, who is an enemy from then on (
SAME_SIDE).
Things break the same way (
DO_STRIKE): the blow against the thing's defence, and then the weapon against the thing, so striking something hard can break the weapon too.
Chance
RANDOM mixes its last result -- seeded from the R register at the start -- with bytes read through a pointer that steps through memory, and scales the result into a range. It is uneven: measured over 3000 calls, asking for 0 to 4 brings up 0 and 4 about half as often as the numbers between.
Each new game makes its own choices (
NEW_GAME_CHOICES): which road is shut, and which of Gollum's two riddles he will ask (
RIDDLES). Characters wander at random, take a random number of orders, and every blow in a fight has a random part. That, and the characters moving on their own, is why no two games play alike.
Winning, dying and starting again
The game is won at the end of a turn in which the valuable treasure is in the wooden chest (
CHECK_WON). Dying (
PLAYER_DIES) prints "you are dead.", shows the score and waits for a key.
Either way the game starts again without reloading. At the very start
START copies every object record and room record aside, to
WORLD_COPY, and the variables and timers below the game; every new game (
NEW_GAME) copies them back. SAVE and LOAD write and read the same blocks, and the three bytes of Bard's script that the game rewrites are carried with them.
Pictures
Each picture is a program for
RUN_PICTURE, found through the picture table at
PICTURE_TABLE; each picture's own page here shows it being drawn, at the speed the game draws it. The stream starts with a border colour and the picture area's colours, then: move the pen, draw a line (two bytes: direction, length and a Bresenham step), flood-fill a region in a colour, or paint a path of attribute cells; $00 ends it.
The flood fill (
FLOOD_FILL) keeps its queue of seed points on the machine stack and tests every pixel from each side it can be reached from, which is why the pictures fill so slowly. Holding N at the title screen turns the pictures off (
PICTURES_ON), and in the dark the canvas is blacked out instead of drawn (
CLEAR_CANVAS).
One picture changes as the game goes on. The trolls' clearing starts with a black border and black paper -- night -- and when day dawns and the trolls turn to stone,
TROLLS_TURN_TO_STONE rewrites those first two bytes of its stream for a cyan border and cyan paper; every new game puts the night back (
NEW_GAME).
The same pictures can be drawn nine times faster without changing a pixel of them: see
Patches.