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.