Skip to main content

Voxam

A Specification-Accurate Z-Machine Implementation
Early and Late Infocom + Modern Inform

Built with Python Voxam is released under the MIT license.

CI status Conventional Commits: 1.0.0

Open with vscode

An interpreter for the Z-Machine, written in Python, with Glulx to follow.

The Z-Machine is the virtual machine Infocom designed in 1979 to run its text adventures, and which the interactive fiction community has used ever since. Voxam reads a compiled story file and executes it, with two guiding commitments: fidelity to the Z-Machine Standard -- every rule the interpreter enforces cites the section it came from -- and reproducibility, so that a recorded play session replays identically, forever.

Voxam is developed against real games. The Zork trilogy, Cutthroats, Deadline, Seastalker, Trinity, and A Mind Forever Voyaging have all been played to winning conclusions under Voxam -- several across multiple releases, two to perfect scores -- alongside modern classics from Colossal Cave to the IF Comp winner All Roads, each verified end-to-end by the acceptance harness described below.

Status

Version 0.x: early, honest, and playable.

Works today: story file versions 1 through 5 -- Infocom's whole main-line catalog and the modern Inform and PunyInform games built on version 5. That includes the full parser and object machinery, a seeded random number generator for reproducible sessions, the screen model (split windows, single-keystroke input), custom alphabet tables and the accented extra characters, SAVE, RESTORE, and RESTART in the standard Quetzal format, multi-level UNDO, and an acceptance-script harness for recording, replaying, and probing whole playthroughs.

Voxam is verified against the community's interpreter test suites: CZECH (versions 3, 4, and 5), Praxix, TerpEtude, and Strict Z Test all pass clean, and every remaining gap halts loudly with a citation instead of guessing.

Under construction: the Standard 1.1 extras -- custom Unicode translation tables and print_unicode -- and auxiliary-file saves, each a named frontier a test suite is already waiting behind.

Not yet: sampled sound, real-time timed input, version 6, and Glulx. For recorded sessions, seeds substitute for saves: a script replays a whole game in moments.

Installation

Voxam requires Python 3.12 or later.

pip install voxam

or, as an isolated tool:

pipx install voxam        # or: uv tool install voxam

Voxam ships no story files. Bring your own: the IF Archive hosts thousands of freely available games, and story files you own from commercial collections work as-is.

Playing stories

Point Voxam at a story file and play at the terminal:

voxam path/to/story.z3

Add --seed to make the dice reproducible: the same seed and the same commands produce the same session, every time.

voxam --seed 1137 path/to/story.z3

Typing save in a game writes a Quetzal file beside the story -- zork1.z3 saves to zork1.sav -- and restore reads it back. Quetzal is the standard interchange format, so saves travel between Voxam and other interpreters.

Acceptance scripts

A play session can be saved as an acceptance script and replayed:

voxam --accept some-session.accept

An acceptance script is a plain text file of typed commands plus a few directives:

! SEED=99
! GAME=path/to/story.z1

# Comments annotate the session; blank lines are ignored.

x me. x mailbox            # inline comments start at whitespace + #
> open mailbox             # the > prefix is optional transcript style

The rules, line by line:

  • ! KEY=VALUE is a directive: GAME names the story file to run, and SEED fixes the dice (a --seed argument overrides it). A relative GAME path counts from the script's own directory, and forward slashes work on every platform.
  • # at the start of a line is a comment.
  • An inline comment begins at whitespace followed by #.
  • A leading > is optional and stripped; it also escapes the rare command that genuinely begins with # or !. A > alone types an empty line.
  • A line starting with three backticks is a fence: everything until the next fence is skipped entirely, directives included. Text after the backticks labels the fence, and an unclosed fence skips the rest of the file -- handy while working out a section that a seed change will invalidate, or to replay only the start of a longer script.
  • Anything else is typed into the game exactly as written.
  • When the commands run out, the session ends as if the player had reached end of input.

While recording a longer session, --replay types the script and then leaves you at the prompt instead of ending, so a work-in-progress script catches you up to where you left off:

