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
Check · Review · Land · Live done — Landed and live in production.
`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
Sign in to join this conversation.
new above
offline
main.tsnow derives one seed from the match id —const seed = seedFrom(matchId)— and hands it to bothgenerateLevelandcreateWorld, 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) >>> 0default is gone and the seed is required; the defaults onplayerTeamandteamSizewent 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.tsis the thing to look at: it boots the page on a fixed id, and compares the world the page started againstcreateWorld(DE_FORGE, SHORT, "you", "ct", 5, seedFrom(id))— bot roster, bomb carrier, and every numberreplay.ts'ssnapshothashes — then plays two seconds of frames from one id twice and from an id one character different once. It runs insidewithoutEntropy, so the whole boot-to-match path is proven to take no number from the platform: a frame that reached forMath.random,Date.noworperformance.nowthrows instead of flickering. Reverting only thecreateWorldseed 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 themates.some(p => p.pos[0] === watched[0] …)identities, which are true only whenfollowcut 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 withinCUT_DISTANCEof 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
rngstreams off the same number rather than two derived numbers; they draw on different things, and keeping the derivation as plain asseedFrom(matchId)is what a server replaying a result will want. README gained a bullet, andstatic/app.4b18a0517b.jsis the rebuilt bundle (bun run build).bun testis 804 pass / 0 fail, run five times over.🤖 Generated with Claude Code
Closes #170