feat(engine): getRoyalPieces hook + engine royal dispatch

Phase A.1 of the rule-variants epic — enables preset-driven royalty
override. A preset can now declare WHICH pieces count as royal for a
given color; the engine unions contributions across active presets
and threads the resolved set through isInCheck, isCheckmate,
isStalemate, and the self-check filter.

- Adds RoyalContext type + getRoyalPieces hook on PresetDef.
- Widens isInCheck/isCheckmate/isStalemate/filterSelfCheckMoves with
  optional royalEntityIds param; empty set short-circuits to 'no
  royalty, nothing in check'; undefined keeps legacy king-only path.
- Adds private ChessEngine.getActiveRoyalEntityIds(color) that
  returns undefined when no preset contributed (preserves back-compat)
  and the union of contributions otherwise.
- Threads the resolver into all three internal consumers:
  getAllLegalMoves self-check filter, applyMove opponent-check
  logging, and checkGameResult checkmate/stalemate detection.
- Adds packages/chess/src/presets/royal-pieces.test.ts (11 tests)
  covering default, single-preset, overlap-union, and empty-royal
  paths at both the pure-function and engine-integration layers.

Tests: 1428 passing (was 1417, +11 new). Typecheck + lint clean.
Blocks: unblocks B.1 (knightmate-rules), C.1 (coregal), C.2
(dual-king), C.3 (weak-dual-king), D.1 (suicide-chess), D.2
(capture-all).
This commit is contained in:
Joey Yakimowich-Payne 2026-04-20 20:04:29 -06:00
commit 4d05473919
No known key found for this signature in database
7 changed files with 664 additions and 38 deletions

View file

@ -70,3 +70,57 @@ work progresses.
## Running progress
(append entries as you ship tasks, one per task, newest at top)
## [2026-04-20 20:03] Task: A.1 — getRoyalPieces hook + engine royal dispatch
**Shipped**:
- `RoyalContext` type + `getRoyalPieces` hook on `PresetDef`
(`packages/chess/src/presets/registry.ts`). Hook returns
`readonly EntityId[] | undefined`. Undefined = no opinion (fall
through). `[]` = explicit empty (no royalty).
- `isInCheck(session, color, royalEntityIds?)` widened
(`packages/chess/src/rules/check.ts`). Empty set → always false.
Default (undefined) → legacy "every PieceType=king" path via the
new private `defaultRoyalIds` helper.
- `filterSelfCheckMoves(session, moves, color, royalEntityIds?)`
widened. Empty set → every move passes. Threaded through `isInCheck`
on the temp session.
- `isCheckmate(session, color, royalEntityIds?)` and
`isStalemate(session, color, royalEntityIds?)` widened identically.
- `ChessEngine.getActiveRoyalEntityIds(color): readonly EntityId[] | undefined`
private method (`engine.ts`). Unions contributions from every active
preset's `getRoyalPieces`. Returns `undefined` iff NO preset
contributed (preserves legacy default path). Threaded into all three
internal call sites: the self-check filter in `getAllLegalMoves`,
`opponentInCheck` logging in `applyMove`, and `checkmate`/`stalemate`
in `checkGameResult`.
- New test file: `packages/chess/src/presets/royal-pieces.test.ts`
(11 tests). Covers: default path unchanged, knight-royal via preset,
empty-royal short-circuit, overlap-union dedupe, pure-function
widening of all three rule predicates.
**Verification**: `bun run check` green. 1428 tests (was 1417, +11 new).
124 files (was 122, +2 — royal-pieces.test.ts + its test utilities
section).
**Gotchas learned**:
- `ActivationRequest` requires `turnsRemaining` (can be `null`).
Omitting it → TypeScript error. Caught by LSP diagnostics
immediately, not at test time.
- Test board construction: the default engine has the FIDE pawn rank,
so `pieceAt(engine, "e1")` patterns on a modified default are
fragile. Prefer `clearBoard(engine, { preserveKings: false })` +
explicit `placePiece` for every piece.
- `filterSelfCheckMoves` doesn't validate piece mechanics — it only
verifies king-exposure. Good for engine-internal use; make tests
aware that geometrically-illegal moves may pass the filter anyway.
**Files touched**:
- `packages/chess/src/presets/registry.ts` (+33 lines: RoyalContext + hook)
- `packages/chess/src/rules/check.ts` (+~60 lines: widened isInCheck + filterSelfCheckMoves + defaultRoyalIds helper)
- `packages/chess/src/rules/checkmate.ts` (+~15 lines: widened isCheckmate)
- `packages/chess/src/rules/stalemate.ts` (+~25 lines: widened hasAnyLegalMove + isStalemate)
- `packages/chess/src/engine.ts` (+~60 lines: getActiveRoyalEntityIds + 3 call-site threads + RoyalContext import)
- `packages/chess/src/presets/royal-pieces.test.ts` (new, 300+ lines, 11 tests)
**Blocks**: A.1 now clears B.1, C.1, C.2, C.3, D.1, D.2. Phase A remaining: A.2 (filterLegalMoves), A.3 (shouldAdvanceTurn + HalfMovesThisTurn), A.4 (overridePieceMoves), A.5 (gate + PRESET-API.md docs).