Chess on the ATmosphere checkmate.blue
chess
18
fork

Configure Feed

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

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)

+40 -11
+16 -1
SPEC.md
··· 165 165 }, 166 166 "resultReason": { 167 167 "type": "string", 168 - "knownValues": ["checkmate", "resignation", "draw_agreement", "stalemate", "insufficient", "repetition", "fifty_moves"] 168 + "knownValues": ["checkmate", "resignation", "agreement", "stalemate", "insufficient", "repetition", "fifty_moves", "abandonment"] 169 + }, 170 + "lastMoveAt": { 171 + "type": "string", 172 + "format": "datetime", 173 + "description": "Timestamp of the most recent move" 169 174 }, 170 175 "parentGameUri": { 171 176 "type": "string", ··· 175 180 "drawOffered": { 176 181 "type": "boolean", 177 182 "description": "Whether a draw has been offered by the last moving player" 183 + }, 184 + "variant": { 185 + "type": "string", 186 + "knownValues": ["standard", "really-bad-chess", "chess960"], 187 + "description": "Chess variant for this game. Omit for standard chess." 178 188 }, 179 189 "timeControl": { 180 190 "type": "ref", ··· 228 238 "status": { 229 239 "type": "string", 230 240 "knownValues": ["open", "accepted", "expired", "cancelled"] 241 + }, 242 + "variant": { 243 + "type": "string", 244 + "knownValues": ["standard", "really-bad-chess", "chess960"], 245 + "description": "Chess variant for this challenge. Omit for standard chess." 231 246 } 232 247 } 233 248 }
+3 -1
lexicons/README.md
··· 8 8 9 9 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. 10 10 11 + 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. 12 + 11 13 `timeControl` and `moveTimes` are defined but not yet implemented. All games are currently untimed. 12 14 13 15 ### `blue.checkmate.challenge` 14 16 15 - Challenge to play. Can target a specific opponent (by DID) or be left open. Once accepted, `gameUri` is set and `status` moves to `accepted`. 17 + 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.
+3 -3
lexicons/blue.checkmate.challenge.json
··· 29 29 "knownValues": ["open", "accepted", "expired", "cancelled"], 30 30 "description": "Current challenge status" 31 31 }, 32 - "challengerColor": { 32 + "variant": { 33 33 "type": "string", 34 - "knownValues": ["white", "black"], 35 - "description": "Color the challenger will play" 34 + "knownValues": ["standard", "really-bad-chess", "chess960"], 35 + "description": "Chess variant for this challenge. Omit for standard chess." 36 36 } 37 37 } 38 38 }
+5
lexicons/blue.checkmate.game.json
··· 82 82 "type": "boolean", 83 83 "description": "Whether a draw has been offered by the last moving player" 84 84 }, 85 + "variant": { 86 + "type": "string", 87 + "knownValues": ["standard", "really-bad-chess", "chess960"], 88 + "description": "Chess variant for this game. Omit for standard chess." 89 + }, 85 90 "timeControl": { 86 91 "type": "ref", 87 92 "ref": "#timeControl"
+2 -1
src/lib/types.ts
··· 18 18 lastMoveAt?: string; 19 19 parentGameUri?: string; 20 20 drawOffered?: boolean; 21 + variant?: 'standard' | 'really-bad-chess' | 'chess960'; 21 22 timeControl?: TimeControl; 22 23 moveTimes?: number[]; 23 24 } ··· 29 30 opponent?: string; 30 31 gameUri?: string; 31 32 status: 'open' | 'accepted' | 'expired' | 'cancelled'; 32 - challengerColor?: 'white' | 'black'; 33 + variant?: 'standard' | 'really-bad-chess' | 'chess960'; 33 34 } 34 35 35 36 export type PlayerColor = 'white' | 'black';
+11
src/routes/challenge/[did]/[rkey]/+page.svelte
··· 41 41 loading = false; 42 42 } 43 43 44 + const isDirectedAtSomeoneElse = $derived( 45 + (challenge as ChallengeRecord | null)?.opponent != null && 46 + (challenge as ChallengeRecord | null)?.opponent !== auth.did 47 + ); 48 + 44 49 async function acceptChallenge() { 45 50 if (!auth.agent || !auth.did || !challenge?.gameUri) return; 51 + if (isDirectedAtSomeoneElse) { 52 + error = 'This challenge is for a specific player'; 53 + return; 54 + } 46 55 accepting = true; 47 56 error = ''; 48 57 ··· 118 127 </p> 119 128 {/if} 120 129 </div> 130 + {:else if isDirectedAtSomeoneElse} 131 + <p class="text-text-secondary">This challenge is for a specific player.</p> 121 132 {:else} 122 133 <button 123 134 onclick={acceptChallenge}
-5
src/routes/play/+page.svelte
··· 45 45 status: 'waiting', 46 46 }); 47 47 48 - const challengerColor: 'white' | 'black' = colorChoice === 'random' 49 - ? (white === auth.did ? 'white' : 'black') 50 - : colorChoice; 51 - 52 48 const challengeResult = await createChallenge(auth.agent, { 53 49 opponent: opponentDid, 54 50 }); ··· 56 52 await updateChallenge(auth.agent, challengeResult.rkey, { 57 53 gameUri: gameResult.uri, 58 54 status: 'open', 59 - challengerColor, 60 55 }); 61 56 62 57 goto(`/game/${auth.did}/${gameResult.rkey}`);