feat(chess): add movement primitive rules (P2.8)
This commit is contained in:
parent
d4cee82b20
commit
eb58441752
2 changed files with 198 additions and 0 deletions
94
packages/chess/src/rules/primitives.test.ts
Normal file
94
packages/chess/src/rules/primitives.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
rookCandidates, bishopCandidates, queenCandidates,
|
||||||
|
knightCandidates, kingCandidates,
|
||||||
|
pawnSingleAdvance, pawnDoubleAdvance, pawnCaptureSqares,
|
||||||
|
} from "./primitives.js";
|
||||||
|
|
||||||
|
describe("rookCandidates", () => {
|
||||||
|
it("rook at a1 (0): right ray has 7 squares, others are bounded", () => {
|
||||||
|
const rays = rookCandidates(0);
|
||||||
|
expect(rays).toHaveLength(4);
|
||||||
|
expect(rays[0]).toHaveLength(7); // right: b1-h1
|
||||||
|
expect(rays[1]).toHaveLength(0); // left: nothing (a-file)
|
||||||
|
expect(rays[2]).toHaveLength(7); // up: a2-a8
|
||||||
|
expect(rays[3]).toHaveLength(0); // down: nothing (rank1)
|
||||||
|
});
|
||||||
|
it("rook at e4 (28): all 4 rays non-empty", () => {
|
||||||
|
const rays = rookCandidates(28);
|
||||||
|
expect(rays.every(r => r.length > 0)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bishopCandidates", () => {
|
||||||
|
it("bishop at a1 (0): only one diagonal (up-right)", () => {
|
||||||
|
const rays = bishopCandidates(0);
|
||||||
|
expect(rays[0]).toHaveLength(7); // up-right diagonal: b2-h8
|
||||||
|
expect(rays[1]).toHaveLength(0); // down-right: nothing
|
||||||
|
expect(rays[2]).toHaveLength(0); // up-left: nothing
|
||||||
|
expect(rays[3]).toHaveLength(0); // down-left: nothing
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("queenCandidates", () => {
|
||||||
|
it("queen at e4 (28): 8 rays total", () => {
|
||||||
|
const rays = queenCandidates(28);
|
||||||
|
expect(rays).toHaveLength(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("knightCandidates", () => {
|
||||||
|
it("knight at e4 (28) has 8 moves", () => {
|
||||||
|
expect(knightCandidates(28)).toHaveLength(8);
|
||||||
|
});
|
||||||
|
it("knight at a1 (0) has 2 moves", () => {
|
||||||
|
expect(knightCandidates(0)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("kingCandidates", () => {
|
||||||
|
it("king at e4 (28) has 8 moves", () => {
|
||||||
|
expect(kingCandidates(28)).toHaveLength(8);
|
||||||
|
});
|
||||||
|
it("king at a1 (0) has 3 moves", () => {
|
||||||
|
expect(kingCandidates(0)).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pawnSingleAdvance", () => {
|
||||||
|
it("white pawn at e2 (12) advances to e3 (20)", () => {
|
||||||
|
expect(pawnSingleAdvance(12, "white")).toBe(20);
|
||||||
|
});
|
||||||
|
it("black pawn at e7 (52) advances to e6 (44)", () => {
|
||||||
|
expect(pawnSingleAdvance(52, "black")).toBe(44);
|
||||||
|
});
|
||||||
|
it("white pawn at rank8 returns null", () => {
|
||||||
|
expect(pawnSingleAdvance(60, "white")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pawnDoubleAdvance", () => {
|
||||||
|
it("white pawn at e2 (12) can double-advance to e4 (28)", () => {
|
||||||
|
expect(pawnDoubleAdvance(12, "white")).toBe(28);
|
||||||
|
});
|
||||||
|
it("white pawn at e3 (20) cannot double-advance", () => {
|
||||||
|
expect(pawnDoubleAdvance(20, "white")).toBeNull();
|
||||||
|
});
|
||||||
|
it("black pawn at e7 (52) can double-advance to e5 (36)", () => {
|
||||||
|
expect(pawnDoubleAdvance(52, "black")).toBe(36);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pawnCaptureSquares", () => {
|
||||||
|
it("white pawn at e4 (28) can capture on d5 (35) and f5 (37)", () => {
|
||||||
|
const sq = pawnCaptureSqares(28, "white");
|
||||||
|
expect(sq).toContain(35); // d5
|
||||||
|
expect(sq).toContain(37); // f5
|
||||||
|
expect(sq).toHaveLength(2);
|
||||||
|
});
|
||||||
|
it("white pawn at a4 (24) can only capture right (b5=33)", () => {
|
||||||
|
const sq = pawnCaptureSqares(24, "white");
|
||||||
|
expect(sq).toHaveLength(1);
|
||||||
|
expect(sq[0]).toBe(33);
|
||||||
|
});
|
||||||
|
});
|
||||||
104
packages/chess/src/rules/primitives.ts
Normal file
104
packages/chess/src/rules/primitives.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
/**
|
||||||
|
* Movement primitive rules for @paratype/chess.
|
||||||
|
* Each primitive computes candidate target squares for a piece type.
|
||||||
|
* These are used by higher-level rule files (pawn.ts, knight.ts, etc.)
|
||||||
|
*
|
||||||
|
* NOTE: These are CANDIDATE moves only — they do not check:
|
||||||
|
* - Whether target squares are occupied (capture/blocking handled in P2.9+)
|
||||||
|
* - Whether the move would leave the king in check (P2.18)
|
||||||
|
* - Turn order (P2.13)
|
||||||
|
*/
|
||||||
|
import type { PieceColor, Square } from "../schema.js";
|
||||||
|
import {
|
||||||
|
slidingSquares,
|
||||||
|
knightSquares as knightTargets,
|
||||||
|
kingSquares as kingTargets,
|
||||||
|
rankOf,
|
||||||
|
fileOf,
|
||||||
|
} from "../coord.js";
|
||||||
|
|
||||||
|
export interface CandidateMove {
|
||||||
|
readonly pieceId: number;
|
||||||
|
readonly from: Square;
|
||||||
|
readonly to: Square;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sliding piece helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** All candidate squares for a rook (4 orthogonal rays). */
|
||||||
|
export function rookCandidates(from: Square): Square[][] {
|
||||||
|
return [
|
||||||
|
slidingSquares(from, 1, 0), // right
|
||||||
|
slidingSquares(from, -1, 0), // left
|
||||||
|
slidingSquares(from, 0, 1), // up
|
||||||
|
slidingSquares(from, 0, -1), // down
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All candidate squares for a bishop (4 diagonal rays). */
|
||||||
|
export function bishopCandidates(from: Square): Square[][] {
|
||||||
|
return [
|
||||||
|
slidingSquares(from, 1, 1), // up-right
|
||||||
|
slidingSquares(from, 1, -1), // down-right
|
||||||
|
slidingSquares(from, -1, 1), // up-left
|
||||||
|
slidingSquares(from, -1, -1), // down-left
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All candidate squares for a queen (8 rays). */
|
||||||
|
export function queenCandidates(from: Square): Square[][] {
|
||||||
|
return [...rookCandidates(from), ...bishopCandidates(from)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Leap piece helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Knight candidate squares (already filters off-board). */
|
||||||
|
export function knightCandidates(from: Square): Square[] {
|
||||||
|
return knightTargets(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** King candidate squares (already filters off-board). */
|
||||||
|
export function kingCandidates(from: Square): Square[] {
|
||||||
|
return kingTargets(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Pawn helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pawn single-advance target (no capture).
|
||||||
|
* Returns null if pawn is on the back rank (invalid).
|
||||||
|
*/
|
||||||
|
export function pawnSingleAdvance(from: Square, color: PieceColor): Square | null {
|
||||||
|
const rank = rankOf(from);
|
||||||
|
const targetRank = color === "white" ? rank + 1 : rank - 1;
|
||||||
|
if (targetRank < 0 || targetRank > 7) return null;
|
||||||
|
return (from + (color === "white" ? 8 : -8)) as Square;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pawn double-advance target (from home rank only).
|
||||||
|
* White home rank = rank 1 (squares 8-15)
|
||||||
|
* Black home rank = rank 6 (squares 48-55)
|
||||||
|
*/
|
||||||
|
export function pawnDoubleAdvance(from: Square, color: PieceColor): Square | null {
|
||||||
|
const rank = rankOf(from);
|
||||||
|
const homeRank = color === "white" ? 1 : 6;
|
||||||
|
if (rank !== homeRank) return null;
|
||||||
|
return (from + (color === "white" ? 16 : -16)) as Square;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pawn diagonal capture squares (1 or 2, within board bounds).
|
||||||
|
* Pawns capture diagonally forward — this returns the squares, not whether
|
||||||
|
* an enemy occupies them (capture validation is in P2.9).
|
||||||
|
*/
|
||||||
|
export function pawnCaptureSqares(from: Square, color: PieceColor): Square[] {
|
||||||
|
const rank = rankOf(from);
|
||||||
|
const targetRank = color === "white" ? rank + 1 : rank - 1;
|
||||||
|
if (targetRank < 0 || targetRank > 7) return [];
|
||||||
|
const file = fileOf(from);
|
||||||
|
const squares: Square[] = [];
|
||||||
|
if (file > 0) squares.push((from + (color === "white" ? 7 : -9)) as Square); // left diagonal
|
||||||
|
if (file < 7) squares.push((from + (color === "white" ? 9 : -7)) as Square); // right diagonal
|
||||||
|
return squares;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue