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

forked from trevato/cs · proposes

A match is seeded by the wall clock: make it reproducible from its id #183

Merged opened this

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

Merged

`main.ts` now derives one seed from the match id — `const seed = seedFrom(matchId)` — and hands it to both `generateLevel` and `createWorld`, so the id the leaderboard already stores is the whole of a match: the map, which spawn each body takes, which terrorist carries the bomb, what the bots buy and where every shot lands inside its cone. `createWorld`'s `(Date.now() & 0xffff) >>> 0` default is g…

shipped production is serving it deploy ae07c2d

opened this

main.ts now derives one seed from the match id — const seed = seedFrom(matchId) — and hands it to both generateLevel and createWorld, so the id the leaderboard already stores is the whole of a match: the map, which spawn each body takes, which terrorist carries the bomb, what the bots buy and where every shot lands inside its cone. createWorld's (Date.now() & 0xffff) >>> 0 default is gone and the seed is required; the defaults on playerTeam and teamSize went with it, because a required parameter cannot follow an optional one and all 30-odd call sites already passed both. world.ts's doc comment says who passes what.

The new test in src/client/main.test.ts is the thing to look at: it boots the page on a fixed id, and compares the world the page started against createWorld(DE_FORGE, SHORT, "you", "ct", 5, seedFrom(id)) — bot roster, bomb carrier, and every number replay.ts's snapshot hashes — then plays two seconds of frames from one id twice and from an id one character different once. It runs inside withoutEntropy, so the whole boot-to-match path is proven to take no number from the platform: a frame that reached for Math.random, Date.now or performance.now throws instead of flickering. Reverting only the createWorld seed argument fails it on the roster.

Which of main.test.ts's assertions were seed-sensitive: the spectating test, "dying moves the camera behind a teammate, and fire moves it behind the next" — the two camera positions and the mates.some(p => p.pos[0] === watched[0] …) identities, which are true only when follow cut rather than eased, and that depends on where the seed put the bodies. I measured the condition that broke #101's pull: 90 of 1,000 seeds put the first two watched teammates within CUT_DISTANCE of each other. The bug under it is fixed, so nothing fails today — I swept the file over seeds 0–63 and the spectating test over 64–320, all green — but those assertions were a sample of the seed space on each check and are now one fixed world. Nothing else in the file reads rng-derived state.

What I chose not to do: no UI or route for entering an id to replay (#173), no match-of-the-day (#174), and no input recording (#93) — this is only the seeding those need. The map generator and the world's rng are two rng streams off the same number rather than two derived numbers; they draw on different things, and keeping the derivation as plain as seedFrom(matchId) is what a server replaying a result will want. README gained a bullet, and static/app.4b18a0517b.js is the rebuilt bundle (bun run build). bun test is 804 pass / 0 fail, run five times over.

🤖 Generated with Claude Code

Closes #170

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs