feat(engine): state effect primitives (seed/add/multiply/direction/capture-flag)

T3 Wave 2 batch A. Implements 5 state-mutating primitives that compose
to seed and adjust EAV facts on a piece during profile apply():

- seed-attribute: insert {attr,value} (overwrites existing).
- add-to-attribute: read existing number (or 0 if absent), write +delta.
- multiply-attribute: read existing number (no-op if absent), write *factor.
- add-direction: append named directions (forward/backward/...) into the
  T1-shared DirectionAdditions string[] with dedupe. Composes additively
  with the T1 direction-additions descriptor's writes — the engine's
  existing generateDirectionMoves walker handles both contributions.
- set-capture-flag: OR a CaptureFlag bitflag into the existing CaptureFlags
  field (idempotent).

All 5 register in PRIMITIVE_REGISTRY via side-effect import. Each primitive
ships with ≥3 vitest scenarios. The barrel (primitives/index.ts) imports
all 5 in registration-order.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-19 17:41:22 -06:00
commit d17f4bd1fd
No known key found for this signature in database
13 changed files with 740 additions and 0 deletions

View file

@ -0,0 +1,87 @@
import { Session } from "@paratype/rete";
import { describe, expect, it } from "vitest";
import { ChessEngine } from "../../engine.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import { ADD_AURA_PRIMITIVE } from "./add-aura.js";
import type { PrimitiveApplyContext } from "./types.js";
import "./add-aura.js";
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
const session = new Session();
const pieceId = session.nextId();
const ctx: PrimitiveApplyContext = {
engine: new ChessEngine(),
session,
pieceId,
depth: 0,
descriptor: {
id: "custom:test-add-aura",
type: "data",
version: 1,
},
};
return { ctx, session };
}
describe("add-aura primitive — registry", () => {
it("registers in PRIMITIVE_REGISTRY under key 'add-aura'", () => {
expect(PRIMITIVE_REGISTRY.has("add-aura")).toBe(true);
expect(PRIMITIVE_REGISTRY.get("add-aura")).toBe(ADD_AURA_PRIMITIVE);
});
});
describe("add-aura primitive — apply()", () => {
it("seeds one aura spec entry", () => {
const { ctx, session } = makeContext();
ADD_AURA_PRIMITIVE.apply(ctx, {
radius: 2,
targetAttr: "HpBonus",
delta: 1,
});
expect(session.get(ctx.pieceId, "AuraSpec")).toEqual([
{ radius: 2, targetAttr: "HpBonus", delta: 1 },
]);
});
it("composes multiple aura specs into an array", () => {
const { ctx, session } = makeContext();
ADD_AURA_PRIMITIVE.apply(ctx, {
radius: 1,
targetAttr: "RangeBonus",
delta: 2,
});
ADD_AURA_PRIMITIVE.apply(ctx, {
radius: 3,
targetAttr: "DamageResistance",
delta: -0.25,
});
expect(session.get(ctx.pieceId, "AuraSpec")).toEqual([
{ radius: 1, targetAttr: "RangeBonus", delta: 2 },
{ radius: 3, targetAttr: "DamageResistance", delta: -0.25 },
]);
});
});
describe("add-aura primitive — paramsSchema", () => {
it("rejects radii outside the [1, 7] range", () => {
expect(() =>
ADD_AURA_PRIMITIVE.paramsSchema.parse({
radius: 0,
targetAttr: "HpBonus",
delta: 1,
}),
).toThrow();
expect(() =>
ADD_AURA_PRIMITIVE.paramsSchema.parse({
radius: 8,
targetAttr: "HpBonus",
delta: 1,
}),
).toThrow();
});
});

View file

@ -0,0 +1,37 @@
import { z } from "zod";
import type { ChessAttrMap } from "../../schema.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { EffectPrimitive, PrimitiveApplyContext } from "./types.js";
const schema = z.object({
radius: z.number().int().min(1).max(7),
targetAttr: z.string(),
delta: z.number(),
});
type Params = z.infer<typeof schema>;
const descriptor: EffectPrimitive<Params> = {
kind: "add-aura",
label: "Add Aura",
description:
"Seeds an AuraSpec fact entry consumed by the aura engine's recomputation phase.",
paramsSchema: schema,
apply(ctx: PrimitiveApplyContext, params: Params): void {
const existing =
(ctx.session.get(ctx.pieceId, "AuraSpec") as
| ChessAttrMap["AuraSpec"]
| undefined) ?? [];
ctx.session.insert(ctx.pieceId, "AuraSpec", [
...existing,
{
radius: params.radius,
targetAttr: params.targetAttr,
delta: params.delta,
},
]);
},
};
PRIMITIVE_REGISTRY.register(descriptor);
export { descriptor as ADD_AURA_PRIMITIVE };

View file

@ -0,0 +1,109 @@
import { describe, expect, it } from "vitest";
import { Session } from "@paratype/rete";
import { ChessEngine } from "../../engine.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { PrimitiveApplyContext } from "./types.js";
import "./add-direction.js";
import { ADD_DIRECTION_PRIMITIVE } from "./add-direction.js";
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
const session = new Session();
const pieceId = session.nextId();
const ctx: PrimitiveApplyContext = {
engine: new ChessEngine(),
session,
pieceId,
depth: 0,
descriptor: {
id: "custom:test-add-direction",
type: "data",
version: 1,
},
};
return { ctx, session };
}
describe("add-direction primitive — registry", () => {
it("registers in PRIMITIVE_REGISTRY under key 'add-direction'", () => {
expect(PRIMITIVE_REGISTRY.has("add-direction")).toBe(true);
expect(PRIMITIVE_REGISTRY.get("add-direction")).toBe(ADD_DIRECTION_PRIMITIVE);
});
});
describe("add-direction primitive — apply()", () => {
it("adds one direction to empty state", () => {
const { ctx, session } = makeContext();
ADD_DIRECTION_PRIMITIVE.apply(ctx, { directions: ["forward"] });
expect(session.get(ctx.pieceId, "DirectionAdditions")).toEqual(["forward"]);
});
it("concatenates with existing directions", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "DirectionAdditions", ["right"]);
ADD_DIRECTION_PRIMITIVE.apply(ctx, { directions: ["left"] });
expect(session.get(ctx.pieceId, "DirectionAdditions")).toEqual([
"right",
"left",
]);
});
it("deduplicates overlapping directions by name", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "DirectionAdditions", ["forward"]);
ADD_DIRECTION_PRIMITIVE.apply(ctx, {
directions: ["forward", "diagonal-fr"],
});
expect(session.get(ctx.pieceId, "DirectionAdditions")).toEqual([
"forward",
"diagonal-fr",
]);
});
it("composes additively with the T1 direction-additions descriptor's writes", () => {
// The T1 modifier writes `string[]` of named directions. This primitive
// must read+merge that exact shape so the engine's existing
// `generateDirectionMoves` walker handles both contributions.
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "DirectionAdditions", ["forward", "left"]);
ADD_DIRECTION_PRIMITIVE.apply(ctx, {
directions: ["right", "diagonal-fl"],
});
expect(session.get(ctx.pieceId, "DirectionAdditions")).toEqual([
"forward",
"left",
"right",
"diagonal-fl",
]);
});
it("rejects unknown direction names at the schema layer", () => {
const parsed = ADD_DIRECTION_PRIMITIVE.paramsSchema.safeParse({
directions: ["northwest"],
});
expect(parsed.success).toBe(false);
});
it("throws when the existing DirectionAdditions fact has a wrong shape", () => {
// If a stale or upstream-buggy fact seeds DirectionAdditions with a
// non-string entry, the primitive must fail loudly rather than silently
// mixing two incompatible shapes into the same array. We bypass the
// chess-typed insert by going through the underlying rete session API
// (which accepts `unknown` values).
const { ctx, session } = makeContext();
const factId = ctx.pieceId;
// Insert via the untyped rete path so we can plant a wrong-shape value
// without triggering the chess attr-map type check.
session.insert(factId, "DirectionAdditions", [{ dx: 1, dy: 0 }]);
expect(() => {
ADD_DIRECTION_PRIMITIVE.apply(ctx, { directions: ["forward"] });
}).toThrow();
});
});

View file

