Update documents with new IF system
This commit is contained in:
parent
108091bfae
commit
47ef606e7f
4 changed files with 245 additions and 5 deletions
|
|
@ -56,6 +56,7 @@ Update docs when:
|
|||
- content loading: TOML definitions for commands and combat moves, loaded at startup
|
||||
- entity model: Entity base class, Player and Mob subclasses sharing common interface
|
||||
- editor mode: in-world text editor with syntax highlighting and search/replace
|
||||
- IF mode: interactive fiction via dfrotz subprocess, mode stack integration, spectator broadcasting. see `docs/how/if-terminal.txt`
|
||||
|
||||
## Style
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Level 1 — terminal mode
|
|||
|
||||
Subprocess (dfrotz), text in/out, spectators see text scrolling on a screen. Player sits at an arcade terminal, plays Zork. Others in the room see the output. The IF world is opaque — a black box.
|
||||
|
||||
Works today with dfrotz. Proven technology.
|
||||
Implemented. See ``docs/how/if-terminal.txt`` for how the system works.
|
||||
|
||||
Level 2 — inspectable world
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -174,12 +174,12 @@ Input routing
|
|||
Room-local broadcasting
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Not implemented yet. IF integration is the first use case. Terminal object maintains spectator list, broadcasts output to all. Pattern will be reused for ambient messages, weather, room events.
|
||||
Implemented for IF. ``broadcast_to_spectators()`` in ``if_session.py`` sends to all players at the same (x,y) location. Pattern will be reused for ambient messages, weather, room events.
|
||||
|
||||
State storage
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Quetzal saves as blobs in SQLite. Same pattern as other binary persistence.
|
||||
Quetzal saves stored as files in ``data/if_saves/{player}/{game}.qzl``. Filesystem approach (simpler than SQLite blobs for dfrotz, which already writes to disk).
|
||||
|
||||
Terminal game object
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -238,15 +238,17 @@ Concrete next steps, roughly ordered. Update as items get done.
|
|||
|
||||
- [ ] prototype the hybrid: pick 5-10 common opcodes, port them from viola to zvm's architecture. see how the pattern feels. if it's smooth, the hybrid is viable. if every opcode is a battle, reconsider.
|
||||
|
||||
- [ ] build level 1 prototype: regardless of interpreter choice, implement the terminal object, IF mode, and subprocess dfrotz path. this proves the MUD-side architecture (mode stack, spectators, save/restore) independently of the interpreter question.
|
||||
- [x] build level 1 prototype: regardless of interpreter choice, implement the terminal object, IF mode, and subprocess dfrotz path. this proves the MUD-side architecture (mode stack, spectators, save/restore) independently of the interpreter question. (done — see ``docs/how/if-terminal.txt``)
|
||||
|
||||
- [ ] study MojoZork's multiplayer model: read the MultiZork source for how it handles multiple players in one z-machine. document the pattern for our eventual level 4.
|
||||
|
||||
- [ ] find the game files: locate freely distributable z-machine story files for the games we care about. Wizard Sniffer, Lost Pig, Zork (if legally available).
|
||||
- [x] find the game files: locate freely distributable z-machine story files for the games we care about. Wizard Sniffer, Lost Pig, Zork (if legally available). (zork1.z3 bundled in content/stories/)
|
||||
|
||||
related documents
|
||||
-----------------
|
||||
|
||||
``docs/how/if-terminal.txt`` — how the level 1 IF system works (implementation reference)
|
||||
|
||||
``docs/how/if-integration.txt`` — original research and integration plan (predates audits)
|
||||
|
||||
``docs/how/viola-embedding-audit.rst`` — detailed viola architecture audit
|
||||
|
|
|
|||
152
docs/how/if-terminal.txt
Normal file
152
docs/how/if-terminal.txt
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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.
|
||||
85
docs/lessons/dfrotz-prompt-detection.txt
Normal file
85
docs/lessons/dfrotz-prompt-detection.txt
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
dfrotz prompt detection — reading output from a z-machine subprocess
|
||||
====================================================================
|
||||
|
||||
the problem
|
||||
===========
|
||||
|
||||
dfrotz writes game text to stdout, then displays a ">" prompt and waits
|
||||
for input. there's no clean "end of response" delimiter — just the prompt
|
||||
character appearing at the end of the output.
|
||||
|
||||
this sounds simple. it wasn't.
|
||||
|
||||
|
||||
what made it hard
|
||||
=================
|
||||
|
||||
1. the prompt format varies:
|
||||
- "> " (with trailing space) — most common
|
||||
- ">\n" or ">\r\n" — sometimes
|
||||
- bare ">" with no preceding newline — edge case but real
|
||||
|
||||
2. the ">" character can appear IN game text:
|
||||
- a bare ">" at end of text isn't always the prompt
|
||||
- only strip it when the raw bytes show trailing whitespace after it
|
||||
(confirming it's the prompt pattern, not game content)
|
||||
|
||||
3. chunked I/O:
|
||||
- data arrives in arbitrary chunks (not line-by-line)
|
||||
- the prompt might be split across chunks
|
||||
- can't just check the last character of each read
|
||||
|
||||
4. timing:
|
||||
- after writing a command, dfrotz takes variable time to respond
|
||||
- short responses (one line) arrive fast
|
||||
- long responses (room descriptions) come in multiple chunks
|
||||
- no way to know in advance how much output to expect
|
||||
|
||||
|
||||
what we do
|
||||
==========
|
||||
|
||||
_read_response() in if_session.py uses a chunked reader with dual checks:
|
||||
|
||||
read up to 1024 bytes per chunk
|
||||
after each chunk, check the accumulated buffer for prompt patterns
|
||||
idle timeout of 100ms — once data stops flowing, return what we have
|
||||
overall deadline of 5 seconds — safety net
|
||||
|
||||
prompt detection checks (in order):
|
||||
1. stripped text ends with "\n>" — always a prompt, strip it
|
||||
2. stripped text is just ">" — empty response, return ""
|
||||
3. raw bytes end with "> " or ">\n" AND stripped text ends with ">"
|
||||
— prompt confirmed by trailing whitespace, strip it
|
||||
|
||||
the key insight: "\n>" is unambiguously a prompt (game text doesn't end
|
||||
that way). bare ">" is ambiguous — only treat it as a prompt when the
|
||||
raw bytes have trailing whitespace confirming the prompt pattern.
|
||||
|
||||
|
||||
what we tried that didn't work
|
||||
==============================
|
||||
|
||||
- reading until timeout only: too slow (100ms wait after every response)
|
||||
or too fast (miss data that's still coming). need both prompt detection
|
||||
AND timeout as fallback.
|
||||
|
||||
- reading line by line: dfrotz doesn't always end the prompt with a
|
||||
newline. sometimes the ">" has a trailing space and nothing else.
|
||||
readline() hangs waiting for \n that never comes.
|
||||
|
||||
- checking only the last character: ">" appears in game text. false
|
||||
positives everywhere.
|
||||
|
||||
|
||||
the fix pattern
|
||||
===============
|
||||
|
||||
dual-mode detection: fast path checks for prompt patterns after each
|
||||
chunk (instant return when found), slow path falls back to idle timeout
|
||||
(catches edge cases where prompt detection fails). overall deadline
|
||||
prevents infinite hangs.
|
||||
|
||||
this pattern works for any subprocess that uses a prompt character to
|
||||
signal readiness. the specific prompt patterns are dfrotz-specific, but
|
||||
the chunk-accumulate-check loop is reusable.
|
||||
Loading…
Reference in a new issue