A work-in-progress tool-assisted speedrun of Zork I for Apple II

I’ve been working on a tool-assisted speedrun (TAS) of Zork I (revision 88) for the Apple II. What makes a speedrun tool-assisted is that, rather than being played live with a controller or keyboard, it is played in an emulator, taking advantage of the ability to pause, rewind, make savestates, and inspect the contents of memory. A TAS is developed in a process of incremental optimization, with the goal of producing a recorded sequence of timed button presses that, when played back in the emulator, finish the game quickly. A TAS is kind of the limit of “what if you had perfect reflexes and foresight and you had a run where everything went right?”

You can see the development history of the Zork I TAS, so far, starting with this post at the TASVideos.org forum. I’ve just finished a draft of a new, faster route through the game. The main feature of the route is manipulating the thief into carrying treasure. I’m posting the route here to see if anyone has ideas for improvement as I go about the process of optimization. Here’s a video of the TAS being played back on an emulator, alongside a monitor of the player’s and the thief’s inventories:

To run the TAS yourself, you will need to set up the BizHawk emulator for Apple II, load the Zork I disk image, and play the input file (.bk2 format).

A note on timing: TASes are usually measured according to real time elapsed (or, equivalently, by frame count), starting when the emulator powers on and ending at the final input. That’s the standard this TAS uses; in particular, it’s not necessarily trying to minimize the move count. The distinction matters on a slow computer like the Apple II, because not all moves take an equal amount of time. This run makes 236 moves in 20685 frames (5:44.75 at 60 FPS). The category is any%, which in the case of Zork I is the same as 100%, because you can’t finish the game with less than the maximum number of points.

This TAS is a bit special in that the whole thing is coordinated by a program—necessary to cope with the game’s randomness. The program is here; go to line 494 for the commented sequence of commands. The program is written in Fennel, a Lisp that compiles to Lua, and controls the emulator via BizHawk’s Lua integration. The biggest advantage of a tool-assisted speedrun, in the case of Zork I, is the ability to control the outcome of random events. When an undesirable random event happens (such as getting hit in combat), the Fennel program backtracks and tries again, altering the letter case of commands to tweak the state of the game’s random number generator (RNG).

A primary constraint on a Zork I speedrun is the limited carrying capacity of the player character. The player can carry at most 95 units of weight. (You would think it would be 100 units, but the WEIGHT routine includes the size of the WINNER object itself, which is 5 units!) That leads to the main idea of this route: getting the thief to carry treasure for us.

Unlike the player, the thief has unlimited carrying capacity. He wanders the dungeon along a set route and has a high chance of picking up any treasure he finds. With RNG manipulation we can make the chance 100%. The thief drops treasure periodically at his lair, the TREASURE-ROOM, which is conveniently located just 3 moves away from the LIVING-ROOM and the trophy case where you need to eventually deposit the treasures. We can instantly summon him to the TREASURE-ROOM by entering the room. The main constraint on the thief’s ability to help with treasure collection is that he does not steal from rooms the player has not yet been to. The major activity of this route is efficiently “visiting” rooms that contain treasure, in order to make the treasure eligible for taking by the thief. (There are, however, several special cases that require special handling, such as a few rooms and items that are marked “sacred” and are off-limits to the thief.) The only treasure that must be held by the thief at some point is the EGG, because only the thief can open it to expose another treasure (the CANARY) that is inside.

This is a schematic summary of the route. Here’s a map to follow along.

  1. Get the EGG, SWORD, and LAMP, defeat the troll, then go through the maze, getting the BAG-OF-COINS on the way. The cyclops is located at the entrance to the thief’s TREASURE-ROOM. Scare away the cyclops, which opens a quick path back to the LIVING-ROOM. Drop the EGG and BAG-OF-COINS for the thief to pick up later; also drop the SWORD to free up 30 units of inventory space. (We won’t need the sword again until the final battle with the thief.) Enter the TREASURE-ROOM and use a magic word to warp to NORTH-TEMPLE.
  2. Get the COFFIN (which is a “sacred” item the thief cannot move for us), use a magic word to drop it in the TREASURE-ROOM, then warp right back. Get the TORCH and SCEPTRE, which are not only treasures but also tools we will need later. Take the BELL, BOOK, and CANDLES to the ENTRANCE-TO-HADES (manipulating RNG to keep the CANDLES from blowing out) and do a ritual to open the gates. Visit the crystal SKULL past the gates, leaving it for the thief to get. Then go to the dam area, passing through the EW-PASSAGE on the way to earn 5 points.
  3. In the dam MAINTENANCE-ROOM, get the SCREWDRIVER and the WRENCH and use the WRENCH to drain the RESERVOIR and make it passable. Visit the TRUNK and TRIDENT past the reservoir, solve the LOUD-ROOM puzzle to make the BAR eligible for taking by the thief, then take the INFLATABLE-BOAT and PUMP to WHITE-CLIFFS-SOUTH, adjacent to the river.
  4. Inflate the boat and use it to cross the river. On the river there is a BUOY that contains an EMERALD. The thief cannot enter water rooms, so we must collect the EMERALD, not just visit it. Take the SHOVEL and dig up the SCARAB, leaving it for the thief. Go to ARAGAIN-FALLS and wave the SCEPTRE to make a rainbow bridge. Take the bridge down to the base of the waterfall, visiting the POT-OF-GOLD, and return to the house through the overworld. Drop off the junk and treasure we’re carrying, except for the LAMP, SCREWDRIVER, TORCH, and GARLIC, which we’ll need in the coal mine.
  5. Take the TREASURE-ROOM warp to NORTH-TEMPLE again, then warp from MIRROR-ROOM-2 to MIRROR-ROOM-1. Holding the GARLIC incapacitates the bat, which lets us get the JADE. (The BAT-ROOM is a sacred room, so we have to pick this one up.) Put the SCREWDRIVER and TORCH in the dumbwaiter. Dispose of the GARLIC in order to take advantage of the bat’s attack. When the GARLIC is not present, the bat seizes you and takes you to a random room in the vicinity of the coal mine. By manipulating RNG, we force the bat to drop us at the far end of the mine, saving travel time. Take the COAL, walk out through the mine, place the COAL in the dumbwaiter, and lower the dumbwaiter. Use the bat to warp over the mine again, drop everything to get through the narrow passage, collect the items from the dumbwaiter, and convert the COAL into the DIAMOND using the machine. Raise the dumbwaiter, walk back out through the mine, recollect the TORCH and DIAMOND from the dumbwaiter, and take the slide back to the CELLAR below the LIVING-ROOM.
  6. Take a short detour to the GALLERY to get the PAINTING. The thief could get this one, but we’re now too close to the end of the run for him to have time to get there. Go to the LIVING-ROOM to drop off the treasure we’re carrying. Go through the cyclops hole, pick up the SWORD, and enter the thief’s lair.
  7. Entering the TREASURE-ROOM causes the thief to warp there, along with the treasure he’s carrying. We manipulate an ideal sequence of random combat outcomes: 3 MISSEDs from the thief, and 2 SERIOUS-WOUNDs and a KILLED from the player. There is now a pile of treasure here we have to carry to the nearby LIVING-ROOM. We start with the CANARY (plus as much else as we can carry): we must take it outside to the forest for one final piece of treasure. Use a magic word to warp to NORTH-TEMPLE, then another to warp from SOUTH-TEMPLE to FOREST-1. Wind the CANARY to make the songbird drop the BAUBLE. Re-enter the house and drop off the treasure. Make two round trips through the cyclops hole to fetch the rest of the treasure.

I think there’s potential to make the route still faster. I’ve marked some potential changes with TODO comments in the source code. These are some ideas I have:

  • Save the COFFIN for last, rather than doing the awkward back-and-forth warp as soon as we get it. Collect the COFFIN on the final go-round with the CANARY, because we have to pass through that area anyway.
  • Maybe also leave the TORCH for last, or just visit it for the thief to pick up. The CANDLES work equally well as a light source in the dumbwaiter puzzle in the coal mine.
  • Consider collecting treasure as long as there’s inventory space. I haven’t audited which segments of the route have excess inventory capacity that might be used for carrying treasure. It’ll be worth it if we can save one round trip during the final treasure haul.
  • Dropping the LAMP for the final treasure haul will leave room for slightly more treasure per trip. There are 2 dark rooms between the TREASURE-ROOM and the LIVING-ROOM, but entering the first dark room is safe, and the second one can be RNG-manipulated to prevent a grue attack.
  • I want to try a reordering where you do the coal mine before the river area. The slide after the coal mine returns you basically to the house, which is relatively close to the bottom of the waterfall. Waving the SCEPTRE from the bottom works to get up to the SCARAB and EMERALD, then return to the house through the overworld.

That’s in addition to some micro-optimizations having to do with the syntax of commands, which I’ll make a separate post about.

11 Likes

This post is for people who know the Zork command parser well. It has to do with different but equivalent commands taking different amounts of time to execute, because of different code paths they exercise in the parser.

In preparing the speedrun route (the sequence of commands to execute), I’ve so far used the heuristic that shorter commands are better, because they are faster to enter. On the emulated Apple II with its emulated floppy drive, however, shorter commands are not always faster. Expressing a command in a different way can make it run faster, even if sometimes means typing more characters.

I saw an instance of this while I was preparing the new route of the first post. Near the end, there’s a place where we are holding many pieces of treasure, and the lamp. We want to put all the treasure in the trophy case, but keep the lamp. I found that the straightforward command put all but lamp in case was about 51 frames slower than put all treasu in case, which has the same effect.

(The Zork I command parser only looks at the first 6 letters of each word. So you can always abbreviate treasure to treasu. That’s separate from the main point.)

The game’s command parser is complicated and I am far from fully understanding it, but you can get some insight into how it works by seeing how objects are defined. Every object has a set of SYNONYM words and a set of ADJECTIVE words. Take the definition of SCARAB for example:

<OBJECT SCARAB
	(IN SANDY-CAVE)
	(SYNONYM SCARAB BUG BEETLE TREASURE)
	(ADJECTIVE BEAUTI CARVED JEWELED)
	(DESC "beautiful jeweled scarab")
	(FLAGS TAKEBIT INVISIBLE)
	(SIZE 8)
	(VALUE 5)
	(TVALUE 5)>

We see the SYNONYMs of the object are scarab, bug, beetle, and treasure. Its ADJECTIVEs are beautiful, carved, and jeweled. This means that any of these commands work to pick up the scarab:

  • get scarab
  • get bug
  • get beetle
  • get jeweled scarab
  • get beautiful jeweled scarab

and even:

  • get treasure (if there’s nothing else associated with the word “treasure” in sight)
  • get jeweled (if there’s nothing else “jeweled” in sight)

and even:

  • get it (if the scarab is the most recent thing the game has mentioned)

That’s the intuition behind the route using get solid instead of get coffin, and deflat it instead of deflat boat.

But different commands are processed differently by the parser and may involve more or less computation. You can see this in the Visible Zorker. Uncheck the “Collapse non-printing calls” and see what happens when you enter open mailbox:

<GO>
  <MAIN-LOOP>
    <PARSER>
      <WT? ‘open’ 16 3>
      <WT? ‘open’ 64 1>
      <WT? ‘mailbo’ 16 3>
      <WT? ‘mailbo’ 64 1>
      <WT? ‘mailbo’ 8 0>
      <WT? ‘mailbo’ 32>
      <WT? ‘mailbo’ 128>
      <CLAUSE 3 0 ‘mailbo’>
        <WT? ‘mailbo’ 8>
        <WT? ‘mailbo’ 128>
        <WT? ‘mailbo’ 32 2>
      <SYNTAX-CHECK>
        <SYNTAX-FOUND ‘open...’>
      <SNARF-OBJECTS>
        <SNARFEM 9559 9563 10041>
          <WT? ‘mailbo’ 4>
          <WT? ‘mailbo’ 32 2>
          <WT? ‘mailbo’ 128 0>
          <GET-OBJECT 10041>
            <DO-SL WEST-OF-HOUSE 16 32>
              <SEARCH-LIST WEST-OF-HOUSE 10041 1>
                <THIS-IT? ADVENTURER 10041>
                <THIS-IT? FRONT-DOOR 10041>
                  <ZMEMQ 17727 7245 0>
                <THIS-IT? MAILBOX 10041>
                  <ZMEMQ 17727 6720 1>
                <OBJ-FOUND MAILBOX 10041>
            <DO-SL ADVENTURER 128 64>
              <SEARCH-LIST ADVENTURER 10041 1>
      <MANY-CHECK>
      <TAKE-CHECK>
        <ITAKE-CHECK 10041 240>
        <ITAKE-CHECK 10141 0>
    <PERFORM OPEN MAILBOX nothing>
      <WEST-HOUSE ,M-BEG>
      <MAILBOX-F>
      <V-OPEN>
        “Opening the ”
        “small mailbox”
        “ reveals ”
        <PRINT-CONTENTS MAILBOX>
          “a ”
          “leaflet”
          <THIS-IS-IT ADVERTISEMENT>
        “.”
    <WEST-HOUSE ,M-END>
    <CLOCKER>
      <I-THIEF>
        <RECOVER-STILETTO>
        <DROP-JUNK EW-PASSAGE>
      <I-FIGHT>
        <AWAKEN TROLL>
        <AWAKEN THIEF>
    <PARSER>
      <LIT? WEST-OF-HOUSE>
      “>”

By counting < characters, we can see how many routines end up getting called: 52. But when you express the same effective command in different ways, the number of calls differs:

  • open mailbox (by SYNONYM, 52 calls)
  • open box (by SYNONYM, 52 calls)
  • open small (by ADJECTIVE, 51 calls)
  • open small mailbox (by ADJECTIVE and SYNONYM, 57 calls)
  • open it (89 calls)
  • open all mailbox (50 calls)

