Ladder room

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.

4 thoughts on “Ladder room

  1. 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

Leave a comment