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)
50 KiB
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-hppreset extends the chess schema (packages/chess/src/presets/piece-hp.ts) with anhp: numberattribute on everyPieceWME, and replaces the FIDE capture production. Any rule that reads or writes HP must declareRequires: 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:
- White pawn on e4, e3 empty →
e4→e3is a legal move. - White pawn on e4, enemy piece on d3 →
e4→d3is not legal (no backward captures). - White pawn on e4, white piece on e3 →
e4→e3is not legal (destination occupied).
Edge Cases:
- Backward move does not create an en passant target square (the FIDE
EnPassantTargetWME 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
HasMovedWME, 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:
- White pawn on e4, e5 and e6 empty →
e4→e6is legal and assertsEnPassantTarget(e5). - Black pawn on d5 after white plays
e4→e6→ Blackd5×e6is not en-passant-legal; butd5×e5is legal en passant (standard FIDE consequence). - White pawn on e7, e8 is the promotion rank →
e7→e8single push with promotion is legal;e7→e9is 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-backwardbecause 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:
- White pawn on e4, d5 and f5 empty → both
e4→d5ande4→f5are legal quiet moves. - White pawn on e4, d5 occupied by a black piece →
e4→d5is a legal capture (normal FIDE behaviour, unchanged). - White pawn on e4, d5 occupied by a white piece →
e4→d5is 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 offerse5→d6ore5→f6as 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:
- White knight on b1, all squares empty →
b1→a3→c4is legal (lands on c4);b1→c4alone is not a legal single-leap destination, so the double-leap is required. - White knight on b1, a3 occupied by a friendly pawn →
b1→?→c4via 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. - White knight on b1, enemy piece on c3 →
b1→c3single-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:
- White bishop on c1, d1 empty →
c1→d1is a legal one-square orthogonal step. - White bishop on c1, b1 occupied by white knight →
c1→b1is illegal (friendly blocker). - White bishop on c1, b2 occupied by an enemy pawn →
c1×b2is legal as the normal diagonal capture;c1→c2is 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:
- White rook on a1, entire 1st rank empty →
a1→h1is legal normal slide;a1⇒h1via warp is also legal but produces the same destination, so only the normal move is listed. - White rook on a4, all squares a5..a8 empty, a1..a3 empty, h4 occupied by enemy → slide
a4→a8is legal normal; warpa4⇒h4is a legal capture (the slide from a4 up to a8, then wrapping down from a1 to h4). - 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.
- 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 = trueand 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-boardare incompatible becausewrap-boardmakes 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:
- White rook on a4, all squares empty →
a4→h4is legal via wrap (westward one step);a4→b4, c4, …also legal normally. - White bishop on a1, diagonal empty →
a1→h2is legal (one diagonal step westward wraps to h2). - White pawn on a5 (black to move with a black pawn on h5) → Black pawn on h5 may capture
h5×a6diagonally through the wrap? No — pawn captures wrap:h5×a6is 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-boardoverrides 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:
- 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).
- 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).
- 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).
- 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:
- 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.
- White king at HP=3, White ends turn not in check → king HP stays at 3 (cap).
- White king at HP=2, Black delivers a legal "capture" on the king (only possible because
piece-hpis 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-healsdoes 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:
- 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).
- 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.
- 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:
- Starting position, White plays
1.e4 d5 2.exd5→ White wins immediately on move 2 (first capture). - Game reaches a position with no legal captures for either side and the side to move has no other legal moves → stalemate, draw.
- 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:
- White has only a king on e1; Black captures it with a queen → Black wins. No "check" warning fires in any prior position.
- 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.
- 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-winbecause 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:
- 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).
- White rook captures black pawn already at hp=1 → pawn hp becomes 0, pawn retracted, white rook ends on target square (standard FIDE appearance).
- 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 inpackages/chess/src/presets/piece-hp.tsand 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:
- White bishop on c1, black knight on h6 along the diagonal, all intermediate empty →
c1×h6is 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). - Black bishop gives check to white king via a diagonal that passes through no knight → check is legal and must be resolved normally.
- 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:
- White rook (hp=2) ends the turn on e4 → rook hp becomes 1 at end-of-turn.
- White knight (hp=1) ends the turn on d5 → knight hp becomes 0 and is retracted at end-of-turn.
- 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-healsis active) takes poison damage too. This can create "king on d4 = dies" positions; combined withking-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.
Rule Variants Gallery (v2, 2026)
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'sgetRoyalPieceshook 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 nofilter-legal-movesprimitive; 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, viaset-board-topologyfrom 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.
computeAuraFactsrecomputes aura WMEs (T28).fireOnDamagedHooksuses the pre-move HP snapshot to detect drops.fireOnCaptureHooksfires on the attacker piece.fireOnCapturedHooksfires on the defender (dying) piece, BEFORE its facts are otherwise gone from the dispatcher's perspective.fireOnPromotionHooksfires on pawns whosePieceTypeflipped this move.fireOnMoveHooksfires on every piece whosePositionWME changed (mover plus castling rook plus en-passant pawn relocation).fireOnMovedOntoSquareHooksevaluates each moved piece's destination against the hook's filter.fireOnCheckReceivedHooksuses the pre/post check-state diff; royal pieces only; edge-triggered.fireOnCheckDeliveredHooksuses the pre/post attacker-set diff; attributes discovered checks to the revealing piece.fireConditionalHooksbranches on current facts (theconditionalprimitive).fireOnTurnEndHooksfires on pieces whoseOnTurnEndHookscolor matches the mover's color (the turn that just ended).fireOnTurnStartHooksfires on pieces whoseOnTurnStartHookscolor 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-receivedfires once (when the first attack lands). To run logic every turn while in check, combineon-turn-start+ aconditionalthat 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 } } ] }