@ -0,0 +1,66 @@
import { z } from "zod";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { EffectPrimitive, PrimitiveApplyContext } from "./types.js";
/**
* `DirectionAdditions` is the existing T1 modifier attribute (see
* `descriptors/direction-additions.ts`). Its value is a `readonly string[]`
* of named, color-relative directions interpreted by `generateDirectionMoves`.
* This primitive composes additively with that descriptor — appending names
* into the same array — so the engine's existing direction-walker handles
* both T1-built-in and T3-DSL contributions identically.
*/
const directionNameSchema = z.enum([
"forward",
"backward",
"left",
"right",
"diagonal-fl",
"diagonal-fr",
"diagonal-bl",
"diagonal-br",
]);
const schema = z.object({
directions: z.array(directionNameSchema).min(1),
});
type DirectionName = z.infer<typeof directionNameSchema>;
type Params = z.infer<typeof schema>;
function dedupe(directions: readonly DirectionName[]): DirectionName[] {
const seen = new Set<string>();
const out: DirectionName[] = [];
for (const d of directions) {
if (seen.has(d)) continue;
seen.add(d);
out.push(d);
}
return out;
}
const descriptor: EffectPrimitive<Params> = {
kind: "add-direction",
label: "Add Direction",
description: "Appends movement directions into DirectionAdditions with dedupe.",
paramsSchema: schema,
apply(ctx: PrimitiveApplyContext, params: Params): void {
const existing = ctx.session.get(ctx.pieceId, "DirectionAdditions");
const existingDirections = existing ?? [];
if (!Array.isArray(existingDirections)) {
throw new Error("add-direction expected DirectionAdditions to be an array");
}
const parsed = z.array(directionNameSchema).safeParse(existingDirections);
if (!parsed.success) {
throw new Error(
"add-direction expected DirectionAdditions to be a string[] of named directions",
);
}
const merged = [...parsed.data, ...params.directions];
ctx.session.insert(ctx.pieceId, "DirectionAdditions", dedupe(merged));
},
};
PRIMITIVE_REGISTRY.register(descriptor);
export { descriptor as ADD_DIRECTION_PRIMITIVE };

View file

@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { Session } from "@paratype/rete";
import { ChessEngine } from "../../engine.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { PrimitiveApplyContext } from "./types.js";
import "./add-to-attribute.js";
import { ADD_TO_ATTRIBUTE_PRIMITIVE } from "./add-to-attribute.js";
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
const session = new Session();
const pieceId = session.nextId();
const ctx: PrimitiveApplyContext = {
engine: new ChessEngine(),
session,
pieceId,
depth: 0,
descriptor: {
id: "custom:test-add-to-attribute",
type: "data",
version: 1,
},
};
return { ctx, session };
}
describe("add-to-attribute primitive — registry", () => {
it("registers in PRIMITIVE_REGISTRY under key 'add-to-attribute'", () => {
expect(PRIMITIVE_REGISTRY.has("add-to-attribute")).toBe(true);
expect(PRIMITIVE_REGISTRY.get("add-to-attribute")).toBe(
ADD_TO_ATTRIBUTE_PRIMITIVE,
);
});
});
describe("add-to-attribute primitive — apply()", () => {
it("adds delta to an existing number", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "HpBonus", 2);
ADD_TO_ATTRIBUTE_PRIMITIVE.apply(ctx, { attr: "HpBonus", delta: 3 });
expect(session.get(ctx.pieceId, "HpBonus")).toBe(5);
});
it("treats an absent fact as 0", () => {
const { ctx, session } = makeContext();
ADD_TO_ATTRIBUTE_PRIMITIVE.apply(ctx, { attr: "RangeBonus", delta: 4 });
expect(session.get(ctx.pieceId, "RangeBonus")).toBe(4);
});
it("throws when existing value is non-numeric", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "PieceType", "pawn");
expect(() => {
ADD_TO_ATTRIBUTE_PRIMITIVE.apply(ctx, { attr: "PieceType", delta: 1 });
}).toThrow(/expected numeric value/);
});
});

View file

@ -0,0 +1,29 @@
import { z } from "zod";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { EffectPrimitive, PrimitiveApplyContext } from "./types.js";
const schema = z.object({
attr: z.string(),
delta: z.number(),
});
type Params = z.infer<typeof schema>;
const descriptor: EffectPrimitive<Params> = {
kind: "add-to-attribute",
label: "Add To Attribute",
description: "Adds delta to the current attribute value, treating missing as 0.",
paramsSchema: schema,
apply(ctx: PrimitiveApplyContext, params: Params): void {
const existing = ctx.session.get(ctx.pieceId, params.attr);
const baseValue = existing === undefined ? 0 : existing;
if (typeof baseValue !== "number" || Number.isNaN(baseValue)) {
throw new Error(
`add-to-attribute expected numeric value for attr "${params.attr}" but got ${typeof baseValue}`,
);
}
ctx.session.insert(ctx.pieceId, params.attr, baseValue + params.delta);
},
};
PRIMITIVE_REGISTRY.register(descriptor);
export { descriptor as ADD_TO_ATTRIBUTE_PRIMITIVE };

