152 lines
4.4 KiB
Text
152 lines
4.4 KiB
Text
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.
|