From bc5e829f6bbb0e0ebe350aa152bc7d8e5212ee16 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Mon, 9 Feb 2026 12:25:44 -0500 Subject: [PATCH] Add a terminal plan --- docs/plans/if-terminal.txt | 264 +++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 docs/plans/if-terminal.txt diff --git a/docs/plans/if-terminal.txt b/docs/plans/if-terminal.txt new file mode 100644 index 0000000..131f157 --- /dev/null +++ b/docs/plans/if-terminal.txt @@ -0,0 +1,264 @@ +if-terminal — playing interactive fiction from the mud +====================================================== + +goal: sit at a terminal in the mud, boot up zork, play it. other players +in the room see text scrolling. single player for now, but designed so +spectating and "follow" interactions aren't foreclosed. + +references: + docs/how/if-journey.rst — five levels of integration, interpreter audits + docs/how/if-integration.txt — original research, subprocess vs embed analysis + docs/how/mojozork-audit.rst — multiplayer z-machine proof (reference only) + docs/how/viola-embedding-audit.rst — viola interpreter audit + docs/how/zvm-embedding-audit.rst — zvm interpreter audit + + +what "playing zork" means +========================= + +a player walks into a room. there's something there — a terminal, a cabinet, +a glowing orb, whatever. they type a command to start playing. their input +now routes to dfrotz (z-machine interpreter) instead of the mud command +dispatcher. they're IN zork. + +they're still a mud entity in the room. they haven't vanished. other players +see them sitting at the terminal. when they quit zork, they're back to normal +mud mode. their zork save persists. + + +the spectator question +====================== + +when player A is playing zork, player B walks into the room and sees: + + Jared is here, hunched over a glowing terminal. + + > open mailbox + Opening the small mailbox reveals a leaflet. + +player B sees the output scrolling. they're spectating. they didn't choose +to — they just see it because they're in the room. + +this is level 1 from if-journey.rst. the IF world is opaque (we don't know +what room the player is in, we can't inject items), but the TEXT is visible. +subprocess dfrotz, text piped in/out, broadcast to room. + + +the follow question +=================== + +"follow " doesn't exist yet. when it does: + +- following player A means you auto-move when they move between rooms +- if A sits at a terminal and enters zork, you DON'T auto-enter zork +- you're in the same room, you see the text (spectator mode) +- if A gets up from the terminal and walks away, you follow them out + +this is the right default. entering zork is a deliberate choice, not +something that happens because you're following someone. + +future possibility (NOT this plan): two players both playing zork in the +same room. each has their own dfrotz process. each sees their own game. +they see "ghost" indicators of each other — "a faint shimmer suggests +Jared is nearby" — but can't truly interact through zork's parser. this +is cosmetic multiplayer, not real shared-world (level 4). it's achievable +with level 1 architecture: just track which IF room each player is in +(parse the room name from output) and show ghosts when rooms match. a +fun enhancement that doesn't require embedded interpreters. + + +what we're building +=================== + +phase 0 — prerequisites +------------------------ + +- install dfrotz (user action: sudo dnf install frotz or equivalent) +- zork1.z3 is already at content/stories/zork1.z3 (from mojozork, free release) +- verify dfrotz can run the story file standalone + + +phase 1 — if session handler +------------------------------ + +the core. equivalent of editor.py but for interactive fiction. + +new file: src/mudlib/if_session.py + + IFSession class: + process: asyncio.subprocess.Process (dfrotz child) + player: Player (who's playing) + spectators: list[Player] (others in room, updated dynamically) + story_path: str (path to .z3/.z5 file) + game_name: str ("zork1") + save_dir: str (per-player save directory) + + async start(): + spawn dfrotz as async subprocess (stdin=PIPE, stdout=PIPE, stderr=PIPE) + read initial output (game intro text) + broadcast intro to player + spectators + + async handle_input(text: str) -> IFResponse: + if text is a mud escape command (e.g. "::quit", "::save"): + handle at mud layer (quit session, force save, etc) + else: + write text to dfrotz stdin + read response from dfrotz stdout + broadcast response to player + spectators + return IFResponse(output=response, done=False) + + on "::quit" or dfrotz process exit: + return IFResponse(output=goodbye_text, done=True) + + async stop(): + save game state (send "save" to dfrotz, capture file) + terminate dfrotz process + cleanup + + dfrotz flags: + -p (plain text, no formatting escapes) + -w N (screen width, match terminal or default 80) + possibly -h N (screen height) + + escape prefix "::": + ::quit — leave the terminal, return to mud + ::save — force save (dfrotz save command) + ::help — show escape commands + regular input goes straight to dfrotz + + +phase 2 — mode stack integration +---------------------------------- + +wire IFSession into the existing mode stack, following the editor pattern. + +server.py shell loop addition: + + elif player.mode == "if" and player.if_session: + response = await player.if_session.handle_input(inp) + if response.output: + await player.send(response.output) + if response.done: + player.if_session = None + player.mode_stack.pop() + +player.py addition: + + if_session: IFSession | None = None + +entering IF mode: + + player.if_session = IFSession(player, story_path, ...) + await player.if_session.start() + player.mode_stack.append("if") + + +phase 3 — the play command +---------------------------- + +how a player starts a game. for now, a simple command. terminal objects +come later. + + play zork + +this: +1. looks up "zork" in available stories (content/stories/) +2. creates IFSession with the story file +3. pushes "if" mode onto mode stack +4. player sees zork's intro text + +later this becomes "use terminal" or an interaction with a room object. +for now, a direct command keeps things simple and testable. + + +phase 4 — spectator broadcasting +---------------------------------- + +when a player is in IF mode, other players in the same room see output. + +on IFSession output: + send to player (the one playing) + for each other player in the same room: + send formatted version: + [Jared's terminal] + > open mailbox + Opening the small mailbox reveals a leaflet. + +spectator list updates when players enter/leave the room. + +"look" in the room shows: + Jared is here, playing on a glowing terminal. + +this needs the room-local broadcasting pattern mentioned in if-integration.txt. +it's the first use case for ambient output in a room. + + +phase 5 — save/restore persistence +------------------------------------ + +player's zork progress persists across sessions. + +- saves stored per-player per-game (e.g. saves/jared/zork1.sav) +- on "play zork": check for existing save, offer to restore +- on "::quit": auto-save before exiting +- dfrotz handles save/restore natively via its save command +- we just need to manage the save file locations + +could use SQLite blobs (like the persistence doc suggests) or just +filesystem saves. filesystem is simpler for dfrotz since it already +writes save files. start with filesystem. + + +what we're NOT building (yet) +============================= + +- embedded interpreter (levels 2-5). dfrotz subprocess is the whole story. +- multiplayer z-machine. one player per dfrotz process. +- terminal room objects. "play" command is the entry point for now. +- follow command. not in scope but the design accommodates it. +- ghost presence in IF rooms. cool idea, deferred. +- item bridge between IF and mud worlds. level 5, way out. +- non-zork games. architecture supports any z-machine game but we test + with zork1 only. + + +open questions +============== + +1. dfrotz output reading + dfrotz doesn't have a clean "here's one response" delimiter. it writes + to stdout and then waits for input. we need to detect when it's done + writing. options: read with timeout, look for the ">" prompt, or use + expect-style pattern matching. needs experimentation. + +2. save file management + dfrotz save files are opaque blobs. where do we store them? how do we + name them? per-player directory under a saves/ dir? or blob them into + sqlite? + +3. screen width + dfrotz formats text to a width. do we detect the player's terminal + width (NAWS) and pass it to dfrotz? or use a fixed width? if the + player resizes, can we tell dfrotz? + +4. escape prefix + "::" is arbitrary. could conflict with game input (unlikely for IF + but possible). alternatives: ".", "/", or a control character. + +5. spectator bandwidth + if 10 people are in the room, every zork output goes to 10 players. + is this a performance concern? probably not at our scale, but worth + noting. + + +estimated phases and what each delivers +======================================== + +phase 0: can run zork standalone in a terminal +phase 1-2: can play zork inside the mud ("play zork", input routed, "::quit" to leave) +phase 3: "play zork" is a real command, discoverable +phase 4: others in the room see the game text +phase 5: progress saves between sessions + +phases 1-3 are the MVP. a player can sit in the mud, play zork, and quit. +phases 4-5 make it social and persistent.