Store each decided Online game once, owned by the Guest in each Seat #373

Closed
opened 2026-10-11 16:26:24 +00:00 by root · 8 comments
root commented 2026-10-11 16:26:24 +00:00 (Migrated from gitlab.core-project.space)

Imported from GitHub amaurycoudr/chess-project#373, opened by @amaurycoudr on 2026-10-11 16:26 UTC.


Parent

#355

What to build

When an Online game is decided, the server stores it once. The Room stamps when its current Game started, from its injected clock, at open and at each restart. It gains a constructor option: a callback it calls exactly once when the current Game becomes decided. That happens by a Move that leads to checkmate or draw, by resign, or by accepting a draw Offer.

The record holds:

  • the UCI line from STARTPOS;
  • how it ended: checkmate or resignation with the winner, or draw with its reason, agreed included;
  • the started and ended timestamps;
  • for each Color, the Guest occupying the Seat that holds it at that moment, or none for a Seat that was vacated or never taken.

A Game cut short by a restart, an eviction or a redeploy is never reported. The Room stays framework-free.

How a Game ended is defined once in the contracts, reusing the engine's Draw reasons plus agreed. RoomOutcome shares it. A branded GameId joins the contracts.

A new games/ feature module in the API owns storage, and RoomService wires the Room's callback to it. The write is asynchronous and the Room never waits for it, so the deciding call answers and fans out at once. A failure is logged and dropped. The module keeps track of writes still in flight, ready for the list to wait on. The Game id is minted server-side through a constructor option: at least 128 random bits, unguessable.

The schema adds a migration on top of #354's baseline:

  • guests: id, unique secret hash, created at. A row is upserted by hash in the same transaction as the first Game that Guest owns.
  • games: id; a nullable reference to guests for White and one for Black, emptied if that Guest is deleted; line; winner, set exactly when the ending is not a draw; ending; draw reason, set exactly for draw; started at and ended at as timestamptz.
  • An index on each owner column with ended at.
  • No Room code and nothing derived (ADR 0014, ADR 0021). Owner columns are named per occupant kind and per Color, leaving room for Accounts v1 (ADR 0035).

Seam: the Room with its fixtures (a decided Game reported exactly once, for every ending); the RPC wire with the in-process app on the in-process database (storage failure, what the database holds).

Acceptance criteria

  • Every ending is reported once with the right ending, winner and draw reason: Checkmate, each Draw reason, agreed Draw, and Resignation by either Color (story 8).
  • An Offer resolving after the decision does not report the Game a second time.
  • A restart mid-game reports nothing. A restart after a decided Game starts the next one with a fresh start time (story 9).
  • A Seat that changed hands mid-game is owned by the occupant at the end (story 7).
  • A vacated or never-taken Seat at the end is stored with no owner on that side (story 10).
  • After a decided Game over the wire, the database holds one games row and the Guest rows it needs. No secret is stored, only its hash (story 22).
  • When the write fails, the deciding call still answers and the Room stays playable (story 11).
  • The migration is generated and committed, and the gate's no-uncommitted-migration check passes.
  • The API's new games/ folder is declared in the API's layer order and framework-import rules (the owner ruled on the accounts breakdown that declaring a folder the spec creates counts as the requested lint change; nothing else in lint config changes)
