feat(chess): add capture resolution (P2.14)

This commit is contained in:
Joey Yakimowich-Payne 2026-04-16 14:53:37 -06:00
commit 483e4ef686
No known key found for this signature in database
2 changed files with 108 additions and 0 deletions

View file

@ -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);
});
});

View file

@ -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;
}