feat(chess): add attribute schema and piece fact shape (P2.5)

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

View file

@ -0,0 +1,62 @@
// @vitest-environment node
import { describe, it, expect } from "vitest";
import {
GAME_ENTITY, chessFact, oppositeColor, PROMOTION_PIECES,
type Square,
} from "./schema.js";
import type { EntityId } from "@paratype/rete";
const mkId = (n: number) => n as EntityId;
describe("GAME_ENTITY", () => {
it("is EntityId 0", () => { expect(GAME_ENTITY).toBe(0); });
});
describe("chessFact()", () => {
it("creates typed PieceType fact", () => {
const f = chessFact(mkId(1), "PieceType", "pawn");
expect(f).toEqual({ id: 1, attr: "PieceType", value: "pawn" });
});
it("creates numeric Position fact", () => {
const f = chessFact(mkId(2), "Position", 28);
expect(f.value).toBe(28);
expect(typeof f.value).toBe("number");
});
it("creates game-level Turn fact using GAME_ENTITY", () => {
const f = chessFact(GAME_ENTITY, "Turn", "white");
expect(f.id).toBe(0);
expect(f.attr).toBe("Turn");
});
it("creates HasMoved boolean fact", () => {
const f = chessFact(mkId(3), "HasMoved", false);
expect(f.value).toBe(false);
});
});
describe("oppositeColor()", () => {
it("white -> black", () => { expect(oppositeColor("white")).toBe("black"); });
it("black -> white", () => { expect(oppositeColor("black")).toBe("white"); });
});
describe("PROMOTION_PIECES", () => {
it("has exactly 4 entries", () => { expect(PROMOTION_PIECES).toHaveLength(4); });
it("excludes king and pawn", () => {
expect(PROMOTION_PIECES).not.toContain("king");
expect(PROMOTION_PIECES).not.toContain("pawn");
});
it("includes queen, rook, bishop, knight", () => {
expect(PROMOTION_PIECES).toContain("queen");
expect(PROMOTION_PIECES).toContain("rook");
});
});
describe("Square encoding", () => {
it("a1=0, h1=7, a8=56, h8=63, e4=28", () => {
const a1: Square = 0;
const h8: Square = 63;
const e4: Square = 28; // rank3*8 + file4
expect(a1).toBe(0);
expect(h8).toBe(63);
expect(e4).toBe(28);
});
});

View file

@ -0,0 +1,68 @@
/**
* Chess attribute schema for @paratype/chess.
* All EAV attribute types for the chess game.
* Squares: 0..63 where square = rank * 8 + file
* file: 0=a, 1=b, ..., 7=h
* rank: 0=rank1, 1=rank2, ..., 7=rank8
* e.g. a1=0, h1=7, a8=56, h8=63, e4=28
*/
import type { EntityId } from "@paratype/rete";
export type PieceType = "pawn" | "knight" | "bishop" | "rook" | "queen" | "king";
export type PieceColor = "white" | "black";
export type Square = number; // 0..63
export type GameStatus = "active" | "checkmate" | "stalemate" | "draw";
export type GameResult = PieceColor | "draw";
/** EntityId reserved for game-level facts (Turn, HalfmoveClock, etc.). */
export const GAME_ENTITY: EntityId = 0 as EntityId;
/**
* Maps attribute name to its TypeScript type.
* Used for typed fact construction and WM queries.
*/
export interface ChessAttrMap {
// Piece attributes
PieceType: PieceType;
Color: PieceColor;
Position: Square;
HasMoved: boolean;
// Game-level attributes
Turn: PieceColor;
HalfmoveClock: number;
FullmoveNumber: number;
EnPassantTarget: Square | null;
GameStatus: GameStatus;
Winner: GameResult | null;
// Custom rule attributes (from RULES.md presets)
Hp: number;
PoisonedSquare: boolean;
}
export type ChessAttrKey = keyof ChessAttrMap;
/** A typed chess fact triple. */
export interface ChessFact<K extends ChessAttrKey = ChessAttrKey> {
readonly id: EntityId;
readonly attr: K;
readonly value: ChessAttrMap[K];
}
/** Construct a typed chess fact. */
export function chessFact<K extends ChessAttrKey>(
id: EntityId,
attr: K,
value: ChessAttrMap[K],
): ChessFact<K> {
return { id, attr, value };
}
/** Return the opposite color. */
export function oppositeColor(color: PieceColor): PieceColor {
return color === "white" ? "black" : "white";
}
/** Valid promotion piece types (excludes pawn and king). */
export const PROMOTION_PIECES: ReadonlyArray<PieceType> = [
"queen", "rook", "bishop", "knight",
];