voxam --replay some-session.accept

Refusal warnings

During a replay, Voxam listens for the parser's refusal dialect -- responses like "You can't see any statuette here!" or "You should close it first" that mean a recorded command did not do what it said. Each one is reported with the script line that drew it:

voxam: line 31: 'lock door' looks refused: You should close it first.

Refusals scroll past a human reader without registering, and the missing side effect may not surface until dozens of turns later. The warning points at the moment it happened, which turns the most common recording bug from an archaeology expedition into a one-line fix.

Probing a recording

When a recording goes wrong -- a death that survives every retry, a walkthrough command the game will not speak -- the fix is empirical, and voxam.probe is the instrument. A seeded script is a deterministic timeline, so a probe can replay the recorded prefix exactly and then ask "what would happen if...?", as many times as it takes:

from voxam.probe import Probe

probe = Probe.load("acceptance/advent.accept")
run = probe.attempt(["ne", "give eggs to troll", "ne"], drop_last=2)

for step in run.steps:
    line = step.response.strip().splitlines()[0] if step.response.strip() else ""
    flag = f"  <<< {step.refusal}" if step.refusal else ""
    print(f"[{step.command}] {line}{flag}")

Each step pairs a typed command with everything the story said back, and flags any response spoken in the refusal dialect -- which turns a hundred-command stretch into a one-screen diagnosis. attempt replays the script and tries a variant tail (drop_last re-tries the ending without editing the file); run takes the whole command list for surgery the prefix cannot express, such as inserting turns mid-timeline; and the returned machine is left standing for post-mortem reads of globals, object parents, or the clock. Every run boots fresh from the seed, so no experiment can contaminate another.

Probe scripts themselves are throwaways -- write one in a scratch file, find the answer, record the fix in the .accept annotations, and delete it. The harness is the part worth keeping.

Development

Working on Voxam itself needs uv for dependency and environment management:

git clone https://github.com/jeffnyman/voxam.git
cd voxam
uv sync --all-groups

All commands below assume that environment.

Task Command
Run the test suite uv run pytest
Run tests without coverage uv run pytest --no-cov
Lint uv run ruff check .
Lint and autofix uv run ruff check --fix .
Format uv run ruff format .
Check formatting only uv run ruff format --check .
Type check uv run mypy
Build distributions uv build

Project conventions

  • Layout. Source lives under src/voxam, tests under tests/. The src layout ensures tests exercise the installed package rather than the working directory.
  • Typing. mypy runs in strict mode over both src and tests, and the package ships a py.typed marker so downstream consumers get its types.
  • Coverage. The suite is gated at 100% branch coverage. This is deliberate for a project of this size; adjust fail_under in pyproject.toml if it stops being useful.
  • Spec citations. The § references in code, docstrings, and output follow the HTML rendering of the Z-Machine Standard 1.1 vendored at entharion/z-machine-standard/. Other renderings of the same Standard, including the PDF beside it, number some paragraphs differently.
  • Line endings. LF everywhere except Windows script files, enforced by both .gitattributes and .editorconfig.
  • Recordings. Complete playthroughs live under acceptance/ in the repository (they are not part of the installed package). They reference games under the optional entharion submodule, so they replay locally rather than in CI -- and they double as the project's archaeology notebook, annotating where the games' published walkthroughs go wrong.

Pre-commit hooks

Install the hooks once, after which lint, format, and type checks run on every commit, and commit messages are validated:

uv run pre-commit install

Every hook is a repo: local entry that runs its tool out of the project environment via uv run, so pre-commit never clones hook repositories or builds cached environments under ~/.cache/pre-commit. Tool versions have a single source of truth: uv.lock.

To run every hook against the whole tree:

uv run pre-commit run --all-files

Commit messages

Commit messages follow Conventional Commits, enforced at commit time by commitizen through the commit-msg hook installed above:

feat: add object table parsing
fix(memory): reject story files shorter than the header
docs: explain the save file format

