if terminal — how the interactive fiction system works
======================================================

what it is
==========

players can play z-machine interactive fiction games (zork, etc) from inside
the mud. input routes to a dfrotz subprocess instead of the command dispatcher.
other players in the room see the output scrolling on a virtual terminal.

this is level 1 from if-journey.rst — the IF world is a black box. we pipe
text in and out. no interpreter embedding, no VM introspection.


how it works
============

three pieces: the session, the command, and the server integration.


IFSession (if_session.py)
-------------------------

manages a single dfrotz subprocess. one session per player.

spawns dfrotz with:
    dfrotz -p -w 80 -m <story_path>

    -p  plain ASCII (no formatting escapes)
    -w  80-column width
    -m  no MORE prompts (continuous output)

IFResponse dataclass carries output + done flag back to caller.

input flow:
    player types something
    -> server routes to if_session.handle_input()
    -> escape commands (::quit, ::save, ::help) handled at mud layer
    -> everything else written to dfrotz stdin
    -> response read from dfrotz stdout
    -> IFResponse returned to server

reading output:
    dfrotz writes text then shows a ">" prompt and waits for input.
    _read_response() reads in 1024-byte chunks until it detects the prompt.
    prompt detection checks for "> ", ">\n", ">\r\n" at end of raw bytes,
    and "\n>" in the stripped text. bare ">" with no preceding newline
    is only stripped when the raw bytes confirm a prompt pattern.

    idle timeout: 100ms (returns once data stops flowing)
    overall deadline: 5 seconds per response
    this was tricky to get right — see docs/lessons/dfrotz-prompt-detection.txt


escape commands
---------------

prefixed with :: to avoid conflicting with game input.

    ::quit   exit game (auto-saves first)
    ::save   force save to disk
    ::help   show available escape commands


play command (commands/play.py)
-------------------------------

entry point. registered as a normal-mode command.

    play            list available stories
    play zork       start zork (exact or prefix match)

story discovery: scans content/stories/ for .z3, .z5, .z8, .zblorb files.
prefix matching: "zork" matches "zork1.z3".

on start:
    1. create IFSession with story path
    2. spawn dfrotz, read intro text
    3. push "if" onto player.mode_stack
    4. if save file exists, auto-restore
    5. broadcast intro/restored text to spectators


server integration (server.py)
------------------------------

the shell loop checks player.mode and routes input:

    if player.mode == "if" and player.if_session:
        route to if_session.handle_input()

on done (::quit):
    stop session, pop mode stack, send "you leave the terminal."

on disconnect:
    session cleaned up in finally block (auto-saves)


spectator broadcasting
======================

broadcast_to_spectators() in if_session.py iterates the global players
dict, finds everyone at the same (x,y) who isn't the playing player,
sends them formatted output:

    [Jared's terminal]
    > open mailbox
    Opening the small mailbox reveals a leaflet.

triggered on: game start/restore, every game output, and when the
player leaves the terminal.


save/restore
============

saves stored at: data/if_saves/{player_name}/{game_name}.qzl
player names sanitized (non-alphanumeric chars replaced with _).
quetzal format (.qzl) — z-machine standard.

save flow:
    send "save\n" to dfrotz -> read filename prompt -> send path ->
    read confirmation. auto-confirms "overwrite" if file exists.
    checks for "Failed" in response.

restore flow:
    on "play zork" with existing save file, sends "restore\n" to dfrotz,
    provides save path, reads restored game text.

double-save prevention:
    _saved flag prevents saving twice (::quit triggers save, then stop()
    would save again). flag resets on regular game input.


file layout
===========

    src/mudlib/if_session.py       IFSession class + broadcast_to_spectators
    src/mudlib/commands/play.py    play command
    content/stories/zork1.z3       bundled story file
    data/if_saves/                 per-player save files (gitignored)


what's not here (yet)
=====================

- terminal room objects (currently just the "play" command)
- embedded interpreter (levels 2-5)
- multiplayer z-machine
- ghost presence in IF rooms

see if-journey.rst for the bigger picture.
