Public: anyone can read this; the crew can change it.

forked from trevato/cs · proposes · 4 behind

A seeded round is pinned: a golden digest proves an engine extraction changed nothing #22

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`src/client/replay.ts` turns "the game plays exactly as before" into a number. It holds the run and nothing else: a fixed script of beats (`{ atTick, input, buy? }` — a buy in the freeze that is refused at 1000 and taken at 650, walking, strafing, a jump, three-shot bursts from a semi-automatic pistol, a reload, a `use` and a crouch), the list of what is hashed as data rather than prose (`PLAYER_F…

shipped production is serving it deploy ae07c2d

opened this

src/client/replay.ts turns "the game plays exactly as before" into a number. It holds the run and nothing else: a fixed script of beats ({ atTick, input, buy? } — a buy in the freeze that is refused at 1000 and taken at 650, walking, strafing, a jump, three-shot bursts from a semi-automatic pistol, a reload, a use and a crouch), the list of what is hashed as data rather than prose (PLAYER_FIELDS, MATCH_FIELDS: position, velocity, yaw, pitch, health, armour, duck amount and magazine per player, plus phase, timer and score), and a fold that hashes the state after every tick into one sha256 with a 48-bit checkpoint every second. Effects, the feed and the messages are not hashed — they are presentation and would make the digest fragile for nothing.

Floats are quantised to 1e-4 and written as integers. The reason is in the comment, not just the what: the simulation runs on Math.hypot, atan2, sin and exp, which are library functions whose last bit the standard does not pin, so hashing raw doubles would fail on someone else's CPU for a reason that is not a bug; a ten-thousandth of a unit is 2.5 microns of map, and a real behaviour change moves whole units within a tick or two.

src/client/replay.test.ts pins two runs, both computed on main at 06a3c81 (this branch's tip is that commit, and nothing here touches the simulation): forty seconds of createWorld(SHORT, "me", "ct", 5, 7) — the person buys armour, walks out, fires, reloads and is killed at second 38.9 while nine bots run the policy around them — and forty seconds of duels through src/rl/arena.ts, stepped by the same script with the arena's own stepInput and reset on each episode end, so reset's own draws are pinned too. Both run inside withoutEntropy, which replaces Math.random, Date.now and performance.now with functions that throw and puts them back in a finally; nothing in either run reached for them, so there was no non-determinism to fix. compare(run, pin) is the other half of the work: worth looking at the shape of its message, which names the first second that differs (second 21 of 40 (ticks 1280..1343): pinned …, ran …), how many seconds went with it, and prints both constants ready to paste — I checked it by deliberately breaking the script and reading what came out.

Measured on this machine: the two pinned runs together are 0.52 s (round 0.46 s, duel 0.06 s) and the whole test file is 0.66 s of the suite's 3.3 s. bun test is green at 184 passing (176 before this pull; the issue's 166 predates two merges). The README gains a src/client/replay.ts bullet with the rule: an extraction that moves a digest has changed behaviour and is not an extraction, while a deliberate change updates the digest in the same pull and says what moved.

What I did not do: the ammo reserve is not hashed, because the issue's list says magazine — one consequence is visible in the pinned seconds, where a run that fires more shots re-converges after the reload refills the magazine. No behaviour changed, no bundle was rebuilt, and src/client still does not import src/rl — the arena is pinned by handing its stepper to the exported fold from the test.

🤖 Generated with Claude Code

Closes #19

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs