diff --git a/docs/how/if-journey.rst b/docs/how/if-journey.rst index 9334fa3..38aff21 100644 --- a/docs/how/if-journey.rst +++ b/docs/how/if-journey.rst @@ -198,7 +198,7 @@ How many of the ~62 missing zvm opcodes are actually exercised by V3 games? V3 u UPDATE: Opcode tracing (via ``scripts/trace_zmachine.py``) found Zork 1 uses 69 opcodes. zvm had 36 implemented. 33 were ported from viola. All 69 are now implemented in the hybrid interpreter (``src/mudlib/zmachine/``). -Remaining gaps: save/restore (QuetzalWriter needs completion) and sread tokenization. +All V3 gaps have been resolved. save/restore works, and sread tokenization works correctly. 2. zvm/viola memory layout compatibility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -260,6 +260,8 @@ What works: - ``step()`` method for async MUD integration — single instruction at a time, no blocking loop - instruction trace deque (last 20 instructions) for debugging state errors - smoke test: ``scripts/run_zork1.py`` runs the game headless, exercises core opcode paths +- parser and lexer: all Zork 1 commands work (look, open mailbox, read leaflet, inventory, take, drop, navigation) +- the interpreter is fully playable for Zork 1 What this enables: diff --git a/docs/how/zmachine-garbled-output-investigation.rst b/docs/how/zmachine-garbled-output-investigation.rst index 340c5b1..2b4dd75 100644 --- a/docs/how/zmachine-garbled-output-investigation.rst +++ b/docs/how/zmachine-garbled-output-investigation.rst @@ -220,6 +220,112 @@ Debugging approach for next session: 5. Look at how the ``and`` result (step 21 in trace) flows through to the verb syntax lookup +Diagnostic Tools +~~~~~~~~~~~~~~~~ + +``scripts/debug_zstrings.py`` traces all z-string decoding with caller info. + +Run with:: + + echo -e "look\nquit\ny" | python3 scripts/debug_zstrings.py 2>/tmp/trace.log + +Trace goes to stderr, game output to stdout. + +To add instruction tracing after sread, add to ``zcpu.py``:: + + # In step(), after decoding: + print(f"PC={pc:#06x} {handler_name}({', '.join(str(a) for a in args)})", + file=sys.stderr) + +Session 3: Parser Fixed - Interpreter Now Playable +--------------------------------------------------- + +Bug 8: op_jl uses unsigned comparison +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In ``zcpu.py``, ``op_jl`` (jump if less-than) compared raw 16-bit values +without ``_make_signed()``, the same class of bug as Bug 7 (op_dec_chk +and op_inc_chk). + +Additionally, ``op_jl`` had a non-standard signature:: + + def op_jl(self, a, *others): + for b in others: + if a < b: + ... + +The Z-machine spec says JL takes exactly 2 operands. Compare with +``op_jg`` which correctly uses ``def op_jg(self, a, b)``. + +Fix: add ``_make_signed()`` calls to both operands, normalize signature +to ``(self, a, b)`` matching ``op_jg``. + +This alone did not fix the parser - needed in combination with Bug 9. + +Bug 9: finish_routine return value storage - THE PARSER BUG +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two sub-bugs in ``finish_routine()`` in ``zstackmanager.py``: + +**Range check used decimal instead of hex**:: + + if result_variable < 10: # WRONG: should be 0x10 (16) + self.set_local_variable(result_variable, result) + +Z-machine variable numbering: 0 = stack push, 1-15 = locals, 16+ = globals. +The check ``< 10`` meant local variables 10-15 were written as globals. + +**Missing index adjustment for 1-based variable numbering**:: + + self.set_local_variable(result_variable, result) # WRONG + +Z-machine variables are 1-based (variable 1 = first local), but +``set_local_variable()`` uses 0-based indexing (index 0 = first local). +Should be ``set_local_variable(result_variable - 1, result)``. + +Both ``_read_variable()`` and ``_write_result()`` in ``zcpu.py`` +correctly used ``addr - 1`` and ``< 0x10``, but ``finish_routine()`` +in ``zstackmanager.py`` did not. + +Effect: when a function returned a value to a local variable, it went +to the wrong slot. The caller read the correct slot and got stale data +(0 or the initialization value from the function call). + +This is why every verb was recognized by the dictionary but rejected by +the parser. The word-type checker function returned the correct type, +but the return value landed in the wrong local variable. The parser saw +0 and could not match any syntax pattern. + +Room descriptions worked because those code paths used stack returns +(variable 0) or global returns, not local variable returns. + +Fix: change ``< 10`` to ``< 0x10``, add ``- 1`` to the +``set_local_variable`` call. + +Bug 10: zlexer does not truncate words for dictionary lookup +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In ``zlexer.py``, V3 dictionary entries store words truncated to 6 +characters (4 bytes of Z-string = 2 Z-words). V4+ uses 9 characters. + +``parse_input()`` looked up full-length input words, so "mailbox" (7 +characters) did not match "mailbo" (6 characters) in the dictionary. + +Fix: truncate lookup key to ``6 if version <= 3 else 9`` before +``dict.get()``. The original word is preserved in the return list for +correct parse buffer positions. + +What's Fixed Now +~~~~~~~~~~~~~~~~ + +After all fixes: + +- All Zork 1 commands work: look, open mailbox, read leaflet, go north, + inventory, quit, take, drop +- Navigation, object manipulation, multi-word commands, game logic all + functional +- The interpreter is playable + Diagnostic Tools ----------------