feat(chess): add coordinate + color helpers (P2.7)
This commit is contained in:
parent
cc584d4e33
commit
d4cee82b20
2 changed files with 228 additions and 0 deletions
102
packages/chess/src/coord.test.ts
Normal file
102
packages/chess/src/coord.test.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
fileOf, rankOf, squareOf, isOnBoard, isValidSquare, squareColor,
|
||||
squareToAlgebraic, algebraicToSquare, slidingSquares, knightSquares, kingSquares,
|
||||
} from "./coord.js";
|
||||
|
||||
describe("fileOf / rankOf / squareOf", () => {
|
||||
it("a1=0: file=0, rank=0", () => {
|
||||
expect(fileOf(0)).toBe(0);
|
||||
expect(rankOf(0)).toBe(0);
|
||||
});
|
||||
it("h1=7: file=7, rank=0", () => {
|
||||
expect(fileOf(7)).toBe(7);
|
||||
expect(rankOf(7)).toBe(0);
|
||||
});
|
||||
it("a2=8: file=0, rank=1", () => {
|
||||
expect(fileOf(8)).toBe(0);
|
||||
expect(rankOf(8)).toBe(1);
|
||||
});
|
||||
it("e4=28: file=4, rank=3", () => {
|
||||
expect(fileOf(28)).toBe(4);
|
||||
expect(rankOf(28)).toBe(3);
|
||||
});
|
||||
it("h8=63: file=7, rank=7", () => {
|
||||
expect(fileOf(63)).toBe(7);
|
||||
expect(rankOf(63)).toBe(7);
|
||||
});
|
||||
it("squareOf round-trips", () => {
|
||||
expect(squareOf(0, 0)).toBe(0);
|
||||
expect(squareOf(4, 3)).toBe(28);
|
||||
expect(squareOf(7, 7)).toBe(63);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOnBoard / isValidSquare", () => {
|
||||
it("(0,0) is on board", () => { expect(isOnBoard(0, 0)).toBe(true); });
|
||||
it("(7,7) is on board", () => { expect(isOnBoard(7, 7)).toBe(true); });
|
||||
it("(-1,0) is off board", () => { expect(isOnBoard(-1, 0)).toBe(false); });
|
||||
it("(8,0) is off board", () => { expect(isOnBoard(8, 0)).toBe(false); });
|
||||
it("square 0 valid", () => { expect(isValidSquare(0)).toBe(true); });
|
||||
it("square 63 valid", () => { expect(isValidSquare(63)).toBe(true); });
|
||||
it("square -1 invalid", () => { expect(isValidSquare(-1)).toBe(false); });
|
||||
it("square 64 invalid", () => { expect(isValidSquare(64)).toBe(false); });
|
||||
});
|
||||
|
||||
describe("squareColor", () => {
|
||||
it("a1 (0) is dark (0+0=0 even)", () => { expect(squareColor(0)).toBe("dark"); });
|
||||
it("b1 (1) is light (1+0=1 odd)", () => { expect(squareColor(1)).toBe("light"); });
|
||||
it("h8 (63) is dark (7+7=14 even)", () => {
|
||||
expect(squareColor(63)).toBe("dark");
|
||||
});
|
||||
});
|
||||
|
||||
describe("squareToAlgebraic / algebraicToSquare", () => {
|
||||
it("0 → 'a1'", () => { expect(squareToAlgebraic(0)).toBe("a1"); });
|
||||
it("28 → 'e4'", () => { expect(squareToAlgebraic(28)).toBe("e4"); });
|
||||
it("63 → 'h8'", () => { expect(squareToAlgebraic(63)).toBe("h8"); });
|
||||
it("'e4' → 28", () => { expect(algebraicToSquare("e4")).toBe(28); });
|
||||
it("'a1' → 0", () => { expect(algebraicToSquare("a1")).toBe(0); });
|
||||
it("'h8' → 63", () => { expect(algebraicToSquare("h8")).toBe(63); });
|
||||
it("invalid → -1", () => { expect(algebraicToSquare("z9")).toBe(-1); });
|
||||
});
|
||||
|
||||
describe("slidingSquares", () => {
|
||||
it("rook from a1 going right: b1,c1,...,h1", () => {
|
||||
const sq = slidingSquares(0, 1, 0);
|
||||
expect(sq).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
||||
});
|
||||
it("bishop from h8 going down-left: g7,f6,e5,d4,c3,b2,a1", () => {
|
||||
const sq = slidingSquares(63, -1, -1);
|
||||
expect(sq).toHaveLength(7);
|
||||
expect(sq[0]).toBe(54); // g7
|
||||
});
|
||||
it("stops at board edge immediately if no room", () => {
|
||||
const sq = slidingSquares(7, 1, 0); // rook at h1 going right
|
||||
expect(sq).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("knightSquares", () => {
|
||||
it("knight at e4 (28) has 8 moves", () => {
|
||||
expect(knightSquares(28)).toHaveLength(8);
|
||||
});
|
||||
it("knight at a1 (0) has 2 moves only", () => {
|
||||
expect(knightSquares(0)).toHaveLength(2);
|
||||
});
|
||||
it("knight at h8 (63) has 2 moves only", () => {
|
||||
expect(knightSquares(63)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("kingSquares", () => {
|
||||
it("king at e4 (28) has 8 moves", () => {
|
||||
expect(kingSquares(28)).toHaveLength(8);
|
||||
});
|
||||
it("king at a1 (0) has 3 moves only", () => {
|
||||
expect(kingSquares(0)).toHaveLength(3);
|
||||
});
|
||||
it("king at h8 (63) has 3 moves only", () => {
|
||||
expect(kingSquares(63)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
126
packages/chess/src/coord.ts
Normal file
126
packages/chess/src/coord.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* Board coordinate helpers for @paratype/chess.
|
||||
* Square encoding: square = rank * 8 + file
|
||||
* file: 0=a, 1=b, 2=c, 3=d, 4=e, 5=f, 6=g, 7=h
|
||||
* rank: 0=rank1, 1=rank2, ..., 7=rank8
|
||||
* Examples: a1=0, h1=7, e4=28, h8=63
|
||||
*/
|
||||
import type { Square } from "./schema.js";
|
||||
|
||||
/** Extract file (0=a..7=h) from a square. */
|
||||
export function fileOf(square: Square): number {
|
||||
return square % 8;
|
||||
}
|
||||
|
||||
/** Extract rank (0=rank1..7=rank8) from a square. */
|
||||
export function rankOf(square: Square): number {
|
||||
return Math.floor(square / 8);
|
||||
}
|
||||
|
||||
/** Construct a square from file and rank. Does NOT validate range. */
|
||||
export function squareOf(file: number, rank: number): Square {
|
||||
return (rank * 8 + file) as Square;
|
||||
}
|
||||
|
||||
/** Is the (file, rank) pair on the board (0..7 for both)? */
|
||||
export function isOnBoard(file: number, rank: number): boolean {
|
||||
return file >= 0 && file <= 7 && rank >= 0 && rank <= 7;
|
||||
}
|
||||
|
||||
/** Is a square in the valid 0..63 range? */
|
||||
export function isValidSquare(square: number): boolean {
|
||||
return square >= 0 && square <= 63;
|
||||
}
|
||||
|
||||
/**
|
||||
* Light/dark square color. Returns "light" or "dark".
|
||||
* A square is light if (file + rank) is odd, dark if even.
|
||||
*/
|
||||
export function squareColor(square: Square): "light" | "dark" {
|
||||
const file = fileOf(square);
|
||||
const rank = rankOf(square);
|
||||
return (file + rank) % 2 === 0 ? "dark" : "light";
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a square to algebraic notation (e.g., 0 → "a1", 28 → "e4").
|
||||
* Useful for debugging and PGN parsing.
|
||||
*/
|
||||
export function squareToAlgebraic(square: Square): string {
|
||||
const file = fileOf(square);
|
||||
const rank = rankOf(square);
|
||||
return String.fromCharCode(97 + file) + String(rank + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse algebraic notation to square number (e.g., "e4" → 28).
|
||||
* Returns -1 for invalid input.
|
||||
*/
|
||||
export function algebraicToSquare(notation: string): Square {
|
||||
if (notation.length !== 2) return -1 as Square;
|
||||
const file = notation.charCodeAt(0) - 97; // 'a'=0
|
||||
const rank = parseInt(notation[1]!, 10) - 1;
|
||||
if (!isOnBoard(file, rank)) return -1 as Square;
|
||||
return squareOf(file, rank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all squares a sliding piece can reach in one direction,
|
||||
* stopping when it hits the board edge (exclusive).
|
||||
* @param from — starting square
|
||||
* @param fileDelta — file change per step (-1, 0, or 1)
|
||||
* @param rankDelta — rank change per step (-1, 0, or 1)
|
||||
*/
|
||||
export function slidingSquares(
|
||||
from: Square,
|
||||
fileDelta: number,
|
||||
rankDelta: number,
|
||||
): Square[] {
|
||||
const squares: Square[] = [];
|
||||
let file = fileOf(from) + fileDelta;
|
||||
let rank = rankOf(from) + rankDelta;
|
||||
while (isOnBoard(file, rank)) {
|
||||
squares.push(squareOf(file, rank));
|
||||
file += fileDelta;
|
||||
rank += rankDelta;
|
||||
}
|
||||
return squares;
|
||||
}
|
||||
|
||||
/**
|
||||
* Knight move offsets: all 8 L-shaped moves.
|
||||
* Returns valid target squares from a given square.
|
||||
*/
|
||||
export function knightSquares(from: Square): Square[] {
|
||||
const offsets: [number, number][] = [
|
||||
[1, 2], [2, 1], [2, -1], [1, -2],
|
||||
[-1, -2], [-2, -1], [-2, 1], [-1, 2],
|
||||
];
|
||||
const file = fileOf(from);
|
||||
const rank = rankOf(from);
|
||||
const squares: Square[] = [];
|
||||
for (const [df, dr] of offsets) {
|
||||
const nf = file + df;
|
||||
const nr = rank + dr;
|
||||
if (isOnBoard(nf, nr)) squares.push(squareOf(nf, nr));
|
||||
}
|
||||
return squares;
|
||||
}
|
||||
|
||||
/**
|
||||
* King move squares: all 8 adjacent squares (within board bounds).
|
||||
*/
|
||||
export function kingSquares(from: Square): Square[] {
|
||||
const file = fileOf(from);
|
||||
const rank = rankOf(from);
|
||||
const squares: Square[] = [];
|
||||
for (let df = -1; df <= 1; df++) {
|
||||
for (let dr = -1; dr <= 1; dr++) {
|
||||
if (df === 0 && dr === 0) continue;
|
||||
const nf = file + df;
|
||||
const nr = rank + dr;
|
||||
if (isOnBoard(nf, nr)) squares.push(squareOf(nf, nr));
|
||||
}
|
||||
}
|
||||
return squares;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue