Update if-journey.rst with V8/Lost Pig milestone and corrections
Wizard Sniffer is Glulx (.gblorb), not z-machine — out of scope. Lost Pig is V8, not V5 as originally assumed. Added milestone section documenting the V8 support work, bugs found and fixed, and new opcode implementations. Updated game descriptions and version notes. Added trace_lostpig.py utility script.
This commit is contained in:
parent
8a5ef7b1f6
commit
14816478aa
2 changed files with 160 additions and 4 deletions
|
|
@ -133,12 +133,12 @@ The games that motivated this work:
|
||||||
The Wizard Sniffer (Buster Hudson, 2017)
|
The Wizard Sniffer (Buster Hudson, 2017)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You play a pig who sniffs out wizards. IFComp winner, XYZZY winner. Screwball comedy. The pig/wizard game that started this. Z-machine format. Would need V5 support.
|
You play a pig who sniffs out wizards. IFComp winner, XYZZY winner. Screwball comedy. The pig/wizard game that started this. Glulx format (.gblorb) — out of scope for embedded interpreter unless we subprocess to a Glulx interpreter.
|
||||||
|
|
||||||
Lost Pig (Admiral Jota, 2007)
|
Lost Pig (Admiral Jota, 2007)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Grunk the orc chases an escaped pig. IFComp winner, 4 XYZZY awards. Famous for its responsive parser and comedy writing. Z-machine format. V5.
|
Grunk the orc chases an escaped pig. IFComp winner, 4 XYZZY awards. Famous for its responsive parser and comedy writing. Z-machine V8 format (not V5 as originally assumed). Now playable in the hybrid interpreter.
|
||||||
|
|
||||||
Zork I, II, III
|
Zork I, II, III
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
@ -152,7 +152,7 @@ Funny, frustrating, great spectator game. V3.
|
||||||
|
|
||||||
Also: Anchorhead, Photopia, Spider and Web, Shade, Colossal Cave.
|
Also: Anchorhead, Photopia, Spider and Web, Shade, Colossal Cave.
|
||||||
|
|
||||||
V3 covers the Infocom catalog. V5 covers most modern IF including the pig games. V8 covers big modern Inform 7 games but is lower priority.
|
V3 covers the Infocom catalog. V5/V8 covers most modern IF. Lost Pig is V8 (same opcodes as V5, different packed address scaling). Wizard Sniffer is Glulx (out of scope for embedded). V8 support is now implemented.
|
||||||
|
|
||||||
Note: no Python Glulx interpreter exists. Games that target Glulx (some modern Inform 7) are out of scope unless we subprocess to a C interpreter.
|
Note: no Python Glulx interpreter exists. Games that target Glulx (some modern Inform 7) are out of scope unless we subprocess to a C interpreter.
|
||||||
|
|
||||||
|
|
@ -205,7 +205,7 @@ All V3 gaps have been resolved. sread tokenization works correctly. save/restore
|
||||||
|
|
||||||
Do they represent z-machine memory the same way? Both use bytearrays, but header parsing, object table offsets, string encoding — are these compatible enough that porting opcodes is translation, or is it a deeper rewrite?
|
Do they represent z-machine memory the same way? Both use bytearrays, but header parsing, object table offsets, string encoding — are these compatible enough that porting opcodes is translation, or is it a deeper rewrite?
|
||||||
|
|
||||||
UPDATE: Less urgent now that the hybrid interpreter works end-to-end for V3. The layout question mainly matters for V5 opcode porting (Lost Pig, Wizard Sniffer). The hybrid already handles all V3 memory operations correctly.
|
UPDATE: Resolved. The hybrid interpreter now works end-to-end for both V3 and V8. V8 uses the same object model as V5 (14-byte frames, 48 attributes, 65535 objects) and the same opcodes. The only difference is packed address scaling (×8 vs ×4). Memory layout handled correctly for all supported versions.
|
||||||
|
|
||||||
3. Async model
|
3. Async model
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
@ -268,6 +268,12 @@ Concrete next steps, roughly ordered. Update as items get done.
|
||||||
|
|
||||||
- [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/)
|
- [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/)
|
||||||
|
|
||||||
|
- [x] V8 support and Lost Pig: relaxed version gates (V8 = V5 with ×8 packed addresses), implemented extended opcode decoder, ported V5+ opcodes (aread, call_vn/vn2/vs2, save_undo stub, log/art shift, scan_table, tokenize, copy_table, print_unicode). found and fixed: insert_object wrong removal order, double-byte operand decoder reading type bytes interleaved with operands instead of all types first, opcode detection using 7-bit mask instead of 5-bit. Lost Pig now runs to completion (101K steps, 61 opcodes). (done — LostPig.z8 bundled in content/stories/, see ``scripts/trace_lostpig.py``)
|
||||||
|
|
||||||
|
- [ ] wire V8 games to MUD: ``EmbeddedIFSession`` currently only handles .z3 files. extend to .z5/.z8 so Lost Pig is playable via ``play lostpig`` in the MUD.
|
||||||
|
|
||||||
|
- [ ] implement real save_undo: currently stubs returning -1 ("not available"). a proper implementation needs in-memory state snapshots (dynamic memory + call stack). Lost Pig works without undo but players expect it.
|
||||||
|
|
||||||
milestone — Zork 1 playable in hybrid interpreter
|
milestone — Zork 1 playable in hybrid interpreter
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -316,6 +322,29 @@ What this enables:
|
||||||
- foundation for level 3 (moldable world — write z-machine state from MUD)
|
- foundation for level 3 (moldable world — write z-machine state from MUD)
|
||||||
- no external dependency on dfrotz for V3 games
|
- no external dependency on dfrotz for V3 games
|
||||||
|
|
||||||
|
milestone — V8 support: Lost Pig playable
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
The hybrid interpreter now supports V8 (and by extension V5) story files. Lost Pig — the 4-time XYZZY award winner about an orc chasing a pig — runs to completion in the interpreter.
|
||||||
|
|
||||||
|
What was needed:
|
||||||
|
|
||||||
|
- version gates relaxed: V8 uses the same object model and opcodes as V5, just different packed address scaling (×8 vs ×4) and larger max file size (512KB)
|
||||||
|
- extended opcode decoder: V5+ uses 0xBE prefix for extended opcodes. the decoder was stubbed. implemented reading opcode number + type byte after prefix
|
||||||
|
- double-byte operand fix: ``call_vs2`` and ``call_vn2`` have two operand type bytes. the old decoder read them interleaved with operands (type1, operands, type2, operands) instead of both types first. refactored into ``_read_type_byte()`` + ``_parse_operand_list()``
|
||||||
|
- double-byte detection fix: the check used ``opcode[0:7]`` (7-bit mask) instead of ``opcode_num`` (5-bit). never matched, so double-byte opcodes were always mis-parsed
|
||||||
|
- ``insert_object`` fix: was inserting into new parent before removing from old. corrupted sibling chains when moving within same parent. now removes first per spec
|
||||||
|
- 15 new opcodes: ``aread``, ``call_vn``/``vn2``/``vs2``, ``catch``, ``check_arg_count``, ``save_undo`` (stub), ``restore_undo`` (stub), ``log_shift``, ``art_shift``, ``scan_table``, ``tokenize``, ``copy_table``, ``set_font``, ``print_unicode``, ``check_unicode``
|
||||||
|
|
||||||
|
Lost Pig trace: 101K instructions, 61 unique opcodes, full gameplay loop working (room descriptions, parser, object manipulation). ``scripts/trace_lostpig.py`` for tracing.
|
||||||
|
|
||||||
|
What this enables:
|
||||||
|
|
||||||
|
- modern IF games compiled with Inform 6/7 to Z-machine V5/V8 format are now playable
|
||||||
|
- same level 2 inspection works (object tree, location, state reading)
|
||||||
|
- level 3 write APIs already handle V4-8 object format (14-byte frames, 48 attributes, 65535 objects)
|
||||||
|
- Wizard Sniffer is Glulx (out of scope), but Lost Pig is the better game anyway
|
||||||
|
|
||||||
related documents
|
related documents
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|
|
||||||
127
scripts/trace_lostpig.py
Normal file
127
scripts/trace_lostpig.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
"""Trace Lost Pig V8 opcodes — find what's needed for V5+ support.
|
||||||
|
|
||||||
|
Runs the interpreter step-by-step, collecting unique opcodes hit.
|
||||||
|
When an unimplemented opcode is encountered, reports what was seen
|
||||||
|
and what's missing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ruff: noqa: E402
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(project_root / "src"))
|
||||||
|
|
||||||
|
from mudlib.zmachine import ZMachine, zopdecoder, zstream, zui
|
||||||
|
from mudlib.zmachine.trivialzui import (
|
||||||
|
TrivialAudio,
|
||||||
|
TrivialFilesystem,
|
||||||
|
TrivialScreen,
|
||||||
|
)
|
||||||
|
from mudlib.zmachine.zcpu import (
|
||||||
|
ZCpuNotImplemented,
|
||||||
|
ZCpuQuit,
|
||||||
|
ZCpuRestart,
|
||||||
|
)
|
||||||
|
|
||||||
|
story_path = project_root / "content" / "stories" / "LostPig.z8"
|
||||||
|
if not story_path.exists():
|
||||||
|
print(f"ERROR: {story_path} not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
story_bytes = story_path.read_bytes()
|
||||||
|
print(f"Loaded LostPig.z8: {len(story_bytes)} bytes, version {story_bytes[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
class AutoInputStream(zstream.ZInputStream):
|
||||||
|
"""Input stream that auto-feeds commands."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._input_count = 0
|
||||||
|
self._max_inputs = 3
|
||||||
|
|
||||||
|
def read_line(self, *args, **kwargs):
|
||||||
|
self._input_count += 1
|
||||||
|
if self._input_count > self._max_inputs:
|
||||||
|
raise ZCpuQuit
|
||||||
|
return "look"
|
||||||
|
|
||||||
|
|
||||||
|
audio = TrivialAudio()
|
||||||
|
screen = TrivialScreen()
|
||||||
|
keyboard = AutoInputStream()
|
||||||
|
filesystem = TrivialFilesystem()
|
||||||
|
ui = zui.ZUI(audio, screen, keyboard, filesystem)
|
||||||
|
zm = ZMachine(story_bytes, ui)
|
||||||
|
|
||||||
|
opcodes_seen = Counter()
|
||||||
|
step_count = 0
|
||||||
|
max_steps = 500_000
|
||||||
|
|
||||||
|
print(f"Tracing up to {max_steps} steps...")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while step_count < max_steps:
|
||||||
|
pc = zm._cpu._opdecoder.program_counter
|
||||||
|
(opcode_class, opcode_number, operands) = (
|
||||||
|
zm._cpu._opdecoder.get_next_instruction()
|
||||||
|
)
|
||||||
|
|
||||||
|
cls_str = zopdecoder.OPCODE_STRINGS.get(opcode_class, f"?{opcode_class}")
|
||||||
|
key = f"{cls_str}:{opcode_number:02x}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
implemented, func = zm._cpu._get_handler(opcode_class, opcode_number)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ILLEGAL at step {step_count}: {key} (pc={pc:#x})")
|
||||||
|
print(f" {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
opcodes_seen[f"{key} ({func.__name__})"] += 1
|
||||||
|
|
||||||
|
if not implemented:
|
||||||
|
print(f"UNIMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
|
||||||
|
print(f" PC: {pc:#x}")
|
||||||
|
print()
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
func(zm._cpu, *operands)
|
||||||
|
except ZCpuQuit:
|
||||||
|
print(f"Game quit after {step_count} steps")
|
||||||
|
break
|
||||||
|
except ZCpuRestart:
|
||||||
|
print(f"Game restart after {step_count} steps")
|
||||||
|
break
|
||||||
|
except ZCpuNotImplemented as e:
|
||||||
|
print(f"NOT IMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
|
||||||
|
print(f" PC: {pc:#x}")
|
||||||
|
print(f" Error: {e}")
|
||||||
|
print()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR at step {step_count}: {key} -> {func.__name__}")
|
||||||
|
print(f" PC: {pc:#x}")
|
||||||
|
print(f" Operands: {operands}")
|
||||||
|
print(f" {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
zm._cpu._dump_trace()
|
||||||
|
break
|
||||||
|
|
||||||
|
step_count += 1
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\nInterrupted at step {step_count}")
|
||||||
|
|
||||||
|
print(f"\nTotal steps: {step_count}")
|
||||||
|
print(f"Unique opcodes seen: {len(opcodes_seen)}")
|
||||||
|
print()
|
||||||
|
print("Opcodes by frequency:")
|
||||||
|
for op, count in opcodes_seen.most_common():
|
||||||
|
print(f" {count:>8} {op}")
|
||||||
Loading…
Reference in a new issue