Rockford a study of the 1988 game

How the simulation works

These rules are read from the disassembly of the VGA executable, not inferred from play, and each address is the instruction a statement came from.

The C# rebuild implements them with the same citations. The last part lists what is still open.

The model

All of this comes from the dispatch loop at 0x140DE and the loader at 0x108C0.

  • A runtime cell is two bytes: a value byte and a facing or sub-state byte.
  • Horizontal stride 2, vertical stride 0x100, so a row holds 128 cells: the leftmost 40 are the cave, and a parallel plane sits at +0x80.
  • The scan runs top to bottom, left to right, 22 rows of 40.
  • Values below 4 are inert and skip dispatch. Every other value calls a handler from the table at 0x13C50, indexed by 2 * value.
  • A step waits for at least 6 ticks (0x1334F) of an interrupt that runs at 60 Hz, from PIT divisor 0x4DAE, so the simulation runs at 10 Hz.

Every "random" behaviour is drawn inside a step, so a frequency quoted per second means little without the step rate.

The eleven rules that were wrong

The first engine here was written from the cave data and the game's help text. An independent audit on 15 September 2026 found that all eleven of its asserted rules diverged from the executable.

The worst was the simplest. Authored 0x20 is the player: the loader at 0x1093A gives it a two-cell sprite and stores its position in two globals. Authored 0x74 becomes the quota-gated exit at 0x14316. The first engine had them swapped, so Rockford began every cave standing on the exit.

The rest were subtler, and taught more:

  • Facing was taken to be the low two bits of every authored value. The loader translates each value explicitly, and facing lives in the second runtime byte.
  • Enemies were given three movement attempts. The original makes two and then turns on the spot.
  • Fire was allowed to push in four directions. The original pushes only horizontally.

A rule set wrong in eleven places describes a different game, so that engine was retired rather than patched.

The lesson that generalises. Every wrong rule came from reasoning about what the level design implied, and every correction from reading the code. Inference from data quickly gives an answer that hangs together, which is no evidence that it is right.

The dispatch table

Most of what follows came from reading the whole table rather than sampling it. The version here is abridged.

Runtime values and what handles them
RuntimeHandlerWhat it is
00-03noneInert: space, dirt, steel, ordinary wall
070x14316Closed exit, opens on the quota
08-0B0x14445Activatable downward-producing entity: the acid tap
0C-0F0x140AEActivatable upward-producing entity: the fire tap
11-14, 1B-1E0x1467C, then 0x14126 or 0x14130Explosion phases, to treasure and to space
15, 160x14632, 0x14677Expanding wall, and its deferred form
30-330x14301Open exit
40-430x1414A, 0x14223, 0x142F7Treasure: resting, falling, deferred
44-470x13EF7, 0x13FD0, 0x140A4Boulder: resting, falling, deferred
48-4B0x13E50Amoeba
74-770x1467FThe timed converting wall
78-7B0x13ED8The first worm's egg
7C-7F0x14613The second worm's egg
8C0x11B54Enemy family, clockwise
900x11B91Enemy family, anticlockwise
A0-A70x13E78The first worm's body
B0-B70x145B3The second worm's body
F0-F4five, belowDeferred seeds

State chains, and why the game has projectiles

The handler at 0x1467C is a bare inc on the cell: a value in one of its runs advances a step per scan visit until a terminal handler replaces it. That is the whole animation system, and how things transform.

The chains, with what each becomes
ChainTerminalBecomes
11 12 1314 at 0x14126Treasure
17 18 191A at 0x1412BTreasure
1B 1C 1D1E at 0x14130Space
1F 20 21 2223 at 0x14322Falling acid: falls on, or blows up a creature below it
28 29 2A2B at 0x1434ESettled acid: moves itself down one cell as 1F
2C 2D 2E2F at 0x14473Fire: moves itself up one cell as 2C
34 35 3637 at 0x14135Boulder
38 39 3A3B at 0x1413ACalls 0x1356C, which writes 90, the anticlockwise enemy
3C 3D 3E3F at 0x14140Boulder
6C 6D 6E6F at 0x14441Back to 6C: an endless four-frame loop
70 71 7273 at 0x14145Back to 70: another endless loop

The two loops are the bonus collectibles: authored 78, the treasure bonus, loads as 6C, and authored 7C, the time bonus, as 70.

