"""Regression tests for z-machine game compatibility. Smoke tests a suite of games to ensure the interpreter can load them, execute basic commands, and reach the input prompt without crashing. """ from pathlib import Path import pytest from mudlib.zmachine import ZMachine, zscreen, zstream, zui from mudlib.zmachine.trivialzui import ( TrivialAudio, TrivialFilesystem, TrivialScreen, ) from mudlib.zmachine.zcpu import ZCpuQuit, ZCpuRestart STORIES_DIR = Path(__file__).parent.parent / "content" / "stories" # Game test suite: (filename, commands to feed) GAMES = [ ("zork1.z3", ["look", "open mailbox", "read leaflet"]), ("curses.z5", ["look", "inventory", "north"]), ("photopia.z5", ["look", "yes", "look"]), ("Tangle.z5", ["look", "inventory", "north"]), ("shade.z5", ["look", "inventory", "look"]), ("LostPig.z8", ["look", "inventory", "north"]), ("anchor.z8", ["look", "inventory", "north"]), ] class QuietScreen(TrivialScreen): """Screen for testing that never shows [MORE] prompts.""" def __init__(self): super().__init__() # Set infinite rows to prevent [MORE] prompts self._rows = zscreen.INFINITE_ROWS class AutoInputStream(zstream.ZInputStream): """Input stream that auto-feeds commands.""" def __init__(self, commands=None): super().__init__() self._commands = commands or [] self._input_count = 0 def read_line(self, *args, **kwargs): if self._input_count >= len(self._commands): raise ZCpuQuit cmd = self._commands[self._input_count] self._input_count += 1 return cmd def read_char(self, *args, **kwargs): if self._input_count >= len(self._commands): raise ZCpuQuit cmd = self._commands[self._input_count] self._input_count += 1 # Return first character as ord return ord(cmd[0]) if cmd else ord(" ") @pytest.mark.parametrize("game,commands", GAMES, ids=[g[0] for g in GAMES]) def test_game_smoke(game, commands): """Run game through interpreter, feed commands, verify no crash.""" story_path = STORIES_DIR / game if not story_path.exists(): pytest.skip(f"{game} not found in {STORIES_DIR}") # Load story story_bytes = story_path.read_bytes() # Create test UI components audio = TrivialAudio() screen = QuietScreen() keyboard = AutoInputStream(commands) filesystem = TrivialFilesystem() ui = zui.ZUI(audio, screen, keyboard, filesystem) # Create interpreter zm = ZMachine(story_bytes, ui) # Step through instructions step_count = 0 max_steps = 500_000 # Enough to get through several commands try: while step_count < max_steps: (opcode_class, opcode_number, operands) = ( zm._cpu._opdecoder.get_next_instruction() ) implemented, func = zm._cpu._get_handler(opcode_class, opcode_number) # If unimplemented, that's a test failure assert implemented, ( f"Unimplemented opcode {opcode_class}:{opcode_number:02x}" ) # Execute instruction func(zm._cpu, *operands) step_count += 1 except ZCpuQuit: # Normal exit (ran out of commands) pass except ZCpuRestart: # Some games restart - this is fine for smoke test pass # Sanity check: at least some instructions executed assert step_count >= 100, ( f"Only {step_count} instructions executed, expected at least 100" )