676 lines
25 KiB
Text
676 lines
25 KiB
Text
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
|