From 9611c0ae0ecf21d5215e7da729a44dd01d8a80b3 Mon Sep 17 00:00:00 2001 From: "claude (duplicate_chess)" Date: Tue, 19 May 2026 00:08:50 -0400 Subject: [PATCH] init: scaffold duplicate_chess project and design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local browser sandbox for "duplicate chess" — a four-player coupled-board chess variant invented by Andrew Freiberg. Scaffold per CREATE_PROJECT.md plus the approved design spec from this session's brainstorming. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 7 + CLAUDE.md | 44 ++ DECISIONS.md | 31 ++ IDEA.md | 42 ++ .../2026-05-19-duplicate-chess-design.md | 380 ++++++++++++++++++ 5 files changed, 504 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 DECISIONS.md create mode 100644 IDEA.md create mode 100644 docs/superpowers/specs/2026-05-19-duplicate-chess-design.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfd2f1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.superpowers/ +.DS_Store +*.local +*.tsbuildinfo +GITEA_API.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b88c081 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,44 @@ +# Duplicate Chess + +> Local browser sandbox for "duplicate chess" — a four-player coupled-board chess variant. + +## Start Here + +**Read the latest handoff first:** `.claude/handoffs/` (most recent file). +It has session state, in-progress work, and ordered next steps. + +Then check `IDEA.md` for the project brief, `DECISIONS.md` for settled choices, and +`docs/superpowers/specs/2026-05-19-duplicate-chess-design.md` for the full design. + +## Project Identity + +A digitization of "duplicate chess," a four-player chess variant invented by Andrew +Freiberg (Seth's father — also the inventor of the blind-chess variant behind the +sibling `blind_chess` project). Four players (North, South, East, West) and four +boards (NW, NE, SW, SE); each player controls one colour on two boards and must play +the identical algebraic move on both. A captured piece leaves a frozen, capturable +"ghost" twin on the player's other board. Every individual board always remains a +legal game of orthodox chess. + +This project is a **local, single-operator sandbox/visualizer**: one screen, all four +boards, an engine that enforces the synchronized-move coupling, renders ghosts, and +detects the endgame. Its purpose is to let the variant be *seen* and studied — the +inventor's own point is that it cannot be understood from prose alone. + +## Current State + +- **Phase:** spec complete; implementation not started. +- **Repo:** `git.sethpc.xyz/Seth/duplicate_chess`. +- **Deploy target:** n/a — runs locally (`pnpm dev`). +- **Stack:** Vite + Svelte 5 + TypeScript, `chess.js`. Single package; engine in + `src/engine/` (pure, DOM-free, vitest-tested), UI in `src/lib/`. +- **Next:** implementation plan via the writing-plans skill, then build. + +## Conventions + +- Inherits homelab conventions from `~/bin/CLAUDE.md` (gitea CLI, conventional + commits, `.claude/handoffs/` for session state). +- The engine (`src/engine/`) is DOM-free and the single source of truth for game + state. The UI never computes legality itself. +- Provisional endgame rules are marked PROVISIONAL in the spec — they are Claude's + chosen defaults, not Andrew's rulings. Flag them if revisited. diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 0000000..a65ab0e --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1,31 @@ +# DECISIONS.md — Duplicate Chess Decision Log + +Project-specific decisions. For global/cross-cutting decisions, see `~/bin/DECISIONS.md`. + +Format: `YYYY-MM-DD: ` + +## Architecture + +2026-05-19: First deliverable is a local single-operator sandbox/visualizer, not a networked game — the inventor's stated need is to *see* a mock game to comprehend the variant; networked four-player play is a separable later project. +2026-05-19: Single Vite + Svelte 5 + TS app, no server — duplicate chess is perfect-information, so the whole engine and all four boards live client-side. No pnpm workspace (unlike blind_chess, which needs a server as its trusted view boundary). +2026-05-19: Engine lives in a pure, DOM-free `src/engine/` tree (vitest-tested); Svelte UI in `src/lib/`. Keeps the coupled-legality logic isolated and portable if a networked version is ever built. +2026-05-19: Engine = 4× `chess.js` + an intersection layer. A player's legal moves = the moves legal on *both* their boards, keyed by `(from,to,promotion)`. Ghost immobility, the synchronized-checkmate definition, and en-passant/castling divergence all fall out of the intersection — no special-case code. +2026-05-19: Authoritative game state is an ordered list of synchronized moves; "replay to ply N" rebuilds all four `chess.js` boards. Powers undo and history scrubbing. + +## Implementation + +2026-05-19: Compass UI — four boards rendered as 45° diamonds in an X/pinwheel (NW 225°, NE 135°, SW 315°, SE 45°), players seated in the four V-notches, pieces rotate *with* their board so each player's army faces their seat. Confirmed against Andrew's sketch (`USERFILES/4personchess.png` in blind_chess). +2026-05-19: Four player colours (one per player) instead of White/Black fill — makes two-board ownership readable at a glance. +2026-05-19: Intersection highlighting (teaching mode) — grabbing a piece shows playable squares (legal on both boards) AND board-local-only squares, so the coupling is visible. +2026-05-19: Coordinate notation (`e2e4`) for the move log, not SAN — SAN's disambiguation differs between a player's two boards once they diverge, so it cannot be the single identical token. +2026-05-19: Click-to-move, not drag — cleaner on rotated boards. +2026-05-19: Provisional endgame rules picked by Claude, marked PROVISIONAL — Andrew can revise. First terminal event ends the game; any no-synchronized-move stalemate ends the game with all four drawing; threefold/50-move tracked on the whole four-board system; insufficient material not auto-detected. + +## Deferred / Rejected + +2026-05-19: Networked four-player play — deferred; separable later project, would reuse the `src/engine/` tree. +2026-05-19: AI opponents — deferred; the sandbox is operator-driven. +2026-05-19: Free position editor — rejected for v1; play-from-start + history scrubbing keeps every shown position reachable by legal play, preserving the "every board is real chess" invariant. +2026-05-19: Counter-rotating pieces upright on the diamonds — rejected; pieces rotate with their board so each player's army faces their seat (the point of the compass). +2026-05-19: Deployment behind Caddy — deferred; v1 runs locally, the static build can be hosted later trivially. +2026-05-19: A separate "dual view" of the on-move player's two boards — rejected; the compass is the whole UI, with the triple-highlight happening directly on it. diff --git a/IDEA.md b/IDEA.md new file mode 100644 index 0000000..242ce3c --- /dev/null +++ b/IDEA.md @@ -0,0 +1,42 @@ +# IDEA.md — Duplicate Chess + +## What is this? + +A local, browser-based sandbox/visualizer for **"duplicate chess"** — a four-player +chess variant invented by Andrew Freiberg (Seth's father). This project is to +duplicate chess what the sibling `blind_chess` project is to blind chess: a +digitization that lets an otherwise hard-to-play variant actually be played and +studied. + +Duplicate chess: four players — North, South, East, West — and four chessboards set +up between adjacent compass points (NW, NE, SW, SE). North and South play White; +East and West play Black. Each player controls one colour on **two** boards (North = +White on NW and NE; West = Black on NW and SW; and so on). Turn order is N → S → E → W. + +The defining rule: on your turn you make the **identical algebraic move on both of +your boards**. A move is legal only if it is legal on both. When one of your pieces +is captured on one board, its twin on your other board freezes into a **"ghost"** — +it can never move again (no synchronized move exists for it), but it still occupies +its square, blocks lines, can give discovered check, and can itself be captured. +Every individual board always remains a legal game of orthodox chess. + +## Problem it solves + +In the inventor's own words, the variant can't be grasped from prose: "this is not +really a variant that can be understood verbally after a certain point ... diagrams +are probably essential." The physical game is fiddly to set up and play. This tool +puts all four boards on one screen, enforces the synchronized-move coupling, renders +ghosts, and shows *why* a move is or isn't legal — so the variant can be seen. + +## Constraints / preferences + +- Local, single-operator tool. No server, no networking, no accounts — duplicate + chess is perfect-information, so one screen shows the whole game truthfully. +- Stack: Vite + Svelte 5 + TypeScript, `chess.js` per board. Single package. +- Sibling project to `blind_chess`; same homelab conventions. +- v1 is a visualizer/sandbox. Networked four-player play and AI opponents are + explicitly out of scope for now. + +See `docs/superpowers/specs/2026-05-19-duplicate-chess-design.md` for the full design, +and `USERFILES/4-person-chess.txt` in the `blind_chess` repo for the original +inventor conversation this is based on. diff --git a/docs/superpowers/specs/2026-05-19-duplicate-chess-design.md b/docs/superpowers/specs/2026-05-19-duplicate-chess-design.md new file mode 100644 index 0000000..9f17af9 --- /dev/null +++ b/docs/superpowers/specs/2026-05-19-duplicate-chess-design.md @@ -0,0 +1,380 @@ +# Duplicate Chess — Design Spec + +**Date:** 2026-05-19 +**Status:** Approved (brainstorming complete); ready for implementation planning. +**Author:** Claude + Seth, from a brainstorming session based on the inventor +conversation in `blind_chess/USERFILES/4-person-chess.txt`. + +--- + +## 1. Overview + +Duplicate chess is a four-player chess variant invented by Andrew Freiberg. This +project is a **local, single-operator, browser-based sandbox/visualizer** for it — +the digital equivalent of what `blind_chess` did for blind chess. One operator +drives all four players; the tool enforces the rules, renders the state, and shows +*why* moves are or are not legal. Its purpose is comprehension: the inventor's stated +position is that the variant cannot be understood from prose, only from seeing it +played. + +**Key property — perfect information.** Unlike blind chess, duplicate chess hides +nothing. Every player sees all four boards. There is therefore no view filter and no +trusted server boundary: the whole engine and UI run client-side in one app. + +--- + +## 2. The variant — rules + +### 2.1 Setup + +- Four players: **North, South, East, West**. +- Four boards, one between each adjacent pair of compass points: **NW, NE, SW, SE**. +- North and South play **White**; East and West play **Black**. +- Each player controls one colour on **two** boards: + + | Board | White player | Black player | + |-------|--------------|--------------| + | NW | North | West | + | NE | North | East | + | SW | South | West | + | SE | South | East | + +- Every board starts from the standard chess position. + +### 2.2 Turn order and the synchronized move + +- Turn order is **N → S → E → W**, repeating. +- On your turn you make **one move**, applied **identically to both of your boards** + (same from-square, same to-square, same promotion piece). +- A move is **legal only if it is legal on both** of your boards. If it is not legal + on both, you may not play it. +- "Identical" means identical algebraic coordinates. The two boards may differ in + what the move *does* (a capture on one board, a quiet move on the other) — that is + the normal source of divergence. + +### 2.3 Ghosts + +- When one of your pieces is captured on one board, its **twin** on your other board + becomes a **ghost**: it can never move again, because no synchronized move exists + for it (its counterpart is gone). +- A ghost is **not removed**. It still occupies its square, blocks lines, defends + squares, can produce a discovered check, can restrict enemy king movement, and can + itself be captured (if that capture is a legal synchronized move for the capturing + player). +- Ghost status is a **one-way, three-state lifecycle** for every original piece-pair: + (1) both twins alive and moving in lockstep → (2) one captured, one ghost → (3) + both gone. There is no recursion: once a twin is captured there is no surviving + counterpart to generate further ghosts. + +### 2.4 Check, checkmate, stalemate + +- Each individual board, viewed in isolation, is always a legal game of orthodox + chess. The single exception is the definition of checkmate (below). +- **Checkmate** = a player to move is in check on at least one of their boards and + has **no synchronized legal move**. A board viewed alone might show an escape, but + if that escape cannot be duplicated on the other board, the player is mated. +- **Stalemate** = a player to move is **not** in check and has no synchronized legal + move. + +### 2.5 Special moves + +- **Castling**: allowed only if legal on both of the player's boards. +- **Promotion**: a synchronized pawn advance to the last rank promotes on both + boards, necessarily to the **same piece**. +- **En passant**: handled by the general rule — the move is legal iff the identical + `(from,to)` is legal on both boards; it may be an en-passant capture on one board + and a different (or illegal) move on the other. + +### 2.6 Result + +- The winner is the player who delivers checkmate; the loser is the player mated; + the other two draw. It is possible for everyone to draw. +- See §6 for the **provisional** rulings on cases the inventor conversation left + underspecified. + +--- + +## 3. Architecture + +A **single Vite + Svelte 5 + TypeScript** application. No server, no pnpm workspace. + +``` +duplicate_chess/ + src/ + engine/ pure TypeScript, DOM-free, vitest-tested + boards.ts board/player/turn-order constants and maps + game.ts DuplicateGame: state, move application, history + legality.ts synchronized-move intersection + ghosts.ts ghost derivation + endgame.ts checkmate / stalemate / draw detection + notation.ts coordinate notation, save/load JSON + types.ts shared engine types + lib/ Svelte 5 components + Compass.svelte the four-board pinwheel + Board.svelte one board (rotatable, click-to-move, highlights) + Panel.svelte turn indicator, move log, legend, controls + PromotionDialog.svelte + stores/game.svelte.ts reactive wrapper over the engine + App.svelte + main.ts +``` + +- `chess.js` provides per-board orthodox chess: move generation, application, + check detection, FEN. +- The **engine is the single source of truth**. The UI never computes legality; it + calls the engine and renders the result. +- The engine is DOM-free so it is unit-testable and liftable into a package if a + networked four-player version is ever built. + +--- + +## 4. The engine + +### 4.1 The core insight — intersection + +The coupled game reduces to an intersection. Hold four `chess.js` instances. On +player `P`'s turn: + +``` +movesA = chess[boardA].moves({ verbose: true }) // P's moves on board A +movesB = chess[boardB].moves({ verbose: true }) // P's moves on board B +synced = movesA ∩ movesB keyed by (from, to, promotion) +``` + +`synced` **is** `P`'s legal move set. Three otherwise-hard rules need no special +code: + +- **Ghosts cannot move** — a ghost on board A has no twin on board B, so no move + from its square can appear in `synced`. +- **Checkmate** — `synced` empty *and* `P` in check on ≥1 board. A board showing an + un-synchronizable escape is handled automatically, because that escape is not in + `synced`. +- **En passant / castling divergence** — same `(from,to[,promotion])` or it is + simply absent from `synced`. + +The turn order N→S→E→W also gives every individual board a clean White-then-Black +alternation, so each `chess.js` instance stays internally consistent and +`chess.moves()` always returns the moves of the player whose global turn it is. + +### 4.2 Constants (`boards.ts`) + +``` +BOARDS = ['NW','NE','SW','SE'] +PLAYERS = ['N','S','E','W'] // also the turn order +PLAYER_BOARDS = { N:['NW','NE'], S:['SW','SE'], E:['NE','SE'], W:['NW','SW'] } +PLAYER_COLOR = { N:'w', S:'w', E:'b', W:'b' } +BOARD_PLAYERS = { NW:{w:'N',b:'W'}, NE:{w:'N',b:'E'}, + SW:{w:'S',b:'W'}, SE:{w:'S',b:'E'} } +``` + +### 4.3 State and the move list + +The authoritative state is an **ordered list of synchronized moves**: + +``` +history: { player: Player, from: Square, to: Square, promotion?: PieceSymbol }[] +``` + +`replayTo(n)` builds four fresh `chess.js` boards and applies the first `n` history +entries (each entry applied to its player's two boards). This single function powers +construction, **undo** (`replayTo(history.length - 1)`), and **history scrubbing** +(`replayTo(k)` for view-only display). Making a new move while scrubbed truncates +history after the scrub point — standard behaviour. + +`currentPlayer = PLAYERS[history.length % 4]`. + +### 4.4 Legality (`legality.ts`) + +- `legalSyncedMoves(player) → Move[]` — the intersection from §4.1. +- For the UI's triple-highlight, the engine also exposes, for a grabbed square `s` + belonging to the current player: `movesA.from(s)`, `movesB.from(s)`, and the + `synced` subset from `s`. The UI renders the `synced` subset as **playable** + (green) on both boards and the board-local remainder as **legal-here-only** + (grey). Grabbing a ghost therefore visibly yields zero playable moves. + +### 4.5 Ghosts (`ghosts.ts`) + +Invariant: a player's **non-ghost** pieces always occupy identical squares on both +their boards (they move in lockstep; a ghost is exactly a piece whose lockstep +broke). Therefore: + +> A piece of player `P`'s colour on board A at square `s` is a **ghost** iff board B +> (P's other board) has no `P`-colour piece at `s`. + +`ghosts() → { board, square }[]` over all four players. Used for rendering only; +legality already excludes ghost moves via the intersection. + +### 4.6 Endgame (`endgame.ts`) + +After each move, evaluate the next player `P`: + +- `synced` non-empty → game continues. +- `synced` empty and `P` in check on ≥1 board → **checkmate**: `P` loses; each + opponent on a board where `P` is in check is a **winner**; the remaining player(s) + draw. +- `synced` empty and `P` not in check → **stalemate**: game ends, all four draw + (provisional — see §6). +- **Global threefold repetition**: the combined key (four boards' piece placement + + castling rights + en-passant squares, plus `currentPlayer`) has occurred three + times → game ends, all draw. +- **Global 50-move rule**: 50 full rounds with no capture and no pawn move on any + board → game ends, all draw. + +The result is a per-player map of `'win' | 'draw' | 'loss'`. The game ends at the +first terminal event. + +### 4.7 Save / load (`notation.ts`) + +```json +{ "variant": "duplicate-chess", "version": 1, + "moves": [ { "player": "N", "from": "e2", "to": "e4" }, ... ] } +``` + +Save = serialize `history` and trigger a file download. Load = parse and `replayTo` +the full list. The move list is sufficient to reconstruct everything. + +--- + +## 5. The UI + +### 5.1 The compass + +Confirmed against the inventor's sketch (`blind_chess/USERFILES/4personchess.png`). + +- Four boards rendered as **45° diamonds** in an X / pinwheel. +- Per-board rotation: **NW 225°, NE 135°, SW 315°, SE 45°**. Each rotation puts that + board's White player's home rank on the edge facing their seat, oriented to read + right-way-up from that seat (standard chess: your pieces near you, glyphs pointing + away into the board). +- Pieces rotate **with** their board — so each player's army faces their seat. +- Players sit in the four V-notches between the diamonds: North top, East right, + South bottom, West left. +- The on-move player's two boards carry a coloured **turn-glow**. + +### 5.2 Player colours + +Four distinct piece colours, one per player (one suggested palette: North blue, +South red, East violet, West orange — final palette is an implementation detail). +Recolouring rather than White/Black fill makes two-board ownership instantly +readable: North's army is the same colour on both NW and NE. Pieces carry a dark +outline so they stay legible on both square shades. Pieces may be Unicode glyphs for +v1; a tintable SVG set is a possible upgrade. + +### 5.3 Intersection highlighting (teaching mode) + +When the operator clicks (grabs) a piece belonging to the current player, both that +player's boards highlight: + +- **Green dot** — a *playable* destination (legal on both boards; in `synced`). +- **Grey dashed dot** — legal on *that board only*; the coupling forbids it. +- **Cyan outline** — the grabbed square. + +This makes the divergence between a player's two boards directly visible, and is the +reason the project exists. Clicking a destination plays the move; clicking elsewhere +or the grabbed piece again cancels. + +### 5.4 Ghosts + +Rendered in place at reduced opacity with a dashed ring in the owning player's +colour. + +### 5.5 Side panel + +- **Turn indicator** — whose move, ghost counts, check status. +- **Move log** — coordinate notation, one row per round, four columns (N/S/E/W), + one identical token per player (`e2e4`). SAN is not used: its disambiguation + differs between a player's two boards once they diverge, so it cannot be the + single identical token. +- **Legend** — highlight meanings, ghost marker, the four player colours. +- **Controls** — New game, Undo, Prev/Next (history scrubbing), Save, Load. + +### 5.6 Move input and promotion + +- **Click-to-move**: click a piece → triple-highlight appears → click a destination. + Click-to-move (not drag) is cleaner on rotated boards. +- **Promotion**: when the chosen move is a pawn reaching the last rank, a dialog + picks the piece; both pawns promote identically. + +--- + +## 6. Provisional endgame rules + +The inventor conversation fully specifies the common ending (first checkmate → one +winner, one loser, two draws) but leaves edge cases open. The operator chose to ship +**provisional defaults now**, clearly marked, for Andrew to revise later. These are +**PROVISIONAL — Claude's defaults, not Andrew's rulings**: + +| Case | Provisional ruling | +|------|--------------------| +| **Single-player stalemate** (no synchronized move, not in check) | The whole game ends; all four players draw. No frozen-board continuation — this keeps the engine free of multi-player-elimination logic and matches "it is possible for everyone to draw." | +| **Double-board checkmate** (mated while in check on both boards, by both opponents) | Both checking opponents are recorded as winners; the mated player loses; the fourth player draws. Generalizes "the winner is the one who checkmates" without a tiebreak. | +| **Threefold repetition / 50-move** | Evaluated on the whole four-board system (all four positions + side-to-move), not per board. Triggers an all-draw game end. | +| **Insufficient material** | Not auto-detected (rare and hard to define across four coupled boards). The operator may declare a draw manually. | + +Each provisional rule must be marked in code (a `PROVISIONAL` comment or constant) +so a future ruling from Andrew can be located and applied cleanly. + +--- + +## 7. Scope + +**In scope (v1):** + +- Play a full game from the standard start, operator-driving all four players. +- The compass UI with pinwheel boards and four player colours. +- Intersection (teaching-mode) highlighting. +- Ghost detection and rendering. +- Checkmate / stalemate / draw detection with the provisional rules. +- Coordinate-notation move log. +- Undo and history scrubbing. +- Save / load a game to a JSON file. + +**Out of scope (v1):** + +- Networked four-player play (separable later project; would reuse `src/engine/`). +- AI opponents (the sandbox is operator-driven). +- A free position editor (play-from-start keeps every shown position reachable by + legal play, preserving the "every board is real chess" invariant). +- Insufficient-material auto-detection. +- Deployment behind Caddy (the static build can be hosted later trivially). +- Mobile-specific polish (four boards want a wide screen; desktop-first). + +--- + +## 8. Testing + +- `src/engine/` is pure TypeScript with no DOM — covered by **vitest**: + - synchronized-move intersection (including divergence, castling, en passant); + - ghost derivation (lifecycle: lockstep → ghost → gone); + - endgame detection (single- and double-board checkmate, stalemate, threefold, + 50-move); + - history replay / undo / scrub correctness; + - a scripted full game played to a terminal state. +- Svelte components: `svelte-check` plus manual browser testing — same division of + labour as `blind_chess` (no component test harness by design). + +--- + +## 9. Open questions / future + +- **The provisional rules in §6** need Andrew's confirmation; the + double-board-mate winner rule and the stalemate-ends-the-game rule are the two + most consequential. +- **50-move counting units** (rounds vs plies) — to be pinned during implementation; + provisionally "50 rounds." +- **Rotated-board ergonomics** — playing on a board rotated 135–225° is harder to + read than an upright board. v1 ships the static pinwheel as drawn. If play proves + awkward, a candidate enhancement is a click-to-focus that temporarily uprights the + active player's two boards. Not in v1 scope. +- **Networked four-player play** is the natural follow-on project and the reason the + engine is kept DOM-free and self-contained. + +--- + +## 10. Source material + +- `blind_chess/USERFILES/4-person-chess.txt` — the original inventor conversation + defining the variant. +- `blind_chess/USERFILES/4personchess.png` — Andrew's sketch of the compass layout. +- Brainstorming mockups: `blind_chess/.superpowers/brainstorm/.../content/` + (`layout-v6.html` is the approved compass layout).