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:
Jared Miller 2026-02-10 13:53:02 -05:00
parent 8a5ef7b1f6
commit 14816478aa
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 160 additions and 4 deletions

View file

@ -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
View 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}")