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

Round: one server tick, a roster, three minutes and an intermission #8

open not picked up yet opened this
opened this

One authoritative loop in the server process that owns the world, admits players, runs a 180-second round and a 20-second intermission, and publishes frames — the single truth every door reads.

Done looks like

  • [ ] src/round.ts (new) owns exactly one 20 Hz interval, exported as start() / stop() / tick();

importing the module starts nothing, so tests never race a live loop.

  • [ ] join(playerKey, name) places a mote within one tick and returns its id; a player who has not

steered for 60 s leaves the roster; the roster caps at 24 and the 25th arrival is a spectator who still sees everything.

  • [ ] Queued input is applied once per tick in arrival order; input from someone not in the roster is

ignored rather than an error.

  • [ ] Every second tick (10 Hz) a frame is published: motes, bloom, gate, a downsampled field, the

clock — under 4 KB with 24 motes, compact arrays rather than objects.

  • [ ] A round ends on the bloom crossing the gate or on the clock; the round and its scores are written

through the actions from #DATA in one transaction; round.started and round.ended emit on the existing bus in src/live.ts.

  • [ ] The clock comes from the tick count; an overrunning tick is skipped rather than queued, so the

loop never falls more than one tick behind.

  • [ ] src/round.test.ts drives tick() by hand — no sleeping tests: a round reaches the clock and

ends, a bloom pushed through the gate ends it sooner, an idle player is dropped, scores land in the database, and a restart mid-round begins a clean round rather than resuming a broken one.

  • [ ] src/server.ts starts the loop when it serves, never on import.

Files src/round.ts (new), src/round.test.ts (new), src/server.ts, src/live.ts, src/actions.ts.

Risks A loop that leaks into bun test makes every other test flaky — that is the failure to design against. Frames are the bandwidth budget of the whole app: measure one and put the number in the pull.

Fits Vesper's brief: a game has a loop, a state and a way to save.

commented

Split. The loop half moved to #53 — the one 20 Hz interval, the clock from the tick count, the round that ends on the clock or on the gate, the intermission, the write through recordRound, the round.started / round.ended emits, the skipped overrun and serve starting and stopping it — because none of it needs a seat decided, so it can be built and tested today against #5 and #6, both on main. This issue waits on it and keeps the half that does need #49: join and the roster, the idle drop, the 25th arrival as a spectator, the input queue applied once per tick in arrival order, and the 10 Hz frame under 4 KB. Invariant 4 is re-proved here, where motes can join, not there.

Same shape as the split #23 made from #9: the part that needs nothing running goes first.

commented

Also waits on #59 now. join will read key off the socket and believe it, and on main that key is anon:<random> for every guest connection — a seat per socket rather than a seat per person, which is the hole #49's record closed on paper and #59 closes in src/socket.ts. Admission and the spectator answer stay yours; what a key is is settled before you read one.

commented

Two more things belong in this issue's acceptance, both surfaced by #58's pull (#64).

  1. The rollover. #64 releases every seat when a round ends — the record does say a seat opens when the round ends — and nothing in this issue re-takes one. As written, the next round begins with an empty sea full of drifting bodies, and everybody who is still connected has to do something to get back in. #11 promises the opposite in as many words: nobody does anything to be in the next round. So this issue owns the rule: at the start of a round every connected socket that holds a key is seated, in the order those keys arrived, up to ROSTER_MAX, and anyone who was a spectator only because the sea was full becomes a swimmer without asking. A test should run two rounds back to back and assert the same keys are swimming in both.
  1. A seat taken during the intermission. #64's pull flags it: the idle and unsteered sweep runs only while a round is running, so a seat taken in the quiet is not released for failing to steer a round that has not started, and it is carried into the new world as a fresh mote with its clock restarted. That is a reasonable answer — decide it here rather than leaving it in a pull description, and say what a person joining at second 178 of a round sees: the end of that round, or the start of the next.

Neither is new work in a new file; both are rules this issue is the only place for. Also still waiting on #66 for the frame it publishes and #59 for what a key is.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs