Guest history: a Guest holds the Seat and owns its decided Games, listed on this browser, read and exported by link #355
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
spec
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amaurycoudr/chess-project#355
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
When an Online game ends, it is gone: the Room keeps only its current Game, a restart replaces it, and an eviction or a redeploy wipes the Room. A Player cannot find a game they won, show it to a friend, or take it to an analysis tool. There are no accounts yet, so the history has to belong to someone who has none.
Meanwhile a browser proves its place in a Room with a per-Room Seat token. That secret is worth one Room and cannot gather a browser's Games across Rooms. Keeping it beside a second, browser-wide secret would give every Player two identities and leave the accounts to come a transfer flow to build (ADR 0034).
Solution
A browser that creates a Room or takes a Seat without a Guest is given one by the server: a secret it keeps and sends with every call. That Guest is the only proof of a Seat, in every Room; the Seat token is gone. Reloading, or opening the Room in another tab, still lands the Player back in their Seat.
When an Online game is decided (by Checkmate, a Draw of any reason, an agreed Draw or a Resignation), the server stores it once. The stored Game keeps the Moves as one UCI line, who won and how it ended, when it started and ended, and the Guest in each Seat at that moment. A Player opens "My games" to see the decided Games of this browser, newest first, the opponent shown as "Guest". Every stored Game has a page that anyone with its link can read (board, move list, review controls, result) and export as PGN. A Game cut short (restart mid-game, Room evicted, redeploy) is not stored, and Local games stay in the browser.
User Stories
Seams
Room, through its public methods withroom.fixtures.ts, unit. The Room seats occupants by Guest, and reports a decided Game exactly once, for every ending, with its line, how it ended, its timestamps and the Guest in each Seat at the end. Covers 6, 7, 8, 9, 10.rpc.fixtures.tson the in-process database the Postgres spec gives it, integration. Minting on create and take and never on a refusal, acting for a Seat with and without its Guest, storage failure, the list and its read-after-decide guarantee, reading by id, and what the database holds. Covers 1, 3, 5, 11, 12, 13, 22.apps/e2e): one new spec, and the existing reload and second-tab specs kept green on the Guest. Two Players play; one resigns; each opens "My games" and sees the Game with their own result; one opens it; a third browser context opens its link and reads the result and Moves. Covers 4, 17, and walks 1, 7, 13 end to end.The engine's PGN change (below) is tested at the engine barrel, in the existing PGN tests. It covers no story of its own; it is what makes story 20 true.
Implementation Decisions
The Guest replaces the Seat token (ADR 0034, the Guest half)
tokenfield ofroom.createandseat.take, the per-Room storage in the front, and the API's rule that names its header. Every code comment pointing at ADR 0016 is repointed at ADR 0034 or dropped.room.createandseat.takemint a Guest only when the call carries none. Minting is a constructor option, like every source of non-determinism (API rule 2): at least 128 random bits, URL-safe. The response says which Guest it minted, or that it minted none; the secret crosses the wire only in the response that minted it.seat.takemints only when the Room has a Seat to give. A refused take (NO_FREE_SEAT) returns no Guest, so a Spectator never gets one.localStorageunder one key of its own, sends it in its header on every call, and replaces it with whatever a response minted. Storage that throws means no lasting Guest: play works, but a reload loses the Seat and history is empty, the same stance as the Seat token had.seat.take(deduplicated per Room code, as today), since the browser can no longer tell from its own storage whether it holds a Seat there. The answer hands back the held Seat, takes a free one, or refuses, and a refusal makes the browser a Spectator (ADR 0015).Recording a decided Game
Roomstamps when its current Game started, at open and at each restart, from its injected clock.Roomgains one more constructor option: a callback it calls exactly once when the current Game becomes decided:checkmateordraw;resign;drawOffer.checkmateorresignationwith the winner, ordrawwith its reason,agreedincluded), started and ended timestamps, and for each Color the Guest of the occupant of the Seat holding it at that moment, or none for a never-taken or vacated Seat.room.tsstays framework-free (API rule 1).games/feature module in the API owns storage and thegameprocedures.RoomServicewires the Room's callback to it.game.listwaits for them to settle before reading. One API process serves each stack, so this is enough to guarantee story 12 without the Room waiting on the database.GameId, minted through a constructor option.Schema (on top of the Postgres spec's baseline migration)
guests: an id, the secret's hash (unique), created at. A row is written the first time a Guest owns a decided Game, inside the same transaction as that Game, upserted by hash. A Guest that never owns a Game leaves no row, so minting needs no database.games:guests, emptied if that Guest is ever deleted;draw;Contract: a new
gamerouter in@chess/contracts(ADR 0027).game.list,GET /games: the caller's decided Games, newest first, capped at the 50 most recent. Each item has the id, started and ended, the caller's Color, how it ended, the other side's occupant and the line, from which the front counts Moves. A call with no Guest, or with a Guest that owns nothing, gets an empty list, not an error.game.get,GET /games/{id}: the line, how it ended, both timestamps, each side's occupant, and the viewer: the owner of a Color, or a reader. An unknown id, malformed ones included, isGAME_NOT_FOUND(404); the API treats an id it could not have minted as unknown before it reaches the database.guestornone; the Accounts v1 spec addsaccountwith its username. No hash and no secret ever crosses the wire.agreed, and shared withRoomOutcomeand the front's existing ending type where it fits (root rule 6).Engine
Game.pgntakes the result token from its caller for an ending Status cannot express (Resignation, agreed Draw), since the engine can never produce those (ADR 0017). Without one it derives the token from Status, as today.Front
games/, holding both views, their services and their query options (ADR 0011). Routes:/games("My games") and/games/$id(the Game page). A "My games" link joins the site navigation.room/andgames/may import it, since a vertical never imports a sibling.room/'s client sends the Guest header and stores what create and take mint;games/has its own client, which only sends it.game/'s game screen read-only: board, player rows, move list, review controls, captured trays and result line. Player rows name the viewer's own side "You" and every other side "Guest" (Last product decisions, point 6). It adds "Export PGN" and "Copy link".ADRs and docs
games/in both apps. The glossary already has Guest and the Seat token flagged ambiguity.Testing Decisions
room-play.test.tsandroom-offer.test.tsfor Room endings, androom.test.tsfor seating;rpc.integration.test.tswithrpc.fixtures.tsfor the wire, itsseatedGameandclientFormoving from tokens to Guests;resign.spec.ts,reload.spec.ts,presence.spec.tsandroom.fixtures.ts(seatedPair,spectator,openAnotherTab) for e2e;Out of Scope
Further Notes
gamewire; front views; e2e). Split withto-tickets.Revised in place to follow ADR 0034, which supersedes ADRs 0016 and 0033, and Last product decisions before the account specs. The title changed to match. What changed:
seat.take.gueststable (id, unique hash, created at) is written when a Guest first owns a decided Game.gamesnow references a Guest per Color instead of storing hashes inline, so Accounts v1 can add an Account owner beside it.guest, ornonefor an ownerless side; Accounts v1 addsaccount).youis now a viewer union. A malformed id answersGAME_NOT_FOUND.mentioned in issue #372
mentioned in issue #373
mentioned in issue #374
mentioned in issue #375
mentioned in issue #376
mentioned in issue #377
mentioned in issue #354
marked this issue as related to #354
marked this issue as related to #372
marked this issue as related to #373
marked this issue as related to #374
marked this issue as related to #375
marked this issue as related to #376
marked this issue as related to #377
mentioned in commit
6ba8bbcfa7mentioned in merge request !515
mentioned in commit
57d73a71bcmentioned in issue #393
marked this issue as related to #393
mentioned in commit
b613f6452dmentioned in commit
dc15bc4fb6mentioned in issue #394