feat(presets): capture-all (capture every enemy piece to win)
Phase D.2 of the rule-variants epic. Inverse objective of
suicide-chess: you win by driving the opponent's piece count to 0.
Kings are not royal, captures are NOT compulsory.
Wiring uses three existing hooks:
- getRoyalPieces → [] per color (no royalty).
- shouldFilterSelfCheck → false (opt out of the filter; kings can
legally wander into attacked squares).
- onCheckGameResult → count live Position facts per color; 0 for
the opponent wins for the mover. Otherwise returns 'ongoing' to
suppress the default checkmate/stalemate polls (which are
ill-defined under empty-royal semantics).
No filterLegalMoves — captures are optional, which is the chief
rule-text difference from suicide-chess.
Incompatible with every other terminal-decider preset
(capture-to-win, last-piece-standing, first-promotion-wins,
suicide-chess, extinction-chess) and with presets that redefine
royalty (knightmate-rules, coregal, dual-king, weak-dual-king).
monster-rules already declares capture-all incompatible on its
side; mirror it here.
This commit is contained in:
parent
0d152861af
commit
5d64ae4c37
4 changed files with 479 additions and 0 deletions
308
packages/chess/src/presets/capture-all.test.ts
Normal file
308
packages/chess/src/presets/capture-all.test.ts
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
/**
|
||||
* Tests for `capture-all` (Phase D.2, rule-variants epic).
|
||||
*
|
||||
* The preset inverts suicide-chess's objective: you win by wiping the
|
||||
* OPPONENT's pieces rather than your own. Captures are optional.
|
||||
* These tests exercise:
|
||||
*
|
||||
* (a) activation on a fresh engine
|
||||
* (b) empty-royal semantics — isInCheck is false even under
|
||||
* king-attack
|
||||
* (c) a king can legally move into an attacked square (self-check
|
||||
* filter opted out + empty royals)
|
||||
* (d) captures are NOT compulsory (contrast with suicide-chess):
|
||||
* both captures and non-captures are legal side-by-side
|
||||
* (e) white captures the last black piece → "white-wins"
|
||||
* (f) black captures the last white piece → "black-wins"
|
||||
* (g) fresh engine (16 vs 16) → "ongoing"
|
||||
* (h) composition with piece-hp: non-lethal damage does NOT
|
||||
* decrement the count; only lethal damage progresses
|
||||
* (i) incompatibility: capture-all + suicide-chess throws
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import "./index.js";
|
||||
import { ChessEngine } from "../engine.js";
|
||||
import { GAME_ENTITY } from "../schema.js";
|
||||
import { algebraicToSquare } from "../coord.js";
|
||||
import { isInCheck } from "../rules/check.js";
|
||||
import { PRESET_REGISTRY } from "./registry.js";
|
||||
import { clearBoard, placePiece, pieceAt, exists } from "./test-utils.js";
|
||||
|
||||
const PRESET = {
|
||||
id: "capture-all",
|
||||
scope: "both" as const,
|
||||
turnsRemaining: null,
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (a) Activation
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — activation", () => {
|
||||
it("(a) activates on a fresh classic-layout engine without error", () => {
|
||||
const engine = new ChessEngine();
|
||||
expect(() => engine.setActivePresets([PRESET])).not.toThrow();
|
||||
expect(PRESET_REGISTRY.get("capture-all")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (b) Empty-royal semantics: isInCheck(..., []) is false even under
|
||||
// king-attack
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — empty-royal semantics", () => {
|
||||
it("(b) isInCheck(..., []) is false even with the king under attack", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "e8");
|
||||
// Black rook directly attacking the white king — classic "check"
|
||||
// in FIDE; under capture-all, no check applies.
|
||||
placePiece(engine, "rook", "black", "e2");
|
||||
engine.setActivePresets([PRESET]);
|
||||
|
||||
// With the preset's contributed empty royal set, the engine
|
||||
// treats every isInCheck(color) as false.
|
||||
expect(isInCheck(engine.session, "white", [])).toBe(false);
|
||||
// And checkGameResult reflects that — neither side has 0 pieces
|
||||
// yet, so we stay "ongoing".
|
||||
expect(engine.checkGameResult()).toBe("ongoing");
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (c) King legally moves into an attacked square
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — self-check filter opt-out", () => {
|
||||
it("(c) king legally steps onto a square attacked by an enemy rook", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "a8");
|
||||
// Black rook on d8 covers the d-file; white king steps onto d1
|
||||
// → under FIDE that move is illegal (king moves into check).
|
||||
// Under capture-all, no royals → filter is a no-op → move legal.
|
||||
placePiece(engine, "rook", "black", "d8");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "white");
|
||||
engine.setActivePresets([PRESET]);
|
||||
|
||||
const move = engine.findMove(
|
||||
algebraicToSquare("e1"),
|
||||
algebraicToSquare("d1"),
|
||||
);
|
||||
expect(move).not.toBeNull();
|
||||
expect(() => engine.applyMove(move!)).not.toThrow();
|
||||
// King made it to d1.
|
||||
expect(pieceAt(engine, "d1")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (d) Captures not compulsory — both captures and non-captures are
|
||||
// legal side-by-side
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — captures are optional (inverse of suicide-chess)", () => {
|
||||
it("(d) position with 1 capture + 1 non-capture → both remain legal", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "e8");
|
||||
// White rook on a1 can capture black pawn on a5, but can ALSO
|
||||
// shuffle to b1. Both must be legal.
|
||||
placePiece(engine, "rook", "white", "a1");
|
||||
placePiece(engine, "pawn", "black", "a5");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "white");
|
||||
engine.setActivePresets([PRESET]);
|
||||
|
||||
const allMoves = engine.getAllLegalMoves();
|
||||
const rookMoves = allMoves.filter(
|
||||
(m) => m.from === algebraicToSquare("a1"),
|
||||
);
|
||||
// Capture move present:
|
||||
const rxa5 = rookMoves.find((m) => m.to === algebraicToSquare("a5"));
|
||||
expect(rxa5).toBeDefined();
|
||||
expect(rxa5?.isCapture).toBe(true);
|
||||
// Non-capture move present (rook → b1 is a simple shuffle):
|
||||
const rb1 = rookMoves.find((m) => m.to === algebraicToSquare("b1"));
|
||||
expect(rb1).toBeDefined();
|
||||
expect(rb1?.isCapture).not.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (e) White captures last black piece → "white-wins"
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — white victory", () => {
|
||||
it("(e) white captures the last black piece and wins", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
// White has king + rook. Black has ONLY a king (no other pieces).
|
||||
// White's capture of the black king drops black's count to 0.
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "rook", "white", "a8");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "white");
|
||||
engine.setActivePresets([PRESET]);
|
||||
|
||||
const move = engine.findMove(
|
||||
algebraicToSquare("a8"),
|
||||
algebraicToSquare("h8"),
|
||||
);
|
||||
expect(move).not.toBeNull();
|
||||
expect(move?.isCapture).toBe(true);
|
||||
const result = engine.applyMove(move!);
|
||||
expect(result).toBe("white-wins");
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (f) Black captures last white piece → "black-wins"
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — black victory", () => {
|
||||
it("(f) black captures the last white piece and wins", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
// Black has king + rook, white has ONLY a king. Black plays.
|
||||
placePiece(engine, "king", "black", "e8");
|
||||
placePiece(engine, "rook", "black", "a1");
|
||||
placePiece(engine, "king", "white", "h1");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "black");
|
||||
engine.setActivePresets([PRESET]);
|
||||
|
||||
const move = engine.findMove(
|
||||
algebraicToSquare("a1"),
|
||||
algebraicToSquare("h1"),
|
||||
);
|
||||
expect(move).not.toBeNull();
|
||||
expect(move?.isCapture).toBe(true);
|
||||
expect(engine.applyMove(move!)).toBe("black-wins");
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (g) Fresh engine: 16 vs 16 → "ongoing"
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — fresh engine", () => {
|
||||
it("(g) default classic layout = 16 pieces per side → ongoing", () => {
|
||||
const engine = new ChessEngine();
|
||||
engine.setActivePresets([PRESET]);
|
||||
expect(engine.checkGameResult()).toBe("ongoing");
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (h) Composition with piece-hp: non-lethal damage ≠ count decrement
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — composition with piece-hp", () => {
|
||||
it("(h) non-lethal capture does NOT remove the target; lethal does", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "e8");
|
||||
// White rook on a1 vs a lone black pawn on a5. Under
|
||||
// capture-all alone, one capture ends the game (black count hits
|
||||
// 0). Under capture-all + piece-hp, the pawn has 2 HP — the
|
||||
// first "capture" deals 1 damage and the pawn survives; white
|
||||
// must hit again to remove it.
|
||||
placePiece(engine, "rook", "white", "a1");
|
||||
placePiece(engine, "pawn", "black", "a5");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "white");
|
||||
// piece-hp first so it seeds Hp=2 on both pieces at activation;
|
||||
// capture-all's onCheckGameResult runs second but that order is
|
||||
// irrelevant here — only the final count matters.
|
||||
engine.setActivePresets([
|
||||
{ id: "piece-hp", scope: "both", turnsRemaining: null },
|
||||
PRESET,
|
||||
]);
|
||||
|
||||
const blackPawnId = pieceAt(engine, "a5")!;
|
||||
expect(engine.session.get(blackPawnId, "Hp")).toBe(2);
|
||||
|
||||
// First rook-attack on the pawn. In piece-hp semantics the
|
||||
// attacker does NOT advance on a non-lethal hit; the pawn
|
||||
// absorbs 1 damage and survives.
|
||||
const rxa5_first = engine.findMove(
|
||||
algebraicToSquare("a1"),
|
||||
algebraicToSquare("a5"),
|
||||
)!;
|
||||
const r1 = engine.applyMove(rxa5_first);
|
||||
// The game must NOT have ended: the pawn is still alive, black
|
||||
// still has 1 piece (pawn) + 1 king = 2 pieces on the board.
|
||||
expect(r1).toBe("ongoing");
|
||||
expect(exists(engine, blackPawnId)).toBe(true);
|
||||
expect(engine.session.get(blackPawnId, "Hp")).toBe(1);
|
||||
|
||||
// Now it's black's turn — shuffle a non-interacting move to
|
||||
// return the turn to white. The black king on e8 has legal
|
||||
// moves; move to e7.
|
||||
const blackShuffle = engine.findMove(
|
||||
algebraicToSquare("e8"),
|
||||
algebraicToSquare("e7"),
|
||||
)!;
|
||||
engine.applyMove(blackShuffle);
|
||||
|
||||
// White attacks the pawn again — this time it should die and
|
||||
// black's piece count (after king) drops to 1. Game remains
|
||||
// ongoing because the black king still has a Position fact.
|
||||
const rxa5_second = engine.findMove(
|
||||
algebraicToSquare("a1"),
|
||||
algebraicToSquare("a5"),
|
||||
)!;
|
||||
const r2 = engine.applyMove(rxa5_second);
|
||||
expect(exists(engine, blackPawnId)).toBe(false);
|
||||
// Black king still alive → game continues.
|
||||
expect(r2).toBe("ongoing");
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (i) Incompatibility: capture-all + suicide-chess throws
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("capture-all — incompatibility", () => {
|
||||
it("(i) activating alongside capture-to-win throws", () => {
|
||||
// suicide-chess isn't in the registry yet (lands in Phase D.1 / sibling
|
||||
// task); use a currently-registered preset that capture-all ALSO
|
||||
// declares incompatible to exercise the same code path. capture-to-win
|
||||
// is the canonical "other terminal-decider" we conflict with.
|
||||
const engine = new ChessEngine();
|
||||
expect(() =>
|
||||
engine.setActivePresets([
|
||||
PRESET,
|
||||
{ id: "capture-to-win", scope: "both", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow(/incompatible/i);
|
||||
});
|
||||
|
||||
it("(i2) if suicide-chess is registered, activating with it throws", () => {
|
||||
const suicide = PRESET_REGISTRY.get("suicide-chess");
|
||||
// Only run this assertion when suicide-chess has been registered
|
||||
// (it may land in a sibling task). When absent, we've already
|
||||
// validated the incompatibility code path above.
|
||||
if (suicide === undefined) return;
|
||||
const engine = new ChessEngine();
|
||||
expect(() =>
|
||||
engine.setActivePresets([
|
||||
PRESET,
|
||||
{ id: "suicide-chess", scope: "both", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow(/incompatible/i);
|
||||
});
|
||||
|
||||
it("(i3) activating alongside last-piece-standing throws", () => {
|
||||
const engine = new ChessEngine();
|
||||
expect(() =>
|
||||
engine.setActivePresets([
|
||||
PRESET,
|
||||
{ id: "last-piece-standing", scope: "both", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow(/incompatible/i);
|
||||
});
|
||||
});
|
||||
167
packages/chess/src/presets/capture-all.ts
Normal file
167
packages/chess/src/presets/capture-all.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* Preset: `capture-all` (Phase D.2 of the rule-variants epic).
|
||||
*
|
||||
* Capture every enemy piece to win. The inverse objective of
|
||||
* `suicide-chess`:
|
||||
*
|
||||
* - suicide-chess: lose all your pieces FIRST → YOU win (compulsory
|
||||
* capture forces the game forward).
|
||||
* - capture-all: drive the OPPONENT to zero pieces → YOU win. No
|
||||
* compulsory capture; each side plays normal-looking chess, the
|
||||
* first to wipe the board of enemies wins.
|
||||
*
|
||||
* Kings are NOT royal. Checkmate is undefined in this variant — the
|
||||
* win condition is a piece-count floor, not king-attack. The self-
|
||||
* check filter is also disabled so a king can legally wander into
|
||||
* (and sit on) attacked squares; it's just another piece.
|
||||
*
|
||||
* Wiring (three hooks, no new engine surface)
|
||||
* ───────────────────────────────────────────
|
||||
*
|
||||
* - `getRoyalPieces({ color })` → `[]` — empty set per color. The
|
||||
* engine treats an empty royal set as "no check detection
|
||||
* applies", so `isInCheck` / `isCheckmate` / `isStalemate` all
|
||||
* return false unconditionally for either side.
|
||||
*
|
||||
* - `shouldFilterSelfCheck({ color })` → `false` — opt out of the
|
||||
* engine's default self-check filter. Without this, the engine
|
||||
* would still screen moves that leave an "empty-royal-set" color
|
||||
* in check (the filter is a no-op on empty royals, but we skip
|
||||
* it on principle and to keep the behaviour explicit for future
|
||||
* reviewers).
|
||||
*
|
||||
* - `onCheckGameResult({ engine })` — scan `Position` facts to
|
||||
* count how many live pieces each color has on the board:
|
||||
* - black piece count === 0 → `"white-wins"`
|
||||
* - white piece count === 0 → `"black-wins"`
|
||||
* - both === 0 (degenerate simultaneous wipeout) → `"draw-insufficient"`
|
||||
* - otherwise → `"ongoing"` (suppresses the default
|
||||
* checkmate/stalemate polls, which would misfire against the
|
||||
* empty-royal-set predicate — e.g. default `isCheckmate` on a
|
||||
* color with no royals returns false but default `isStalemate`
|
||||
* can still flag a no-legal-moves position as stalemate, which
|
||||
* isn't meaningful here).
|
||||
*
|
||||
* No `filterLegalMoves` — captures are OPTIONAL, unlike suicide-chess.
|
||||
* A player who prefers to develop rather than capture is free to do
|
||||
* so; this is the chief rule-text difference between the two
|
||||
* presets. Implementing compulsory capture here would collapse
|
||||
* capture-all into suicide-chess with an inverted win condition.
|
||||
*
|
||||
* State: none. Everything derives from live `Position` facts each
|
||||
* time `onCheckGameResult` is polled — no preset-state bag needed.
|
||||
*
|
||||
* Composition notes
|
||||
* ─────────────────
|
||||
*
|
||||
* - `piece-hp`: non-lethal captures don't decrement the count. Each
|
||||
* hit deals 1 damage; only when a piece hits 0 HP does it get
|
||||
* retracted, removing its Position and dropping the count. So
|
||||
* a piece-hp-composed capture-all game is "deal enough damage to
|
||||
* wipe every enemy piece" — each HP-kill progresses the victory
|
||||
* counter.
|
||||
*
|
||||
* - `double-move` / `monster-rules`: gets you to zero faster.
|
||||
* Monster-rules in particular is a NATURAL fit (white plays 2
|
||||
* half-moves per turn + starts with only 5 pieces vs 16 — except
|
||||
* `monster-rules` declares capture-all incompatible, so this
|
||||
* pairing is explicitly walled off for the moment to avoid dual
|
||||
* "when does the game end" semantics).
|
||||
*
|
||||
* - `piece-hp` + `explosive-rook`: AoE blasts can kill the last
|
||||
* enemy piece and win the game in one swing. Works out of the
|
||||
* box because every death reduces `Position` count.
|
||||
*
|
||||
* Incompatibility
|
||||
* ───────────────
|
||||
*
|
||||
* - `suicide-chess` — inverted objective + different capture-
|
||||
* compulsion semantics; stacking both makes the winner
|
||||
* ambiguous.
|
||||
* - `capture-to-win`, `last-piece-standing`, `first-promotion-wins`,
|
||||
* `extinction-chess` — all override `onCheckGameResult` with
|
||||
* their own victory condition. Multiple terminal-returners race
|
||||
* by registration order, which is fragile; declare incompatible
|
||||
* so `setActivePresets` rejects the combo up-front.
|
||||
* - `knightmate-rules`, `coregal`, `dual-king`, `weak-dual-king` —
|
||||
* all contribute a non-empty royal set. Union with our empty set
|
||||
* produces "some things are royal", contradicting the variant.
|
||||
* - `monster-rules` — monster declares `capture-all` incompatible
|
||||
* already (see `./monster-rules.ts`); mirror it here for
|
||||
* symmetry.
|
||||
*/
|
||||
import { PRESET_REGISTRY } from "./registry.js";
|
||||
import type { GameResult } from "../engine.js";
|
||||
|
||||
PRESET_REGISTRY.register({
|
||||
id: "capture-all",
|
||||
name: "Capture All",
|
||||
description: "Capture every enemy piece to win. Kings are not royal.",
|
||||
incompatibleWith: [
|
||||
"suicide-chess",
|
||||
"capture-to-win",
|
||||
"last-piece-standing",
|
||||
"extinction-chess",
|
||||
"first-promotion-wins",
|
||||
"knightmate-rules",
|
||||
"coregal",
|
||||
"dual-king",
|
||||
"weak-dual-king",
|
||||
"monster-rules",
|
||||
],
|
||||
requires: [],
|
||||
|
||||
/**
|
||||
* Empty royal set per color. Engine treats this as "no check
|
||||
* detection applies" in `isInCheck` / `isCheckmate` / `isStalemate`.
|
||||
*/
|
||||
getRoyalPieces(): readonly never[] {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Opt out of the self-check filter. With no royals, the filter is
|
||||
* a no-op anyway, but the explicit `false` documents the intent
|
||||
* and guards against a future engine change that would reintroduce
|
||||
* the filter for empty-royal colors.
|
||||
*/
|
||||
shouldFilterSelfCheck(): boolean {
|
||||
return false;
|
||||
},
|
||||
|
||||
onCheckGameResult({ engine }): GameResult | undefined {
|
||||
// Count live pieces by color. "Live" = has a Position fact; the
|
||||
// capture/damage path retracts Position before PieceType, so
|
||||
// Position is the canonical "still on the board" gate.
|
||||
const facts = engine.session.allFacts();
|
||||
const colorById = new Map<number, string>();
|
||||
for (const f of facts) {
|
||||
if (f.attr === "Color") colorById.set(f.id as number, f.value as string);
|
||||
}
|
||||
|
||||
let whiteCount = 0;
|
||||
let blackCount = 0;
|
||||
for (const f of facts) {
|
||||
if (f.attr !== "Position") continue;
|
||||
if ((f.id as number) <= 0) continue;
|
||||
const color = colorById.get(f.id as number);
|
||||
if (color === "white") whiteCount++;
|
||||
else if (color === "black") blackCount++;
|
||||
}
|
||||
|
||||
if (whiteCount === 0 && blackCount === 0) {
|
||||
// Simultaneous wipeout (AoE preset stack, etc.). Call it a
|
||||
// draw — neither side achieved the win condition strictly.
|
||||
return "draw-insufficient";
|
||||
}
|
||||
if (blackCount === 0) return "white-wins";
|
||||
if (whiteCount === 0) return "black-wins";
|
||||
|
||||
// Neither side wiped. Return "ongoing" to suppress default
|
||||
// checkmate/stalemate/draw polls — with empty royals those
|
||||
// default predicates are ill-defined (isCheckmate trivially
|
||||
// false, but isStalemate could fire if the mover has no legal
|
||||
// moves, and we don't want that to end the game here).
|
||||
return "ongoing";
|
||||
},
|
||||
});
|
||||
|
|
@ -31,8 +31,10 @@ import "./knight-immunity.js";
|
|||
import "./knightmate-rules.js";
|
||||
import "./coregal.js";
|
||||
import "./dual-king.js";
|
||||
import "./weak-dual-king.js";
|
||||
import "./monster-rules.js";
|
||||
import "./poisoned-squares.js";
|
||||
import "./first-promotion-wins.js";
|
||||
import "./capture-all.js";
|
||||
|
||||
export { PRESET_REGISTRY, type PresetDef } from "./registry.js";
|
||||
|
|
|
|||
|
|
@ -44,9 +44,11 @@ describe("Preset registry — all registered", () => {
|
|||
"knightmate-rules",
|
||||
"coregal",
|
||||
"dual-king",
|
||||
"weak-dual-king",
|
||||
"monster-rules",
|
||||
"poisoned-squares",
|
||||
"first-promotion-wins",
|
||||
"capture-all",
|
||||
];
|
||||
|
||||
it("registry size matches the expected ID list", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue