feat(chess): add starting-position fact generator (P2.6)

This commit is contained in:
Joey Yakimowich-Payne 2026-04-16 14:46:34 -06:00
commit cc584d4e33
No known key found for this signature in database
2 changed files with 238 additions and 0 deletions

View file

@ -0,0 +1,120 @@
import { describe, it, expect } from "vitest";
import { generateStartingPosition, STARTING_PIECES_DEF } from "./starting-position.js";
import { Session } from "@paratype/rete";
import { GAME_ENTITY } from "./schema.js";
describe("generateStartingPosition()", () => {
it("inserts exactly 32 piece entities", () => {
const session = new Session({ autoFire: false });
const ids = generateStartingPosition(session);
expect(ids).toHaveLength(32);
expect(new Set(ids).size).toBe(32); // all unique
});
it("inserts 4 facts per piece (PieceType, Color, Position, HasMoved)", () => {
const session = new Session({ autoFire: false });
generateStartingPosition(session);
const facts = session.allFacts();
// Count piece facts (exclude game-level facts on GAME_ENTITY)
const pieceFacts = facts.filter((f) => f.id !== GAME_ENTITY);
// 32 pieces × 4 attrs per piece
expect(pieceFacts).toHaveLength(128);
});
it("has exactly 8 white pawns on rank 2 (squares 8-15)", () => {
const session = new Session({ autoFire: false });
generateStartingPosition(session);
const facts = session.allFacts();
const whitePawnPositions = facts
.filter((f) => f.attr === "Position" && f.id !== GAME_ENTITY)
.map((f) => {
const colorFact = facts.find(
(c) => c.id === f.id && c.attr === "Color"
);
const typeFact = facts.find((t) => t.id === f.id && t.attr === "PieceType");
return {
position: f.value as number,
color: colorFact?.value,
type: typeFact?.value,
};
})
.filter((p) => p.color === "white" && p.type === "pawn")
.map((p) => p.position);
expect(whitePawnPositions).toHaveLength(8);
expect(whitePawnPositions.every((sq) => sq >= 8 && sq <= 15)).toBe(true);
});
it("white king is on e1 (square 4)", () => {
const session = new Session({ autoFire: false });
generateStartingPosition(session);
const facts = session.allFacts();
const kingEntity = facts.find((f) => {
if (f.attr !== "PieceType" || f.value !== "king" || f.id === GAME_ENTITY) {
return false;
}
const colorFact = facts.find((c) => c.id === f.id && c.attr === "Color");
return colorFact?.value === "white";
})?.id;
expect(kingEntity).toBeDefined();
const kingPos = facts.find((f) => f.id === kingEntity && f.attr === "Position")?.value;
expect(kingPos).toBe(4); // e1
});
it("game-level Turn is white initially", () => {
const session = new Session({ autoFire: false });
generateStartingPosition(session);
const turnFact = session.get(GAME_ENTITY, "Turn");
expect(turnFact).toBe("white");
});
it("HasMoved is false for all pieces initially", () => {
const session = new Session({ autoFire: false });
generateStartingPosition(session);
const facts = session.allFacts();
const movedFacts = facts.filter(
(f) => f.attr === "HasMoved" && f.id !== GAME_ENTITY
);
expect(movedFacts).toHaveLength(32);
expect(movedFacts.every((f) => f.value === false)).toBe(true);
});
it("black king is on e8 (square 60)", () => {
const session = new Session({ autoFire: false });
generateStartingPosition(session);
const facts = session.allFacts();
const kingEntity = facts.find((f) => {
if (f.attr !== "PieceType" || f.value !== "king" || f.id === GAME_ENTITY) {
return false;
}
const colorFact = facts.find((c) => c.id === f.id && c.attr === "Color");
return colorFact?.value === "black";
})?.id;
expect(kingEntity).toBeDefined();
const kingPos = facts.find((f) => f.id === kingEntity && f.attr === "Position")?.value;
expect(kingPos).toBe(60); // e8
});
it("STARTING_PIECES_DEF has 32 entries", () => {
expect(STARTING_PIECES_DEF).toHaveLength(32);
});
it("game-level facts are correctly initialized", () => {
const session = new Session({ autoFire: false });
generateStartingPosition(session);
expect(session.get(GAME_ENTITY, "Turn")).toBe("white");
expect(session.get(GAME_ENTITY, "HalfmoveClock")).toBe(0);
expect(session.get(GAME_ENTITY, "FullmoveNumber")).toBe(1);
expect(session.get(GAME_ENTITY, "EnPassantTarget")).toBe(null);
expect(session.get(GAME_ENTITY, "GameStatus")).toBe("active");
expect(session.get(GAME_ENTITY, "Winner")).toBe(null);
});
});