The acid and fire rows are projectiles, which the first engine lacked. Acid falls a cell at a time and fire rises, each re-entering its chain after the move. Both come from taps, the acid tap and the fire tap (the prior art's magic lamp), and an armed tap emits one every eight steps.

Fire and acid destroy each other: 0x1448A tests the cell above the fire against 20-2B, the acid's whole state range, and throws a blast. Fire striking treasure turns it into a creature: 0x1355E writes 8C, the clockwise family, which yields treasure when killed.

Five values that exist only because of the scan order

This is the most satisfying thing in the dispatch table. The handlers for F0 to F4 do exactly one thing: replace the value with another. They make no motion, sound or side effect, and play gives no hint of why they are there.

The deferred seeds
SeedHandlerBecomes
F00x1443D28, acid settled
F10x145AF2C, fire head
F20x146D025, acid blocked twice
F30x13ED4A7, the first worm's oldest segment
F40x1460FB7, the second worm's oldest segment

The scan order imposes a rule on every handler in the game: a handler writing into a cell the scan has not reached yet must write something inert. Fire moving left writes the live value, because the scan has already passed that cell this step. Fire moving right writes the seed, because a live value there would advance again in the same step and the projectile would lose a frame.

The effect is visible from outside, which matters for tests: a projectile created downwards or rightwards is one phase further on by the end of the step that created it, because the scan catches up with it.

The ranged action

The help text says fire is used to "pick up treasures, push things over, and blow up dirt". In the code it acts on the adjacent cell in the facing direction without moving the player, and pushes only horizontally. Firing at a tap arms it. Walking into a worm's egg or firing at it sets that worm's counter to 0x4B0, 1200 ticks, so it hatches on the next scan; left alone, an egg hatches after twenty seconds anyway.

The worms

This page once listed B0-B7 as an unsettled "segmented moving entity"; it is the second worm's body. Both worms were read in full and ported on 17 September 2026, and the prior art's account of them proved right in every particular.

A routine, not a cell

The scan never moves a worm. The main loop calls a routine for each one once a step, at 0xEDBC and 0xEDBF, ahead of the scan, and the cells the scan sees are the trail already laid. 0x1194B and 0x11A68 are the same routine twice. Each keeps its own direction, length, head cell, tail cell and two flags in globals, and both use the enemies' delta table at ds:0x1656.

Hatching

Each worm starts as an egg. Runtime 78-7B watches ds:0x16AA and becomes A0; runtime 7C-7F watches ds:0x16AC and becomes B0. Hatching, at 0x13EE4, sets the length to one. The loader at 0x109FB seeded the head much earlier with the egg's cell.

The body

A0 is the newest segment and A7 the oldest, and B0 to B7 work the same way. The handler at 0x13E78 ages every segment by one and erases any that pass A7, except the tail pointer's cell. That one stays pinned at A7 until the movement routine erases it as the ninth, which is why a worm's drawn length equals its counted one.

A worm boxed in with a body behind it reverses (0x119F4): head and tail swap, and it retraces the cells it laid. Boxed in with no body, it freezes (0x13EB1), which stops a one-cell worm ageing itself out of existence.

What they change

The two worms are opposites: one spoils a cave and the other mends it. Neither writes the thing it is making. Each writes the first value of one of the chains above, so the change takes four more scans and can be seen happening.

What each worm does to what it meets
WormTreasureBoulderEnemy 90
Evil, authored 70, body A0-A73C, then boulder38, then enemy 90, one time in eight-
Good, authored 84, body B0-B7-17, then treasure, every time34, then boulder

Anything in a worm's way costs one number from the shared random generator, to choose which way to turn, and one more if both sides are blocked; clear space costs none. The evil worm also rolls its one in eight for every boulder it meets. The good worm converts boulders without that roll.

The two headings, ds:0x1610 and ds:0x1618, are not cleared between caves, so a worm hatches facing wherever the last worm faced, possibly several caves earlier. The rebuild carries them across too.

When a cave ends

The end of a cave is decided outside the scan, and this part was read from the EGA executable. The game plays one of three cartoons:

  • WIN: Rockford entered the open exit with the quota met, and the cave was not ended by a key.
  • LOSE: he died with time on the clock.
  • TIME: he died with the clock at zero or below.

Running out of time kills him as a hazard does, so only the clock tells LOSE from TIME. Each frame shows for 9 ticks (150 ms), and a cartoon runs for 1 to 6 seconds.

The VGA executable sets the same flags in the same places, but its cartoon routine has not been read. The formats page has the timings and where the rebuild differs. The rebuild's outcome screen makes the same split, with "cave clear" for a completed cave and "out of time" for a death on the clock.

What is not settled

  • Effective random frequencies during play, because input and sound draw from the same generator.
  • Behaviour under interrupt delay, pause, and 16-bit timer wraparound.
  • Whether the EGA and VGA releases simulate identically. They are known to differ outside the simulation: the VGA debug keyboard has nine keys and a cheat flag, the EGA one three keys and no cheat.

Run against the original, but not in play

All of it matches the disassembly as read. The cave loader and the main loop's step have since been run beside the original's own code in an interpreter, fifty passes a cave in all forty caves, with nobody at the controls. The loader agrees on every cell; the step agrees for most caves, and the small residual is traced to state the harness cannot set up (details). Real play, with input, has not been compared, so a misreading that only input reaches would not have been caught.

A second check would be cicodis, a static recompiler someone has already put the game through. It was examined on 20 September 2026 but not built or run. It translates the EGA executable, byte for byte bar twelve bytes of inert stack padding, while every address here is VGA, and the two releases were built separately, so they cannot be lined up by address. Its translation is also wrong in at least one place: its patched treasure routine zeroes the quota on the first gem, where the original's JNS at 0x1FAF3 clamps only on a negative.

A cicodis build that dumps its 40 by 22 grid once a step could still be compared with this engine cell by cell, from the same recorded input, with no need to agree on memory layout. One difference is known in advance: the rebuild seeds every cave's generator with 0x04D2, where the original carries one stream from cave to cave.