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 <player>" 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.
