Add a terminal plan

This commit is contained in:
Jared Miller 2026-02-09 12:25:44 -05:00
parent 4b52051bed
commit bc5e829f6b
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

264
docs/plans/if-terminal.txt Normal file
View file

@ -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 <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.