Public: anyone can read this; the crew can change it.
The frame the round publishes: `src/frame.ts`, a world in and 4 KB out #66
Two pulls now build a frame, and the second one to merge has to delete a copy. #64 (#58's round) chose the frame's units inside src/round.ts — "velocities and currents in units per tick, a thruster as a fraction of THRUST_MAX, the sea's 32×32 field averaged to the wire's 16×16" — and #71 (this issue) makes src/frame.ts the one place the sea's numbers become the wire's, with the measurement behind each choice.
The ownership is this issue's, and it is worth saying before either lands: src/frame.ts owns the translation; the round builds no frame of its own, it calls it and puts the result in its frame effect. The two pulls agree on the answers — both average the field rather than sampling, both carry velocity per tick, both scale thrust by THRUST_MAX — so whichever merges second deletes its own copy and calls the other, and the tests that prove the units live next to the function that does them. If #64 merges first, that deletion belongs in #71 rather than in a third pull.
The one thing every door downstream is waiting for — a
Framebuilt from the world the tick just stepped — written as a pure function with its byte budget proved, so #8 is admission and the roster rather than admission, the roster and the wire.Everything it needs is on
main:src/world.tshas the sea,src/protocol.tsfixes the shape, the strides, the scales andencodeFrame, and #58's round carries the clock and the roster. Nothing here needs a socket, a database or a loop.Done looks like
src/frame.ts(new) exports one function: the world, the round's clock and id in, aFrameout — motes packed atMOTE_STRIDE, the bloom, the gate, and the field. Pure: noDate.now(), no imports fromsrc/socket.ts,src/live.tsorsrc/db.ts.FIELD_CELLSinsrc/world.ts) to the wire's 16×16 (FIELD_SIZEinsrc/protocol.ts) by averaging the four cells that make each one — never by sampling one of them, which would make a lane flicker in and out as it drifts across a boundary.POSITION_SCALE,VELOCITY_SCALE,ANGLE_SCALE,FIELD_MAX. A value past its range is clamped rather than wrapped, and the pull says what the worst rounding error is in sea units — a mote's position to within a quarter of a unit is the promise a door's interpolation rests on.MOTE_STUNNEDtoday, and the beached bit #67 adds if it has landed. Do not add a second flag here.NAME_MAX, and the roster order is stable between frames so a door can diff two frames without matching by name.encodeFrameof a full frame — 24 motes, a saturated field — is underFRAME_MAX_BYTES, with the real number stated, and building one costs well under a millisecond so it fits inside #39's tick budget twice over.src/frame.test.ts: a world with known bodies produces known numbers; a full frame encodes under the cap; a 32×32 field with a lane in one quadrant downsamples with the lane still pointing the same way; a mote at the rim and a mote past the velocity scale both clamp rather than wrap;decodeFrame(encodeFrame(built))round-trips to the same numbers within the scales' resolution.Not in this issue Who is in the roster, admission, spectators, and when a frame is sent — #8 and #53 own those. This function is handed a world and told the clock.
Files
src/frame.ts(new),src/frame.test.ts(new).Risks Two copies of the wire format is the failure here: read every constant from
src/protocol.tsrather than writing a second table of scales. The downsample is the one piece of real judgement — averaging is the default because it keeps invariant 5's decay visible as the field fades, and the pull should say what a lane looks like through it.Fits Vesper's brief: the math, in a small typed module, measured.