feat(engine): primitive types and registry
T3 Wave 1 (T2). Lays the type-system foundation that the 15 effect
primitives in Wave 2 will conform to.
- PrimitiveKind: discriminated literal of all 15 T3 primitive ids (ADR-2).
- EffectPrimitive<Params>: descriptor contract with paramsSchema (Zod),
apply(ctx, params), and optional childPrimitives() for nested-tree
walking by the validator.
- EffectPrimitiveNode: runtime instance shape — kind + opaque params.
- PrimitiveApplyContext: { engine, session, pieceId, depth, descriptor } —
depth threads through for the recursion cap (ADR-3).
- CustomModifierDescriptorRef: forward-declared trunk so primitives
doesn't import custom (one-way import graph; full descriptor lives in
custom/types.ts).
- PrimitiveRegistryClass + PRIMITIVE_REGISTRY singleton mirroring the
MODIFIER_REGISTRY pattern (Map<kind, descriptor>, throws on duplicate,
list() preserves registration order).
Side-effect registration of individual primitives lands in Wave 2.
This commit is contained in:
parent
60b89d8c5e
commit
2e655a0c1a
4 changed files with 237 additions and 0 deletions
11
packages/chess/src/modifiers/primitives/index.ts
Normal file
11
packages/chess/src/modifiers/primitives/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export { PrimitiveRegistryClass, PRIMITIVE_REGISTRY } from "./registry.js";
|
||||||
|
export type {
|
||||||
|
PrimitiveKind,
|
||||||
|
EffectPrimitive,
|
||||||
|
EffectPrimitiveNode,
|
||||||
|
PrimitiveApplyContext,
|
||||||
|
CustomModifierDescriptorRef,
|
||||||
|
Session,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
// Side-effect registration of individual primitives is added by Wave 2 (T4-T18).
|
||||||
125
packages/chess/src/modifiers/primitives/registry.test.ts
Normal file
125
packages/chess/src/modifiers/primitives/registry.test.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
PrimitiveRegistryClass,
|
||||||
|
type EffectPrimitive,
|
||||||
|
type PrimitiveKind,
|
||||||
|
} from "./index.js";
|
||||||
|
|
||||||
|
function primitive<P>(args: {
|
||||||
|
kind: PrimitiveKind;
|
||||||
|
label?: string;
|
||||||
|
paramsSchema: EffectPrimitive<P>["paramsSchema"];
|
||||||
|
}): EffectPrimitive<P> {
|
||||||
|
return {
|
||||||
|
kind: args.kind,
|
||||||
|
label: args.label ?? args.kind,
|
||||||
|
description: `${args.kind} primitive`,
|
||||||
|
paramsSchema: args.paramsSchema,
|
||||||
|
apply: () => {
|
||||||
|
// no-op in registry tests
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PrimitiveRegistryClass", () => {
|
||||||
|
it("register() + get() round-trip returns the same descriptor", () => {
|
||||||
|
const registry = new PrimitiveRegistryClass();
|
||||||
|
const descriptor = primitive({
|
||||||
|
kind: "seed-attribute",
|
||||||
|
paramsSchema: z.object({ key: z.string(), value: z.number() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.register(descriptor);
|
||||||
|
|
||||||
|
expect(registry.get("seed-attribute")).toBe(descriptor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("register() throws on duplicate primitive kind", () => {
|
||||||
|
const registry = new PrimitiveRegistryClass();
|
||||||
|
registry.register(
|
||||||
|
primitive({
|
||||||
|
kind: "add-to-attribute",
|
||||||
|
paramsSchema: z.object({ key: z.string(), delta: z.number() }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
registry.register(
|
||||||
|
primitive({
|
||||||
|
kind: "add-to-attribute",
|
||||||
|
paramsSchema: z.object({ key: z.string(), delta: z.number() }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}).toThrow(/add-to-attribute/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list() preserves registration order", () => {
|
||||||
|
const registry = new PrimitiveRegistryClass();
|
||||||
|
const first = primitive({
|
||||||
|
kind: "set-capture-flag",
|
||||||
|
paramsSchema: z.object({ flag: z.string() }),
|
||||||
|
});
|
||||||
|
const second = primitive({
|
||||||
|
kind: "reflect-damage",
|
||||||
|
paramsSchema: z.object({ ratio: z.number() }),
|
||||||
|
});
|
||||||
|
const third = primitive({
|
||||||
|
kind: "override-promotion",
|
||||||
|
paramsSchema: z.object({ to: z.string() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.register(first);
|
||||||
|
registry.register(second);
|
||||||
|
registry.register(third);
|
||||||
|
|
||||||
|
expect(registry.list()).toEqual([first, second, third]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has() is false before registration and true after", () => {
|
||||||
|
const registry = new PrimitiveRegistryClass();
|
||||||
|
|
||||||
|
expect(registry.has("add-direction")).toBe(false);
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
primitive({
|
||||||
|
kind: "add-direction",
|
||||||
|
paramsSchema: z.object({ direction: z.string() }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(registry.has("add-direction")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("get() returns undefined for an unregistered known kind", () => {
|
||||||
|
const registry = new PrimitiveRegistryClass();
|
||||||
|
|
||||||
|
expect(registry.get("conditional")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generic params type is preserved at register call site", () => {
|
||||||
|
const registry = new PrimitiveRegistryClass();
|
||||||
|
|
||||||
|
const typed = primitive({
|
||||||
|
kind: "modify-movement-range",
|
||||||
|
paramsSchema: z.object({
|
||||||
|
mode: z.literal("set"),
|
||||||
|
value: z.number(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function registerAndReturn<P>(
|
||||||
|
localRegistry: PrimitiveRegistryClass,
|
||||||
|
descriptor: EffectPrimitive<P>,
|
||||||
|
): EffectPrimitive<P> {
|
||||||
|
localRegistry.register(descriptor);
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registered = registerAndReturn(registry, typed);
|
||||||
|
const parsed = registered.paramsSchema.parse({ mode: "set", value: 3 });
|
||||||
|
|
||||||
|
expect(parsed.value).toBe(3);
|
||||||
|
expect(registry.get("modify-movement-range")).toBe(typed);
|
||||||
|
});
|
||||||
|
});
|
||||||
30
packages/chess/src/modifiers/primitives/registry.ts
Normal file
30
packages/chess/src/modifiers/primitives/registry.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type { EffectPrimitive, PrimitiveKind } from "./types.js";
|
||||||
|
|
||||||
|
class PrimitiveRegistryClass {
|
||||||
|
readonly #byId = new Map<PrimitiveKind, EffectPrimitive>();
|
||||||
|
|
||||||
|
register<P>(primitive: EffectPrimitive<P>): void {
|
||||||
|
if (this.#byId.has(primitive.kind)) {
|
||||||
|
throw new Error(
|
||||||
|
`PrimitiveRegistry: duplicate primitive kind "${primitive.kind}". ` +
|
||||||
|
`Each primitive descriptor must have a unique kind.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.#byId.set(primitive.kind, primitive as EffectPrimitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(kind: PrimitiveKind): EffectPrimitive | undefined {
|
||||||
|
return this.#byId.get(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(): readonly EffectPrimitive[] {
|
||||||
|
return [...this.#byId.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
has(kind: PrimitiveKind): boolean {
|
||||||
|
return this.#byId.has(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PrimitiveRegistryClass };
|
||||||
|
export const PRIMITIVE_REGISTRY = new PrimitiveRegistryClass();
|
||||||
71
packages/chess/src/modifiers/primitives/types.ts
Normal file
71
packages/chess/src/modifiers/primitives/types.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import type { EntityId, Session } from "@paratype/rete";
|
||||||
|
import type { ZodType } from "zod";
|
||||||
|
import type { ChessEngine } from "../../engine.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* T3 primitive ids (ADR-2).
|
||||||
|
*/
|
||||||
|
export type PrimitiveKind =
|
||||||
|
| "seed-attribute"
|
||||||
|
| "add-to-attribute"
|
||||||
|
| "multiply-attribute"
|
||||||
|
| "add-direction"
|
||||||
|
| "set-capture-flag"
|
||||||
|
| "absorb-damage-with-attribute"
|
||||||
|
| "reflect-damage"
|
||||||
|
| "block-move-type"
|
||||||
|
| "modify-movement-range"
|
||||||
|
| "override-promotion"
|
||||||
|
| "add-aura"
|
||||||
|
| "on-turn-start"
|
||||||
|
| "on-capture"
|
||||||
|
| "on-damaged"
|
||||||
|
| "conditional";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward-declared shape of the back-reference passed to primitive
|
||||||
|
* `apply()` calls. The full descriptor lives in `../custom/types.js`
|
||||||
|
* — declaring only the trunk here keeps the import graph one-way
|
||||||
|
* (custom imports primitives, never the reverse).
|
||||||
|
*
|
||||||
|
* Adding a structurally-compatible field here is fine, but the
|
||||||
|
* canonical shape is owned by `../custom/types.ts`.
|
||||||
|
*/
|
||||||
|
export interface CustomModifierDescriptorRef {
|
||||||
|
readonly id: string;
|
||||||
|
readonly type: "data";
|
||||||
|
readonly version: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runtime primitive node embedded in custom descriptor trees. */
|
||||||
|
export interface EffectPrimitiveNode {
|
||||||
|
readonly kind: PrimitiveKind;
|
||||||
|
readonly params: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Context passed to primitive apply functions. */
|
||||||
|
export interface PrimitiveApplyContext {
|
||||||
|
readonly engine: ChessEngine;
|
||||||
|
readonly session: Session;
|
||||||
|
readonly pieceId: EntityId;
|
||||||
|
readonly depth: number;
|
||||||
|
readonly descriptor: CustomModifierDescriptorRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primitive descriptor contract implemented by each T3 primitive.
|
||||||
|
*
|
||||||
|
* `childPrimitives` is provided by primitives that own nested primitive lists
|
||||||
|
* (e.g. trigger/conditional primitives) so validators can walk the tree.
|
||||||
|
*/
|
||||||
|
export interface EffectPrimitive<Params = unknown> {
|
||||||
|
readonly kind: PrimitiveKind;
|
||||||
|
readonly label: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly paramsSchema: ZodType<Params>;
|
||||||
|
readonly apply: (ctx: PrimitiveApplyContext, params: Params) => void;
|
||||||
|
readonly maxDepth?: number;
|
||||||
|
readonly childPrimitives?: (params: Params) => EffectPrimitiveNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Session };
|
||||||
Loading…
Add table
Add a link
Reference in a new issue