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.