264 lines
9.1 KiB
Text
264 lines
9.1 KiB
Text
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.
|