feat(chess): add knight move rules (P2.10)

This commit is contained in:
Joey Yakimowich-Payne 2026-04-16 14:51:56 -06:00
commit 6994d5fe55
No known key found for this signature in database
2 changed files with 115 additions and 0 deletions

View file

@ -0,0 +1,87 @@
import { describe, it, expect } from "vitest";
import { Session } from "@paratype/rete";
import type { EntityId } from "@paratype/rete";
import { getLegalKnightMoves } from "./knight.js";
const mkId = (n: number) => n as EntityId;
function insertPiece(
session: Session,
id: number,
type: string,
color: string,
square: number,
): EntityId {
const eid = mkId(id);
session.insert(eid, "PieceType", type);
session.insert(eid, "Color", color);
session.insert(eid, "Position", square);
return eid;
}
describe("getLegalKnightMoves", () => {
it("knight at e4 (28) has 8 moves when board is empty", () => {
const session = new Session({ autoFire: false });
const knight = insertPiece(session, 1, "knight", "white", 28);
const moves = getLegalKnightMoves(session, knight);
expect(moves).toHaveLength(8);
// All 8 L-shapes from e4: f6=45, g5=38, g3=22, f2=13, d2=11, c3=18, c5=34, d6=43
const targets = moves.map(m => m.to).sort((a, b) => a - b);
expect(targets).toEqual([11, 13, 18, 22, 34, 38, 43, 45]);
});
it("knight at a1 (0) has 2 moves only (board edge)", () => {
const session = new Session({ autoFire: false });
const knight = insertPiece(session, 1, "knight", "white", 0);
const moves = getLegalKnightMoves(session, knight);
expect(moves).toHaveLength(2);
// From a1: b3=17, c2=10
const targets = moves.map(m => m.to).sort((a, b) => a - b);
expect(targets).toEqual([10, 17]);
});
it("knight ignores intervening pieces (leaps)", () => {
const session = new Session({ autoFire: false });
const knight = insertPiece(session, 1, "knight", "white", 28); // e4
// Block all orthogonally adjacent squares — knight should still leap over them.
insertPiece(session, 2, "pawn", "white", 27); // d4
insertPiece(session, 3, "pawn", "white", 29); // f4
insertPiece(session, 4, "pawn", "white", 20); // e3
insertPiece(session, 5, "pawn", "white", 36); // e5
const moves = getLegalKnightMoves(session, knight);
// None of the blockers sits on a knight-target square, so all 8 reachable.
expect(moves).toHaveLength(8);
});
it("cannot capture ally piece", () => {
const session = new Session({ autoFire: false });
const knight = insertPiece(session, 1, "knight", "white", 28); // e4
insertPiece(session, 2, "rook", "white", 38); // g5 — knight target
const moves = getLegalKnightMoves(session, knight);
expect(moves.find(m => m.to === 38)).toBeUndefined();
expect(moves).toHaveLength(7);
});
it("can capture enemy piece", () => {
const session = new Session({ autoFire: false });
const knight = insertPiece(session, 1, "knight", "white", 28); // e4
insertPiece(session, 2, "rook", "black", 38); // g5 — knight target
const move = getLegalKnightMoves(session, knight).find(m => m.to === 38);
expect(move).toBeDefined();
expect(move?.isCapture).toBe(true);
});
it("quiet moves are marked isCapture=false", () => {
const session = new Session({ autoFire: false });
const knight = insertPiece(session, 1, "knight", "white", 28);
const moves = getLegalKnightMoves(session, knight);
for (const m of moves) {
expect(m.isCapture).toBe(false);
}
});
it("returns empty list when pieceId has no Position/Color", () => {
const session = new Session({ autoFire: false });
expect(getLegalKnightMoves(session, mkId(999))).toEqual([]);
});
});

View file

@ -0,0 +1,28 @@
/**
* Knight movement rules for @paratype/chess.
*
* Knights leap they do NOT check for intervening pieces on the path
* between `from` and `to`. Only the target square matters:
* - empty legal quiet move
* - enemy legal capture
* - ally illegal
*/
import type { Session, EntityId } from "@paratype/rete";
import { knightCandidates } from "./primitives.js";
import type { LegalMove } from "./types.js";
import { getPiecePosition, getPieceColor, isAllyAt, isPieceAt } from "./board-queries.js";
/** Get all legal knight moves (leaps — no path obstruction check). */
export function getLegalKnightMoves(session: Session, pieceId: EntityId): LegalMove[] {
const from = getPiecePosition(session, pieceId);
const color = getPieceColor(session, pieceId);
if (from === null || color === null) return [];
const moves: LegalMove[] = [];
for (const to of knightCandidates(from)) {
if (isAllyAt(session, to, color)) continue;
const isCapture = isPieceAt(session, to);
moves.push({ pieceId, from, to, isCapture });
}
return moves;
}