Kitchen

You are in the kitchen of the white house. A table has recently been used for the preparation of food. A passage leads west and a dark staircase leads upward. A chimney leads down and to the east is a small window which is open.

A bottle is sitting on the table.
The glass bottle contains:
A quantity of water
On the table is an elongated brown sack, smelling of hot peppers.

> go down the chimney

Only Santa Claus climbs down chimneys.

> go west


Last time we built a memory manager for our story state on top of an immutable byte memory manager, but we never got a story file off of disk.

In 1988, the last days of Infocom, a stripped-down version of Zork designed to be playable on a Commodore 64 with a cassette drive was released. This strikes me as somewhat bizarre; the 1541 disk drive had been available for six years by then. But anyways, this “Mini-Zork” was released for free in a British computer magazine shortly thereafter, and so I don’t feel bad about checking it in to github. (Though I was an unrepentant pirate in my misspent youth, I later came to own legally-purchased versions of all the original Infocom games, after Activision re-released them.)

Die-hard Zork aficionados will already have noted that the game transcripts I’ve stared each episode with are from Mini-Zork. Some of the room descriptions are slightly different from the original, and the map is different. In original Zork the pile of 69,150 leaves is in a clearing to the north of the tree with the jewel-encrusted egg, for example. This version has only 15 treasures to collect.

Anyways, I’ve checked it in at https://github.com/ericlippert/flathead/tree/blog3. Let’s load that thing off disk!

Again in keeping with the spirit of feeling like a thin wrapper around 40 year old C libraries, OCaml’s file management is surprisingly retro. Instead of being some purely functional monadic whosiwhatsis, it’s just straight up side-effecting calls that open and close file streams. Or rather: file streams are called “channels” in OCaml. In OCaml “streams” are how OCaml does C# style lazily-computed IEnumerable sequences, and I’m not going to get into them in this project. Again, I’m crawling before I try to run here.

I’ll start with a little helper function that loads an entire file as a string:

let get_file filename =
  let channel = open_in_bin filename in
  let length = in_channel_length channel in
  let file = really_input_string channel length in
  close_in channel;
  file

Every story file begins with a 64 byte header; if the story is not at least that big then we have a problem.

The location of the beginning of static memory is in a word stored starting at byte 14.

Let’s verify that we have at least a header, fetch the static memory offset, and split this into dynamic and static memory:

let header_size = 64
let static_memory_base_offset = Word_address 14
let load filename =
  let file = get_file filename in
  let len = String.length file in
  if len < header_size then
    failwith (Printf.sprintf "%s is not a valid story file" filename)
  else
    let high = dereference_string (address_of_high_byte static_memory_base_offset) file in
    let low = dereference_string (address_of_low_byte static_memory_base_offset) file in
    let dynamic_length = high * 256 + low in
    if dynamic_length > len then
      failwith (Printf.sprintf "%s is not a valid story file" filename)
    else 
      let dynamic = String.sub file 0 dynamic_length in
      let static = String.sub file dynamic_length (len - dynamic_length) in
      make dynamic static

A few things to note here:

We have a print-to-string version of printf that is quite handy; we’ll make good use of it later.

We could also check that static memory begins after byte 64, which is a requirement, but I’m not going to bother. Detecting every possible violation of the Z-machine spec is not one of my goals here.

OCaml has a straightforward but somewhat minimal string manipulation library. We’ll be adding our own helper functions later. Note in particular that the designers of the OCaml string library did not choose to use wrapper types to represent string lengths, indices, and so on, to prevent the transposition bugs possible in these calls to sub.

All right, we can now read a story off disk and both examine and change its memory state. Let’s try. The version number is at byte zero in a story file:

let () = 
  let version_address = Byte_address 0 in
  let story = Story.load "minizork.z3" in
  let version = Story.read_byte story version_address in
  Printf.printf "%d\n" version

And sure enough, mini-Zork is a version 3 Z-machine story file.

Next time on FAIC: the Z-machine stores text in an odd compressed format. Let’s decode it!

11 thoughts on “Kitchen

  1. Pingback: Behind house | Fabulous adventures in coding

  2. The roman numeral confused me for a few seconds. I originally read “a stripped-down version of Zork I designed…” as “a stripped-down version of Zork Eric Lippert designed…”

  3. In original Zork the pile of 69,150 leaves is in a clearing to the north of the tree with the jewel-encrusted egg, for example.

    That should be 69,105. 🙂

    Fun fact: 69 hex = 105 decimal, and 69 decimal = 105 octal.

  4. In the spirit of your “small functions” philosophy I’d suggest a version of dereference_string that returns whole words; the “let high = …; let low = …” pattern looks like it’s going to happen a lot.

    I’ve been following along in Haskell, which has typeclasses; the biggest change I’ve made is to create a ByteAddressable typeclass to encapsulate the various versions of {read,write}_{byte,word}.

  5. In 1988, the last days of Infocom, a stripped-down version of Zork designed to be playable on a Commodore 64 with a cassette drive was released. This strikes me as somewhat bizarre; the 1541 disk drive had been available for six years by then.

    It might have been for the international market. Here in the UK, tape drives were far and away the most common storage medium for 8-bit computers, certainly for the three platforms with the largest market shares, the Commodore 64, Sinclair ZX Spectrum and Amstrad CPC. The vast majority of games for these platforms were sold on tape, and playground game-swapping was almost exclusively tapes. Very few disks were swapped except among the rich kids with BBC Micros.

  6. My introduction to Zork was the original Commodore 64 disk version, circa 1983/4. I knew several people here in Ireland who bought a 1541 drive for the sole purpose of playing Infocom games; at that time, the disk drive cost comfortably more than the computer it connected to.

    Loving this series so far Eric; I get to learn some functional programming and have a great trip down nostalgia lane at the same time!

  7. It seems that given an address, you perform the action of retrieving the values of the low and high and finally compute the value of the address as a whole. Why not making a helper method?

    Maybe something like that:

    let get_address_value offset string=
    let high = dereference_string (address_of_high_byte offset) string in
    let low = dereference_string (address_of_low_byte offset) string in
    high * 256 + low

  8. Pingback: Temple | Fabulous adventures in coding

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s