From ed6ffbdc5d894ea997b7c9b9689a9b809bf70c5e Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Mon, 9 Feb 2026 11:38:12 -0500 Subject: [PATCH] Add research on IF possibilities --- docs/how/if-integration.txt | 676 +++++++++++++++++++++++++++++ docs/how/if-journey.rst | 258 +++++++++++ docs/how/viola-embedding-audit.rst | 156 +++++++ docs/how/zvm-embedding-audit.rst | 377 ++++++++++++++++ 4 files changed, 1467 insertions(+) create mode 100644 docs/how/if-integration.txt create mode 100644 docs/how/if-journey.rst create mode 100644 docs/how/viola-embedding-audit.rst create mode 100644 docs/how/zvm-embedding-audit.rst diff --git a/docs/how/if-integration.txt b/docs/how/if-integration.txt new file mode 100644 index 0000000..54820a1 --- /dev/null +++ b/docs/how/if-integration.txt @@ -0,0 +1,676 @@ +interactive fiction integration — hosting z-machine games in a MUD +==================================================================== + +where this fits. the dreambook calls for IF games as in-world content — a room +in the MUD becomes a portal to a z-machine adventure. players enter IF mode, +play through a self-contained story, and other players see "jared is playing +zork in the arcade." fully isolated from world noise, but visible to +spectators in the same room. + +the architecture is ready. we have the mode stack, input routing, editor mode +pattern. the missing pieces: + - z-machine interpreter (either embedded or subprocess) + - room-local spectator broadcasting (others see your game output) + - "terminal" game object concept (a room object that hosts IF sessions) + - save/restore integration (quetzal snapshots to sqlite) + +this doc synthesizes research on three z-machine implementations and proposes +integration paths. + + +the z-machine landscape +======================= + +three implementations evaluated: + +sussman/zvm (python, pure stdlib): + - versions 1-5 claimed, but only ~40% of opcodes implemented + - interpreter HALTS on any unimplemented opcode — can't gracefully degrade + - excellent pluggable I/O: abstract ZUI, ZScreen, ZInputStream, ZOutputStream + - quetzal save/restore infrastructure exists but opcodes are stubbed + - pure python, zero dependencies — perfect for embedding + - blocking run loop, not async-compatible + - clean API: ZMachine(story_bytes, ui=custom_ui).run() + - python 3 port was a "blind fix" — functional but fragile + - verdict: TOO INCOMPLETE. would need 2-4 weeks of opcode implementation + before any real game runs. the I/O architecture is excellent reference + material but the CPU is not production-ready. + +viola (python, pygame dependency): + - versions 1-8, comprehensive modern standard 1.1 compliance + - 120+ opcodes implemented — ALL documented z-spec instructions have + implementations + - clean stream architecture: 5 output streams, each individually addressable + - full quetzal 1.4 save/restore support with undo + - active maintenance (2024 copyright, author engaged) + - dependencies: pygame (for rendering/input), numpy (for image processing) + - bundled ififf submodule for blorb/quetzal/IFF handling + - blocking main loop (execloop), module-level global state + - no tests whatsoever + - clean module separation: zcode/ (VM core) vs vio/ (I/O layer) + - ~8,500 lines in zcode/ + ~2,000 in vio/ + - verdict: MOST COMPLETE python z-machine available. pygame dependency is + only in vio/ — replace that module with a text-callback I/O backend and + the core VM is usable. strongest candidate for embedding. + +mojozork (C, educational implementation): + - version 3 only (rejects other versions with error) + - ~45 opcodes, complete for v3 (covers ~90% of infocom catalog) + - excellent pluggable I/O via function pointer callbacks + - has runInstruction() for single-step execution — async-friendly by design + - multiple frontends: stdio, libretro, SDL3, and a TELNET MULTIPLAYER SERVER + - MultiZork uses sqlite for multiplayer state persistence + - zlib license (very permissive) + - author: ryan c. gordon (well-known open source dev) + - custom save format (not quetzal) + - C only — would need ctypes/CFFI wrapper or subprocess + - verdict: EXCELLENT REFERENCE for architecture patterns. MultiZork shows + exactly how to embed a z-machine in a multiplayer telnet server. v3-only + is limiting but covers classic infocom games. best studied, potentially + wrapped via C extension. + + +integration options +=================== + +three paths forward: + +option A: embed viola (python native) + - fork or vendor viola's zcode/ module + - replace vio/ with our own I/O backend: MudZUI class + - wrap the blocking execloop with async bridge (run_in_executor) + - state serialization via viola's quetzal support + - pros: pure python, full z-machine 1-8 support, comprehensive opcodes, + native access to VM state + - cons: no tests in upstream, blocking loop requires thread pool, + global state needs refactoring for multiple concurrent games + +option B: subprocess dfrozt (quick prototype) + - spawn dfrotz in a subprocess, pipe stdin/stdout + - asyncio subprocess API handles I/O + - save/restore via dfrotz's built-in quetzal support + - pros: FASTEST PATH TO WORKING. dfrozt is battle-tested, z-machine 1-8, + comprehensive game compatibility + - cons: less control over VM state, harder to implement spectators (need to + tee output), subprocess management overhead, can't introspect game state + for GMCP/rich features + +option C: write our own (long-term ideal) + - study mojozork and sussman/zvm architectures + - implement enough opcodes for target games (start with v3) + - async-native from the start + - pros: perfect fit for dreambook philosophy, full control, async-native, + clean integration with MUD internals + - cons: SIGNIFICANT DETOUR. weeks/months before first game runs. opcode + implementation is tedious and error-prone. spec compliance is hard. + +recommendation: START WITH B (dfrozt subprocess), PLAN FOR A (viola embedding). + +rationale: dfrozt gets IF working in hours. players can play zork today. we +learn what the integration needs (spectator broadcasting, save management, +input routing) with real usage. once the architecture is proven, we can +replace the subprocess with embedded viola for better control and state +access. option C remains on the table if viola doesn't fit, but we don't +invest in a VM until we know what the MUD needs from it. + + +the "terminal" game object +========================== + +a terminal is a room object that hosts an IF session. mechanically: + + - a terminal has a z-machine story file (path or blob reference) + - when a player enters the terminal, it pushes IF mode onto their stack + - input from the player routes to the z-machine interpreter + - output from the z-machine routes to the player (and spectators) + - the terminal tracks active sessions (who's playing, current state) + +example from a player's perspective: + + > look + you stand in a neon-lit arcade. rows of terminals hum quietly. + a large screen on the north wall shows "ZORK - The Great Underground + Empire" in green phosphor text. + + jared is standing here, absorbed in a terminal. + sarah is watching jared's game on a nearby screen. + + > use terminal + you sit down at a terminal. the screen flickers to life. + + [IF mode engaged. type 'quit' to exit the game.] + + ZORK I: The Great Underground Empire + Copyright (c) 1981, 1982, 1983 Infocom, Inc. All rights reserved. + ZORK is a registered trademark of Infocom, Inc. + Revision 88 / Serial number 840726 + + West of House + You are standing in an open field west of a white house, with a boarded + front door. + There is a small mailbox here. + + > + +at this point the player's input goes to the z-machine. the terminal object +manages the interpreter subprocess/instance. other players in the room see +"jared is playing zork" in their ambient messages. + + +spectator broadcasting +====================== + +key feature from mojozork's MultiZork: others can watch your game. + +in the MUD: if sarah is in the same room as jared, she can see his game +output in near-realtime. not keystroke-by-keystroke, but command and response. + +implementation: + + - terminal object has a set of active_spectators (player refs) + - when the z-machine sends output to the active player, terminal mirrors + that output to all spectators in the same room + - spectators see: "[jared's game] > n\nYou are in a maze of twisty little + passages, all alike." + - spectators can 'watch jared' to start spectating, 'stop watching' to stop + - spectators are in normal mode, not IF mode. they see their own world + events too. + +this creates communal play. someone stuck on a puzzle can have friends watch +and suggest commands. speedruns become spectator events. IF becomes social. + +technical: output from the z-machine goes to a broadcast function: + + def send_to_player_and_spectators(player, output): + player.send(output) + terminal = player.current_terminal + for spectator in terminal.spectators: + if spectator.location == terminal.location: + spectator.send(f"[{player.name}'s game] {output}") + +spectators don't share game state, just output. they can't send commands to +someone else's game. + + +viola embedding details +======================== + +if we go with option A, here's how viola integration would work: + +replace vio/ with MudZUI: + + class MudZUI: + """viola I/O backend that routes to MUD player sessions""" + + def __init__(self, player, terminal): + self.player = player + self.terminal = terminal + self.output_buffer = [] + + def write(self, text): + """called by VM to send output""" + self.output_buffer.append(text) + + def flush(self): + """send buffered output to player and spectators""" + output = ''.join(self.output_buffer) + self.terminal.broadcast_output(self.player, output) + self.output_buffer.clear() + + def read_line(self): + """called by VM to get player input""" + # return input from player's IF mode input queue + return self.player.if_input_queue.get() + + def read_char(self): + """single-key input for timed events""" + # viola supports this, we'd need to too + return self.player.if_input_queue.get_char() + +wrap the blocking VM loop: + + async def run_if_game(player, terminal, story_path): + """run z-machine in thread pool, bridge to async""" + loop = asyncio.get_event_loop() + + # load story file + with open(story_path, 'rb') as f: + story_data = f.read() + + # create VM with our I/O backend + zui = MudZUI(player, terminal) + zmachine = ZMachine(story_data, zui) + + # run in executor (thread pool) since VM loop is blocking + def run_vm(): + try: + zmachine.run() + except GameOver: + pass # normal exit + + await loop.run_in_executor(None, run_vm) + +state management: + + - viola has full quetzal save/restore + - when player types 'save', VM writes quetzal IFF to bytes + - we store that blob in sqlite: game_saves table (player_id, terminal_id, + save_slot, quetzal_data, timestamp) + - when player types 'restore', we fetch the blob and pass to VM + - viola handles all the z-machine state serialization + +multiple concurrent games: + + - viola uses module-level global state in some places + - need to audit zcode/ and refactor globals into instance state + - each terminal object owns a ZMachine instance + - multiple players can play the same game in different terminals + - each gets their own VM instance with independent state + +this is work, but manageable. viola's clean separation of zcode/ (VM) from +vio/ (I/O) makes the I/O replacement straightforward. the async bridge is a +standard pattern. the global state refactor is the biggest risk. + + +dfrozt subprocess details +========================== + +if we go with option B for prototyping, here's the implementation: + +spawn dfrozt: + + async def run_if_game_subprocess(player, terminal, story_path): + """run dfrotz as subprocess, pipe I/O""" + process = await asyncio.create_subprocess_exec( + 'dfrotz', + '-p', # plain output, no formatting + '-m', # disable MORE prompts + story_path, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + # bridge player input to subprocess stdin + async def input_loop(): + while process.returncode is None: + command = await player.if_input_queue.get() + process.stdin.write(f"{command}\n".encode()) + await process.stdin.drain() + + # bridge subprocess stdout to player and spectators + async def output_loop(): + while process.returncode is None: + line = await process.stdout.readline() + if not line: + break + text = line.decode('utf-8', errors='replace') + terminal.broadcast_output(player, text) + + # run both loops concurrently + await asyncio.gather(input_loop(), output_loop()) + await process.wait() + +save/restore: + + - dfrotz handles saves via its own commands (SAVE, RESTORE) + - save files go to filesystem + - we could intercept SAVE/RESTORE commands, manage files in a + player-specific directory, copy to sqlite for persistence + - or let dfrozt write to /tmp/ and just copy the files on exit + +spectator output: + + - subprocess output goes to all (player + spectators) via broadcast + - tee is automatic — we control the output loop + +exit/cleanup: + + - player types 'quit' or EOF — we send QUIT to process, wait for exit + - process crash — we catch it, log error, tell player "game crashed" + - terminal cleanup — kill subprocess if still running + +pros: simple, robust, dfrozt is battle-tested. +cons: less control. can't introspect VM state for GMCP data. can't implement +custom opcodes. subprocess overhead (but negligible for text games). + + +mojozork patterns to study +=========================== + +MultiZork is mojozork's multiplayer telnet server. it shows: + +1. session management: each player gets a separate VM instance, but the game + world is shared. players see each other's actions. this is DIFFERENT from + our model (each terminal is isolated) but the architecture is instructive. + +2. sqlite persistence: MultiZork stores game state in sqlite. saves are just + snapshots of the VM memory. we'd do the same with quetzal blobs. + +3. command broadcasting: when one player types something, MultiZork echoes it + to others in the same location. same as our spectator model. + +4. pluggable I/O: mojozork's I/O is function pointers: + + typedef struct { + void (*print)(const char *text); + char *(*read_line)(void); + void (*save_state)(const uint8_t *data, size_t len); + uint8_t *(*restore_state)(size_t *len); + } IOCallbacks; + + clean, simple. viola's ZUI is the python equivalent. + +5. tick-based execution: MultiZork runs the VM in a loop, checking for input + each tick. same as our game loop. z-machine opcodes are fast — dozens + execute per tick. + +MultiZork source is excellent reference for "how to embed IF in a multiplayer +server." if we write our own VM (option C), MultiZork is the template. + + +save/restore and persistence +============================= + +z-machine has built-in save/restore opcodes. quetzal is the standard format. + +in our MUD: + + - player types SAVE in the IF game + - VM serializes its state to quetzal IFF (binary format) + - we capture that blob and write to sqlite: + + CREATE TABLE if_saves ( + id INTEGER PRIMARY KEY, + player_id INTEGER NOT NULL, + terminal_id INTEGER NOT NULL, + slot INTEGER DEFAULT 0, + quetzal_data BLOB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(player_id, terminal_id, slot) + ); + + - player types RESTORE, we fetch the blob and pass back to VM + +autosave: + + - when player exits IF mode (QUIT command or connection drop), we could + autosave their state + - on reconnect/re-enter, offer to resume from last autosave + +multiple save slots: + + - z-machine games usually support named saves ("SAVE mysave") + - we could parse the filename and use it as the slot identifier + - or just offer slots 1-10 and map them to quetzal blobs + +cross-session saves: + + - player saves in session A, restores in session B (different terminal) + - works fine if both terminals host the same story file + - could enable "speedrun mode" — reset to a specific save and time from + there + +undo: + + - z-machine has UNDO opcode + - viola implements it via a stack of quetzal snapshots + - we could surface undo as a MUD command outside the game (safety net if + player makes a mistake) + + +if mode on the mode stack +========================== + +when player enters a terminal, push IF mode: + + player.mode_stack.push(Mode.IF) + +IF mode behavior: + + - all input goes to if_input_queue, not command dispatcher + - world events (chat, movement, ambient) are buffered, not displayed + - combat can't start (you're isolated) + - on exit, pop mode, show summary of buffered events + +commands available in IF mode: + + - anything the z-machine game accepts (depends on the game) + - special MUD commands prefixed with / or !: + - /quit — exit the game, pop IF mode + - /save [slot] — trigger VM save + - /restore [slot] — trigger VM restore + - /undo — trigger VM undo + - /help — show IF mode help + +input routing: + + before IF mode: player types "north" → command dispatcher → cmd_move + during IF mode: player types "north" → if_input_queue → z-machine + +the mode stack handles this cleanly. IF mode is just another mode, like combat +or editor. + + +terminal object implementation sketch +====================================== + +rough structure: + + class IFTerminal: + """a room object that hosts z-machine games""" + + def __init__(self, story_path, name, description): + self.story_path = story_path # path to .z3/.z5/.z8 file + self.name = name + self.description = description + self.active_sessions = {} # player -> IFSession + self.spectators = set() # players watching + + async def enter(self, player): + """player sits at terminal, start IF session""" + if player in self.active_sessions: + player.send("You're already playing this game.") + return + + # push IF mode + player.mode_stack.push(Mode.IF) + + # create session (VM instance or subprocess) + session = await self.create_session(player) + self.active_sessions[player] = session + + # announce to room + self.broadcast_except(player, f"{player.name} sits down at {self.name}.") + + async def create_session(self, player): + """spawn VM or subprocess for this player""" + # option A: return ZMachineSession(player, self) + # option B: return DfrotzSession(player, self) + pass + + def broadcast_output(self, player, output): + """send game output to player and spectators""" + player.send(output) + for spectator in self.spectators: + if spectator.location == self.location: + spectator.send(f"[{player.name}'s game]\n{output}") + + async def exit(self, player): + """player leaves terminal, cleanup session""" + session = self.active_sessions.pop(player, None) + if session: + await session.cleanup() + player.mode_stack.pop() # exit IF mode + self.broadcast_except(player, f"{player.name} stands up from {self.name}.") + +terminals are room objects. they could be defined in world data: + + - rooms: + - id: arcade + description: "a dimly lit arcade. terminals line the walls." + objects: + - type: if_terminal + name: "zork terminal" + story: "games/zork1.z3" + description: "an old CRT displaying 'ZORK I' in green text" + +or created by players (builder commands): + + > create terminal "hitchhiker terminal" games/hhgg.z5 + You create a new IF terminal. + + > describe terminal "A sleek modern terminal. The screen reads: + 'The Hitchhiker's Guide to the Galaxy - Don't Panic'" + + +phased implementation plan +=========================== + +phase 1: dfrozt subprocess prototype + - define IFTerminal class + - implement IF mode on mode stack + - spawn dfrozt subprocess for a hardcoded game (zork1.z3) + - route player input to subprocess stdin + - route subprocess stdout to player + - /quit command to exit + goal: player can sit at a terminal and play zork. no saves, no spectators. + estimate: 1-2 days. + +phase 2: spectator broadcasting + - terminal tracks spectators (players in same room) + - tee game output to spectators with prefix + - 'watch [player]' and 'stop watching' commands + goal: others can watch your game in realtime. + estimate: half day. + +phase 3: save/restore via filesystem + - intercept SAVE/RESTORE commands + - manage save files in player-specific directory + - copy to sqlite on exit for persistence + goal: players can save progress and resume later. + estimate: 1 day. + +phase 4: terminal as room object + - define terminals in world data (YAML) + - load at startup, spawn on player enter + - multiple terminals, different games + goal: arcade room with multiple games, each on its own terminal. + estimate: 1 day. + +phase 5: evaluate viola embedding + - fork/vendor viola's zcode/ + - implement MudZUI (I/O backend) + - wrap VM loop with run_in_executor + - test with zork1, compare to dfrozt + goal: decide if viola is better than subprocess. + estimate: 2-3 days. + +phase 6: switch to viola if proven + - replace dfrozt sessions with viola sessions + - refactor terminal to use embedded VM + - quetzal saves to sqlite (direct, no filesystem) + - expose VM state for GMCP (current room, inventory, score) + goal: full control over VM, native python integration. + estimate: 2-3 days (assuming viola works). + +phase 7: builder tools + - 'create terminal' command for admins/builders + - upload .z3/.z5/.z8 files to server + - terminal object editor (change description, story file) + goal: builders can add new IF games without code changes. + estimate: 2 days. + +total estimate: 10-15 days for full IF integration with all features. + +if we stop after phase 4, we have working IF-in-MUD with spectators and saves. +that's enough to prove the concept. phases 5-7 are refinement and tooling. + + +what we learn from this +======================== + +IF integration exercises several MUD systems: + + - mode stack: IF mode is the third mode (after normal and editor). proves + the stack abstraction works for diverse isolation needs. + + - session management: each terminal session is a persistent object (VM or + subprocess) tied to a player. different from stateless command dispatch. + + - room-local broadcasting: spectator output is the first use of "broadcast + to players in this room." will be useful for ambient messages, weather, + room events later. + + - binary blob persistence: quetzal saves are blobs in sqlite. same pattern + will apply to uploaded files, cached terrain chunks, whatever. + + - game objects with behavior: terminals are room objects that do things + (spawn VMs, route I/O). pattern extends to doors, NPCs, triggers. + +getting IF right means the architecture can handle editor mode, crafting +mini-games, puzzles, dialogue trees, anything with isolated state and +custom input handling. + + +risks and unknowns +================== + +viola's lack of tests: + we'd be trusting ~10k lines of untested code. bugs in opcode implementation + would surface as "game doesn't work" with no clear cause. mitigation: test + with a suite of known games (zork, hhgg, curses, anchorhead) before + committing. if bugs emerge, consider dfrozt as the long-term solution. + +async bridge overhead: + viola's blocking loop in a thread pool adds latency. run_in_executor is + designed for this but it's not zero-cost. mitigation: measure latency in + prototype. if it's perceptible (>100ms), consider writing an async-native + VM or sticking with subprocess. + +global state in viola: + haven't audited zcode/ for globals yet. if it's pervasive, the refactor to + instance state could be large. mitigation: audit before committing to viola. + if globals are deeply baked in, subprocess is safer. + +z-machine version support: + viola claims 1-8, mojozork does 3 only. most infocom classics are v3 or v5. + modern IF (inform 7) compiles to v8. if we only care about classics, v3 is + enough. if we want modern games, need v8. mitigation: decide target game + set before choosing VM. + +spectator bandwidth: + if 10 players are watching one game, every line gets sent 11 times (player + + 10 spectators). that's fine for text but worth monitoring. mitigation: + spectator output could be rate-limited or summarized (show every 5 lines, + not every line). + +save file size: + quetzal saves are typically 20-100KB. if player saves frequently, sqlite + grows. mitigation: limit saves per player per game (10 slots?), auto-prune + old saves, or compress quetzal blobs (they're IFF, should compress well). + + +closing +======= + +IF-in-MUD is not a detour. it's a proof that the architecture supports +isolated, stateful, player-driven content. the mode stack, room objects, +save/restore, multiplayer isolation — all tested in a concrete use case. + +start with dfrozt subprocess (phase 1-4). prove the concept. if it works, +consider viola for tighter integration. if viola is risky, subprocess is +fine long-term. mojozork shows that C via subprocess is viable. + +the dream: player-created IF games, built in the editor, tested in-world, +published for others. that requires a DSL and builder tools. but the +foundation is terminals + mode stack + save/restore. get that right and the +dream is achievable. + + +code +---- + +this document docs/how/if-integration.txt +dreambook DREAMBOOK.md +architecture docs/how/architecture-plan.txt diff --git a/docs/how/if-journey.rst b/docs/how/if-journey.rst new file mode 100644 index 0000000..5154064 --- /dev/null +++ b/docs/how/if-journey.rst @@ -0,0 +1,258 @@ +if journey — from arcade terminal to moldable worlds +===================================================== + +This doc tracks the IF (interactive fiction) integration effort for the MUD engine. It's a living document — updated as research progresses and decisions are made. For detailed technical analysis, see the two interpreter audits (``viola-embedding-audit.rst`` and ``zvm-embedding-audit.rst``) and the original integration notes (``if-integration.txt``). + +the vision — five levels of integration +---------------------------------------- + +Five levels emerged from design discussion. They represent a spectrum of how deeply IF worlds integrate with the MUD world, from simplest to most ambitious. + +Level 1 — terminal mode +~~~~~~~~~~~~~~~~~~~~~~~~ + +Subprocess (dfrotz), text in/out, spectators see text scrolling on a screen. Player sits at an arcade terminal, plays Zork. Others in the room see the output. The IF world is opaque — a black box. + +Works today with dfrotz. Proven technology. + +Level 2 — inspectable world +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Embedded interpreter. MUD can READ z-machine state. Know what room the player is in, describe it to spectators, track progress. Spectators don't just see text — they know "jared is in the Trophy Room." Read-only bridge between MUD and IF world. + +Level 3 — moldable world +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Embedded interpreter. MUD can READ AND WRITE z-machine state. Inject items into IF game objects. Put a note in the Zork mailbox. The z-machine object tree (parent/child/sibling with attributes and properties) becomes accessible from MUD code. Two-way bridge. Game world is modifiable from outside. + +Level 4 — shared world +~~~~~~~~~~~~~~~~~~~~~~~ + +Multiple MUD players mapped into the same z-machine instance. Each has their own player object. Independent inventory and position. MojoZork's MultiZork proved this works for V3 games. The IF world is a zone in the MUD that multiple players inhabit simultaneously. + +Level 5 — transcendent world +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Z-machine object tree and MUD entity model are unified. An item in Zork IS a MUD item. Pick it up in the IF world, carry it back to the MUD. The mailbox exists at coordinates in both worlds simultaneously. Full bidirectional entity bridge. + +Note: Level 1 uses subprocess. Levels 2-5 require being inside the interpreter. Once you're at level 2, the jump to 3 is small (reading memory vs writing it). Level 4 is the MojoZork leap. Level 5 is the dream. + +what we know — audit findings +------------------------------ + +Two Python z-machine interpreters were audited in detail. Don't repeat everything here — see the audit docs for details. Focus on decision-relevant facts. + +viola (DFillmore/viola) +~~~~~~~~~~~~~~~~~~~~~~~ + +Can run games today. All V1-V5 opcodes implemented, partial V6-V8. But global state is deeply tangled (13/18 modules with mutable globals). Multiple instances in one process: not feasible without major refactor. + +pygame dependency is cleanly separable (adapter pattern, unidirectional). ``sys.exit()`` in error paths (8+ locations) — needs patching for server use. Memory leaks (5+ unbounded growth patterns) — fixable with cleanup hooks. + +Object tree accessors exist and are wired through all opcodes. Full quetzal save/restore working. + +See: ``docs/how/viola-embedding-audit.rst`` + +zvm (sussman/zvm) +~~~~~~~~~~~~~~~~~ + +Cannot run any game. ~46/108 opcodes implemented, including ZERO input opcodes. But instance-based state (mostly clean, two minor global leaks fixable in ~20 lines). Multiple instances in one process: structurally possible. + +IO abstraction is excellent — purpose-built for embedding (abstract ZUI with stubs). Proper exception hierarchy, zero ``sys.exit()`` calls. No memory leaks, clean bounded state. + +Object tree parser has complete read API, mostly complete write API. BUT: many object opcodes not wired up at CPU level. Save parser works, save writer is stubbed. + +See: ``docs/how/zvm-embedding-audit.rst`` + +The verdict from the audits: "zvm has the architecture you'd want. viola has the implementation you'd need." + +MojoZork (C, Ryan C. Gordon) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +V3 only but has working multiplayer telnet server (MultiZork). Proves multiplayer z-machine works (up to 4 players, separate inventories). ``runInstruction()`` for single-step execution — the async pattern we want. Excellent reference architecture, not directly usable (C only). + +the hybrid path — option D +--------------------------- + +This emerged from comparing the audits side by side. Use zvm's architecture (clean IO abstraction, instance-based state, exception model) as the skeleton. Port viola's working opcode implementations into it. + +Why this is attractive: + +- zvm's opcodes take ``self`` (ZCpu) and access state through instance attributes +- viola's opcodes use module-level globals (``zcode.game.PC``, ``zcode.memory.data``, etc) +- porting means translating global lookups to instance attribute lookups +- that's mechanical, not creative — could port 5-10 opcodes per hour once the pattern is established +- gets the clean design with the working code + +Why this could be harder than it sounds: + +- viola and zvm may represent z-machine internals differently +- memory layout assumptions, stack frame format, string encoding details +- porting opcodes may require porting the data structures they operate on +- need to verify each ported opcode against the z-machine spec, not just translate + +Estimated effort: medium. Less than finishing zvm from scratch, less than refactoring viola's globals. But not trivial. + +the object tree — key to moldable worlds +----------------------------------------- + +This is what makes levels 3-5 possible. The z-machine has an object tree — every game entity is a node with parent/child/sibling pointers, attributes (boolean flags), and properties (variable-length data). + +What the object tree gives us: + +- Read what room the player is in (player object's parent) +- Read container contents (children of the container object) +- Inject items (create objects, parent them to containers) +- Modify game state (set/clear attributes, change properties) +- Query the dictionary (what words the parser recognizes) + +Both interpreters have object tree parsers: + +- viola: complete read + write, all opcodes wired, working +- zvm: complete read, mostly complete write (missing ``set_attr``/``clear_attr``), many opcodes unwired at CPU level, bug in ``insert_object`` + +The dictionary problem (level 3+) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Injecting an object into the mailbox works for "look in mailbox" — the game iterates children and prints short names. But "take [new item]" fails unless the word exists in the game's dictionary. The dictionary is baked into the story file. + +Options: + +- Add words to dictionary at runtime (memory surgery — relocating/expanding the dictionary) +- Intercept input before the parser and handle custom items at the MUD layer +- Use existing dictionary words for injected items ("note", "scroll", "key" are common) +- Hybrid: intercept unrecognized words, check if they match MUD-injected items, handle outside z-machine + +This is a level 3-5 problem. Not a blocker for levels 1-2. + +games we care about +------------------- + +The games that motivated this work: + +The Wizard Sniffer (Buster Hudson, 2017) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You play a pig who sniffs out wizards. IFComp winner, XYZZY winner. Screwball comedy. The pig/wizard game that started this. Z-machine format. Would need V5 support. + +Lost Pig (Admiral Jota, 2007) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Grunk the orc chases an escaped pig. IFComp winner, 4 XYZZY awards. Famous for its responsive parser and comedy writing. Z-machine format. V5. + +Zork I, II, III +~~~~~~~~~~~~~~~ + +The classics. Everyone should play them. V3. + +Hitchhiker's Guide to the Galaxy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Funny, frustrating, great spectator game. V3. + +Also: Anchorhead, Photopia, Spider and Web, Shade, Colossal Cave. + +V3 covers the Infocom catalog. V5 covers most modern IF including the pig games. V8 covers big modern Inform 7 games but is lower priority. + +Note: no Python Glulx interpreter exists. Games that target Glulx (some modern Inform 7) are out of scope unless we subprocess to a C interpreter. + +architecture fit +---------------- + +How this fits the existing MUD architecture. The codebase is ready: + +Mode stack +~~~~~~~~~~ + +Push "if" mode. All input routes to IF handler, bypassing command dispatch. Same pattern as editor mode. Already proven. + +Input routing +~~~~~~~~~~~~~ + +``server.py`` shell loop checks ``player.mode``. Add elif for "if" mode, route to ``if_game.handle_input()``. Same as ``editor.handle_input()``. + +Room-local broadcasting +~~~~~~~~~~~~~~~~~~~~~~~~ + +Not implemented yet. IF integration is the first use case. Terminal object maintains spectator list, broadcasts output to all. Pattern will be reused for ambient messages, weather, room events. + +State storage +~~~~~~~~~~~~~ + +Quetzal saves as blobs in SQLite. Same pattern as other binary persistence. + +Terminal game object +~~~~~~~~~~~~~~~~~~~~ + +A room object (or coordinate-anchored object) that hosts IF sessions. Players "use" it to enter IF mode. Pattern extends to other interactive objects. + +open questions +-------------- + +Things we haven't figured out yet. Update this as questions get answered. + +1. V3 opcode footprint +~~~~~~~~~~~~~~~~~~~~~~ + +How many of the ~62 missing zvm opcodes are actually exercised by V3 games? V3 uses a smaller subset. If we target V3 first, the hybrid might need 30 ported, not 62. Research: run a V3 game through viola with opcode tracing, collect the set. + +2. zvm/viola memory layout compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Do they represent z-machine memory the same way? Both use bytearrays, but header parsing, object table offsets, string encoding — are these compatible enough that porting opcodes is translation, or is it a deeper rewrite? + +3. Async model +~~~~~~~~~~~~~~ + +Both interpreters have blocking run loops. Options: + +- ``run_in_executor`` (thread pool) — standard pattern, adds latency +- extract ``step()`` and call from async loop — zvm audit says this is ~5 lines +- run in separate thread with queue-based IO — more complex but natural + +Which is best for the MUD's tick-based game loop? + +4. Multiplayer z-machine +~~~~~~~~~~~~~~~~~~~~~~~~ + +MojoZork does this for V3. What would it take for V5? The V5 object model is larger (65535 objects vs 255). Do V5 games assume single-player in ways that break multiplayer? + +5. Game file licensing +~~~~~~~~~~~~~~~~~~~~~~ + +Infocom games are abandonware but not legally free. Modern IF games (Lost Pig, Wizard Sniffer) are freely distributable. Need to figure out what we can bundle vs what players bring. + +6. Dictionary injection feasibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +How hard is it to add words to a z-machine dictionary at runtime? The dictionary is in static memory. Adding words means expanding it, which means relocating it if there's no space. Is this practical? + +what to do next +--------------- + +Concrete next steps, roughly ordered. Update as items get done. + +- [ ] trace V3 opcode usage: run zork through viola with opcode logging, get the actual set of opcodes a real game uses. this tells us how much porting work the hybrid path actually requires. + +- [ ] compare memory layouts: look at how viola and zvm represent z-machine memory, object tables, string tables. determine if opcode porting is mechanical translation or deeper adaptation. + +- [ ] prototype the hybrid: pick 5-10 common opcodes, port them from viola to zvm's architecture. see how the pattern feels. if it's smooth, the hybrid is viable. if every opcode is a battle, reconsider. + +- [ ] build level 1 prototype: regardless of interpreter choice, implement the terminal object, IF mode, and subprocess dfrotz path. this proves the MUD-side architecture (mode stack, spectators, save/restore) independently of the interpreter question. + +- [ ] study MojoZork's multiplayer model: read the MultiZork source for how it handles multiple players in one z-machine. document the pattern for our eventual level 4. + +- [ ] find the game files: locate freely distributable z-machine story files for the games we care about. Wizard Sniffer, Lost Pig, Zork (if legally available). + +related documents +----------------- + +``docs/how/if-integration.txt`` — original research and integration plan (predates audits) + +``docs/how/viola-embedding-audit.rst`` — detailed viola architecture audit + +``docs/how/zvm-embedding-audit.rst`` — detailed zvm architecture audit with comparison + +``docs/how/architecture-plan.txt`` — MUD engine architecture plan + +``DREAMBOOK.md`` — project vision and philosophy diff --git a/docs/how/viola-embedding-audit.rst b/docs/how/viola-embedding-audit.rst new file mode 100644 index 0000000..e44d870 --- /dev/null +++ b/docs/how/viola-embedding-audit.rst @@ -0,0 +1,156 @@ +============================= +viola embedding audit — Z-machine interpreter feasibility +============================= + +viola is a Python Z-machine interpreter being evaluated for embedding in mudlib +to run interactive fiction games over telnet. this audit covers architecture, +isolation requirements, and modification paths. + + +1. global state — fork-level entangled +======================================= + +13 of 18 modules in zcode/ have mutable module-level globals. + +critical three: + +- game.py — PC, callstack, currentframe, undolist, interruptstack. modified by + 20+ functions across multiple modules via global declarations +- memory.py — data (entire Z-machine memory as a bytearray). read/written by + nearly every module +- screen.py — currentWindow, zwindow list, color spectrum dict. all mutated at + runtime + +every module does ``import zcode`` then accesses ``zcode.game.PC``, +``zcode.memory.data``, etc. hardcoded module-level lookups, not +dependency-injected. state mutation touches 1000+ function calls. + +wrapping in a class would require threading a state parameter through the +entire codebase. process-level isolation (subprocess per game) is the realistic +path for concurrent games. + + +2. pygame boundary — clean +=========================== + +zero pygame imports in zcode/. architecture uses adapter pattern: + +- zcode/ imports only vio.zcode as io (aliased) +- vio/zcode.py contains ALL pygame calls (1044 lines) +- dependency is unidirectional — vio.zcode never imports from zcode/ +- zcode/ extends io classes via inheritance (window, font, sound channels) + +can swap vio/zcode.py for a telnet IO adapter without touching VM core. + + +3. error paths — server-hostile +================================ + +8+ locations call sys.exit() directly: + +- memory out-of-bounds (5 locations in memory.py) +- invalid opcode (opcodes.py) +- division by zero (numbers.py) +- corrupt story file (viola.py) + +fatal path: ``zcode.error.fatal()`` -> ``sys.exit()``. not catchable exceptions. + +fix: patch error.fatal() to raise a custom exception instead. + + +4. version coverage — V1-V5 solid, V6 partial, V7-V8 theoretical +================================================================= + +- V1-V5: all opcodes implemented. most IF games are V3 or V5. +- V6: graphics framework exists but 3 opcodes marked "unfinished" +- V7-V8: supported at memory/opcode level but likely never tested +- no test suite in the repo + +for a MUD, V3 and V5 cover Zork, Curses, Anchorhead, etc. + + +5. input model — moderate-to-hard adapter needed +================================================= + +line input (READ): + easy — maps naturally to telnet line mode + +single char (READ_CHAR): + hard — needs raw telnet mode or buffering + +timed input: + hard — needs async server-side timer + +mouse input: + impossible — stub it + +arrow/function keys: + moderate — parse ANSI escape sequences + +pygame is hardwired with no abstraction layer. need to create a TelnetInput +class implementing the same interface as vio.zcode.input. interface surface is +small: getinput, starttimer, stoptimer. + + +6. step execution mode — moderate refactor, feasible +===================================================== + +main loop in routines.py:60 is clean:: + + while not quit and not restart: + check interrupts -> decode(PC) -> runops(oldpc) + +one instruction = decode + execute. only blocking call is pygame.event.wait() +at bottom of input stack. natural yield points are input requests. + +execute_one() API achievable by: + +1. making IO non-blocking +2. converting z_read()'s inner while loop to a state machine +3. unwrapping recursive interrupt_call() -> execloop() pattern + + +7. memory/cleanup — will leak, fixable +======================================= + +unbounded growth: + +- undo stack (game.py:23) — each undo saves full memory copy (MBs). never + cleared. +- command history (input.py:22) — every input appended forever +- static routine cache (routines.py:30) — never cleared +- static word cache (memory.py:108) — never cleared +- object caches (objects.py:18-26) — 4 dicts, never purged + +estimate: 10-100+ MB/hour depending on undo usage. fixable by adding cleanup +calls to restart/setup path. + + +8. IFIFF submodule — quetzal is independent +============================================ + +- quetzal (saves) and blorb (multimedia) are architecturally separate +- zcode/game.py imports only quetzal — no blorb references in save/restore +- quetzal API: qdata(), save(), restore() +- pure stdlib, no external deps +- can cherry-pick quetzal.py without pulling in blorb + + +priority for upstream PRs +========================== + +1. patch error.fatal() to raise instead of sys.exit() — small effort, unblocks + server use +2. add cleanup hooks for caches/undo on restart — small effort, fixes memory + leaks +3. create IO abstraction interface — medium effort, enables telnet adapter +4. step execution mode (execute_one()) — medium effort, enables async embedding +5. extract quetzal as standalone — small effort, clean dependency + + +pragmatic path +============== + +the global state issue is bypassed with subprocess isolation (one Python +process per game). fine for a MUD with less than 100 concurrent players. +process-per-game sidesteps the entire global state refactor. diff --git a/docs/how/zvm-embedding-audit.rst b/docs/how/zvm-embedding-audit.rst new file mode 100644 index 0000000..290a992 --- /dev/null +++ b/docs/how/zvm-embedding-audit.rst @@ -0,0 +1,377 @@ +============================= +zvm embedding audit — Z-machine interpreter feasibility +============================= + +zvm is a Python Z-machine interpreter (by Ben Collins-Sussman) being evaluated +for embedding in mudlib to run interactive fiction games over telnet. this audit +covers architecture, isolation requirements, and modification paths. compared +against the viola audit for apples-to-apples decision-making. + + +1. global state — mostly clean, two leaks +========================================== + +state is instance-based. ``ZMachine.__init__`` wires everything together:: + + ZMemory(story) -- memory + ZStringFactory(mem) -- string decoding + ZObjectParser(mem) -- object tree + ZStackManager(mem) -- call/data stacks + ZOpDecoder(mem, stack) -- instruction decoder + ZStreamManager(mem, ui) -- I/O streams + ZCpu(mem, opdecoder, stack, objects, string, streams, ui) -- CPU + +dependency graph flows one way: ZCpu depends on everything else, everything +else depends on ZMemory. no circular dependencies. clean layering. + +two global leaks: + +- zlogging.py — executes at import time. opens debug.log and disasm.log in cwd, + sets root logger to DEBUG. all instances share the same loggers +- zcpu.py uses ``random.seed()`` / ``random.randint()`` — global PRNG state. + multiple instances would interfere with each other's randomness + +both fixable with ~20 lines. the logging can be made instance-scoped, the PRNG +replaced with ``random.Random()`` instances. + +compared to viola's 13/18 modules with mutable globals, this is dramatically +better. multiple ZMachine instances in one process is structurally possible. + + +2. IO boundary — excellent, purpose-built for embedding +======================================================== + +this is zvm's strongest feature. the README explicitly states the design goal: +"no user interface. meant to be used as the backend in other programs." + +IO is fully abstracted via a four-component ``ZUI`` object (zui.py):: + + ZUI(audio, screen, keyboard_input, filesystem) + +each component is an abstract base class with NotImplementedError stubs: + +- ZScreen (zscreen.py) — write(), split_window(), select_window(), + set_cursor_position(), erase_window(), set_text_style(), set_text_color() +- ZInputStream (zstream.py) — read_line(), read_char() with full signatures + for timed input, max length, terminating characters +- ZFilesystem (zfilesystem.py) — save_game(), restore_game(), + open_transcript_file_for_writing/reading() +- ZAudio (zaudio.py) — play_bleep(), play_sound_effect() + +trivialzui.py is the reference stdio implementation showing how to subclass. + +for MUD embedding: implement ZScreen.write() to push to telnet session, +ZInputStream.read_line() to receive from telnet reader, ZFilesystem to store +saves in SQLite. natural fit. + +compared to viola where pygame is hardwired and you'd need to create a +TelnetInput class from scratch, zvm hands you the interface contract. + + +3. error paths — server-friendly +================================= + +zero sys.exit() calls in the zvm/ package. only sys.exit() is in the CLI +runner run_story.py, which is appropriate. + +clean exception hierarchy: + +- ZMachineError (zmachine.py) +- ZCpuError -> ZCpuIllegalInstruction, ZCpuDivideByZero, ZCpuNotImplemented +- ZMemoryError -> ZMemoryIllegalWrite, ZMemoryOutOfBounds, ZMemoryBadMemoryLayout +- ZObjectError -> ZObjectIllegalObjectNumber, ZObjectIllegalAttributeNumber +- ZStackError -> ZStackNoRoutine, ZStackNoSuchVariable, ZStackPopError +- QuetzalError -> QuetzalMalformedChunk, QuetzalMismatchedFile + +CPU run loop does not catch exceptions — errors propagate to the caller. +correct behavior for embedding. + +one weakness: some opcodes have bare ``assert`` statements that would be +stripped with ``python -O``. + +compared to viola's 8+ sys.exit() calls via error.fatal(), this is exactly +what you want for a server. + + +4. version coverage — V1-V5 declared, V3-V5 structural +======================================================== + +zmemory.py handles v1-v5 with version-switched code throughout. v6-v8 are +not supported (raises ZMemoryUnsupportedVersion). + +opcode table (zcpu.py) has version annotations like ``(op_call_2s, 4)`` +meaning "available from v4 onward". systematic and well-done. + +v1-v3 object model uses 1-byte pointers (max 255 objects), v4-v5 uses +2-byte (max 65535). properly handled in zobjectparser.py. + +test suite only uses curses.z5 — v1-v3 support exists structurally but has +untested code paths. + +compared to viola's solid V1-V5 plus partial V6 and theoretical V7-V8, +zvm is narrower but the architecture is cleaner where it does exist. + + +5. input model — abstracted but unimplemented +=============================================== + +line input (read_line): + interface complete — full signature with original_text, max_length, + terminating_characters, timed_input_routine, timed_input_interval. + trivial implementation handles basic line editing, ignores timed input. + +single char (read_char): + interface complete — signature includes timed_input_routine and + timed_input_interval. CPU explicitly raises ZCpuNotImplemented if + time/routine are nonzero. + +timed input: + feature flag system exists (``features["has_timed_input"] = False``). + completely unimplemented at CPU level. + +**critical problem**: op_sread (v1-v3) has an empty body — silently does +nothing. op_sread_v4 and op_aread (v5) both raise ZCpuNotImplemented. +the interpreter cannot accept text input. it cannot reach the first prompt +of any game. + +compared to viola where input works and needs an adapter, zvm has the +better interface design but no working implementation behind it. + + +6. step execution mode — not available, easy refactor +====================================================== + +run loop in zcpu.py:: + + def run(self): + while True: + (opcode_class, opcode_number, operands) = self._opdecoder.get_next_instruction() + implemented, func = self._get_handler(opcode_class, opcode_number) + if not implemented: + break + func(self, *operands) + +tight while-True with no yielding. no step() method, no async support, no +callbacks between instructions, no instruction count limit. + +the loop body is clean and self-contained though. extracting step() is a ~5 +line change — pull the body into a method, call it from run(). + +for async MUD embedding, options are: + +1. extract step() and call from an async loop with awaits at IO points +2. run each ZMachine in a thread (viable since state is instance-based) + +compared to viola's similar tight loop, the refactor here is actually easier +because the body is simpler (no interrupt handling, no recursive execloop). + + +7. memory/cleanup — clean, no leak risk +========================================= + +memory is a bytearray, fixed size, bounded by story file. +ZMachine.__init__ creates two copies (pristine + working). + +ZStackManager uses a Python list as call stack. frames are popped in +finish_routine(). bounded by Z-machine stack design. + +ZStringFactory, ZCharTranslator, ZLexer all pre-load from story file at init. +bounded by file content. + +ZObjectParser does not cache anything — every access reads directly from +ZMemory. + +one minor leak: quetzal.py QuetzalParser stores self._file and closes it +manually but does not use a context manager. exception during parsing would +leak the file handle. + +compared to viola's 5+ unbounded growth patterns (undo stack, command history, +routine cache, word cache, object caches), zvm is dramatically cleaner. + + +8. object tree API — complete read, mostly complete write +========================================================== + +zobjectparser.py public API: + +read: +- get_attribute(objectnum, attrnum) — single attribute (0/1) +- get_all_attributes(objectnum) — list of all set attribute numbers +- get_parent(objectnum) — parent object number +- get_child(objectnum) — first child +- get_sibling(objectnum) — next sibling +- get_shortname(objectnum) — object's short name as string +- get_prop(objectnum, propnum) — property value +- get_prop_addr_len(objectnum, propnum) — property address and length +- get_all_properties(objectnum) — dict of all properties +- describe_object(objectnum) — debug pretty-printer + +write: +- set_parent(objectnum, new_parent_num) +- set_child(objectnum, new_child_num) +- set_sibling(objectnum, new_sibling_num) +- insert_object(parent_object, new_child) — handles unlinking +- set_property(objectnum, propnum, value) + +missing: set_attribute() and clear_attribute(). the parser can read attributes +but cannot set them. would need to be added. + +bug in insert_object(): sibling walk loop at line 273 never advances current +or prev — would infinite-loop. needs fix. + +**critical for MUD embedding**: many CPU opcodes that USE the parser are +unimplemented at the CPU level: + +- op_test_attr, op_set_attr, op_clear_attr — not wired up +- op_get_sibling — not wired up (parser method exists) +- op_jin (test parent) — not wired up +- op_remove_obj, op_print_obj — not wired up +- op_get_prop_addr, op_get_next_prop, op_get_prop_len — not wired up + +the parser infrastructure is there and correct. the CPU just doesn't call it. + +compared to viola's zcode/objects.py which has working accessors wired through +all opcodes, zvm has a better parser design but the plumbing is incomplete. + + +9. save/restore — parse works, write is stubbed +================================================= + +quetzal.py QuetzalParser can parse Quetzal save files: +- IFhd chunks (metadata) — working +- CMem chunks (compressed memory) — working +- UMem chunks (uncompressed memory) — has a bug (wrong attribute name) +- Stks chunks (stack frames) — working + +QuetzalWriter is almost entirely stubbed. all three data methods return "0". +file writing logic exists but writes nonsense. + +all save/restore CPU opcodes (op_save, op_restore, op_save_v4, op_restore_v4, +op_save_v5, op_restore_v5, op_save_undo, op_restore_undo) raise +ZCpuNotImplemented. + +alternative for MUD: since ZMemory is a bytearray, could snapshot/restore +raw memory + stack state directly without Quetzal format. + +compared to viola's working quetzal implementation, zvm's save system needs +significant work. + + +10. test suite — minimal +========================= + +5 registered test modules: + +- bitfield_tests.py — 6 tests, BitField bit manipulation +- zscii_tests.py — 4 tests, string encoding/decoding +- lexer_tests.py — 3 tests, dictionary parsing +- quetzal_tests.py — 2 tests, save file parsing +- glk_tests.py — 7 tests, requires compiled CheapGlk .so + +not tested at all: ZCpu (no opcode tests), ZMemory, ZObjectParser, +ZStackManager, ZOpDecoder, ZStreamManager. + +all tests that need a story file use stories/curses.z5 with hardcoded paths. + +compared to viola which has no tests at all, zvm has some but they don't +cover the critical subsystems. + + +11. dependencies — zero +======================== + +pure stdlib. setup.py declares no dependencies. python >= 3.6. + +uses: logging, random, time, itertools, re, chunk, os, sys. +ctypes only for optional Glk native integration. + +both viola and zvm are pure stdlib. no advantage either way. + + +12. completeness — the dealbreaker +==================================== + +of ~108 Z-machine opcodes, zvm implements approximately 46. the remaining ~62 +are stubbed with ZCpuNotImplemented or have empty bodies. + +unimplemented opcodes include fundamentals: + +- ALL input opcodes (op_sread, op_aread) — cannot accept player input +- ALL save/restore opcodes — cannot save or load games +- critical object opcodes (test_attr, set_attr, clear_attr, get_sibling, + remove_obj, print_obj at CPU level) +- many branch/comparison ops +- string printing variants + +the interpreter cannot execute any real interactive fiction game to +completion. it cannot reach the first prompt of Zork. + + +verdict — comparison with viola +================================ + ++---------------------+--------------+----------------+ +| criterion | zvm | viola | ++---------------------+--------------+----------------+ +| IO abstraction | excellent | needs adapter | +| global state | mostly clean | deeply tangled | +| multi-instance | structurally | process-only | +| error handling | exceptions | sys.exit() | +| memory leaks | none | 5+ patterns | +| object tree parser | complete | complete | +| object tree opcodes | ~half wired | all wired | +| opcode coverage | ~46/108 | all V1-V5 | +| can run a game | NO | YES | +| input handling | abstracted | working | +| save/restore | parse only | working | +| dependencies | zero | zero | +| tests | minimal | none | +| maintenance | abandoned | abandoned | ++---------------------+--------------+----------------+ + +zvm has the architecture you'd want. viola has the implementation you'd need. + +zvm is a well-designed skeleton — clean IO abstraction, instance-based state, +proper exceptions, no memory leaks. but it's roughly half-built. finishing the +62 missing opcodes is weeks of work equivalent to writing a new interpreter, +except you're also debugging someone else's partial implementation. + +viola is a working interpreter with terrible architecture for embedding — +global state everywhere, sys.exit() in error paths, pygame hardwired. but it +can run Zork right now. the refactoring targets are known and bounded. + + +pragmatic path +=============== + +the "moldable world" vision (levels 3-5 from the design discussion) requires +being inside the interpreter with access to the object tree. both interpreters +have the parser infrastructure for this. + +option A — fix viola's embedding problems: + 1. patch error.fatal() to raise (small) + 2. swap pygame IO for telnet adapter (medium) + 3. add cleanup hooks for caches (small) + 4. subprocess isolation handles global state (free) + total: working IF in a MUD, with known limitations on multi-instance + +option B — finish zvm's implementation: + 1. implement ~62 missing opcodes (large) + 2. fix insert_object bug (small) + 3. add set_attribute/clear_attribute to parser (small) + 4. complete save writer (medium) + total: clean embeddable interpreter, but weeks of opcode work first + +option C — write our own interpreter: + designed for embedding from day one. state is a first-class object. object + tree is an API. multiple games in one process. but it's the longest path and + testing against real games is the hard part. + +option D — hybrid: + use zvm's architecture (ZUI interface, exception model, instance-based state) + as the skeleton. port viola's working opcode implementations into it. gets + the clean design with the working code. medium effort, high reward. + +the hybrid path is probably the most interesting. zvm got the hard design +decisions right. viola got the hard implementation work done. merging the two +is less work than either finishing zvm or refactoring viola.