diff --git a/CLAUDE.md b/CLAUDE.md index 00ed4ca..f61d12b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ # checkmate.blue -Federated chess platform built on AT Protocol. Players authenticate with Bluesky identity, play real-time 1v1 chess with game state stored as atproto records, move updates via Jetstream. No application database, no server-side game logic. +Federated chess platform built on the ATmosphere. Players authenticate with their ATmosphere account, play real-time 1v1 chess with game state stored as AT Protocol records, move updates via Jetstream. No application database, no server-side game logic. See `SPEC.md` for full technical specification. diff --git a/README.md b/README.md index abd8c83..12681d8 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,32 @@ # checkmate.blue -Federated chess on [AT Protocol](https://atproto.com). Play real-time 1v1 chess using your Bluesky identity -- no accounts to create, no server to trust. Game state lives in each player's personal data repository. +Federated chess on the [ATmosphere](https://atproto.com). Play real-time 1v1 chess using your ATmosphere account -- no new accounts to create, no server to trust. Game state lives in each player's personal data repository. **https://checkmate.blue** ## How it works -1. Sign in with your Bluesky account (via AT Protocol OAuth) +1. Sign in with your ATmosphere account (via AT Protocol OAuth) 2. Challenge another player by handle or accept an open challenge 3. Play chess -- moves are written to your PDS as `blue.checkmate.game` records 4. Your opponent's moves arrive in real-time via [Jetstream](https://docs.bsky.app/blog/jetstream) There is no application server. The entire app is a static site that talks directly to each player's PDS. Each player maintains their own copy of the game record with the full PGN. The board reconciles state by reading both records and using the one with more moves. +## Features + +- **Real-time play** -- moves delivered via Jetstream WebSocket with polling fallback +- **Challenge system** -- invite by handle, share links, post challenges to Bluesky +- **Spectator mode** -- anyone can watch live games without signing in +- **Draw offers** -- offer, accept, decline, or retract draws mid-game +- **Rematch** -- one-click rematch with live notification to your opponent +- **Game result sharing** -- editable share-to-Bluesky post with @mentions and link cards +- **Abandon detection** -- claim a win after 7 days of opponent inactivity +- **Post-game tools** -- PGN download and Lichess analysis board +- **Sound effects** -- move, capture, check, and game-end sounds +- **PWA** -- installable as a mobile app +- **Accessible** -- WCAG 2.1 AA compliant (focus indicators, screen reader support, reduced motion) + ## Stack | Layer | Technology | @@ -23,6 +37,7 @@ There is no application server. The entire app is a static site that talks direc | Auth | [@atproto/oauth-client-browser](https://github.com/bluesky-social/atproto) | | Data | [AT Protocol](https://atproto.com) records on each player's PDS | | Real-time | [Jetstream](https://docs.bsky.app/blog/jetstream) WebSocket | +| Queries | [Constellation](https://constellation.microcosm.blue) / [Slingshot](https://slingshot.microcosm.blue) | | Styling | [Tailwind CSS](https://tailwindcss.com) | ## Development @@ -35,7 +50,7 @@ npm run preview # Preview production build npm run check # Type check ``` -The dev server binds to `127.0.0.1` (not `localhost`) because atproto OAuth loopback clients require an IP address. +The dev server binds to `127.0.0.1` (not `localhost`) because AT Protocol OAuth loopback clients require an IP address. ## Lexicons @@ -66,7 +81,7 @@ Player A (White) Player B (Black) +-----------------+ +-----------------+ ``` -Each player writes moves only to their own PDS (atproto enforces per-user write permissions). Both records contain the full PGN. On load, the app reads both records and uses the longer PGN, since each record is one move behind on the opponent's turns. +Each player writes moves only to their own PDS (AT Protocol enforces per-user write permissions). Both records contain the full PGN. On load, the app reads both records and uses the longer PGN, since each record is one move behind on the opponent's turns. ## License diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 3011702..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,234 +0,0 @@ -# checkmate.blue Roadmap - -## Phase 1: Challenge & Invite Flow -- DONE - -Implemented on branch `phase-1-challenge-invite-flow`. - -- **1a. Post-Based Challenges** -- "Post to Bluesky" button on the game waiting screen with @mention facets and game link. -- **1b. DM-Based Challenges** -- "Send via DM" button copies game link to clipboard and opens bsky.app/messages. Full API-driven DMs deferred (requires `transition:chat.bsky` scope). -- **1c. Color Selection** -- White/Black/Random picker on `/play`. Game page logic fully decoupled from record ownership. -- **Check-before-join guard** -- re-fetches game record before joining to reduce race conditions on shared links. - -See `plans/phase-1-challenge-invite-flow.md` for full details. - ---- - -## Phase 3: Spectator Mode -- DONE - -Pulled forward and implemented alongside Phase 1. - -- **Read-only game view** for non-participants with flip-board button. -- **Unauthenticated viewing** -- no login required to watch a game. -- **Dual Jetstream connections** -- spectators receive live updates from both players. -- **Game-full fallback** -- extra visitors become spectators, not errors. - -See `plans/phase-3-spectator-mode.md` for full details. - ---- - -## Phase 2: Shareability & Branding -- NEXT - -Priority: **High**. Links shared on social platforms currently show bare URLs. A recognizable brand and rich link previews drive organic growth. - -### 2a. Logo - -FontAwesome chess knight icon in AT Protocol blue (`#0085FF`). - -**Usage:** -- Favicon (SVG) -- Navbar icon (32x32) -- OG image fallback (centered on dark background) -- PWA icon (192x192, 512x512) - -**Implementation:** -- Create an SVG from the FA `chess-knight` glyph, colored `#0085FF`. -- Generate PNG variants at required sizes for PWA manifest and Apple touch icon. -- Replace the current placeholder favicon. - -### 2b. Open Graph Meta Tags - -Rich link previews when sharing game/challenge URLs on Bluesky, Twitter, Discord, etc. - -**Problem:** The app is a pure SPA (SSR disabled, static adapter with `fallback: index.html`). Every route serves the same HTML shell, so crawlers see the same generic meta tags regardless of URL. - -**Options (in order of preference):** - -1. **Cloudflare Worker in front of static site** -- intercept requests from known bot user agents, fetch game/challenge data from the PDS, return a minimal HTML page with correct `og:title`, `og:description`, `og:image` tags. All other requests pass through to the static SPA. - -2. **OG image generation service** -- a separate edge function that renders a board position as a PNG. URL pattern: `checkmate.blue/og/{did}/{rkey}.png`. The Cloudflare Worker references these in the `og:image` tag. - -3. **Static fallback only** -- use the same generic card (logo + "checkmate.blue - Chess on the Atmosphere") for all links. Easiest but least compelling. - -**Recommended approach:** Start with option 3 (generic card using the logo) and add the Cloudflare Worker (option 1) as a fast follow. The OG image generation (option 2) is nice-to-have. - -**Meta tags for different routes:** - -| Route | og:title | og:description | -|-------|----------|---------------| -| `/` | checkmate.blue | Chess on the Atmosphere | -| `/game/{did}/{rkey}` | Chess Game - {white} vs {black} | {status} - {move count} moves | -| `/challenge/{did}/{rkey}` | Chess Challenge from {handle} | {handle} wants to play chess | -| `/profile/{handle}` | {handle} on checkmate.blue | Chess profile | - ---- - -## Phase 4: Homepage & Game Discovery - -Priority: **Medium**. Makes the platform feel alive and gives new visitors something to see. - -### 4a. Active Games List - -Show ongoing games on the homepage, sorted by most recent activity. - -**Deduplication:** Only show White's records (canonical). Black's records have `parentGameUri` set, so filter those out. A game record without `parentGameUri` and with `status: active` is a canonical active game. - -**Implementation:** - -- Query approach TBD -- depends on what Constellation supports for global queries. Options: - - Query Constellation for all `blue.checkmate.game` records globally (if supported). - - Maintain a known-players list and query each player's games (doesn't scale). - - Use the Jetstream firehose to build a client-side index of recent games (complex, ephemeral). - - Add a lightweight indexing endpoint (breaks the "no server" constraint). -- Filter to `status: active` and `parentGameUri` absent (White's record only). -- Sort by last activity. **Requires a `lastMoveAt` field on the lexicon** -- PGN parsing for sort order is too expensive for a list view. - -**Lexicon change:** -``` -"lastMoveAt": { - "type": "string", - "format": "datetime", - "description": "Timestamp of the most recent move" -} -``` - -This field gets updated on every `putRecord` call when a move is made. - -- Display each game as a card: White handle vs Black handle, move count, last activity time, link to spectate. - -### 4b. Completed Games Feed - -Below active games, show recently completed games with results. - -- Same query approach as active games, filtered to `status: completed`. -- Show result (1-0, 0-1, draw), result reason, player handles. -- Links to view the final position. - ---- - -## Phase 5: Game Result Sharing - -Priority: **Medium**. The viral loop. Players share their wins (and losses) to Bluesky. - -**Implementation:** - -- After a game ends (checkmate, resignation, draw), show a "Share to Bluesky" button on the game page. -- Create a `app.bsky.feed.post` record with: - - Text describing the result: "Checkmate! I beat @{opponent} on checkmate.blue" / "Good game -- @{opponent} got me this time" / "Draw with @{opponent} on checkmate.blue" - - Mention facet for the opponent - - Link facet to the game URL - - Embed with game link card (benefits from Phase 2 OG tags) -- Include move count and result reason in the post text for flavor. -- This is always opt-in. Show a pre-filled post that the player can edit before posting. - -**Note:** `src/lib/bluesky.ts` (created in Phase 1) already has `buildFacets()` and `postToBluesky()` which can be reused. The main new work is `composeGameResultPost()` and the editable textarea UI on the game page. - ---- - -## Phase 6: Polish - -Priority: **Lower**. Quality-of-life improvements that make gameplay feel complete. - -### 6a. Sound Effects - -- Move sound (piece placement) -- Capture sound -- Check sound -- Game over sound (checkmate, draw) -- Opponent move notification sound - -Use small audio files (MP3/OGG). Lichess sounds are BSD-licensed and could be used directly. - -### 6b. Rematch Button - -After a game ends, show a "Rematch" button that creates a new game with the same opponent, colors swapped. - -- Creates a new `blue.checkmate.game` record with White/Black reversed. -- The rematch is essentially a new challenge to the same opponent, auto-linked. -- Could use the challenge record to track the rematch offer, or just create the game directly and show the link. - -### 6c. PGN Export & Analysis - -- "Download PGN" button on completed (or in-progress) games. -- "Analyze on Lichess" button that opens the Lichess analysis board with the game's PGN. -- Lichess analysis URL: `https://lichess.org/analysis/pgn/{url-encoded-pgn}` - -### 6d. Abandoned Game Detection - -- If a game has been inactive for a configurable period (e.g., 7 days), show an "Abandon" option to the waiting player. -- The abandoning player writes `status: abandoned` to their record. -- Could also auto-mark games as abandoned based on `lastMoveAt` (requires the lexicon change from Phase 4). -- No server-side enforcement -- this is a convention. A bot (separate project) could handle automated cleanup. - -### 6e. Mobile PWA - -- Add a `manifest.json` with app name, icons (from Phase 2a logo), theme color. -- Add a service worker for offline shell caching. -- The static SvelteKit build is already most of the way there. - ---- - -## Phase 7: Open Challenge Board (Deferred) - -Priority: **Lower**. Allow players to browse and accept open challenges from anyone, not just targeted invites. - -- Add a public list of open challenges to the homepage via Constellation queries. -- Requires investigation into whether Constellation supports global collection queries (not just DID-targeted). -- Display open challenges with challenger handle, creation time. -- Filter out expired challenges client-side (e.g., older than 24h). -- Auto-expiry is a convention (UI hides old challenges), not enforced server-side. - ---- - -## Separate Project: checkmate.blue Bot - -**Not part of this project.** Tracked here for reference. - -A Jetstream listener service that watches `blue.checkmate.game` records and posts game results from a `@checkmate.blue` bot account. Separate repo, separate deployment, runs as a persistent process (not a static site). - -Responsibilities: -- Watch for `status: completed` game records on Jetstream. -- Post game results to the bot's Bluesky feed. -- Potentially: abandoned game detection, global game indexing, rating calculation. - ---- - -## Lexicon Changes Summary - -Changes needed across the remaining roadmap: - -| Field | Lexicon | Phase | Status | -|-------|---------|-------|--------| -| `challengerColor` | `blue.checkmate.challenge` | 1c | DONE | -| `lastMoveAt` | `blue.checkmate.game` | 4a | Pending | - ---- - -## Dependency Graph - -``` -Phase 1 (Challenge Invite Flow) ── DONE -Phase 3 (Spectator Mode) ── DONE - -Phase 2a (Logo) ─────────────────── prerequisite for 2b -Phase 2b (OG Tags) ─────────────── benefits 5 - -Phase 4a (Active Games List) ────── requires lexicon change (lastMoveAt) -Phase 4b (Completed Games Feed) ── same query infrastructure as 4a - -Phase 5 (Game Result Sharing) ──── benefits from 2b (OG tags make shared links look good) - reuses bluesky.ts from Phase 1 - -Phase 6 (Polish) ───────────────── independent, can be interleaved anywhere - -Phase 7 (Open Challenge Board) ─── deferred, depends on Constellation global query support -``` diff --git a/SPEC.md b/SPEC.md index f017f10..883625d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2,7 +2,7 @@ ## Overview -**checkmate.blue** is a federated chess platform built entirely on AT Protocol. Players authenticate with their Bluesky identity, play real-time 1v1 chess with game state stored as atproto records, and receive move updates via Jetstream. There is no application database and no server-side game logic — the browser is the app, the PDS is the database, and the protocol is the infrastructure. +**checkmate.blue** is a federated chess platform built on the ATmosphere. Players authenticate with their ATmosphere account, play real-time 1v1 chess with game state stored as AT Protocol records, and receive move updates via Jetstream. There is no application database and no server-side game logic -- the browser is the app, the PDS is the database, and the protocol is the infrastructure. **Target:** Working PoC in 2 days. @@ -240,7 +240,7 @@ Created when a player wants to start a game. Contains a reference to who they're ## Authentication -Use `@atproto/oauth-client-browser` for fully client-side OAuth. Sessions are stored in the browser's IndexedDB — no server-side session management needed. +Uses `@atproto/oauth-client-browser` for fully client-side ATmosphere OAuth. Sessions are stored in the browser's IndexedDB -- no server-side session management needed. ### Setup @@ -264,15 +264,15 @@ export const oauthClient = new BrowserOAuthClient({ }); ``` -For local development, use the loopback client pattern as specified in the atproto OAuth spec — `http://localhost` with any port is treated as a special development client that doesn't require a publicly accessible client-metadata endpoint. +For local development, use the loopback client pattern as specified in the AT Protocol OAuth spec -- `http://localhost` with any port is treated as a special development client that doesn't require a publicly accessible client-metadata endpoint. ### Flow -1. User clicks "Sign in with Bluesky" and enters their handle -2. `oauthClient.signIn(handle)` redirects to their PDS +1. User clicks "Sign in" and enters their ATmosphere handle +2. `oauthClient.signIn(handle)` redirects to their PDS for authorization 3. User authorizes, PDS redirects back to `/oauth/callback` 4. `oauthClient.init()` on page load restores sessions from IndexedDB -5. Create an `Agent` from the session for all atproto operations +5. Create an `Agent` from the session for all AT Protocol operations ```typescript import { Agent } from '@atproto/api'; @@ -284,7 +284,7 @@ const agent = new Agent(session); ### Client Metadata Endpoint -Serve a static JSON file at `/oauth/client-metadata.json`. This must be publicly accessible and match the `client_id` URL exactly. In SvelteKit, create this as a server route or static file. +Serve a static JSON file at `/oauth/client-metadata.json`. This must be publicly accessible and match the `client_id` URL exactly. In SvelteKit, this is handled as a prerendered server route. --- @@ -375,7 +375,7 @@ Each player updates THEIR OWN record with the full PGN after making a move. The 7. Player B makes their move, writes to their own record 8. Player A receives via Jetstream, validates, updates board -**Why two records?** Because atproto permissions are per-user. You can only write to your own repo. This is the natural pattern — each player maintains their own copy of the game state, and the protocol ensures both can be read by anyone. +**Why two records?** Because AT Protocol permissions are per-user. You can only write to your own repo. This is the natural pattern -- each player maintains their own copy of the game state, and the protocol ensures both can be read by anyone. ### 4. Game Over @@ -391,7 +391,7 @@ Draw offer: could be a field on the record (`drawOffered: true`) that the oppone ## Jetstream Integration -Jetstream delivers real-time events from the atproto firehose over WebSocket. The client connects directly to Jetstream and filters for the opponent's record updates. +Jetstream delivers real-time events from the AT Protocol firehose over WebSocket. The client connects directly to Jetstream and filters for the opponent's record updates. ### Connection @@ -488,7 +488,7 @@ Useful for displaying opponent handles/avatars without querying the PDS directly ### `/` — Landing Page - Logo/mascot placeholder + tagline -- "Sign in with Bluesky" button +- "Sign in" button (ATmosphere OAuth) - If authenticated: player handle, avatar, quick actions - Active games list (fetched from Constellation or direct PDS query) @@ -588,7 +588,7 @@ checkmate-blue/ │ │ │ ├── GameControls.svelte # Resign, draw offer buttons │ │ │ ├── PromotionModal.svelte │ │ │ ├── MoveList.svelte # PGN move list display -│ │ │ └── LoginButton.svelte # "Sign in with Bluesky" +│ │ │ └── LoginButton.svelte # ATmosphere sign-in │ │ └── types.ts # Shared TypeScript types │ ├── routes/ │ │ ├── +layout.svelte # Global layout, auth init @@ -634,19 +634,19 @@ Dark theme, blue accents. Minimal, clean, mobile-first. ### Mascot / Logo -Goose wearing a chess crown — inspired by ATmosphereConf's "Goodstuff Goosetopher." Art to be provided separately (permission pending from original artist). Design all layout slots to accommodate a square mascot image at 48x48 (nav), 128x128 (hero), 32x32 (favicon). +`#checkmate` wordmark with blue hash symbol. The `#` is the chess notation for checkmate and doubles as a hashtag. Path-based SVG favicon with no font dependency. ### Color Palette ```css :root { - --bg-primary: #0f1419; + --bg-primary: #0B0F14; --bg-secondary: #1a2332; --bg-board: #2a3a4a; --accent-blue: #1d9bf0; --accent-blue-hover: #1a8cd8; - --text-primary: #e7e9ea; - --text-secondary: #71767b; + --text-primary: #e6edf3; + --text-secondary: #8b9098; --success: #00ba7c; --danger: #f4212e; --warning: #ffd400; @@ -704,6 +704,7 @@ System sans-serif stack for body and headings. Monospace (`JetBrains Mono` or sy - **PDS rate limits.** Writing one record per move is fine. A typical game is 40 moves, so 40 `putRecord` calls spread over minutes to hours. Well within any rate limit. - **Offline / disconnect.** If a player closes their browser, the game state is on the PDS. They can reopen the game URL, read the current PGN, and continue. No session to restore. - **OAuth token expiry.** `@atproto/oauth-client-browser` handles token refresh automatically via IndexedDB. Shorter token lifetimes (public client) are fine for gameplay sessions. +- **ATmosphere ecosystem.** This app works with any ATmosphere account (any PDS), not just Bluesky. The AT Protocol is the underlying protocol; the ATmosphere is the ecosystem of apps and services built on it. --- @@ -737,14 +738,11 @@ checkmate.blue { - Server-side move validation - ELO rating calculation -- Spectator mode - Tournament system - Full chess clocks / time controls - Anti-cheat - Custom PDS or relay - Chat / messaging -- Sound effects - Custom board themes / piece sets - Move timer / timeout enforcement - Game search / filtering beyond Constellation queries -- Bluesky post integration (sharing game results) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..8b7cf98 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,63 @@ +# checkmate.blue Roadmap + +## Up Next + +### Homepage & Game Discovery (Phase 4) + +Makes the platform feel alive and gives new visitors something to see. + +**Active games list:** Show ongoing games on the homepage sorted by most recent activity. Only canonical records (no `parentGameUri`). Display as cards with player handles, move count, last activity, and a link to spectate. + +**Completed games feed:** Recently finished games with results, player handles, and links to view the final position. + +**Blocker:** Depends on Constellation supporting global collection queries. Current options if that lands: +- Query Constellation for all `blue.checkmate.game` records globally +- Filter to `status: active` (or `completed`) with `parentGameUri` absent +- Sort by `lastMoveAt` + +Without global queries, the alternatives are a known-players list (doesn't scale), a Jetstream firehose client-side index (complex, ephemeral), or a lightweight indexing endpoint (breaks the static SPA constraint). + +### Open Challenge Board (Phase 7) + +Browse and accept open challenges from anyone, not just targeted invites. + +- Public list of open challenges on the homepage via Constellation +- Filter out expired challenges client-side (older than 24h) +- **Same blocker as Phase 4** -- needs Constellation global query support + +### Dynamic OG Tags (Phase 2b) + +Per-route Open Graph meta tags for richer link previews when sharing game URLs. + +- Cloudflare Worker intercepts requests from known bot user agents +- Fetches game/challenge data from the PDS +- Returns minimal HTML with correct `og:title`, `og:description`, `og:image` +- All other requests pass through to the static SPA + +| Route | og:title | og:description | +|-------|----------|---------------| +| `/` | checkmate.blue | Chess on the ATmosphere | +| `/game/{did}/{rkey}` | Chess Game - {white} vs {black} | {status} - {move count} moves | +| `/challenge/{did}/{rkey}` | Chess Challenge from {handle} | {handle} wants to play chess | +| `/profile/{handle}` | {handle} on checkmate.blue | Chess profile | + +**Blocker:** Needs Cloudflare (or equivalent edge worker) set up in front of the static site. + +--- + +## Separate Project: checkmate.blue Bot + +**Not part of this repo.** A Jetstream listener service that watches `blue.checkmate.game` records and posts game results from a `@checkmate.blue` bot account. Separate repo, separate deployment, runs as a persistent process. + +Potential responsibilities: game result posts, abandoned game detection, global game indexing, rating calculation. + +--- + +## Completed + +- **Phase 1: Challenge & Invite Flow** -- Post to Bluesky, DM via clipboard, color selection, check-before-join guard +- **Phase 2a: Branding** -- `#checkmate` wordmark, path-based favicon, PWA icons, OG image with composite board +- **Phase 3: Spectator Mode** -- read-only view, unauthenticated viewing, dual Jetstream, game-full fallback +- **Phase 5: Game Result Sharing** -- editable share-to-Bluesky post with facets, grapheme counting, link card embed +- **Phase 6: Polish** -- sounds, rematch with live notification, PGN export, Lichess analysis, abandon detection, PWA +- **Accessibility** -- WCAG 2.1 AA (focus indicators, ARIA roles, reduced motion, route titles, contrast) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..91d7c8d --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,140 @@ +# How checkmate.blue Works + +checkmate.blue is a real-time chess game built entirely on the ATmosphere. There is no application server, no database, no WebSocket server. The browser is the app, each player's PDS is their database, and AT Protocol is the infrastructure. + +This document walks through how a single move flows through the system -- from one player's browser to the other's. + +## The Setup + +Both players have the game page open in their browsers. Each player has their own `blue.checkmate.game` record on their own PDS (Personal Data Server). These records contain the full PGN (Portable Game Notation) -- the complete move history. The players are connected to each other via Jetstream, a WebSocket service that broadcasts AT Protocol record changes in real-time. + +## Player A Makes a Move + +Player A drags a piece on the board. The app asks chess.js: "is this legal?" If it's a pawn reaching the back rank, a promotion modal appears first. Otherwise, the move is applied to the local chess.js instance immediately -- the board updates before anything hits the network. + +The app generates the updated PGN from chess.js (including standard headers like player DIDs and the event name) and writes it to Player A's own PDS via `putRecord`. This is the only write -- one record update containing the full PGN, the game status, and a timestamp. Player A cannot write to Player B's PDS. AT Protocol enforces this: you can only write to your own repository. + +## The Move Reaches Player B + +Jetstream, which indexes the AT Protocol firehose, sees that Player A's `blue.checkmate.game` record changed. Player B's browser has an open WebSocket to Jetstream filtered to Player A's DID and the `blue.checkmate.game` collection. Jetstream pushes the event. + +Player B's app receives the updated record. It extracts the PGN and compares it to the local chess.js state. If the incoming PGN has more moves (it should -- exactly one more), the app replaces the local chess.js instance with the new PGN. The board updates. A sound plays. + +If Jetstream disconnects (network issues, server restart), the app falls back to polling Player A's PDS directly every 3 seconds until the WebSocket reconnects. Either way, the data source is the same: Player A's record on their PDS. + +## Player B Responds + +Now it's Player B's turn. The same flow happens in reverse. Player B moves, chess.js validates, the PGN is written to Player B's PDS, Jetstream delivers it to Player A. + +## Why Two Records? + +Each player maintains their own copy of the game because AT Protocol is built around personal data repositories -- you own your data, you write your data. There's no shared database to update. This means at any given moment, one record is one move ahead of the other (whichever player moved last). When the game page loads or reloads, the app reads both records and uses the longer PGN, ensuring no moves are lost. + +## When the Game Ends + +If chess.js detects checkmate, stalemate, or a draw condition after a move, the writing player includes `status: "completed"` and the result in their record update. The opponent receives this via Jetstream, verifies the result by replaying the PGN through chess.js, and mirrors the completion to their own record. Resignations and draw agreements follow the same pattern -- the acting player writes to their record, the opponent verifies and syncs. + +## The Flow + +```mermaid +sequenceDiagram + participant A as Player A (Browser) + participant CJ as chess.js + participant A_PDS as Player A's PDS + participant JS as Jetstream + participant B_PDS as Player B's PDS + participant B as Player B (Browser) + + Note over A,B: Both players have the game open.
Each has their own game record on their own PDS.
Both are connected to Jetstream. + + rect rgb(30, 40, 55) + Note over A,CJ: Player A makes a move + A->>CJ: Drag piece (orig, dest) + CJ->>CJ: Is this legal? + CJ-->>A: Yes -- board updates instantly + end + + rect rgb(30, 40, 55) + Note over A,A_PDS: Write to own PDS + A->>A_PDS: putRecord(blue.checkmate.game)
Updated PGN + status + timestamp + Note over A,A_PDS: Player A can only write to
their own repository + end + + rect rgb(40, 35, 50) + Note over A_PDS,B: Move delivered via Jetstream + A_PDS-->>JS: Record change event + JS-->>B: Filtered by Player A's DID
+ blue.checkmate.game collection + end + + rect rgb(30, 40, 55) + Note over B,CJ: Player B receives the move + B->>CJ: Compare PGN lengths + CJ-->>B: Incoming PGN is longer -- accept + B->>B: Update board + play sound + end + + Note over A,B: Now it's Player B's turn.
The same flow happens in reverse. + + rect rgb(30, 40, 55) + Note over B,CJ: Player B responds + B->>CJ: Drag piece (orig, dest) + CJ->>CJ: Is this legal? + CJ-->>B: Yes -- board updates instantly + end + + rect rgb(30, 40, 55) + Note over B,B_PDS: Write to own PDS + B->>B_PDS: putRecord(blue.checkmate.game)
Updated PGN + status + timestamp + end + + rect rgb(40, 35, 50) + Note over A,B_PDS: Move delivered via Jetstream + B_PDS-->>JS: Record change event + JS-->>A: Filtered by Player B's DID + end + + rect rgb(30, 40, 55) + Note over A,CJ: Player A receives the move + A->>CJ: Compare PGN lengths + CJ-->>A: Incoming PGN is longer -- accept + A->>A: Update board + play sound + end +``` + +## Two Records, One Game + +```mermaid +flowchart TB + subgraph A_PDS["Player A's PDS"] + A_REC["blue.checkmate.game/3abc...

pgn: 1. e4 e5 2. Nf3
status: active
white: did:plc:playerA
black: did:plc:playerB"] + end + + subgraph B_PDS["Player B's PDS"] + B_REC["blue.checkmate.game/3xyz...

pgn: 1. e4 e5 2. Nf3 Nc6
status: active
parentGameUri: at://playerA/..."] + end + + A_REC -.-|"Player B's record points
back via parentGameUri"| B_REC + + LOAD["On page load or reconnect:
Read BOTH records.
Use the longer PGN."] + + A_REC --> LOAD + B_REC --> LOAD +``` + +## Jetstream Fallback + +```mermaid +stateDiagram-v2 + [*] --> Connected: WebSocket opens to Jetstream + + Connected --> Connected: Receive opponent's moves + Connected --> Disconnected: WebSocket closes + + Disconnected --> Polling: Start polling opponent's PDS (3s interval) + Disconnected --> Reconnecting: Exponential backoff (1s to 30s) + + Polling --> Connected: WebSocket reopens + Reconnecting --> Connected: WebSocket reopens + + Note right of Polling: Same data source either way --
the opponent's record on their PDS +```