At the east end of this narrow passage, a ladder leads upward. There’s a strong draft from the west, where the passage narrows even further.
There is a small pile of coal here.
> take the coal
Taken.
> go west
You can’t fit through with that load.
> go up
> go east
> go northwest
> go west
> go south
Code for this episode can be found at https://github.com/ericlippert/flathead/tree/blog19
Last time we got as far as printing out the copyright message of the game. The serial number is encoded in some words in the header, and the code we crashed on will read them out and do some bit arithmetic. I want to implement a bunch of very easy instructions here, which I present without further comment:
let handle_test bitmap flags interpreter = if (bitmap land flags) = flags then 1 else 0 let handle_or a b interpreter = a lor b let handle_and a b interpreter = a land b
While I’m at it I want to also implement two instructions that pop the stack. One pops and returns, the other just pops.
In later versions of the Z-machine the pop instruction opcode was used instead for catch, so I’ll implement a stub for that while I’m here:
let handle_ret_popped interpreter instruction = let result = peek_stack interpreter in let popped_interpreter = pop_stack interpreter in interpret_return popped_interpreter result let handle_pop interpreter = pop_stack interpreter let handle_catch interpreter = failwith "TODO: catch instruction not yet implemented"
All right, that was boring, but now when we run our test program we get a little farther:
MINI-ZORK I: The Great Underground Empire Copyright (c) 1988 Infocom, Inc. All rights reserved. ZORK is a registered trademark of Infocom, Inc. Release 34 / Serial number 871124 Exception: Failure "TODO: 6bc7: test_attr g00 0d ?6bd4 \n ".
My guess would be that the serial number is in fact the date that the release build was made: November 24th, 1987. We’ll discuss the significance of the serial number again later when we implement loading and saving.
The next instruction to implement is test_attr. As we know, objects have properties and attributes; attributes are a fixed set of bit flags that have whatever meaning the game implementers decide. Properties are of arbitrary size, and have default values. Since we’re doing bitwise operations anyways in this episode it seems reasonable to implement them here. The instruction handlers are just thin wrappers around the actual code, which I’ll put with the rest of the object tree code:
let handle_test_attr obj attr interpreter = let obj = Object obj in let attr = Attribute attr in if Object.attribute interpreter.story obj attr then 1 else 0 let handle_set_attr obj attr interpreter = let obj = Object obj in let attr = Attribute attr in { interpreter with story = Object.set_attribute interpreter.story obj attr } let handle_clear_attr obj attr interpreter = let obj = Object obj in let attr = Attribute attr in { interpreter with story = Object.clear_attribute interpreter.story obj attr }
Note that I have of course added another wrapper type Attribute
to wrap the attribute number. I’ve also added a tuple type that keeps track of both a byte address and a bit number, since attributes are stored in a bitfield:
type attribute_address = Attribute_address of byte_address * bit_number type local_variable = Local of int
And the code in the object module is straightforward:
let attribute_count story = if Story.v3_or_lower (Story.version story) then 32 else 48 let attribute_address story obj (Attribute attribute) = if attribute < 0 || attribute >= (attribute_count story) then failwith "bad attribute" else let offset = attribute / 8 in let (Object_address obj_addr) = address story obj in let bit = Bit_number (7 - (attribute mod 8)) in Attribute_address ((Byte_address (obj_addr + offset)), bit) let attribute story obj attribute = let (Attribute_address (address, bit)) = attribute_address story obj attribute in Story.read_bit story address bit let set_attribute story obj attribute = let (Attribute_address (address, bit)) = attribute_address story obj attribute in Story.write_set_bit story address bit let clear_attribute story obj attribute = let (Attribute_address (address, bit)) = attribute_address story obj attribute in Story.write_clear_bit story address bit
I added code to the story memory manager to read and write individual bits. It is super boring so I won’t bother to mention it here.
Now if we run our program:
MINI-ZORK I: The Great Underground Empire Copyright (c) 1988 Infocom, Inc. All rights reserved. ZORK is a registered trademark of Infocom, Inc. Release 34 / Serial number 871124 West of House Exception: Failure "TODO: 6bec: get_prop g00 0e ->local2 \n ".
We have made it as far as putting the player in the starting room and printing out its name! This is really coming together here.
Next time on FAIC: properties, obviously.
Hi Eric. I’ve been following along and translating your OCaml to (mostly) functional-style Objective-C and with today’s post my results have diverged from yours. Instead of “West of House” my code prints “It is pitch black. You are likely to be eaten by a grue.” I suspect I made a translation error when going from OCaml to Obj-C, and I was wondering if you could share your instruction disassembly starting from after the code that prints the serial number (around instruction address 0x58fb). Mine looks like this:
58fb: rtrue
380b: new_line
380c: call 61f4 ->sp
61f5: call 6b8e 01 ->sp
6b95: jz local0 ?~6b9c
6b9c: store local1 01
6b9f: jz g26 ?~6bc7
6ba2: print It is pitch black. You are likely to be eaten by a grue.
6bc5: new_line
6bc6: rfalse
…
The disassembly looks fine. This is a logical problem in the interpreter somewhere. The routine here is to check whether the room is lit naturally or if there is a light source in it; if not, you are likely to be eaten by a grue. Clearly there is no light source, so probably the attribute that determines if the room is always lit is being read incorrectly.
If that’s not sufficient to find the bug let me know and I’ll post a transcript of all the instructions so far.
Thanks, Eric. That was enough to help me debug the problem. I remembered a previous blog post where you talked about the instructions:
37f7: store g00 2e
37fa: store g7a a7
37fd: store g26 01
3800: store g73 1e
3803: insert_obj g73 g00
And what those instructions do. By tracing what was happening with g26 (the global that appears to track whether a room is lit), I found the problem deep in my immutable bytes implementation and the key I was using to record edits. I thought my dictionary key was properly comparable, but the same address locations were resulting in dictionary keys that did not compare as equals. Once I fixed that, my bug was resolved.
I’m loving this series and learning a lot about functional-style programming!
Thanks for continuing this series!
-peter
Nice! Glad you figured it out.