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