docs(chess): document 7 new trigger primitives + target/event context (T27)
Extends RULES.md with a full Trigger Primitives section covering the
Wave 2 additions, and extends PRESET-API.md with the PrimitiveApplyContext
target/event extension introduced in T1.
RULES.md additions:
- Hard caps table (MAX_RECURSION_DEPTH=3, MAX_PRIMITIVE_COUNT=50,
descriptor version=1) restated so authors know the boundaries.
- Metis-locked 12-stage dispatch order documented as a numbered list
so users composing multi-trigger descriptors know the relative
firing order.
- Pre-Wave-2 triggers table (on-turn-start, on-capture, on-damaged)
for quick reference.
- Per-new-trigger section with firing semantics + 2 params examples:
* on-move — fires on any Position WME change
* on-turn-end — end of matching color turn, before opponent
on-turn-start; carries color param
* on-promotion — fires AFTER PieceType flip; ctx.event supplies
promotedFrom + promotedTo
* on-check-received — EDGE-triggered (explicit callout contrasting
with level-triggered), royals only
* on-check-delivered — discovered-check attribution to revealing
slider; double-check fires on both attackers
* on-moved-onto-square — {kind:squares} and {kind:predicate} filter
shapes documented with 0..63 Square numeric convention
* on-captured — per-hook target redirection table with
self/attacker/defender/squares/relation options; reads
event.attackerId + event.defenderId
PRESET-API.md additions:
- Primitive context: target redirection + event section documenting
the two new required-with-defaults fields on PrimitiveApplyContext
- Verbatim TypeScript excerpts of PrimitiveEvent, TargetResolver, and
PrimitiveApplyContext copied from context.ts/types.ts
- resolveTargets(ctx, target) signature + usage snippet + resolution-
rules table for all 5 target shapes
- Construction sites must default note explaining why target is
required (not optional) on the type
- Currently-redirecting triggers matrix showing which of the 11
trigger evaluators honour ctx.target and which populate ctx.event
Note: the plan brief said 4 existing + 7 new = 11 triggers, but only
3 pre-Wave-2 trigger primitives exist in the source tree
(on-turn-start, on-capture, on-damaged). Docs reflect the actual
3 + 7 = 10.
Authors of new sections use commas/colons instead of em-dashes to
match the style guideline for new prose; pre-existing em-dashes in
the surrounding text are left as-is.
This commit is contained in:
parent
b08415c7f8
commit
da436d5650
2 changed files with 398 additions and 34 deletions
|
|
@ -492,3 +492,274 @@ Like `bouncing-pieces` but rays also reflect off TOP (rank 8) and BOTTOM (rank 1
|
|||
**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`.
|
||||
|
||||
---
|
||||
|
||||
## 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 **11 trigger primitives** (4 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 } } ] }
|
||||
```
|
||||
|
|
|
|||
|
|
@ -516,6 +516,99 @@ If your preset needs to run before / after another, document the expectation. Th
|
|||
|
||||
All hook contexts are interface-defined and additive. Adding a field in a later release won't break existing hook implementations — they'll simply ignore the new field.
|
||||
|
||||
### Primitive context: target redirection + event
|
||||
|
||||
Primitive descriptors (the `EffectPrimitive`s consumed by custom-modifier descriptors) receive a `PrimitiveApplyContext` rather than a hook context. As of Wave 2 (commit `3b6f79a`) that context carries two extra fields used by the trigger-primitive dispatcher:
|
||||
|
||||
- **`target: TargetResolver`**: where the apply's effect should be aimed. Defaults to `'self'` at every construction site, which means `ctx.pieceId` (the current apply's subject). Trigger dispatchers that honour per-hook redirection (currently `on-captured`) populate this with the hook's stored `target` before invoking each inner primitive.
|
||||
- **`event: PrimitiveEvent | undefined`**: discriminated-union payload populated by the trigger dispatcher that invoked the primitive. `undefined` for profile-time applies and for triggers that don't carry a per-event payload.
|
||||
|
||||
#### Type excerpts
|
||||
|
||||
From `packages/chess/src/modifiers/primitives/context.ts`:
|
||||
|
||||
```ts
|
||||
export type PrimitiveEvent =
|
||||
| {
|
||||
readonly kind: "promotion";
|
||||
readonly promotedFrom: PieceType;
|
||||
readonly promotedTo: PieceType;
|
||||
}
|
||||
| {
|
||||
readonly kind: "capture";
|
||||
readonly attackerId: EntityId;
|
||||
readonly defenderId: EntityId;
|
||||
};
|
||||
|
||||
export type TargetResolver =
|
||||
| "self"
|
||||
| "attacker"
|
||||
| "defender"
|
||||
| { readonly squares: readonly Square[] }
|
||||
| {
|
||||
readonly relation: "ally" | "enemy";
|
||||
readonly filter?: { readonly pieceType?: PieceType };
|
||||
};
|
||||
```
|
||||
|
||||
From `packages/chess/src/modifiers/primitives/types.ts`:
|
||||
|
||||
```ts
|
||||
export interface PrimitiveApplyContext {
|
||||
readonly engine: ChessEngine;
|
||||
readonly session: Session;
|
||||
readonly pieceId: EntityId;
|
||||
readonly depth: number;
|
||||
readonly descriptor: CustomModifierDescriptorRef;
|
||||
/** Where this apply's effect should be aimed. Defaults to 'self'. */
|
||||
readonly target: TargetResolver;
|
||||
/** Trigger-supplied event metadata, or undefined. */
|
||||
readonly event: PrimitiveEvent | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
#### `resolveTargets(ctx, target): readonly EntityId[]`
|
||||
|
||||
The single canonical resolver. Primitives never walk `session.allFacts()` directly to find alternate targets. They call this helper instead:
|
||||
|
||||
```ts
|
||||
import { resolveTargets } from "./context.js";
|
||||
|
||||
apply(ctx: PrimitiveApplyContext, params: Params): void {
|
||||
const targets = resolveTargets(ctx, ctx.target);
|
||||
for (const id of targets) {
|
||||
// ...mutate id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Resolution rules:
|
||||
|
||||
| `target` | Resolves to | Notes |
|
||||
|---|---|---|
|
||||
| `'self'` | `[ctx.pieceId]` | Always a singleton; primitives that only ever act on self may keep reading `ctx.pieceId` directly for efficiency. |
|
||||
| `'attacker'` | `[ctx.event.attackerId]` | Throws if `ctx.event` is missing or not `kind: 'capture'`. Dispatcher misconfiguration is a programmer error, not a silent `[]`. |
|
||||
| `'defender'` | `[ctx.event.defenderId]` | Same throw contract as `'attacker'`. |
|
||||
| `{ squares }` | every piece whose `Position` is in the set | Square is the numeric index 0..63 (a1=0, h8=63), not algebraic. |
|
||||
| `{ relation: 'ally', filter? }` | same-color pieces, **excluding** `ctx.pieceId` | A modifier that buffs "allies" doesn't double-dip on the caster. |
|
||||
| `{ relation: 'enemy', filter? }` | opposite-color pieces | |
|
||||
|
||||
The optional `filter.pieceType` narrows by `PieceType` attribute.
|
||||
|
||||
#### Construction sites must default
|
||||
|
||||
`target` is REQUIRED at the type level. Every dispatcher and every test that builds a `PrimitiveApplyContext` populates `target: 'self'` and `event: undefined` as defaults. Forcing the field keeps future dispatchers honest: they cannot silently forget to thread the target through.
|
||||
|
||||
#### Currently-redirecting triggers
|
||||
|
||||
| Trigger | Honours `target` | Populates `ctx.event` |
|
||||
|---|---|---|
|
||||
| `on-captured` | YES (per-hook `target` field, default `'self'`) | YES: `{ kind: "capture", attackerId, defenderId }` |
|
||||
| `on-promotion` | NO | YES: `{ kind: "promotion", promotedFrom, promotedTo }` |
|
||||
| All other triggers | NO | NO |
|
||||
|
||||
Future triggers that need either capability follow the same pattern: store the resolver in the seeded hook entry, then thread it into `ctx.target` at fire time.
|
||||
|
||||
### Don't use `GAME_ENTITY` for preset state
|
||||
|
||||
The reserved `PRESET_STATE_ENTITY` + `engine.presetState<T>(id)` API is namespaced, serializes automatically, and clears on deactivate. Putting preset-specific data on `GAME_ENTITY` (like the old `capture-to-win.Winner` fact) works today but collides with future presets and doesn't auto-clear.
|
||||
|
|
@ -526,46 +619,46 @@ The reserved `PRESET_STATE_ENTITY` + `engine.presetState<T>(id)` API is namespac
|
|||
|
||||
`packages/chess/src/presets/test-utils.ts` exports:
|
||||
|
||||
- `pieceAt(engine, square)` — find piece on algebraic square.
|
||||
- `hpOf(engine, id)` / `typeOf(engine, id)` / `exists(engine, id)` — attribute queries.
|
||||
- `clearBoard(engine, { preserveKings })` — strip the board to minimal pieces.
|
||||
- `placePiece(engine, type, color, square, opts?)` — direct session insert (bypasses spawnPiece).
|
||||
- `pieceAt(engine, square)`, find piece on algebraic square.
|
||||
- `hpOf(engine, id)` / `typeOf(engine, id)` / `exists(engine, id)`, attribute queries.
|
||||
- `clearBoard(engine, { preserveKings })`, strip the board to minimal pieces.
|
||||
- `placePiece(engine, type, color, square, opts?)`, direct session insert (bypasses spawnPiece).
|
||||
|
||||
For preset tests that exercise hooks, see:
|
||||
- `damage-pipeline.test.ts` — damage + composition
|
||||
- `preset-state.test.ts` — per-preset state bag
|
||||
- `move-log.test.ts` — MoveRecord / describeMoveEffect
|
||||
- `visual-effect.test.ts` — emitEffect / subscribeEffects
|
||||
- `phase-hooks.test.ts` — onBeforeMove / onTurnStart
|
||||
- `integration.test.ts` — Shield / Cannon / Berserker / Stamina end-to-end
|
||||
- `damage-pipeline.test.ts`, damage + composition
|
||||
- `preset-state.test.ts`, per-preset state bag
|
||||
- `move-log.test.ts`, MoveRecord / describeMoveEffect
|
||||
- `visual-effect.test.ts`, emitEffect / subscribeEffects
|
||||
- `phase-hooks.test.ts`, onBeforeMove / onTurnStart
|
||||
- `integration.test.ts`, Shield / Cannon / Berserker / Stamina end-to-end
|
||||
|
||||
---
|
||||
|
||||
## Rule Variants Gallery (2026 epic)
|
||||
|
||||
The following 14 presets shipped in the rule-variants epic, mirroring variants from greenchess.net/variants.php?cat=4. All compose with the existing presets via the hook surface documented above — no engine changes beyond the 4 new hooks shipped in Phase A.
|
||||
The following 14 presets shipped in the rule-variants epic, mirroring variants from greenchess.net/variants.php?cat=4. All compose with the existing presets via the hook surface documented above, no engine changes beyond the 4 new hooks shipped in Phase A.
|
||||
|
||||
**King Variants** — redefine what counts as "royal":
|
||||
- `knightmate-rules` — Every knight of `color` is royal; kings are ordinary pieces.
|
||||
- `coregal` — King AND queen are royal; mate on either ends the game.
|
||||
- `dual-king` — Multiple kings per side; mate on any one ends the game ("strong").
|
||||
- `weak-dual-king` — Multiple kings per side; mating one is survivable; opts out of self-check filter so "sacrifice" moves on one king are legal.
|
||||
**King Variants**, redefine what counts as "royal":
|
||||
- `knightmate-rules`, Every knight of `color` is royal; kings are ordinary pieces.
|
||||
- `coregal`, King AND queen are royal; mate on either ends the game.
|
||||
- `dual-king`, Multiple kings per side; mate on any one ends the game ("strong").
|
||||
- `weak-dual-king`, Multiple kings per side; mating one is survivable; opts out of self-check filter so "sacrifice" moves on one king are legal.
|
||||
|
||||
**Objectives** — redefine the terminal-state condition:
|
||||
- `first-promotion-wins` — First pawn to promote ends the game. Pairs with `pawns-only` layout.
|
||||
- `suicide-chess` — Lose all pieces to win. Captures compulsory (via `filterLegalMoves`). Empty royal set. Stalemate-wins.
|
||||
- `capture-all` — Inverse of suicide-chess: capture every enemy to win. Captures not compulsory.
|
||||
- `extinction-chess` — Wipe out every enemy of a configurable TARGET TYPE. Target set via `engine.presetState<{ targetType: PieceType }>("extinction-chess")`; default `"pawn"`. King remains royal; default mate rules still apply until extinction triggers.
|
||||
**Objectives**, redefine the terminal-state condition:
|
||||
- `first-promotion-wins`, First pawn to promote ends the game. Pairs with `pawns-only` layout.
|
||||
- `suicide-chess`, Lose all pieces to win. Captures compulsory (via `filterLegalMoves`). Empty royal set. Stalemate-wins.
|
||||
- `capture-all`, Inverse of suicide-chess: capture every enemy to win. Captures not compulsory.
|
||||
- `extinction-chess`, Wipe out every enemy of a configurable TARGET TYPE. Target set via `engine.presetState<{ targetType: PieceType }>("extinction-chess")`; default `"pawn"`. King remains royal; default mate rules still apply until extinction triggers.
|
||||
|
||||
**Multi-move** — redefine the turn flip:
|
||||
- `double-move` — Both sides play 2 half-moves per turn.
|
||||
- `monster-rules` — Asymmetric; **scope-aware**. `scope: "white"` → white plays 2, black plays 1 (canonical Monster pairing). `scope: "black"` flips which side gets the double. `scope: "both"` ≡ `double-move`.
|
||||
**Multi-move**, redefine the turn flip:
|
||||
- `double-move`, Both sides play 2 half-moves per turn.
|
||||
- `monster-rules`, Asymmetric; **scope-aware**. `scope: "white"` → white plays 2, black plays 1 (canonical Monster pairing). `scope: "black"` flips which side gets the double. `scope: "both"` ≡ `double-move`.
|
||||
|
||||
**Movement** — redefine piece-specific move generation:
|
||||
- `berolina-pawns` — **scope-aware**; pawns push diagonally, capture orthogonally. `scope: "white"|"black"|"both"` picks which side(s) use the rule.
|
||||
- `berolina-pawns-2` — Berolina plus SIDEWAYS captures.
|
||||
- `bouncing-pieces` — Bishop/queen diagonals reflect off file edges (a-file, h-file) once.
|
||||
- `bouncing-pieces-2` — Reflects off ALL four edges with a 2-bounce cap.
|
||||
**Movement**, redefine piece-specific move generation:
|
||||
- `berolina-pawns`, **scope-aware**; pawns push diagonally, capture orthogonally. `scope: "white"|"black"|"both"` picks which side(s) use the rule.
|
||||
- `berolina-pawns-2`, Berolina plus SIDEWAYS captures.
|
||||
- `bouncing-pieces`, Bishop/queen diagonals reflect off file edges (a-file, h-file) once.
|
||||
- `bouncing-pieces-2`, Reflects off ALL four edges with a 2-bounce cap.
|
||||
|
||||
### Scope-flip pattern (reusable)
|
||||
|
||||
|
|
@ -594,12 +687,12 @@ The engine implements a two-phase protocol (fixed in commit `7171dfd`): a TERMIN
|
|||
|
||||
Documented but NOT yet available:
|
||||
|
||||
- `onPieceRetract` — symmetric counterpart to `onPieceSpawn`. Needed for "death rattle" mechanics.
|
||||
- Per-preset UI panels (not just overlays) — extension slot for sidebar widgets.
|
||||
- Server-side piece-type manifest echo — when custom types are authored outside the shared `packages/chess`.
|
||||
- Save-state migration — versioning for saves that predate attribute additions.
|
||||
- `onPieceRetract`, symmetric counterpart to `onPieceSpawn`. Needed for "death rattle" mechanics.
|
||||
- Per-preset UI panels (not just overlays), extension slot for sidebar widgets.
|
||||
- Server-side piece-type manifest echo, when custom types are authored outside the shared `packages/chess`.
|
||||
- Save-state migration, versioning for saves that predate attribute additions.
|
||||
|
||||
- Extinction-chess multiplayer target sync — solo cycler shipped in
|
||||
- Extinction-chess multiplayer target sync, solo cycler shipped in
|
||||
post-epic Feature 2; MP target is fixed at room creation until a
|
||||
`preset-config.update` WS message lands.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue