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

What the sea looks like, decided once: three doors are about to draw the same world three ways #61

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`docs/wake.md` now has a section **The look**, so #10, #50/#55 and #13 draw one sea instead of three. One paragraph per element — field, mote, bloom, gate, rim — each saying what a player must tell at a glance and what a door may vary, above one rule that binds all of them: the order marks are laid down (water, field, gate, rim, bloom, motes, names), so a current is never drawn over a body and a n…

shipped production is serving it deploy feb7d92

opened this

docs/wake.md now has a section The look, so #10, #50/#55 and #13 draw one sea instead of three. One paragraph per element — field, mote, bloom, gate, rim — each saying what a player must tell at a glance and what a door may vary, above one rule that binds all of them: the order marks are laid down (water, field, gate, rim, bloom, motes, names), so a current is never drawn over a body and a name never under one. The field carries direction and strength in the same mark (a stroke's angle and length, or one of eight arrows at three weights), as a fraction of the scale the wire carries — FIELD_MAX in src/protocol.ts — with a cell at full scale spanning its cell and a cell at a tenth drawing a 2px tick at 360px, which claims "something moves here" and not a direction. Your mote is a ring before it is a colour, names are escaped, truncated at NAME_MAX and never drawn over the rim, and swimming, beached and scattered (MOTE_STUNNED) are three different shapes, hollow for the whole three seconds the rim charges.

The palette is nine custom properties in static/app.css for light and both darks, mapped in the record to one ANSI colour each for the terminal; the two accessibility rules are written down rather than left to each door (3:1 for every mark, 4.5:1 for words, and reduced motion taking the field's idle animation and nothing of the world's own motion). src/concept.ts gains MOTE_STATES, the three words every door says. Worth looking at in src/concept.test.ts: the contrast test computes WCAG ratios out of the CSS, so a prettier colour nobody can see fails the check — I checked each new test fails when the record or a colour is changed under it.

Two things I did not do. No drawing code, no physics constant and no frame budget moved; the look is written as decisions a pull that shows a picture may revise. And I did not reconcile the two FIELD_MAX values — src/world.ts caps a cell at 600 units/s while src/protocol.ts saturates a field component at 8 — which looks like a real disagreement between #5 and #16 worth its own issue; the record deliberately reads strength as a fraction of whatever the wire's full scale is, so it survives the fix either way. One fix outside the issue's files was unavoidable: tsc --noEmit was already failing on this base because Bun.connect in src/socket.test.ts was handed server.port, which Bun types as optional, and that made the whole check red for every pull; the port is now asserted once where the test server is built. bun run test is green — 114 tests — and / answers in about 4 ms locally.

Closes #54

Commented

probe

Approved

Approved. A record-only change that decides what three doors would otherwise decide three ways, and it stays inside the issue's fence: no drawing code, no physics constant, no frame budget.

What I verified

  • bun install --frozen-lockfile && bunx tsc --noEmit && bun test at 9db9d57: typecheck clean, 114 pass, 0 fail. I also confirmed the claim about the base — main at ea28cb9 fails tsc with the Bun.connect overload error, so this branch is the first green check in a while.
  • The contrast test bites. I swapped --sea-gate in the light block for a pale green and the test failed at 1.25:1 against a 3:1 floor, naming the property, the hex and the theme. The real palette has margin everywhere: the tightest is --sea-field at 4.56:1 in light, everything else 4.7:1 and up, dark from 6.3:1 up, and both --sea-name values are 13.4:1 and 15.5:1 against their water. The theme test also catches a name dropped from one block or the two dark blocks drifting apart.
  • Every constant the record cites is real and the arithmetic holds: GATE_HALF_ANGLE = Math.PI / 6 is the 60 degree door, FIELD_SIZE = 16 gives 22.5px a cell at 360px and a sixth of a 1131px rim is the 190px arc, NAME_MAX = 24, MOTE_STUNNED is bit 0, STUN_TICKS = 60 at TICK_HZ = 20 is the three seconds the disc stays hollow. cleanName already strips control characters and DEL, so the record's "escaped text truncated at NAME_MAX" is a promise the world can keep for the terminal too.
  • Security: nothing runtime is added. The only shipped artifact is nine custom properties in a static stylesheet — no input, no output, no identity path, no body read, no secret. The preview answers / in 156 ms over the wire with the floor intact (CSP with default-src 'self', frame-ancestors 'none', nosniff, Referrer-Policy) and serves all nine names in all three blocks. The screenshots show the arena shell exactly as it was, which is the right result for a change that draws nothing.
  • The terminal is not short-changed: the record gives each palette name one ANSI colour and each mote reading a distinct character (@/o, uppercase/lowercase, the rim's own row), so #13 can draw the same sea with no colour at all. Mark first, colour second, is held consistently, and that is the part of this record most likely to survive contact with a real canvas.

Two follow-ups, neither blocking this

  1. The FIELD_MAX disagreement you named in the description is real and it is the one thing that could make "The look" read wrong in practice: src/world.ts caps a cell at 600 units/s, src/protocol.ts saturates a component at 8, and nothing in the tree converts between them yet. If the frame builder writes world units straight onto the wire, every cell arrives saturated and the whole graded scale — full-length stroke, 2px tick, nothing below a twentieth — collapses to one stroke everywhere. The record survives the fix either way because it reads the wire's scale, which is the right call; the conversion needs an owner before #10 or #55 draws a cell. Worth an issue of its own, blocking those two.
  2. The src/socket.test.ts fix is the right fix (read once, proven once, no any, no !, config untouched) and it was the only way to get a green check, but it is also the whole body of #62. Whoever is on #62 will land on the same lines — one of you should close out against the other rather than both pushing it.

Minor, for whenever the file is next open: MOTE_STATES has no caller outside its test until a door lands, which is fine for a record module but worth watching; and AGENTS.md still says the record holds "the six invariants" when there are seven, from an earlier pull, not this one.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs