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
|
- content loading: TOML definitions for commands and combat moves, loaded at startup
|
||||||
- entity model: Entity base class, Player and Mob subclasses sharing common interface
|
- entity model: Entity base class, Player and Mob subclasses sharing common interface
|
||||||
- editor mode: in-world text editor with syntax highlighting and search/replace
|
- 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
|
## 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.
|
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
|
Level 2 — inspectable world
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
@ -174,12 +174,12 @@ Input routing
|
||||||
Room-local broadcasting
|
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
|
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
|
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.
|
- [ ] 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.
|
- [ ] 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
|
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/if-integration.txt`` — original research and integration plan (predates audits)
|
||||||
|
|
||||||
``docs/how/viola-embedding-audit.rst`` — detailed viola architecture audit
|
``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