View file

@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { Session } from "@paratype/rete";
import { ChessEngine } from "../../engine.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { PrimitiveApplyContext } from "./types.js";
import "./multiply-attribute.js";
import { MULTIPLY_ATTRIBUTE_PRIMITIVE } from "./multiply-attribute.js";
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
const session = new Session();
const pieceId = session.nextId();
const ctx: PrimitiveApplyContext = {
engine: new ChessEngine(),
session,
pieceId,
depth: 0,
descriptor: {
id: "custom:test-multiply-attribute",
type: "data",
version: 1,
},
};
return { ctx, session };
}
describe("multiply-attribute primitive — registry", () => {
it("registers in PRIMITIVE_REGISTRY under key 'multiply-attribute'", () => {
expect(PRIMITIVE_REGISTRY.has("multiply-attribute")).toBe(true);
expect(PRIMITIVE_REGISTRY.get("multiply-attribute")).toBe(
MULTIPLY_ATTRIBUTE_PRIMITIVE,
);
});
});
describe("multiply-attribute primitive — apply()", () => {
it("multiplies an existing number", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "Hp", 6);
MULTIPLY_ATTRIBUTE_PRIMITIVE.apply(ctx, { attr: "Hp", factor: 1.5 });
expect(session.get(ctx.pieceId, "Hp")).toBe(9);
});
it("is a no-op when the attribute is absent", () => {
const { ctx, session } = makeContext();
MULTIPLY_ATTRIBUTE_PRIMITIVE.apply(ctx, { attr: "RangeBonus", factor: 2 });
expect(session.get(ctx.pieceId, "RangeBonus")).toBeUndefined();
});
it("throws when existing value is non-numeric", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "PieceType", "rook");
expect(() => {
MULTIPLY_ATTRIBUTE_PRIMITIVE.apply(ctx, { attr: "PieceType", factor: 2 });
}).toThrow(/expected numeric value/);
});
});

View file

@ -0,0 +1,31 @@
import { z } from "zod";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { EffectPrimitive, PrimitiveApplyContext } from "./types.js";
const schema = z.object({
attr: z.string(),
factor: z.number(),
});
type Params = z.infer<typeof schema>;
const descriptor: EffectPrimitive<Params> = {
kind: "multiply-attribute",
label: "Multiply Attribute",
description: "Multiplies an existing attribute value by the provided factor.",
paramsSchema: schema,
apply(ctx: PrimitiveApplyContext, params: Params): void {
const existing = ctx.session.get(ctx.pieceId, params.attr);
if (existing === undefined) {
return;
}
if (typeof existing !== "number" || Number.isNaN(existing)) {
throw new Error(
`multiply-attribute expected numeric value for attr "${params.attr}" but got ${typeof existing}`,
);
}
ctx.session.insert(ctx.pieceId, params.attr, existing * params.factor);
},
};
PRIMITIVE_REGISTRY.register(descriptor);
export { descriptor as MULTIPLY_ATTRIBUTE_PRIMITIVE };

View file