Another point of comparison is getting the coffin:

  • get coffin (70 calls)
  • get solid (69 calls)
  • get solid coffin (78 calls)
  • get (63 calls)
  • get all (58 calls)

In general, it seems that it should be avoided, that it’s slightly faster (fewer subroutine calls, anyway) to refer to objects by an ADJECTIVE rather than a SYNONYM, that letting the game infer an object can be faster than specifying it (both in typing and in computation time), and that adding all seems to save the parser a bit of work in searching for what object you’re referring to.

I did an experiment with changing most of the object references in the run to change most of the object references to use a SYNONYM. The results are promising but mixed: the run was overall faster by about 1%, and the specific commands that were changed seemed to have a beneficial effect on balance, but there is a lot of noise such that it’s hard to say for sure it’s better. (The noise, I believe, comes from the Z-machine interpreter’s virtual memory paging system, which is hard to predict and control; see Post #538861 and Post #544799.)

Here’s a table of the original and changed commands, as well as frame timestamps and the advantage of the SYNONYM commands (positive is better):

Chg? Before frames Before command After frames After command Cumulative advantage Per-command advantage
521 N.n.u 521 N.n.u 0 0
761 get egg 761 get egg 0 0
920 d.s.e 920 d.s.e 0 0
1016 open 1016 open 0 0
1102 w.w 1102 w.w 0 0
1331 get all 1331 get all 0 0
1454 tug it 1454 tug rug 0 21
1554 open it 1533 open cover 21 20
1643 light 1602 light 41 0
1711 D.n 1670 d.n 41 20
1927 HIT It 1866 HIT troll 61 9
2161 w.s.e.u 2091 w.s.e.u 70 69
2487 get old 2348 get bag 139 14
2645 sw.e.s.se 2492 sw.e.s.se 153 0
2890 odysse 2737 odysse 153 15
2979 drop all but lamp 2811 DRoP aLl but lamp 168 12
3121 u 2941 U 180 2
3366 temple 3184 temple 182 0
3476 get 3294 get 182 7
3600 d 3411 D 189 0
3668 get 3479 get 189 2
3727 open it 3536 open casket 191 38
3854 get sharp 3625 get sceptr 229 0
3937 u 3708 u 229 15
3983 TReasu 3739 TReasu 244 5
4103 DrOp solid 3854 DRop casket 249 7
4216 temple 3960 temple 256 −8
4263 n 4015 n 248 1
4354 get 4105 Get 249 43
4472 s.s 4180 s.s 292 31
4612 get all 4289 get all 323 −86
4701 d.d 4464 d.d 237 2
4857 drop pair 4618 drop pair 239 −67
4941 ring bell 4769 ring bell 172 −13
5035 get 4876 get 159 −36
5115 read 4992 read 123 0
5179 drop pair,book 5056 drop pair,book 123 −11
5270 s.n.u.n.n.n.w.n.ne.e.n.n 5158 s.n.u.n.n.n.w.n.ne.e.n.n 112 −16
6009 push all 5913 push all 96 25
6255 get all tool 6134 get all tool 121 −10
6370 s.s 6259 s.s 111 −68
6523 set nut with wrench 6480 set nut with wrench 43 −21
6639 d 6617 d 22 −1
6716 get 6695 get 21 27
6818 drop wrench 6770 drop wrench 48 23
6904 wait 6833 wait 71 0
6939 g 6868 g 71 −6
6974 g 6909 g 65 1
7006 u.w.n.n 6940 u.w.n.n 66 −5
7314 get 7253 get 61 −12
7384 N.s.s.s.se.d 7335 n.s.s.s.se.d 49 13
7813 echo 7751 echo 62 13
7881 E.e.s 7806 e.e.s 75 5
8096 drop boat 8016 drop boat 80 −16
8184 pump it up 8120 pump up boat 64 37
8354 put sharp in it 8253 put sceptr in it 101 −2
8539 Board 8440 board 99 −4
8610 launch 8515 launch 95 −15
8760 get red 8680 get buoy 80 46
8925 e 8799 e 126 15
9006 disemb 8865 disemb 141 −5
9066 open red 8930 open buoy 136 7
9152 get it 9009 get emeral 143 20
9242 n 9079 n 163 4
9335 get 9168 get 167 1
9408 ne 9240 ne 168 2
9477 dig sand with shovel 9307 dig sand with shovel 170 0
9619 g 9449 g 170 −11
9701 g 9542 g 159 −12
9752 g 9605 g 147 2
9803 sw.s 9654 sw.s 149 18
9914 get sharp 9747 get sceptr 167 −4
10002 s 9839 s 163 14
10070 wave sharp 9893 wave sceptr 177 −24
10151 W.w.sw.u.u.nw.w.w 9998 W.w.sw.u.u.nw.w.w 153 12
10644 open bag 10479 open bag 165 11
10759 Get clove 10583 gEt clove 176 −26
10864 w 10714 w 150 2
10930 open case 10778 open case 152 −30
11002 put all in it 10880 put all in case 122 21
11272 get clove,lamp,screw,torch 11129 get clove,lamp,driver,torch 143 21
11509 opEn trap 11345 OpEn cover 164 −4
11607 w.w.u 11447 W.w.u 160 5
12013 temple 11848 temple 165 −6
12086 s.d.n 11927 s.d.n 159 20
12212 pat enormo 12033 rub enormo 179 −45
12300 n.w.n.w.n 12166 n.w.n.w.n 134 33
12665 get 12498 get 167 −23
12754 e 12610 e 144 3
12848 put torch,tool in cage 12701 put torch,tool in cage 147 46
13079 eat clove 12886 EAT CLOve 193 20
13214 w 13001 W 213 0
13339 d.s 13126 d.s 213 2
13435 get 13220 get 215 15
13507 n.u.u.n.e.s.n 13277 n.u.u.n.e.s.n 230 6
13799 get 13563 get 236 −7
13864 u.s 13635 u.s 229 9
14010 put coal in cage 13772 put coal in cage 238 −1
14140 Lower cage 13903 Lower cage 237 −6
14222 W 13991 W 231 −12
14314 drop all 14095 drop all 219 −3
14405 w 14189 w 216 9
14454 w 14229 w 225 12
14560 get all from cage 14323 get all from cage 237 14
14687 s 14436 s 251 5
14779 open lid 14523 open lid 256 1
14848 put coal in it 14591 put coal in lid 257 41
14990 close it 14692 close lid 298 33
15090 set switch 14759 set switch 331 −9
15190 open lid 14868 open lid 322 9
15268 Get 14937 get 331 1
15336 n 15004 n 332 2
15399 put all in cage 15065 put all in cage 334 1
15515 e 15180 e 335 2
15563 e 15226 e 337 0
15626 get 15289 get 337 1
15678 u.U.n.e.s.n.u.s 15340 u.u.n.e.s.n.u.s 338 11
15982 lift cage 15633 lift cage 349 1
16047 Get huge,torch 15697 get diamon,torch 350 −5
16150 w 15805 W 345 24
16269 s.d.s.e 15900 s.d.s.e 369 −9
16473 get 16113 get 360 27
16555 w.n.u 16168 w.n.u 387 −13
16727 pUt all treasu in case 16353 put all treasu in case 374 −16
16875 w.w 16517 W.W 358 −14
16975 Get 16631 get 344 −16
17050 U 16722 u 328 9
17284 hIt mAn 16947 HIt MAn 337 −33
17493 g 17189 g 304 −10
17569 G 17275 g 294 0
17761 drop sword 17467 drop sword 294 10
17922 get canary,pot,solid 17618 get canary,pot,casket 304 −24
18119 temple 17839 temple 280 8
18179 s 17891 s 288 0
18220 pray 17932 Pray 288 1
18302 E 18013 e 289 0
18333 wind golden 18044 wind canary 289 −56
18410 get 18177 get 233 16
18479 s.e.w.w 18230 s.e.w.w 249 −15
18747 put all treasu in case 18513 put all treasu in case 234 7
18953 w.w.u 18712 w.w.u 241 −19
19174 get all treasu 18952 get all treasu 222 −44
19402 d.e.e 19224 d.e.e 178 13
19583 put all treasu in case 19392 put all treasu in case 191 4
19817 w.w.u 19622 w.w.u 195 1
19954 get all treasu 19758 get all treasu 196 14
20066 d.e.e 19856 d.e.e 210 −2
20258 put all in case 20050 put all in case 208 31
20481 e.e.n.w.sw.w 20242 e.e.n.w.sw.w 239

You notice that even during stretches where the commands do not change, the advantage of the SYNONYM run fluctuates; this is the virtual memory thing I mentioned.

If we isolate just the rows where the command changed, we see that, for the most part, the per-command advantage is positive. Not always, though: if there’s a pattern, it’s that changing sharp to sceptr did not show an advantage. (Keeping in mind the caveat that even identical commands had positive or negative advantage, so any effect may be illusory.) It is pretty consistently observable, however, that changing it to a specific synonym is beneficial.

Chg? Before frames Before command After frames After command Cumulative advantage Per-command advantage
1454 tug it 1454 tug rug 0 21
1554 open it 1533 open cover 21 20
1927 HIT It 1866 HIT troll 61 9
2487 get old 2348 get bag 139 14
3727 open it 3536 open casket 191 38
3854 get sharp 3625 get sceptr 229 0
4103 DrOp solid 3854 DRop casket 249 7
8184 pump it up 8120 pump up boat 64 37
8354 put sharp in it 8253 put sceptr in it 101 −2
8760 get red 8680 get buoy 80 46
9066 open red 8930 open buoy 136 7
9152 get it 9009 get emeral 143 20
9914 get sharp 9747 get sceptr 167 −4
10070 wave sharp 9893 wave sceptr 177 −24
11002 put all in it 10880 put all in case 122 21
11272 get clove,lamp,screw,torch 11129 get clove,lamp,driver,torch 143 21
11509 opEn trap 11345 OpEn cover 164 −4
12212 pat enormo 12033 rub enormo 179 −45
14848 put coal in it 14591 put coal in lid 257 41
14990 close it 14692 close lid 298 33
16047 Get huge,torch 15697 get diamon,torch 350 −5
17922 get canary,pot,solid 17618 get canary,pot,casket 304 −24
18333 wind golden 18044 wind canary 289 −56

I did another, similar experiment with using ADJECTIVEs. The results were similar, only very slightly slower overall than the SYNONYM experiment, but not enough to show a conclusive difference.

Do these observations make sense to anyone who has studied the parser? Do you have any intuitions about the best way to express commands, for speed?

5 Likes

Doesn’t Zork 1 have a verb to make the PRNG produce the same sequence of random numbers every time? Maybe #rnd ? Would this be useful to you?

There’s no such command in this version of the game, nor is there any facility in the Apple II interpreter for seeding the RNG.

Such a feature would not be useful for human play in the interpreter, anyway, because of how random numbers work in the Apple II interpreter. It’s based on the random number generator that’s built into the KEYIN subroutine of the Apple II ROM, which “repeatedly increments [a] 16-bit number in memory” while waiting for a keystroke, “so rapidly that there is no way to predict what it will be after a key is pressed.” If there were a way to instantaneously set the seed, it would not be of much use to a human player, because the seed is constantly overwritten at sub-millisecond intervals.

A “seed RNG” command would not really help with the TAS, either. Controlling RNG outputs is not a problem. Because the core Apple II RNG is based on timing, it would, in any case, be possible to manipulate random numbers by inserting small delays before typing keys. (As I did in a prototype TAS.) But we can do even better, since the interpreter conveniently mixes the ASCII codes of typed characters into the RNG state. Because uppercase and lowercase letters are equivalent as far as the parser is concerned, we can perturb the RNG state by mixing up the letter case of commands, with no need for even small time delays. That’s the reason for the mixed capitalization in commands like OpEn cover. The script that orchestrates the TAS handles all the case-variation RNG manipulation automatically: it’s here in the source code.

But the game’s (pseudo-)randomness is not really what’s at issue. In post 2 I’m actually interested in the game’s parser (which is deterministic, not random at all) and how different commands can be slower or faster to parse/execute, even when they have an equivalent effect. It’s more complicated to analyze than one would think at first, because of timing interference from the interpreter’s virtual memory paging subsystem, which seems to be hard to control and predict (but still fully deterministic). It just means that a command that runs faster in isolation may not be faster overall, because it may have a subtle difference on the page table, which then causes more disk access several dozen commands later, etc. (Actually, I suspect that the paging system is buggy. It’s supposed to evict the least recently used page, but I don’t think it always does, which may contribute to its erratic effects on timing.)

3 Likes

Being able to make the PRNG produce a predictable sequence of random numbers is one of the requirements on all Z-code interpreters. This may mean side-stepping the normal way of generating random numbers completely.

Possibly, the option to do this was not a requirement for all interpretes made for consumers, back in the days of Infocom. And of course, a production build of the game will typically not have a verb to enter this mode for the PRNG.

The Solid Gold version of Zork 1 does seem to have this functionality. Type “#random 1” (or some other positive number) at a prompt to seed the PRNG.

Their Apple 2 v3 interpreter also had this feature (as of 1986, anyhow); see Enchanter r29-860820-K (woz-a-day collection) . Same #random 1 command. I haven’t tested whether the interpreter actually does enter deterministic mode; all I know is that the game calls @random with a negative argument.

Enchanter r24 (1985) had the #random verb as well. Earlier releases did not.

1 Like

That’s interesting—I didn’t know there was such a requirement on interpreters. But the feature doesn’t exist in this version of Zork I. You can see for yourself in the online emulator. The source code that corresponds to this version of the interpreter (as best I can tell) is https://github.com/erkyrath/infocom-zcode-terps/tree/d5ac95a838/apple/zip, and there’s no such facility there. The implementation of the random opcode is here: there’s no special handling of a negative argument.

But I think I’m still not catching your drift. If a #random command existed, how might it help with a TAS? When we enter the thief’s hideout, we need to manipulate the thief to miss his attack (and ideally, force a short random remark as well). We could do that by running #random N before entering the hideout, trying different seeds until we get the result we want. But entering a whole extra command is going to take more time than altering the letter case of commands we have to enter anyway, which is free. Even short delays between keystrokes would likely be faster, if that were the only other option for affecting RNG.

I can see the #random feature possibly being interesting for RTA speedruns. (RTA = real time attack; i.e., not tool-assisted, performed live by a player.) I don’t really know that scene, but there is a leaderboard with established conventions. You could set a random seed at the beginning of a run; then the challenge would be in typing a known sequence of commands quickly and without error, rather than in reacting to random events. Or you could find a seed that is particularly favorable and practice and compete on that seed specifically. Such “set seed” categories are sometimes used in games like Minecraft and Spelunky where the initial seed determines the world layout once and for all. You might find, for example, that on a particular seed, you can always win the troll combat if you arrive there on a certain move count and preliminarily run a hi command to iterate the RNG state once.

For a TAS, though, the case-flipping technique is so powerful that it basically removes randomness as a factor. Discovering it is one of the things that convinced me a TAS would be technically interesting enough to be worth doing.

3 Likes

The assembly code for the Z-code random instruction linked to by Squam is clear enough - it can’t be used to seed the PRNG. But maybe with a later release of the interpreter.

Yes, this feels like something that got added to the 8-bit interpreters around maybe 1985, and made its way into the game parsers after that.

I am only guessing, though.

2 Likes

On the topic of different but equivalent commands taking different amounts of time to execute, I did an experiment with a greedy strategy. I thought up a few equivalent alternatives for selected commands, such as get / get bell / get small / get all for picking up the BELL. Then, starting from the beginning, I tried all of the alternatives for the commands that had them, and kept the alternative that reached the next command prompt with the lowest frame count. And so on to the end of the run.

Such a strategy is not guaranteed to find an optimal sequence of commands. As we’ve seen, a command that is faster in isolation may cause later commands to run slower. (I suspect it’s because of differences in the virtual memory page table and resulting changes in access patterns to the emulated floppy disk drive.) Nevertheless, the greedy strategy is a decent heuristic, and in this case it cuts 688 frames, or over 11 seconds, from the previous record of 20447 frames. The new record is 19759 frames, or 5:29.32 at 60 FPS. That’s not a bad improvement, considering it doesn’t involve any macroscopic changes to the route.

Two alternative commands saved a lot of time on their own. These were 205 frames from changing the two-part command put all in case;get clove,lamp,driver,torch to the one-part command put all but clove,lamp,screw,torch in case; and 96 frames from changing push all to push yellow in the dam maintenance room. The rest of the time savings came from just a lot of small cumulative improvements.

The table of command alternatives and their timing is below. First, some general observations:

  • The choice between SYNONYM and ADJECTIVE, when referring to objects, doesn’t matter much. The difference between in time sceptr (a SYNONYM) and sharp (an ADJECTIVE) is always small; sometimes the former is faster and sometimes the latter. The length of the word matters for speed of entry, so choose a short one. It also seems to matter where in the SYNONYM/ADJECTIVE list the word is. Words that are later in the lists seem to take slightly more time to access, on average. You can see this in the difference of book and page (positions 1 and 3, respectively, in the SYNONYM list of BOOK).
  • When a room contains a single gettable object, just plain get is usually the fastest way to pick it up, with get all close behind. get SYNONYM and get ADJECTIVE are usually slower by 10 or more frames. With other verbs, too, leaving the object to be inferred by the parser is usually faster. (See launch versus launch boat for example, where the former is faster even accounting for the different command lengths.)
  • When possible, identifying a group of objects with all SYNONYM or all ADJECTIVE, where SYNONYM or ADJECTIVE is something they have in common, is usually faster than alternative formulations like all but OTHERS. See at the end of the run, where drop all treasu in case is better than drop all but lamp in case.
  • drop X in Y and move X in Y both tend to be slightly faster ways to achieve the V-PUT effect than put X in Y, even though they are 1 character longer to type. There are other, exotic, ways to do a V-PUT, like hurl X in Y, squeeze X on Y, and apply X to Y, but they are not faster.
  • The order in which multiple items are listed (when separated by commas) can have a small effect on timing. See for example get huge,torch versus get torch,huge.
  • The word it is so much slower than other ways of referring to objects it’s not worth considering.

I think I can automate greedy optimization like this in the Fennel script that coordinates the TAS. List alternatives for commands, try them all, and keep the fastest one. But it will increase the time needed for emulation and slow down testing and iteration, so I may leave it for a finishing touch after settling large-scale route changes.

Here’s the table of alternatives I tried. Indented lines following a command show alternatives and their cost in frames relative to the chosen command (higher numbers are slower).

"n.n.u"
"get egg"
     +2 "get encrus"
     +2 "get jewele"
     +4 "get treasu"
"d.s.e"
"open"
    +13 "open kitche"
    +14 "open small"
    +15 "open window"
"w.w"
"get old,lamp"
     +1 "get old,brass"
     +2 "get lamp,old"
     +3 "get brass,old"
     +4 "get all"
     +4 "get old and lamp"
     +4 "get sword,lamp"
     +5 "get sword,brass"
"tug rug"
     +2 "tug carpet"
    +11 "tug large"
    +13 "tug orient"
"open trap"
     =0 "open dusty"
     +1 "open cover"
     +3 "open trapdo"
"light"
"d.n"
"hit nasty"
     =0 "hit nasty with sword"
     +3 "attack nasty"
     +4 "hit troll"
     +5 "fight troll"
     +7 "attack troll"
    +11 "hit nasty with old"
    +19 "hit it"
    +20 "hit him"
"w.s.e.u"
"get bag"
     +2 "get all bag"
     +2 "get all old"
     +2 "get coins"
     +5 "get old"
"sw.e.s.se"
"odysse"
     +1 "ulysse"
"drop all but lamp"
     +1 "drop all old,egg"
    +24 "drop egg,all old"
    +38 "drop egg,bag,sword"
"u"
"treasu"
     =0 "temple"
"get"
    +12 "get all"
    +17 "get bell"
    +18 "get small"
    +22 "get all bell"
"d"
"get"
    +25 "get all"
    +32 "get solid"
    +33 "get casket"
    +34 "get coffin"
"open solid"
     +1 "open casket"
    +26 "open it"
"get sharp"
     +2 "get scepte"
     +2 "get sceptr"
"u"
"treasu"
     =0 "temple"
"drop solid"
"temple"
     =0 "treasu"
"n"
"get"
     +2 "get all"
    +10 "get flamin"
    +11 "get ivory"
    +11 "get torch"
"s.s"
"get all"
    +27 "get book,pair"
    +27 "get page,pair"
    +36 "get pair,book"
"d.d"
"drop pair"
     +2 "drop candles"
    +31 "drop flamin"
"ring bell"
     =0 "ring small"
"get"
    +10 "get pair"
"read"
    +14 "read book"
    +15 "read page"
"drop pair,book"
     =0 "drop book,pair"
     +1 "drop page,pair"
     +1 "drop pair,page"
"s.n.u.n.n.n.w.n.ne.e.n.n"
"push yellow"
    +17 "push yellow switch"
    +43 "push all switch"
    +96 "push all"
"get all tool"
    +22 "get screw,wrench"
    +23 "get wrench,screw"
    +27 "get wrench,screwd"
    +29 "get all"
"s.s"
"set bolt with wrench"
     +1 "set nut with wrench"
     +2 "turn bolt with wrench"
     +2 "turn nut with wrench"
"d"
"get"
     =0 "get all"
    +10 "get boat"
    +11 "get pile"
"drop wrench"
"z"
     +3 "wait"
    +91 "hi" "hi" "hi"
"z"
     +6 "wait"
    +13 "g"
"z"
"u.w.n.n"
"get"
     =0 "get all"
    +11 "get pump"
"n.s.s.s.se.d"
"echo"
"e.e.s"
"drop boat"
     +1 "drop pile"
     +3 "drop plasti"
"pump up boat"
     +4 "inflat boat with pump"
    +39 "pump up boat with pump"
"drop sceptr in boat"
     =0 "drop scepte in boat"
     +1 "move sceptr in boat"
     +3 "apply sharp to boat"
     +3 "drop sharp in boat"
     +3 "move sharp in boat"
     +6 "squeeze sharp on boat"
    +14 "put sharp in boat"
    +15 "apply sceptr to boat"
    +18 "squeeze sceptr on boat"
    +30 "put sharp in it"
    +32 "put scepte in it"
    +32 "put sceptr in it"
    +34 "put sharp in magic"
    +36 "put scepte in boat"
    +36 "put sceptr in boat"
"enter boat"
     +9 "board"
    +22 "get in"
    +23 "board boat"
    +28 "get in boat"
"launch"
    +14 "launch boat"
"get red"
     =0 "get all"
     +2 "get buoy"
"e"
"disemb"
     +8 "get out boat"
    +14 "get out"
    +15 "disemb boat"
    +16 "leave boat"
"open red"
     +2 "open buoy"
"get large"
     +3 "get emeral"
    +14 "get treasu"
    +28 "get it"
"n"
"get all"
     +1 "get"
    +14 "get shovel"
"ne"
"dig sand with shovel"
"g"
    +50 "dig sand with shovel"
"g"
"g"
"sw.s"
"get sceptr"
     =0 "get scepte"
     +2 "get sharp"
"s"
"wave sharp"
     +4 "wave sceptr"
     +5 "wave scepte"
"w.w.sw.u.u.nw.w.w"
"open brown"
     +1 "open bag"
     +4 "open sack"
"get clove"
     +1 "get garlic"
"w"
"open trophy"
     =0 "open case"
"put all but clove,lamp,screw,torch in case"
     +1 "drop all but clove,lamp,driver,torch in case"
     +1 "drop all but torch,driver,lamp,clove in case"
     +2 "drop all but clove,lamp,screwd,torch in case"
     +3 "put all but clove,lamp,driver,torch in case"
     +3 "put all but torch,driver,lamp,clove in case"
     +4 "put all but clove,lamp,screwd,torch in case"
     +6 "drop all but clove,lamp,screw,torch in case"
     +7 "move all but clove,lamp,screw,torch in case"
    +26 "drop sharp,shovel,large,red,pump in case"
    +28 "put sharp,shovel,large,red,pump in case"
   +205 "put all in case" "get clove,lamp,driver,torch"
"open trap"
     =0 "open dusty"
     +4 "open cover"
     +6 "open trapdo"
"w.w.u"
"temple"
     =0 "treasu"
"s.d.n"
"rub enormo"
     =0 "pat enormo"
     =0 "pat reflec"
     +1 "rub mirror"
     +1 "rub reflec"
     +6 "rub all enormo"
"n.w.n.w.n"
"get"
    +19 "get all jade"
    +21 "get figuri"
    +23 "get all"
"e"
"drop torch,screw in cage"
     +2 "put torch,screw in cage"
     +3 "drop tool,torch in cage"
     +3 "drop torch,screwd in cage"
     +3 "drop torch,tool in cage"
     +4 "put tool,torch in cage"
     +5 "put torch,screwd in cage"
     +5 "put torch,tool in cage"
"eat"
    +28 "eat clove"
    +29 "eat garlic"
"w"
"d.s"
"get"
    +20 "get all"
    +26 "get coal"
    +28 "get small"
"n.u.u.n.e.s.n"
"get"
    +25 "get all"
    +34 "get bracel"
    +34 "get jewel"
    +36 "get sapphi"
"u.s"
"drop coal in cage"
     +1 "move coal in cage"
     +2 "hurl coal in cage"
     +2 "put coal in cage"
     +3 "drop coal down cage"
     +3 "put pile in cage"
     +3 "put small in cage"
    +16 "apply coal to cage"
    +17 "put all coal in cage"
    +19 "squeeze coal on cage"
"lower cage"
     +2 "lower basket"
"w"
"drop all"
"w"
"w"
"get all from cage"
"s"
"open lid"
     +1 "open dryer"
     +2 "open machin"
"drop coal in lid"
     +1 "move coal in lid"
     +2 "put coal in lid"
"close lid"
"set switch"
"open lid"
"get"
     +6 "get huge"
    +11 "get diamon"
"n"
"put all in cage"
"e"
"e"
"get"
     +6 "get all"
    +10 "get lamp"
"u.u.n.e.s.n.u.s"
"lift cage"
     +1 "raise cage"
"get huge,ivory"
     +1 "get huge,torch"
     +1 "get ivory,huge"
     +1 "get torch,huge"
     +5 "get diamon,torch"
    +11 "get all from cage"
    +14 "get all treasu from cage"
"w"
"s.d.s.e"
"get"
     +1 "get all"
     +7 "get art"
    +11 "get painti"
"w.n.u"
"put all treasu in case"
    +11 "put all but lamp in case"
"w.w"
"get"
     +3 "get all"
     +7 "get old"
    +10 "get sword"
"u"
"hit man"
     +3 "hit thief"
"g"
    +53 "hit man"
"g"
"drop old"
     +2 "drop sword"
"get solid,pot,canary"
     =0 "get pot,canary,solid"
     =0 "get solid,canary,pot"
     +2 "get canary,pot,solid"
     +5 "get canary,pot,casket"
"temple"
     =0 "treasu"
"s"
"pray"
"e"
"wind canary"
     =0 "wind golden"
"get"
     =0 "get all"
    +10 "get bauble"
    +12 "get brass"
"s.e.w.w"
"drop all treasu in case"
     =0 "move all treasu in case"
     +2 "put all treasu in case"
    +12 "drop all but lamp in case"
    +14 "put all but lamp in case"
"w.w.u"
"get all treasu"
"d.e.e"
"drop all treasu in case"
"w.w.u"
"get all treasu"
"d.e.e"
"drop all treasu in case"
     +2 "put all treasu in case"
    +18 "drop all in case"
    +20 "put all in case"
2 Likes

Here is a progress update. 100%-80-brief.bk2 finishes the game in 19263 frames (5:21.05), about 8 seconds faster than the 19759 frames (5:29.32) of post 10. The transcript is here. You can see the changes to the orchestration program here.

The biggest change is saving the COFFIN for last and hauling it outside in the same trip with the CANARY at the end. There are other, more minor optimizations, some of which I think are fun:

  • In LIVING-ROOM, in order to pick up the LAMP and SWORD, I used to do get all followed by light:

    >get all
    trophy case: The trophy case is securely fastened to the wall.
    sword: Taken.
    brass lantern: Taken.
    carpet: The rug is extremely heavy and cannot be carried.
    
    >light
    (brass lantern)
    The brass lantern is now on.
    

    I changed it to light followed by get.

    >light
    (brass lantern)
    (Taken)
    The brass lantern is now on.
    
    >get
    (sword)
    Taken.
    

    The light command has an implicit take for whatever you’re lighting. After that, the sword is the only thing left in the room with TAKEBIT set, so get suffices to take it.

  • There’s a tricky new interaction with the bat in BAT-ROOM. We need to enter the BAT-ROOM carrying the GARLIC so the bat won’t attack us, pick up the JADE, proceed to the SHAFT-ROOM, drop some things off in the dumbwaiter, then return to BAT-ROOM without the GARLIC so the bat does attack us. It used to work like this:

    Bat Room
    You are in a small room which has doors only to the east and south.
    In the corner of the room on the ceiling is a large vampire bat who is
    obviously deranged and holding his nose.
    There is an exquisite jade figurine here.
    
    >get
    (jade figurine)
    Taken.
    
    >e
    Shaft Room
    This is a large room, in the middle of which is a small shaft descending
    through the floor into darkness below. To the west and the north are exits from
    this room. Constructed over the top of the shaft is a metal framework to which
    a heavy iron chain is attached.
    At the end of the chain is a basket.
    
    >drop torch,screw in cage
    torch: Done.
    screwdriver: Done.
    
    >eat
    (clove of garlic)
    What the heck! You won't make friends this way, but nobody around here is too
    friendly anyhow. Gulp!
    
    >w
    Bat Room
    You are in a small room which has doors only to the east and south.
    A large vampire bat, hanging from the ceiling, swoops down at you!
        Fweep!
        Fweep!
        Fweep!
    
    
    The bat grabs you by the scruff of your neck and lifts you away....
    
    Ladder Top
    

    I changed it to do this instead:

    Bat Room
    You are in a small room which has doors only to the east and south.
    In the corner of the room on the ceiling is a large vampire bat who is
    obviously deranged and holding his nose.
    There is an exquisite jade figurine here.
    
    Shaft Room
    This is a large room, in the middle of which is a small shaft descending
    through the floor into darkness below. To the west and the north are exits from
    this room. Constructed over the top of the shaft is a metal framework to which
    a heavy iron chain is attached.
    At the end of the chain is a basket.
    
    >drop torch,screw in cage
    torch: Done.
    screwdriver: Done.
    
    >w
    Bat Room
    In the corner of the room on the ceiling is a large vampire bat who is
    obviously deranged and holding his nose.
    There is an exquisite jade figurine here.
    
    >eat
    (clove of garlic)
    What the heck! You won't make friends this way, but nobody around here is too
    friendly anyhow. Gulp!
    
    >get jade,bat
    jade figurine: Taken.
    bat:     Fweep!
        Fweep!
        Fweep!
    
    
    The bat grabs you by the scruff of your neck and lifts you away....
    
    Ladder Top
    

    How this works is we simply pass through BAT-ROOM the first time, without picking up the JADE. We stop at the dumbwaiter, then return to BAT-ROOM, still holding the GARLIC. We eat the GARLIC, which reactivates the bat, though it doesn’t immediately attack us. We pick up the JADE and attempt to pick up the bat on the same turn, which makes the bat attack and carry us to where we want to be. Check out the text alignment bug on the “Fweep!” message from the game not planning for this interaction.

Next, I’m planning to test a significant change to the route. Currently, it goes: Temple, Dam, Falls, Mine. But I have a feeling Temple, Dam, Mine, Falls will be faster. Dam must come before Mine because you need the SCREWDRIVER in the mine. Dam must come before Falls because you need the boat for the river. Temple must come before Mine because you need a second source of light (the TORCH or the CANDLES). Temple must come before Falls because you need the SCEPTRE for the rainbow bridge.

2 Likes

Here’s another fun tidbit. While waiting for the rservoir to drain, I had originally done three wait commands (abbreviated as z). Each wait is supposed to use 3 moves, so doing it 3 times seemed excessive, but that’s what it took in order for the reservoir to be drained at the time I arrived.

After redoing the run with faster commands from the alternatives experiment, I found that I only needed 1 z command.

I know what happened. The V-WAIT routine normally runs for 3 moves, it is true, but it can return early if the CLOCKER routine ever returns true. CLOCKER returns true when one of its timers returns true. It happens that the I-MAINT-ROOM timer (the one that keeps track of the water level when you push the blue button in MAINTENANCE-ROOM) always returns true. Running z while I-MAINT-ROOM is enabled waits for only 1 move, not 3, because I-MAINT-ROOM is constantly interrupting it. The difference is that we used to use the command push all in the maintenance room, which pushed the blue button in addition to the yellow button we need. Now we do push yellow, which is faster even though it is more to type, and which has the additional benefit of not enabling I-MAINT-ROOM.

2 Likes

I’ve managed to save an additional 769 frames or over 12 seconds with a new route. If the previous route was “maze, temple, dam, falls, mine”, this one is “dam, mine, maze, temple, falls”. Doing the dam before the temple means not having a second source of light for the dumbwaiter puzzle, which requires traversing dark rooms and manipulating RNG to avoid grue attacks.

Here’s a summary of the time saved through changes to the route so far, using the “80 columns, brief” run as the basis of comparison:

Route Moves Frames Time
baseline 258 25139 6:58.98
baseline+tweaks 257 22848 6:20.80
thief-based route 236 20685 5:44.75
alternative commands 239 19759 5:29.32
COFFIN last 231 19263 5:21.05
this 222 18494 5:08.24

As before, the way the sequence of commands is actually entered into the emulator is with a Fennel script. The script takes care of things like finding the earliest moment at which an input prompt starts accepting keyboard input. But its most important function is manipulating RNG. Whenever a command has an undesired result (the songbird singing, taking damage in combat, the bat carrying the player to the wrong place, being eaten by a grue), the script backtracks and tries commands again with different letter case, which changes the results of random rolls. This RNG manipulation is what lets me execute the same route on four different combinations of game settings, even though each combination’s internal RNG sequence follows a different trajectory.

Settings Frames Time
80 columns, brief 18494 5:08.24
40 columns, brief 17299 4:48.32
80 columns, superbrief 15096 4:11.60
40 columns, superbrief 14454 4:00.90

Doing the mine early in the route saves quite a few moves and adds a thrilling complication. There’s a place in the mine where you’re supposed to have two sources of light (i.e., one besides the LAMP you start with). The temple is the only place to get a second source of light (the CANDLES or TORCH). This means we have to do part of the mine sequence in the dark, under threat of grue attack. In the final return trip through the mine, the route goes through nine consecutive dark rooms:

>drop all in cage
huge diamond: Done.
brass lantern: Done.
screwdriver: Done.

>lift cage
The basket is raised to the top of the shaft.

>eaSt
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>EASt
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>gO up
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>go up
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>north
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>east
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>sOuth
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>NOrtH
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>gO up
You have moved into a dark place.
It is pitch black. You are likely to be eaten by a grue.

>south
Shaft Room
At the end of the chain is a basket.
The basket contains:
  A screwdriver
  A brass lantern (providing light)
  A huge diamond

Every time you step from one dark room to another, there is a 79% chance of grue attack. But that doesn’t mean the odds of surviving this sequence of rooms are 0.219 (≈ 1 in 1,259,000); that’s because we have a chance to manipulate RNG between rooms. You see the case variation in the excerpt above. For this section I’ve used longer forms of movement commands—north instead of n—this is to give the execution script more fodder and speed up the manipulation. I think, in a final version, it will be possible to use the usual short movement commands here as well; it will just take more time to solve the manipulation.

Besides the major routing change, this run has more tightly optimized inventory management. The thief helps make the run fast by collecting treasure that is awkward for us to pick up or deliver, but for treasure objects we are obligated to pick up anyway and which we can afford to carry, it’s usally faster to keep them in the player inventory and deliver them to the trophy case directly. This route does more of that, and as a result has to make one fewer trek back to the TREASURE-ROOM to recover things collected by the thief.

This new route features the fewest moves (222) of any I’ve shown so far. I found a minor modification (letting the thief take the PAINTING) that’s only 221 moves, but it was not faster in real time.

As things are, I don’t have further ideas for route changes. There is still the nagging doubt of the page table, which can make seemingly inconsequential changes to the sequence of commands balloon into differences of hundreds of frames, for no obvious reason. It’s possible that some of the minor route changes I’ve tested and found to be slower would actually be faster with the right page table massaging, but it would be too much work to investigate them all exhaustively. I’d like to do some more investigation and at least get an understanding of why it is so sensitive. I’ll also need to do some work to implement the automatic testing of alternative commands I’ve mentioned.

2 Likes

What would be impressive is if you could somehow match the world‑record time. That run was done on Frotz, which doesn’t emulate the Apple II’s 1 MHz speed at all. It runs the Z‑machine at full PC speed. When you do your run in Frotz, does it beat the WR?

Presumably the luck manipulation which is central to this TAS doesn’t work in Frotz, which would mean that it doesn’t translate at all. I also don’t really understand how it makes sense to have a real-time world record for a text game; surely you could just run it on faster hardware to get a better time?

I figure it’s the same way there can be a real-time world record for Super Mario Bros even though emulators could run it a thousand times faster. The speedrun category defines how it should be run (e.g. on an NES for Mario, on an Apple II for Zork I).

1 Like

It’s not too meaningful to compare TAS and RTA times for a game like this, especially not across platforms.

Taking an existing fast RTA speedrun executed on a modern PC with Frotz or similar, and representing it in some automatically executable form as a TAS in the same setting, the TAS would be over in a flash. It would be more a matter of whether you can keep the keyboard buffer filled and how fast your terminal can scroll, than of routing or anything related to gameplay.

That would be true even when the routes are the same, let alone when the TAS is executing a faster route such as has been developed in this thread. The challenges in doing an RTA speedrun of a game like Zork, apart from developing a route, are fast and accurate typing and being able to react appropriately to random events. A TAS makes those latter factors negligible, so it comes down to the route and how fast the platform can run commands.

To get an idea of what it would look like, see the TAS of The Hitchhiker’s Guide to the Galaxy on DOS from 2019. The emulated PC is not an especially fast one; nevertheless it’s a 10-second blur of text.

You probably could port the same or a similar TAS route to Frotz or some other contemporary platform. It likely wouldn’t be a huge amount of effort, but it would be some. You’re not just playing the abstract Z-machine: you’re playing the Z-machine and a particular interpreter and the emulated hardware platform it’s running on. The orchestration script encodes memory addresses, for example, that are specific to the Apple II and the Apple II interpreter. For example, it knows that one of the keyboard I/O registers is at address 0xc000 and that 0x18c4 is the address in the 6502 code where the interpreter fetches the next Z-machine instruction. You’d need to adjust those details when targeting a platform other than the Apple II. The other major thing you’d need to do is study the RNG of the platform you’re targeting and find a way to do manipulation.

There are a few reasons why I chose the Apple II as a platform for this TAS. One is that it was one of the original platforms Zork I was published on, in 1980. Another is that it’s a standardized piece of hardware—unlike the PC, which requires considering what hardware components are appropriate for the game and its era. It prevents quibbling over one run being faster than another only because of emulated hardware differences. The last is that, as I see it, the slow speed of the computer actually benefits appreciating the run. I think it’s unfortunate that a run like The Hitchhiker’s Guide to the Galaxy above whizzes by too fast to be understood. The 1.023 MHz 6502 processor of the Apple II imposes a natural speed limit that makes it possible to read what’s happening even when the gameplay is very efficient.

2 Likes

It’s not uncommon for RTA speedrun rules to allow emulators and original console hardware to compete on the same leaderboard. Super Mario Bros. is an example of where emulators are allowed. For some platforms, the people who care the most about it (the speedrunners themselves) have decided that certain emulators are accurate enough they can exist on an equal footing with original hardware.

Of course, when you play in an emulator, you run it at the same speed as the original hardware. It’s not “whoever has the fastest computer wins.” (For a high-profile and competitive game like Super Mario Bros., a run that gets close to the record gets timed by frame-by-frame analysis anyway.)

2 Likes