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

The turn: offer, accept, pass — and whose move it is #8

shipped production is serving it opened this has it
opened this

Intent. The act ledger that makes it a game: one act per turn, in seat order, with the standing offer and its acceptances derived from the acts rather than stored twice.

Done looks like

  • [ ] Actions makeOffer(db, who, { gameId, accord }), acceptOffer(db, who, { gameId }), passTurn(db, who, { gameId }), each writing one row to act and refusing by name when it is not your turn, you hold no seat, or the table is not playing.
  • [ ] An accord is validated at the boundary: exactly one setting per clause of this table's charter, no unknown clause, no unknown setting, no missing clause — a test for each.
  • [ ] The standing offer is the latest offer act; its acceptances are the accept acts after it; a new offer clears them, and a test proves an acceptance before an offer does not count for it.
  • [ ] tableState gains turn (the seat to act), your_turn for the viewer, standing (the accord and who offered it), acceptances (which seats, in order) and acts (the whole public log, newest last).
  • [ ] The same person cannot accept the standing offer twice, and the offerer may take their own turn to accept it.
  • [ ] emit("act.made", …) and emit("turn.changed", …); the API gains POST /api/tables/:id/offer, /accept, /pass answering with the status the failure names.
  • [ ] Tests play a whole four-seat table through offers, acceptances and passes and assert the turn order after each act.

Files src/actions.ts, src/actions.test.ts, src/routes.ts, src/server.test.ts.

Risks Deriving the standing offer on every read must stay fast — index act (game_id, id). Turn order from ordinal plus the count of acts is simpler than a stored pointer; pick one and say why in the pull.

Brief that fits @ledger — an append-only ledger with the rule at the boundary. (A person assigns.)

commented

Amended by the planner. This issue also carries startTable(db, who, { gameId }) (moved from #7): host or admin, 4 to 6 seated, fixes the seat order, stores the seed, deals the charter and every seat's charge with deal() from src/board.ts (#6), sets quorum to ⌈n/2⌉+1 and the deadline, moves the table to playing, and emits table.started — refused by name with three seats, by a non-host, and on a second call. POST /api/tables/:id/start is its route.

commented

Amended by the planner, now that #36 is merged: src/round.ts holds the arithmetic this issue used to describe, so #8 is wiring, not a rules engine.

  • The act shape is already decided there and the schema stores exactly it: { seat, kind: "offer" | "accept" | "pass", accord?, cite?, at }, plus the ids and authorship storage needs. Do not add a field the rules do not read.
  • whoseTurn, standing, quorum, binds and mayAct are imported, never re-derived. The turn is the act count modulo the seat count — derived, as #36 argued — so there is no pointer column and nothing to keep in step.
  • tableState builds a Round from the rows and answers from these functions: turn, your_turn, standing (accord, who, the acceptances in order) and acts. A rule that appears in an action and also in round.ts is a bug; if one is missing there, add it there.
  • The boundary stays here and is the real work of this run: validAccord from src/board.ts against this table's charter, it-is-not-your-turn, no-seat, not-playing, and the rate that stops an agent spending a turn instantly — each refused by name, with the status the failure names at every door.
  • startTable (moved here earlier) deals with deal() and stores the seed, the charter and every charge in one transaction.

The checklist otherwise stands. src/example.ts is §10's table as data — use it in the tests rather than building a fourth copy of a charter.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs