diff --git a/.sisyphus/plans/rule-variants-v2.md b/.sisyphus/plans/rule-variants-v2.md new file mode 100644 index 0000000..466b77a --- /dev/null +++ b/.sisyphus/plans/rule-variants-v2.md @@ -0,0 +1,978 @@ +# Rule Variants — Greenchess cat=4 Preset Suite (execution plan) + +> **For the agent picking this up:** This is the execution-ready successor to +> `.sisyphus/plans/rule-variants.md` (design doc, Momus-approved). The design +> doc has the full rationale + risks + hook architecture sketch. THIS file is +> the step-by-step to-do list with commit discipline and verification gates, +> formatted like the recently-shipped `modifier-profiles-t3.md`. Read both +> — this one for the work, the design doc for the WHY. + +## TL;DR + +> Ship 4 new preset-API hooks (`getRoyalPieces`, `filterLegalMoves`, +> `shouldAdvanceTurn`, `overridePieceMoves`) plus `HalfMovesThisTurn` game +> fact, then 14 rule-variant presets built on them. Tier 1 (4 presets) +> closes deferrals from the starting-layouts plan. Tier 2 (10 presets) +> rounds out greenchess cat=4 coverage. Tier 3 (`royalty-transfer`) is +> explicitly deferred. +> +> **Deliverables**: +> - 4 additive hook signatures on `PresetDef` +> - `HalfMovesThisTurn` game fact + turn-advance gating +> - 14 preset implementations with 8-15 behavioral tests each +> - `dual-classic` layout (2 kings per side) — needed by `dual-king` preset +> - Rule-variant Playwright e2e smoke (3 representative variants) +> - `PRESET-API.md` hook-ordering diagram + preset gallery +> - Lobby RulesDrawer preset grouping (King / Objective / Movement / Multimove) +> +> **Estimated Effort**: Large (31 tasks, 6 phases) +> **Parallel Execution**: YES within each phase; phases are hard-gated +> **Critical Path**: Phase A hooks → Phase B T1 presets → Phase C/D/E T2 presets → Phase F integration + e2e +> **Explicit Deferral**: `royalty-transfer` — needs mid-game "which royal is active" state toggle; UX outside "one move at a time". + +--- + +## Context + +### What Shipped Last (commits `abc5c86` back to `cbe4a4b`) + +- **modifier-profiles-t3** complete (32 tasks + 4 reviewers all APPROVE) + plus audit follow-ups (gaps 1-7 + Q3.2/Q4.2/Q4.4/Q4.6/Q4.7). + Codebase state: 1417 unit tests + 80/80 Playwright, all green. Master + at `abc5c86`. +- **starting-layouts** shipped: added Dunsany, Monster, Pawns-Only, Horde, + Knightmate, Chess960 as layout-only presets. **Each of those layouts + declared `suggestedPresets` pointing at presets that DON'T EXIST YET.** + Tier 1 of this plan ships those presets. +- **preset-flexibility-architecture** shipped: `PIECE_TYPE_REGISTRY`, + `engine.presetState(id)`, `engine.spawnPiece`, `engine.emitEffect`, + scope-aware hook dispatch, `MoveRecord`/`engine.moveLog`, + `onBeforeMove`/`onTurnStart` hooks all live. Everything this plan + builds on is in place. + +### What T3's Trip Taught Us + +Load-time consumer integrity check pattern (T3 Q3.2, in +`packages/chess/src/modifiers/primitives/manifest.ts`) converts +silent-inert features into loud load-time failures. This plan uses the +same pattern for the new hooks: every preset declaring a new hook is +structural; we don't need a manifest for hooks themselves, but document +hook-ordering semantics clearly in `PRESET-API.md` so future contributors +can't accidentally compose a first-wins hook in a commutative way. + +### Key File Anchors (verified live) + +- `packages/chess/src/rules/check.ts:83` — `isInCheck(session, color)`: + hardcoded to `"king"` via `findKingPosition(session, color)`. +- `packages/chess/src/rules/checkmate.ts:80`, + `packages/chess/src/rules/stalemate.ts:92,105` — call `isInCheck` and + iterate "every king" implicitly. +- `packages/chess/src/engine.ts:855` — `getAllLegalMoves()`: aggregation + point where `filterLegalMoves` will hook in (POST self-check filter). +- `packages/chess/src/engine.ts:964` — `applyMove`: turn-flip lives here; + `shouldAdvanceTurn` intercepts before the flip. +- `packages/chess/src/engine.ts` — `getAllLegalMoves` dispatches per-piece + via `PIECE_TYPE_REGISTRY.get(type).generateMoves`; `overridePieceMoves` + runs BEFORE this dispatch. +- `packages/chess/src/presets/registry.ts:297-500` — `PresetDef` hook + surface. New hooks slot in here. + +### Out of Scope (deferred) + +- **Royalty Transfer Chess** (T3 in the design doc). Needs mid-game + "which king is active" flip triggered by a non-move action. UX outside + "one move at a time". Post-v1. +- **4+ player variants**. Engine assumes exactly two sides. +- **Non-8×8 variants** from other greenchess categories (Capablanca, + Janus). Needs board-dimensions refactor. +- **PGN notation with non-king royalty** (e.g., `N#` in Knightmate). + Rabbit hole; notation stays king-centric; move log displays via + `describeMoveEffect` instead. +- **Check-banner multi-royal indicator UX**. Risk 4 in the design doc; + deferred to a small follow-up UI task after this plan lands. +- **Extinction Chess target-type UI chip** (D.3 of the design doc). + Preset ships with configurable `presetState({targetType})`; a + drawer-UI chip for cycling targets is a follow-up. + +--- + +## Conventions for the Executing Agent + +### Repo Discipline + +- Strict TypeScript: no `as any`, no `@ts-ignore`, no `as unknown as X`. + One judicious `as X` boundary cast is OK with JSDoc explaining why. + Audit with `grep -rn "as unknown as" packages/chess/src packages/server/src --include='*.ts' --include='*.tsx' | grep -v '\.test\.'`. +- Pre-commit hook runs `bun run check` (typecheck + lint + vitest). Must + be green before every commit. NEVER bypass with `--no-verify`. +- Tests: `bun run test` (vitest), NOT `bun test`. Playwright from repo + root: `bunx playwright test`. For multiplayer scenarios the server + must be running: + `tmux new-session -d -s ws-server -c /home/joey/Projects/rules 'bun run packages/server/src/index.ts'` + (the dev server does NOT hot-reload protocol changes — restart after + any `packages/server/src/protocol.ts` edit). +- Atomic commits per task. Conventional-commit messages + (`feat(engine): ...`, `test(e2e): ...`, `docs(adr): ...`, + `chore(sisyphus): ...`). +- Notepad at `.sisyphus/notepads/rule-variants/` — create on plan start. + APPEND (never overwrite) under `## [YYYY-MM-DD HH:MM] Task: X.Y` + headings. + +### T3-Learned Tactics (don't repeat the pain) + +- **Tool-cap fragility**: delegating a large phase to a single + deep/visual agent reliably hits the 200-tool-call limit. Prefer + shipping each preset as its OWN delegation OR doing them inline. + Never batch 15+ file creations under one agent. +- **Parallel waves**: run at most 3-5 agents per wave, each scoped to a + disjoint file set. A preset + its own test file is one good unit. +- **Commit discipline for delegated work**: if an agent is at risk of + hitting the tool cap, INSTRUCT IT to commit incrementally every 2-3 + files. Uncommitted work at cancellation requires orchestrator cleanup. +- **Schema v3 ↔ v4 mirror**: if a task requires server protocol changes + (this plan has none — all hooks are chess-package-internal), be + vigilant. Cross-package parity test in + `packages/server/src/custom-modifier-wire-parity.test.ts` is the + template for how to prove agreement. +- **`test.fixme` policy**: only use for scenarios that genuinely need + engine wiring not yet in place. Every `fixme` in this plan's e2e + task MUST be marked with a `// blocked on: TASK_ID` comment so + cleanup knows what unblocks it. + +### `MUST` / `SHOULD` / `MAY` + +- `MUST` = blocks phase completion. +- `SHOULD` = expected but permissible to defer with written rationale + in the notepad. +- `MAY` = optional; do it if tool budget allows. + +--- + +## Architecture Sketch (recap — full version in design doc) + +### Hook signatures (verbatim) + +```ts +// rules/check.ts + registry.ts +interface RoyalContext { + readonly engine: ChessEngine; + readonly color: "white" | "black"; +} +interface PresetDef { + // Phase A.1 — return the set of piece ENTITY IDs that count as + // royal for this color. Undefined = "don't contribute" (engine falls + // back to union of other presets' returns; empty union → default + // {PieceType === "king"}). + readonly getRoyalPieces?: (ctx: RoyalContext) => readonly EntityId[] | undefined; + + // Phase A.2 — post-aggregation filter on the per-color legal-move + // list. Applied in preset-registration order AFTER the self-check + // filter. Returning a subset is the only sanctioned way to express + // compulsory-capture-style rules. + readonly filterLegalMoves?: ( + ctx: FilterLegalMovesContext, + ) => readonly LegalMove[]; + + // Phase A.3 — returning false cancels the turn flip for this + // applyMove. First false wins. Undefined = no opinion. Engine + // increments HalfMovesThisTurn BEFORE polling; presets read it via + // ctx.halfMovesThisTurn. + readonly shouldAdvanceTurn?: ( + ctx: TurnAdvanceContext, + ) => boolean | undefined; + + // Phase A.4 — REPLACE the default move generator for this piece. + // First non-undefined return wins (documented as a guardrail, not + // an opt-in; conflicts should be caught via incompatibleWith). + readonly overridePieceMoves?: ( + engine: ChessEngine, + pieceId: EntityId, + ) => readonly LegalMove[] | undefined; +} + +// engine.ts +// The move-generation dispatch order (documented in PRESET-API.md): +// 1) overridePieceMoves — first match wins, skips default +// 2) PIECE_TYPE_REGISTRY — default per-type generator +// 3) getExtraMoves — each preset appends +// 4) filterMoves — each preset filters per-piece +// 5) aggregate all pieces +// 6) self-check filter — suppressible via shouldFilterSelfCheck +// 7) filterLegalMoves — each preset runs in registration order +``` + +### New game fact: `HalfMovesThisTurn` + +```ts +// packages/chess/src/schema.ts — ChessAttrMap additions +HalfMovesThisTurn: number; // on GAME_ENTITY. Resets to 0 on turn flip. +``` + +Seeded in `starting-position.ts` alongside `Turn`. Incremented in +`applyMove` BEFORE the turn-flip decision. Reset to 0 when the flip +happens. + +### Royal-piece dispatch (pseudo) + +```ts +// engine.ts, private +getActiveRoyalEntityIds(color): EntityId[] { + const acc = new Set(); + let anyPresetContributed = false; + for (const entry of this.activePresets.list()) { + const def = PRESET_REGISTRY.get(entry.id); + const result = def?.getRoyalPieces?.({ engine: this, color }); + if (result === undefined) continue; + anyPresetContributed = true; + for (const id of result) acc.add(id); + } + if (!anyPresetContributed) { + // Default: every piece with PieceType === "king" of this color. + return defaultKingIds(this.session, color); + } + return [...acc]; +} +``` + +Empty royal set = "no royalty" (Suicide, Capture-all interpretation). +`isInCheck` / `isCheckmate` / `isStalemate` handle this degenerate case +by returning `false` always when the set is empty. + +### Tier 1 → Tier 2 dependency graph + +Tier 1 presets CAN ship without Tier 2. Tier 2 presets MAY depend on +Tier 1 hooks but NOT on Tier 1 presets behaviorally. + +- `knightmate-rules` → getRoyalPieces only +- `double-move` → shouldAdvanceTurn + HalfMovesThisTurn +- `monster-rules` → shouldAdvanceTurn + scope-aware dispatch +- `first-promotion-wins` → onAfterMove (existing) + onCheckGameResult + (existing) + shouldFilterSelfCheck (existing). No new hooks needed + but grouped in T1 because it closes a layouts deferral. +- `coregal`, `dual-king`, `weak-dual-king` → getRoyalPieces only +- `suicide-chess` → filterLegalMoves + getRoyalPieces (empty) +- `capture-all`, `extinction-chess` → existing hooks only +- `berolina-pawns`, `berolina-pawns-2` → overridePieceMoves +- `bouncing-pieces`, `bouncing-pieces-2` → existing `getExtraMoves` + +--- + +## Phase A — Hook API Extensions (architecture) + +**Goal**: Add 4 hooks + `HalfMovesThisTurn` fact; zero behavioral change +to any existing preset/game. + +- [ ] A.1. **`getRoyalPieces` hook + engine royal-dispatch + isInCheck refactor** + + **What to do**: + - `packages/chess/src/presets/registry.ts`: add `RoyalContext` type + and `getRoyalPieces` hook signature on `PresetDef`. + - `packages/chess/src/rules/check.ts`: `isInCheck(session, color)` + gains an optional 3rd param `royalEntityIds?: readonly EntityId[]`. + When provided, iterate those entities' positions instead of calling + `findKingPosition`. When absent, keep current behaviour (back-compat + for callers that don't pass the param). + - `packages/chess/src/rules/checkmate.ts` + `stalemate.ts`: same + optional-param widening. + - `packages/chess/src/engine.ts`: add private + `getActiveRoyalEntityIds(color): EntityId[]` that unions every + active preset's `getRoyalPieces` returns. Empty-union fallback = + default king entities via `findKingPosition`-equivalent logic. + Use this in every internal call to `isInCheck` / + `isCheckmate` / `isStalemate` / `filterSelfCheckMoves`. + - Handle empty-royal case: `isInCheck` / `isCheckmate` / + `isStalemate` MUST return `false` when the royal set is empty + (document + test). + + **Files touched**: `packages/chess/src/presets/registry.ts`, + `packages/chess/src/rules/check.ts`, + `packages/chess/src/rules/checkmate.ts`, + `packages/chess/src/rules/stalemate.ts`, + `packages/chess/src/engine.ts`, + `packages/chess/src/presets/royal-pieces.test.ts` (new). + + **Verification**: `bun run check` green. All existing 1417 tests pass + unchanged. The new `royal-pieces.test.ts` must include: + - Prototype "all-kings-royal" preset returning `[king1, king2]` + when a piece spawned as a king exists — isInCheck behaves + correctly. + - Empty-royal preset (`getRoyalPieces → []`) — isInCheck returns + false, no crash. + - Two presets contributing overlapping sets — union dedupes. + - No preset contributes — default king detection path unchanged. + + **Recommended Agent Profile**: `deep` OR orchestrator-direct. + + **Parallelization**: Phase A. Blocks: B.1, C.1, C.2, C.3, D.1, D.2. + Blocked by: none. + + **Commit**: `feat(engine): getRoyalPieces hook + engine royal dispatch` + +- [ ] A.2. **`filterLegalMoves` hook** + + **What to do**: + - `packages/chess/src/presets/registry.ts`: add + `FilterLegalMovesContext { readonly engine: ChessEngine; readonly color: "white" | "black"; readonly moves: readonly LegalMove[] }` + + `filterLegalMoves` hook. + - `packages/chess/src/engine.ts` `getAllLegalMoves`: after the + existing self-check filter (step 6), iterate active presets' + `filterLegalMoves` in registration order, passing the + accumulated list through each. Document "order matters; use + incompatibleWith when composition would be lossy". + + **Files touched**: `packages/chess/src/presets/registry.ts`, + `packages/chess/src/engine.ts`, + `packages/chess/src/presets/filter-legal-moves.test.ts` (new). + + **Verification**: + - Prototype "no-a-file" preset dropping every a-file source/target + move; assert across multiple pieces. + - No active preset with hook = getAllLegalMoves unchanged. + + **Parallelization**: Phase A. Blocks: D.1. Blocked by: none. + + **Commit**: `feat(engine): filterLegalMoves hook` + +- [ ] A.3. **`shouldAdvanceTurn` hook + `HalfMovesThisTurn` tracking** + + **What to do**: + - `packages/chess/src/schema.ts`: add `HalfMovesThisTurn: number` + to `ChessAttrMap`. + - `packages/chess/src/starting-position.ts`: seed + `HalfMovesThisTurn = 0` on `GAME_ENTITY` wherever `Turn` is seeded. + - `packages/chess/src/engine.ts` `applyMove`: + 1. BEFORE the turn-flip decision, increment `HalfMovesThisTurn`. + 2. Build `TurnAdvanceContext { mover, halfMovesThisTurn }`. + 3. Iterate active presets' `shouldAdvanceTurn`. First `false` wins + → skip the flip. Otherwise flip Turn and reset + `HalfMovesThisTurn = 0`. + - `packages/chess/src/presets/registry.ts`: new + `TurnAdvanceContext` + hook. + - Snapshot/restore: verify `HalfMovesThisTurn` round-trips via + `session.allFacts` (save/load, game.state snapshot). + + **Files touched**: `packages/chess/src/schema.ts`, + `packages/chess/src/starting-position.ts`, + `packages/chess/src/engine.ts`, + `packages/chess/src/presets/registry.ts`, + `packages/chess/src/presets/turn-advance.test.ts` (new). + + **Verification**: + - Prototype "never-flip" preset; white keeps moving forever. + - Fact resets on flip; increments on each non-cancelled move. + - Existing 1417 tests unchanged (no preset implements the hook). + + **Parallelization**: Phase A. Blocks: B.2, B.3. Blocked by: none. + + **Commit**: `feat(engine): shouldAdvanceTurn hook + HalfMovesThisTurn fact` + +- [ ] A.4. **`overridePieceMoves` hook** + + **What to do**: + - `packages/chess/src/presets/registry.ts`: new `overridePieceMoves` + hook on `PresetDef`. + - `packages/chess/src/engine.ts`: in per-piece move generation, + BEFORE `PIECE_TYPE_REGISTRY.get(type).generateMoves` runs, iterate + presets' `overridePieceMoves(engine, pieceId)`. First non-undefined + → replace the generator output for this piece and skip steps 2-3 + (default generator + getExtraMoves). Still apply steps 4 (filterMoves) + and 7 (filterLegalMoves). + - Collision handling: when a second preset also returns non-undefined + for the same piece, log a dev-mode console warning with both preset + ids. Don't throw (let `incompatibleWith` handle this at config + time). + + **Files touched**: `packages/chess/src/presets/registry.ts`, + `packages/chess/src/engine.ts`, + `packages/chess/src/presets/override-piece-moves.test.ts` (new). + + **Verification**: + - Prototype "lame-knight" preset replacing knight moves with + single-square orthogonal; knight behaves differently only with + preset active. + - getExtraMoves still contributes when no override is declared. + - Collision warning fires in dev mode (verify via console spy). + + **Parallelization**: Phase A. Blocks: E.1, E.2. Blocked by: none. + + **Commit**: `feat(engine): overridePieceMoves hook` + +- [ ] A.5. **Phase A verification gate + PRESET-API.md hook-ordering doc** + + **What to do**: + - Run `bun run check` — expect 1417 + new prototype tests green. + - Run full Playwright suite — expect 80/80 (no behavioral change). + - `packages/chess/docs/PRESET-API.md`: add "Hook ordering" section + with the 7-step dispatch diagram from the design doc. Worked + example for each of the 4 new hooks. + - Append phase summary to `.sisyphus/notepads/rule-variants/learnings.md`. + + **Parallelization**: Phase A finale. Blocks: Phase B onward. + + **Commit**: `docs(preset-api): hook ordering + Phase A summary` + +--- + +## Phase B — Tier 1 Presets (close layouts-plan deferrals) + +**Goal**: 4 T1 presets, each unblocks its matching layout's +`suggestedPresets` chip. + +- [ ] B.1. **`knightmate-rules` preset** + + **What to do**: + - `packages/chess/src/presets/knightmate-rules.ts`: implements + `getRoyalPieces` returning every Knight entity of `color`. + Declares `requires: []` (works with any layout, best with + Knightmate). `incompatibleWith: ["coregal", "dual-king", + "weak-dual-king"]`. + - `packages/chess/src/presets/knightmate-rules.test.ts`: 10+ tests: + (a) king moves into attacked square legally, (b) knight in check + restricts moves to resolving check, (c) checkmating the last + knight ends the game, (d) losing all knights-but-one is not + game-over if any knight survives but IS game-over if ALL royals + captured, (e) composition with piece-hp — attacking a knight + drops HP instead of killing. + - `packages/chess/src/layouts/knightmate.ts`: populate + `suggestedPresets: ["knightmate-rules"]`. + + **Parallelization**: Phase B, runs with B.2/B.3/B.4 concurrently + (disjoint files). Blocks: F.2, F.4. Blocked by: A.1. + + **Commit**: `feat(presets): knightmate-rules` + +- [ ] B.2. **`double-move` preset** + + **What to do**: + - `packages/chess/src/presets/double-move.ts`: implements + `shouldAdvanceTurn` returning `false` when + `halfMovesThisTurn < 2`. No royal override — uses default king + logic. `incompatibleWith: ["monster-rules"]`. + - `packages/chess/src/presets/double-move.test.ts`: 12+ tests: + (a) white plays two moves before black, (b) halfMovesThisTurn + resets on flip, (c) check on first half-move — opponent must + still respond on their first half-move (design call: check is + allowed, opponent is NOT forced to block on move 1 — they can + play any legal move including an intermediate move, and check + filtering applies on each half-move separately; document this + interpretation), (d) composition with piece-hp, (e) composition + with king-heals (heal fires on full-turn-flip only). + + **Parallelization**: Phase B concurrent. Blocks: F.4. Blocked by: A.3. + + **Commit**: `feat(presets): double-move` + +- [ ] B.3. **`monster-rules` preset** + + **What to do**: + - `packages/chess/src/presets/monster-rules.ts`: scope-aware. + For `scope: "white"`, `shouldAdvanceTurn` returns `false` when + `mover === "white"` and `halfMovesThisTurn < 2`. For + `scope: "black"`, behaves as default. `incompatibleWith: + ["double-move", "suicide-chess", "capture-all"]`. + - `packages/chess/src/presets/monster-rules.test.ts`: 10+ tests. + White moves twice, black moves once. Combined with Monster + layout → white plays king + 4 pawns; verify both moves + resolve before black responds. + - `packages/chess/src/layouts/monster.ts`: populate + `suggestedPresets: ["monster-rules"]`. + + **Parallelization**: Phase B concurrent. Blocks: F.2, F.4. Blocked by: A.3. + + **Commit**: `feat(presets): monster-rules` + +- [ ] B.4. **`first-promotion-wins` preset** + + **What to do**: + - `packages/chess/src/presets/first-promotion-wins.ts`: + `onAfterMove` inspects `engine.moveLog[last].promotion`; if + non-null, records winner via `engine.presetState`. + `onCheckGameResult` returns winner if recorded, else `undefined`. + Also implements `shouldFilterSelfCheck → false` so a pawn under + check can still promote. `incompatibleWith: ["capture-to-win", + "last-piece-standing", "extinction-chess", "suicide-chess", + "capture-all"]`. + - `packages/chess/src/presets/first-promotion-wins.test.ts`: 8+ + tests: (a) first pawn promotion wins, (b) works with Pawns-Only + layout, (c) composition with double-move (second half-move + promotion also wins), (d) composition with piece-hp. + - `packages/chess/src/layouts/pawns-only.ts`: populate + `suggestedPresets: ["first-promotion-wins"]`. + + **Parallelization**: Phase B concurrent. Blocks: F.2, F.4. Blocked by: A.1, A.3. + + **Commit**: `feat(presets): first-promotion-wins` + +- [ ] B.5. **Phase B verification gate** + + **What to do**: `bun run check` green. Manual Playwright smoke: + open lobby, pick Knightmate layout → chip for knightmate-rules + appears → enabling it + Play Solo starts a game with knight as + the royal. + + **Commit**: `chore(sisyphus): Phase B (Tier 1 presets) complete` + +--- + +## Phase C — Tier 2 Royal-Variant Presets + +**Goal**: 3 multi-royal presets exercising `getRoyalPieces` in +broader ways. + +- [ ] C.1. **`coregal` preset** + + **What to do**: + - `packages/chess/src/presets/coregal.ts`: `getRoyalPieces` + returns union of king + queen entities for `color`. + `incompatibleWith: ["knightmate-rules", "dual-king", + "weak-dual-king", "suicide-chess"]`. + - `packages/chess/src/presets/coregal.test.ts`: 10+ tests: + (a) mating only the king ends the game (queen can't save), + (b) mating only the queen ends the game, (c) no queen on board + → behaves as FIDE, (d) pins affect the queen (moving pinned queen + leaves king in check → illegal), (e) composition with piece-hp — + queen loses HP on capture attempts. + + **Parallelization**: Phase C concurrent. Blocks: F.1. Blocked by: A.1. + + **Commit**: `feat(presets): coregal` + +- [ ] C.2. **`dual-classic` layout + `dual-king` preset** + + **What to do**: + - `packages/chess/src/layouts/dual-classic.ts`: new layout based + on FIDE but with 2 kings per side (e.g., kings on e1 + d1 for + white; queens moved to a file or omitted; document the choice). + Register in layouts barrel. + - `packages/chess/src/presets/dual-king.ts`: `getRoyalPieces` + returns ALL king entities of `color`. Works with any layout but + designed for dual-classic. `incompatibleWith: ["weak-dual-king", + "coregal", "knightmate-rules"]`. + - `packages/chess/src/presets/dual-king.test.ts`: 8+ tests. Mating + EITHER king ends the game (strong variant). Pawn + underpromotion-to-king increases the royal count — integration + test with 3 kings. + + **Parallelization**: Phase C concurrent. Blocks: F.1, F.2. + Blocked by: A.1. + + **Commit**: `feat(presets): dual-king + dual-classic layout` + +- [ ] C.3. **`weak-dual-king` preset** + + **What to do**: + - `packages/chess/src/presets/weak-dual-king.ts`: `getRoyalPieces` + returns ALL king entities of `color`. Adds + `onCheckGameResult` override: game-over only when EVERY royal + of one side is captured OR all remaining royals are mated + simultaneously. First mate of ONE king does NOT end the game + — the other king plays on. `incompatibleWith: ["dual-king", + "coregal", "knightmate-rules"]`. + - `packages/chess/src/presets/weak-dual-king.test.ts`: 8+ tests: + (a) one king lost → game continues, (b) both lost → game ends, + (c) last king mated → game ends. + + **Parallelization**: Phase C concurrent. Blocks: F.1. Blocked by: A.1. + + **Commit**: `feat(presets): weak-dual-king` + +- [ ] C.4. **Phase C verification gate** + + **What to do**: `bun run check` green. Integration test: coregal + + piece-hp — queen loses HP but king is still the primary royal; + attacker must checkmate AT LEAST ONE royal to end. + + **Commit**: `chore(sisyphus): Phase C (royal variants) complete` + +--- + +## Phase D — Tier 2 Objective-Variant Presets + +**Goal**: 3 win-condition-override presets. + +- [ ] D.1. **`suicide-chess` preset** + + **What to do**: + - `packages/chess/src/presets/suicide-chess.ts`: + - `filterLegalMoves`: if any move in the aggregated list has + `isCapture === true`, return only captures. + - `shouldFilterSelfCheck → false` (king is not royal). + - `getRoyalPieces → []` (explicit empty). + - `onCheckGameResult`: if one side has 0 pieces on the board, + that side WINS (suicide inverts). + - `incompatibleWith: ["capture-to-win", "last-piece-standing", + "monster-rules", "knightmate-rules", "coregal", "dual-king", + "weak-dual-king", "capture-all", "extinction-chess", + "first-promotion-wins"]`. + - `packages/chess/src/presets/suicide-chess.test.ts`: 14+ tests: + (a) captures compulsory when any capture available, (b) + non-captures legal otherwise, (c) losing all pieces wins, + (d) pawn promotion still legal, (e) composition with piece-hp — + an HP-tagged capture that doesn't kill still satisfies the + compulsion (design call documented in-file). + + **Parallelization**: Phase D concurrent. Blocks: F.1, F.4. + Blocked by: A.1, A.2. + + **Commit**: `feat(presets): suicide-chess` + +- [ ] D.2. **`capture-all` preset** + + **What to do**: + - `packages/chess/src/presets/capture-all.ts`: + - `shouldFilterSelfCheck → false`. + - `getRoyalPieces → []`. + - `onCheckGameResult`: winner = side whose opponent has 0 + pieces. + - `incompatibleWith: ["suicide-chess", "capture-to-win", + "last-piece-standing", "extinction-chess", + "first-promotion-wins"]`. + - `packages/chess/src/presets/capture-all.test.ts`: 8+ tests. + + **Parallelization**: Phase D concurrent. Blocks: F.1. Blocked by: A.1. + + **Commit**: `feat(presets): capture-all` + +- [ ] D.3. **`extinction-chess` preset (configurable target type)** + + **What to do**: + - `packages/chess/src/presets/extinction-chess.ts`: + Configurable target type via + `engine.presetState<{ targetType: PieceType }>("extinction-chess")`. + Default: `"pawn"` (common, exercises attrition). + `onCheckGameResult` returns winner when opposite side has 0 + pieces of `targetType`. No royal override; king can still be + captured freely. `incompatibleWith: ["suicide-chess", + "capture-all", "first-promotion-wins"]`. + - `packages/chess/src/presets/extinction-chess.test.ts`: 10+ + tests covering each target type (pawn, knight, bishop, rook, + queen, king). + - **UI follow-up note**: Cycling target via drawer chip is out + of scope; document as a post-landing follow-up. + + **Parallelization**: Phase D concurrent. Blocks: F.1. Blocked by: none. + + **Commit**: `feat(presets): extinction-chess` + +- [ ] D.4. **Phase D verification gate** + + **What to do**: `bun run check` green. + + **Commit**: `chore(sisyphus): Phase D (objective variants) complete` + +--- + +## Phase E — Tier 2 Movement-Variant Presets + +**Goal**: 4 move-generation-override presets. + +- [ ] E.1. **`berolina-pawns` preset** + + **What to do**: + - `packages/chess/src/presets/berolina-pawns.ts`: + `overridePieceMoves` for pawn pieces. Generates: + - Forward diagonals → non-capture push (one square; two from + home rank) + - Forward orthogonal → capture only + - En passant reinterpreted: if opponent just double-diagonal- + pushed adjacent to yours, capture orthogonally to the + skipped square (document the rule choice in-file — several + variants exist). + - Promotion: diagonal push to promotion rank still promotes. + `incompatibleWith: ["pawn-diagonal-no-capture", + "berolina-pawns-2", "pawns-move-backward"]`. + - `packages/chess/src/presets/berolina-pawns.test.ts`: 15+ tests + covering double-push, capture, en-passant, promotion, block + detection, composition with piece-hp. + + **Parallelization**: Phase E concurrent. Blocks: E.2, F.4. + Blocked by: A.4. + + **Commit**: `feat(presets): berolina-pawns` + +- [ ] E.2. **`berolina-pawns-2` preset** + + **What to do**: + - `packages/chess/src/presets/berolina-pawns-2.ts`: identical to + `berolina-pawns` but also allows sideways captures (same-rank, + adjacent file). + `incompatibleWith: ["berolina-pawns", ...same list]`. + - `packages/chess/src/presets/berolina-pawns-2.test.ts`: 6+ tests + targeting the DIFF from berolina-pawns. + + **Parallelization**: Phase E concurrent. Blocks: F.4. + Blocked by: E.1 (for shared helpers). + + **Commit**: `feat(presets): berolina-pawns-2` + +- [ ] E.3. **`bouncing-pieces` preset** + + **What to do**: + - `packages/chess/src/presets/bouncing-pieces.ts`: `getExtraMoves` + for bishop + queen. Computes diagonal rays that reflect off + left/right file edges. Stops on capture or friendly block. + Respects the existing move-generator blocking logic. Stays + within rank bounds. + `incompatibleWith: ["bouncing-pieces-2", "wrap-board"]` + (cylinder already wraps; bouncing is redundant). + - `packages/chess/src/presets/bouncing-pieces.test.ts`: 10+ tests: + bishop on e4 reaches squares via right-edge bounce, queen rays + reflect and stop on block, capture on reflected ray works, + friendly block stops before the reflection. + + **Parallelization**: Phase E concurrent. Blocks: E.4. Blocked by: none. + + **Commit**: `feat(presets): bouncing-pieces` + +- [ ] E.4. **`bouncing-pieces-2` preset** + + **What to do**: + - `packages/chess/src/presets/bouncing-pieces-2.ts`: like + bouncing-pieces but reflects off ALL four edges. **Max + reflection count capped at 2** (prevents infinite-loop ray + computation on empty board; document). Extends/replaces + bouncing-pieces; both shouldn't be active simultaneously. + - `packages/chess/src/presets/bouncing-pieces-2.test.ts`: 8+ + tests. + + **Parallelization**: Phase E concurrent. Blocks: F.1. + Blocked by: E.3. + + **Commit**: `feat(presets): bouncing-pieces-2` + +- [ ] E.5. **Phase E verification gate** + + **What to do**: `bun run check` green. + + **Commit**: `chore(sisyphus): Phase E (movement variants) complete` + +--- + +## Phase F — Lobby Integration + E2E + +- [ ] F.1. **Audit `incompatibleWith` graph** + + **What to do**: Add `packages/chess/src/presets/presets.test.ts` + (if missing) or extend the existing preset-registry test. Reflexive + + symmetric check: if A declares B incompatible, B MUST declare A + incompatible (or explicitly opt out via an `allowedOneWay: ["B"]` + field with a comment — prefer mutual). Runs across every registered + preset. Failure message names the pair. + + **Parallelization**: Phase F. Blocks: F.5. Blocked by: B-E. + + **Commit**: `test(presets): enforce incompatibleWith symmetry` + +- [ ] F.2. **`suggestedPresets` wiring in premade layouts** + + **What to do**: + - `packages/chess/src/layouts/knightmate.ts`: + `suggestedPresets: ["knightmate-rules"]` (done in B.1 task; this + step is the audit that every layout's suggestions point at real + presets). + - `packages/chess/src/layouts/monster.ts`: `["monster-rules"]`. + - `packages/chess/src/layouts/pawns-only.ts`: + `["first-promotion-wins"]`. + - `packages/chess/src/layouts/dunsany.ts`: `[]` (no canonical rules). + - `packages/chess/src/layouts/horde.ts`: `[]`. + - `packages/chess/src/layouts/dual-classic.ts`: `["dual-king"]`. + - `packages/chess/src/ui/LayoutPicker.tsx` (from layouts plan): + render a small "Suggested rules" badge group next to each layout + when `suggestedPresets.length > 0`. Tap to enable. + - Add a unit test verifying every `suggestedPresets` entry + references a registered preset id (prevents dangling links). + + **Parallelization**: Phase F. Blocks: F.4, F.5. Blocked by: B.1-B.4, + C.2. + + **Commit**: `feat(layouts-ui): suggested-preset chips` + +- [ ] F.3. **Lobby RulesDrawer preset grouping** + + **What to do**: + - `packages/chess/src/ui/RulesDrawer.tsx`: group presets by category + (King Variants, Objectives, Movement, Multi-move, Pieces, Misc). + Each preset declares a `category: "king" | "objective" | "movement" | "multi-move" | "pieces" | "misc"` + field (new, optional for legacy presets — default to `"misc"`). + Render a section header per category; preserve existing filter/ + search UI. + - Visual separator between categories (thin `border-neutral-200`). + - Keep the existing enable/disable chip semantics; no functional + change, purely organizational. + + **Parallelization**: Phase F. Blocked by: B-E. + + **Commit**: `feat(ui): group rules drawer presets by category` + +- [ ] F.4. **Rule-variant e2e smoke** + + **What to do**: `packages/chess/e2e/rule-variants.spec.ts` (new). + Three representative scenarios end-to-end: + 1. **Knightmate**: open lobby → pick Knightmate layout → enable + knightmate-rules chip → Play Solo → make any move → attack + a knight → check banner appears → attack the king → no + check banner (king not royal). + 2. **Double-move**: enable double-move → white plays two moves + → black plays one → turn indicator flips correctly. + 3. **Suicide-chess**: enable suicide-chess → play pieces until + a capture is available → verify only captures are offered + in the legal-move UI. + + **Parallelization**: Phase F. Blocks: F.5. + Blocked by: B.1, B.2, D.1, F.2. + + **Commit**: `test(e2e): rule-variant vertical slice` + +- [ ] F.5. **Docs + final regression** + + **What to do**: + - `packages/chess/docs/PRESET-API.md`: add a "Rule Variants + Gallery" section with a one-paragraph description of each of + the 14 new presets. + - `packages/chess/RULES.md`: cross-reference each preset with the + greenchess.net entry, link back to + https://greenchess.net/variants.php?cat=4 for authority. + - Run full regression: `bun run check` + full Playwright suite + (server up for multiplayer scenarios). + + **Parallelization**: Phase F finale. Blocks: Final Wave. + Blocked by: F.1, F.2, F.3, F.4. + + **Commit**: `docs(preset-api): rule variants gallery + RULES.md cross-refs` + +--- + +## Final Verification Wave + +Run every reviewer AFTER Phase F lands. All must return APPROVE before +marking the plan complete. Pattern mirrors T3's F1-F4. + +- [ ] F1. **Plan compliance audit** — oracle + - All 31 top-level tasks checked. + - 4 new hooks declared in `packages/chess/src/presets/registry.ts`. + - 14 preset `.ts` files + matching `.test.ts` exist. + - `HalfMovesThisTurn` in `ChessAttrMap`. + - `dual-classic.ts` layout file present and registered. + - Tier 3 (`royalty-transfer`) explicitly NOT present. + +- [ ] F2. **Code quality review** — unspecified-high + - Zero `as any`, `@ts-ignore`, `@ts-expect-error` in non-test source. + - `as unknown as X` count does not increase vs baseline. + - Every new preset declares `incompatibleWith`. + - `incompatibleWith` graph is symmetric (F.1 test proves). + - Every new preset has ≥ 8 behavioral tests. + - Hook dispatch in `engine.ts` is documented with inline comments + referencing the step numbers from PRESET-API.md. + +- [ ] F3. **Manual QA** — unspecified-high with playwright + - Full Playwright regression green. + - Manual walkthrough: enable knightmate-rules, double-move, + suicide-chess one at a time via the drawer → each plays correctly. + - Visual check: RulesDrawer category grouping renders cleanly. + +- [ ] F4. **Scope fidelity** — deep + - No hook added outside the 4 declared (getRoyalPieces, + filterLegalMoves, shouldAdvanceTurn, overridePieceMoves). + - No royalty-transfer preset smuggled in. + - No PGN notation changes. + - No ranking/ELO code. + - Non-8×8 variants absent. + +**Approval workflow**: Each reviewer returns APPROVE or REJECT. On +reject, fix in the same session via `task(session_id=..., prompt= +"REJECT reason: ... fix by ...")`. Re-run after fix. Do NOT mark the +plan complete until all four report APPROVE. + +**Commit**: `chore(sisyphus): Rule Variants Final Verification Wave — all reviewers APPROVE` + +--- + +## Parallel Execution Map + +``` +Phase A (architecture) — 5 tasks, A.1-A.4 parallel, A.5 gate + A.1 getRoyalPieces ──┐ + A.2 filterLegalMoves ─┤ + A.3 shouldAdvanceTurn ┤ → A.5 docs + gate + A.4 overridePieceMoves┘ + +Phase B (Tier 1) — 5 tasks, B.1-B.4 parallel, B.5 gate + B.1 knightmate ──────┐ + B.2 double-move ─────┤ + B.3 monster ─────────┤ → B.5 gate + B.4 first-promotion ─┘ + +Phase C (royal T2) — 4 tasks, C.1-C.3 parallel, C.4 gate +Phase D (objective T2) — 4 tasks, D.1-D.3 parallel, D.4 gate +Phase E (movement T2) — 5 tasks, E.1+E.3 parallel, E.2 after E.1, + E.4 after E.3, E.5 gate + +Phase F (integration) — 5 tasks, F.1-F.3 parallel, F.4 after F.2, + F.5 finale + +Final Wave — F1-F4 parallel +``` + +Recommended wave size: 3-5 tasks per parallel batch. Never > 5. +Every delegated agent runs with its own disjoint file set. + +--- + +## Risks (recap — full rationale in design doc) + +1. **Hook ordering**. `filterLegalMoves` is pipeline-composable (safe), + but `shouldAdvanceTurn` and `overridePieceMoves` are first-match-wins. + Mitigation: document in PRESET-API.md + `incompatibleWith` between + any two presets that both override the same hook for the same + subject. +2. **Suicide + HP edge case**. Capture-compulsory + non-lethal HP + capture: DOES the no-kill hit satisfy compulsion? YES (the move IS + a capture). Documented in D.1. +3. **Multi-royal performance**. Check detection iterates royals. With + dual-king (2) + coregal (1 king + N queens), cost is linear in + royals. Realistic max ~5 pieces. Profile only if it shows up. +4. **Check banner UX with multi-royal**. Which royal is in check? + Deferred to a small follow-up UI task (Risks 4 of design doc). +5. **Promotion to king (dual-king underpromotion)**. 3+ kings per side + possible. Rules stay sound; verify `getAllLegalMoves` doesn't explode + via an integration test with 3 kings. +6. **Berolina en passant interpretation**. Multiple valid reads. Pick + one (reflected en passant to the skipped square) and document + in-file. +7. **Override-piece-moves collisions**. Two presets overriding pawns = + undefined behavior. Mitigation: `incompatibleWith` declarations + + dev-mode console warning (implemented in A.4). + +--- + +## Glossary + +- **Royal**: a piece whose capture/mate ends the game. FIDE default = + PieceType king. Knightmate = knight. Coregal = king + queen. +- **Half-move**: one player's single move. Turn = typically 1 half-move; + Monster/Double-move turn = 2 half-moves. +- **Compulsory capture**: Suicide Chess — if any capture is legal, the + mover MUST choose a capture. +- **Royal set**: the `EntityId[]` returned from `getRoyalPieces`. Can + be empty (Suicide) for "no royalty". + +--- + +## When to Stop Mid-Plan + +If tool budget or context pressure forces an early stop: + +- **After Phase A**: foundation lands; no new presets. Document the + hooks as "available, no active users" — Tier 1 can ship in a + follow-up. +- **After Phase B**: T1 presets close the layouts deferrals. Ship. + Tier 2 is a future epic. +- **After Phase C or D**: Tier 2 royal/objective variants ship. The + remaining tier (movement variants E.1-E.4) becomes its own follow-up + mini-epic. +- **After Phase E but before F**: feature-complete but UI integration + (category grouping, suggestion chips) is a polish follow-up. + +In every case: update this plan's checkboxes to reflect actual landed +state, append a `## Early Stop` section with rationale, and do NOT run +the Final Verification Wave — reserve that for the full plan.