Chess on the ATmosphere checkmate.blue
chess
13
fork

Configure Feed

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

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#

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.<br/>Each has their own game record on their own PDS.<br/>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)<br/>Updated PGN + status + timestamp
    Note over A,A_PDS: Player A can only write to<br/>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<br/>+ 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.<br/>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)<br/>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#

flowchart TB
    subgraph A_PDS["Player A's PDS"]
        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"]
    end

    subgraph B_PDS["Player B's PDS"]
        B_REC["blue.checkmate.game/3xyz...<br/><br/>pgn: 1. e4 e5 2. Nf3 Nc6<br/>status: active<br/>parentGameUri: at://playerA/..."]
    end

    A_REC -.-|"Player B's record points<br/>back via parentGameUri"| B_REC

    LOAD["On page load or reconnect:<br/>Read BOTH records.<br/>Use the longer PGN."]

    A_REC --> LOAD
    B_REC --> LOAD

Jetstream Fallback#

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 --<br/>the opponent's record on their PDS