feat(chess): allow pawns-move-backward and double-pawn-sprint simultaneously
Audited all 15 preset incompatibilities. Five of the six declared pairs are genuine mechanical conflicts (rook-warp vs wrap-board have contradictory board-topology semantics; piece-hp vs explosive-rook define capture as damage vs removal; capture-to-win vs last-piece-standing are competing win conditions). The pawns-move-backward vs double-pawn-sprint pair is not — both are pure additive getExtraMoves hooks over the base pawn rule; the move sets are disjoint (backward-1 vs forward-2); and no generated move contradicts any other. Dropped the declaration from both preset files. Moved the mutually-incompatible-pair tests to use rook-warp vs wrap-board, which is the canonical genuine-conflict pair and exercises the same compat machinery.
This commit is contained in:
parent
de059fe707
commit
08b5f794a7
5 changed files with 37 additions and 24 deletions
|
|
@ -61,32 +61,34 @@ describe("ActivePresetSet", () => {
|
|||
});
|
||||
|
||||
describe("replaceAll — loose compatibility", () => {
|
||||
// Uses rook-warp ↔ wrap-board as the canonical mutually-incompatible
|
||||
// pair — they define contradictory board-topology semantics and
|
||||
// genuinely cannot coexist. See RULES.md for the full matrix.
|
||||
it("blocks incompatible presets when scopes overlap (both+any)", () => {
|
||||
// pawns-move-backward is incompatibleWith double-pawn-sprint
|
||||
expect(() =>
|
||||
set.replaceAll([
|
||||
{ id: "pawns-move-backward", scope: "both", turnsRemaining: null },
|
||||
{ id: "double-pawn-sprint", scope: "white", turnsRemaining: null },
|
||||
{ id: "rook-warp", scope: "both", turnsRemaining: null },
|
||||
{ id: "wrap-board", scope: "white", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow(/incompatible/);
|
||||
});
|
||||
|
||||
it("allows incompatible presets when scopes are disjoint", () => {
|
||||
// White-only pawns-move-backward + black-only double-pawn-sprint —
|
||||
// per the loose rule these never overlap, so it's allowed.
|
||||
// White-only rook-warp + black-only wrap-board — per the loose
|
||||
// rule these never overlap, so the block is lifted.
|
||||
set.replaceAll([
|
||||
{ id: "pawns-move-backward", scope: "white", turnsRemaining: null },
|
||||
{ id: "double-pawn-sprint", scope: "black", turnsRemaining: null },
|
||||
{ id: "rook-warp", scope: "white", turnsRemaining: null },
|
||||
{ id: "wrap-board", scope: "black", turnsRemaining: null },
|
||||
]);
|
||||
expect(set.has("pawns-move-backward")).toBe(true);
|
||||
expect(set.has("double-pawn-sprint")).toBe(true);
|
||||
expect(set.has("rook-warp")).toBe(true);
|
||||
expect(set.has("wrap-board")).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks when both are the same explicit color", () => {
|
||||
expect(() =>
|
||||
set.replaceAll([
|
||||
{ id: "pawns-move-backward", scope: "white", turnsRemaining: null },
|
||||
{ id: "double-pawn-sprint", scope: "white", turnsRemaining: null },
|
||||
{ id: "rook-warp", scope: "white", turnsRemaining: null },
|
||||
{ id: "wrap-board", scope: "white", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow(/incompatible/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
* preset only adds the double advance when the pawn is OFF its home rank.
|
||||
*
|
||||
* Mode: override (conceptually removes the `HasMoved = false` guard).
|
||||
* Incompatible with `pawns-move-backward`.
|
||||
* Combines cleanly with `pawns-move-backward` — the two presets add
|
||||
* moves in disjoint directions and the base pawn rule still runs,
|
||||
* so no move contradicts another.
|
||||
*/
|
||||
import type { EntityId } from "@paratype/rete";
|
||||
import type { ChessEngine } from "../engine.js";
|
||||
|
|
@ -56,7 +58,7 @@ PRESET_REGISTRY.register({
|
|||
name: "Perpetual Sprint",
|
||||
description:
|
||||
"Pawns may advance 2 squares straight forward from ANY rank (not only the home rank).",
|
||||
incompatibleWith: ["pawns-move-backward"],
|
||||
incompatibleWith: [],
|
||||
requires: [],
|
||||
getExtraMoves: getDoubleSprintMove,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,12 +32,14 @@ describe("Preset registry (catalog)", () => {
|
|||
).toThrow(/not registered/);
|
||||
});
|
||||
|
||||
it("cannot activate two mutually-incompatible presets under overlapping scope", () => {
|
||||
it("mutually-incompatible presets throw under overlapping scope", () => {
|
||||
// rook-warp + wrap-board is the canonical hard-incompatible pair
|
||||
// (see RULES.md): they define conflicting board topology semantics.
|
||||
const set = new ActivePresetSet();
|
||||
expect(() =>
|
||||
set.replaceAll([
|
||||
{ id: "pawns-move-backward", scope: "both", turnsRemaining: null },
|
||||
{ id: "double-pawn-sprint", scope: "both", turnsRemaining: null },
|
||||
{ id: "rook-warp", scope: "both", turnsRemaining: null },
|
||||
{ id: "wrap-board", scope: "both", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow(/incompatible/);
|
||||
});
|
||||
|
|
@ -56,11 +58,11 @@ describe("Preset: pawns-move-backward", () => {
|
|||
return pos ? (pos.id as EntityId) : null;
|
||||
}
|
||||
|
||||
it("declares incompatibility with double-pawn-sprint", () => {
|
||||
expect(preset().incompatibleWith).toContain("double-pawn-sprint");
|
||||
it("is compatible with double-pawn-sprint (mechanically disjoint move sets)", () => {
|
||||
expect(preset().incompatibleWith).not.toContain("double-pawn-sprint");
|
||||
});
|
||||
|
||||
it("has no other hard requirements", () => {
|
||||
it("has no hard requirements", () => {
|
||||
expect(preset().requires).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
* Pawns may additionally move exactly one square straight backward to an
|
||||
* empty square. Backward moves may NOT capture and do NOT enable en passant.
|
||||
*
|
||||
* Mode: additive. Incompatible with `double-pawn-sprint`.
|
||||
* Mode: additive. Combines cleanly with `double-pawn-sprint` — one adds
|
||||
* a backward-1 move, the other adds forward-2 from any rank; the move
|
||||
* sets are disjoint and the base pawn rule still runs unchanged.
|
||||
*/
|
||||
import type { EntityId } from "@paratype/rete";
|
||||
import type { ChessEngine } from "../engine.js";
|
||||
|
|
@ -42,7 +44,7 @@ PRESET_REGISTRY.register({
|
|||
name: "Backward-Marching Pawns",
|
||||
description:
|
||||
"Pawns may also move 1 square straight backward to an empty square (no capture).",
|
||||
incompatibleWith: ["double-pawn-sprint"],
|
||||
incompatibleWith: [],
|
||||
requires: [],
|
||||
getExtraMoves: getPawnBackwardMove,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,11 +54,16 @@ describe("Preset registry — all 15 registered", () => {
|
|||
});
|
||||
|
||||
describe("Incompatibility declarations", () => {
|
||||
it("pawns-move-backward ↔ double-pawn-sprint are mutually incompatible", () => {
|
||||
it("pawns-move-backward ↔ double-pawn-sprint are now compatible", () => {
|
||||
// Historical note: these were originally incompatible on design
|
||||
// grounds (author felt oscillating pawns were ugly). Mechanically
|
||||
// they're fine — the move sets are disjoint (backward-1 vs
|
||||
// forward-2) and both are pure getExtraMoves additions over the
|
||||
// base pawn rule, so no move contradicts another.
|
||||
const a = PRESET_REGISTRY.getAll().find(p => p.id === "pawns-move-backward")!;
|
||||
const b = PRESET_REGISTRY.getAll().find(p => p.id === "double-pawn-sprint")!;
|
||||
expect(a.incompatibleWith).toContain("double-pawn-sprint");
|
||||
expect(b.incompatibleWith).toContain("pawns-move-backward");
|
||||
expect(a.incompatibleWith).not.toContain("double-pawn-sprint");
|
||||
expect(b.incompatibleWith).not.toContain("pawns-move-backward");
|
||||
});
|
||||
|
||||
it("rook-warp ↔ wrap-board are mutually incompatible", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue