quendor
v0.3.0
Published
A Specification-Accurate Z-Machine Implementation
Maintainers
Readme
And thus begins my foray into learning how to emulate the Z-Machine. What does that mean, you ask? Well, let's head down a maze of twisty little passages.
And thus begins Zork, a text adventure game from the 1980s by a company called Infocom. It's really what started off the whole Z-Machine business and thus, indirectly, started off my interest in creating an emulator for it.
The goal of the Quendor project is to provide a working emulator of the Z-Machine and an interpreter for z-code programs that will run on that machine.
As part of this project page, I state that Quendor will strive to be specification accurate, which is different than saying specification complete. A reference implementation needs to be specification complete. That's why it's a reference implementation in the first place. Quendor's aims are a bit more modest in that this project is entirely pedagogical in nature, essentially being created in order to understand how to write the emulator and interpreter and to better understand the Z-Machine itself.
The Z-Machine is the virtual machine Infocom designed in 1979 to run its text adventures, and that Inform still targets today. Quendor — named for the ancient kingdom that became the Great Underground Empire — is a faithful reimplementation of that machine: give it a story file and it plays.
⚠️ Pre-1.0, and honest about it. Quendor plays Z-code versions 1–5 — enough to run Zork I (both its v3 and v5 releases) from start to finish, to play v4 games like Trinity, and to pass the czech v3, v4, and v5 conformance suites (349/349, 367/367, and 406/406). Save/restore (Quetzal, including v5's in-memory undo) and the windowed screen model — status line, quote boxes, single-keystroke input — work today. Coloured text, Unicode, and mouse input are not yet implemented, and versions 6–8 are still to come. See Version support.
Install
npm install -g quendorThis puts two commands on your path — quendor and its short alias qdor — which are the same terminal player.
Play
quendor path/to/zork1.z3Options
| Flag | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| --seed N | Fix the RNG seed for reproducible playthroughs (handy for testing and bug reports). |
| --tandy | Set the v1–3 "Tandy" flag (some games soften their prose when it's set). |
| --interpreter N | Set the interpreter number reported to the game (default 6, IBM PC). |
| --interpreter-version C | Set the interpreter version letter (default A). |
| --accept FILE | Replay a solution file (one command per line) and print the transcript — see Scripted playthroughs. |
| --oracle FILE | With --accept, diff the transcript against a saved one; exit 1 on any difference. |
| -h, --help | Print usage and exit. |
Scripted playthroughs
Rather than typing, you can hand quendor a solution file — a plain-text list of commands, one per line (although they can be chained commands with periods) — and it plays the game through, printing the transcript:
quendor --accept walkthrough.txt zork1.z3The file is just commands, with # comments and blank lines ignored:
# West of House — grab the egg from the tree
n
n
u
get eggAt a "press a key" prompt, a line naming a key sends that single keystroke: SPACE, RETURN, ESC, or an arrow (UP / DOWN / LEFT / RIGHT).
Because quendor's randomness is seeded, the same solution, seed, and game replay identically every time — even a game that leans on chance, like a Zork sword fight. That makes a whole playthrough reproducible: record one, then check it still holds later with --oracle.
# Record a known-good transcript…
quendor --accept walkthrough.txt --seed 1 zork1.z3 > transcript.txt
# …then confirm a later run still matches it, byte for byte:
quendor --accept walkthrough.txt --seed 1 --oracle transcript.txt zork1.z3--oracle exits 0 on a match and 1 on the first difference (printing where the two diverge) — a quick way to catch anything that changes how a game plays, whether in the game file or in quendor itself. Pin --seed so the reproducibility is explicit rather than riding the default.
Use as a library
Quendor ships as an engine, not just a player. The main entry is pure: no DOM, no node: built-ins. So, it runs unchanged in the browser or in Node. The Node-only story loader lives behind a separate quendor/node entry, so importing the engine never pulls in node:fs.
import { Machine, RunState } from "quendor";
import { loadStoryFromFile } from "quendor/node";
const story = await loadStoryFromFile("zork1.z3");
const machine = new Machine(story, { randomSeed: 1 });
machine.onOutput = (text) => process.stdout.write(text);
// Drive the fetch–decode–execute loop, feeding input when the game asks.
while (machine.run() === RunState.WaitingForInput) {
machine.provideInput(await readCommandFromSomewhere());
}In a browser you would load the story bytes yourself (fetch → Uint8Array → new Story(bytes)) and skip the quendor/node import entirely.
The package is fully typed; Machine, Story, the instruction decoder, the Z-text codec, and the object/property tables are all exported from the main entry.
Version support
| Z-code version | Status | | -------------- | ---------------------------------------------------------------------------------------------- | | v1 – v5 | Playable. Runs Zork I (v3 and v5) and v4 games like Trinity; passes czech v3, v4 & v5. | | v6 | Not yet (graphical, needs the full screen model). | | v7 – v8 | Not yet. |
Save/restore uses the standard Quetzal format, cross-compatible with other interpreters, and v5's in-memory undo (save_undo / restore_undo) works too. The windowed screen model — split windows, status line, quote boxes, and single-keystroke input — is in place for v1–5. Coloured text, the Unicode opcodes, and mouse input are not yet implemented, and v6-style graphics are not yet supported.
Conformance
Correctness is checked against czech (Comprehensive Z-machine Emulation CHecker), a test program that self-verifies a large fraction of the opcode set and prints a pass/fail report. Quendor passes it clean for v3, v4, and v5:
v3 — Passed: 349, Failed: 0, Print tests: 19
v4 — Passed: 367, Failed: 0, Print tests: 19
v5 — Passed: 406, Failed: 0, Print tests: 19All three suites run as part of the automated test suite on every CI build, so opcode regressions surface immediately. The v3 run is additionally replayed end-to-end and diffed byte-for-byte against a recorded transcript, so even subtle changes in output are caught.
Development
vp install # install dependencies
vp test # run the unit + conformance tests
vp pack # build the libraryQuendor is part of a larger project — a Z-Machine engine plus a companion debugger — developed in the open as a study in specification-accurate implementation. See the project repository for the full story, architecture notes, and the reference material behind it.
License
MIT © Jeff Nyman