View file

@ -0,0 +1,118 @@
/**
* FIDE starting position generator.
* Inserts 32 piece entities + game-level facts into a Session.
* Per SPEC.md §ID Authority, pieces get incremental EntityIds; game uses GAME_ENTITY (0).
*/
import type { EntityId } from "@paratype/rete";
import type { Session } from "@paratype/rete";
import {
GAME_ENTITY,
type PieceType,
type PieceColor,
type Square,
} from "./schema.js";
interface PieceDef {
type: PieceType;
color: PieceColor;
square: Square;
}
const WHITE_BACK_RANK: PieceType[] = [
"rook",
"knight",
"bishop",
"queen",
"king",
"bishop",
"knight",
"rook",
];
const BLACK_BACK_RANK: PieceType[] = [
"rook",
"knight",
"bishop",
"queen",
"king",
"bishop",
"knight",
"rook",
];
function buildStartingPieces(): PieceDef[] {
const pieces: PieceDef[] = [];
// White back rank (rank 1 = squares 0-7)
for (let file = 0; file < 8; file++) {
pieces.push({
type: WHITE_BACK_RANK[file]!,
color: "white",
square: file,
});
}
// White pawns (rank 2 = squares 8-15)
for (let file = 0; file < 8; file++) {
pieces.push({
type: "pawn",
color: "white",
square: 8 + file,
});
}
// Black pawns (rank 7 = squares 48-55)
for (let file = 0; file < 8; file++) {
pieces.push({
type: "pawn",
color: "black",
square: 48 + file,
});
}
// Black back rank (rank 8 = squares 56-63)
for (let file = 0; file < 8; file++) {
pieces.push({
type: BLACK_BACK_RANK[file]!,
color: "black",
square: 56 + file,
});
}
return pieces;
}
const STARTING_PIECES = buildStartingPieces();
/**
* Insert all 32 FIDE starting pieces + game-level facts into a Session.
* Each piece gets 4 facts: PieceType, Color, Position, HasMoved.
* Game level gets: Turn, HalfmoveClock, FullmoveNumber, EnPassantTarget, GameStatus, Winner.
* Returns the array of EntityIds assigned to pieces (in STARTING_PIECES order).
*/
export function generateStartingPosition(session: Session): EntityId[] {
const pieceIds: EntityId[] = [];
// Insert all 32 pieces with their attributes
for (const piece of STARTING_PIECES) {
const id = session.nextId();
session.insert(id, "PieceType", piece.type);
session.insert(id, "Color", piece.color);
session.insert(id, "Position", piece.square);
session.insert(id, "HasMoved", false);
pieceIds.push(id);
}
// Game-level facts
session.insert(GAME_ENTITY, "Turn", "white");
session.insert(GAME_ENTITY, "HalfmoveClock", 0);
session.insert(GAME_ENTITY, "FullmoveNumber", 1);
session.insert(GAME_ENTITY, "EnPassantTarget", null);
session.insert(GAME_ENTITY, "GameStatus", "active");
session.insert(GAME_ENTITY, "Winner", null);
return pieceIds;
}
/** The canonical FIDE starting piece definitions (for testing and reference). */
export const STARTING_PIECES_DEF: ReadonlyArray<Readonly<PieceDef>> =
STARTING_PIECES;