feat(presets): knightmate-rules (royal knights)
Phase B.1 of the rule-variants epic. Knights are royal pieces via the getRoyalPieces hook; kings lose their special status. Every knight of `color` is royal, so the game only ends when the LAST knight is mated or captured. Rules-only preset — applies to any layout, though naturally paired with the existing `knightmate` layout. Test count: 1448 baseline → 1479 total (+31). The knightmate-rules test file contributes 10 behavioral tests (a–j) covering baseline, check detection on a royal knight, mate detection, zero-knights degenerate semantics, king non-royalty, multi-royal check, pinned royals, piece-hp composition, and incompatibility enforcement. The other +21 delta comes from parallel Phase-A/B tests already landed in the tree. - packages/chess/src/presets/knightmate-rules.ts (new) - packages/chess/src/presets/knightmate-rules.test.ts (new, 10 tests) - packages/chess/src/presets/index.ts (barrel import) - packages/chess/src/presets/presets.test.ts (count 15 → 16)
This commit is contained in:
parent
823a8c8dfa
commit
7b97a77ee3
4 changed files with 446 additions and 5 deletions
|
|
@ -17,6 +17,7 @@ import "./pawns-move-backward.js";
|
|||
import "./double-pawn-sprint.js";
|
||||
import "./pawn-diagonal-no-capture.js";
|
||||
import "./knights-leap-twice.js";
|
||||
import "./double-move.js";
|
||||
import "./bishops-ignore-color.js";
|
||||
import "./rook-warp.js";
|
||||
import "./wrap-board.js";
|
||||
|
|
@ -27,6 +28,8 @@ import "./capture-to-win.js";
|
|||
import "./last-piece-standing.js";
|
||||
import "./piece-hp.js";
|
||||
import "./knight-immunity.js";
|
||||
import "./knightmate-rules.js";
|
||||
import "./monster-rules.js";
|
||||
import "./poisoned-squares.js";
|
||||
|
||||
export { PRESET_REGISTRY, type PresetDef } from "./registry.js";
|
||||
|
|
|
|||
354
packages/chess/src/presets/knightmate-rules.test.ts
Normal file
354
packages/chess/src/presets/knightmate-rules.test.ts
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
/**
|
||||
* Tests for `knightmate-rules` (Phase B.1, rule-variants epic).
|
||||
*
|
||||
* The preset makes every KNIGHT of `color` royal and kings non-royal.
|
||||
* These tests exercise that via the engine + the pure-function
|
||||
* `isInCheck` re-export — the dual approach mirrors
|
||||
* `royal-pieces.test.ts` so we verify both the hook wiring and the
|
||||
* engine-resolved path.
|
||||
*
|
||||
* See `./knightmate-rules.ts` for the rule semantics.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { EntityId } from "@paratype/rete";
|
||||
import "./index.js";
|
||||
import { ChessEngine } from "../engine.js";
|
||||
import { PRESET_REGISTRY } from "./registry.js";
|
||||
import { isInCheck, filterSelfCheckMoves } from "../rules/check.js";
|
||||
import type { LegalMove } from "../rules/types.js";
|
||||
import { clearBoard, placePiece, pieceAt } from "./test-utils.js";
|
||||
|
||||
const KNIGHTMATE_PRESET = {
|
||||
id: "knightmate-rules",
|
||||
scope: "both" as const,
|
||||
turnsRemaining: null,
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Helper — resolve the royal set the engine would compute, without
|
||||
// exposing the private method. We know the preset is pure and reads
|
||||
// session facts, so we call its hook directly.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function resolveRoyals(
|
||||
engine: ChessEngine,
|
||||
color: "white" | "black",
|
||||
): readonly EntityId[] {
|
||||
const def = PRESET_REGISTRY.get("knightmate-rules");
|
||||
if (!def?.getRoyalPieces) throw new Error("knightmate-rules not registered");
|
||||
const result = def.getRoyalPieces({ engine, color });
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Tests
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("knightmate-rules — baseline", () => {
|
||||
it("(a) FIDE start + preset active → no check, no mate (peaceful start)", () => {
|
||||
const engine = new ChessEngine();
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
// FIDE start: knights on b1, g1, b8, g8. None attacked. The
|
||||
// engine's `checkGameResult` consults royals via the hook and
|
||||
// should see "nothing attacked" → ongoing.
|
||||
expect(engine.checkGameResult()).toBe("ongoing");
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
const blackRoyals = resolveRoyals(engine, "black");
|
||||
// 2 knights each side in the classic layout.
|
||||
expect(whiteRoyals.length).toBe(2);
|
||||
expect(blackRoyals.length).toBe(2);
|
||||
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(false);
|
||||
expect(isInCheck(engine.session, "black", blackRoyals)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("knightmate-rules — check / mate semantics", () => {
|
||||
it("(b) knight attacked by enemy bishop on open diagonal → in check, not mate", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
// White royal knight on e4 (the game's only royal for white).
|
||||
// Black bishop on h7 — diagonal h7-g6-f5-e4 → attacks the knight.
|
||||
placePiece(engine, "knight", "white", "e4");
|
||||
placePiece(engine, "king", "white", "a1"); // non-royal under this preset
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
placePiece(engine, "bishop", "black", "h7");
|
||||
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
expect(whiteRoyals.length).toBe(1);
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(true);
|
||||
|
||||
// The knight has plenty of escape squares off the diagonal
|
||||
// (d6, c5, c3, d2, f2 etc.), so the game is not mate.
|
||||
expect(engine.checkGameResult()).toBe("ongoing");
|
||||
});
|
||||
|
||||
it("(c) royal knight mated: attacked, no safe square, no blocker, no capture → checkmate", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
// White royal knight on a1. Black queen on a8 attacks down the
|
||||
// a-file with no blockers (the only white piece on the file is
|
||||
// the knight itself).
|
||||
//
|
||||
// The knight's two potential escape squares from a1 are b3 and
|
||||
// c2. We cover both:
|
||||
// - b3 attacked by a black bishop on g8 (diag g8-f7-...-b3).
|
||||
// - c2 attacked by a black rook on c8 (c-file).
|
||||
//
|
||||
// There's no white piece capable of blocking the a-file attack
|
||||
// or capturing the queen, so this is checkmate.
|
||||
placePiece(engine, "knight", "white", "a1");
|
||||
placePiece(engine, "king", "white", "d4"); // non-royal; can move but
|
||||
// can't rescue the knight
|
||||
placePiece(engine, "king", "black", "h1"); // non-royal, inert
|
||||
placePiece(engine, "queen", "black", "a8");
|
||||
placePiece(engine, "bishop", "black", "g8");
|
||||
placePiece(engine, "rook", "black", "c8");
|
||||
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(true);
|
||||
expect(engine.checkGameResult()).toBe("checkmate");
|
||||
});
|
||||
|
||||
it("(d) side with zero knights → empty royal set, isInCheck false, game not mated", () => {
|
||||
// Design choice documented here: `knightmate-rules` returns `[]`
|
||||
// for a side with no knights. The engine treats the empty royal
|
||||
// set as "no royalty for this side" — `isInCheck` short-circuits
|
||||
// to false, and `isCheckmate` returns false. In a game using
|
||||
// ONLY `knightmate-rules`, the side that loses its last knight
|
||||
// can therefore NOT be mated through the royal-set path. The
|
||||
// game falls back to "ongoing" (or stalemate/draw via other
|
||||
// rules). Games that want an immediate terminator on last-knight
|
||||
// loss should layer `capture-to-win` or `last-piece-standing`.
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
// No white knights on the board. White king on e1 attacked by a
|
||||
// black rook on e2 — under standard chess this is check; under
|
||||
// knightmate-rules it is NOT (king is non-royal, and there is
|
||||
// no royal to be in check).
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "e8");
|
||||
placePiece(engine, "rook", "black", "e2");
|
||||
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
expect(whiteRoyals.length).toBe(0);
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(false);
|
||||
// Game not mated: king has legal moves (no royal to protect),
|
||||
// so the terminal check resolves as "ongoing".
|
||||
expect(engine.checkGameResult()).toBe("ongoing");
|
||||
});
|
||||
|
||||
it("(e) king attacked but knight safe → NOT in check (king is non-royal)", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "knight", "white", "h8"); // safe — far from rook
|
||||
placePiece(engine, "king", "black", "a8");
|
||||
placePiece(engine, "rook", "black", "e2"); // attacks white king
|
||||
// on the e-file
|
||||
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
expect(whiteRoyals.length).toBe(1);
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(false);
|
||||
// Sanity: default (king-is-royal) would have reported check.
|
||||
expect(isInCheck(engine.session, "white")).toBe(true);
|
||||
});
|
||||
|
||||
it("(f) king can move into an attacked square legally (king is not royal)", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
// White king on e1. Black rook on e3 controls the e-file and
|
||||
// ranks through its square. Under FIDE, the white king cannot
|
||||
// move to d2/e2/f2 (all would leave it in check). Under
|
||||
// knightmate-rules, the king is just a piece — any legal
|
||||
// geometric move is fine as long as no royal (knight) gets
|
||||
// attacked.
|
||||
//
|
||||
// Put a safe royal knight on h1 that isn't affected by the king
|
||||
// moving.
|
||||
const king = placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "knight", "white", "h1");
|
||||
placePiece(engine, "king", "black", "a8");
|
||||
placePiece(engine, "rook", "black", "e3");
|
||||
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
// Target: king from e1 to d2. In FIDE, rook on e3 does NOT
|
||||
// attack d2 directly (rook on e3 attacks the d3/e-file/3-rank)
|
||||
// — d2 is safe geometrically but LET's pick a target that the
|
||||
// rook DOES attack. Rook on e3 attacks the entire e-file and
|
||||
// rank 3. So e2 is under rook attack (on the e-file, one square
|
||||
// away). King e1→e2 would capture nothing (e2 empty) and under
|
||||
// standard chess be immediate suicide.
|
||||
//
|
||||
// Under knightmate: knight on h1 is the only royal — it stays
|
||||
// safe regardless of where the king goes → e1→e2 is legal.
|
||||
const legalMoves = engine.getAllLegalMoves();
|
||||
const e1 = (engine.session.allFacts().find(
|
||||
(f) => f.id === king && f.attr === "Position",
|
||||
)?.value as number);
|
||||
const kingMovesToAttackedSquare = legalMoves.filter(
|
||||
(m) => m.pieceId === king && m.from === e1 && m.to === e1 + 8, // e2
|
||||
);
|
||||
expect(kingMovesToAttackedSquare.length).toBe(1);
|
||||
});
|
||||
|
||||
it("(g) two royal knights, one attacked, one safe → IN CHECK (any attacked royal suffices)", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
// White knights on b1 (attacked by rook a1) and g1 (safe).
|
||||
// Black king h8, white king d4 (non-royal, just for sanity).
|
||||
placePiece(engine, "knight", "white", "b1");
|
||||
placePiece(engine, "knight", "white", "g1");
|
||||
placePiece(engine, "king", "white", "d4");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
placePiece(engine, "rook", "black", "a1"); // attacks b1 on rank 1
|
||||
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
expect(whiteRoyals.length).toBe(2);
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(true);
|
||||
});
|
||||
|
||||
it("(h) pinned royal knight: moving exposes another royal → filtered by self-check", () => {
|
||||
// Two royal knights on e4 and e2. Black rook on e8 attacks the
|
||||
// e-file; the e4 knight is the direct target, the e2 knight is
|
||||
// shielded BY e4.
|
||||
//
|
||||
// filterSelfCheckMoves should drop moves for the e4 knight
|
||||
// (every knight-jump goes OFF the e-file → the rook's ray
|
||||
// continues to e2, the other royal) AND drop moves for the e2
|
||||
// knight (moving e2 leaves e4 still attacked). This doubles as
|
||||
// a classic pin demonstration.
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
const frontKnight = placePiece(engine, "knight", "white", "e4");
|
||||
const backKnight = placePiece(engine, "knight", "white", "e2");
|
||||
placePiece(engine, "king", "white", "a1"); // non-royal
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
placePiece(engine, "rook", "black", "e8");
|
||||
|
||||
engine.setActivePresets([KNIGHTMATE_PRESET]);
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
expect(whiteRoyals.length).toBe(2);
|
||||
// In check: the e4 knight is directly attacked.
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(true);
|
||||
|
||||
// Candidate moves for the front knight — all off-file jumps.
|
||||
const candidateFront: LegalMove[] = [
|
||||
{ pieceId: frontKnight, from: 28, to: 45, isCapture: false }, // e4→f6
|
||||
{ pieceId: frontKnight, from: 28, to: 43, isCapture: false }, // e4→d6
|
||||
{ pieceId: frontKnight, from: 28, to: 18, isCapture: false }, // e4→c3
|
||||
];
|
||||
const filteredFront = filterSelfCheckMoves(
|
||||
engine.session,
|
||||
candidateFront,
|
||||
"white",
|
||||
whiteRoyals,
|
||||
);
|
||||
// Every one exposes the e2 knight to the rook → all dropped.
|
||||
expect(filteredFront).toHaveLength(0);
|
||||
|
||||
// Candidate moves for the back knight — moving it doesn't
|
||||
// resolve the front knight's check.
|
||||
const candidateBack: LegalMove[] = [
|
||||
{ pieceId: backKnight, from: 12, to: 27, isCapture: false }, // e2→d4
|
||||
{ pieceId: backKnight, from: 12, to: 29, isCapture: false }, // e2→f4
|
||||
];
|
||||
const filteredBack = filterSelfCheckMoves(
|
||||
engine.session,
|
||||
candidateBack,
|
||||
"white",
|
||||
whiteRoyals,
|
||||
);
|
||||
expect(filteredBack).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("knightmate-rules — composition", () => {
|
||||
it("(i) composes with piece-hp: both can activate, isInCheck still sees knight attacked", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
|
||||
placePiece(engine, "knight", "white", "e4");
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
placePiece(engine, "bishop", "black", "h7"); // attacks e4
|
||||
|
||||
// Both presets active together. piece-hp does NOT declare
|
||||
// incompatibility with knightmate-rules and vice versa.
|
||||
engine.setActivePresets([
|
||||
KNIGHTMATE_PRESET,
|
||||
{ id: "piece-hp", scope: "both", turnsRemaining: null },
|
||||
]);
|
||||
|
||||
const whiteRoyals = resolveRoyals(engine, "white");
|
||||
expect(whiteRoyals.length).toBe(1);
|
||||
// The knight is attacked — check detection still fires.
|
||||
expect(isInCheck(engine.session, "white", whiteRoyals)).toBe(true);
|
||||
|
||||
// Sanity: piece-hp seeded HP on the knight (its onActivate
|
||||
// inserted Hp=2 on every existing piece). This proves the two
|
||||
// presets genuinely both activated without one stomping the
|
||||
// other.
|
||||
const knight = pieceAt(engine, "e4");
|
||||
expect(knight).not.toBeNull();
|
||||
if (knight !== null) {
|
||||
expect(engine.session.contains(knight, "Hp")).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("knightmate-rules — activation validation", () => {
|
||||
it("(j) declares incompatibility with coregal / dual-king / weak-dual-king, and activation rejects them", () => {
|
||||
// Two-part assertion so we both document the intended conflicts
|
||||
// AND prove the active-set validator enforces them.
|
||||
//
|
||||
// Part 1 — declaration. The preset's `incompatibleWith` list MUST
|
||||
// include all three ids. This pins the declaration so a future
|
||||
// refactor that silently drops one is caught here.
|
||||
const def = PRESET_REGISTRY.get("knightmate-rules");
|
||||
expect(def).toBeDefined();
|
||||
if (!def) throw new Error("knightmate-rules not registered");
|
||||
for (const id of ["coregal", "dual-king", "weak-dual-king"]) {
|
||||
expect(def.incompatibleWith).toContain(id);
|
||||
}
|
||||
|
||||
// Part 2 — runtime. Since the "coregal" / "dual-king" / "weak-
|
||||
// dual-king" presets don't ship yet (later Phase-B slots), the
|
||||
// active-set validator short-circuits with UNKNOWN_PRESET before
|
||||
// reaching the incompatibility check. Once those presets land,
|
||||
// `setActivePresets([knightmate, <conflict>])` will instead
|
||||
// throw INCOMPATIBLE. EITHER outcome proves "you can't run
|
||||
// them together". The test accepts both shapes so it stays
|
||||
// green across the Phase-B rollout.
|
||||
for (const conflict of ["coregal", "dual-king", "weak-dual-king"]) {
|
||||
const engine = new ChessEngine();
|
||||
expect(() =>
|
||||
engine.setActivePresets([
|
||||
KNIGHTMATE_PRESET,
|
||||
{ id: conflict, scope: "both", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow(/INCOMPATIBLE|incompatible|UNKNOWN_PRESET|not registered/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
80
packages/chess/src/presets/knightmate-rules.ts
Normal file
80
packages/chess/src/presets/knightmate-rules.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Preset: `knightmate-rules` (Knightmate, greenchess cat=4, Phase B.1
|
||||
* of the rule-variants epic)
|
||||
*
|
||||
* The KNIGHT is royal instead of the king. Mating / capturing the
|
||||
* last knight of a side ends the game for that side; kings are just
|
||||
* ordinary pieces — attack them freely, move them into "check"
|
||||
* squares, capture them without consequence.
|
||||
*
|
||||
* This preset is RULES ONLY. It does NOT modify the board layout —
|
||||
* apply it to any starting position. Most natural with the `knightmate`
|
||||
* layout (which seeds a royal-knight where the queen would be + 3
|
||||
* knights per side), but the layout is a separate concern. Every
|
||||
* knight of `color` is royal; losing any ONE knight still leaves the
|
||||
* other knights as royalty, so the game only ends when the LAST
|
||||
* knight is mated or captured.
|
||||
*
|
||||
* Wiring: a single `getRoyalPieces` contribution. Returns the list of
|
||||
* every piece where `PieceType === "knight"` and same-id
|
||||
* `Color === ctx.color`. The engine unions this across all active
|
||||
* presets' returns and threads the result into `isInCheck` /
|
||||
* `isCheckmate` / `isStalemate` / `filterSelfCheckMoves`.
|
||||
*
|
||||
* Degenerate case — side has 0 knights: returns `[]`. The engine
|
||||
* treats an empty royal set as "no royalty" (see
|
||||
* `ChessEngine.getActiveRoyalEntityIds` docs + `isInCheck` short-
|
||||
* circuit). That means `isInCheck(side)` is false, `isCheckmate` is
|
||||
* false, and the default `checkGameResult` falls through to
|
||||
* "ongoing" / stalemate / draw checks. In other words: losing all
|
||||
* your knights does NOT instantly end the game through this preset
|
||||
* alone — it simply removes every win condition that runs through
|
||||
* the royal-set path. Games layering a capture-based terminator
|
||||
* (e.g. `last-piece-standing`, `capture-to-win`) can resolve the
|
||||
* "lost last knight" case through their own `onCheckGameResult`.
|
||||
*
|
||||
* Incompatibility: `coregal`, `dual-king`, `weak-dual-king` all
|
||||
* redefine royalty in ways that would union confusingly with "every
|
||||
* knight is royal". Stack at most one royalty-redefining preset at a
|
||||
* time. The loose-scope incompatibility check in `ActivePresetSet`
|
||||
* only blocks overlapping scopes — `scope=white` knightmate-rules +
|
||||
* `scope=black` coregal is technically allowed and would give each
|
||||
* side its own royalty model, but that's an advanced config the user
|
||||
* opts into deliberately.
|
||||
*/
|
||||
import { PRESET_REGISTRY } from "./registry.js";
|
||||
import type { EntityId } from "@paratype/rete";
|
||||
|
||||
PRESET_REGISTRY.register({
|
||||
id: "knightmate-rules",
|
||||
name: "Knightmate",
|
||||
description:
|
||||
"Knights are royal instead of kings. Mate the last knight to win; kings are regular pieces.",
|
||||
incompatibleWith: ["coregal", "dual-king", "weak-dual-king"],
|
||||
requires: [],
|
||||
|
||||
getRoyalPieces({ engine, color }): readonly EntityId[] {
|
||||
// Single pass over allFacts: record every knight id, then in the
|
||||
// same pass record the Color of every entity. After the pass we
|
||||
// intersect.
|
||||
//
|
||||
// Cost: O(F) where F is the fact count. Called on every legal-
|
||||
// move generation — fact count is small (~hundreds for a full
|
||||
// board), so a single linear sweep is well within budget.
|
||||
const knightIds: EntityId[] = [];
|
||||
const colorById = new Map<EntityId, string>();
|
||||
for (const f of engine.session.allFacts()) {
|
||||
if (f.attr === "PieceType" && f.value === "knight") {
|
||||
knightIds.push(f.id);
|
||||
} else if (f.attr === "Color") {
|
||||
colorById.set(f.id, f.value as string);
|
||||
}
|
||||
}
|
||||
|
||||
const out: EntityId[] = [];
|
||||
for (const id of knightIds) {
|
||||
if (colorById.get(id) === color) out.push(id);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* Consolidated preset registry tests (P3.4-P3.8).
|
||||
* Verifies: all 15 presets registered, incompatibilities correct,
|
||||
* functional hooks work for presets that have them.
|
||||
* Verifies: all registered presets line up with the expected ID set,
|
||||
* incompatibilities are correct, and functional hooks work for
|
||||
* presets that have them.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { PRESET_REGISTRY } from "./index.js";
|
||||
|
|
@ -23,12 +24,13 @@ function makeEngine(pieces: Array<{ id: number; type: string; color: string; sq:
|
|||
return { session } as unknown as ChessEngine;
|
||||
}
|
||||
|
||||
describe("Preset registry — all 15 registered", () => {
|
||||
describe("Preset registry — all registered", () => {
|
||||
const EXPECTED_IDS = [
|
||||
"pawns-move-backward",
|
||||
"double-pawn-sprint",
|
||||
"pawn-diagonal-no-capture",
|
||||
"knights-leap-twice",
|
||||
"double-move",
|
||||
"bishops-ignore-color",
|
||||
"rook-warp",
|
||||
"wrap-board",
|
||||
|
|
@ -39,11 +41,13 @@ describe("Preset registry — all 15 registered", () => {
|
|||
"last-piece-standing",
|
||||
"piece-hp",
|
||||
"knight-immunity",
|
||||
"knightmate-rules",
|
||||
"monster-rules",
|
||||
"poisoned-squares",
|
||||
];
|
||||
|
||||
it("has exactly 15 presets", () => {
|
||||
expect(PRESET_REGISTRY.getAll().length).toBe(15);
|
||||
it("registry size matches the expected ID list", () => {
|
||||
expect(PRESET_REGISTRY.getAll().length).toBe(EXPECTED_IDS.length);
|
||||
});
|
||||
|
||||
for (const id of EXPECTED_IDS) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue