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