> _Imported from GitHub [amaurycoudr/chess-project#373](https://github.com/amaurycoudr/chess-project/issues/373), opened by [@amaurycoudr](https://github.com/amaurycoudr) on 2026-10-11 16:26 UTC._ <!-- github-issue:373 --> --- ## Parent #355 ## What to build When an **Online game** is decided, the server stores it once. The **Room** stamps when its current **Game** started, from its injected clock, at open and at each restart. It gains a constructor option: a callback it calls exactly once when the current **Game** becomes decided. That happens by a **Move** that leads to `checkmate` or `draw`, by `resign`, or by accepting a `draw` **Offer**. The record holds: - the **UCI** line from **STARTPOS**; - how it ended: `checkmate` or `resignation` with the winner, or `draw` with its reason, `agreed` included; - the started and ended timestamps; - for each **Color**, the **Guest** occupying the **Seat** that holds it at that moment, or none for a **Seat** that was vacated or never taken. A **Game** cut short by a restart, an eviction or a redeploy is never reported. The **Room** stays framework-free. How a **Game** ended is defined once in the contracts, reusing the engine's **Draw** reasons plus `agreed`. `RoomOutcome` shares it. A branded `GameId` joins the contracts. A new `games/` feature module in the API owns storage, and `RoomService` wires the **Room**'s callback to it. The write is asynchronous and the **Room** never waits for it, so the deciding call answers and fans out at once. A failure is logged and dropped. The module keeps track of writes still in flight, ready for the list to wait on. The **Game** id is minted server-side through a constructor option: at least 128 random bits, unguessable. The schema adds a migration on top of #354's baseline: - `guests`: id, unique secret hash, created at. A row is upserted by hash in the same transaction as the first **Game** that **Guest** owns. - `games`: id; a nullable reference to `guests` for White and one for Black, emptied if that **Guest** is deleted; line; winner, set exactly when the ending is not a draw; ending; draw reason, set exactly for `draw`; started at and ended at as timestamptz. - An index on each owner column with ended at. - No **Room code** and nothing derived (ADR 0014, ADR 0021). Owner columns are named per occupant kind and per **Color**, leaving room for Accounts v1 (ADR 0035). Seam: the `Room` with its fixtures (a decided **Game** reported exactly once, for every ending); the RPC wire with the in-process app on the in-process database (storage failure, what the database holds). ## Acceptance criteria - [ ] Every ending is reported once with the right ending, winner and draw reason: **Checkmate**, each **Draw** reason, agreed **Draw**, and **Resignation** by either **Color** (story 8). - [ ] An **Offer** resolving after the decision does not report the **Game** a second time. - [ ] A restart mid-game reports nothing. A restart after a decided **Game** starts the next one with a fresh start time (story 9). - [ ] A **Seat** that changed hands mid-game is owned by the occupant at the end (story 7). - [ ] A vacated or never-taken **Seat** at the end is stored with no owner on that side (story 10). - [ ] After a decided **Game** over the wire, the database holds one `games` row and the **Guest** rows it needs. No secret is stored, only its hash (story 22). - [ ] When the write fails, the deciding call still answers and the **Room** stays playable (story 11). - [ ] The migration is generated and committed, and the gate's no-uncommitted-migration check passes. - [ ] The API's new `games/` folder is declared in the API's layer order and framework-import rules (the owner ruled on the accounts breakdown that declaring a folder the spec creates counts as the requested lint change; nothing else in lint config changes)
root commented 2026-10-11 16:26:24 +00:00 (Migrated from gitlab.core-project.space)

GitHub issue relations, imported as "related" links (GitLab CE has no sub-issues or blocking links).

  • Parent (sub-issue of): #355
  • Blocked by: #372
  • Blocking: #374
> _GitHub issue relations, imported as "related" links (GitLab CE has no sub-issues or blocking links)._ <!-- github-relations --> - Parent (sub-issue of): #355 - Blocked by: #372 - Blocking: #374
root commented 2026-10-13 08:39:45 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #374

mentioned in issue #374
root commented 2026-10-13 08:41:21 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #355

mentioned in issue #355
root commented 2026-10-13 08:41:24 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #372

mentioned in issue #372
root commented 2026-10-13 08:42:27 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #355

marked this issue as related to #355
root commented 2026-10-13 08:42:35 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #372

marked this issue as related to #372
root commented 2026-10-13 08:42:35 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #374

marked this issue as related to #374
root commented 2026-10-13 14:34:16 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !515

mentioned in merge request !515
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-14 09:31:57 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
amaurycoudr/chess-project#373
No description provided.