diff --git a/specs/really-bad-chess.md b/specs/really-bad-chess.md new file mode 100644 index 0000000..e3ca9f6 --- /dev/null +++ b/specs/really-bad-chess.md @@ -0,0 +1,299 @@ +# Really Bad Chess -- Spec + +## Overview + +A chess variant where each side gets a randomized set of pieces instead of the standard arrangement. Pieces are balanced by total material value but asymmetric between sides -- you might get 3 queens and no rooks, your opponent might get 5 knights and 2 bishops. Standard movement rules apply. Kings are mandatory, everything else is up to chance. + +Inspired by Zach Gage's Really Bad Chess. An earlier standalone prototype lives at `/Users/hadsie/Development/really-bad-chess` (React, client-side only). + +## Game Modes + +### Custom Game (human vs human) + +Same flow as standard chess on checkmate.blue, but with a randomized starting position. Either player can create a Really Bad Chess game via the challenge flow. Both players see the same board. The board is generated when the game is created and stored in the game record as a FEN string. Both sides get the same target material value (random in [45, 70]) for a fair match. + +### Custom Game (human vs bot) + +Player challenges one of the bot accounts to a Really Bad Chess game. Player is always white with a material advantage (0-7 points over black). + +### Daily Challenge (human vs bot) + +A new random board is generated each day from a deterministic seed. Every player who plays the daily challenge gets the same board and plays as white against the bot. White has the material advantage as usual. Results can be compared: fewest moves to win, fastest time, or just whether you won or lost. + +**Depends on:** the checkmate.blue bot being deployed and playable. + +--- + +## Board Generation + +### Rules + +Ported from the standalone prototype with one adjustment (seeded PRNG for daily mode). + +**Material values:** P=1, N=3, B=3, R=5, Q=9, K=0 (king is mandatory, not counted) + +### Point Budget + +The total material value accounts for ALL 15 non-king pieces. There is no separate backfill step -- every piece is budgeted. + +**White (human):** +1. Pick a target: random integer in [45, 68] +2. Fill 15 piece slots to reach the target, allowing a variance of +/- 5 (actual total lands in [target - 5, target + 5], clamped to [40, 73]) + +**Black (computer/opponent):** +1. Pick a disadvantage: random percentage in [0%, 13%] +2. Black's target = floor(whiteActualTotal * (1 - disadvantage)) +3. Fill 15 piece slots with the same variance of +/- 5 + +This means black is between 0% and 13% weaker than white. At the extremes: if white has 73 points and black has 0% disadvantage, it's a fair fight. If white has 73 and black has 13% disadvantage, black gets ~63 points. + +For reference, standard chess material is 39 per side (Q=9, 2R=10, 2B=6, 2N=6, 8P=8). So even the low end (~40) is stronger than a standard army. + +For human vs human custom games, both sides use the same target with 0% disadvantage. + +### Piece Generation + +**Per side (15 pieces + 1 king = 16 total):** + +1. White is guaranteed at least 1 queen +2. Each side gets 1-5 pawns (random cap per side) +3. Fill all 15 slots by picking random piece types, tracking the remaining budget +4. As slots fill up, constrain choices so the remaining budget can be spent across the remaining slots (minimum 1 point per remaining slot for pawns, maximum 9 for queens) +5. The +/- 5 variance means the algorithm doesn't need to hit the target exactly -- it just needs to land within 5 points, which keeps generation simple and avoids awkward forced fills at the end + +### Placement + +1. Kings on their standard starting squares: white king on e1, black king on e8 +2. Pawns CANNOT go on ranks 1/8 (chess.js rejects this as invalid FEN). Pawns go on rank 2 (white) / rank 7 (black) only. +3. Back rank (7 slots after king): queens first, then other non-pawn pieces (shuffled) +4. Second rank (8 slots): overflow non-pawn pieces, then pawns + +### FEN Generation + +Convert the board to a FEN string with: +- Castling rights: `-` (no castling -- pieces aren't in standard positions) +- En passant: `-` +- Halfmove clock: `0` +- Fullmove number: `1` +- Active color: `w` + +Example: `qnQbKQnr/BNNBRPPP/8/8/8/8/nnbrqppp/rnqbkbnr w - - 0 1` + +### Seeded PRNG (Daily Challenge) + +For the daily challenge, all randomness must be deterministic from a date seed: + +1. Seed string: `"really-bad-chess-YYYY-MM-DD"` (e.g., `"really-bad-chess-2026-04-11"`) +2. Hash the seed to a 32-bit integer (simple string hash or use a library) +3. Use a seeded PRNG (e.g., mulberry32, xoshiro128) for all random calls in board generation +4. The resulting FEN is identical for every player on the same day + +For custom games, use `Math.random()` (or `crypto.getRandomValues` for better entropy). + +--- + +## Lexicon Changes + +**`blue.checkmate.game`** -- two new optional fields: + +```json +"variant": { + "type": "string", + "knownValues": ["standard", "really-bad-chess", "chess960"], + "description": "Chess variant for this game. Omit for standard chess." +}, +"startingFen": { + "type": "string", + "maxLength": 200, + "description": "FEN of the starting position for non-standard variants. Omit for standard chess." +} +``` + +**`blue.checkmate.challenge`** -- one new optional field: + +```json +"variant": { + "type": "string", + "knownValues": ["standard", "really-bad-chess", "chess960"], + "description": "Chess variant for this challenge. Omit for standard chess." +} +``` + +Both fields are omitted for standard chess games (backward compatible). The challenge record carries the variant so the opponent knows what they're accepting before viewing the game. The `startingFen` lives only on the game record since that's where both clients read it. + +--- + +## Chess Rules + +Standard chess movement rules apply, with these exceptions/clarifications: + +- **No castling.** The FEN has `-` for castling rights. Kings and rooks are not in standard positions. +- **Pawn promotion works normally.** If a pawn reaches the back rank, it promotes. chess.js handles this out of the box. +- **En passant works normally.** chess.js handles this from the FEN. +- **Threefold repetition, fifty-move rule, stalemate, insufficient material** -- all handled by chess.js as usual. +- **Check, checkmate** -- standard rules, handled by chess.js. + +The standalone prototype didn't implement castling, en passant, or promotion because it used a custom move engine. checkmate.blue uses chess.js, which handles all of these automatically from any valid FEN. + +--- + +## chess.js Integration + +chess.js can load an arbitrary FEN via its constructor: + +```typescript +const chess = new Chess(startingFen); +``` + +All subsequent move validation, game-over detection, and PGN generation work normally from any legal FEN. The PGN will include a `[FEN "..."]` header and a `[SetUp "1"]` header, which is standard PGN for games starting from non-standard positions. + +### game.svelte.ts changes + +`init()` currently creates a `new Chess()` (standard position) and optionally loads a PGN. For Really Bad Chess: + +```typescript +init(options: { + pgn?: string; + startingFen?: string; // NEW + myColor?: PlayerColor; + // ... +}) { + chess = options.startingFen ? new Chess(options.startingFen) : new Chess(); + if (options.pgn) { + chess.loadPgn(options.pgn); + } + // ... +} +``` + +If both `startingFen` and `pgn` are provided (game in progress), load the PGN -- it already contains the `[FEN]` header, so chess.js reconstructs the correct position. + +### game-logic.ts changes + +`makePgn()` needs to include the `[FEN]` and `[SetUp]` headers when a starting FEN is present: + +```typescript +export function makePgn(chess: Chess, white?: string, black?: string, startingFen?: string): string { + // existing headers... + if (startingFen) { + headers.push(['SetUp', '1']); + headers.push(['FEN', startingFen]); + headers.push(['Variant', 'Really Bad Chess']); + } + // ... +} +``` + +--- + +## Game Flow + +### Creating a Really Bad Chess game + +1. On `/play`, add a variant selector (toggle or dropdown): "Standard" | "Really Bad Chess" +2. When "Really Bad Chess" is selected, generate the board FEN client-side before creating the game record +3. Write the game record with `variant: 'really-bad-chess'` and `startingFen: ""` +4. Everything else (challenge flow, Jetstream, spectating) works identically + +### Loading a Really Bad Chess game + +1. `loadGame()` reads the game record +2. If `record.variant === 'really-bad-chess'` and `record.startingFen`, pass the FEN to `game.init()` +3. chess.js loads the position, chessground renders it, game proceeds normally +4. PGN reconciliation between two records works the same -- the PGN contains the FEN header + +### Daily Challenge flow + +1. Homepage or `/play` shows a "Daily Challenge" button (only if the bot is available) +2. Clicking it generates today's FEN from the date seed +3. Creates a game record with the bot as the opponent, `variant: 'really-bad-chess'`, `startingFen` from the seed +4. Player is always white +5. On completion, show result alongside a leaderboard or comparison (future feature) + +--- + +## UI Changes + +### /play page + +Add a variant selector: + +``` +[Standard] [Really Bad Chess] +``` + +When Really Bad Chess is selected, the opponent handle input and color picker still work normally. The FEN is generated on create, not shown to the user beforehand. + +### Game page + +- No changes to the board rendering -- chessground renders whatever position chess.js has +- The variant name could be shown in the game header or status area: "Really Bad Chess" instead of just showing nothing +- Promotion modal works as-is (chess.js triggers it when a pawn reaches the back rank) + +### Homepage + +If the bot is deployed and the daily challenge is available, show a "Daily Challenge" card with today's date. + +--- + +## New Files + +### `src/lib/really-bad-chess.ts` + +Board generation logic. Exports: + +```typescript +// Generate a random Really Bad Chess FEN (for custom games) +export function generateRandomFen(): string + +// Generate today's daily challenge FEN (deterministic) +export function generateDailyFen(date?: Date): string + +// Seeded PRNG (internal) +// Board-to-FEN conversion (internal) +``` + +Port the generation algorithm from the standalone prototype, replacing `Math.random()` calls with a PRNG interface that can be seeded or unseeded. + +--- + +## Files to Modify + +| File | Change | +|------|--------| +| `lexicons/blue.checkmate.game.json` | Add `variant` and `startingFen` fields | +| `src/lib/types.ts` | Add `variant` and `startingFen` to `GameRecord` | +| `src/lib/stores/game.svelte.ts` | Accept `startingFen` in `init()`, pass to Chess constructor | +| `src/lib/game-logic.ts` | `makePgn()` includes FEN/SetUp headers when applicable | +| `src/lib/atproto.ts` | Pass `variant` and `startingFen` through `createGame()` and `updateGame()` | +| `src/routes/play/+page.svelte` | Variant selector UI, generate FEN on create | +| `src/routes/game/[did]/[rkey]/+page.svelte` | Read `variant`/`startingFen` from record, pass to `game.init()` | +| `src/routes/+page.svelte` | Daily challenge card (future, depends on bot) | + +### New file + +| File | Purpose | +|------|---------| +| `src/lib/really-bad-chess.ts` | Board generation, seeded PRNG, FEN conversion | + +--- + +## Verification + +1. **Board generation**: Generate 100 random boards, verify each has exactly 1 king per side (e1 and e8), exactly 15 non-king pieces per side, white material within +/- 5 of target (target in [45, 68]), black material 0-13% less than white's actual total, valid FEN syntax. +2. **chess.js loading**: Load generated FENs into chess.js, verify `isGameOver()` returns false, verify legal moves exist for white. +3. **Daily seed**: Generate today's FEN twice, verify identical. Generate yesterday's FEN, verify different. +4. **Game creation**: Create a Really Bad Chess game, verify `variant` and `startingFen` are written to the PDS record. +5. **Game play**: Play a full game with a Really Bad Chess position -- make moves, verify chess.js validates them correctly, verify PGN includes FEN header. +6. **Spectator**: Open a Really Bad Chess game as a spectator, verify the board renders the custom position correctly. +7. **Pawn promotion**: Move a pawn to the back rank in a Really Bad Chess game, verify the promotion modal appears and works. +8. **Reconciliation**: Reload a Really Bad Chess game mid-play, verify both records are read and the longer PGN is used with the correct starting position. + +--- + +## Out of Scope (for now) + +- Daily challenge leaderboard (needs the bot's API for aggregation) +- Difficulty selection for daily challenge (inherits from which bot handle you challenge) +- Game history filtering by variant +- Rating/ELO for Really Bad Chess games diff --git a/src/lib/atproto.ts b/src/lib/atproto.ts index 2b552ee..893f340 100644 --- a/src/lib/atproto.ts +++ b/src/lib/atproto.ts @@ -28,7 +28,14 @@ async function getPublicAgent(did: string): Promise { export async function createGame( agent: Agent, - options: { white?: string; black?: string; status?: GameRecord['status']; parentGameUri?: string } + options: { + white?: string; + black?: string; + status?: GameRecord['status']; + parentGameUri?: string; + variant?: GameRecord['variant']; + startingFen?: string; + } ): Promise<{ uri: string; rkey: string }> { const record: GameRecord = { $type: 'blue.checkmate.game', @@ -38,6 +45,8 @@ export async function createGame( black: options.black, status: options.status ?? 'waiting', parentGameUri: options.parentGameUri, + variant: options.variant, + startingFen: options.startingFen, }; const response = await agent.com.atproto.repo.createRecord({ diff --git a/src/lib/components/CreateGameForm.svelte b/src/lib/components/CreateGameForm.svelte new file mode 100644 index 0000000..08786a9 --- /dev/null +++ b/src/lib/components/CreateGameForm.svelte @@ -0,0 +1,140 @@ + + +
+
+

