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

805 lines
50 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
---
## 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](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](https://greenchess.net/variants.php?cat=4) |
| 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](https://greenchess.net/variants.php?cat=4) |
| 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](https://greenchess.net/variants.php?cat=4) |
| 26 | `berolina-pawns` | Berolina Pawns | Movement Modifier | [Berolina Chess](https://greenchess.net/variants.php?cat=4) |
| 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`](./docs/PRESET-API.md#primitive-context-target-redirection--event).
### 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**:
```json
{ "primitives": [ /* nested EffectPrimitiveNode[] */ ] }
```
**Examples**:
```json
// Berserker: +1 AttackBonus on every move
{ "primitives": [
{ "kind": "add-to-attribute", "params": { "attr": "AttackBonus", "delta": 1 } }
] }
```
```json
// 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**:
```json
{ "color": "white" | "black" | "both", "primitives": [ /* … */ ] }
```
**Examples**:
```json
// Decay: lose 1 HP at every turn end (either color)
{ "color": "both",
"primitives": [ { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": -1 } } ] }
```
```json
// 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**:
```json
{ "primitives": [ /* … */ ] }
```
**Examples**:
```json
// Promotion Feast: gain 5 HP on promote
{ "primitives": [
{ "kind": "seed-attribute", "params": { "attr": "Hp", "value": 5 } }
] }
```
```json
// 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**:
```json
{ "primitives": [ /* … */ ] }
```
**Examples**:
```json
// Panic Mode: gain 2 Shield the first time we're checked
{ "primitives": [
{ "kind": "add-to-attribute", "params": { "attr": "Shield", "delta": 2 } }
] }
```
```json
// 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**:
```json
{ "primitives": [ /* … */ ] }
```
**Examples**:
```json
// Vampire: gain 1 HP every time we newly deliver check
{ "primitives": [
{ "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": 1 } }
] }
```
```json
// 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):
```ts
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**:
```json
{ "filter": { "kind": "squares", "squares": [27, 28, 35, 36] },
"primitives": [ /* … */ ] }
```
**Examples**:
```json
// 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 } } ] }
```
```json
// 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**:
```ts
{
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**:
```json
// Kamikaze: damage the attacker on death
{ "target": "attacker",
"primitives": [ { "kind": "add-to-attribute", "params": { "attr": "Hp", "delta": -2 } } ] }
```
```json
// 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 } } ] }
```