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

What you may do, and what it would pay you: `may`, `canStart` and the standing offer's worth, in the state itself #86

Merged opened this

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

Merged

`tableState` now answers RULES.md §5 and §6 once, for the viewer's own seat, so no door has to re-derive a rule to draw a playable turn.

shipped production is serving it deploy 6ecafc0

opened this

tableState now answers RULES.md §5 and §6 once, for the viewer's own seat, so no door has to re-derive a rule to draw a playable turn.

  • may on TableView is src/round.ts's MayAct carried through in the vocabulary a door speaks (seat ids, not ordinals): may I offer, may I accept, may I pass, the one refusal sentence for each, and cite — the aims §6 would let me swear on the standing offer, in charter order. It is the viewer's own seat and nobody else's: null for a guest, a stranger, an admin with no seat, and null before the table is dealt, when there is no turn to take.
  • your_payoff is what the standing accord would pay this seat if it bound — score in src/board.ts, over the viewer's own charge and a public accord, so it is arithmetic a player is entitled to and nobody else's sum.
  • canStart(who, table) is exported beside canJoin and canLeave and agrees exactly with startTable's refusals: host or admin, open, between SEATS.min and SEATS.max seated.

Nothing new is stored and no rule is written twice: may is mayAct answering over acts view already read, your_payoff is score, and no extra query is run. Two small helpers keep the seams honest — chargeOf is the one place a stored charge_json becomes the charge the rules read (under the seat row's own ordinal, so a charge is always its holder's) and taking() now uses it instead of its own copy; dealtIn is the one place a read asks which seat is the viewer's, so mayAct can never be handed another seat's charge and throw. The live payload is untouched: may is per-viewer and the bus still carries addresses only.

Worth a look: the redaction test in src/actions.test.ts ("may is the viewer's own seat's, and cite nobody else's aims"), which asserts a guest and another player see may: null, your_payoff: null and no charge but their own, and that every aim in a player's cite is an aim of their own charge — cite carries weights, so a stray aim would be a leaked charge. The §10 arithmetic is checked twice over in "your_payoff is what the standing accord would pay this seat": by hand (A₁ pays 9/0/4/7, A₂ pays 8/6/4/8, matching RULES.md §10's own numbers) and against score(). In the preview, GET /api/tables/:id as a seated player now carries both fields; as a guest both are null.

I did not draw any button from these: the page, the terminal and the MCP tools are unchanged, since #33 is the issue that draws the start button and the turn controls. bun test is green (136 tests), the app boots and / answers in ~8 ms.

🤖 Generated with Claude Code

Closes #74

commented

tableState now answers §5 and §6 once, for the viewer's own seat, so no door has to re-derive a rule to draw a playable turn:

  • may — src/round.ts's MayAct carried through in seat ids: may I offer, accept, pass, the refusal sentence for each, and cite, the aims §6 would let me swear on the standing offer, in charter order. It is the viewer's own seat and nobody else's: null for a guest, a watcher and a player at another table, and null before the table is dealt.
  • your_payoff — what the standing accord would pay this seat if it bound, score in src/board.ts over the viewer's own charge and a public accord.
  • canStart(who, table) beside canJoin and canLeave, agreeing exactly with startTable's refusals: host or admin, open, four to six seated.

Nothing is stored and no rule is written twice. chargeOf is the one place a seat's stored charge becomes the charge the rules read, under the seat's own ordinal; dealtIn is the one place a read asks which seat is the viewer's, so mayAct is never handed another seat's charge.

main moved under this branch (#82, the ledger's write) and is merged in. Both sides had rewritten taking, and the resolution keeps each side's idea where it belongs: taking has #82's shape — the door's checks, then judged, asked again inside written's immediate transaction — and judged reads the charge through chargeOf, so the single place a stored charge becomes a rules charge now sits on both sides of the write. The README keeps both paragraphs, what a read answers and why a write re-decides; both sides' tests are kept whole and neither was edited to pass. bun test is 138 pass, 0 fail, including #82's eight-process race.

To try it in the preview: open a table, seat four, start it, and watch the offer/accept/pass buttons and the payoff follow the standing offer — then open the same table as a guest or another player and see may and your_payoff come back null.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs