From 3fcb3513fcfa98477ac59d913c7edb3ba2d07640 Mon Sep 17 00:00:00 2001
From: Scott Hadfield
Date: Wed, 15 Apr 2026 18:04:12 -0700
Subject: [PATCH] Clean up lexicons: add variant, remove challengerColor,
enforce opponent
- Add variant field to both game and challenge lexicons/types
- Remove redundant challengerColor from challenge (color is already
determined by the game record white/black fields)
- Enforce opponent check on challenge acceptance so directed challenges
can only be accepted by the intended player
- Sync SPEC.md lexicon blocks with actual lexicon files (add lastMoveAt,
variant; fix resultReason values)
---
SPEC.md | 17 ++++++++++++++++-
lexicons/README.md | 4 +++-
lexicons/blue.checkmate.challenge.json | 6 +++---
lexicons/blue.checkmate.game.json | 5 +++++
src/lib/types.ts | 3 ++-
src/routes/challenge/[did]/[rkey]/+page.svelte | 11 +++++++++++
src/routes/play/+page.svelte | 5 -----
7 files changed, 40 insertions(+), 11 deletions(-)
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 @@