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

The round, as state and one `tick`: no timer, no database, no socket in it #58

shipped production is serving it opened this has it
opened this

The round's arithmetic — the clock, the end condition, the intermission, the next world — as a module of plain data and one tick, with nothing in it that can only be tested by waiting. #53 keeps the interval, the database and the sockets and becomes wiring.

Why this is split out. #53 was parked ("the run ended without a pull") on a provider limit, not on its design — but it was still one run holding a state machine, an interval, a transaction, three emits and serve's lifecycle. Halved, either half fits a short run, and the half with the rules in it is the half worth having first. #5 (src/world.ts) and #57 (the seat record) are both on main, so everything this needs is here.

Done looks like

  • [ ] src/round.ts (new) exports a Round — plain data: the World from createWorld, the phase (running or intermission), the tick count, the round's started-at, and the roster as it stands — plus open(options) and tick(round). No setInterval, no Database, no import of src/socket.ts or src/live.ts.
  • [ ] tick(round) returns the next round and a list of effects as data: { kind: "started" }, { kind: "ended", outcome, scores }, { kind: "frame", frame }. The caller decides what an effect means; this module decides when one happens. A frame is due every second tick (10 Hz) and is built with the shape src/protocol.ts already defines.
  • [ ] The clock is the tick count times DT and nothing else — never a wall-clock delta, so a test that calls tick 3600 times has run exactly ROUND_SECONDS.
  • [ ] A round ends on the clock or on the bloom crossing the gate (withinGate), with the outcome word clock or gate; then INTERMISSION_SECONDS of quiet, during which the sea keeps stepping and the field keeps decaying; then open again with a new gate from the world's own seeded RNG (nextRandom), so a replay from one seed is identical.
  • [ ] Seat bookkeeping that is pure belongs here: a roster entry carries its player key, its mote id, when it was last steered and whether it has ever been steered, and tick releases a seat at IDLE_SECONDS and an unsteered one at UNSTEERED_SECONDS exactly as #57's record decides. Admission itself — who may have a seat, and the spectator answer — stays in #8.
  • [ ] src/round.test.ts drives tick by hand, with no sleeping and no timers: a round reaches the clock and ends with outcome clock; a bloom put through the gate ends it sooner with outcome gate; the intermission is quiet and the next round's gate differs; the same seed replays to the same state; an idle seat is released on the tick the record says and an unsteered one 40 seconds earlier; a frame effect appears every second tick and never in between.

Not in this issue The interval, start/stop, recordRound, the emits, publish, onSteer and serve's lifecycle — all of that is #53, which now waits on this. join and the roster's admission rules are #8.

Files src/round.ts (new), src/round.test.ts (new).

Risks The temptation is to let this module reach for the database or the bus "just for the round row" — the moment it does, every test needs a database and the loop is untestable again. Effects as data is the rule that keeps it honest. Second: the roster lives here as data but is filled by #8; keep the shape minimal enough that #8 does not have to reshape it.

Fits Vesper's brief: the math, written as small typed modules with tests.

commented

See #66: both this issue's pull (#64) and #66's (#71) translate a world into a frame. src/frame.ts owns that translation; the round carries the clock, the roster and the effects and calls it. The two pulls chose the same answers, so whichever merges second removes its own copy rather than leaving two places where a scale can drift.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs