Skip to content

Speedrun: calculate a sheet's time headless — node runner and a Calculate button (#509) - #511

Merged
dhobi merged 6 commits into
masterfrom
speedrun-headless-509
Oct 6, 2026
Merged

dhobi merged 6 commits into
masterfrom
speedrun-headless-509

Conversation

@dhobi

@dhobi dhobi commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Closes #509.

What it does

  • npm run speedrun -w taoot -- --headless plays a sheet in node, with no browser and no drawing. It uses the workbench's own driver over a stand-in window, so every gesture is the same code.
  • Calculate on the workbench plays the open sheet the same way in a Web Worker, with a progress bar. It ends with the in-game time and frames, or with the line it stopped at, how far in, and a go to line N button. The game on screen is not touched.

Same numbers as the browser

From a cold boot with seed 20, all 361 lines up to the sheet's then-failure point gave the same in-game time, to the millisecond, in node and in Playwright (2:45.4 in both runners). The smokestack → end leg matches pass for pass (1:25.1). Now the full sheet: 6:30.4 in-game in 40.6 s headless. Calculate takes about 52 s in the worker.

To get there, these engine and runner rules changed (they hold for every runner, not just headless):

  • A standing watch (watchFor) is checked by the session on every pass and stops the game on the pass it comes true. Only the watch's own action moves the game until it is done. Before, the ESC landed some passes late, and how many depended on the machine.
  • The runner does not look at the game while it waits on a file. A pause isn't over and a wait doesn't close until the load lands.
  • The watchdog's quarter-second poll no longer holds up the next line.
  • Budgets count only frames in which the game could run (headless and worker).

Fixes found on the way

  • say: a film after the puppet has closed belongs to the room. The purser leaving opens his office (maino1.mov) on the same pass, and say pressed ESC at it, which left Frank in the corridor. It came in with Speedrun clock: in-game time, the same on any machine and any connection (#508) #510. With it fixed, the full sheet runs to the end again.
  • aim searches only where the thing can be hit. hammer(startfence) swept the whole screen 743 times per fencing bout (39 s of CPU); now 2.6 s.
  • The save verb says why the engine refused a save.

Known

  • One browser run in three is one pass longer on two lines of the smokestack leg; headless never varies.
  • Calculate's dice are live, as on the workbench.

Tests

Session tests for the watch halt and the load rule, the say room-film and inline-clip cases, and a node suite that plays the sheet's opening twice and needs identical numbers. The full suite passes apart from the known 30 s timeouts (container-refs puppets, sets.ts).

🤖 Generated with Claude Code

dhobi and others added 5 commits October 5, 2026 21:15
…rowser (#509)

`npm run speedrun -w taoot -- --headless` plays a sheet in node with the
workbench's own driver over a stand-in window. From a cold boot, seed 20,
all 361 lines of the sheet came out the same in-game time in node and in
the browser (2:45 in 17 s against 3:36).

What it took, beyond the runner:
- a standing watch is asked by the session on every pass and halts the
  game on the pass it rises; only the watch's action moves it until done
- the watchdog's quarter-second tick no longer holds up the next line
- the runner does not look at the game while it waits on a file
- headless frames give each callback its own task, as a browser does
- budgets count the headless run's own frames, not the wall

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Calculate button plays the open sheet from a cold boot in a worker,
headless and at full CPU speed, with a progress bar, and ends in either
the sheet's in-game time and frames or the line it stopped at, how far
in, and a button that puts the editor on that line.

- the headless driver moves into the engine (speedrun/headless.ts), shared
  by the node runner and the worker; Titanic's input routing into src
- budgets count only frames in which the game could run: not while a file
  loads, not while a standing watch has halted it
- the save verb says why the engine refused a save

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The purser leaving opens maino1.mov, his office with the cargo manifest,
on the very pass he goes. On a sheet's clock (#508) `say` saw that film
before it saw the conversation end, took it between parked frames (no
regions) for an inline clip, and pressed ESC at it: the office shut and
Frank stood in the corridor. A conversation is now over once its puppet
has closed, whatever is on screen; a film over a puppet still open is
still skipped as an inline clip.

With this the full sheet plays to the end again (seed 20: 6:30.4
in-game, headless).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`hammer(startfence)` asks `aim` every pass whether the fencing bout's
start button is back, and an answer of "not there" was a sweep of the
whole screen: twelve thousand hit tests, about 50 ms, 743 times in one
bout, 39 s of CPU against 41 s of game. Over a DreamFactory 4 flat with
the room away a hit is one of the flat's buttons or a drawn prop, so a
name no drawn prop carries is searched for only inside its button's
rectangle (`flatWhere`, shared by both drivers).

The fencing split: 40.9 s in-game unchanged, 41.7 s -> 2.6 s headless;
the full sheet 1:10 -> 40.6 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#509)

The driver calls `win.setTimeout(…)`, and a browser's (a worker's) own
setTimeout refuses a `this` that is not its global: "Illegal invocation"
at the first standing watch's poll, line 867 of the full sheet. Node
does not check, so headless runs never showed it. The full sheet now
calculates to the end in the worker (6:31.5 in-game, unseeded, 52 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 6, 2026

Copy link
Copy Markdown

@dhobi
dhobi merged commit ba987d9 into master Oct 6, 2026
4 checks passed
@dhobi
dhobi deleted the speedrun-headless-509 branch October 6, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Speedrun: calculate a sheet's frames headlessly — node first, then a worker on the workbench

1 participant