This is a large domed temple. A piece of rope descends from the railing of the dome, about 20 feet above, ending some five feet above your head. On the east wall is an ancient inscription, probably a prayer in a long-forgotten language. Below the prayer, a stair leads down. The temple’s altar is to the south. In the center of the room sits a white marble pedestal.
There is a brass bell here.
Sitting on the pedestal is a flaming torch, made of ivory.
> turn off the brass lantern
The brass lantern is now off.
> take the torch
> read the inscription
The prayer is a philippic against small insects, absent-mindedness, and the picking up and dropping of small objects. All evidence indicates that the beliefs of the ancient Zorkers were obscure.
> go east
Wait, I thought the language was long-forgotten?
Anyways, a fundamental data structure in the Z-machine is the object tree, or, more accurately, the object forest. It works like this:
- Everything you interact with in the game is an object. Objects may contain other objects. In the example above, for instance, the temple contains the player, the torch and the bell. The player contains the lamp and the sword. After taking the torch, the player now contains the torch and the temple does not.
- Every object in the game is identified by an object number. In some versions this is a one-byte integer; in some it is a word. In either case, zero is the marker for an invalid object.
- Every object has a parent, sibling and child, any of which can be the invalid object. This is the standard way to implement an n-ary tree: every node in the tree has both a parent and a linked list of children.
- Every object has 32 (or in some versions, 48) Booleans associated with it called its attributes. The meaning of each attribute is entirely up to the game authors to decide. For example, the lamp and the torch probably have an “is lit” attribute. Rooms probably have an “is dark” attribute. And so on.
- Every object also has 31 (or 63) properties. Properties are arbitrarily-sized data, though they are typically one or two bytes. Again, the meaning of each property is entirely up to the game authors.
- Every property has a default value associated with it, so that it is not necessary to waste memory listing every property of every object in cases where that might lead to redundancy.
- Objects also have an optional “short name”, which, unsurprisingly, is zstring encoded.
If this is sounding a little complicated, well, it is. Decoding property lists is particularly tricky, and we’re going to put that off for a good long time. In fact, today we’re just going to solve a very simple problem.
The size of an object number differs depending on the version number of the story file. Using a single byte for object numbers means you can never have more than 255 objects in your game, which is not very many. The Infocom developers wisely decided to bump this up to two-byte object numbers. In the Mini-Zork story file we’re taking apart here we only have to worry about single-byte object numbers, but I’d like to start developing this library to work with any version. So we’re going to need a way to know what version we’re in, and manipulate that programmatically.
Code for this and the next few episodes can be found at https://github.com/ericlippert/flathead/tree/blog7.
The obvious way to do it would be to make yet another wrapper type around version numbers. But in this particular case there are only eight possible versions, so I feel pretty justified in making what is essentially an enumerated type:
type version = | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8
The version number is, as we discussed several episodes ago, encoded in byte zero of the story.
let version_offset = Byte_address 0 let version story = match read_byte story version_offset with | 1 -> V1 | 2 -> V2 | 3 -> V3 | 4 -> V4 | 5 -> V5 | 6 -> V6 | 7 -> V7 | 8 -> V8 | _ -> failwith "unknown version"
That would probably have been a much more gentle introduction to pattern matching than jumping right into tuples and wrapper types. Oh well!
Now the problem is that we cannot manipulate these guys as numbers, since they are not numbers. But remember, there is no task too small to make into a function. The fundamental question I want to ask is: are we in version 1, 2, or 3? Because those have one-byte object numbers. Or are we in version 4, 5, 6, 7, or 8, with two-byte object numbers?
let v3_or_lower v = match v with | V1 | V2 | V3 -> true | V4 | V5 | V6 | V7 | V8 -> false
Piece of cake.
I think this might be the first time we’ve seen Boolean literals. They are exactly as you’d expect. Another thing to note here that I don’t think we’ve seen before is that multiple patterns can be made to have the same result, which makes for more compact code.
Next time on FAIC: we’ll actually start decoding the object table.
I presume you’re missing the word “ago” in the sentence after the enumerated type definition, since you initially brought up the version number’s location when we were in the kitchen. Enjoying the series, by the way!
I, too, am enjoying the series.
I think the v3_or_lower is rather misnamed. What if you introduce a V9 with 1 byte object numbers? You really don’t care whether you are at v3 or less. You care whether the object number is one or two bytes. Seems like a better name for the bool would be object_num_byte (or object_num_is_word) or something like that.
What if you introduce 3 byte object numbers?
Good point! I am directly conflating semantics with mechanisms here. I had not noticed this before but I think I might change the code now.
This is especially important once you get into the later versions, since V7-8 are extensions of V5, leaving V6 as a weird evolutionary dead end.
So OCAML doesn’t allow pattern matching right in the function header, like other functional languages? Along the lines of
let v3_or_lower V1 = true
let v3_or_lower V2 = true
let v3_or_lower V3 = true
let v3_or_lower _ = false
You can use a syntactic sugar like this:
let v3_or_lower = function
| V1 | V2 | V3 -> true
| V4 | V5 | V6 | V7 | V8 -> false
I’ve considered introducing that sugar in this series but was putting it off. I find it almost too concise; I like to think of my functions as having, you know, parameters! But it does seem to be idiomatic OCaml.
A quick question about object attributes and properties. You note their meaning is up to the game authors, but it’s unclear whether those choices are per game or per object type. I suspect it’s the former, though could see it being the latter.
From the Z-machine’s perspective, they’re just numbers, and they might as well be unique for every object. The interpreter only needs to handle operations like “test if attribute 5 is set on object 19”.
From the game developer’s perspective, there’s a lot of variation in the scope of the property/attribute meanings:
Some vary per object. Inform 6 defines an attribute called “general” that just means the object is in a special state, and it’s up to the author to decide what that means. Maybe the bear gets “general” when it’s been tamed and the troll gets it when he’s been woken up.
Some vary per object type, which helps conserve the limited number of identifiers. IIRC, Zork uses the same attribute to identify treasures the player can earn points for and rooms the thief can’t enter. (Note that even the definition of “object type” is up to the game: Infocom games put all rooms inside a room container, whereas Inform games identify them using properties or sometimes no marker at all.)
Some vary per game. That “treasure” attribute doesn’t exist in games that aren’t about treasure hunting, and some games implement the same features using different attribute numbers.
Some are even defined by the development system. Inform reserves a few property numbers to implement language features like class inheritance.