Files
duplicate_chess/docs/superpowers/specs/2026-05-19-duplicate-chess-design.md
T
claude (duplicate_chess) 9611c0ae0e init: scaffold duplicate_chess project and design spec
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) <noreply@anthropic.com>
2026-05-19 00:08:50 -04:00

381 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 135225° 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).