From 83d99778db84fc3b843ded68312b0268da06862b Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 16 Apr 2026 14:52:51 -0600 Subject: [PATCH] feat(chess): add king basic move rules (P2.12) --- packages/chess/src/rules/king.test.ts | 74 +++++++++++++++++++++++++++ packages/chess/src/rules/king.ts | 32 ++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/chess/src/rules/king.test.ts create mode 100644 packages/chess/src/rules/king.ts diff --git a/packages/chess/src/rules/king.test.ts b/packages/chess/src/rules/king.test.ts new file mode 100644 index 0000000..cc00b18 --- /dev/null +++ b/packages/chess/src/rules/king.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from "vitest"; +import { Session } from "@paratype/rete"; +import type { EntityId } from "@paratype/rete"; +import { getLegalKingMoves } from "./king.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("getLegalKingMoves", () => { + it("king at e4 (28) has 8 moves on empty board", () => { + const session = new Session({ autoFire: false }); + const king = insertPiece(session, 1, "king", "white", 28); + expect(getLegalKingMoves(session, king)).toHaveLength(8); + }); + + it("king at a1 (0) has 3 moves on empty board", () => { + const session = new Session({ autoFire: false }); + const king = insertPiece(session, 1, "king", "white", 0); + expect(getLegalKingMoves(session, king)).toHaveLength(3); + }); + + it("king at h8 (63) has 3 moves on empty board", () => { + const session = new Session({ autoFire: false }); + const king = insertPiece(session, 1, "king", "black", 63); + expect(getLegalKingMoves(session, king)).toHaveLength(3); + }); + + it("king blocked by own pieces", () => { + const session = new Session({ autoFire: false }); + const king = insertPiece(session, 1, "king", "white", 28); // e4 + insertPiece(session, 2, "pawn", "white", 27); // d4 + insertPiece(session, 3, "pawn", "white", 29); // f4 + const moves = getLegalKingMoves(session, king); + expect(moves.find(m => m.to === 27)).toBeUndefined(); + expect(moves.find(m => m.to === 29)).toBeUndefined(); + expect(moves).toHaveLength(6); + }); + + it("king can capture enemy piece", () => { + const session = new Session({ autoFire: false }); + const king = insertPiece(session, 1, "king", "white", 28); // e4 + insertPiece(session, 2, "pawn", "black", 27); // d4 + const capture = getLegalKingMoves(session, king).find(m => m.to === 27); + expect(capture).toBeDefined(); + expect(capture?.isCapture).toBe(true); + }); + + it("quiet moves are marked isCapture=false", () => { + const session = new Session({ autoFire: false }); + const king = insertPiece(session, 1, "king", "white", 28); + const moves = getLegalKingMoves(session, king); + 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(getLegalKingMoves(session, mkId(999))).toEqual([]); + }); +}); diff --git a/packages/chess/src/rules/king.ts b/packages/chess/src/rules/king.ts new file mode 100644 index 0000000..cfc7565 --- /dev/null +++ b/packages/chess/src/rules/king.ts @@ -0,0 +1,32 @@ +/** + * King movement rules for @paratype/chess. + * + * Basic king moves: 1 square in any of 8 directions. + * Excludes: + * - Castling (P2.15) + * - Check-aware rejection / king-in-check filter (P2.18) + * + * Rules: + * - empty target square → legal quiet move + * - enemy on target → legal capture + * - ally on target → illegal + */ +import type { Session, EntityId } from "@paratype/rete"; +import { kingCandidates } from "./primitives.js"; +import type { LegalMove } from "./types.js"; +import { getPiecePosition, getPieceColor, isAllyAt, isPieceAt } from "./board-queries.js"; + +/** Get all legal basic king moves (1 square, any direction). */ +export function getLegalKingMoves(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 kingCandidates(from)) { + if (isAllyAt(session, to, color)) continue; + const isCapture = isPieceAt(session, to); + moves.push({ pieceId, from, to, isCapture }); + } + return moves; +}