Wednesday, August 30, 2017

Your load is too heavy: Zork deep reading

This past weekend a screenshot went around Twitter (my part of Twitter at least!)

weight = num_items * Max_held_mult;
  if( weight <= random(100) ) ?label8;
  print "You're holding too many things already!";
  new_line;
  rfalse;
.label8;
  move noun to player;
(-- @icculus, Aug 26)
The clear reading of this code (as the screenshot says) is that the inventory limit in Zork 1 is random, not a fixed number of items. Each item you pick up makes it more likely that you'll hit a "holding too many things" error. But since it's a random chance, you can just try again -- it might work next time.
This was passed around in a commentary cloud of "This game was unfair," "games in the 80s were terrible," and so on. (See this NeoGAF thread, for example.)
This is fascinating! I played Zork, as I played all the Infocom games, and I didn't remember this inventory detail. It felt dimly familiar when I was reminded of it, though.
Research time!

Is it true?

That's the first question, of course. Let's try it.
>i        
You are carrying:
  A rope
  A nasty knife
  A brass lantern (providing light)
  A clove of garlic
  A lunch
  A brown sack
  A glass bottle
  The glass bottle contains:
    A quantity of water
  A jewel-encrusted egg
  A leaflet

>get sword
You're holding too many things already!

>get sword
Taken.
It's true! It's true!
Here I'm playing Zork 1, revision 88, serial number 840726. This is by far the most common version you'll find today, because it was the version included on the Lost Treasures of Infocom CD. You can play it today on iOS (at least until iOS11 hits) or on GOG. Or I'm pretty sure you can find it with a web search.

But is it evil?

Well, that's a more complicated question.
If you look at the Twitter thread, you'll see that the code snippet is taken from this source code listing (wayback link from 2004). Let's look at a larger chunk of this file:
     if( parent(noun) in player ) ?label4;
     weight = QueryWeight(noun);
     if( (weight + QueryWeight(player)) <= Load_max ) ?label4;
     if( ~~vb ) ?label5;
     print "Your load is too heavy";
     if( Load_max >= Load_allowed ) ?label6;
     print ", especially in light of your condition.";
     jump label7;
  .label6;
     print ".";
  .label7;
     new_line;
  .label5;
     return 2;
  .label4;
     if( Verb ~= ##Take ) ?label8;
     num_items = CCount(player);
     if( num_items <= Maximum_held ) ?label8;
     weight = num_items * Max_held_mult;
     if( weight <= random(100) ) ?label8;
     print "You're holding too many things already!";
     new_line;
     rfalse;
  .label8;
     move noun to player;
Here it becomes apparent that there are two independent limit tests when you pick up an object. First it checks the sum of your weight and the weight of what you're picking up. This is a constant test: the sum must be less than Load_max (100 pounds), or it displays the error "Your load is too heavy." (If you're wounded, the weight limit decreases.)
Then it checks the number of items you're holding. This is the randomized test, but there's a safety zone: anything up to Maximum_held (7 items) is safe. In fact, if you're carrying 7 items, the next TAKE command is safe. Beyond eight items, the chance of failure is num_items * Max_held_mult (N*8) as a percentage.
So this is already less evil than it looked at first. You can carry up to 100 pounds and eight items, but the item limit is soft -- you get some wiggle room on that.
More trivia:
  • Object weights range from 2 (the leaflet, matchbook, etc) to 55 (the gold coffin).
  • Weight is figured recursively -- objects in a container still count towards the weight limit.
  • Object count is not recursive, so you can work around the "too many things" error with careful sack-management.
  • Worn clothing is counted as weight 1. But there is no clothing in Zork 1, so this doesn't help you much!
  • The original MIT Zork/Dungeon game, the predecessor to Zork 1/2/3, did not have the item count check (randomized or otherwise). It only checked total weight.

So is Zork 1 evil?

Of course it's evil! There's a thief who can walk into the room and kill you! Or steal vital equipment from you and hide it in an inaccessible room! Your lamp dies after 385 turns! Evil, sheesh.
But that's not a complete answer.
Remember that we are looking at the dawn of computing gaming history. The very idea that a videogame should be fair, or even winnable, was hazy. It was perfectly normal for a game to just get harder and harder until it killed you. (Think Pac-Man, Asteroids, etc.)
The adventure genre has always presented itself as "solvable", but of course that is itself a subjective standard. Through the 1980s, we took for granted that solving a game took repeated attempts -- death after death, retry after retry, mistake after mistake -- learning (hopefully) a little each time. Today we say "masocore"; back then it was just the way games were. The thief was an annoyance. "Unfair" sequencing, like being able to accidentally destroy or lose a crucial object, wasn't even worth a blink.
(Jason Dyer has been going through the adventure games of the 1970s; there's also the Digital Antiquarian. Those blog series give an excellent introduction to just how arbitrary, buggy, and poorly tested a lot of those early games were. Remember, Infocom's canon stood out for being much better than the rest. Honest.)
The soft item count limit, taken as a game mechanic, wasn't unreasonable. In particular, you can't say it was a trivial annoyance whose only purpose was to make the player retype a command. Wasting a turn is a meaningful penalty in Zork. Your lamp is slowly dying; the thief is out there wandering. If you run into that "too many items" error, you've failed at inventory management. You should have paid more attention and put something in your sack first.
Was inventory management a good game design idea? Well, no. I thought it was annoying and tedious then; I still think so today. But it was part of the Zork ethos, and the item limit was part of the inventory system.

Are we really looking at the Zork 1 source code?

Excellent question! Always question hot takes you see on Twitter. The answer is "yes, sort of."
The Zork file we've been discussing is not the source code that Lebling and Blank wrote at Infocom circa 1980. Rather, it is a disassembly of the Z-machine game file that Infocom sold. (And which can be found on the Lost Treasures CD, etc.)
The Z-machine format was never published by Infocom, but it was reverse-engineered around 1990 and is now well understood. You can find tools which disassemble the Infocom game files (and Inform game files, for that matter). I use txd, found in the ztools package.
Run txd on the Zork 1 file, and you'll see... raw Z-machine assembly code. The txd output for the function we've been discussing looks like:
L0003: GET_PARENT      G76 -> -(SP)
       JIN             (SP)+,G6f [TRUE] L0007
       CALL            R0241 (G76) -> L03
       CALL            R0241 (G6f) -> -(SP)
       ADD             L03,(SP)+ -> -(SP)
       JG              (SP)+,G85 [FALSE] L0007
       JZ              L00 [TRUE] L0006
       PRINT           "Your load is too heavy"
       JL              G85,G86 [FALSE] L0004
       PRINT           ", especially in light of your condition."
       JUMP            L0005
L0004: PRINT           "."
L0005: NEW_LINE
L0006: RET             #02
L0007: JE              G78,#5d [FALSE] L0008
       CALL            R0240 (G6f) -> L01
       JG              L01,G3b [FALSE] L0008
       MUL             L01,G3a -> L03
       RANDOM          #64 -> -(SP)
       JG              L03,(SP)+ [FALSE] L0008
       PRINT           "You're holding too many things already!"
       NEW_LINE
       RFALSE
L0008: INSERT_OBJ      G76,G6f
If you compare this to the code above, you can see they behave the same. But the game file is compiled code. All the symbols -- the variable and function names -- have been stripped out.
So where did that original file, with its nice labels, come from? If you go back to the twitter thread, you'll see:
Holy crap, you pulled that literal code from my horrible, horrible decompiler output from the early 2000s!
(-- @allengarvin, Aug 27)
Allen Garvin started with that raw disassembly. Then he laboriously figured out what each line did, and gave every function and variable an appropriate label. (Not, of course, the same labels that the Infocom authors used!)
As Allen's tweet implies, we've been looking at a crude, early attempt. He has a much cleaner Zork 1 source file posted today:
if (parent(noun) notin player) {
    weight = QueryWeight(noun);
    if (weight + QueryWeight(player) > Load_max) {
        if (vb) {
            print "Your load is too heavy";
            if (Load_max < Load_allowed) {
                print ", especially in light of your condition.";
            } else {
                print ".";
            }
            new_line;
        }
        return A_FAILURE;
    }
}
if (action == ##Take) {
    num_items = CCount(player);
    if (num_items > Maximum_held) {
        weight = num_items * Max_held_mult;
        if (weight > random(100)) {
            print "You're holding too many things already!";
            new_line;
            rfalse;
        }
    }
}
move noun to player;
It does exactly the same thing, but it's much more readable, right? It's also recompilable Inform 6 source code.

What about the Zork 2 stuff?

In the swirling tweet-gyre, Jason Scott (noted Infocom historian!) wrote:
The main thing is, I am coming to the conclusion that what this really is Zork II's FUMBLE spell effects on you.
(-- @textfiles, Aug 28)
(FUMBLE is one of the curses that the Wizard of Frobozz casts on you. But the Wizard doesn't show up until Zork 2.)
Jason points out that the Infocom games re-used parser code all the time. It was a big chunk, essentially a library, and the Infocom authors copied and pasted it from one game to the next. (Remember that detail about clothing?) In fact, if you look a little farther down the original Zork 1 file, you'll see:
  .label8;
     move noun to player;
     give noun visited;
     Zork2_deletion();
     ScoreObj(noun);
     rtrue;
In fact the whole Zork 1 parser is studded with Zork2_deletion() calls. But, on the other hand, we've seen that the randomizer really does take effect in Zork 1. So Jason is right about re-used code, but wrong about the FUMBLE curse theory. (Sorry!)
So what's going here?
First of all, remember that Zork2_deletion is a label that Allen Garvin added. The compiled game file just has a do-nothing call to an empty function.
Why did Allen use that label? If you look at a disassembly of Zork 2, there's a very similar routine, but it has some extra code that can print the message "When you touch the [noun] it immediately disappears!" This has to do with the FANTASIZE curse, which makes you hallucinate fake objects.
It's pretty clear that both games (and probably Zork 3) were compiled with a common parser library. But the library was rigged a lot of special curse conditions which were only compiled in when building Zork 2. (#ifdef code, we'd say today.)
The randomized item limit was not one of these curses. It really did apply in both games.
Most of Infocom's games had inventory limits, but the form varied.
  • Enchanter: Weight and item limit, just as in Zork 1.
  • Zork 3: Weight and item limit, with a twist: "Oh, no. The [obj1] slips from your arms while taking the [obj2] and both tumble to the ground."
  • Lurking Horror: Weight and item limit, but not randomized.
And so on.
(Yes, I keep all the Infocom game files in disassembled form, to answer questions just like this. But no, I'm not going to go through and catalog the inventory limits in every single one.)

Conclusions

This post has gotten really long and I haven't figured out a conclusion for it. (Yes, you say, like so many other blog posts...) Well, try this:
Detail matters. And comparing different versions of the same file can be surprisingly interesting.

10 comments:

  1. I'm already thinking of footnotes I should have added...

    I say the weight limit is "100 pounds", but of course it's just 100, as a bare number. There's no attempt to use realistic pounds (or kilograms, or anything else).

    In real life, a solid gold sarcophagus would weigh many tons. That always annoyed me about Zork.

    ReplyDelete
  2. I found that once you rigged your setup right, saved game state re-hits were how to get over a bunch of things. But, it sucks the fun out of it, simply using that to get over a random number throw. Getting to the point of flood control dam #3 control room and mis-remembering how to apply to or how to instruct the robot in the electric room of death just depressed me, so I had saved state for each to re-play until I got over it.

    As I get older, zork and advent blur together, bits from each fuse in my mind. I think zork had more humour, if it also had more random evil.

    ReplyDelete
  3. I am so glad someone with skills took a look at this.

    I was confused because I was looking at the actual original Zork I source code (with the original Imp labels) and they use the term/call FUMBLE for the can't-hold-everything code, as well as for a spell in Zork II. But also, I'm not a programmer and that original code is dense, super dense.

    Like, I don't know, EVERY GAME SOURCE TREE EVER, there's a lot of minor sins in Infocom game code, mostly because there's no incentive to make it picture-perfect when you're dealing with a wide range of bugs to fix and the great environment Blank and Anderson and the others made ensures "plenty" of space to work in. It's only when you get to edge-cases like Trinity where the limits are being reached that dead stuff needs to go out in any intense way.

    The code back then worth revisiting with modern tools to find all sorts of fun crazy bugs that nobody has ever tried. I hope someone does that, sometime.

    ReplyDelete
    Replies
    1. Happy to take a look.

      Yeah, I believe the original variable FUMBLE is what Allen labelled Maximum_held.

      The fun discovery for me is that the Imps didn't just copy-and-paste the library forward. They pushed it back into older games, so that this 1984 release of Zork 1 contains a parser which has been modified for Zork 2 and later.

      (I haven't looked at the Solid Gold release (1987) at all.)

      Delete
  4. Very nice write-up. Yep, it took a lot of time to generate those labels. I'd look through the code, make a guess about functions or variables or addresses, add that to the config file, the decompile again. It was a laborious process that took a lot of hours to do. We did have the source code to mini-Zork at the time, so some of the names reflect the ZIL names. Other times, I'd just add something reasonable.

    Comparing the disassembled versions is very interesting: it tells you what bugs they fixed. I found a number of previously unknown bugs that way. Also, it tells you when they modified the compiler. The code generation changed 3-4 times in the history of Zork I, which has the most versions and the widest time-frame. Most of the time, those changes are pretty minor but they did introduce a couple small optimizations, and it may have generated in different orders (or, that might have been linker modifications). It's too bad that, apparently, Infocom didn't use source revision tools. We know they used a 68k Sun for storing sources of games and interpreters, so RCS would have been available in SunOS.

    One thing somewhat disappointing looking back is looking at decompiled code for later games, where they optimized the hell out of the source, replacing strings with print_obj code where they could to get extra abbreviations, but it's clear the "library" of code they had could have been optimized to recover 1-2k, or more. Graham did it well in his libraries. All those function calls with packed addresses, that did nothing more than print a string and return. I think if they had done a rigorous analysis of the outputted zcode, they could have crammed a lot more into the games (also, minor peephole optimizers, where you see things like a print string; newline; return, when a print_ret [to use modern instruction labels] would have reduced some).

    ReplyDelete
    Replies
    1. The Sun comes way, way later in the Infocom timeline, though. It's an attempt to get away from the mainframe into something more reasonable, but the lion's share of development was on the PDP-10 and stayed there. That we have the source code at all is due mostly to that Sun development environment, being experimental and all, then being shipped to California to Mediagenic.

      Delete
  5. For permanent interest, here was a list of new bugs I found by decompiling and comparing versions. I don't mention the versions for some of them, annoyingly. But there are some nice details on why they're bugs:

    http://plover.net/~agarvin/zorkbugs.txt

    ReplyDelete
  6. Perhaps it felt dimly familiar because I reminded you about it back in January :-)

    ReplyDelete
  7. Allen has followed up by posting his Reform data files for the known Infocom game files. These are essentially the reconstructed "symbol tables" -- names for variables, functions, object IDs, and so on.

    Not all of these are meaningful; some are just lists of numeric IDs with no names filled in. Nonetheless, a useful resource, and maybe people will contribute to it over time.

    https://github.com/allengarvin/reform-conf

    ReplyDelete