A couple of months ago I referred to a quote from Infocom's internal ZIL manual: The other four tokens—ON-GROUND, IN-ROOM, HELD, and CARRIED—are incredibly confusing, and no one really understands them except Stu, so he should probably write ...
A couple of months ago I referred to a quote from Infocom's internal ZIL manual:
The other four tokens—ON-GROUND, IN-ROOM, HELD, and CARRIED—are incredibly confusing, and no one really understands them except Stu, so he should probably write this bit.
-- Learning ZIL, chapter 9.6
That post was about the social context in which Steve Meretzky wrote those words. So I didn't get into what the ZIL tokens meant.
But this week the question came up on the Visible Zorker Discord. Let's get technical!
(This post is also available on my Patreon.)
What kind of tokens are we talking about? The manual again:
There are several tokens which can appear in parentheses within a syntax definition: HAVE, TAKE, MANY, EVERYWHERE, ADJACENT, HELD, CARRIED, ON-GROUND, and IN-ROOM. This parenthetical list appears after either or both OBJECTs:
<SYNTAX
GIVE
OBJECT (HAVE)
TO
OBJECT (ON-GROUND IN-ROOM)
= V-GIVE>
(I've spaced out the <SYNTAX> line for clarity.)
This example defines a grammar line: GIVE ___ TO ___. The V-GIVE routine will handle the action. The parenthesized tokens define some behavior for the two object slots. The parser will use this information when searching the world for objects to match the player's command.
(Note: The manual dates from 1989. EVERYWHERE and ADJACENT were added with the "new" V6 parser used in Zork Zero, etc. I'm still dealing with the early games, so I'll skip EVERYWHERE and ADJACENT in this post.)
So how to figure out what ON-GROUND and IN-ROOM mean? Look at the parser code, right? Here we run into our first problem: the parser code and the syntax definitions use different names.
Here's a set of definitions from Zork 1:
<CONSTANT SH 128>
<CONSTANT SC 64>
<CONSTANT SIR 32>
<CONSTANT SOG 16>
<CONSTANT STAKE 8>
<CONSTANT SMANY 4>
<CONSTANT SHAVE 2>
The initial S is ZIL convention for constants (perhaps "static"), followed by abbreviations. SOG is ON-GROUND, for example. Annoyingly, SH must be HELD rather than HAVE!
By the way, note Infocom's preference for numbering from the high bit down. Clearly HELD was defined first. SHAVE needed more letters because SH was already taken. And they haven't used the low bit at all (yet). You see the same thing with attribute flags and property numbers; all games start with the high value, but not all make it down to zero.
It would be smart to verify our understanding in, as it were, real life. Let's look at the GIVE TO grammar line in Zork 1. Turns out the manual example was simplified. Here's the real definition:
<SYNTAX
GIVE
OBJECT (MANY HELD HAVE)
TO
OBJECT (FIND ACTORBIT) (ON-GROUND)
= V-GIVE PRE-GIVE>
Messier... We have both an action routine (V-GIVE) and a preaction routine (PRE-GIVE). The preaction routine checks prerequisites before the start of action handling proper.
We also have a different kind of object flag: (FIND ACTORBIT). This is well-explained in the manual. If the player omits the second object (by typing GIVE SANDWICH), the parser will try to fill in the blank by looking for an object in the room with the ACTORBIT attribute. That is to say, an NPC. If there's exactly one, great! If there's two or more, ask for disambiguation.
But back to the ON-GROUND stuff. We can rip apart the compiled game file to look at the grammar table. (I use the txd tool for this.) The grammar line is eight bytes:
[02 00 ff 00 1e 86 10 3f] "give OBJ to OBJ"
I'll spare you the full decoding. (See Michael Ko's document for that.) The relevant bytes are $86 for the first object (MANY HELD HAVE) and $10 for the second (ON-GROUND). Do those bits match up with the definitions above? Yes! Whew.
Let me rewrite the table, showing both the SYNTAX names and the CONSTANT labels:
HELD SH 128 ($80)
CARRIED SC 64 ($40)
IN-ROOM SIR 32 ($20)
ON-GROUND SOG 16 ($10)
TAKE STAKE 8 ($08)
MANY SMANY 4 ($04)
HAVE SHAVE 2 ($02)
(unused) 1 ($01)
Armed with this knowledge, we can dig into the mysterious tokens.
TAKE and HAVE aren't that mysterious. The manual tells us that TAKE means that we will try to automatically take a (portable) object before the action begins. HAVE means the object must be in the player's inventory for the action to succeed.
You'd think these would always go together. Not always! For example, the READ action has the TAKE flag but not HAVE. You'd prefer to be holding a book in order to read it, but a plaque bolted to the wall is still readable.
Conversely, the DROP action has the HAVE flag but not TAKE. You can only drop something you're holding, but it would be peculiar to auto-take something in order to drop it.
The rules point up some interesting corner cases. The EAT action has TAKE, but the DRINK action does not. Why? Because drinkables are liquids, and taking liquids always has special rules -- if it's possible at all. You should be able DRINK from a stream, or at least try, without executing TAKE WATER behind the scenes.
(Mind you, the rules aren't always clear. In Zork 1, lots of actions have TAKE, but only a few have both TAKE and HAVE. Looks like BURN, LIGHT, EXTINGUISH, and WAVE. Why those?)
Okay, let's get to the mysterious tokens.
SH, SC, SOG, and SIR are used in exactly one place in the parser code. It's this stanza:
<COND (,LIT
<FCLEAR ,PLAYER ,TRANSBIT>
<DO-SL ,HERE ,SOG ,SIR>
<FSET ,PLAYER ,TRANSBIT>)>
<DO-SL ,PLAYER ,SH ,SC>)>
Notice that they are used in pairs: SH with SC, SOG with SIR. That's how they're handed off to the DO-SL routine, which is quite short. Feel free to look at it, but here's the gist:
DO-SL takes a container and two bit flags. If the slot has the first flag, we'll check the container's immediate children. If the slot has the second flag, we'll check the container's indirect descendants (those at the second level or below). If the slot has both flags, we therefore wind up checking all of the container's descendants, at every level. (There's a special case for this but it's just a shortcut.)
That may seem rather abstract, but think about how it works with the stanza above. The line <DO-SL ,PLAYER ,SH ,SC> simply means: Check the player's inventory. A SH (HELD) slot will match anything the player is directly holding. A SC (CARRIED) slot will match anything the player is carrying in a container. If a slot has both tokens (which is by far the common case), any object anywhere in the player's inventory will match.
The line <DO-SL ,HERE ,SOG ,SIR> does exactly the same thing, but checking the room contents, with the SOG (ON-GROUND) and SIR (IN-ROOM) flags. The first means directly on the ground; the second means things in containers in the room; both flags together mean anywhere in the room. Except for the player's inventory! We briefly set the player non-transparent, so this line doesn't search inside the player. That keeps the HERE search from getting mixed up with the previous PLAYER search.
Again, most verbs pair ON-GROUND with IN-ROOM. (It's odd for an action to apply to only things in containers.)
Oh, and the room search only runs if the location is LIT. Zork convention is that dropped objects are inaccessible in pitch darkness. You can manipulate your inventory in the dark, like a good spelunker should -- but only your inventory.
Well, now that we've dug through the details, it seems straightforward. Why did Meretzky say it was confusing?
Turns out I skipped over one tricky detail. The SH, SC, SOG, SIR tokens are suggestions, not requirements.
HAVE is a requirement. Some actions can only be done when you're carrying a thing; they need to fail when you're not. But HELD (SH) and company only come into play for disambiguation.
Let's go back to that original manual example:
<SYNTAX
GIVE
OBJECT (MANY HELD HAVE)
TO
OBJECT (FIND ACTORBIT) (ON-GROUND)
= V-GIVE PRE-GIVE>
If you're carrying the lunch, you can type GIVE LUNCH TO TROLL -- that's fine. (The troll eats it.)
But you can equally well type GIVE TROLL TO LUNCH. That fails the HAVE test ("You're not carrying the troll") -- but before that point, it skims right by the HELD token for the troll and the ON-GROUND token for the lunch. Like I said, just suggestions.
Say you were carrying a spicy meatball and also a spicy pepper in a glass jar and there was a spicy burrito on the floor. Then the command GIVE SPICY TO TROLL would have three options. The HELD token would cause it to prefer the meatball, because you're holding it directly.
Similarly, in Deadline, GIVE HERRING TO WOMAN would prefer Mrs Rouke (standing in the room) to Ms Dunbar (sitting on the couch, and therefore not ON-GROUND).
Again, it's common for these tokens to appear in pairs. Zork 1 has the syntax
<SYNTAX LUBRICATE OBJECT WITH OBJECT (HELD CARRIED) = V-OIL>
So LUBRICATE HINGES WITH GREASY would prefer a greasy object anywhere in your inventory to one lying on the ground.
(Of course Zork doesn't have even one greasy object. That command is for the benefit of people trying a trick that worked in Colossal Cave!)
But look at this one:
<SYNTAX TALK TO OBJECT (FIND ACTORBIT) (IN-ROOM) = V-TELL>
This doesn't have the ON-GROUND token. So TALK TO WOMAN would disambiguate to a female NPC sitting on the couch, rather than one standing in the room. Why on earth would you want that behavior?
The answer is, you wouldn't! But this situation can't happen in Zork; the three NPCs never enter containers. So the mistake isn't noticeable. In fact the NPCs in Deadline don't sit on the furniture either, so it never comes up there either. (My example with Rourke and Dunbar was fake, sorry.)
Similarly:
<SYNTAX PUT ON OBJECT (IN-ROOM ON-GROUND CARRIED MANY) = V-WEAR>
This lacks HELD, so it prefers items you're carrying in containers to items you're holding directly. This is ridiculous. PUT ON HAT should not prefer the hat in your backpack to the hat in your hand.
The problem is, demonstrating these bugs is hard. You need to find two objects with a synonym in common, put one of them in a container, and then try a particular verb.
Here's a demonstration. Remember that all treasures in Zork, including the PAINTING, can be referred to as TREASURE...
> I
You are carrying:
A painting
A brass lantern (providing light)
A sword
A brown sack
The brown sack contains:
A jewel-encrusted egg
A clove of garlic
> PUT ON TREASURE
You can't wear the jewel-encrusted egg.
See? Picking the egg over the painting is silly! But of course the command PUT ON TREASURE was silly to begin with. I guarantee that nobody at Infocom ever tested it. You need to spend a week staring at the parser logic to know how to even set this experiment up.
To perform the parallel experiment for ON-GROUND/IN-ROOM, drop both the sack and the painting and type TALK TO TREASURE.
The upshot is that if you're building a <SYNTAX> line with ON-GROUND and IN-ROOM, getting it wrong almost doesn't matter. You'll never get any feedback that you should have used the other one (or both). Thus, confusing. It's a detail which is almost impossible to learn.
The difference between HELD and CARRIED is a bit clearer, because inventory containers are common and you don't want to screw up the affordances of the DROP action. But the DROP syntax is the same in every game; they copied those basic verbs around. Most Infocom folks probably never needed to know why DROP is written the way it is.
In conclusion...
Nah, I'm not building up to a grand thesis here. I'm pointing out the ways that a design system can be opaque, even to the people who invented it.
I guess the lesson is that ZIL should have provided a simpler set of options for common use. Maybe define HELD and IN-ROOM for most verbs, and then fancier terms (HELD-DIRECT vs HELD-INDIRECT, IN-ROOM-DIRECT vs IN-ROOM-INDIRECT) for the few cases that really required them. If there were any.
Also, more regression tests. Create a "game" with a playground of objects, containers, and NPCs; run through every combination of actions and objects and containment setups. Or at least enough combinations to exercise every possible parsing outcome, plus all the weird experiments above.
As far as I know, ZIL never had this testing setup. Neither did the hobbyist IF systems of the 1990s. Inform 7 has a very large suite of unit tests, but I don't know if they're written to exercise the parser as distinct from the I7 compiler.
Future goals? Maybe.