Introduces a pluggable StartingLayout abstraction so the engine can
open from positions other than FIDE without per-caller special casing.
- layouts/{types,registry,index}.ts: StartingLayout + LAYOUT_REGISTRY,
mirroring the PRESET_REGISTRY / PIECE_TYPE_REGISTRY pattern.
- starting-position.ts: CLASSIC_LAYOUT + applyLayout(session, layout)
as the parametrized spawn path. generateStartingPosition stays as
a thin back-compat wrapper so no existing call site changes.
- layouts/{classic,empty}.ts: first two premades registered via
side-effect imports from the barrel.
- engine.ts: ChessEngine constructor now accepts either legacy
(activePresets) positional or new options-bag form
({ activePresets?, layout? }). Detection uses a method-shape
probe rather than instanceof so both overload forms compose
cleanly under strict TS.
- Tests: 11 new tests in starting-position.test.ts + engine-presets
cover applyLayout ordering, hasMoved pre-revocation, empty
layout, classic equivalence, options-bag equivalence with legacy.
Also lands the full execution plans for starting-layouts and
rule-variants under .sisyphus/plans/ (both Momus-reviewed OKAY).
941 tests passing; bun run check clean.
31 KiB
Rule Variants — Greenchess cat=4 Preset Suite
TL;DR
Ship the rule-variant presets from greenchess.net cat=4. Adds four new preset-API hooks (royal piece override, global legal-move filter, turn-advancement override, per-piece-move override), then 12 new presets built on them.
Deliverable: users toggle any of 12 new rule-variant presets in the lobby, which compose cleanly with existing presets (HP, queen-splits, etc.) and the layouts system from the prior plan. Existing 930+ tests stay green; each new preset ships with 8-15 behavioral tests.
Tiered so the user can stop mid-execution. Tier 1 (T1) must ship to close the deferred rules from the layouts plan; Tier 2 (T2) rounds out greenchess cat=4 coverage; Tier 3 (T3) is explicitly deferred (Royalty Transfer needs mid-game state we don't want to build yet).
Phases: 6 phases, 31 tasks, hard-gated by bun run check between each.
Context
What Shipped Last
- Preset-flexibility architecture (commit
cc30545): hook context objects,PIECE_TYPE_REGISTRY, damage pipeline, preset-state storage, visual-effect bus. - Starting Layouts plan (
.sisyphus/plans/starting-layouts.md, queued): introduces Dunsany, Monster, Pawns-Only, Horde, Knightmate, Chess960 as layout-only presets — layout descriptions explicitly flag that the rules for Monster/Knightmate/Pawns-Only ship here. - Existing relevant presets:
wrap-board(cylinder),pawns-move-backward,double-pawn-sprint,knights-leap-twice,knight-immunity,bishops-ignore-color,rook-warp,explosive-rook,piece-hp,king-heals,poisoned-squares,queen-splits,capture-to-win,last-piece-standing,pawn-diagonal-no-capture.
What the Current Preset API Can't Express
Audited every greenchess cat=4 variant against the current PresetDef surface. Gaps found:
- Royal piece override —
isInCheck,isCheckmate,filterSelfCheckMoves,isStalemateall hardcode"king"as the royal piece type. Knightmate (knight is royal), Coregal (king AND queen royal), Dual (two kings, either mates) can't be expressed without a hook.- File anchors:
rules/check.ts:83-97,rules/checkmate.ts,rules/stalemate.ts,rules/check.ts:filterSelfCheckMoves.
- File anchors:
- Global legal-move filter — current
filterMoves(moves, engine, pieceId)is per-piece. Suicide Chess's "compulsory capture" rule needs "if ANY move on the board is a capture, remove every non-capture". Requires post-aggregation filter.- File anchor:
engine.ts:getAllLegalMoves— aggregation happens but no per-color post-filter hook exists.
- File anchor:
- Turn-advancement override —
applyMoveunconditionally flips Turn. Double-move and Monster Chess need "don't flip until N half-moves this turn".- File anchor:
engine.ts:applyMoveturn flip near line ~900.
- File anchor:
- Piece-move replacement —
getExtraMovesadds to a piece's move list;filterMovessubtracts. Neither cleanly REPLACES. Berolina's pawns move diagonally and capture orthogonally — you don't want to intersect with FIDE pawn rules, you want to swap them wholesale.- Today possible via
filterMovesreturning[]+getExtraMovesreturning the new set, but that's a foot-gun (two hooks must stay in sync; other presets may still contribute extras).
- Today possible via
What DOES Work Already
- Bouncing bishops/queens:
getExtraMoveson bishop/queen PieceType that computes reflected rays. Adds, doesn't replace — great. - Extinction Chess (capture-all-of-a-type-wins):
onCheckGameResult+presetStatefor tracking. Pure existing surface. - Capture-all (no check/checkmate):
shouldFilterSelfCheck → false+onCheckGameResultthat reads "any opponent pieces left?". - Pawns-only first-promotion-wins:
describeMoveEffectdetects promotion +onCheckGameResultreads promotion count. - Knightmate / Coregal / Dual ROYAL-PIECE handling needs the new hook; the actual "checkmate the royal" flow is then automatic via existing machinery.
Out of Scope (Deferred Hard)
- Royalty Transfer Chess (two kings but only one is active at a time). Needs mid-game "which king is active" state flip triggered by a non-move action. This UX doesn't fit current "one move at a time" plumbing. Deferred to a post-v1 plan.
- Any 4-player / multi-side variants. Engine assumes exactly two sides; changing that is not in cat=4 anyway.
- Non-8×8 board variants from other greenchess cats (Capablanca, Janus, etc.). Needs board-dimensions refactor — see the layouts plan's deferrals section.
- Castling interactions with Knightmate. Knightmate says "regular pieces capture normally; knight is royal." Castling rights conceptually apply to the royal piece today (king); under Knightmate we treat castling as FIDE-normal (involves the non-royal king on e1/e8 — unchanged). Document this as a design call, not a feature gap.
- PGN notation updates. Algebraic notation with non-king royalty (
N+,N#?) is a rabbit hole. The move-log will display effects viadescribeMoveEffect, not via FIDE notation. Defer.
Work Objectives
Core Objective
Extend the preset API with four additive hooks that unblock 12 new variants, then ship the variants themselves, each fully tested in isolation and in composition with at least one compatible existing preset.
Primary Deliverables
- Hook:
getRoyalPieces(ctx): EntityId[] | undefined— overrides "which pieces of this color count as royal" for check/checkmate/self-check-filter detection. Default (undefined from every preset) → engine usesPieceType === "king". - Hook:
filterLegalMoves(ctx): LegalMove[]— post-aggregation filter on the full per-color legal-move list. Engine applies these in preset-registration order. - Hook:
shouldAdvanceTurn(ctx): boolean | undefined— preset intervenes in the turn-flip decision. Firstfalsewins; default (all undefined) → turn advances normally. - Hook:
overridePieceMoves(engine, pieceId): LegalMove[] | undefined— return non-undefined to REPLACE the default generator's output for that piece. First match wins. - Tier 1 presets (close the layouts-plan deferrals):
knightmate-rules— knight is royal piece.double-move— both players move twice per turn (last move of turn flips sides).monster-rules— white moves twice; black moves once (asymmetric scope).first-promotion-wins— first player to promote a pawn wins.
- Tier 2 presets (cat=4 completeness):
coregal— king AND queen are royal.dual-king— two kings per side; mating either wins (strong).weak-dual-king— two kings per side; must mate both (weak).suicide-chess— captures are compulsory; lose-all-pieces wins; no check/checkmate.capture-all— no check/checkmate; capture every opposing piece to win.extinction-chess— capture every opposing piece of a chosen type to win.berolina-pawns— pawns push diagonally, capture orthogonally forward.berolina-pawns-2— like berolina plus sideways capture.bouncing-pieces— bishop/queen bounce off left/right edges.bouncing-pieces-2— bounce off all four edges.
- Test utilities updates:
test-utils.tshelpers extended for multi-royal assertions.
Secondary Deliverables
- Lobby UI: the RulesDrawer's dependency-chip UI already handles
requires/incompatibleWith. Each new preset declares these correctly — no UI change needed. - Auto-suggest in layouts picker: when a user picks "Monster" layout, show a dimmed chip "Suggested: monster-rules" with a one-click enable. (Implemented in layouts plan; this plan just populates the
suggestedPresetsfield for each layout.) - E2E smoke: create room with Dunsany layout + suicide-chess preset; verify compulsory capture enforces server-side.
Non-Goals (clarifying)
- No Chess960 rule changes (Chess960 is a layout, not a rule, per the layouts plan).
- No UI mini-animations for royal-piece-in-check unique to each variant — reuse the existing check banner.
- No ranking / ELO / SPRT tuning. These presets are opt-in novelty.
Tier Ordering
Each variant is labeled T1 (must-ship) / T2 (should-ship) / T3 (deferred).
| Variant | Tier | Rationale |
|---|---|---|
| knightmate-rules | T1 | Closes layouts-plan deferral for Knightmate. |
| double-move | T1 | Closes layouts-plan deferral for Monster / Double-move. |
| monster-rules | T1 | Asymmetric double-move + Monster layout. |
| first-promotion-wins | T1 | Closes layouts-plan deferral for Pawns-Only. |
| coregal | T2 | Cheap once royal hook exists. |
| dual-king | T2 | Same. |
| weak-dual-king | T2 | Same. |
| suicide-chess | T2 | Popular variant; exercises filterLegalMoves. |
| capture-all | T2 | Pure existing-API composition. |
| extinction-chess | T2 | Pure existing-API. |
| berolina-pawns | T2 | Exercises overridePieceMoves. |
| berolina-pawns-2 | T2 | Trivial once berolina exists. |
| bouncing-pieces | T2 | Uses existing getExtraMoves. |
| bouncing-pieces-2 | T2 | Extension of bouncing. |
| royalty-transfer | T3 | Mid-game state flip — deferred. |
Architecture Sketch
Hook additions to PresetDef
// rules/check.ts and callers
interface RoyalContext extends HookContext {
readonly color: "white" | "black";
}
// Returned array = the royal pieces for this color. Engine uses the
// UNION of all active presets' returns (for Coregal: king + queens).
// Returning undefined = "I don't override, use default (king)."
readonly getRoyalPieces?: (ctx: RoyalContext) => readonly EntityId[] | undefined;
// Returns the filtered legal-move list. Engine runs each hook in
// preset-registration order, piping the result into the next.
interface FilterLegalMovesContext extends HookContext {
readonly moves: readonly LegalMove[];
readonly color: "white" | "black";
}
readonly filterLegalMoves?: (ctx: FilterLegalMovesContext) => LegalMove[];
// Returns false to KEEP the current turn (same side to move).
// Engine iterates in preset-registration order; first false wins.
interface TurnAdvanceContext extends HookContext {
readonly mover: "white" | "black";
readonly halfMovesThisTurn: number; // tracked by engine
}
readonly shouldAdvanceTurn?: (ctx: TurnAdvanceContext) => boolean | undefined;
// Return non-undefined to REPLACE default moves for `pieceId`.
// First preset to return wins.
readonly overridePieceMoves?: (
engine: ChessEngine,
pieceId: EntityId,
) => LegalMove[] | undefined;
Engine integration
// rules/check.ts — isInCheck becomes:
export function isInCheck(session: Session, color: PieceColor, royalTypes?: Set<PieceType>): boolean
// With royalTypes undefined: uses {"king"} (current behavior).
// ChessEngine wraps this: computes royalTypes from preset returns, passes in.
// Engine state: tracks halfMovesThisTurn for shouldAdvanceTurn.
// Resets to 0 on every turn flip. Increments on each applyMove.
// Piece move generation dispatch order:
// 1) overridePieceMoves (first match wins)
// 2) PIECE_TYPE_REGISTRY default
// 3) getExtraMoves (each preset contributes)
// 4) filterMoves (each preset filters)
// 5) aggregate all pieces' moves
// 6) self-check filter (suppressible via shouldFilterSelfCheck)
// 7) filterLegalMoves (each preset runs in order)
New session fact: HalfMovesThisTurn on GAME_ENTITY
Used by shouldAdvanceTurn hook. Serialized naturally with other game-entity facts. Reset to 0 on turn flip.
Phase A — Hook API Extensions (architecture)
A.1 — getRoyalPieces hook
[packages/chess/src/presets/registry.ts]Add:RoyalContexttype,getRoyalPieceshook signature onPresetDef— expect: compiles; existing presets continue to declare no royal override (all undefined).[packages/chess/src/rules/check.ts]Refactor: Add optionalroyalTypes?: Set<PieceType>parameter toisInCheck; when present, find royal pieces by those types, else default{"king"}. Same forisSquareAttackedcallers that need it — expect: tests still green without passing the new param (default maintained).[packages/chess/src/rules/checkmate.ts]Refactor: Accept royal-pieces set; treat loss as "no legal move AND at least one royal in check". Document behavior under multi-royal: with Coregal, game ends if ANY royal is checkmated (OR), not all.[packages/chess/src/rules/stalemate.ts]Refactor: Accept royal-pieces set; stalemate = "no legal move AND no royal in check".[packages/chess/src/engine.ts]Refactor: Add privategetActiveRoyalTypes(color): Set<PieceType>that unions returns from every active preset'sgetRoyalPieceshook, maps EntityId → PieceType via session facts. When union is empty → default{"king"}. Use this in all check/mate/stalemate calls — expect: no active royal-override preset = engine behavior unchanged; 930+ tests green.[packages/chess/src/presets/royal-pieces.test.ts]Create: Prototype "double-king" preset that returns all kings (should typically be one) for each side; assertisInCheckstill works — expect: green.
A.2 — filterLegalMoves hook
[packages/chess/src/presets/registry.ts]Add:FilterLegalMovesContext,filterLegalMoveshook — expect: compiles.[packages/chess/src/engine.ts]Refactor: After existing self-check filter, iterate active presets and pipe each'sfilterLegalMovesthrough the list. Document: "order matters; preset authors should commute where possible or declareincompatibleWith" — expect: no-op when no preset implements.[packages/chess/src/presets/filter-legal-moves.test.ts]Create: Prototype "no-a-file moves" preset filtering out all moves from/to a-file; assert engine respects it across multiple pieces — expect: green.
A.3 — shouldAdvanceTurn hook + HalfMovesThisTurn tracking
[packages/chess/src/schema.ts]Add:HalfMovesThisTurntoChessAttrMapwith value typenumber— expect: compiles.[packages/chess/src/starting-position.ts]Update: SeedHalfMovesThisTurn = 0on GAME_ENTITY alongside Turn — expect: every new engine/layout has the fact.[packages/chess/src/layouts/classic.ts]and all layouts in the layouts plan: no change needed because layouts call the shared starting-position helper for game facts.[packages/chess/src/presets/registry.ts]Add:TurnAdvanceContext,shouldAdvanceTurnhook — expect: compiles.[packages/chess/src/engine.ts]Refactor: InapplyMove, incrementHalfMovesThisTurnbefore the turn-flip decision. Iterate active presets'shouldAdvanceTurn; if any returnsfalse, skip the turn flip. When flipping, resetHalfMovesThisTurn = 0— expect: no active turn-override = behavior unchanged.[packages/chess/src/presets/turn-advance.test.ts]Create: Prototype "never-flip" preset; assert white keeps moving indefinitely — expect: green.
A.4 — overridePieceMoves hook
[packages/chess/src/presets/registry.ts]Add:overridePieceMoveshook — expect: compiles.[packages/chess/src/engine.ts]Refactor: IngetAllLegalMoves, before callinglookupMoveGenerator, iterate active presets'overridePieceMoves(engine, pieceId). First non-undefined wins; cache result and skip default generator. Log collision warnings (two presets override same piece) in dev — expect: no preset overrides = default generator used.[packages/chess/src/presets/override-piece-moves.test.ts]Create: Prototype "lame-knight" preset that overrides knight moves to single-square orthogonal; assert knight behaves differently only with preset active — expect: green.
A.5 — Phase A verification gate
[repo root]Verify:bun run check— expect: 930+ tests green; no behavioral change from any new hook being undefined.[packages/chess/docs/PRESET-API.md]Update: Document the four new hooks with worked examples. Add a "Hook ordering" diagram covering all 10 preset hooks.
Phase B — Tier 1 Presets (layouts deferrals)
B.1 — knightmate-rules
[packages/chess/src/presets/knightmate-rules.ts]Create: ImplementsgetRoyalPiecesreturning all knight entities ofcolor. Since Knightmate's layout has kings replacing knights (non-royal), this preset treats knights as the royal type. Declaresrequires: [](works with any layout that has knights). Comment: "works best with the Knightmate layout but not strictly dependent — with FIDE layout, kings become non-royal and knights become royal, which is playable but weird" — expect: registers, typechecks.[packages/chess/src/presets/knightmate-rules.test.ts]Create: 10+ tests. Load Knightmate layout; assert (a) moving a non-royal king into attack is legal, (b) a knight in check restricts moves to resolving the check, (c) checkmating the last knight ends the game, (d) capture-all-knights is NOT game-over if any knight survives, (e) composition with piece-hp — knight loses HP on capture attempts rather than dying — expect: green.[packages/chess/src/layouts/knightmate.ts](from layouts plan) Update:suggestedPresets: ["knightmate-rules"]now points at a real preset — expect: layout picker shows enabled chip.
B.2 — double-move
[packages/chess/src/presets/double-move.ts]Create: ImplementsshouldAdvanceTurnreturningfalsewhenhalfMovesThisTurn < 2. Also implementsgetRoyalPieces? No — use default king logic. Notes: "cannot be in check mid-turn" is the classic Double-move rule; we enforce it viashouldFilterSelfCheck— second half-move still considers king safety. DeclaresincompatibleWith: ["monster-rules"](both override turn-advance for white). Scope defaults to"both"— expect: registers, typechecks.[packages/chess/src/presets/double-move.test.ts]Create: 12+ tests. (a) white plays two moves before black, (b) halfMovesThisTurn resets on flip, (c) checking opponent on first move ends opponent's turn immediately (edge case: some Double-move rulesets forbid check on move 1 — we go with the common "check allowed, opponent must respond on both of their moves" interpretation and document), (d) composition with piece-hp, (e) composition with king-heals (heal fires on second-move turn-flip only) — expect: green.
B.3 — monster-rules
[packages/chess/src/presets/monster-rules.ts]Create: Scope-aware. For scope=white,shouldAdvanceTurnreturns false whenmover === "white"andhalfMovesThisTurn < 2. For scope=black, black behaves normally. DeclaresincompatibleWith: ["double-move", "suicide-chess"]— expect: registers.[packages/chess/src/presets/monster-rules.test.ts]Create: 10+ tests. White moves twice, black moves once. Combined with Monster layout → white plays king + 4 pawns twice each turn — expect: green.[packages/chess/src/layouts/monster.ts](from layouts plan) Update:suggestedPresets: ["monster-rules"].
B.4 — first-promotion-wins
[packages/chess/src/presets/first-promotion-wins.ts]Create:onAfterMoveinspects the just-applied move viaengine.moveLog[last]; ifpromotion !== null, records winner viapresetState.onCheckGameResultreturns the winner if set, else undefined. Also implementsshouldFilterSelfCheck → falseand overrides checkmate logic so the game doesn't end prematurely on check. DeclaresincompatibleWith: ["capture-to-win", "last-piece-standing", "extinction-chess", "suicide-chess", "capture-all"]— expect: registers.[packages/chess/src/presets/first-promotion-wins.test.ts]Create: 8+ tests. (a) first pawn promotion wins, (b) works with Pawns-Only layout, (c) composition with double-move (second move can also trigger win), (d) composition with piece-hp — expect: green.[packages/chess/src/layouts/pawns-only.ts](from layouts plan) Update:suggestedPresets: ["first-promotion-wins"].
B.5 — Phase B verification gate
[repo root]Verify:bun run check— expect: green. Manual smoke: open lobby, pick Knightmate layout + knightmate-rules, play a short game via Playwright script.
Phase C — Tier 2 Royal-Variant Presets
C.1 — coregal
[packages/chess/src/presets/coregal.ts]Create:getRoyalPiecesreturns union of kings + queens forcolor.incompatibleWith: ["knightmate-rules", "dual-king", "weak-dual-king"]— expect: registers.[packages/chess/src/presets/coregal.test.ts]Create: 10+ tests. (a) mating only the king ends the game (queen can't save), (b) mating only the queen ends the game, (c) with no queen on board, behaves as FIDE, (d) pins now affect the queen too (moving a pinned queen into attack leaves another royal in check → illegal) — expect: green.
C.2 — dual-king
[packages/chess/src/presets/dual-king.ts]Create:getRoyalPiecesreturns ALL king entities forcolor(any king in check that can't escape = checkmate). Works best with a 2-king layout, but also makes pawn-underpromotion-to-king meaningful.incompatibleWith: ["weak-dual-king", "coregal", "knightmate-rules"]— expect: registers.[packages/chess/src/presets/dual-king.test.ts]Create: 8+ tests. Use a custom test layout with 2 kings per side. Assert mating either king ends the game — expect: green.- Note: Premade "Dual Chess" layout not shipped in layouts plan. For this preset, ship a
dual-classiclayout (2 kings each, otherwise FIDE) in the layouts plan OR recommend users construct one via the custom editor. Add a new layout filepackages/chess/src/layouts/dual-classic.tshere.
C.3 — weak-dual-king
[packages/chess/src/presets/weak-dual-king.ts]Create: Implements bothgetRoyalPieces(returns all kings) AND an override toonCheckGameResult. Game-over only when ALL kings of one side are captured/mated. First mate of one king does NOT end the game.incompatibleWith: ["dual-king", "coregal", "knightmate-rules"]— expect: registers.[packages/chess/src/presets/weak-dual-king.test.ts]Create: 8+ tests. (a) one king lost → game continues, (b) both kings lost → game ends, (c) last king in check with mate → game ends — expect: green.
C.4 — Phase C verification gate
[repo root]Verify:bun run check— expect: green. Add an integration test: player runs coregal + piece-hp; asserts queen loses HP but king is still the primary royal.
Phase D — Tier 2 Objective-Variant Presets
D.1 — suicide-chess
[packages/chess/src/presets/suicide-chess.ts]Create:filterLegalMoves: if any move in the list hasisCapture === true, return only captures.shouldFilterSelfCheck: returns false (king is a regular piece in Suicide).getRoyalPieces: returns[]empty array (no royalty — interpret: "no pieces are royal, check detection is a no-op"). This requires the engine's isInCheck to handle "empty royal set" → returns false always. Add this edge-case handling in the Phase A refactor if not already.onCheckGameResult: if one side has 0 pieces, declares that side the WINNER (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"]— expect: registers.
[packages/chess/src/presets/suicide-chess.test.ts]Create: 14+ tests. (a) captures compulsory when available, (b) non-captures legal otherwise, (c) losing all pieces wins, (d) pawn promotion still legal (promotion to queen is fine), (e) composition with piece-hp — captures only count when piece actually dies (capture with HP remaining doesn't satisfy compulsion? — design call: YES it does, because the attempt IS a capture move even if the target survives. Document) — expect: green.
D.2 — capture-all
[packages/chess/src/presets/capture-all.ts]Create:shouldFilterSelfCheck → falsegetRoyalPieces → []onCheckGameResult: winner = side whose opponent has 0 pieces.incompatibleWith: ["suicide-chess", "capture-to-win", "last-piece-standing", "extinction-chess", "first-promotion-wins"]— expect: registers.
[packages/chess/src/presets/capture-all.test.ts]Create: 8+ tests — expect: green.
D.3 — extinction-chess
[packages/chess/src/presets/extinction-chess.ts]Create: Configurable target type viapresetState({targetType: PieceType}). Default:"pawn"(common extinction target — game about pawn attrition). UI chip shows "Extinction: pawns".onCheckGameResultreturns winner when opposite side has 0 pieces of target type — expect: registers.[packages/chess/src/presets/extinction-chess.test.ts]Create: 10+ tests covering each target type — expect: green.- UI stretch: Cycle target type via chip click in RulesDrawer. Out-of-scope for this phase; add as a follow-up UI task after the plan lands.
D.4 — Phase D verification gate
[repo root]Verify:bun run check— expect: green.
Phase E — Tier 2 Movement-Variant Presets
E.1 — berolina-pawns
[packages/chess/src/presets/berolina-pawns.ts]Create:overridePieceMovesfor pawn pieces only. Generates: forward diagonals (non-capture push), forward orthogonal (capture only). Double-move from home rank = two diagonal pushes. En passant reinterpreted: if the opponent's pawn just made a double diagonal push landing on a square adjacent to yours, you can capture it orthogonally to the skipped square. DeclaresincompatibleWith: ["pawn-diagonal-no-capture", "berolina-pawns-2", "pawns-move-backward"]— expect: registers.[packages/chess/src/presets/berolina-pawns.test.ts]Create: 15+ tests. Double-push, capture, en passant, promotion (yes — diagonal push to promotion rank still promotes), block detection — expect: green.
E.2 — berolina-pawns-2
[packages/chess/src/presets/berolina-pawns-2.ts]Create: Identical to berolina-pawns but also allows sideways captures (same-rank, adjacent file).incompatibleWith: ["berolina-pawns", ...same list]— expect: registers.[packages/chess/src/presets/berolina-pawns-2.test.ts]Create: 6+ tests (diff from berolina-pawns) — expect: green.
E.3 — bouncing-pieces
[packages/chess/src/presets/bouncing-pieces.ts]Create:getExtraMovesfor bishop + queen. Computes diagonal rays that reflect off the left/right file edges (file wraps to the opposite side with reflected direction). 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, so bouncing is redundant) — expect: registers.[packages/chess/src/presets/bouncing-pieces.test.ts]Create: 10+ tests. Bishop on e4 can reach squares via right-edge bounce; queen rays reflect and stop on block; capture on reflected ray works — expect: green.
E.4 — bouncing-pieces-2
[packages/chess/src/presets/bouncing-pieces-2.ts]Create: Like bouncing-pieces but reflects off all four edges. Max reflection count capped at 2 (prevents infinite-loop ray computation on empty board). Document the cap — expect: registers.[packages/chess/src/presets/bouncing-pieces-2.test.ts]Create: 8+ tests — expect: green.
E.5 — Phase E verification gate
[repo root]Verify:bun run check— expect: green.
Phase F — Lobby Integration + E2E
F.1 — Audit incompatibleWith graph
[packages/chess/src/presets/presets.test.ts]Add: Reflexive + symmetric check — if A declares B incompatible, assert B declares A incompatible (or explicitly opt out with a comment). Automate across all presets — expect: all 12 new presets pair correctly.
F.2 — suggestedPresets wiring in layouts
[packages/chess/src/layouts/*.ts](each premade) Update: PopulatesuggestedPresetsfor Knightmate → knightmate-rules, Monster → monster-rules, Pawns-Only → first-promotion-wins, Dunsany → [] (no canonical rules), Horde → [] — expect: lobby shows suggestion chips.[packages/chess/src/ui/LayoutPicker.tsx](from layouts plan) Extend: Render a small "Suggested rules" badge group next to each layout whensuggestedPresets.length > 0. Tapping a badge enables that preset — expect: click-to-enable works.
F.3 — Lobby rule filter
[packages/chess/src/ui/RulesDrawer.tsx]Extend: Group the 12+existing presets into sections (King Variants, Objectives, Movement, Multimove, Pieces, …). Use a visual separator. Existing filtering/search still works — expect: drawer is navigable with 20+ presets.
F.4 — E2E: rule-variant smoke
[packages/chess/e2e/rule-variants.spec.ts]Create: Playwright test covering 3 representative variants end-to-end. (a) Knightmate: attack king, make a non-king move, attack knight → receive check banner; (b) Double-move: white plays two moves, black plays one, repeat; (c) Suicide-chess: capture becomes compulsory when available — expect: green.
F.5 — Docs + final check
[packages/chess/docs/PRESET-API.md]Update: New hooks section with worked example for each. Add a "Rule Variants Gallery" with one-paragraph descriptions of all 12.[packages/chess/RULES.md]Update: Cross-reference each new preset with greenchess's description. Link back to greenchess.net/variants.php?cat=4 for authority.[repo root]Verify:bun run check+ Playwright — expect: full green.
Risks
- Hook ordering matters more than before.
filterLegalMovesis pipeline-composable, butshouldAdvanceTurnandoverridePieceMovesuse first-match-wins. Mitigation: document ordering in PRESET-API.md; add incompatibleWith between presets that both override the same hook for the same subject (e.g., double-move vs monster-rules). - Suicide + HP edge case. Capture-is-compulsory + non-lethal-capture (HP): does "any capture available" check count moves that DEAL damage even if target survives? Design call in the plan: YES (the move IS a capture). Documented in D.1 notes.
- Multi-royal performance. Check detection iterates all royal pieces. With Dual-king (2 kings) and Coregal (1 king + N queens), per-move cost grows linearly. Mitigation: cap the royal set effectively at ~5 pieces (realistic max: king + all queens on a mid-game board). Profile only if it shows up on the critical path.
- Check banner UX with multi-royal. Current UI shows "Check!" on the king. With Coregal, which royal is attacked? Mitigation: adjust the banner to say "Check!" with a highlighted piece indicator rendered via the existing check-indicator component; deferred to a small follow-up UI task — not in this plan.
- Promotion to king (
dual-king+ underpromotion). Possible to pile up 3+ kings per side via promotion. Game rules stay sound; just verifygetAllLegalMovesdoesn't explode. Mitigation: integration test with 3 kings per side. - Berolina en passant interpretation. Several competing interpretations exist; we pick one (reflected en passant to the passed-over square) and document it in the preset's comments. If users disagree, open follow-up issue.
- Override-piece-moves collisions. Two presets overriding pawns: undefined behavior. Mitigation:
incompatibleWithdeclarations + dev-mode console warning.
Deferred / Post-Landing
- Royalty Transfer Chess (T3). Needs mid-game "which king is active" toggle — UX outside "one move at a time".
- PGN notation with non-king royalty. E.g.,
+on knight-in-check in Knightmate. Rabbit hole. - Ranked / ELO tracking per variant. Out of scope; variants are opt-in novelty.
- "Fairy chess" pieces (Amazon, Nightrider, Archbishop). Registered via
PIECE_TYPE_REGISTRYeasily; but these are a separate piece-type plan, not rule variants. - Per-variant tutorials / help popovers. Static text live in RULES.md and RulesDrawer descriptions today; a tutorial overlay is nice-to-have.
- Allow Extinction Chess target type to be picked via UI chip. Noted inside D.3; small follow-up UI task.