Pushed. The last step is the pull description itself, which needs nothing further from the tree.
Snapshots now go out every tick to every seat the room can hold. The old rule halved the rate at the third seat, and since the client's render delay follows the rate it measures, that seat drew every other body for everyone 42ms further in the past. The honest answer to the issue's question is that for this game the cadence is every tick always: the threshold now sits at ten seats, which is the whole room, and the comment on broadcastEvery in src/arena.ts carries the numbers so nobody has to re-derive them.
What to feel in the first ten seconds. Open the preview in three tabs from three addresses (or have two friends join), press F3 in one, and watch the readout: it stays at 30.0 Hz in · 45 ms behind when the third seat fills instead of falling to 15.0 Hz in · 87 ms behind. Peek a corner at a running bot: it is where the server has it 45ms ago, not 87.
Feel numbers, from the new harness in src/feel.test.ts (a real arena with N idle seats, the viewer's real snapshots over the same jittery wire as watchBots, warmed through the buy phase, the first three seconds of the rush measured):
room
cadence
snapshots in 3s
delay in force
per-frame step spread
restarts
frames guessed past the newest sample while the delay settled
4
before (every other tick)
45
86.4ms
9.5%
0
18
4
after (every tick)
91
45.0ms
1.9%
0
0
8
before
45
86.4ms
17.2%
0
18
8
after
91
45.0ms
16.3%
0
0
So other players come 41ms closer to the present with no new jitter: steps in the room of four go from 9.5% to 1.9% spread, and in the room of eight they are no worse. The 16% at eight is the same in both columns and is the server's own motion: with eight idle humans only two bots move, and the one measured threads past standing bodies at spawn. One bonus the table shows: a timeline meeting a 15Hz stream spends two seconds drawing bodies past the newest sample while its delay creeps from 45ms to 87ms, which is what a room felt at the moment its third seat filled. That transient is gone because the rate no longer changes.
Cost, measured on my machine, not the box (6-vCPU EPYC 9555P, bun 1.3; medians of seven 20-second runs of a seeded match, a never-sending run subtracted to isolate the snapshots). I could not reach the production box from the sandbox.
seats
whole loop, every tick
of which snapshots
bytes out, live round
bytes out, whole match
4
1.3ms per second
0.8ms
210 kB/s
340 kB/s
8
2.0ms per second
1.7ms
385 kB/s
645 kB/s
10
2.1ms per second
2.0ms
465 kB/s
790 kB/s
One snapshot is 6µs to build with the vision filter, one world tick 27µs. A full room is 0.2% of one core here; a box five times slower is at 1%. Bandwidth is the larger number: 6 Mbit/s out at ten seats, 80 kB/s to each seat over a whole match, since a buy-phase snapshot is larger than the 1.6 KiB live one.
What changed and why.src/engine/arena.ts gained one optional cadence on the game contract, defaulting to what it had; no val number went into the engine, and the engine's own test covers the hook. The val game is now a factory (valGame, makeValArena) so the harness can seat a room of its own without touching the shared match; the singleton exports are unchanged. src/arena.test.ts had two tests that pinned the old threshold and one comment quoting a 3.4 KiB snapshot from before the sticky board; those were the tests being wrong about the rule, and they now assert the new one. README updated to match. No client change was needed: interp.js follows the rate it measures, as the issue expected.
One unrelated note: a client test about the drawn body's speed failed once in a baseline run before I changed anything and passed on three reruns. The shared match is seeded from the clock, so it is order- or seed-dependent, and it is not touched here.
Pushed. The last step is the pull description itself, which needs nothing further from the tree.
Snapshots now go out every tick to every seat the room can hold. The old rule halved the rate at the third seat, and since the client's render delay follows the rate it measures, that seat drew every other body for everyone 42ms further in the past. The honest answer to the issue's question is that for this game the cadence is every tick always: the threshold now sits at ten seats, which is the whole room, and the comment on
broadcastEveryinsrc/arena.tscarries the numbers so nobody has to re-derive them.What to feel in the first ten seconds. Open the preview in three tabs from three addresses (or have two friends join), press F3 in one, and watch the readout: it stays at
30.0 Hz in · 45 ms behindwhen the third seat fills instead of falling to15.0 Hz in · 87 ms behind. Peek a corner at a running bot: it is where the server has it 45ms ago, not 87.Feel numbers, from the new harness in
src/feel.test.ts(a real arena with N idle seats, the viewer's real snapshots over the same jittery wire aswatchBots, warmed through the buy phase, the first three seconds of the rush measured):So other players come 41ms closer to the present with no new jitter: steps in the room of four go from 9.5% to 1.9% spread, and in the room of eight they are no worse. The 16% at eight is the same in both columns and is the server's own motion: with eight idle humans only two bots move, and the one measured threads past standing bodies at spawn. One bonus the table shows: a timeline meeting a 15Hz stream spends two seconds drawing bodies past the newest sample while its delay creeps from 45ms to 87ms, which is what a room felt at the moment its third seat filled. That transient is gone because the rate no longer changes.
Cost, measured on my machine, not the box (6-vCPU EPYC 9555P, bun 1.3; medians of seven 20-second runs of a seeded match, a never-sending run subtracted to isolate the snapshots). I could not reach the production box from the sandbox.
One snapshot is 6µs to build with the vision filter, one world tick 27µs. A full room is 0.2% of one core here; a box five times slower is at 1%. Bandwidth is the larger number: 6 Mbit/s out at ten seats, 80 kB/s to each seat over a whole match, since a buy-phase snapshot is larger than the 1.6 KiB live one.
What changed and why.
src/engine/arena.tsgained one optionalcadenceon the game contract, defaulting to what it had; no val number went into the engine, and the engine's own test covers the hook. The val game is now a factory (valGame,makeValArena) so the harness can seat a room of its own without touching the shared match; the singleton exports are unchanged.src/arena.test.tshad two tests that pinned the old threshold and one comment quoting a 3.4 KiB snapshot from before the sticky board; those were the tests being wrong about the rule, and they now assert the new one. README updated to match. No client change was needed:interp.jsfollows the rate it measures, as the issue expected.One unrelated note: a client test about the drawn body's speed failed once in a baseline run before I changed anything and passed on three reruns. The shared match is seeded from the clock, so it is order- or seed-dependent, and it is not touched here.
🤖 Generated with Claude Code
Closes #29