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.

Zork I on the Apple II is two programs: a 6,656-byte interpreter written for the 6502, and an 81,920-byte story written for the Z-machine, a computer Infocom made up. The story is bigger than the machine's memory, so the interpreter keeps its first 18 KB and reads the rest from the disk a page at a time, while the player waits at the prompt.

01 · The game

The game

The player stands west of a white house, in a field, with a mailbox. Under the house is a dungeon, the Great Underground Empire, with nineteen treasures in it and a thief who wants them.

The opening screen: the status line reads WEST OF HOUSE, SCORE: 0/0, then the banner and the description of the field west of the house

The first screen, after the banner. The top row is the status line: the room's name and the score over the number of moves.

The cellar, eleven moves in: the trap door has crashed shut, and the sword glows with a faint blue glow

Eleven moves later, in the cellar, with the lamp lit and 35 points scored. Everything the player typed is in lower case; everything the game prints is in capitals.

Everything happens in words. The player types a sentence, take the lamp or open the trap door, and the game answers in a paragraph. Points come from finding the treasures and from putting them in the trophy case in the house's living room; 350 is the most there is.

Two programs share the work. The interpreter is 6502 code: it reads the keyboard, prints on the screen and reads the disk. The story is the game itself (the parser, the rooms, the objects, and every sentence), written in Z-code, the instructions of Infocom's imaginary computer. The interpreter runs the story the way the Apple II runs the interpreter, one instruction at a time.

02 · The parser's first half

The interpreter reads six letters of each word

The interpreter cuts each line into words and looks every word up in the story's dictionary before the story sees any of it. A word is known by its first six letters, so lantern and lanterns are the same word.

The interpreter's own code, ported to the page: the copy of the line into the text buffer ($1D69), the split into words ($130B), the encoder ($1A25) and the dictionary search ($1432), reading the dictionary from the listing's bytes. A test runs each routine on the same bytes in the kit's 6502 simulator beside the port: every dictionary word, 2,000 random words with capitals and punctuation in them, 511 whole lines, and every object's name, with the same result every time. What each bit of a dictionary entry's data means is read from the words that carry it: the story's own parser, which reads them, is Z-code on the disk, and it was not traced.

A space, a question mark, or a RETURN ends a word and is thrown away. The story's dictionary names three more separators (the comma, the full stop, and the quotation mark), and each of those ends a word and is a word of its own, so take lamp, sword is four words. For each word the interpreter writes four bytes into the parse buffer: the address of its dictionary entry, or 0 for a word it does not know, then its length and where it starts in the line. The story's parser takes over from there.

A word becomes six 5-bit characters before the search ($1A25). The small letters are 6 to 31. A digit or a punctuation mark takes two characters, a shift and then its place in a third alphabet, and a character in none of the alphabets takes four. A capital would take a shift too, but the interpreter makes every capital small as it copies the line ($1D82), so none reaches the encoder. A word shorter than six is padded with 5s. The six pack into two 16-bit words, three to a word, with the top bit of the second set to mark the end. Those four bytes are what the dictionary holds, and the search compares them with its 617 entries one by one ($1432).

Each entry has three more bytes, which the interpreter never reads. The first is a set of flags, the kinds of word it can be, and the next two are numbers for those kinds: the verb's number, the adjective's, or for a direction the number of the property that holds that exit in every room. north is direction 31, and property 31 of a room is where north leads (the map). When a word is two kinds at once, the low two bits of the flags say which kind's number comes first.

03 · The world

Every room and every thing is one of 255 objects

The house, the mailbox, the leaflet in the mailbox and the player are all entries in one table. Rooms are the 110 objects inside an object with no name, and a thing is where it is because of the object it sits in.

Read from the object table in the listing (story $0148, memory $2D48) as the interpreter reads it ($16BB, $167D); the names are decoded by a port of the interpreter's string printer ($18D2), held to $0DDD for every object. Click a parent, a child or a room to go to it. The properties' meanings come from the story's code, named beside each; the rest are shown as bytes.

An object is nine bytes: 32 attribute flags, then the numbers of its parent, its next sibling, and its first child, then the address of its property table ($16BB). Moving the lamp into the player's hands is moving one entry from one list of children to another. The property table holds the object's short name and its properties, numbered 1 to 31 and kept in descending order ($167D). A property the object lacks is read from a table of 31 defaults.

Exits are properties too. Each direction has a property number, from 31 for north down to 19 for land, and the story's walking routine (story $6808) decides what an exit does from its length alone:

LengthWhat it holdsWhat happens
1a roomthe player goes there
2a messagethe message is printed and the player stays
3a routinethe routine is called; the player goes where it says, if anywhere
4a room, a variable and a messagethe player goes there if the variable is not 0, or else sees the message, or You can't go that way.
5a room, a door and a messagethe player goes there if the door is open (attribute 13), or else sees the message, or The ... is closed. with the door's name

Where a room has no exit at all in the direction asked, the routine checks global 118 first. In a dark place it rolls random 100, and on 74 rolls in 100 it prints Oh, no! You have walked into the slavering fangs of a lurking grue! and the player dies (story $68B9). The first try, live, in the cellar without the lamp, was one of the 74.

04 · The disk

Two programs share one disk

The interpreter and its loader take the first three tracks, and the story the next twenty, a page of the story to each sector. The other twelve are empty, and the disk has no catalog.

Point at a sector.

Tracks run across, sectors down, in the order the game numbers them. The data-field marks are from the disk image (disk.py info); where each story page lies is the interpreter's rule ($1DC1), and the order of the sectors round a track is its table at $2BB8, read here from the listing. The save disk follows $1EC8 and $2064.

The Apple II's boot ROM reads sector 0 of track 0, which is DOS 3.3's boot sector with its code unchanged. That sector reads ten sectors of track 0 into $2200-$2BFF and jumps to $2300, where there is now a loader and a set of disk routines like DOS's. The loader reads tracks 1 and 2 into $0800-$21FF, the interpreter, and jumps to it. The disk routines stay in memory for the rest of the game.

Story page n is on track 3 + n ÷ 16, sector n mod 16 ($1DC1), so the story starts at track 3 and fills twenty tracks exactly: 320 pages of 256 bytes. The disk routines turn each sector number into a position round the track through a table ($2BB8), which puts consecutive pages four sectors apart: while the 6502 copies one page out of the disk's buffer, three sectors pass under the head, and the next page arrives as the routines are ready for it.

The copy protection is one byte. DOS starts each sector's data with the marks D5 AA AD; tracks 1 to 22 of this disk use D5 AA BC, and the game's disk routines read only that ($24FB) and write only that ($245C). A disk reader that expects DOS's marks finds no data on those tracks. A save is written with BC too, into sectors that are already on the disk: the disk routines find a sector by its address marks, so a save disk has to be formatted first, by DOS. The routine that formats a disk is in memory at $2AAF, and nothing calls it.

A save takes four tracks, from track 3 + 4 × the position, so the eight positions fill tracks 3 to 34. Each one holds the Z-machine's registers, its stack, and the first 46 pages of the story, everything the story can change.

05 · Memory

Only the first 18 KB of the story stays in memory

The story is 80 KB, and a 48K Apple II has room for 37 KB of it. The interpreter keeps the story's first 72 pages for good and lends 76 more pages of memory to the rest, one page of the story at a time.

Point at a page.
kept in memoryin a buffer, used lastin a buffer, used longest agoon the disk only

The 320 pages of the story, 32 to a row. Which page each buffer held, and their order of use, are read from the interpreter's tables at $2200-$23FF in two snapshots: the game waiting at its first prompt, and in the cellar after eleven moves.

The story divides itself in two. The first part holds everything that changes as the game is played (the objects, the variables, and the buffers the parser writes) and the tables the story reads, the dictionary among them. The story's header says where the first part ends: high memory starts at story $476C. Above that are the routines and most of the strings, which are only ever run or printed. The interpreter loads pages 0 to $47 into $2C00-$73FF once, at the start, and never reads them from the disk again. The Source tab's listing is memory at that moment, so it holds the interpreter and these 72 pages, and none of the other 248.

Every other page goes into one of the buffers from $7400 up to the top of memory, 76 of them on a 48K machine and 12 with 32K ($08C8). The interpreter finds a page by looking for it among the buffers' page numbers ($18B5), keeps the buffers in a chain from the one used last to the one used longest ago ($1880), and when the page is in none of them it reads the page from the disk into the buffer at the old end of the chain ($1752, $1801). At the first prompt fourteen buffers are in use, and eleven moves later 56. Once all 76 are full, every page the story has not used for a while costs a turn of the disk.

The interpreter tests for memory from $BF00 down, so a 64K or 128K Apple II gets the same 76 buffers as a 48K one; the language card's extra 16K is never used.

06 · The score

Where the 350 points come from

The points are written into the objects. A treasure carries two numbers, one paid the first time it is taken and one paid while it lies in the trophy case, and four rooms pay the first time the player walks in.

Properties 13 and 12 of every object, from the listing. Click a row to see the object above. The story's code: story $61EA pays property 13 and sets it to 0; the living room's routine (story $8814) adds up property 12 of the trophy case's contents (story $898C); story $CC4C pays the Drafty Room's 13 points from global 35.

Property 13 is paid once: the routine that adds it to the score sets it to 0 (story $61EA). Property 12 is different. After every take in the living room and every put into the case, the story works the score out again from the start: the points scored elsewhere, kept in global 129, plus property 12 of every object in the case (story $8814). Taking a treasure back out of the case takes its points away.

The table comes to 337. The last 13 are not in any object: they are global 35, paid once by the Drafty Room's routine to a player who reaches it with a light (story $CC4C). Dying costs 10 (story $6467).

07 · The screen

The screen, read back from memory

The game draws on the Apple II's 40-column text page, a kilobyte of memory where each byte is a letter. This is that kilobyte from the emulator, set in type.

The same screen as the emulator drew it

Left: $0400-$07FF from the snapshot, read by the site's Apple II library (Apple2.text, which the kit holds to its own screen reader for every byte value). Right: the kit's picture of the same snapshot. The letters agree on every row.

Row 0 is the status line, redrawn before every input ($1CD7). The text window is rows 1 to 23, so the Monitor's scrolling leaves the status line where it is. The words on the status line are in inverse, and the spaces between are not: the interpreter turns inverse on and then clears the row with the Monitor's CLREOL, which writes ordinary spaces.

Everything the game prints is in capitals ($1B90), the one case every Apple II can show. The interpreter collects each line at $0200 and breaks it at the last space before column 40, so a word never splits across two rows ($1BB5). It counts the lines since the last input, and when they fill the window it prints [MORE] in inverse and waits for a key ($1C5B).

The rows of the text page are not in order in memory: row 1 starts 128 bytes after row 0, and row 8 only 40 bytes after row 0. The tick box shows each row's address.