feat(chess): add starting-position fact generator (P2.6)
This commit is contained in:
parent
ea4d6e9a69
commit
cc584d4e33
2 changed files with 238 additions and 0 deletions
120
packages/chess/src/starting-position.test.ts
Normal file
120
packages/chess/src/starting-position.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
118
packages/chess/src/starting-position.ts
Normal file
118
packages/chess/src/starting-position.ts
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue