Zork is a heavily data-driven game, programmed in a language derivative of LISP, and with a lot of conventions reminiscent of object oriented programming.
The MDL source code is therefore very rich in information, but trying to extract all of the data from the game would not be an interesting exercise. Almost the entire game is defined in the in-line data tables – they define the rooms, their descriptions, the objects, where they can be used, and what commands can be used where. The tables would be overwhelming to browse, and would mostly reveal what I already figured out by solving the game.
But there were a number of mysteries of Zork that I wanted to solve, by way of examining the source code.
First, here’s a list of things that score points:
Enter Cellar | 25 |
Enter East-West passage | 5 |
Enter Kitchen | 10 |
Enter Land of Dead | 30 |
Enter Lower Shaft | 10 |
Enter Strange Passage | 10 |
Enter Treasure Room | 25 |
Enter Top of Well | 10 |
Get bauble | 1 |
Get bills | 10 |
Get blue sphere | 10 |
Get bracelet | 5 |
Get canary | 6 |
Get chalice | 10 |
Get coffin | 3 |
Get coins | 10 |
Get crown | 15 |
Get diamond | 10 |
Get egg | 5 |
Get emerald | 5 |
Get gold card | 10 |
Get grail | 2 |
Get jade figurine | 5 |
Get necklace | 9 |
Get painting | 4 |
Get platinum bar | 12 |
Get portrait | 10 |
Get pot of gold | 10 |
Get red sphere | 10 |
Get ruby | 15 |
Get spices | 5 |
Get stamp | 4 |
Get statue | 10 |
Get torch | 14 |
Get trident | 4 |
Get trunk | 15 |
Get violin | 10 |
Get white sphere | 6 |
Get zorkmid | 10 |
Deposit bauble | 1 |
Deposit bills | 15 |
Deposit blue sphere | 5 |
Deposit bracelet | 3 |
Deposit canary | 2 |
Deposit chalice | 10 |
Deposit coffin | 7 |
Deposit coins | 5 |
Deposit crown | 10 |
Deposit diamond | 6 |
Deposit egg | 5 |
Deposit emerald | 10 |
Deposit gold card | 15 |
Deposit grail | 5 |
Deposit jade figurine | 5 |
Deposit necklace | 5 |
Deposit painting | 7 |
Deposit platinum bar | 10 |
Deposit portrait | 5 |
Deposit pot of gold | 10 |
Deposit red sphere | 5 |
Deposit ruby | 8 |
Deposit spices | 5 |
Deposit stamp | 10 |
Deposit statue | 13 |
Deposit torch | 6 |
Deposit trident | 11 |
Deposit trunk | 8 |
Deposit violin | 10 |
Deposit white sphere | 6 |
Deposit Woods stamp | 1 |
Deposit zorkmid | 12 |
And here are point-scoring events in the endgame:
Enter Crypt | 5 |
Enter Top of Stairs | 10 |
Enter Inside Mirror | 15 |
Enter Dungeon Entrance | 15 |
Enter Carrow Corridor | 20 |
Enter Treasury of Zork | 35 |
One of the mysteries of Zork that I pondered was, how does the combat actually work? There’s clearly a rather in-depth combat engine, offering much more complexity than what seems warranted. There are only three enemies who can be fought in the whole game – not enemy types, three enemies total, and only two of them can be beaten through combat. There’s the troll, a pushover, and the thief, who is very tough, but becomes easier late in the game, as your strength correlates to your score. And the cyclops, who is impossible to beat in combat.
I studied the code, but I don’t know MDL, and so a lot of my conclusions are guesses.
First of all, each fighting-capable entity – you, the troll, the thief, and the cyclops, have a base strength rating.
Your base strength is determined by this formula:
2 + int(5 * ([score]/616) + 0.5)
It translates to this, table-wise:
Score | Strength |
0-61 | 2 |
62-184 | 3 |
185-307 | 4 |
308-431 | 5 |
432-554 | 6 |
555-616 | 7 |
The troll has a base strength of 2.
The thief has a base strength of 5.
The cyclops has a base strength of 10,000. That’s just a formality, possibly a leftover from an early build. Attacking him doesn’t even invoke any combat functions that would take his strength into account.
HP is determined by base strength, subtracted by 1 for every light wound sustained, and subtracted by 2 for every serious wound sustained. This also affects carrying capacity. Wounds heal over time.
Fighting strength is equivalent to HP remaining. The thief has a special case; if “engrossed,” fighting strength will be lowered to 2 (unless it was already 2 or lower).
Attack and defense are both equal to fighting strength, though the troll gets a -1 defense penalty if attacked with the sword, and the thief gets a -1 defense penalty if attacked with the knife.
When one entity attacks another, a lookup table for a result is selected depending on the attacked target's defense level.
A big thanks to Viila for helping me better understand how the tables work!
If the defense is 1, this table is used:
Attack = 1 | Attack = 2 | Attack >= 3 |
Miss | Miss | Miss |
Miss | Miss | Miss |
Miss | Miss | Stagger |
Miss | Stagger | Stagger |
Stagger | Stagger | Knockout |
Stagger | Knockout | Knockout |
Knockout | Knockout | Kill |
Knockout | Kill | Kill |
Kill | Kill | Kill |
Then, one of these columns of possible results is selected, based on the attacker’s attack value. From the nine cells in the column, a result is randomly selected.
So, if your opponent’s defense is 1, and your attack is 1, then these will be the possible results when you attack:
Miss |
Miss |
Miss |
Miss |
Stagger |
Stagger |
Knockout |
Knockout |
Kill |
And if your attack is 3 or higher, then these will be the possible results:
Miss |
Miss |
Stagger |
Stagger |
Knockout |
Knockout |
Kill |
Kill |
Kill |
Also, if the result is “stagger,” then there will be a 25% chance of the staggered victim being disarmed. Unless you broke a mirror! In that case, there’s a 50% chance of disarming if you’re the staggered victim, and a 10% chance of disarming if you’re the attacker. I love little touches like that. That said, I’m not sure if there’s a meaningful difference between staggering and disarming. Either one, in practice, seems to just cost the victim their turn without doing damage.
If the target’s defense is 2, then the table looks like this:
Attack = 1 | Attack = 2 | Attack = 3 | Attack >= 4 |
Miss | Miss | Miss | Miss |
Miss | Miss | Miss | Stagger |
Miss | Miss | Stagger | Stagger |
Miss | Stagger | Stagger | Light wound |
Miss | Stagger | Light wound | Light wound |
Stagger | Light wound | Light wound | Light wound |
Stagger | Light wound | Light wound | Knockout |
Light wound | Light wound | Knockout | Kill |
Light wound | Knockout | Kill | Kill |
And if the target’s defense is 3 or higher, then this is the result table. You must subtract the target’s defense from the attacker’s attack to get the correct result column. E.g. - if you attack with strength 2 against the thief's 4, the difference is -2, and you select the leftmost column.
A – D <= -2 | A – D = -1 | A – D = 0 | A – D = 1 | A – D >= 2 |
Miss | Miss | Miss | Miss | Miss |
Miss | Miss | Miss | Miss | Stagger |
Miss | Miss | Miss | Stagger | Stagger |
Miss | Miss | Stagger | Stagger | Light wound |
Miss | Stagger | Stagger | Light wound | Light wound |
Stagger | Stagger | Light wound | Light wound | Light wound |
Stagger | Light wound | Light wound | Light wound | Light wound |
Light wound | Light wound | Light wound | Serious wound | Serious wound |
Light wound | Serious wound | Serious wound | Serious wound | Serious wound |
So let’s say you are starting out, and attack the troll with your sword. Your attack value will be 2, since you are at perfect health but have no experience. His base defense is 2, and will be reduced to 1 because of his defense penalty. This lookup table is used:
Attack = 2, Defense 1 |
Miss |
Miss |
Miss |
Stagger |
Stagger |
Knockout |
Knockout |
Kill |
Kill |
That’s essentially a 3/7 chance to do nothing and a 4/7 chance to win the fight, since staggering is pretty much just a free turn for you, and a knockout gives you a guaranteed kill on the next round. This is pretty good odds.
If you fail, then the troll attacks with 2 strength against your defense of 2.
Attack = 2, Defense 2 |
Miss |
Miss |
Miss |
Stagger |
Stagger |
Light wound |
Light wound |
Light wound |
Knockout |
Odds are 3/7 he misses and the ball is in your court again, everything back as the combat started, 3/7 that he lightly wounds you, and 1/7 that he knocks you out (and will probably kill you next round).
Assuming he lightly wounds you, your strength becomes 1, and now this becomes your new lookup table when you attack:
Attack = 1, Defense 1 |
Miss |
Miss |
Miss |
Miss |
Stagger |
Stagger |
Knockout |
Knockout |
Kill |
4/7 odds of missing and reverting control to the troll, and 3/7 odds of winning. Miss, and the troll attacks with 2 against your defense of 1, the same sub-table that you started with.
I did some math, and estimate you have about an 83% chance of beating the troll at the start of the game.
Then I did more math to estimate the odds of beating the thief based on your experience level. This assumes that he gets first strike, that you use the knife, and that he will kill you if you get knocked out. Take this with a grain of salt, because I got really bogged down in the algebra, and I don’t feel like checking it again.
Your level | Approximate odds |
2 | 0.25% |
3 (62 points) | 16% |
4 (185 points) | 23% |
5 (308 points) | 46% |
6 (432 points) | 67% |
7 (555 points) | 89% |
I also did an experiment to see how early in the game I could kill the thief. I got a few early treasures and also robbed the Bank of Zork, giving me a score of 64, and a combat strength of 3. I went to visit the thief, and this happened:
Treasure Room
This is a large room, whose north wall is solid granite. A number
of discarded bags, which crumble at your touch, are scattered about
on the floor. There is an exit down and what appears to be a newly
created passage to the east.
There is a silver chalice, intricately engraved, here.
You hear a scream of anguish as you violate the robber's hideaway.
Using passages unknown to you, he rushes to its defense.
The stiletto flashes faster than you can follow, and blood wells
from your leg.
So, the thief gets first strike when you intrude on his lair. He had a combat strength of 5, I had a combat strength of 3, so he was going to get the most favorable combat table possible, and “light wound” was the result. That left me with a combat strength of 2, but I figured if I gave him treasure, his defense would be only 1 – a base value of 2 for being engrossed, and a -1 penalty for being attacked with the knife.
> give egg to thief
The thief is taken aback by your unexpected generosity, but accepts
the jewel-encrusted egg and stops to admire its beauty.
> kill thief with knife
The quickness of your thrust knocks the thief back, stunned.
The thief slowly regains his feet.
Would he accept more treasure?
> give portrait to thief
The thief is taken aback by your unexpected generosity, but accepts
the portrait of J. Pierpont Flathead and stops to admire its beauty.
> kill thief with knife
The thief is disarmed by a subtle feint past his guard.
The robber, somewhat surprised at this turn of events, nimbly
retrieves his stilletto.
Yep! Continuing:
> give bills to thief
The thief is taken aback by your unexpected generosity, but accepts
the stack of zorkmid bills and stops to admire its beauty.
> kill thief with knife
The thief drops to his knees, staggered.
The thief slowly regains his feet.
> give painting to thief
The thief is taken aback by your unexpected generosity, but accepts
the painting and stops to admire its beauty.
> kill thief with knife
The fatal thrust strikes the thief square in the heart: He dies.
Almost as soon as the thief breathes his last breath, a cloud
of sinister black fog envelops him, and when the fog lifts, the
carcass has disappeared. His booty remains.
As the thief dies, the power of his magic decreases, and his
treasures reappear:
A jewel-encrusted egg
A portrait of J. Pierpont Flathead
A stack of zorkmid bills
A painting
>
And so, I killed the thief pretty early in the game. Of course I made it unwinnable in the process, since you need him alive to open the egg for you, but it shows that if you’re willing to exploit his behavior, you don’t need to put this off for very long, and it your life will be easier without him wandering around the dungeon messing things up.
I had also wondered how the thief behaves when wandering, but couldn’t reach as many conclusions as I wanted to. The thief traverses most of the rooms in Zork, even outdoor rooms like the climbable tree in the forest. He ignores the normal passageways, and instead just moves from room to room in a completely predetermined pattern, based on the room’s internal ID numbers. A large number of rooms are marked “sacred,” including the living room, and I believe this flag prevents the thief from entering.
One last mystery was determining how the inventory limit works, and the data tables proved invaluable for this.
Almost all objects, whether takeable or not, have a “size” parameter. Those that don’t effectively have a “size” of zero. You can hold up to 100 units in your inventory.
Some objects also have a “capacity” parameter, and can hold other objects, with a combined size of up to the container’s capacity. Sometimes they need to be “opened” first, such as the grail, which apparently is more like a stein.
Here is a list of container objects:
Size | Capacity | |
Brochure | 30 | 1 |
Woods Stamp | 1 | 1 |
Blue book | 10 | 2 |
Brick | 9 | 2 |
Green book | 10 | 2 |
Purple book | 10 | 2 |
White book | 10 | 2 |
Glass bottle | 0 | 4 |
Silver chalice | 10 | 5 |
Flask | 10 | 5 |
Grail | 10 | 5 |
Egg | 0 | 6 |
Sack | 3 | 15 |
Red buoy | 10 | 20 |
Steel box | 40 | 20 |
Nest | 0 | 20 |
Coffin | 55 | 35 |
Wooden barrel | 70 | 100 |
Balloon basket | 70 | 100 |
Boat | 20 | 100 |
And here is a list of non-container objects and their sizes. Not all can be taken.
Bag of coins | 15 |
Bloody axe | 25 |
Blue label | 1 |
Black book | 10 |
Broken balloon | 40 |
Stick | 3 |
Timber | 50 |
Coke bottles | 15 |
Useless lantern | 20 |
Card | 1 |
Crown | 10 |
White sphere | 10 |
Trident | 20 |
Violin | 10 |
Green paper | 3 |
Pump | 0 |
Diamond | 0 |
Guano | 20 |
Jade figurine | 10 |
Lamp | 15 |
Emerald | 0 |
Leaflet | 2 |
Cage | 60 |
Matchbook | 2 |
Newspaper | 2 |
Painting | 15 |
Candles | 10 |
Necklace | 10 |
Eatme cake | 10 |
Blue cake | 4 |
Orange cake | 4 |
Red cake | 4 |
Vitreous slag | 10 |
Leaves | 25 |
Plastic | 20 |
Platinum bar | 20 |
Pot of gold | 15 |
Zorkmid | 10 |
Water | 4 |
Rope | 10 |
Ruby | 0 |
Rusty knife | 20 |
Bracelet | 10 |
Skeleton Keys | 10 |
Shovel | 15 |
Coal | 20 |
Printer paper | 70 |
Flathead stamp | 1 |
Statue | 8 |
Stiletto | 10 |
Sword | 30 |
Tan label | 2 |
Rare spices | 8 |
Torch | 20 |
Trunk of jewels | 35 |
Tube | 10 |
Gunk | 6 |
Wire | 1 |
Wrench | 10 |
Zorkmid bills | 10 |
Portrait | 25 |
Bauble | 0 |
Clockwork canary | 0 |
Gold card | 4 |
Blue sphere | 0 |
Mat | 12 |
Red sphere | 0 |
Interesting. Since one of the Steel Boxes has size 0, does it mean that you could put it inside the Brochure (capacity 1)? :P (Or the egg, or the glass bottle, etc.)
ReplyDeleteAs for the mystery code ,DEF3B ,DEF3C>>
The brackets group statements. Eg: <+ 1 1> would return 2. MDL uses prefix notation, so the operator is first element and operands are the following elements.
is some kind of Zork's own wrapper around , it sets value of a global atom (variable) and adds that atom to global PURE-LIST. What that is I have no idea.
,atom is equivalent to , it retrieves the global value of the atom. So, I would expect following code:
,foo
would return 1
makes an UVECTOR (which I presume is a list datastructure of some kind)
I presume returns list, sans the first element. "rest of the list"
So, unpacking the line of code:
Set the global atom DEF3-RES to value of: ,DEF3B ,DEF3C>
Unpacking that, the value is an UVECTOR made out of elements: ,DEF3A (global value of DEF3A); the rest of the list ,DEF3A; global value of ,DEF3B; rest of the list DEF3B; global value of ,DEF3C
So, as far as I can tell, that bit of code duplicates the lists DEF3A and DEF3B, except for the first element and concatenates the original, duplicated lists, and list DEF3C. Why? I have no idea. I presume something then takes the DEF3-RES list and returns an element from it by some logic.
Bummer. Looks like Blogger ate all the brackets. So here's the code analysis with the angle brackets replaced with ()s:
ReplyDeleteAs for the mystery code (PSETG DEF3-RES (UVECTOR ,DEF3A (REST ,DEF3A) ,DEF3B (REST ,DEF3B) ,DEF3C))
The brackets group statements. Eg: (+ 1 1) would return 2. MDL uses prefix notation, so the operator is first element and operands are the following elements.
(PSETG) is some kind of Zork's own wrapper around (SETG), it sets value of a global atom (variable) and adds that atom to global PURE-LIST. What that is I have no idea.
,atom is equivalent to (GVAL atom), it retrieves the global value of the atom. So, I would expect following code:
(PSETG foo 1)
,foo
would return 1
(UVECTOR [elements]) makes an UVECTOR (which I presume is a list datastructure of some kind)
(REST [list]) I presume returns list, sans the first element. "rest of the list"
So, unpacking the line of code:
Set the global atom DEF3-RES to value of: (UVECTOR ,DEF3A (REST ,DEF3A) ,DEF3B (REST ,DEF3B) ,DEF3C)
Unpacking that, the value is an UVECTOR made out of elements: ,DEF3A (global value of DEF3A); the rest of the list ,DEF3A; global value of ,DEF3B; rest of the list DEF3B; global value of ,DEF3C
So, as far as I can tell, that bit of code duplicates the lists DEF3A and DEF3B, except for the first element and concatenates the original, duplicated lists, and list DEF3C. Why? I have no idea. I presume something then takes the DEF3-RES list and returns an element from it by some logic.
I goofed a bit on the steel box. There are two steel boxes in the game; the one in the volcano with the crown and the one in the loud room with the violin. The crown box is weightless, but you can't take it with you, so this isn't very useful. I've taken this one out of the list.
DeleteI'm still a bit confused by your explanation of DEF3-RES. Are you saying there are five elements in the list? That would make plenty of sense if there are exactly five, but what does this mean?
the rest of the list ,DEF3A
DEF3_RES gets used here:
(SET TBL (NTH ,DEF3-RES(+ ATT 3)))
There is a precondition that someone with a defense value of 3 or higher is being attacked. ATT must be a value in between -2 and 2, which means (+ ATT 3) must be a value in between 1 and 5. Therefore, it would make complete sense if DEF3-RES had exactly five elements. It would mean that TBL is set to one of the five elements, based on the ATT value.
I think I get it! There are five elements in DEF3-RES, and each element is itself a UVECTOR. This has some implications for how the other lookup tables work as well. For instance, the DEF1_RES table looks like this:
Delete(UVECTOR ,DEF1 (REST ,DEF1) (REST ,DEF1 2))
I now understand this to mean a three element UVECTOR, each element itself a UVECTOR, and it would be equivalent to this:
* DEF1
* DEF1 minus the first element
* DEF1 minus the first two elements
I've updated the combat section of this post with this new information. Thanks!