diff --git a/SPEC.md b/SPEC.md index 883625d..a8b419e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -165,7 +165,12 @@ A single record represents an entire game. Both players maintain their own copy }, "resultReason": { "type": "string", - "knownValues": ["checkmate", "resignation", "draw_agreement", "stalemate", "insufficient", "repetition", "fifty_moves"] + "knownValues": ["checkmate", "resignation", "agreement", "stalemate", "insufficient", "repetition", "fifty_moves", "abandonment"] + }, + "lastMoveAt": { + "type": "string", + "format": "datetime", + "description": "Timestamp of the most recent move" }, "parentGameUri": { "type": "string", @@ -176,6 +181,11 @@ A single record represents an entire game. Both players maintain their own copy "type": "boolean", "description": "Whether a draw has been offered by the last moving player" }, + "variant": { + "type": "string", + "knownValues": ["standard", "really-bad-chess", "chess960"], + "description": "Chess variant for this game. Omit for standard chess." + }, "timeControl": { "type": "ref", "ref": "#timeControl" @@ -228,6 +238,11 @@ Created when a player wants to start a game. Contains a reference to who they're "status": { "type": "string", "knownValues": ["open", "accepted", "expired", "cancelled"] + }, + "variant": { + "type": "string", + "knownValues": ["standard", "really-bad-chess", "chess960"], + "description": "Chess variant for this challenge. Omit for standard chess." } } } diff --git a/lexicons/README.md b/lexicons/README.md index 7e26037..9449b21 100644 --- a/lexicons/README.md +++ b/lexicons/README.md @@ -8,8 +8,10 @@ AT Protocol lexicon definitions for checkmate.blue. Namespace: `blue.checkmate.* Chess game record. Both players maintain their own copy -- White's is the primary, Black's links back via `parentGameUri`. PGN is the source of truth for game state; updated via `putRecord` on each move. +The `variant` field distinguishes non-standard games (e.g. `really-bad-chess`). For variant games with custom starting positions, the FEN is carried in the PGN headers (`[SetUp "1"]` / `[FEN "..."]`), not as a separate record field. + `timeControl` and `moveTimes` are defined but not yet implemented. All games are currently untimed. ### `blue.checkmate.challenge` -Challenge to play. Can target a specific opponent (by DID) or be left open. Once accepted, `gameUri` is set and `status` moves to `accepted`. +Challenge to play. Can target a specific opponent (by DID) or be left open. Directed challenges are enforced -- only the named opponent can accept. Once accepted, `gameUri` is set and `status` moves to `accepted`. The `variant` field carries through to the game so the opponent knows what they're accepting. diff --git a/lexicons/blue.checkmate.challenge.json b/lexicons/blue.checkmate.challenge.json index 1fb5117..c0d2148 100644 --- a/lexicons/blue.checkmate.challenge.json +++ b/lexicons/blue.checkmate.challenge.json @@ -29,10 +29,10 @@ "knownValues": ["open", "accepted", "expired", "cancelled"], "description": "Current challenge status" }, - "challengerColor": { + "variant": { "type": "string", - "knownValues": ["white", "black"], - "description": "Color the challenger will play" + "knownValues": ["standard", "really-bad-chess", "chess960"], + "description": "Chess variant for this challenge. Omit for standard chess." } } } diff --git a/lexicons/blue.checkmate.game.json b/lexicons/blue.checkmate.game.json index 677c36e..b4be160 100644 --- a/lexicons/blue.checkmate.game.json +++ b/lexicons/blue.checkmate.game.json @@ -82,6 +82,11 @@ "type": "boolean", "description": "Whether a draw has been offered by the last moving player" }, + "variant": { + "type": "string", + "knownValues": ["standard", "really-bad-chess", "chess960"], + "description": "Chess variant for this game. Omit for standard chess." + }, "timeControl": { "type": "ref", "ref": "#timeControl" diff --git a/src/lib/types.ts b/src/lib/types.ts index af23c76..153935a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -18,6 +18,7 @@ export interface GameRecord { lastMoveAt?: string; parentGameUri?: string; drawOffered?: boolean; + variant?: 'standard' | 'really-bad-chess' | 'chess960'; timeControl?: TimeControl; moveTimes?: number[]; } @@ -29,7 +30,7 @@ export interface ChallengeRecord { opponent?: string; gameUri?: string; status: 'open' | 'accepted' | 'expired' | 'cancelled'; - challengerColor?: 'white' | 'black'; + variant?: 'standard' | 'really-bad-chess' | 'chess960'; } export type PlayerColor = 'white' | 'black'; diff --git a/src/routes/challenge/[did]/[rkey]/+page.svelte b/src/routes/challenge/[did]/[rkey]/+page.svelte index 6e1bd9d..eac6639 100644 --- a/src/routes/challenge/[did]/[rkey]/+page.svelte +++ b/src/routes/challenge/[did]/[rkey]/+page.svelte @@ -41,8 +41,17 @@ loading = false; } + const isDirectedAtSomeoneElse = $derived( + (challenge as ChallengeRecord | null)?.opponent != null && + (challenge as ChallengeRecord | null)?.opponent !== auth.did + ); + async function acceptChallenge() { if (!auth.agent || !auth.did || !challenge?.gameUri) return; + if (isDirectedAtSomeoneElse) { + error = 'This challenge is for a specific player'; + return; + } accepting = true; error = ''; @@ -118,6 +127,8 @@

{/if} + {:else if isDirectedAtSomeoneElse} +

This challenge is for a specific player.

{:else}