Cave Story TAS · the long version

How it works

The run, the twin of the game it was searched in, the searches, and the ideas that didn't work. Frame numbers are 50 fps "logic frames" unless noted.

The rules of the game

A bit-exact twin of the game

Searching means replaying the game millions of times, so it can't be the original exe. The work engine is CSE2, a community decompilation of Cave Story, built as the Japanese version with a TAS patch: headless replay, savestates, per-frame traces and a server mode the search tools drive.

The ground truth is the original Doukutsu.exe running under Wine. A proxy dinput.dll feeds it the movie's inputs and logs a per-frame trace: game flags, stage, position, speed, HP, script state and the RNG seed. A diff tool compares the two traces frame by frame. nitsuja's full movie matches on all 149,311 frames, and every record since has been banked only after it matched on the exe too ("real exe IDENTICAL over 148,713 frames" for the current one).

Getting there meant finding the original's quirks. One: the exe's sound initialisation consumes a rand() call, so the harness needs an audio device, even a null one, or the RNG starts one draw off. Another: Hourglass's clock ran 100 ms ahead after the first music change. That's why nitsuja's title-screen press sits 5 entries earlier in his file than on real hardware, which the converter has to reproduce.

The game on a GPU

Shortcuts that keep the world identical

The first 60 frames came from the cautious version of the idea. Cut nitsuja's run into short windows (≤ 240 frames) where no script is running and Quote is in control. Search on the GPU for inputs that reach his exact state at the end of the window sooner. Then keep only variants that leave everything the same at the next door. Two hidden counters survive doors and had to be put back:

The enemy-aware version ("esc") runs an evolution strategy over edited input prefixes in the full GPU world. It scores each candidate at its room exit on frames saved, RNG offset and every word of carried state. Most of the run's small gains came from esc and from re-sweeps of rooms a new record had changed.

Luck engineering

Cave Story uses one RNG, the C library's rand(). Rooms draw from it constantly, so almost any frame saved shifts every later random event. The insight that unlocked most of the run: at a door, the game reloads the room's enemies, so the world is just Quote + carried state + the RNG seed. Land on the same seed with good-enough state, and the reference run's inputs replay from there unchanged.

The periwinkle splice

periwinkle's 2022 WIP takes a Pignon hit in the Graveyard and rides the knockback up the shaft, 261 frames faster than nitsuja. Attaching it to the rest of the game was the hardest problem in the project. Under the new line's luck (532 draws ahead after the Graveyard), the Grasstown kills leave the Missile Launcher at level 2, and a level-2 Balfrog fight costs about 700 frames. A level-3 launcher from extra EXP orbs costs about 845.

The fix wasn't to fight the luck but to re-synchronise it. A GPU luck search re-solved Grasstown up to Santa's house, with a health-refill detour there tuned so that the RNG at the next door equalled the reference's exactly. From there the reference's inputs ran to the end: record-217, a 217-frame jump in one commit. Later, a search found a line that picks up one more heart on the way and skips the refill: record-357, +53. The run now walks into Santa's House with exactly the reference's RNG.

Bosses

Boss fights are about 8,000 of the run's frames. Hitboxes, invulnerability windows, damage caps and phase timers make them search problems of their own:

The Core air timer

After the Core boss, Quote drowns as part of the story, exactly 1,000 frames after his last dive plus 972 frames of frozen script. Diving earlier ends the room earlier, if the boss is dead in time. The water level is an oscillator that never looks at Quote: it accelerates 4/512 px per frame² toward its rest line, giving a ±16 px swing with a 256-frame period. Modelled exactly, it decides where Quote has to stand to stay underwater on each frame. Two constraints shaped the solution. The fight's missile drops must leave at least 8 missiles, or the late game, as far ahead as the Undead Core, plays out differently. And the boss's damage windows each close after 200 damage. The two tide lines banked 24 frames (the sub-50 record) and 16 more.

Keeping the machine running

What didn't work

Most of the project was ideas that didn't pan out. Each one is written up in the repo with its numbers:

How close to optimal?

Lessons, briefly