{title}

+

{subtitle}

+
+ + {#if !auth.isLoggedIn} +

Sign in to create a game.

+ Go to home + {:else} +
+ + +
+ Play as + {#each [['white', 'White'], ['black', 'Black'], ['random', 'Random']] as [value, label]} + + {/each} +
+ + + + {#if error} +

{error}

+ {/if} +
+ + Back to home + {/if} +
diff --git a/src/lib/game-logic.ts b/src/lib/game-logic.ts index ce86aa7..30ef409 100644 --- a/src/lib/game-logic.ts +++ b/src/lib/game-logic.ts @@ -60,7 +60,7 @@ export function gameResult(chess: Chess): { return null; } -export function makePgn(chess: Chess, white?: string, black?: string): string { +export function makePgn(chess: Chess, white?: string, black?: string, startingFen?: string): string { const headers: [string, string][] = [ ['Event', 'checkmate.blue'], ['Site', 'https://checkmate.blue'], @@ -69,6 +69,11 @@ export function makePgn(chess: Chess, white?: string, black?: string): string { ]; if (white) headers.push(['White', white]); if (black) headers.push(['Black', black]); + if (startingFen) { + headers.push(['SetUp', '1']); + headers.push(['FEN', startingFen]); + headers.push(['Variant', 'Really Bad Chess']); + } const result = gameResult(chess); headers.push(['Result', result?.result ?? '*']); diff --git a/src/lib/really-bad-chess.ts b/src/lib/really-bad-chess.ts new file mode 100644 index 0000000..9c47006 --- /dev/null +++ b/src/lib/really-bad-chess.ts @@ -0,0 +1,219 @@ +const PIECE_VALUES: Record = { P: 1, N: 3, B: 3, R: 5, Q: 9 }; +const PIECE_TYPES = ['Q', 'R', 'B', 'N', 'P'] as const; + +function mulberry32(seed: number): () => number { + return () => { + seed |= 0; + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function hashString(s: string): number { + let hash = 0; + for (let i = 0; i < s.length; i++) { + hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0; + } + return hash; +} + +function randInt(min: number, max: number, rng: () => number): number { + return Math.floor(rng() * (max - min + 1)) + min; +} + +function shuffle(arr: T[], rng: () => number): T[] { + const a = [...arr]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +function generatePieces( + target: number, + requireQueen: boolean, + rng: () => number, +): string[] { + const pieces: string[] = []; + let total = 0; + let pawnCount = 0; + const maxPawns = randInt(1, 5, rng); + + if (requireQueen) { + pieces.push('Q'); + total += PIECE_VALUES.Q; + } + + while (pieces.length < 15) { + const remaining = 15 - pieces.length; + const budget = target - total; + const minNeeded = remaining; // 1 point per remaining slot (all pawns) + const maxPossible = remaining * 9; // all queens + + // If we've overshot or can't spend enough, fill with pawns + if (budget <= minNeeded) { + while (pieces.length < 15) { + pieces.push('P'); + total += 1; + pawnCount++; + } + break; + } + + // If we need to spend a lot, force high-value pieces + if (budget >= maxPossible) { + pieces.push('Q'); + total += PIECE_VALUES.Q; + continue; + } + + // Pick a random piece type that keeps us in a feasible range + const affordable = PIECE_TYPES.filter((p) => { + if (p === 'P' && pawnCount >= maxPawns) return false; + const val = PIECE_VALUES[p]; + const newTotal = total + val; + const newRemaining = 15 - pieces.length - 1; + const newBudget = target - newTotal; + // After placing this piece, can we still fill the rest? + return newBudget >= newRemaining && newBudget <= newRemaining * 9; + }); + + if (affordable.length === 0) { + // Fallback: pawn always works if budget >= remaining + pieces.push('P'); + total += 1; + pawnCount++; + continue; + } + + const piece = affordable[Math.floor(rng() * affordable.length)]; + pieces.push(piece); + total += PIECE_VALUES[piece]; + if (piece === 'P') pawnCount++; + } + + return pieces; +} + +function placePiecesOnRanks(pieces: string[], rng: () => number): string[] { + // Pawns cannot go on ranks 1/8 (chess.js rejects this). + // Split into: queens (placed first on back rank), non-pawn/non-queen, pawns. + const queens = pieces.filter((p) => p === 'Q'); + const pawns = shuffle(pieces.filter((p) => p === 'P'), rng); + const others = shuffle( + pieces.filter((p) => p !== 'Q' && p !== 'P'), + rng, + ); + + // Back rank: 8 squares, king on e-file (index 4). Only non-pawn pieces. + const backRank: string[] = new Array(8).fill(''); + backRank[4] = 'K'; + + let qi = 0; + let oi = 0; + for (let col = 0; col < 8; col++) { + if (col === 4) continue; + if (qi < queens.length) { + backRank[col] = queens[qi++]; + } else if (oi < others.length) { + backRank[col] = others[oi++]; + } + } + + // Second rank: remaining non-pawn pieces overflow, then pawns + const secondRank: string[] = new Array(8).fill(''); + let si = 0; + while (oi < others.length && si < 8) { + secondRank[si++] = others[oi++]; + } + let pi = 0; + while (pi < pawns.length && si < 8) { + secondRank[si++] = pawns[pi++]; + } + + return [...secondRank, ...backRank]; +} + +function boardToFen(whiteRanks: string[], blackRanks: string[]): string { + // whiteRanks: [secondRank(rank2), backRank(rank1)] -- 16 squares + // blackRanks: [secondRank(rank7), backRank(rank8)] -- 16 squares + const board: string[][] = Array.from({ length: 8 }, () => + new Array(8).fill(''), + ); + + // White: rank 1 = row 7 (FEN goes from rank 8 to rank 1, top to bottom) + // rank 1 (back rank) = board[7], rank 2 = board[6] + for (let col = 0; col < 8; col++) { + const backPiece = whiteRanks[8 + col]; // back rank + const secondPiece = whiteRanks[col]; // second rank + if (backPiece) board[7][col] = backPiece.toUpperCase(); + if (secondPiece) board[6][col] = secondPiece.toUpperCase(); + } + + // Black: rank 8 (back rank) = board[0], rank 7 = board[1] + for (let col = 0; col < 8; col++) { + const backPiece = blackRanks[8 + col]; // back rank + const secondPiece = blackRanks[col]; // second rank + if (backPiece) board[0][col] = backPiece.toLowerCase(); + if (secondPiece) board[1][col] = secondPiece.toLowerCase(); + } + + const ranks = board.map((row) => { + let fen = ''; + let empty = 0; + for (const cell of row) { + if (!cell) { + empty++; + } else { + if (empty > 0) { + fen += empty; + empty = 0; + } + fen += cell; + } + } + if (empty > 0) fen += empty; + return fen; + }); + + return ranks.join('/') + ' w - - 0 1'; +} + +function generateBoard(rng: () => number, humanVsHuman: boolean): string { + const whiteTarget = randInt(45, 68, rng); + const whitePieces = generatePieces(whiteTarget, true, rng); + const whiteTotal = whitePieces.reduce((s, p) => s + PIECE_VALUES[p], 0); + + let blackTarget: number; + if (humanVsHuman) { + blackTarget = whiteTarget; + } else { + const disadvantage = rng() * 0.13; + blackTarget = Math.floor(whiteTotal * (1 - disadvantage)); + } + + const blackPieces = generatePieces(blackTarget, false, rng); + + const whiteRanks = placePiecesOnRanks(whitePieces, rng); + const blackRanks = placePiecesOnRanks(blackPieces, rng); + + return boardToFen(whiteRanks, blackRanks); +} + +/** Generate a random Really Bad Chess FEN for human-vs-human games (equal material). */ +export function generateRandomFen(): string { + const rng = () => Math.random(); + return generateBoard(rng, true); +} + +/** Generate today's daily challenge FEN (deterministic from date). */ +export function generateDailyFen(date?: Date): string { + const d = date ?? new Date(); + const dateStr = d.toISOString().slice(0, 10); + const seed = hashString(`really-bad-chess-${dateStr}`); + const rng = mulberry32(seed); + return generateBoard(rng, false); +} diff --git a/src/lib/stores/game.svelte.ts b/src/lib/stores/game.svelte.ts index 0f17a1e..1d5a9c2 100644 --- a/src/lib/stores/game.svelte.ts +++ b/src/lib/stores/game.svelte.ts @@ -16,6 +16,7 @@ let pendingPromotion: { orig: string; dest: string } | null = $state(null); let drawOfferedByMe = $state(false); let drawOfferedByOpponent = $state(false); let storedResult: { result: '1-0' | '0-1' | '1/2-1/2'; reason: string } | null = $state(null); +let startingFen: string | undefined = $state(undefined); export const game = { get chess() { return chess; }, @@ -37,9 +38,11 @@ export const game = { get pendingPromotion() { return pendingPromotion; }, get drawOfferedByMe() { return drawOfferedByMe; }, get drawOfferedByOpponent() { return drawOfferedByOpponent; }, + get startingFen() { return startingFen; }, init(options: { pgn?: string; + startingFen?: string; myColor?: PlayerColor; whiteHandle?: string; blackHandle?: string; @@ -47,7 +50,8 @@ export const game = { blackDid?: string; status?: GameRecord['status']; }) { - chess = new Chess(); + chess = options.startingFen ? new Chess(options.startingFen) : new Chess(); + startingFen = options.startingFen; if (options.pgn) { chess.loadPgn(options.pgn); } @@ -61,7 +65,7 @@ export const game = { drawOfferedByMe = false; drawOfferedByOpponent = false; storedResult = null; - }, + }, // end init tryMove(orig: string, dest: string): boolean { if (!this.isMyTurn) return false; @@ -138,5 +142,6 @@ export const game = { drawOfferedByMe = false; drawOfferedByOpponent = false; storedResult = null; + startingFen = undefined; }, }; diff --git a/src/routes/game/[did]/[rkey]/+page.svelte b/src/routes/game/[did]/[rkey]/+page.svelte index 2ebcacc..bb6c58b 100644 --- a/src/routes/game/[did]/[rkey]/+page.svelte +++ b/src/routes/game/[did]/[rkey]/+page.svelte @@ -20,6 +20,7 @@ import { JetstreamConnection } from '$lib/jetstream'; import { resolveIdentity } from '$lib/microcosm'; import { composeChallengePost, composeGameResultPost, openBlueskyCompose, graphemeCount } from '$lib/bluesky'; + import { generateRandomFen } from '$lib/really-bad-chess'; import LoginButton from '$lib/components/LoginButton.svelte'; import type { PieceSymbol } from 'chess.js'; @@ -147,6 +148,7 @@ game.init({ pgn: record.pgn || undefined, + startingFen: record.startingFen, myColor, whiteDid: record.white, blackDid: record.black, @@ -212,7 +214,7 @@ if (game.status === 'active' && game.result && auth.agent && myRkey) { game.setStatus('completed'); await updateGame(auth.agent, myRkey, { - pgn: makePgn(game.chess, game.whiteDid, game.blackDid), + pgn: makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen), status: 'completed', result: game.result.result, resultReason: game.result.reason, @@ -242,7 +244,7 @@ if (auth.agent && myRkey) { game.setStatus('completed'); game.setResult('1/2-1/2', 'agreement'); - const finalPgn = makePgn(game.chess, game.whiteDid, game.blackDid); + const finalPgn = makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); await updateGame(auth.agent, myRkey, { pgn: finalPgn, status: 'completed', @@ -261,7 +263,7 @@ if (verified && auth.agent && myRkey) { game.setStatus('completed'); game.setResult(verified.result as '1-0' | '0-1' | '1/2-1/2', verified.resultReason); - const finalPgn = makePgn(game.chess, game.whiteDid, game.blackDid); + const finalPgn = makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); await updateGame(auth.agent, myRkey, { pgn: finalPgn, status: 'completed', @@ -359,7 +361,7 @@ game.setResult('1/2-1/2', 'agreement'); game.setStatus('completed'); if (auth.agent && myRkey) { - const finalPgn = pgn || makePgn(game.chess, game.whiteDid, game.blackDid); + const finalPgn = pgn || makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); await updateGame(auth.agent, myRkey, { pgn: finalPgn, status: 'completed', @@ -374,7 +376,7 @@ if (verified && auth.agent && myRkey) { game.setStatus('completed'); game.setResult(verified.result as '1-0' | '0-1' | '1/2-1/2', verified.resultReason); - const finalPgn = pgn || makePgn(game.chess, game.whiteDid, game.blackDid); + const finalPgn = pgn || makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); await updateGame(auth.agent, myRkey, { pgn: finalPgn, status: 'completed', @@ -450,6 +452,8 @@ black, status: 'active', parentGameUri: parentUri, + variant: record.variant, + startingFen: record.startingFen, }); myRkey = result.rkey; game.setStatus('active'); @@ -488,7 +492,7 @@ async function writeMove() { if (!auth.agent || !auth.did || !myRkey) return; - const pgn = makePgn(game.chess, game.whiteDid, game.blackDid); + const pgn = makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); const result = gameResult(game.chess); try { await updateGame(auth.agent, myRkey, { @@ -512,6 +516,7 @@ moveError = 'Move failed to save. Your last move has been undone.'; game.init({ pgn: lastPersistedPgn || undefined, + startingFen: game.startingFen, myColor: game.myColor, whiteDid: game.whiteDid, blackDid: game.blackDid, @@ -546,7 +551,7 @@ game.setStatus('completed'); game.setResult('1/2-1/2', 'agreement'); game.clearDrawOffers(); - const pgn = makePgn(game.chess, game.whiteDid, game.blackDid); + const pgn = makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); await updateGame(auth.agent, myRkey, { pgn, status: 'completed', @@ -571,10 +576,13 @@ rematchCreating = true; try { const opponentDid = game.myColor === 'white' ? game.blackDid : game.whiteDid; + const isVariant = game.startingFen != null; const result = await createGame(auth.agent, { white: opponentDid, black: auth.did, status: 'waiting', + variant: isVariant ? 'really-bad-chess' : undefined, + startingFen: isVariant ? generateRandomFen() : undefined, }); goto(`/game/${auth.did}/${result.rkey}`); } catch (e) { @@ -584,7 +592,7 @@ } function downloadPgn() { - const pgn = makePgn(game.chess, game.whiteDid, game.blackDid); + const pgn = makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); const blob = new Blob([pgn], { type: 'application/x-chess-pgn' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -597,7 +605,7 @@ async function openLichessAnalysis() { analyzingOnLichess = true; try { - const pgn = makePgn(game.chess, game.whiteDid, game.blackDid); + const pgn = makePgn(game.chess, game.whiteDid, game.blackDid, game.startingFen); const response = await fetch('https://lichess.org/api/import', { method: 'POST', headers: { diff --git a/src/routes/play/+page.svelte b/src/routes/play/+page.svelte index 23b6a92..592bf28 100644 --- a/src/routes/play/+page.svelte +++ b/src/routes/play/+page.svelte @@ -1,126 +1,12 @@ New Game | checkmate.blue -
-
-

New Game

-

Challenge someone or create an open game

-
- - {#if !auth.isLoggedIn} -

Sign in to create a game.

- Go to home - {:else} -
- - -
- Play as - {#each [['white', 'White'], ['black', 'Black'], ['random', 'Random']] as [value, label]} - - {/each} -
- - - - {#if error} -

{error}

- {/if} -
- - Back to home - {/if} -
+ diff --git a/src/routes/play/really-bad-chess/+page.svelte b/src/routes/play/really-bad-chess/+page.svelte new file mode 100644 index 0000000..7cc2885 --- /dev/null +++ b/src/routes/play/really-bad-chess/+page.svelte @@ -0,0 +1,15 @@ + + + + Really Bad Chess | checkmate.blue + + +