To check a message by hand, or to compose one interactively:

uv run cz check -m "feat: add object table parsing"
uv run cz commit

Because the history is machine-readable, commitizen derives the next version, tags it, and updates the changelog:

uv run cz bump

Reference Material (optional)

An entharion submodule holds the specifications and story files this project is developed against. These are not required as part of building and deploying Voxam, but they help during development. Voxam does not depend on anything under entharion/. It is not needed to install the project and CI does not fetch it. Git leaves submodules empty unless asked, so a plain clone simply skips it.

If you want this reference material and if this is your first time checking out the repo, run this command:

git submodule update --init --recursive

That will fetch the primary repository as well as its submodules:

The latter is my own recomposed version of the Z-Machine Standard document, made a little easier for me to read and consume. You can see this deployed here:

To discard it again, freeing the disk space without affecting the project:

git submodule deinit --all

Dependabot tracks the pinned commit and opens a PR when upstream moves. To move the pin by hand instead:

git submodule update --remote entharion
git add entharion
git commit -m "chore(deps): update entharion submodule"

👨‍💻 Author

Made with 🤍 by Jeff Nyman

Website - Jeff Nyman

LinkedIn - Jeff Nyman

☦️ Doxazein (δοξάζειν)

חֶסֶד וֶאֱמֶת אַל־יַעַזְבֻךָ קָשְׁרֵם עַל־גַּרְגְּרֹתֶיךָ כָּתְבֵם עַל־לוּחַ לִבֶּךָ

"Let not mercy and truth forsake thee:
bind them about thy neck;
write them upon the table of thine heart."
Proverbs 3:3

🕹️ Acknowledgements

This project stands on the shoulders of the team at Infocom, the MIT-born company that invented the Z-Machine to let Zork, and everything that followed, run unmodified across nearly every computer of its era. Particular thanks go to Marc Blank and Joel Berez, who designed the Z-Machine's virtual architecture, and to Tim Anderson, Bruce Daniels, and Dave Lebling, whose work on Zork at MIT gave the format a reason to exist. Thanks also to Graham Nelson, whose Inform language and Z-Machine Standards Document kept the format alive and well-documented long after Infocom itself was gone, making implementations like this one possible.

⚖️ License

The code used in this project is licensed under the MIT license.

Note: This license applies only to the code in this repository. The original Z-Machine concept, design, and any original assets belong to their respective copyright holders.

✨ Long live the classics.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

voxam-0.3.0.tar.gz (75.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

voxam-0.3.0-py3-none-any.whl (85.0 kB view details)

Uploaded Python 3

File details

Details for the file voxam-0.3.0.tar.gz.

File metadata

  • Download URL: voxam-0.3.0.tar.gz
  • Upload date:
  • Size: 75.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for voxam-0.3.0.tar.gz
Algorithm Hash digest
SHA256 43b591e43252ab3e009568e2a33f7e6ff73680feeb88c854a11589222f6406b4
MD5 21054d88b896fa357903b7e2034f808b
BLAKE2b-256 54891498c8b8d3dc01f83b572b57b5962329c018cff50d7823981bf37ea2b50b

See more details on using hashes here.

Provenance

The following attestation bundles were made for voxam-0.3.0.tar.gz:

Publisher: release.yml on jeffnyman/voxam

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file voxam-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: voxam-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 85.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for voxam-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f4524385fd588cc0b6a8e85ac172e850bb8891711b2f9c4ce81946a4564ef5f8
MD5 2a964ac94474c5e58a606db64432d7ea
BLAKE2b-256 ab7210ad35f634bec2a489a5e19fced640c1b8ad2feb66ed780845d82af11d3a

See more details on using hashes here.

Provenance

The following attestation bundles were made for voxam-0.3.0-py3-none-any.whl:

Publisher: release.yml on jeffnyman/voxam

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.7.0

2 files

1.6.0

2 files

1.5.0

2 files

1.4.0

2 files

1.3.0

2 files

1.2.0

2 files

1.1.0

2 files

1.0.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page