feat(presets): berolina-pawns-2 (berolina with sideways captures)
This commit is contained in:
parent
b873095c17
commit
ee08e20871
4 changed files with 540 additions and 0 deletions
341
packages/chess/src/presets/berolina-pawns-2.test.ts
Normal file
341
packages/chess/src/presets/berolina-pawns-2.test.ts
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
/**
|
||||
* Tests for `berolina-pawns-2` (Phase E.2, rule-variants epic).
|
||||
*
|
||||
* The preset is identical to `berolina-pawns` but adds sideways
|
||||
* captures (same rank, adjacent file). These tests focus on the
|
||||
* DIFF — the sideways-capture behaviour — plus a couple of sanity
|
||||
* tests that the baseline berolina rules still work through this
|
||||
* preset (push + orthogonal capture).
|
||||
*
|
||||
* Coverage map:
|
||||
* (a) Sideways-left capture: white pawn e4, enemy d4 → capture.
|
||||
* (b) Sideways-right capture: white pawn e4, enemy f4 → capture.
|
||||
* (c) No sideways capture on empty squares.
|
||||
* (d) No sideways capture on friendly piece.
|
||||
* (e) Edge file (a-file): only right-sideways available.
|
||||
* (f) Mirror for black: black pawn e5, enemies d5/f5 → both
|
||||
* sideways captures available.
|
||||
* (g) Scope `white` flip: black pawns don't get sideways captures;
|
||||
* white pawns do.
|
||||
* (h) Incompatibility: berolina-pawns-2 + berolina-pawns throws.
|
||||
* (i) Compose: diagonal pushes + orthogonal capture + sideways
|
||||
* captures all available simultaneously.
|
||||
* (j) Sanity: baseline berolina push still works under this preset.
|
||||
* (k) Sanity: baseline orthogonal capture still works under this
|
||||
* preset.
|
||||
*/
|
||||
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 type { LegalMove } from "../rules/types.js";
|
||||
import { clearBoard, placePiece } from "./test-utils.js";
|
||||
|
||||
const BEROLINA2 = {
|
||||
id: "berolina-pawns-2",
|
||||
scope: "both" as const,
|
||||
turnsRemaining: null,
|
||||
};
|
||||
|
||||
const BEROLINA2_WHITE = {
|
||||
id: "berolina-pawns-2",
|
||||
scope: "white" as const,
|
||||
turnsRemaining: null,
|
||||
};
|
||||
|
||||
function movesFor(engine: ChessEngine, pieceId: number): LegalMove[] {
|
||||
return engine.getAllLegalMoves().filter((m) => m.pieceId === pieceId);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (a) Sideways-left capture
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — sideways-left capture", () => {
|
||||
it("(a) white pawn e4, enemy d4 → capture to d4 with isCapture=true", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "e4");
|
||||
placePiece(engine, "pawn", "black", "d4");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const d4 = moves.find((m) => m.to === algebraicToSquare("d4"));
|
||||
expect(d4).toBeDefined();
|
||||
expect(d4!.isCapture).toBe(true);
|
||||
// Sideways move is NOT a promotion.
|
||||
expect(d4!.promoteTo).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (b) Sideways-right capture
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — sideways-right capture", () => {
|
||||
it("(b) white pawn e4, enemy f4 → capture to f4 with isCapture=true", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "e4");
|
||||
placePiece(engine, "pawn", "black", "f4");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const f4 = moves.find((m) => m.to === algebraicToSquare("f4"));
|
||||
expect(f4).toBeDefined();
|
||||
expect(f4!.isCapture).toBe(true);
|
||||
expect(f4!.promoteTo).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (c) No sideways on empty squares
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — no sideways on empty", () => {
|
||||
it("(c) white pawn e4, empty d4/f4 → no sideways moves", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "e4");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const targets = new Set(moves.map((m) => m.to));
|
||||
expect(targets.has(algebraicToSquare("d4"))).toBe(false);
|
||||
expect(targets.has(algebraicToSquare("f4"))).toBe(false);
|
||||
// Just the two diagonal pushes.
|
||||
expect(targets.has(algebraicToSquare("d5"))).toBe(true);
|
||||
expect(targets.has(algebraicToSquare("f5"))).toBe(true);
|
||||
expect(moves).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (d) No sideways on friendly
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — no sideways on friendly", () => {
|
||||
it("(d) white pawn e4, friendly d4 → no move to d4", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "e4");
|
||||
placePiece(engine, "knight", "white", "d4");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const targets = new Set(moves.map((m) => m.to));
|
||||
expect(targets.has(algebraicToSquare("d4"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (e) Edge file (a-file)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — edge file pawn", () => {
|
||||
it("(e) white pawn a4 with enemy b4 → right-sideways captures; no left (off-board)", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "e8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "a4");
|
||||
placePiece(engine, "pawn", "black", "b4");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const b4 = moves.find((m) => m.to === algebraicToSquare("b4"));
|
||||
expect(b4).toBeDefined();
|
||||
expect(b4!.isCapture).toBe(true);
|
||||
// Right diagonal push b5 also present (empty).
|
||||
const targets = new Set(moves.map((m) => m.to));
|
||||
expect(targets.has(algebraicToSquare("b5"))).toBe(true);
|
||||
// No phantom wrap to h4.
|
||||
expect(targets.has(algebraicToSquare("h4"))).toBe(false);
|
||||
// Pawn on a-file: only right-diag push + sideways-right = 2
|
||||
// moves (no left diagonal).
|
||||
expect(moves).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (f) Mirror for black
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — mirror for black", () => {
|
||||
it("(f) black pawn e5 with enemies d5/f5 → both sideways captures", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const pawn = placePiece(engine, "pawn", "black", "e5");
|
||||
placePiece(engine, "pawn", "white", "d5");
|
||||
placePiece(engine, "pawn", "white", "f5");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "black");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const d5 = moves.find((m) => m.to === algebraicToSquare("d5"));
|
||||
const f5 = moves.find((m) => m.to === algebraicToSquare("f5"));
|
||||
expect(d5).toBeDefined();
|
||||
expect(d5!.isCapture).toBe(true);
|
||||
expect(f5).toBeDefined();
|
||||
expect(f5!.isCapture).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (g) Scope=white: black pawns don't get sideways; white pawns do
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — scope=white", () => {
|
||||
it("(g) black pawn e5 with enemies d5/f5 under scope=white → no sideways (default FIDE)", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const blackPawn = placePiece(engine, "pawn", "black", "e5");
|
||||
placePiece(engine, "pawn", "white", "d5");
|
||||
placePiece(engine, "pawn", "white", "f5");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "black");
|
||||
engine.setActivePresets([BEROLINA2_WHITE]);
|
||||
|
||||
const moves = movesFor(engine, blackPawn as unknown as number);
|
||||
const targets = new Set(moves.map((m) => m.to));
|
||||
// FIDE black pawn from e5: captures on d4/f4 (diagonal) and push
|
||||
// to e4 (if empty). NOT d5/f5 (those are same-rank).
|
||||
expect(targets.has(algebraicToSquare("d5"))).toBe(false);
|
||||
expect(targets.has(algebraicToSquare("f5"))).toBe(false);
|
||||
});
|
||||
|
||||
it("(g') white pawn e4 with enemy d4 under scope=white → sideways capture available", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const whitePawn = placePiece(engine, "pawn", "white", "e4");
|
||||
placePiece(engine, "pawn", "black", "d4");
|
||||
engine.setActivePresets([BEROLINA2_WHITE]);
|
||||
|
||||
const moves = movesFor(engine, whitePawn as unknown as number);
|
||||
const d4 = moves.find((m) => m.to === algebraicToSquare("d4"));
|
||||
expect(d4).toBeDefined();
|
||||
expect(d4!.isCapture).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (h) Incompatibility with berolina-pawns
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — incompatibility", () => {
|
||||
it("(h) berolina-pawns-2 + berolina-pawns throws at activation", () => {
|
||||
const engine = new ChessEngine();
|
||||
expect(() =>
|
||||
engine.setActivePresets([
|
||||
BEROLINA2,
|
||||
{ id: "berolina-pawns", scope: "both", turnsRemaining: null },
|
||||
]),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (i) Compose: all three move kinds simultaneously
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — all move kinds compose", () => {
|
||||
it("(i) white pawn e4: diagonal pushes d5/f5 + sideways cap d4 + orthogonal cap e5", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "e4");
|
||||
// Sideways target d4 (enemy).
|
||||
placePiece(engine, "pawn", "black", "d4");
|
||||
// Orthogonal capture target e5 (enemy).
|
||||
placePiece(engine, "pawn", "black", "e5");
|
||||
// Diagonal push squares d5, f5 must remain empty.
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const targets = new Map(moves.map((m) => [m.to, m] as const));
|
||||
|
||||
// Diagonal push (non-capture).
|
||||
const d5 = targets.get(algebraicToSquare("d5"));
|
||||
expect(d5).toBeDefined();
|
||||
expect(d5!.isCapture).toBe(false);
|
||||
const f5 = targets.get(algebraicToSquare("f5"));
|
||||
expect(f5).toBeDefined();
|
||||
expect(f5!.isCapture).toBe(false);
|
||||
|
||||
// Orthogonal capture.
|
||||
const e5 = targets.get(algebraicToSquare("e5"));
|
||||
expect(e5).toBeDefined();
|
||||
expect(e5!.isCapture).toBe(true);
|
||||
|
||||
// Sideways capture.
|
||||
const d4 = targets.get(algebraicToSquare("d4"));
|
||||
expect(d4).toBeDefined();
|
||||
expect(d4!.isCapture).toBe(true);
|
||||
|
||||
// No sideways-right (f4 empty) and no extra noise.
|
||||
expect(targets.has(algebraicToSquare("f4"))).toBe(false);
|
||||
expect(moves).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (j) Sanity: baseline berolina push still works
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — baseline diagonal push sanity", () => {
|
||||
it("(j) white pawn e2 empty board → 4 diagonal pushes (no sideways from empty adjacent)", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "e8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "e2");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const targets = new Set(moves.map((m) => m.to));
|
||||
expect(targets.has(algebraicToSquare("d3"))).toBe(true);
|
||||
expect(targets.has(algebraicToSquare("f3"))).toBe(true);
|
||||
expect(targets.has(algebraicToSquare("c4"))).toBe(true);
|
||||
expect(targets.has(algebraicToSquare("g4"))).toBe(true);
|
||||
// No sideways (d2/f2 empty).
|
||||
expect(targets.has(algebraicToSquare("d2"))).toBe(false);
|
||||
expect(targets.has(algebraicToSquare("f2"))).toBe(false);
|
||||
expect(moves).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// (k) Sanity: baseline orthogonal capture still works
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("berolina-pawns-2 — baseline orthogonal capture sanity", () => {
|
||||
it("(k) white pawn e4, enemy e5 → e5 is still a capture", () => {
|
||||
const engine = new ChessEngine();
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
const pawn = placePiece(engine, "pawn", "white", "e4");
|
||||
placePiece(engine, "pawn", "black", "e5");
|
||||
engine.setActivePresets([BEROLINA2]);
|
||||
|
||||
const moves = movesFor(engine, pawn as unknown as number);
|
||||
const e5 = moves.find((m) => m.to === algebraicToSquare("e5"));
|
||||
expect(e5).toBeDefined();
|
||||
expect(e5!.isCapture).toBe(true);
|
||||
});
|
||||
});
|
||||
197
packages/chess/src/presets/berolina-pawns-2.ts
Normal file
197
packages/chess/src/presets/berolina-pawns-2.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/**
|
||||
* Preset: Berolina Pawns (Extended) — Phase E.2, rule-variants epic.
|
||||
*
|
||||
* Same as `berolina-pawns` but adds SIDEWAYS CAPTURES. Use with
|
||||
* `scope: "white"` / `"black"` / `"both"` to pick which side(s) use
|
||||
* the rule.
|
||||
*
|
||||
* Rules (diff from berolina-pawns):
|
||||
* - Pushes: forward diagonal (same as berolina-pawns).
|
||||
* - Captures: forward orthogonal (same as berolina-pawns) OR
|
||||
* SIDEWAYS — same rank, adjacent file (file±1). Sideways capture
|
||||
* requires an enemy piece on the adjacent square; empty or
|
||||
* friendly → no move.
|
||||
* - Sideways moves are NEVER promotions: the destination rank
|
||||
* equals the source rank, so it can't be the promotion rank for
|
||||
* a pawn that wasn't already there (pawns on the promotion rank
|
||||
* should have already promoted).
|
||||
* - No en-passant (same as berolina-pawns — deferred).
|
||||
*
|
||||
* Scope-flip semantics
|
||||
* ────────────────────
|
||||
* Read at hook time from `engine.activePresets.list()` (idiomatic
|
||||
* pattern — see `monster-rules.ts` / `berolina-pawns.ts`). The knob
|
||||
* is interpreted as "which color's pawns play Berolina-Extended":
|
||||
* - `scope: "both"` → override fires for every pawn.
|
||||
* - `scope: "white"` → white pawns use this rule; black pawns keep
|
||||
* default FIDE pawn rules.
|
||||
* - `scope: "black"` → mirror: black pawns use this rule.
|
||||
*
|
||||
* When the scope doesn't match a particular pawn's color, the hook
|
||||
* returns `undefined`, letting the engine fall through to its
|
||||
* default pawn generator (with en-passant / promotion synthesis
|
||||
* reinstated for that pawn).
|
||||
*
|
||||
* Incompatibilities
|
||||
* ─────────────────
|
||||
* - `berolina-pawns` — sibling pawn-override; only one may claim
|
||||
* pawn motion under a given scope.
|
||||
* - `pawn-diagonal-no-capture` — adds diagonal non-capture pushes
|
||||
* to FIDE pawns, which already overlap with this preset's
|
||||
* pushes.
|
||||
* - `pawns-move-backward` — adds moves via `getExtraMoves`, which
|
||||
* runs on top of the overridden set; semantics under-specified.
|
||||
* - `double-pawn-sprint` — forward-push extension; meaningless
|
||||
* overlay on pawns that never push orthogonally.
|
||||
*/
|
||||
import type { EntityId } from "@paratype/rete";
|
||||
import type { ChessEngine } from "../engine.js";
|
||||
import type { LegalMove } from "../rules/types.js";
|
||||
import type { PieceColor, PieceType, Square } from "../schema.js";
|
||||
import { PROMOTION_PIECES } from "../schema.js";
|
||||
import { fileOf, rankOf } from "../coord.js";
|
||||
import {
|
||||
getPiecePosition,
|
||||
getPieceColor,
|
||||
isPieceAt,
|
||||
isEnemyAt,
|
||||
} from "../rules/board-queries.js";
|
||||
import { PRESET_REGISTRY } from "./registry.js";
|
||||
|
||||
const PRESET_ID = "berolina-pawns-2";
|
||||
|
||||
/** Promotion rank for each color. */
|
||||
const PROMOTION_RANK: Record<PieceColor, number> = {
|
||||
white: 7,
|
||||
black: 0,
|
||||
};
|
||||
|
||||
/** Home rank (where pawns start) for each color. */
|
||||
const HOME_RANK: Record<PieceColor, number> = {
|
||||
white: 1,
|
||||
black: 6,
|
||||
};
|
||||
|
||||
/**
|
||||
* Append a move to `out`, expanding into promotion variants when the
|
||||
* destination is on the mover's promotion rank.
|
||||
*/
|
||||
function emitMove(
|
||||
out: LegalMove[],
|
||||
pieceId: EntityId,
|
||||
from: Square,
|
||||
to: Square,
|
||||
isCapture: boolean,
|
||||
color: PieceColor,
|
||||
): void {
|
||||
if (rankOf(to) === PROMOTION_RANK[color]) {
|
||||
for (const promoteTo of PROMOTION_PIECES) {
|
||||
out.push({ pieceId, from, to, isCapture, promoteTo: promoteTo as PieceType });
|
||||
}
|
||||
} else {
|
||||
out.push({ pieceId, from, to, isCapture });
|
||||
}
|
||||
}
|
||||
|
||||
PRESET_REGISTRY.register({
|
||||
id: PRESET_ID,
|
||||
name: "Berolina Pawns (Extended)",
|
||||
description:
|
||||
"Berolina pawns with sideways captures. Pawns push diagonally, capture orthogonally or sideways.",
|
||||
incompatibleWith: [
|
||||
"berolina-pawns",
|
||||
"pawn-diagonal-no-capture",
|
||||
"pawns-move-backward",
|
||||
"double-pawn-sprint",
|
||||
],
|
||||
requires: [],
|
||||
|
||||
overridePieceMoves(engine: ChessEngine, pieceId: EntityId): readonly LegalMove[] | undefined {
|
||||
// Only redefine pawn motion.
|
||||
const type = engine.session.get(pieceId, "PieceType");
|
||||
if (type !== "pawn") return undefined;
|
||||
|
||||
// Recover THIS preset's live scope.
|
||||
let scope: "white" | "black" | "both" = "both";
|
||||
let found = false;
|
||||
for (const entry of engine.activePresets.list()) {
|
||||
if (entry.id === PRESET_ID) {
|
||||
scope = entry.scope;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return undefined;
|
||||
|
||||
const color = getPieceColor(engine.session, pieceId);
|
||||
const from = getPiecePosition(engine.session, pieceId);
|
||||
if (color === null || from === null) return undefined;
|
||||
|
||||
// Scope check.
|
||||
if (scope !== "both" && scope !== color) return undefined;
|
||||
|
||||
const out: LegalMove[] = [];
|
||||
const file = fileOf(from);
|
||||
const rank = rankOf(from);
|
||||
const dir = color === "white" ? 1 : -1;
|
||||
const forwardDelta = dir * 8;
|
||||
const leftDiagDelta = dir * 8 - 1;
|
||||
const rightDiagDelta = dir * 8 + 1;
|
||||
|
||||
// ── Diagonal pushes (non-capture) ─────────────────────────────
|
||||
if (file > 0) {
|
||||
const leftOne = (from + leftDiagDelta) as Square;
|
||||
if (!isPieceAt(engine.session, leftOne)) {
|
||||
emitMove(out, pieceId, from, leftOne, false, color);
|
||||
if (rank === HOME_RANK[color] && file > 1) {
|
||||
const leftTwo = (from + 2 * leftDiagDelta) as Square;
|
||||
if (!isPieceAt(engine.session, leftTwo)) {
|
||||
emitMove(out, pieceId, from, leftTwo, false, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file < 7) {
|
||||
const rightOne = (from + rightDiagDelta) as Square;
|
||||
if (!isPieceAt(engine.session, rightOne)) {
|
||||
emitMove(out, pieceId, from, rightOne, false, color);
|
||||
if (rank === HOME_RANK[color] && file < 6) {
|
||||
const rightTwo = (from + 2 * rightDiagDelta) as Square;
|
||||
if (!isPieceAt(engine.session, rightTwo)) {
|
||||
emitMove(out, pieceId, from, rightTwo, false, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Orthogonal capture (forward, same file) ───────────────────
|
||||
const forwardRank = rank + dir;
|
||||
if (forwardRank >= 0 && forwardRank <= 7) {
|
||||
const forwardSq = (from + forwardDelta) as Square;
|
||||
if (isEnemyAt(engine.session, forwardSq, color)) {
|
||||
emitMove(out, pieceId, from, forwardSq, true, color);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sideways captures (same rank, adjacent file) ──────────────
|
||||
// These NEVER promote (same rank as source), so we emit direct
|
||||
// LegalMove records without going through emitMove's promotion
|
||||
// expansion. That said, a pawn is never ON its own promotion
|
||||
// rank (would have promoted), so emitMove would have been
|
||||
// correct too — we bypass it for clarity.
|
||||
if (file > 0) {
|
||||
const leftSq = (from - 1) as Square;
|
||||
if (isEnemyAt(engine.session, leftSq, color)) {
|
||||
out.push({ pieceId, from, to: leftSq, isCapture: true });
|
||||
}
|
||||
}
|
||||
if (file < 7) {
|
||||
const rightSq = (from + 1) as Square;
|
||||
if (isEnemyAt(engine.session, rightSq, color)) {
|
||||
out.push({ pieceId, from, to: rightSq, isCapture: true });
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
});
|
||||
|
|
@ -39,6 +39,7 @@ import "./capture-all.js";
|
|||
import "./suicide-chess.js";
|
||||
import "./extinction-chess.js";
|
||||
import "./berolina-pawns.js";
|
||||
import "./berolina-pawns-2.js";
|
||||
import "./bouncing-pieces.js";
|
||||
|
||||
export { PRESET_REGISTRY, type PresetDef } from "./registry.js";
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ describe("Preset registry — all registered", () => {
|
|||
"suicide-chess",
|
||||
"extinction-chess",
|
||||
"berolina-pawns",
|
||||
"berolina-pawns-2",
|
||||
"bouncing-pieces",
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue