Port 4 medium opcodes to hybrid z-machine interpreter

Implements op_print_addr, op_print_num, op_ret, and op_show_status following
TDD approach with tests first. Each opcode now properly decodes/prints text,
handles signed numbers, returns from routines, or acts as a no-op as appropriate.
This commit is contained in:
Jared Miller 2026-02-09 20:13:20 -05:00
parent 1b9d84f41a
commit c76ee337d3
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 51 additions and 10 deletions

View file

@ -405,8 +405,9 @@ class ZCpu:
self._write_result(val, store_addr=variable) self._write_result(val, store_addr=variable)
def op_print_addr(self, string_byte_address): def op_print_addr(self, string_byte_address):
"""TODO: Write docstring here.""" """Print the z-encoded string at the given byte address."""
raise ZCpuNotImplemented text = self._string.get(string_byte_address)
self._ui.screen.write(text)
def op_call_1s(self, routine_address): def op_call_1s(self, routine_address):
"""Call the given routine and store the return value.""" """Call the given routine and store the return value."""
@ -421,9 +422,10 @@ class ZCpu:
shortname = self._objects.get_shortname(obj) shortname = self._objects.get_shortname(obj)
self._ui.screen.write(shortname) self._ui.screen.write(shortname)
def op_ret(self, *args): def op_ret(self, value):
"""TODO: Write docstring here.""" """Return from the current routine with the given value."""
raise ZCpuNotImplemented pc = self._stackmanager.finish_routine(value)
self._opdecoder.program_counter = pc
def op_jump(self, offset): def op_jump(self, offset):
"""Jump unconditionally to the given branch offset. This """Jump unconditionally to the given branch offset. This
@ -535,8 +537,8 @@ class ZCpu:
self._ui.screen.write("\n") self._ui.screen.write("\n")
def op_show_status(self, *args): def op_show_status(self, *args):
"""TODO: Write docstring here.""" """Update status line (V3 only). No-op in this implementation."""
raise ZCpuNotImplemented pass
def op_verify(self, *args): def op_verify(self, *args):
"""TODO: Write docstring here.""" """TODO: Write docstring here."""
@ -592,9 +594,10 @@ class ZCpu:
"""Output the given ZSCII character.""" """Output the given ZSCII character."""
self._ui.screen.write(self._string.zscii.get([char])) self._ui.screen.write(self._string.zscii.get([char]))
def op_print_num(self, *args): def op_print_num(self, value):
"""TODO: Write docstring here.""" """Print a signed 16-bit number as text."""
raise ZCpuNotImplemented signed_value = self._make_signed(value)
self._ui.screen.write(str(signed_value))
def op_random(self, n): def op_random(self, n):
"""Generate a random number, or seed the PRNG. """Generate a random number, or seed the PRNG.

View file

@ -250,6 +250,44 @@ class ZMachineOpcodeTests(TestCase):
# Stack should be empty # Stack should be empty
self.assertEqual(len(self.stack.stack), 0) self.assertEqual(len(self.stack.stack), 0)
def test_op_print_addr(self):
"""Test print_addr decodes and prints text at byte address."""
# Configure mock string decoder to return a known string
self.cpu._string.get = Mock(return_value="Hello, world!")
# Print text at address 0x5000
self.cpu.op_print_addr(0x5000)
# Should have called string decoder with the address
self.cpu._string.get.assert_called_once_with(0x5000)
# Should have written the decoded text
self.ui.screen.write.assert_called_once_with("Hello, world!")
def test_op_print_num_positive(self):
"""Test print_num prints positive number."""
self.cpu.op_print_num(42)
self.ui.screen.write.assert_called_once_with("42")
def test_op_print_num_negative(self):
"""Test print_num prints negative number."""
# -1 as unsigned 16-bit is 65535
self.cpu.op_print_num(65535)
self.ui.screen.write.assert_called_once_with("-1")
def test_op_print_num_zero(self):
"""Test print_num prints zero."""
self.cpu.op_print_num(0)
self.ui.screen.write.assert_called_once_with("0")
def test_op_ret(self):
"""Test ret returns from routine with value."""
self.cpu.op_ret(42)
# Should have set PC to caller's address (0x1000 from mock)
self.assertEqual(self.cpu._opdecoder.program_counter, 0x1000)
def test_op_show_status(self):
"""Test show_status is a no-op (V3 only, not needed in MUD)."""
# Should just not raise an exception
self.cpu.op_show_status()
class MockObjectParser: class MockObjectParser:
"""Mock object parser for testing CPU opcodes.""" """Mock object parser for testing CPU opcodes."""