houserules/packages/chess/RULES.md
Joey Yakimowich-Payne ff049ea5eb
feat(thressgame-100): Wave 6 \u2014 preset cross-refs + WONT_FIX manifest
Wave 6 of thressgame-100 epic complete \u2014 closing wave. 70 total recipes.

8 NEW PRESET-STUB RECIPES (W6.0):
For ThressGame rules that are architecturally PRESET-shaped (not modifier-shaped),
shipped as discoverable but inert recipe stubs. Loading them shows a docs panel
pointing at the canonical preset implementation.

- tpl-preset-dual-king         \u2192 preset 'dual-king'
- tpl-preset-coregal           \u2192 preset 'coregal'
- tpl-preset-god-kings         \u2192 preset-hook shape (closest: knightmate-rules)
- tpl-preset-early-promotion   \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-proletariat       \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-short-stop        \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-trains-rights     \u2192 preset-hook shape (not yet a named preset)
- tpl-preset-pacman            \u2192 preset 'wrap-board'

All 8 use empty primitives: [] (validator allows it \u2014 no MIN_PRIMITIVE constraint).

RULES.md CROSS-REFERENCE SECTION (W6.1):
New section 'Cross-References \u2014 ThressGame Rules as Chess Presets' at lines
498\u2013545 of RULES.md. Format: preamble + 8-row mapping table + 'Why these are
preset-shaped' rationale + pointer to WONT_FIX manifest.

UI DISTINGUISHER (W6.2):
CustomModifierEditor.tsx adds visual marker for stub recipes (id startsWith
'tpl-preset-'):
- data-recipe-kind='preset-stub' attribute (vs 'modifier')
- Amber left border (border-l-4 border-l-amber-400)
- 'preset \u2192' badge (amber bg) instead of 'Load' badge (blue bg)
~25 lines added; signals that loading is essentially a no-op \u2014 canonical action
is enabling the preset elsewhere.

WONT_FIX MANIFEST (W6.3):
packages/chess/docs/THRESSGAME_WONT_FIX.md \u2014 151 lines documenting 6 rules
that cannot be implemented because upstream behavior is undefined or out of
scope:

- pawns_with_viagra      (line 1626 of ruleHooks.js: empty {} stub)
- estrogen               (line 1638: empty {} stub)
- knee_surgery           (line 1698: empty {} stub)
- pawns_learned_strength (line 1699: empty {} stub)
- parry (RPS handler)    (lines 1599\u20131601: comment routes to moveHandler.js \u2014
                          parry parity recipe ALREADY ships; this entry just
                          documents the upstream code-location split)
- pacman_style (modifier) (line 1670: body is {}; topology in getWrapMoves
                           outside hook system. Routed to chess preset
                           wrap-board; tpl-preset-pacman cross-references it)

Closing summary table ties back to coverage accounting:
  65 raw rules = 51 modifier-coverable + 8 preset-shaped + 6 WONT_FIX

bun run check: 3270 tests pass (no new tests; 0 regressions).
recipes.test.ts: 5 \u00d7 70 = 545 expect calls (was 513 for 62 recipes).

FINAL EPIC COVERAGE STATE:
- 50 unique ThressGame rules covered as modifier recipes (50/51 = 98 %)
- 8 ThressGame rules cross-referenced via preset stubs
- 6 ThressGame rules in WONT_FIX manifest (upstream stubs)
- 65 raw rules accounted for (50 + 8 + 6 + 1 ice_physics already shipped W2)
                              = 65/65 = 100 % accounted
- 70 total recipes in CUSTOM_MODIFIER_RECIPES
- 3270 tests passing across 265 test files
- 5 e2e specs (wave1\u2013wave5) all green via docker compose dev stack

Plan: .sisyphus/plans/thressgame-100.md
Notepads: .sisyphus/notepads/thressgame-100/
Evidence files: .sisyphus/evidence/thressgame-100-wave{1,2,3,4,5}.txt (gitignored)
2026-04-27 17:32:25 -06:00

50 KiB
Raw Permalink Blame History

Preset Custom Rules

This document is the authoritative specification for the 15 preset custom chess rules shipped with the @rules/chess package (v1). Each rule is implementable as a set of Rete II productions that either add to or override the base FIDE productions. No rule here requires user-authored JavaScript — all behaviour must be expressible via the declarative production + WME vocabulary defined in the engine spec.

This document drives implementation tasks P3.4 – P3.8.

Conventions

  • ID: kebab-case, globally unique, used as the registry key in packages/chess/src/presets/.
  • Mode:
    • additive — the rule adds new productions without retracting any base production.
    • override — the rule retracts (or short-circuits via higher salience) one or more base FIDE productions and installs replacements.
    • hybrid — both additive and override effects.
  • Requires: other preset IDs that must also be active (dependencies form a DAG — no cycles).
  • Incompatible With: preset IDs that produce contradictory WMEs or retract productions the other relies on.
  • HP meta-rule: the piece-hp preset extends the chess schema (packages/chess/src/presets/piece-hp.ts) with an hp: number attribute on every Piece WME, and replaces the FIDE capture production. Any rule that reads or writes HP must declare Requires: piece-hp.

Overview

# ID Name Category
1 pawns-move-backward Backward-Marching Pawns Movement Modifier
2 double-pawn-sprint Perpetual Sprint Movement Modifier
3 pawn-diagonal-no-capture Slanting Pawns Movement Modifier
4 knights-leap-twice Double-Leap Knights Movement Modifier
5 bishops-ignore-color Colour-Blind Bishops Movement Modifier
6 rook-warp Rook Warp Board Geometry
7 wrap-board Cylindrical Board Board Geometry
8 queen-splits Queen Fission Piece Ability
9 king-heals Regenerating King Piece Ability
10 explosive-rook Detonating Rook Piece Ability
11 capture-to-win First Blood Win/Loss Condition
12 last-piece-standing Annihilation Win/Loss Condition
13 piece-hp Hit Points Persistent State
14 knight-immunity Bishop-Proof Knights Piece Ability
15 poisoned-squares Poisoned Centre Persistent State

Rule Definitions

Backward-Marching Pawns

ID: pawns-move-backward Category: Movement Modifier Description: Pawns may, in addition to their normal forward moves, move exactly one square straight backward to an empty square. Backward moves may not capture and do not enable en passant. A pawn that has moved backward is still eligible for promotion once it later reaches the opposite last rank. Base Rule Affected: fide.pawn-single-push (additive — adds a new production preset.pawn-backward-push, does not retract the forward push). Mode: additive Requires: none Incompatible With: double-pawn-sprint

Test Scenarios:

  1. White pawn on e4, e3 empty → e4→e3 is a legal move.
  2. White pawn on e4, enemy piece on d3 → e4→d3 is not legal (no backward captures).
  3. White pawn on e4, white piece on e3 → e4→e3 is not legal (destination occupied).

Edge Cases:

  • Backward move does not create an en passant target square (the FIDE EnPassantTarget WME is only asserted by the double-push production).
  • A pawn that moves backward to its home rank may not then perform a double forward sprint on the following turn (home-rank privilege is tracked by a HasMoved WME, which is asserted on the pawn's first move of any kind).
  • Backward moves do not count toward the fifty-move rule's "pawn move" reset — they do reset it, same as any pawn move.

Perpetual Sprint

ID: double-pawn-sprint Category: Movement Modifier Description: A pawn may advance two squares straight forward from any rank, not only its home rank, provided both intervening squares are empty. The resulting move still asserts an en passant target on the skipped square for exactly one opponent turn. Base Rule Affected: fide.pawn-double-push (override — removes the HasMoved = false guard). Mode: override Requires: none Incompatible With: pawns-move-backward

Test Scenarios:

  1. White pawn on e4, e5 and e6 empty → e4→e6 is legal and asserts EnPassantTarget(e5).
  2. Black pawn on d5 after white plays e4→e6 → Black d5×e6 is not en-passant-legal; but d5×e5 is legal en passant (standard FIDE consequence).
  3. White pawn on e7, e8 is the promotion rank → e7→e8 single push with promotion is legal; e7→e9 is illegal (off board, not a "two-square" candidate).

Edge Cases:

  • The en passant window remains one ply, exactly as in FIDE.
  • A double sprint that jumps over a piece is illegal — both intervening squares must be empty (the slide-check production is retained).
  • This rule is incompatible with pawns-move-backward because the two-square move is ambiguous when combined with an unrestricted backward step (e.g. e4→e2 vs e4→e6 would both be legal, breaking the pawn's identity as "forward-only").

Slanting Pawns

ID: pawn-diagonal-no-capture Category: Movement Modifier Description: Pawns may move one square diagonally forward to an empty square without capturing, in addition to all normal pawn moves. Diagonal moves onto occupied enemy squares still capture exactly as in FIDE; the new behaviour is only the non-capturing diagonal slide. Base Rule Affected: fide.pawn-capture (additive — adds a new production preset.pawn-diagonal-quiet). Mode: additive Requires: none Incompatible With: none

Test Scenarios:

  1. White pawn on e4, d5 and f5 empty → both e4→d5 and e4→f5 are legal quiet moves.
  2. White pawn on e4, d5 occupied by a black piece → e4→d5 is a legal capture (normal FIDE behaviour, unchanged).
  3. White pawn on e4, d5 occupied by a white piece → e4→d5 is illegal (cannot move onto a friendly piece — neither quiet nor capture).

Edge Cases:

  • A quiet diagonal move does not create an en passant target.
  • Diagonal quiet moves may still deliver check / checkmate and may still promote if they land on the last rank.
  • En passant itself is unchanged: a black pawn on d5 may still capture white's e-pawn en passant after e2→e4, even though the diagonal-quiet variant offers e5→d6 or e5→f6 as non-capturing alternatives.

Double-Leap Knights

ID: knights-leap-twice Category: Movement Modifier Description: A knight makes two consecutive L-shaped leaps in a single turn. The knight must land on a square reachable by the composition of two knight moves, and the intermediate square must be empty (it is "flown over" but the knight briefly stops there for legality purposes). The knight may optionally stop after the first leap — any single-leap destination is still legal. Base Rule Affected: fide.knight-move (hybrid — retains the single-leap production and adds preset.knight-double-leap). Mode: hybrid Requires: none Incompatible With: none

Test Scenarios:

  1. White knight on b1, all squares empty → b1→a3→c4 is legal (lands on c4); b1→c4 alone is not a legal single-leap destination, so the double-leap is required.
  2. White knight on b1, a3 occupied by a friendly pawn → b1→?→c4 via a3 is illegal (intermediate square must be empty); other two-leap paths to the same final square (e.g. b1→c3→a4) must be evaluated independently.
  3. White knight on b1, enemy piece on c3 → b1→c3 single-leap capture is legal; b1→c3→? two-leap is illegal because the intermediate square must be empty, not occupied by an enemy.

Edge Cases:

  • The intermediate square is not captured: a knight cannot "capture on the way through". Only the final landing square's occupant (if enemy) is captured.
  • Double-leap may deliver check from squares ordinary knight moves cannot reach; the checkmate-detection production uses the full two-leap move generator when this rule is active.
  • Double-leap does not allow the knight to end on its starting square (a→b→a paths are pruned as null moves).

Colour-Blind Bishops

ID: bishops-ignore-color Category: Movement Modifier Description: A bishop may, in addition to its normal diagonal slides, make a single one-square orthogonal step (N/S/E/W) to an empty or enemy-occupied square. This lets a bishop change diagonal colour over multiple turns and effectively reach every square on the board. Base Rule Affected: fide.bishop-move (additive — adds preset.bishop-orthogonal-step). Mode: additive Requires: none Incompatible With: none

Test Scenarios:

  1. White bishop on c1, d1 empty → c1→d1 is a legal one-square orthogonal step.
  2. White bishop on c1, b1 occupied by white knight → c1→b1 is illegal (friendly blocker).
  3. White bishop on c1, b2 occupied by an enemy pawn → c1×b2 is legal as the normal diagonal capture; c1→c2 is legal as a quiet orthogonal step if c2 is empty.

Edge Cases:

  • The orthogonal step is exactly one square — bishops do not gain full rook mobility. A bishop on c1 cannot reach c3 in one move unless via its native diagonal.
  • Orthogonal steps count as bishop moves for the fifty-move rule (not pawn moves, not captures unless capturing).
  • A bishop that steps orthogonally onto a square of the opposite colour is now permanently on the new colour until it steps again — the schema does not tag bishops by colour.

Rook Warp

ID: rook-warp Category: Board Geometry Description: After a rook completes a normal slide, it may optionally "warp" — that is, continue the slide as if the far edge of the board wrapped to the near edge, landing on the first square of the opposite end of the same rank or file that is empty or contains an enemy piece. The warp is a single atomic move, chosen at move-generation time. Base Rule Affected: fide.rook-move (hybrid — retains base slide and adds preset.rook-warp-slide). Mode: hybrid Requires: none Incompatible With: wrap-board

Test Scenarios:

  1. White rook on a1, entire 1st rank empty → a1→h1 is legal normal slide; a1⇒h1 via warp is also legal but produces the same destination, so only the normal move is listed.
  2. White rook on a4, all squares a5..a8 empty, a1..a3 empty, h4 occupied by enemy → slide a4→a8 is legal normal; warp a4⇒h4 is a legal capture (the slide from a4 up to a8, then wrapping down from a1 to h4).
  3. White rook on d4, d5..d8 empty, d1..d3 empty, but d1 becomes the warp continuation point occupied by a friendly piece → warp is blocked at the friendly piece, so the warp destination is the last empty square before it.
  4. White rook on d4 with enemy on d8, friendly on d1 → normal slide captures d8; warp path (d5..d8..wraps..d1..d3) is blocked immediately at d1 after wrapping around, so no warp capture beyond d8 is possible.

Edge Cases:

  • Castling is unchanged. A rook that has warped once still has HasMoved = true and so cannot castle.
  • A rook cannot warp through its own king; the warp path is interrupted by any friendly piece and by any enemy piece (capturing the first enemy encountered).
  • Warp and wrap-board are incompatible because wrap-board makes every piece's lateral movement wrap, which collapses the rook-specific warp semantics into ambiguity about when a move is a "rook warp" vs an ordinary wrap-slide.

Cylindrical Board

ID: wrap-board Category: Board Geometry Description: The board is topologically a vertical cylinder: the a-file and h-file are adjacent. A piece whose lateral movement would leave the board on the a-side re-enters on the h-file at the same rank, and vice versa. Vertical movement (ranks 1 and 8) does not wrap. Base Rule Affected: All sliding-piece productions (fide.rook-move, fide.bishop-move, fide.queen-move) and fide.king-move, fide.knight-move, fide.pawn-capture — each is overridden with a wrap-aware successor generator. Mode: override Requires: none Incompatible With: rook-warp

Test Scenarios:

  1. White rook on a4, all squares empty → a4→h4 is legal via wrap (westward one step); a4→b4, c4, … also legal normally.
  2. White bishop on a1, diagonal empty → a1→h2 is legal (one diagonal step westward wraps to h2).
  3. White pawn on a5 (black to move with a black pawn on h5) → Black pawn on h5 may capture h5×a6 diagonally through the wrap? No — pawn captures wrap: h5×a6 is legal only if an enemy piece is on a6 (normal capture rule applied modulo wrap). If the white piece is on a6, the capture is legal.

Edge Cases:

  • Castling: the king's two-square hop does not wrap — a king on e1 cannot castle "around the board" to d1 via wrap. Castling targets remain fixed squares (g1/c1 for White).
  • Check detection: a rook on a4 attacks h4 through the wrap; the king-safety production must consider wrap-attacks, so wrap-board overrides check detection too.
  • A pawn's en passant target square is computed modulo wrap — if a black pawn double-sprints from h7 to h5, it can be captured en passant by a white pawn on a5 (moving a5×h6) because the wrap makes a5 and h5 laterally adjacent.

Queen Fission

ID: queen-splits Category: Piece Ability Description: When a queen captures an enemy piece, after the capture resolves the queen is retracted and replaced by a rook and a bishop of the same colour. The rook is placed on the capture square; the bishop is placed on the nearest empty orthogonally- or diagonally-adjacent square (searched in clockwise order N, NE, E, SE, S, SW, W, NW). If no adjacent square is empty, the bishop is forfeit and only the rook remains. Base Rule Affected: fide.queen-capture (override — the queen is not placed on the target square; instead the fission production fires). Mode: override Requires: none Incompatible With: none

Test Scenarios:

  1. White queen on d1, black pawn on d4, d3/d5/c4/e4/c3/e3/c5/e5 all empty → queen captures d4; result: white rook on d4, white bishop on d5 (N is first clockwise empty).
  2. White queen on d1 captures black rook on d4; every adjacent square of d4 is occupied by friendly pieces → result: white rook on d4, bishop forfeit (not placed).
  3. White queen on h1 captures black pawn on h2 (corner capture); adjacency for h2 is only g1, g2, g3, h1, h3 → bishop is placed on the first empty in clockwise order starting N from h2 (i.e. h3 if empty).
  4. White queen gives checkmate by capturing → fission still resolves; if removing the queen and placing a rook on the square no longer delivers checkmate, the game continues (fission is part of the move and is evaluated before legality of the resulting position).

Edge Cases:

  • If the rook placed on the capture square is pinned (i.e. removing the queen leaves the king in check), the queen's capture was illegal to begin with — the production must back-check using the post-fission board, not the mid-capture board.
  • Promotion interaction: a pawn that promotes to a queen and immediately captures (promotion-capture) fissions on the same move: the promotion square receives a rook and the adjacent square receives a bishop.
  • Fissioned pieces do not retain castling rights; they are newly-materialised.

Regenerating King

ID: king-heals Category: Piece Ability Description: At the end of a player's turn, if that player's king is not in check, the king regenerates 1 HP, up to a maximum of 3. The king starts the game with 1 HP. The king can only be eliminated when its HP reaches 0; a capture (or damage from other rules) decrements HP by 1. Base Rule Affected: fide.check-resolution and fide.king-capture (hybrid — adds a new end-of-turn production and modifies capture semantics so that the king is a multi-HP piece). Mode: hybrid Requires: piece-hp Incompatible With: none

Test Scenarios:

  1. White king starts the game with HP=1. White plays a turn, ends not in check → at the end of White's turn, king HP becomes 2.
  2. White king at HP=3, White ends turn not in check → king HP stays at 3 (cap).
  3. White king at HP=2, Black delivers a legal "capture" on the king (only possible because piece-hp is active and king cannot be checkmated in the FIDE sense) → king HP becomes 1, game continues. White's next turn ends not in check → HP becomes 2.

Edge Cases:

  • Being "in check" means at the end of the turn, not during it. A king briefly in check mid-move (impossible under FIDE but possible if combined with rules that allow illegal-intermediate positions) does not prevent healing.
  • Healing fires once per turn-end, regardless of how many pieces threaten the king. If the king is in check the heal production is simply not eligible.
  • king-heals does not disable checkmate detection on its own — checkmate still ends the game when the king has HP=1 and cannot escape; this rule merely gives the king additional hits.

Detonating Rook

ID: explosive-rook Category: Piece Ability Description: When a rook makes a capture, it also removes every other piece (friendly or enemy) on the same rank or same file within a Chebyshev-style distance of 2 squares from the capture square, measured along rank and file only. The capturing rook itself survives and remains on the capture square. Base Rule Affected: fide.rook-capture (override — the post-capture production asserts additional retracts). Mode: override Requires: none Incompatible With: piece-hp

Test Scenarios:

  1. White rook captures on d4; black pieces on d3, d5, d6, c4, e4, f4, d2, d7 → after detonation, removed: d3, d5, d6 (d6 is distance 2), c4, e4, f4 (f4 is distance 2), d2 (distance 2). d7 survives (distance 3).
  2. White rook captures on d4; own king on d5 → own king is blown up. This is legal (self-detonation is permitted); however if it is the only king the game ends with that side losing.
  3. White rook captures on a1 corner; pieces on a2, a3, b1, c1 → all four are in range (distances 1, 2, 1, 2) and are all removed; a4 and d1 (distance 3) survive.

Edge Cases:

  • Detonation does not chain: if a captured piece is itself a rook, its capture does not re-trigger. Only the original capturing rook detonates.
  • A rook that captures en passant — impossible in FIDE (only pawns do), and not introduced by any other preset — is not a special case.
  • Incompatibility with piece-hp: when HP is active, captures are non-lethal damage; the detonation semantics ("removes pieces") conflict with "deals 1 HP damage". Rather than redefining, the v1 preset set treats these two as mutually exclusive.

First Blood

ID: capture-to-win Category: Win/Loss Condition Description: The first player to make a capture — of any enemy piece, including pawns — wins the game immediately. Checkmate is disabled; stalemate still results in a draw if no capture is ever available. Base Rule Affected: fide.checkmate-win, fide.stalemate-draw (override — disables checkmate, retains stalemate; adds preset.first-capture-win). Mode: override Requires: none Incompatible With: last-piece-standing

Test Scenarios:

  1. Starting position, White plays 1.e4 d5 2.exd5 → White wins immediately on move 2 (first capture).
  2. Game reaches a position with no legal captures for either side and the side to move has no other legal moves → stalemate, draw.
  3. White is in check; the only legal response is a capture of the checking piece → White plays the capture and wins (check is not a loss condition under this rule — only capture is a win condition; if White cannot respond, stalemate-by-no-moves rules determine the draw).

Edge Cases:

  • The king is not special: capturing a pawn wins just as capturing a queen does. Players therefore play extremely cautiously, avoiding any offer of exchange.
  • Promotions do not count as captures unless they are capture-promotions (a pawn capturing diagonally onto the last rank).
  • En passant counts as a capture.
  • A move that would give up the player's own piece to be captured by the opponent is not itself a capture — only the capturing move triggers the win.

Annihilation

ID: last-piece-standing Category: Win/Loss Condition Description: A player wins when the opponent has zero remaining pieces. The king has no special status: it may be captured like any other piece, and is not subject to check or checkmate. Stalemate is replaced by the losing condition "no legal moves" = loss. Base Rule Affected: fide.check-detection, fide.checkmate-win, fide.stalemate-draw, fide.king-capture-illegal (override — all four are retracted and replaced by a single annihilation-win production plus a "no legal moves = loss" production). Mode: override Requires: none Incompatible With: capture-to-win

Test Scenarios:

  1. White has only a king on e1; Black captures it with a queen → Black wins. No "check" warning fires in any prior position.
  2. White has king + rook vs Black king. White sacrifices the rook to force Black's king into a position with no legal moves → Black loses by "no legal moves" rule.
  3. White has king only; Black has king only → the game is drawn by the threefold-repetition or fifty-move rule eventually; no "insufficient material" automatic draw because kings are normal pieces here.

Edge Cases:

  • A player may legally move into a "check" — there is no such thing as check. This interacts with pinning: pins do not exist either, since they rely on king-safety semantics.
  • A side with a king and one other piece that cannot legally move loses. This is different from FIDE stalemate (draw).
  • Incompatible with capture-to-win because the two rules trigger on opposite conditions (first capture vs last piece) and cannot both be active.

Hit Points

ID: piece-hp Category: Persistent State Description: Every piece has an integer hp attribute. All non-king pieces start at hp = 2; the king starts at hp = 1 (this can be modified by king-heals). When a piece is "captured" by another piece's move, the attacker's move resolves as in FIDE (the attacker ends on the target square), but instead of retracting the target, the target's hp is decremented by 1. Only when hp reaches 0 is the target retracted. If after decrement the target still has hp > 0, the attacker and defender "stack" on the same square for resolution purposes — in practice this means the defender is pushed to the nearest empty adjacent square (clockwise from N), or retracted if no adjacent empty square exists. Base Rule Affected: fide.capture (override — replaces all capture productions with preset.hp-damage) and the schema itself (adds the hp attribute to the Piece WME). Mode: override Requires: none Incompatible With: explosive-rook

Test Scenarios:

  1. White rook captures black pawn (pawn hp=2) → pawn hp becomes 1, white rook ends on target square, black pawn is pushed to the clockwise-nearest empty adjacent square (e.g. N first).
  2. White rook captures black pawn already at hp=1 → pawn hp becomes 0, pawn retracted, white rook ends on target square (standard FIDE appearance).
  3. White queen captures black knight (hp=2) but every adjacent square to the target is occupied → knight has no push destination and is retracted instead (damage-over-push fallback).

Edge Cases:

  • En passant capture damages the captured pawn on the pawn's actual square (not the en-passant target square). The captured pawn is pushed from its original square if it survives.
  • Promotion: a promoting pawn that captures deals 1 damage as normal. The pawn promotes on the target square regardless of whether the target survives.
  • This rule is incompatible with explosive-rook (see that rule's notes).
  • The schema extension (hp: number) is defined in packages/chess/src/presets/piece-hp.ts and is the single source of truth for HP-related attribute writes; all dependent rules (king-heals, poisoned-squares) read/write through this extension.

Bishop-Proof Knights

ID: knight-immunity Category: Piece Ability Description: A bishop may never capture a knight. Any bishop move that would land on a square occupied by an enemy knight is illegal. The bishop may still pass threats through (for check/pin purposes a bishop still "attacks" the square), but it cannot complete a capture on it. Base Rule Affected: fide.bishop-capture (override — adds a guard: target must not be Piece(type=Knight, color=opponent)). Mode: override Requires: none Incompatible With: none

Test Scenarios:

  1. White bishop on c1, black knight on h6 along the diagonal, all intermediate empty → c1×h6 is illegal; bishop may still move to any empty square along the diagonal up to but not including h6 (i.e., ending on g5 is legal, h6 is not).
  2. Black bishop gives check to white king via a diagonal that passes through no knight → check is legal and must be resolved normally.
  3. Position where the only way to block a bishop-check is to capture the bishop with a knight → this is still legal; knight immunity protects knights from bishops, not bishops from knights.

Edge Cases:

  • For check and checkmate detection purposes, a bishop still threatens the knight's square (so a knight cannot "shield" its king from a bishop's ray — the bishop's attack passes through the knight as if it weren't there for threat-detection, but the actual capture move is illegal). Implementation: the bishop's threat-ray is computed ignoring immune pieces; the bishop's move-generation excludes them.
  • Promotion to bishop: a pawn that promotes to a bishop is subject to the same restriction — it cannot capture knights on its promotion move if the promotion is a capture onto a knight.
  • This rule does not restrict queens (which are not bishops) from capturing knights, even along diagonals.

Poisoned Centre

ID: poisoned-squares Category: Persistent State Description: The four central squares — d4, d5, e4, e5 — are permanently poisoned. Any piece that ends its owner's turn on a poisoned square loses 1 HP. A piece that occupies and then leaves a poisoned square on the same turn is unaffected. Pieces with 0 HP are retracted at end-of-turn evaluation. Base Rule Affected: End-of-turn phase (additive — adds preset.poison-damage production that fires in the post-move resolution phase before turn-pass). Mode: additive Requires: piece-hp Incompatible With: none

Test Scenarios:

  1. White rook (hp=2) ends the turn on e4 → rook hp becomes 1 at end-of-turn.
  2. White knight (hp=1) ends the turn on d5 → knight hp becomes 0 and is retracted at end-of-turn.
  3. White rook on e4 moves to e8 during its turn → no poison damage (rook did not end its turn on a poisoned square).

Edge Cases:

  • The king (hp=1 or higher if king-heals is active) takes poison damage too. This can create "king on d4 = dies" positions; combined with king-heals, a king on a poisoned square at end-of-turn takes 1 damage and heals 1 only if not in check — net zero if not in check, net −1 if in check.
  • If a pawn on d5 promotes and the promoted piece ends on d5, the promoted piece suffers poison (promotion does not grant immunity).
  • Poison fires exactly once per turn per occupant: a piece cannot take 2 damage for being on a poisoned square for two half-moves, because end-of-turn evaluation is per-player-turn.
  • Order of resolution: poison damage resolves before king-heals, so a king that enters a poisoned square while in check will lose HP from poison and not heal (since it is in check), and may be retracted if hp reaches 0.

The following 14 presets were added in the 2026 rule-variants epic. Their design is cross-referenced against the greenchess.net variant catalogue (category 4): https://greenchess.net/variants.php?cat=4.

Each preset below includes a one-line greenchess cross-reference where applicable.

# ID Name Category greenchess.net
16 knightmate-rules Knightmate King Variants Knightmate
17 coregal Coregal King Variants Coregal — king+queen royal
18 dual-king Dual King King Variants Two kings (strong)
19 weak-dual-king Weak Dual King King Variants Two kings (weak)
20 double-move Double Move Multi-move Both sides play 2 half-moves
21 monster-rules Monster Multi-move Monster Chess
22 first-promotion-wins First to Promote Wins Objectives Pairs with Pawns-Only layout
23 suicide-chess Suicide Chess Objectives Losing Chess / Antichess
24 capture-all Capture All Objectives Wipe-out variant
25 extinction-chess Extinction Objectives Extinction Chess
26 berolina-pawns Berolina Pawns Movement Modifier Berolina Chess
27 berolina-pawns-2 Berolina Pawns (Extended) Movement Modifier Berolina + sideways captures
28 bouncing-pieces Bouncing Pieces Board Geometry Reflect off file edges
29 bouncing-pieces-2 Bouncing Pieces (Extended) Board Geometry Reflect off all four edges

Knightmate

ID: knightmate-rules · Scope-aware: no · Mode: hook (getRoyalPieces)

Each side's knights are ROYAL instead of the king. The king is a normal piece with no special status — it can be captured, moved into attacked squares, and is not subject to self-check filtering. Mate the last knight to win. Canonically paired with the knightmate starting layout (queen replaced by a royal knight, 3 knights total per side).

Coregal

ID: coregal · Scope-aware: no · Mode: hook (getRoyalPieces)

Both the KING and the QUEEN are royal. Attack or mate on either ends the game. If the queen is captured, the king remains royal (degrades gracefully to standard chess). Pins affect both royals — moving a pinned queen that would expose the king is illegal, and vice versa. Composes naturally with piece-hp: both royals take damage independently.

Dual King

ID: dual-king · Scope-aware: no · Mode: hook (getRoyalPieces) · Preferred layout: dual-classic

Two kings per side. All kings are royal; mating EITHER king ends the game ("strong" variant). Paired with the dual-classic layout (queens replaced by a second king on d1/d8).

Weak Dual King

ID: weak-dual-king · Scope-aware: no · Mode: hooks (getRoyalPieces + shouldFilterSelfCheck + onCheckGameResult)

Two kings per side, but losing one is SURVIVABLE. Game ends only when a side has zero kings, or every remaining king is simultaneously mated. Opts out of the self-check filter so "sacrifice" moves (letting one king get captured while the other stays safe) are legal. The onCheckGameResult hook declares terminal only on the 0-royal or all-royals-mated conditions.

Double Move

ID: double-move · Scope-aware: no · Mode: hook (shouldAdvanceTurn)

Each side plays TWO half-moves per turn before the flip. Turn order: WW BB WW BB. Checkmate on either half-move ends the game immediately — the attacking side does not "get another half-move". Composes with piece-hp, king-heals, first-promotion-wins.

Monster

ID: monster-rules · Scope-aware: YES · Mode: hook (shouldAdvanceTurn)

Asymmetric multi-move. With scope: "white" (canonical Monster pairing), white plays 2 half-moves per turn and black plays 1. With scope: "black", the asymmetry flips. With scope: "both", both sides play 2 (equivalent to double-move; prefer that preset for that mode). Paired canonically with the monster layout (white king + 4 pawns vs. full black army).

First to Promote Wins

ID: first-promotion-wins · Scope-aware: no · Mode: hooks (onBeforeMove + onAfterMove + onCheckGameResult)

The first player to promote a pawn wins the game immediately. First promotion LOCKS the result; later promotions don't overwrite. Does NOT opt out of the self-check filter (so pinned pawns that would expose the king still can't promote — this is the conservative choice; a permissive follow-up could add that opt-out separately). Paired with the pawns-only layout for a race-to-queen variant.

Suicide Chess

ID: suicide-chess · Scope-aware: no · Mode: hooks (filterLegalMoves + getRoyalPieces + shouldFilterSelfCheck + onCheckGameResult) · aka Losing Chess / Antichess

Lose all your pieces to WIN. Captures are COMPULSORY: if any capture is legal, the mover must capture. Kings are not royal (empty royal set). The self-check filter is off. Stalemate-wins rule: the side with no legal moves WINS (inverted standard stalemate). The side that reaches 0 pieces first wins.

Capture All

ID: capture-all · Scope-aware: no · Mode: hooks (getRoyalPieces + shouldFilterSelfCheck + onCheckGameResult)

The INVERSE of suicide-chess. Capture every enemy piece to WIN. Captures are NOT compulsory (ordinary legal-move semantics). Kings are not royal. Composes with piece-hp: only lethal damage decrements the piece count.

Extinction

ID: extinction-chess · Scope-aware: no · Mode: hook (onCheckGameResult) · Configurable

Win by wiping out every enemy piece of a specific TYPE (default: "pawn"). Target is configurable via engine.presetState<{ targetType: PieceType }>("extinction-chess"). Unlike suicide-chess / capture-all, this preset does NOT opt out of royalty — the king remains royal and FIDE checkmate rules still apply until extinction triggers. Target "king" effectively means "capture any king wins" and requires self-check opt-out via another preset to be practically reachable.

Berolina Pawns

ID: berolina-pawns · Scope-aware: YES · Mode: hooks (overridePieceMoves + onAfterMove)

Pawns push forward DIAGONALLY and capture forward ORTHOGONALLY (inverse of FIDE pawns). Diagonal double-push from home rank, promotions on either push or capture reaching last rank. En-passant follows Parton 1952 semantics (post-epic Feature 3): a double-diagonal push passes through an intermediate square; the opponent may capture onto that square on the very next half-move via their orthogonal-forward capture vector, retracting the double-pushed pawn. Sideways captures are not available on this preset (see berolina-pawns-2). Scope chooses which side(s) use the rule: scope: "white" only white pawns are berolina, scope: "black" only black, scope: "both" both sides.

Berolina Pawns (Extended)

ID: berolina-pawns-2 · Scope-aware: YES · Mode: hooks (overridePieceMoves + onAfterMove)

Like berolina-pawns but ALSO allows SIDEWAYS captures (adjacent file, same rank). Same scope-flip semantics and the same Parton 1952 ep rule (orthogonal-forward ep only — sideways captures do not trigger or accept ep).

Bouncing Pieces

ID: bouncing-pieces · Scope-aware: no · Mode: hook (getExtraMoves)

Bishop and queen diagonal rays REFLECT off the left (a-file) and right (h-file) edges once, continuing in the reflected direction until they hit a friendly block, capture an enemy, or leave the rank. Queens' orthogonal rays are unchanged. No double-bounce (that's bouncing-pieces-2). Incompatible with wrap-board (wrapping geometry is undefined under reflection).

Bouncing Pieces (Extended)

ID: bouncing-pieces-2 · Scope-aware: no · Mode: hook (getExtraMoves)

Like bouncing-pieces but rays also reflect off TOP (rank 8) and BOTTOM (rank 1) edges. Capped at 2 total reflections per ray to prevent infinite zigzag on empty boards. Incompatible with bouncing-pieces and wrap-board.


New Starting Layouts

Dual Classic

ID: dual-classic (source: premade) · Added in the 2026 epic.

Variant of FIDE with queens removed and replaced by a second king per side. White: kings on d1 + e1, rooks/bishops/knights/pawns standard. Black: mirror. Canonical pairing with dual-king (strong) or weak-dual-king.


Cross-References — ThressGame Rules as Chess Presets

Some rules from the ThressGame project (https://github.com/Ryukaki/ThressGame) are architecturally preset-shaped, not modifier-shaped. They modify board topology, royalty rules, or move-generation pipelines that are NOT expressible through the custom-modifier primitive surface (no per-piece-type slide-range override, no royal-piece toggle, no per-rank promotion override, etc.). Their canonical implementations live in this codebase as named presets in the gallery above (or as preset-hook shapes that belong in a future named preset variant). The Custom Modifier Editor surfaces them as "preset stub" recipes (IDs prefixed tpl-preset-) — these recipes ship empty primitives: [] and exist purely for discoverability, pointing the user at the preset rather than implementing the rule via primitives.

This is the closing reference for the thressgame-100 epic (W6).

ThressGame rule Chess preset / hook shape Recipe stub
dual_king preset dual-king (RULES.md § Dual King) tpl-preset-dual-king
coregal preset coregal (RULES.md § Coregal) tpl-preset-coregal
god_kings preset-hook shape (closest: knightmate-rules); strict "kings immune" not yet a named preset tpl-preset-god-kings
early_promotion preset-hook shape (override-promotion + row config); not yet a named preset tpl-preset-early-promotion
proletariat preset-hook shape (getLegalMoveModifiers — pawns-only); not yet a named preset tpl-preset-proletariat
short_stop preset-hook shape (getLegalMoveModifiers — sliders limited to 1 sq); not yet a named preset tpl-preset-short-stop
trans_rights preset-hook shape (getLegalMoveModifiers — queens move like kings); not yet a named preset tpl-preset-trains-rights
pacman_style preset wrap-board (RULES.md § Cylindrical Board); modifier sibling tpl-pacman-style-cross-ref (W4) tpl-preset-pacman

Why these are preset-shaped, not modifier-shaped

  • Royalty toggles (dual_king, coregal, god_kings) — the engine's getRoyalPieces hook is a preset-only API; the modifier surface has no royalty primitive.
  • Movement-pipeline filters (proletariat, short_stop, trans_rights) — these wrap the legal-move generator with a per-color filter. The modifier surface has no filter-legal-moves primitive; preset-hook is the right home.
  • Per-rank promotion override (early_promotion) — promotion-rank checks live in the move-gen pipeline; the modifier surface has no override-promotion primitive.
  • Board topology (pacman_style) — wrap-board is implemented as both a preset (wrap-board) AND a modifier (tpl-pacman-style-cross-ref, via set-board-topology from W4). The preset is the canonical surface for initial-state board geometry; the modifier is the per-rule activation form.

See also

  • docs/THRESSGAME_WONT_FIX.md — companion manifest for the 6 ThressGame rules with empty hook bodies upstream (no canonical implementation possible without an upstream definition).

Trigger Primitives

Custom modifier descriptors compose trigger primitives that wire nested effect primitives into specific points of the engine's per-move dispatch. Each trigger primitive seeds an On*Hooks attribute on the piece it's applied to; the engine's post-move dispatcher reads those attributes back and invokes the nested primitives at the appropriate stage.

This section documents the 10 trigger primitives (3 existing + 7 added in Wave 2), the order in which the engine fires them, and the PrimitiveApplyContext extensions (target redirection + event payload) that nested primitives can read.

For the broader context-object types referenced below (PrimitiveApplyContext, TargetResolver, PrimitiveEvent, resolveTargets), see docs/PRESET-API.md.

Hard caps (unchanged)

Cap Value Source
MAX_RECURSION_DEPTH 3 custom/apply.ts (matches validator)
MAX_PRIMITIVE_COUNT 50 custom/schema.ts (per descriptor)
Descriptor version 1 custom/types.ts

Adding a new trigger does not raise these caps. A descriptor that nests trigger primitives more than 3 levels deep, or that exceeds 50 total primitive nodes across the descriptor, is rejected at validation time.

Dispatch order (Metis-locked, 12 stages)

After every successful applyMove, the engine runs the following dispatch sequence in apply.ts#onAfterMove. Stages must remain in this exact order. Later stages depend on observable state set by earlier stages, and reorderings would silently change semantics.

  1. computeAuraFacts recomputes aura WMEs (T28).
  2. fireOnDamagedHooks uses the pre-move HP snapshot to detect drops.
  3. fireOnCaptureHooks fires on the attacker piece.
  4. fireOnCapturedHooks fires on the defender (dying) piece, BEFORE its facts are otherwise gone from the dispatcher's perspective.
  5. fireOnPromotionHooks fires on pawns whose PieceType flipped this move.
  6. fireOnMoveHooks fires on every piece whose Position WME changed (mover plus castling rook plus en-passant pawn relocation).
  7. fireOnMovedOntoSquareHooks evaluates each moved piece's destination against the hook's filter.
  8. fireOnCheckReceivedHooks uses the pre/post check-state diff; royal pieces only; edge-triggered.
  9. fireOnCheckDeliveredHooks uses the pre/post attacker-set diff; attributes discovered checks to the revealing piece.
  10. fireConditionalHooks branches on current facts (the conditional primitive).
  11. fireOnTurnEndHooks fires on pieces whose OnTurnEndHooks color matches the mover's color (the turn that just ended).
  12. fireOnTurnStartHooks fires on pieces whose OnTurnStartHooks color matches the next side to move.

Composition implication: an on-turn-end hook on the moving side runs before any on-turn-start hook on the opponent. A descriptor that decays HP at end-of-turn and then heals on next-turn-start sees the decay applied first, then the heal, in that order.

Existing triggers (pre-Wave 2)

Kind Fires when Seeds
on-turn-start At the start of a matching-color turn (stage 12). Params carry color: 'white'|'black'|'both'. OnTurnStartHooks
on-capture On the attacker when a move resolves as a capture (stage 3). OnCaptureHooks
on-damaged When this piece's Hp fact decreases (stage 2). OnDamagedHooks

(These were shipped pre-Wave 2; their semantics are unchanged.)

New triggers (Wave 2)

on-move

Fires when: this piece's Position WME changes. Any move the piece makes counts, including captures, the rook leg of castling, and the pawn-relocation half of en-passant. Stage 6.

Seeds: OnMoveHooks

Params:

{ "primitives": [ /* nested EffectPrimitiveNode[] */ ] }

Examples:

// Berserker: +1 AttackBonus on every move
{ "primitives": [
  { "kind": "add-to-attribute", "params": { "attr": "AttackBonus", "delta": 1 } }
] }
// Nomad: heals 1 HP per step
{ "primitives": [
  { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": 1 } }
] }

on-turn-end

Fires when: at the end of a matching-color turn (stage 11), before the opponent's on-turn-start. The color param filters which side's turn-end matters; both fires on either.

Seeds: OnTurnEndHooks

Params:

{ "color": "white" | "black" | "both", "primitives": [ /* … */ ] }

Examples:

// Decay: lose 1 HP at every turn end (either color)
{ "color": "both",
  "primitives": [ { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": -1 } } ] }
// Re-arm flag at the end of white's own turn
{ "color": "white",
  "primitives": [ { "kind": "set-capture-flag", "params": { "flag": 0 } } ] }

on-promotion

Fires when: AFTER this piece's PieceType flips from pawn to its promotion target (stage 5). The dispatcher populates ctx.event = { kind: "promotion", promotedFrom: "pawn", promotedTo: PieceType } so nested primitives can branch on what the pawn became.

Seeds: OnPromotionHooks

Params:

{ "primitives": [ /* … */ ] }

Examples:

// Promotion Feast: gain 5 HP on promote
{ "primitives": [
  { "kind": "seed-attribute", "params": { "attr": "Hp", "value": 5 } }
] }
// Stay-As-Pawn: revert PieceType immediately after the engine flips it
{ "primitives": [
  { "kind": "seed-attribute", "params": { "attr": "PieceType", "value": "pawn" } }
] }

on-check-received

Fires when: the moment this piece transitions from not-in-check to in-check (stage 8). Edge-triggered: a royal that stays in check across consecutive moves does NOT re-fire until the check is broken and re-delivered. Applies only to royal pieces (as resolved by the active preset's royalty set, defaulting to kings).

⚠️ Edge vs. level semantics. This trigger is the edge of the in-check state, NOT the level. If the same attacker pins the king for three turns in a row, on-check-received fires once (when the first attack lands). To run logic every turn while in check, combine on-turn-start + a conditional that reads the current check state.

Seeds: OnCheckReceivedHooks

Params:

{ "primitives": [ /* … */ ] }

Examples:

// Panic Mode: gain 2 Shield the first time we're checked
{ "primitives": [
  { "kind": "add-to-attribute", "params": { "attr": "Shield", "delta": 2 } }
] }
// Berserker King: +1 DamageBonus per fresh check
{ "primitives": [
  { "kind": "add-to-attribute", "params": { "attr": "DamageBonus", "delta": 1 } }
] }

on-check-delivered

Fires when: this piece's threat-line newly reaches the enemy royal after the move resolves (stage 9). Edge-triggered like on-check-received. Discovered-check attribution: when a piece moves out of the way and reveals a slider behind it, the trigger fires on the revealing slider (the piece whose line-of-sight became unblocked), NOT on the piece that physically moved. Double-check fires the trigger on both newly-attacking pieces.

Seeds: OnCheckDeliveredHooks

Params:

{ "primitives": [ /* … */ ] }

Examples:

// Vampire: gain 1 HP every time we newly deliver check
{ "primitives": [
  { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": 1 } }
] }
// Stack a stun counter on the deliverer (combine with target redirect for the royal)
{ "primitives": [
  { "kind": "add-to-attribute", "params": { "attr": "StunCounter", "delta": 1 } }
] }

on-moved-onto-square

Fires when: this piece finishes a move on a square selected by filter (stage 7). Plain moves, captures, castling and en-passant all qualify so long as the destination matches.

Seeds: OnMovedOntoSquareHooks

Filter shape (discriminated union):

type SquareFilter =
  | { kind: "squares"; squares: Square[] }                  // explicit list, 0..63 (a1=0, h8=63)
  | { kind: "predicate"; file?: 0..7; rank?: 0..7 };        // file 0=a..7=h, rank 0=rank1..7=rank8

A predicate with both file and rank set fires only on that exact square; a predicate with just one fires for the whole file or rank. At least one of file / rank must be present.

Params:

{ "filter": { "kind": "squares", "squares": [27, 28, 35, 36] },
  "primitives": [ /* … */ ] }

Examples:

// Center Bonus: +1 HP on entering d4/e4/d5/e5
{ "filter": { "kind": "squares", "squares": [27, 28, 35, 36] },
  "primitives": [ { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": 1 } } ] }
// King Row: project a +1 HpBonus aura when reaching rank 8
{ "filter": { "kind": "predicate", "rank": 7 },
  "primitives": [
    { "kind": "add-aura", "params": { "radius": 1, "targetAttr": "HpBonus", "delta": 1 } }
  ] }

on-captured

Fires when: this piece is lethally captured (stage 4). Per dispatch contract, the hook list is invoked before any later stage can re-seed or mutate the dying piece's facts; nested primitives that need defender attrs should read them from ctx.event.defenderId rather than via session.get(ctx.pieceId, ...) (the engine's actual fact retraction has already occurred inside applyMove by the time this stage runs).

The hook supports per-hook target redirection so a death-rattle can buff allies, debuff the attacker, or hit a specific square, without special-casing the dispatcher.

Seeds: OnCapturedHooks

Params:

{
  target?: TargetResolver;     // default 'self'
  primitives: EffectPrimitiveNode[];
}

TargetResolver (see PRESET-API.md for full type):

  • 'self' (default): the dying piece.
  • 'attacker': the capturing piece (event.attackerId).
  • 'defender': the captured piece (event.defenderId); equivalent to 'self' in this trigger but provided for symmetry.
  • { squares: Square[] }: every piece currently on one of the listed squares.
  • { relation: 'ally' | 'enemy', filter?: { pieceType?: PieceType } }: every ally (excluding self) or enemy of the dying piece, optionally narrowed to a piece type.

The dispatcher populates ctx.event = { kind: "capture", attackerId, defenderId } and sets ctx.target = hook.target before invoking each inner primitive. Nested primitives that opt into redirection call resolveTargets(ctx, ctx.target) from modifiers/primitives/context.ts.

Examples:

// Kamikaze: damage the attacker on death
{ "target": "attacker",
  "primitives": [ { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": -2 } } ] }
// Martyr: heal every allied queen by 1 HP on death
{ "target": { "relation": "ally", "filter": { "pieceType": "queen" } },
  "primitives": [ { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": 1 } } ] }