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!