@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import { Session } from "@paratype/rete";
import { ChessEngine } from "../../engine.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { PrimitiveApplyContext } from "./types.js";
import "./seed-attribute.js";
import { SEED_ATTRIBUTE_PRIMITIVE } from "./seed-attribute.js";
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
const session = new Session();
const pieceId = session.nextId();
const ctx: PrimitiveApplyContext = {
engine: new ChessEngine(),
session,
pieceId,
depth: 0,
descriptor: {
id: "custom:test-seed-attribute",
type: "data",
version: 1,
},
};
return { ctx, session };
}
describe("seed-attribute primitive — registry", () => {
it("registers in PRIMITIVE_REGISTRY under key 'seed-attribute'", () => {
expect(PRIMITIVE_REGISTRY.has("seed-attribute")).toBe(true);
expect(PRIMITIVE_REGISTRY.get("seed-attribute")).toBe(SEED_ATTRIBUTE_PRIMITIVE);
});
});
describe("seed-attribute primitive — apply()", () => {
it("writes a string attribute", () => {
const { ctx, session } = makeContext();
SEED_ATTRIBUTE_PRIMITIVE.apply(ctx, {
attr: "PieceType",
value: "queen",
});
expect(session.get(ctx.pieceId, "PieceType")).toBe("queen");
});
it("writes a number attribute", () => {
const { ctx, session } = makeContext();
SEED_ATTRIBUTE_PRIMITIVE.apply(ctx, {
attr: "Hp",
value: 12,
});
expect(session.get(ctx.pieceId, "Hp")).toBe(12);
});
it("overwrites an existing fact", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "RangeBonus", 1);
SEED_ATTRIBUTE_PRIMITIVE.apply(ctx, {
attr: "RangeBonus",
value: 4,
});
expect(session.get(ctx.pieceId, "RangeBonus")).toBe(4);
});
});

View file

@ -0,0 +1,51 @@
import { z } from "zod";
import type { ChessAttrKey } from "../../schema.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { EffectPrimitive, PrimitiveApplyContext } from "./types.js";
const CHESS_ATTR_KEYS: ReadonlySet<ChessAttrKey> = new Set([
"PieceType",
"Color",
"Position",
"HasMoved",
"Turn",
"HalfmoveClock",
"FullmoveNumber",
"EnPassantTarget",
"GameStatus",
"Winner",
"Hp",
"PoisonedSquare",
"HpBonus",
"RangeBonus",
"DirectionAdditions",
"CaptureFlags",
"PromotionOverride",
"DamageResistance",
]);
function isChessAttrKey(attr: string): attr is ChessAttrKey {
return CHESS_ATTR_KEYS.has(attr as ChessAttrKey);
}
const schema = z.object({
attr: z.string(),
value: z.unknown(),
});
type Params = z.infer<typeof schema>;
const descriptor: EffectPrimitive<Params> = {
kind: "seed-attribute",
label: "Seed Attribute",
description: "Seeds a fact on the target piece, overwriting existing value.",
paramsSchema: schema,
apply(ctx: PrimitiveApplyContext, params: Params): void {
if (!isChessAttrKey(params.attr)) {
return;
}
ctx.session.insert(ctx.pieceId, params.attr, params.value);
},
};
PRIMITIVE_REGISTRY.register(descriptor);
export { descriptor as SEED_ATTRIBUTE_PRIMITIVE };

View file

@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { Session } from "@paratype/rete";
import { ChessEngine } from "../../engine.js";
import { CaptureFlag } from "../../schema.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { PrimitiveApplyContext } from "./types.js";
import "./set-capture-flag.js";
import { SET_CAPTURE_FLAG_PRIMITIVE } from "./set-capture-flag.js";
function makeContext(): { ctx: PrimitiveApplyContext; session: Session } {
const session = new Session();
const pieceId = session.nextId();
const ctx: PrimitiveApplyContext = {
engine: new ChessEngine(),
session,
pieceId,
depth: 0,
descriptor: {
id: "custom:test-set-capture-flag",
type: "data",
version: 1,
},
};
return { ctx, session };
}
describe("set-capture-flag primitive — registry", () => {
it("registers in PRIMITIVE_REGISTRY under key 'set-capture-flag'", () => {
expect(PRIMITIVE_REGISTRY.has("set-capture-flag")).toBe(true);
expect(PRIMITIVE_REGISTRY.get("set-capture-flag")).toBe(
SET_CAPTURE_FLAG_PRIMITIVE,
);
});
});
describe("set-capture-flag primitive — apply()", () => {
it("sets a flag from a clean state", () => {
const { ctx, session } = makeContext();
SET_CAPTURE_FLAG_PRIMITIVE.apply(ctx, {
flag: CaptureFlag.CANNOT_BE_CAPTURED,
});
expect(session.get(ctx.pieceId, "CaptureFlags")).toBe(
CaptureFlag.CANNOT_BE_CAPTURED,
);
});
it("preserves existing flags when adding a new one", () => {
const { ctx, session } = makeContext();
session.insert(ctx.pieceId, "CaptureFlags", CaptureFlag.CAN_CAPTURE_OWN);
SET_CAPTURE_FLAG_PRIMITIVE.apply(ctx, {
flag: CaptureFlag.EN_PASSANT,
});
expect(session.get(ctx.pieceId, "CaptureFlags")).toBe(
CaptureFlag.CAN_CAPTURE_OWN | CaptureFlag.EN_PASSANT,
);
});
it("is idempotent when setting the same flag twice", () => {
const { ctx, session } = makeContext();
SET_CAPTURE_FLAG_PRIMITIVE.apply(ctx, {
flag: CaptureFlag.CAN_CAPTURE_OWN,
});
SET_CAPTURE_FLAG_PRIMITIVE.apply(ctx, {
flag: CaptureFlag.CAN_CAPTURE_OWN,
});
expect(session.get(ctx.pieceId, "CaptureFlags")).toBe(
CaptureFlag.CAN_CAPTURE_OWN,
);
});
});

