diff --git a/packages/chess/src/rules/capture.test.ts b/packages/chess/src/rules/capture.test.ts new file mode 100644 index 0000000..f635d91 --- /dev/null +++ b/packages/chess/src/rules/capture.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from "vitest"; +import { Session } from "@paratype/rete"; +import { applyCapture, getCapturablePieces, canCapture } from "./capture.js"; +import type { EntityId } from "@paratype/rete"; + +const mkId = (n: number) => n as EntityId; +const ins = (s: Session, id: number, type: string, color: string, sq: number) => { + const eid = mkId(id); + s.insert(eid, "PieceType", type); s.insert(eid, "Color", color); s.insert(eid, "Position", sq); + return eid; +}; + +describe("applyCapture", () => { + it("removes all facts for the captured piece", () => { + const session = new Session({ autoFire: false }); + const black = ins(session, 2, "knight", "black", 28); + applyCapture(session, black); + expect(session.contains(black, "PieceType")).toBe(false); + expect(session.contains(black, "Color")).toBe(false); + expect(session.contains(black, "Position")).toBe(false); + }); + + it("does not affect other pieces", () => { + const session = new Session({ autoFire: false }); + const white = ins(session, 1, "pawn", "white", 20); + const black = ins(session, 2, "knight", "black", 28); + applyCapture(session, black); + expect(session.contains(white, "Position")).toBe(true); + }); +}); + +describe("getCapturablePieces", () => { + it("returns all enemy piece ids", () => { + const session = new Session({ autoFire: false }); + ins(session, 1, "pawn", "white", 20); + ins(session, 2, "pawn", "white", 21); + ins(session, 3, "rook", "black", 50); + ins(session, 4, "bishop", "black", 51); + const capturable = getCapturablePieces(session, "white"); + expect(capturable).toContain(mkId(3)); + expect(capturable).toContain(mkId(4)); + expect(capturable).not.toContain(mkId(1)); + }); +}); + +describe("canCapture", () => { + it("returns true when enemy on square", () => { + const session = new Session({ autoFire: false }); + ins(session, 1, "rook", "black", 28); + expect(canCapture(session, 28, "white")).toBe(true); + }); + + it("returns false when own piece on square", () => { + const session = new Session({ autoFire: false }); + ins(session, 1, "rook", "white", 28); + expect(canCapture(session, 28, "white")).toBe(false); + }); + + it("returns false when square is empty", () => { + const session = new Session({ autoFire: false }); + expect(canCapture(session, 28, "white")).toBe(false); + }); +}); diff --git a/packages/chess/src/rules/capture.ts b/packages/chess/src/rules/capture.ts new file mode 100644 index 0000000..5ce9fd9 --- /dev/null +++ b/packages/chess/src/rules/capture.ts @@ -0,0 +1,45 @@ +/** + * Capture resolution for @paratype/chess. + * When a capture occurs, the captured piece's facts are retracted from WM. + */ +import type { Session, EntityId } from "@paratype/rete"; +import type { PieceColor } from "../schema.js"; + +/** All known chess fact attributes for a piece entity. */ +const PIECE_ATTRS = ["PieceType", "Color", "Position", "HasMoved", "Hp"] as const; + +/** + * Apply a capture: retract all known facts for the captured piece. + * Called after confirming the capture is legal. + */ +export function applyCapture(session: Session, capturedId: EntityId): void { + for (const attr of PIECE_ATTRS) { + if (session.contains(capturedId, attr)) { + session.retract(capturedId, attr); + } + } +} + +/** + * Get all enemy piece entity IDs that a given color could potentially capture. + * Returns the IDs of all pieces belonging to the opposite color. + */ +export function getCapturablePieces(session: Session, attackingColor: PieceColor): EntityId[] { + const enemyColor: PieceColor = attackingColor === "white" ? "black" : "white"; + const facts = session.allFacts(); + return facts + .filter(f => f.attr === "Color" && f.value === enemyColor && f.id > 0) + .map(f => f.id as EntityId); +} + +/** + * Check if capturing the piece at the given square is legal + * (i.e., it's an enemy piece, not an ally). + */ +export function canCapture(session: Session, square: number, attackingColor: PieceColor): boolean { + const facts = session.allFacts(); + const positionFact = facts.find(f => f.attr === "Position" && f.value === square); + if (!positionFact) return false; // no piece at square + const colorFact = facts.find(f => f.id === positionFact.id && f.attr === "Color"); + return colorFact?.value !== undefined && colorFact.value !== attackingColor; +}