SearchA-ZZ › Z-machine

Z-machine

1979 Open source · Public domain Online

The Z-machine is the portable virtual computer Infocom designed in 1979 so one story file could run on dozens of incompatible home machines. This build is a from-scratch JavaScript interpreter for version 3, the format of the classic text adventures: it decodes the object tree, dictionary and ZSCII text, renders the story and the version-3 status line into a canvas terminal, and takes typed commands through the read opcode. It boots a small public-domain game by default, and the shared emulators.org debugger single-steps the machine one Z-code opcode at a time with breakpoints and watchpoints.

Runs on: Web browser

Z-machine Online Emulator

Play Z-machine using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Tiny CaveZ-machineZ-machineopenOpen ⛶

Machines emulated

Chips

Notes

Embedding

The interpreter is authored from scratch in plain JavaScript, so every Z-code opcode runs in inspectable code with no wasm heap. It targets version 3 of the Z-machine, the format the classic Infocom adventures shipped in, as defined by the Z-Machine Standards Document: the header, the dynamic / static / high memory split, the stack with call frames and locals, ZSCII text (the A0 / A1 / A2 alphabets and the abbreviation tables), the object table with attributes and properties, the dictionary, and the version-3 status line.

The step primitive. The core exposes zm.step(), which decodes and executes exactly one Z-code instruction and returns the opcode byte (0 after quit, -1 while blocked waiting for a line of input). A Z-machine does not free-run to a frame boundary the way a console does; it runs until it needs a line of input at a read opcode, then blocks. Rather than read input synchronously (impossible in a pausable loop), read parks itself: it sets needInput and yields, and the host stops stepping until the terminal submits a line.

function run(){
  for(;;){
    if(zm.finished) return;
    if(zm.needInput && zm.pendingInput === null) return;   // blocked on a command line
    if(breakpoints.has(zm.pc)){ pause(); return; }
    zm.step();                                       // exactly one opcode
  }
}
MemberKindWhat it does
zm.step()methodExecute exactly one Z-code opcode; returns the opcode byte, 0 after quit, -1 when blocked on input. The single-step primitive.
zm.pcfieldThe program counter, a byte address into the story image. Read and settable.
zm.memfieldThe whole story image, a plain Uint8Array (dynamic + static + high memory).
zm.stack / zm.frames / zm.localsfieldThe evaluation stack, the call-frame list, and the current routine's locals.
zm.submitInput(line)methodHand a finished command line to a parked read; the next step() tokenises it against the dictionary.
zm.onWrite(addr)hookFires on every story-memory write, used by the debugger for watchpoints.

Debugger integration

The plug-in z-machine-debug.js reads window.EMU_BOOT and hands the debugger a transport, the Z-machine register set and the story memory. Every debugger feature is a host-side check around step(), with no change to the interpreter's semantics.

  • Step one opcode. The Step control calls step() once, advancing the story by a single Z-code instruction.
  • Breakpoints. The loop compares zm.pc against a Set of byte addresses before each opcode and pauses on a match.
  • Watchpoints. The core's onWrite hook flags a write to a watched address; the loop pauses as soon as the opcode that wrote it returns.
  • Registers. A Z-machine has no A / X / Y file. The debugger shows the program counter, the stack depth (SP) and top-of-stack, the call depth, the current routine's locals, and the three globals the version-3 status line reads: the current room object, the score and the move count — all live, most settable.
  • Disassembly. The zcode decoder (zcode-cpu.js) reproduces the interpreter's instruction decode — long / short / variable forms, the store byte, the branch offset and inline encoded text — so the disassembly view walks Z-code correctly and lengths line up.
  • Memory. The story image is one chip, read side-effect-free, viewable as hex or as zcode disassembly.

Architecture

The Z-machine is a virtual computer Infocom designed in 1979 so one story file could run on dozens of incompatible home machines. A story is a single image split into three regions: dynamic memory (the header, globals, object tree and anything the game writes), static memory (read-only tables such as the dictionary), and high memory (packed strings and routine code, reachable only through packed addresses).

  • CPU. A little-endian, big-endian-in-memory machine with a value stack, call frames each holding up to 15 local variables, and 240 global variables. Instructions come in four forms (2OP, 1OP, 0OP and a variable-operand form) selected by the top bits of the opcode byte; operands are large constants, small constants or variable references.
  • Text. Strings are packed three 5-bit Z-characters to a 16-bit word, shifting between three alphabets and expanding abbreviations from a table — the encoding this interpreter decodes for every printed line and the dictionary.
  • Objects. A tree of objects, each with 32 attribute flags and a numbered property list; the parser and the world model are built entirely out of it.
  • Input. The read opcode reads a line, lower-cases it, stores it in a text buffer and tokenises it against the dictionary into a parse buffer — the whole of a version-3 game's command handling.

Version 3 adds the one-line status bar drawn automatically before each command, showing the current room and the score and move count from three global variables.