View file

@ -0,0 +1,36 @@
import { z } from "zod";
import {
CaptureFlag,
type CaptureFlag as CaptureFlagValue,
} from "../../schema.js";
import { PRIMITIVE_REGISTRY } from "./registry.js";
import type { EffectPrimitive, PrimitiveApplyContext } from "./types.js";
const schema = z.object({
flag: z.union([
z.literal(CaptureFlag.CAN_CAPTURE_OWN),
z.literal(CaptureFlag.CANNOT_BE_CAPTURED),
z.literal(CaptureFlag.EN_PASSANT),
]),
});
type Params = {
flag: CaptureFlagValue;
};
const descriptor: EffectPrimitive<Params> = {
kind: "set-capture-flag",
label: "Set Capture Flag",
description: "Bitwise-ORs one capture flag into CaptureFlags.",
paramsSchema: schema,
apply(ctx: PrimitiveApplyContext, params: Params): void {
const existing = ctx.session.get(ctx.pieceId, "CaptureFlags");
const baseFlags = existing === undefined ? 0 : existing;
if (typeof baseFlags !== "number" || Number.isNaN(baseFlags)) {
throw new Error("set-capture-flag expected CaptureFlags to be numeric");
}
ctx.session.insert(ctx.pieceId, "CaptureFlags", baseFlags | params.flag);
},
};
PRIMITIVE_REGISTRY.register(descriptor);
export { descriptor as SET_CAPTURE_FLAG_PRIMITIVE };

View file

@ -7,12 +7,24 @@
* e.g. a1=0, h1=7, a8=56, h8=63, e4=28
*/
import type { EntityId } from "@paratype/rete";
import type { EffectPrimitiveNode } from "./modifiers/primitives/types.js";
export type PieceType = "pawn" | "knight" | "bishop" | "rook" | "queen" | "king";
export type PieceColor = "white" | "black";
export type MoveType = "capture" | "step" | "slide";
export type Square = number; // 0..63
export type GameStatus = "active" | "checkmate" | "stalemate" | "draw";
export type GameResult = PieceColor | "draw";
export type ConditionSpec =
| { readonly type: "attr-lt"; readonly attr: string; readonly value: number }
| { readonly type: "attr-gt"; readonly attr: string; readonly value: number }
| {
readonly type: "attr-eq";
readonly attr: string;
readonly value: string | number | boolean | null;
}
| { readonly type: "always" }
| { readonly type: "never" };
/** EntityId reserved for game-level facts (Turn, HalfmoveClock, etc.). */
export const GAME_ENTITY: EntityId = 0 as EntityId;
@ -56,6 +68,23 @@ export interface ChessAttrMap {
CaptureFlags: number;
PromotionOverride: PieceType | "disabled";
DamageResistance: number;
AbsorbDamageAttr: string;
AbsorbDamageRate: number;
ReflectDamagePercent: number;
BlockedMoveTypes: readonly MoveType[];
AuraSpec: readonly {
readonly radius: number;
readonly targetAttr: string;
readonly delta: number;
}[];
OnTurnStartHooks: readonly EffectPrimitiveNode[][];
OnCaptureHooks: readonly EffectPrimitiveNode[][];
OnDamagedHooks: readonly EffectPrimitiveNode[][];
ConditionalHooks: readonly {
readonly condition: ConditionSpec;
readonly then: readonly EffectPrimitiveNode[];
readonly else?: readonly EffectPrimitiveNode[];
}[];
}
export type ChessAttrKey = keyof ChessAttrMap;