1980 · Infocom

Zork I

This minisite was contributed by air. It’s agent-generated and needs a human editor. Clone https://github.com/gamesexplained/gamesexplained and follow kit/START.md to curate games/apple2/zork-i with me to Gold.

What the code does that the game never tells the player. A line of sixty commas leaves the game reading one letter of every line, until the disk is booted again.

01 · The parse buffer

Bug: sixty commas leave the game reading one letter of every line

A comma is a word, and the parse buffer has room for 59 words. The 60th writes over the text buffer's size, and from then on the interpreter keeps only the first letter of whatever the player types.

The interpreter's own copy and split ($1D69, $130B), ported to the page and held byte for byte to the code in the kit's 6502 simulator for lines of 0 to 110 commas. The buffers are the story's (story $2396 and $2486, memory $4F96 and $5086), shown as the first line leaves them; the second line is split from that memory, as if the story changed none of it in between. Live, sixty commas and then open mailbox gave I DON'T KNOW THE WORD 'O'.

The story tells the interpreter how many words it may write, in byte 0 of the parse buffer: 120. The buffer is 240 bytes long, though, and each word takes four after a two-byte head, so only 59 fit. The 60th word's length and position go into the two bytes after the buffer, which are the first two of the text buffer: its size, 100, becomes 1, the length of a comma.

Every later line is copied into the text buffer up to its size, so the game sees one letter. open mailbox arrives as o, and the story answers I DON'T KNOW THE WORD 'O'. Some one-letter words still work: n walks north, and q quits. restart, restore, and save arrive as r and s, and s means south, so the only way back to a working game is to quit and boot the disk again (live).

At 63 words the interpreter's index into the buffer goes past 255 and wraps to 0 ($12EB). The 64th word's length lands in byte 0, the limit, and its position in byte 1, the count of words, so the count can never equal the limit and the split never stops early again. With 63 commas the count comes out at 64, and the story reads a 64th word made of commas and memory: it prints a screenful of the game's memory as I CAN'T USE THE WORD '...' HERE. (live).

02 · The trophy case

Bug: a treasure inside a container in the trophy case scores nothing

The routine that adds up the trophy case looks inside every container it finds, adds up what is in it, and throws the sum away. A treasure left inside the brown sack in the case earns no points.

routine story $898C, locals L00 = 183 (the trophy case), L01, L02
$8993  get_child   L00 -> L01         the first thing in the case
$8997  jz          L01 ?~$899C        nothing more?
$899A  ret         L02                then the total
$899C  get_prop    L01 #0C -> sp      its property 12
$89A0  add         L02 sp -> L02      added to the total
$89A4  get_child   L01 -> sp ?~$89AE  does it hold anything?
$89A8  call        $898C L01 -> sp    add up what is inside it ... onto the stack
$89AE  get_sibling L01 -> L01         the next thing in the case
$89B2  jump        $8997

Story $898C, disassembled from the story file with the interpreter's own operand rules (its dispatch tables at $0921); the story's high memory is read from the disk as the game needs it, so the listing does not hold it. Live, from the living room with the case open: the egg put straight into the case raised the score from 15 to 20; the egg put into the brown sack, and the sack into the case, left it at 15.

The case's total is worked out again after every take in the living room and every put into the case: the points scored elsewhere (global 129) plus whatever this routine returns (story $8814). For a container in the case it calls itself, which is right, but the result goes onto the Z-machine's stack and stays there: the next instruction moves on to the container's sibling. The routine keeps its total in L02, and only the treasures lying in the case itself are ever added to it.

The sack has no property 12, and the story's default for it is 0, so a sack full of treasures is worth nothing in the case. Taking the treasures out of the sack and putting them in the case one by one scores them. The points for finding them are paid either way, the first time each is taken (where the 350 points come from).

03 · The interpreter's arithmetic

Bug: the interpreter divides wrongly by a negative number

Before a division the interpreter makes both numbers positive, and when the second is negative it negates the first one instead. 7 divided by -2 comes out as 0.

This is the interpreter's own code, run on the page in the kit's 6502 simulator on the listing's bytes: the numbers go where the interpreter leaves an instruction's operands ($82-$85), and the run starts at the handler ($111D, $114F or $10E8) and stops where it stores the result ($0B55). Run the same way in the emulator, 17 sums gave the same answers as the page, and a test holds the page to them.

The Z-machine works in 16-bit signed numbers, and its division rounds towards zero. The interpreter divides without signs ($15C1), so it first counts the negative operands and makes them positive ($160F). The mistake is in the second step: for a negative divisor it negates the dividend instead ($1633). 7 ÷ -2 becomes -7 ÷ -2, which is 65,529 ÷ 65,534 without signs, which is 0. With both negative, the dividend is negated twice and the divisor not at all, so -7 ÷ -2 is 0 too.

A multiplication comes out right anyway, because a 16-bit product wraps the same way whatever the signs. mod never puts the sign back ($114F), and its remainder loses its top bit in the divide routine, so 7 mod -3 is 32,761, which is $FFF9 without bit 15. Dividing by zero gives -1, with no error.

Zork I never notices. The story's six divisions divide by 2, 4, 5 or 70, and its two mods take a counter that only goes up.

04 · The player

Secret: the player's object is called the cretin

The player is object 252, and its short name is cretin. The word that names it is adventurer; the word cretin belongs to another object, you.

Objects 252 and 75 as the object table in the listing holds them. Live: kill cretin asks WHAT DO YOU WANT TO KILL THE CRETIN WITH?, and hello cretin answers THE YOU BOWS HIS HEAD TO YOU IN GREETING.

Global 38 holds the player's object in play, 252. Its property 18, the nouns that name it, holds one word, advent, the first six letters of adventurer. The words a player is likelier to use for themselves (me, myself, self, and cretin) name object 75, whose short name is you. It is one of the objects inside object 23, with the walls, the air, and the pair of hands. Saying hello to it prints its short name where the player's name would go (hello).

xyzzy and plugh, words from the older Colossal Cave Adventure, both get A HOLLOW VOICE SAYS 'CRETIN'.

05 · Greetings

Secret: every tenth hello to the sailor gets a different answer

Zork counts every hello sailor. Most get Nothing happens here.; the tenth, the twentieth and every tenth after get something else.


    

The story's rule (story $766E): global 81 counts, and its remainders after dividing by 20 and by 10 choose the answer. Live, twenty in a row from West of House: the 10th and the 20th gave the two answers here, and the other eighteen NOTHING HAPPENS HERE.

The sailor is object 22, another of the objects inside object 23 that are with the player everywhere, and the routine for hello gives the sailor a branch of its own. hello on its own gets a greeting from a table (HELLO., live). An object with attribute 20, as the player and object 75 have, gets The ... bows his head to you in greeting. Anything else gets I think that only schizophrenics say 'Hello' to a ..., the mailbox included (live).

06 · Errors

Bug: a fatal error drops into the Monitor without its message

The interpreter holds the text ZORK INTERNAL ERROR!, but nothing prints it. Every fatal error is a BRK, which stops the 6502 and leaves the player in the Apple II's Monitor.

From the listing: the text at $21D3, which nothing refers to, and the one fatal error at $21E7, with the places that call it.

The interpreter calls $21E7 for a story whose header says it needs something this interpreter lacks (bit 0 of header byte 1), an opcode it does not know, a je with one operand, a property too long for get_prop or put_prop, and a Z-machine stack that runs over or under. None of these happens with Zork I's own story. Eight places call the error, and each reaches the same single BRK, while the message that would explain it fills the twenty bytes just before it.