Chess on the ATmosphere checkmate.blue
chess
18
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update docs with ATmosphere terminology and current project state

Adopt ATmosphere as the ecosystem name (AT Protocol for the protocol,
Bluesky for the app). Update README with full feature list, SPEC with
current color palette and implemented features, ROADMAP condensed to
remaining work with completed phases collapsed. Add architecture doc
explaining the move flow through AT Protocol.

+239 -257
+1 -1
CLAUDE.md
··· 8 8 9 9 # checkmate.blue 10 10 11 - 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. 11 + 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. 12 12 13 13 See `SPEC.md` for full technical specification. 14 14
+19 -4
README.md
··· 1 1 # checkmate.blue 2 2 3 - 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. 3 + 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. 4 4 5 5 **https://checkmate.blue** 6 6 7 7 ## How it works 8 8 9 - 1. Sign in with your Bluesky account (via AT Protocol OAuth) 9 + 1. Sign in with your ATmosphere account (via AT Protocol OAuth) 10 10 2. Challenge another player by handle or accept an open challenge 11 11 3. Play chess -- moves are written to your PDS as `blue.checkmate.game` records 12 12 4. Your opponent's moves arrive in real-time via [Jetstream](https://docs.bsky.app/blog/jetstream) 13 13 14 14 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. 15 15 16 + ## Features 17 + 18 + - **Real-time play** -- moves delivered via Jetstream WebSocket with polling fallback 19 + - **Challenge system** -- invite by handle, share links, post challenges to Bluesky 20 + - **Spectator mode** -- anyone can watch live games without signing in 21 + - **Draw offers** -- offer, accept, decline, or retract draws mid-game 22 + - **Rematch** -- one-click rematch with live notification to your opponent 23 + - **Game result sharing** -- editable share-to-Bluesky post with @mentions and link cards 24 + - **Abandon detection** -- claim a win after 7 days of opponent inactivity 25 + - **Post-game tools** -- PGN download and Lichess analysis board 26 + - **Sound effects** -- move, capture, check, and game-end sounds 27 + - **PWA** -- installable as a mobile app 28 + - **Accessible** -- WCAG 2.1 AA compliant (focus indicators, screen reader support, reduced motion) 29 + 16 30 ## Stack 17 31 18 32 | Layer | Technology | ··· 23 37 | Auth | [@atproto/oauth-client-browser](https://github.com/bluesky-social/atproto) | 24 38 | Data | [AT Protocol](https://atproto.com) records on each player's PDS | 25 39 | Real-time | [Jetstream](https://docs.bsky.app/blog/jetstream) WebSocket | 40 + | Queries | [Constellation](https://constellation.microcosm.blue) / [Slingshot](https://slingshot.microcosm.blue) | 26 41 | Styling | [Tailwind CSS](https://tailwindcss.com) | 27 42 28 43 ## Development ··· 35 50 npm run check # Type check 36 51 ``` 37 52 38 - The dev server binds to `127.0.0.1` (not `localhost`) because atproto OAuth loopback clients require an IP address. 53 + The dev server binds to `127.0.0.1` (not `localhost`) because AT Protocol OAuth loopback clients require an IP address. 39 54 40 55 ## Lexicons 41 56 ··· 66 81 +-----------------+ +-----------------+ 67 82 ``` 68 83 69 - 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. 84 + 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. 70 85 71 86 ## License 72 87
-234
ROADMAP.md
··· 1 - # checkmate.blue Roadmap 2 - 3 - ## Phase 1: Challenge & Invite Flow -- DONE 4 - 5 - Implemented on branch `phase-1-challenge-invite-flow`. 6 - 7 - - **1a. Post-Based Challenges** -- "Post to Bluesky" button on the game waiting screen with @mention facets and game link. 8 - - **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). 9 - - **1c. Color Selection** -- White/Black/Random picker on `/play`. Game page logic fully decoupled from record ownership. 10 - - **Check-before-join guard** -- re-fetches game record before joining to reduce race conditions on shared links. 11 - 12 - See `plans/phase-1-challenge-invite-flow.md` for full details. 13 - 14 - --- 15 - 16 - ## Phase 3: Spectator Mode -- DONE 17 - 18 - Pulled forward and implemented alongside Phase 1. 19 - 20 - - **Read-only game view** for non-participants with flip-board button. 21 - - **Unauthenticated viewing** -- no login required to watch a game. 22 - - **Dual Jetstream connections** -- spectators receive live updates from both players. 23 - - **Game-full fallback** -- extra visitors become spectators, not errors. 24 - 25 - See `plans/phase-3-spectator-mode.md` for full details. 26 - 27 - --- 28 - 29 - ## Phase 2: Shareability & Branding -- NEXT 30 - 31 - Priority: **High**. Links shared on social platforms currently show bare URLs. A recognizable brand and rich link previews drive organic growth. 32 - 33 - ### 2a. Logo 34 - 35 - FontAwesome chess knight icon in AT Protocol blue (`#0085FF`). 36 - 37 - **Usage:** 38 - - Favicon (SVG) 39 - - Navbar icon (32x32) 40 - - OG image fallback (centered on dark background) 41 - - PWA icon (192x192, 512x512) 42 - 43 - **Implementation:** 44 - - Create an SVG from the FA `chess-knight` glyph, colored `#0085FF`. 45 - - Generate PNG variants at required sizes for PWA manifest and Apple touch icon. 46 - - Replace the current placeholder favicon. 47 - 48 - ### 2b. Open Graph Meta Tags 49 - 50 - Rich link previews when sharing game/challenge URLs on Bluesky, Twitter, Discord, etc. 51 - 52 - **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. 53 - 54 - **Options (in order of preference):** 55 - 56 - 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. 57 - 58 - 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. 59 - 60 - 3. **Static fallback only** -- use the same generic card (logo + "checkmate.blue - Chess on the Atmosphere") for all links. Easiest but least compelling. 61 - 62 - **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. 63 - 64 - **Meta tags for different routes:** 65 - 66 - | Route | og:title | og:description | 67 - |-------|----------|---------------| 68 - | `/` | checkmate.blue | Chess on the Atmosphere | 69 - | `/game/{did}/{rkey}` | Chess Game - {white} vs {black} | {status} - {move count} moves | 70 - | `/challenge/{did}/{rkey}` | Chess Challenge from {handle} | {handle} wants to play chess | 71 - | `/profile/{handle}` | {handle} on checkmate.blue | Chess profile | 72 - 73 - --- 74 - 75 - ## Phase 4: Homepage & Game Discovery 76 - 77 - Priority: **Medium**. Makes the platform feel alive and gives new visitors something to see. 78 - 79 - ### 4a. Active Games List 80 - 81 - Show ongoing games on the homepage, sorted by most recent activity. 82 - 83 - **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. 84 - 85 - **Implementation:** 86 - 87 - - Query approach TBD -- depends on what Constellation supports for global queries. Options: 88 - - Query Constellation for all `blue.checkmate.game` records globally (if supported). 89 - - Maintain a known-players list and query each player's games (doesn't scale). 90 - - Use the Jetstream firehose to build a client-side index of recent games (complex, ephemeral). 91 - - Add a lightweight indexing endpoint (breaks the "no server" constraint). 92 - - Filter to `status: active` and `parentGameUri` absent (White's record only). 93 - - Sort by last activity. **Requires a `lastMoveAt` field on the lexicon** -- PGN parsing for sort order is too expensive for a list view. 94 - 95 - **Lexicon change:** 96 - ``` 97 - "lastMoveAt": { 98 - "type": "string", 99 - "format": "datetime", 100 - "description": "Timestamp of the most recent move" 101 - } 102 - ``` 103 - 104 - This field gets updated on every `putRecord` call when a move is made. 105 - 106 - - Display each game as a card: White handle vs Black handle, move count, last activity time, link to spectate. 107 - 108 - ### 4b. Completed Games Feed 109 - 110 - Below active games, show recently completed games with results. 111 - 112 - - Same query approach as active games, filtered to `status: completed`. 113 - - Show result (1-0, 0-1, draw), result reason, player handles. 114 - - Links to view the final position. 115 - 116 - --- 117 - 118 - ## Phase 5: Game Result Sharing 119 - 120 - Priority: **Medium**. The viral loop. Players share their wins (and losses) to Bluesky. 121 - 122 - **Implementation:** 123 - 124 - - After a game ends (checkmate, resignation, draw), show a "Share to Bluesky" button on the game page. 125 - - Create a `app.bsky.feed.post` record with: 126 - - Text describing the result: "Checkmate! I beat @{opponent} on checkmate.blue" / "Good game -- @{opponent} got me this time" / "Draw with @{opponent} on checkmate.blue" 127 - - Mention facet for the opponent 128 - - Link facet to the game URL 129 - - Embed with game link card (benefits from Phase 2 OG tags) 130 - - Include move count and result reason in the post text for flavor. 131 - - This is always opt-in. Show a pre-filled post that the player can edit before posting. 132 - 133 - **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. 134 - 135 - --- 136 - 137 - ## Phase 6: Polish 138 - 139 - Priority: **Lower**. Quality-of-life improvements that make gameplay feel complete. 140 - 141 - ### 6a. Sound Effects 142 - 143 - - Move sound (piece placement) 144 - - Capture sound 145 - - Check sound 146 - - Game over sound (checkmate, draw) 147 - - Opponent move notification sound 148 - 149 - Use small audio files (MP3/OGG). Lichess sounds are BSD-licensed and could be used directly. 150 - 151 - ### 6b. Rematch Button 152 - 153 - After a game ends, show a "Rematch" button that creates a new game with the same opponent, colors swapped. 154 - 155 - - Creates a new `blue.checkmate.game` record with White/Black reversed. 156 - - The rematch is essentially a new challenge to the same opponent, auto-linked. 157 - - Could use the challenge record to track the rematch offer, or just create the game directly and show the link. 158 - 159 - ### 6c. PGN Export & Analysis 160 - 161 - - "Download PGN" button on completed (or in-progress) games. 162 - - "Analyze on Lichess" button that opens the Lichess analysis board with the game's PGN. 163 - - Lichess analysis URL: `https://lichess.org/analysis/pgn/{url-encoded-pgn}` 164 - 165 - ### 6d. Abandoned Game Detection 166 - 167 - - If a game has been inactive for a configurable period (e.g., 7 days), show an "Abandon" option to the waiting player. 168 - - The abandoning player writes `status: abandoned` to their record. 169 - - Could also auto-mark games as abandoned based on `lastMoveAt` (requires the lexicon change from Phase 4). 170 - - No server-side enforcement -- this is a convention. A bot (separate project) could handle automated cleanup. 171 - 172 - ### 6e. Mobile PWA 173 - 174 - - Add a `manifest.json` with app name, icons (from Phase 2a logo), theme color. 175 - - Add a service worker for offline shell caching. 176 - - The static SvelteKit build is already most of the way there. 177 - 178 - --- 179 - 180 - ## Phase 7: Open Challenge Board (Deferred) 181 - 182 - Priority: **Lower**. Allow players to browse and accept open challenges from anyone, not just targeted invites. 183 - 184 - - Add a public list of open challenges to the homepage via Constellation queries. 185 - - Requires investigation into whether Constellation supports global collection queries (not just DID-targeted). 186 - - Display open challenges with challenger handle, creation time. 187 - - Filter out expired challenges client-side (e.g., older than 24h). 188 - - Auto-expiry is a convention (UI hides old challenges), not enforced server-side. 189 - 190 - --- 191 - 192 - ## Separate Project: checkmate.blue Bot 193 - 194 - **Not part of this project.** Tracked here for reference. 195 - 196 - 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). 197 - 198 - Responsibilities: 199 - - Watch for `status: completed` game records on Jetstream. 200 - - Post game results to the bot's Bluesky feed. 201 - - Potentially: abandoned game detection, global game indexing, rating calculation. 202 - 203 - --- 204 - 205 - ## Lexicon Changes Summary 206 - 207 - Changes needed across the remaining roadmap: 208 - 209 - | Field | Lexicon | Phase | Status | 210 - |-------|---------|-------|--------| 211 - | `challengerColor` | `blue.checkmate.challenge` | 1c | DONE | 212 - | `lastMoveAt` | `blue.checkmate.game` | 4a | Pending | 213 - 214 - --- 215 - 216 - ## Dependency Graph 217 - 218 - ``` 219 - Phase 1 (Challenge Invite Flow) ── DONE 220 - Phase 3 (Spectator Mode) ── DONE 221 - 222 - Phase 2a (Logo) ─────────────────── prerequisite for 2b 223 - Phase 2b (OG Tags) ─────────────── benefits 5 224 - 225 - Phase 4a (Active Games List) ────── requires lexicon change (lastMoveAt) 226 - Phase 4b (Completed Games Feed) ── same query infrastructure as 4a 227 - 228 - Phase 5 (Game Result Sharing) ──── benefits from 2b (OG tags make shared links look good) 229 - reuses bluesky.ts from Phase 1 230 - 231 - Phase 6 (Polish) ───────────────── independent, can be interleaved anywhere 232 - 233 - Phase 7 (Open Challenge Board) ─── deferred, depends on Constellation global query support 234 - ```
+16 -18
SPEC.md
··· 2 2 3 3 ## Overview 4 4 5 - **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. 5 + **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. 6 6 7 7 **Target:** Working PoC in 2 days. 8 8 ··· 240 240 241 241 ## Authentication 242 242 243 - Use `@atproto/oauth-client-browser` for fully client-side OAuth. Sessions are stored in the browser's IndexedDB — no server-side session management needed. 243 + 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. 244 244 245 245 ### Setup 246 246 ··· 264 264 }); 265 265 ``` 266 266 267 - 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. 267 + 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. 268 268 269 269 ### Flow 270 270 271 - 1. User clicks "Sign in with Bluesky" and enters their handle 272 - 2. `oauthClient.signIn(handle)` redirects to their PDS 271 + 1. User clicks "Sign in" and enters their ATmosphere handle 272 + 2. `oauthClient.signIn(handle)` redirects to their PDS for authorization 273 273 3. User authorizes, PDS redirects back to `/oauth/callback` 274 274 4. `oauthClient.init()` on page load restores sessions from IndexedDB 275 - 5. Create an `Agent` from the session for all atproto operations 275 + 5. Create an `Agent` from the session for all AT Protocol operations 276 276 277 277 ```typescript 278 278 import { Agent } from '@atproto/api'; ··· 284 284 285 285 ### Client Metadata Endpoint 286 286 287 - 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. 287 + 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. 288 288 289 289 --- 290 290 ··· 375 375 7. Player B makes their move, writes to their own record 376 376 8. Player A receives via Jetstream, validates, updates board 377 377 378 - **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. 378 + **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. 379 379 380 380 ### 4. Game Over 381 381 ··· 391 391 392 392 ## Jetstream Integration 393 393 394 - 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. 394 + 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. 395 395 396 396 ### Connection 397 397 ··· 488 488 ### `/` — Landing Page 489 489 490 490 - Logo/mascot placeholder + tagline 491 - - "Sign in with Bluesky" button 491 + - "Sign in" button (ATmosphere OAuth) 492 492 - If authenticated: player handle, avatar, quick actions 493 493 - Active games list (fetched from Constellation or direct PDS query) 494 494 ··· 588 588 │ │ │ ├── GameControls.svelte # Resign, draw offer buttons 589 589 │ │ │ ├── PromotionModal.svelte 590 590 │ │ │ ├── MoveList.svelte # PGN move list display 591 - │ │ │ └── LoginButton.svelte # "Sign in with Bluesky" 591 + │ │ │ └── LoginButton.svelte # ATmosphere sign-in 592 592 │ │ └── types.ts # Shared TypeScript types 593 593 │ ├── routes/ 594 594 │ │ ├── +layout.svelte # Global layout, auth init ··· 634 634 635 635 ### Mascot / Logo 636 636 637 - 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). 637 + `#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. 638 638 639 639 ### Color Palette 640 640 641 641 ```css 642 642 :root { 643 - --bg-primary: #0f1419; 643 + --bg-primary: #0B0F14; 644 644 --bg-secondary: #1a2332; 645 645 --bg-board: #2a3a4a; 646 646 --accent-blue: #1d9bf0; 647 647 --accent-blue-hover: #1a8cd8; 648 - --text-primary: #e7e9ea; 649 - --text-secondary: #71767b; 648 + --text-primary: #e6edf3; 649 + --text-secondary: #8b9098; 650 650 --success: #00ba7c; 651 651 --danger: #f4212e; 652 652 --warning: #ffd400; ··· 704 704 - **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. 705 705 - **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. 706 706 - **OAuth token expiry.** `@atproto/oauth-client-browser` handles token refresh automatically via IndexedDB. Shorter token lifetimes (public client) are fine for gameplay sessions. 707 + - **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. 707 708 708 709 --- 709 710 ··· 737 738 738 739 - Server-side move validation 739 740 - ELO rating calculation 740 - - Spectator mode 741 741 - Tournament system 742 742 - Full chess clocks / time controls 743 743 - Anti-cheat 744 744 - Custom PDS or relay 745 745 - Chat / messaging 746 - - Sound effects 747 746 - Custom board themes / piece sets 748 747 - Move timer / timeout enforcement 749 748 - Game search / filtering beyond Constellation queries 750 - - Bluesky post integration (sharing game results)
+63
docs/ROADMAP.md
··· 1 + # checkmate.blue Roadmap 2 + 3 + ## Up Next 4 + 5 + ### Homepage & Game Discovery (Phase 4) 6 + 7 + Makes the platform feel alive and gives new visitors something to see. 8 + 9 + **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. 10 + 11 + **Completed games feed:** Recently finished games with results, player handles, and links to view the final position. 12 + 13 + **Blocker:** Depends on Constellation supporting global collection queries. Current options if that lands: 14 + - Query Constellation for all `blue.checkmate.game` records globally 15 + - Filter to `status: active` (or `completed`) with `parentGameUri` absent 16 + - Sort by `lastMoveAt` 17 + 18 + 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). 19 + 20 + ### Open Challenge Board (Phase 7) 21 + 22 + Browse and accept open challenges from anyone, not just targeted invites. 23 + 24 + - Public list of open challenges on the homepage via Constellation 25 + - Filter out expired challenges client-side (older than 24h) 26 + - **Same blocker as Phase 4** -- needs Constellation global query support 27 + 28 + ### Dynamic OG Tags (Phase 2b) 29 + 30 + Per-route Open Graph meta tags for richer link previews when sharing game URLs. 31 + 32 + - Cloudflare Worker intercepts requests from known bot user agents 33 + - Fetches game/challenge data from the PDS 34 + - Returns minimal HTML with correct `og:title`, `og:description`, `og:image` 35 + - All other requests pass through to the static SPA 36 + 37 + | Route | og:title | og:description | 38 + |-------|----------|---------------| 39 + | `/` | checkmate.blue | Chess on the ATmosphere | 40 + | `/game/{did}/{rkey}` | Chess Game - {white} vs {black} | {status} - {move count} moves | 41 + | `/challenge/{did}/{rkey}` | Chess Challenge from {handle} | {handle} wants to play chess | 42 + | `/profile/{handle}` | {handle} on checkmate.blue | Chess profile | 43 + 44 + **Blocker:** Needs Cloudflare (or equivalent edge worker) set up in front of the static site. 45 + 46 + --- 47 + 48 + ## Separate Project: checkmate.blue Bot 49 + 50 + **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. 51 + 52 + Potential responsibilities: game result posts, abandoned game detection, global game indexing, rating calculation. 53 + 54 + --- 55 + 56 + ## Completed 57 + 58 + - **Phase 1: Challenge & Invite Flow** -- Post to Bluesky, DM via clipboard, color selection, check-before-join guard 59 + - **Phase 2a: Branding** -- `#checkmate` wordmark, path-based favicon, PWA icons, OG image with composite board 60 + - **Phase 3: Spectator Mode** -- read-only view, unauthenticated viewing, dual Jetstream, game-full fallback 61 + - **Phase 5: Game Result Sharing** -- editable share-to-Bluesky post with facets, grapheme counting, link card embed 62 + - **Phase 6: Polish** -- sounds, rematch with live notification, PGN export, Lichess analysis, abandon detection, PWA 63 + - **Accessibility** -- WCAG 2.1 AA (focus indicators, ARIA roles, reduced motion, route titles, contrast)
+140
docs/architecture.md
··· 1 + # How checkmate.blue Works 2 + 3 + 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. 4 + 5 + This document walks through how a single move flows through the system -- from one player's browser to the other's. 6 + 7 + ## The Setup 8 + 9 + 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. 10 + 11 + ## Player A Makes a Move 12 + 13 + 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. 14 + 15 + 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. 16 + 17 + ## The Move Reaches Player B 18 + 19 + 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. 20 + 21 + 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. 22 + 23 + 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. 24 + 25 + ## Player B Responds 26 + 27 + 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. 28 + 29 + ## Why Two Records? 30 + 31 + 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. 32 + 33 + ## When the Game Ends 34 + 35 + 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. 36 + 37 + ## The Flow 38 + 39 + ```mermaid 40 + sequenceDiagram 41 + participant A as Player A (Browser) 42 + participant CJ as chess.js 43 + participant A_PDS as Player A's PDS 44 + participant JS as Jetstream 45 + participant B_PDS as Player B's PDS 46 + participant B as Player B (Browser) 47 + 48 + Note over A,B: Both players have the game open.<br/>Each has their own game record on their own PDS.<br/>Both are connected to Jetstream. 49 + 50 + rect rgb(30, 40, 55) 51 + Note over A,CJ: Player A makes a move 52 + A->>CJ: Drag piece (orig, dest) 53 + CJ->>CJ: Is this legal? 54 + CJ-->>A: Yes -- board updates instantly 55 + end 56 + 57 + rect rgb(30, 40, 55) 58 + Note over A,A_PDS: Write to own PDS 59 + A->>A_PDS: putRecord(blue.checkmate.game)<br/>Updated PGN + status + timestamp 60 + Note over A,A_PDS: Player A can only write to<br/>their own repository 61 + end 62 + 63 + rect rgb(40, 35, 50) 64 + Note over A_PDS,B: Move delivered via Jetstream 65 + A_PDS-->>JS: Record change event 66 + JS-->>B: Filtered by Player A's DID<br/>+ blue.checkmate.game collection 67 + end 68 + 69 + rect rgb(30, 40, 55) 70 + Note over B,CJ: Player B receives the move 71 + B->>CJ: Compare PGN lengths 72 + CJ-->>B: Incoming PGN is longer -- accept 73 + B->>B: Update board + play sound 74 + end 75 + 76 + Note over A,B: Now it's Player B's turn.<br/>The same flow happens in reverse. 77 + 78 + rect rgb(30, 40, 55) 79 + Note over B,CJ: Player B responds 80 + B->>CJ: Drag piece (orig, dest) 81 + CJ->>CJ: Is this legal? 82 + CJ-->>B: Yes -- board updates instantly 83 + end 84 + 85 + rect rgb(30, 40, 55) 86 + Note over B,B_PDS: Write to own PDS 87 + B->>B_PDS: putRecord(blue.checkmate.game)<br/>Updated PGN + status + timestamp 88 + end 89 + 90 + rect rgb(40, 35, 50) 91 + Note over A,B_PDS: Move delivered via Jetstream 92 + B_PDS-->>JS: Record change event 93 + JS-->>A: Filtered by Player B's DID 94 + end 95 + 96 + rect rgb(30, 40, 55) 97 + Note over A,CJ: Player A receives the move 98 + A->>CJ: Compare PGN lengths 99 + CJ-->>A: Incoming PGN is longer -- accept 100 + A->>A: Update board + play sound 101 + end 102 + ``` 103 + 104 + ## Two Records, One Game 105 + 106 + ```mermaid 107 + flowchart TB 108 + subgraph A_PDS["Player A's PDS"] 109 + A_REC["blue.checkmate.game/3abc...<br/><br/>pgn: 1. e4 e5 2. Nf3<br/>status: active<br/>white: did:plc:playerA<br/>black: did:plc:playerB"] 110 + end 111 + 112 + subgraph B_PDS["Player B's PDS"] 113 + B_REC["blue.checkmate.game/3xyz...<br/><br/>pgn: 1. e4 e5 2. Nf3 Nc6<br/>status: active<br/>parentGameUri: at://playerA/..."] 114 + end 115 + 116 + A_REC -.-|"Player B's record points<br/>back via parentGameUri"| B_REC 117 + 118 + LOAD["On page load or reconnect:<br/>Read BOTH records.<br/>Use the longer PGN."] 119 + 120 + A_REC --> LOAD 121 + B_REC --> LOAD 122 + ``` 123 + 124 + ## Jetstream Fallback 125 + 126 + ```mermaid 127 + stateDiagram-v2 128 + [*] --> Connected: WebSocket opens to Jetstream 129 + 130 + Connected --> Connected: Receive opponent's moves 131 + Connected --> Disconnected: WebSocket closes 132 + 133 + Disconnected --> Polling: Start polling opponent's PDS (3s interval) 134 + Disconnected --> Reconnecting: Exponential backoff (1s to 30s) 135 + 136 + Polling --> Connected: WebSocket reopens 137 + Reconnecting --> Connected: WebSocket reopens 138 + 139 + Note right of Polling: Same data source either way --<br/>the opponent's record on their PDS 140 + ```