From fea511790b3406f572f3ab01c4a5ad8efd001945 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 18 Apr 2026 22:05:58 -0600 Subject: [PATCH] feat(engine): modifier profile types --- packages/chess/package.json | 3 +- packages/chess/src/modifiers/types.ts | 148 ++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 packages/chess/src/modifiers/types.ts diff --git a/packages/chess/package.json b/packages/chess/package.json index 362fcae..561568d 100644 --- a/packages/chess/package.json +++ b/packages/chess/package.json @@ -21,7 +21,8 @@ "motion": "^12.38.0", "react-router-dom": "^7.14.1", "sonner": "^2.0.7", - "tailwindcss": "^4.2.2" + "tailwindcss": "^4.2.2", + "zod": "^4.3.6" }, "devDependencies": { "@types/canvas-confetti": "^1.9.0", diff --git a/packages/chess/src/modifiers/types.ts b/packages/chess/src/modifiers/types.ts new file mode 100644 index 0000000..ffdeb0c --- /dev/null +++ b/packages/chess/src/modifiers/types.ts @@ -0,0 +1,148 @@ +/** + * Type definitions for the piece modifier profile system. + * + * A ModifierProfile is a first-class entity (orthogonal to layouts and + * presets) that attaches rule modifiers to pieces by type ("all white + * knights have +1 HP") or by layout slot ("the piece on b1 has +2 range"). + * + * Modifier profiles combine with layouts at game start. They are saved + * independently to a library (houserules:modifier-profiles:v1) and can be + * hot-swapped mid-game at turn boundaries. + * + * ADR-2: per-instance modifiers keyed by algebraic-notation square string + * (e.g. "b1"). Profile may optionally specify a layoutId for validation. + */ +import type { EntityId, Session } from "@paratype/rete"; +import type { PieceType, PieceColor, ChessAttrKey } from "../schema.js"; +import type { ZodType } from "zod"; + +/** The six T1 modifier category identifiers. */ +export type ModifierKindId = + | "hp-bonus" + | "range-bonus" + | "direction-additions" + | "capture-flags" + | "promotion-override" + | "damage-resistance"; + +/** + * Named movement directions (from the perspective of the moving piece, + * color-independent — engine interprets "forward" as toward the opponent's + * back rank based on piece color). + * + * Used by DirectionAdditions modifier to specify additional movement directions. + */ +export type Direction = + | "forward" // toward opponent's back rank (1 square) + | "backward" // toward own back rank (1 square) + | "left" // queenside (from white's perspective) + | "right" // kingside (from white's perspective) + | "diagonal-fl" // forward-left + | "diagonal-fr" // forward-right + | "diagonal-bl" // backward-left + | "diagonal-br"; // backward-right + +/** + * A modifier that applies to ALL pieces of a given type+color combo. + * `value` is the modifier-kind-specific value (number, string[], etc.) + */ +export interface TypeModifier { + readonly kind: ModifierKindId; + readonly pieceType: PieceType; + readonly color: PieceColor | "both"; + readonly value: unknown; +} + +/** + * A modifier that applies to the piece at a specific layout square. + * `square` is algebraic notation: "a1" through "h8". + * ADR-2: keyed by layout-slot (square string), not EntityId. + */ +export interface InstanceModifier { + readonly kind: ModifierKindId; + readonly square: string; // algebraic notation, e.g. "b1" + readonly value: unknown; +} + +/** + * A complete modifier profile. + * + * Combines with a layout at game start: perType modifiers apply to all + * matching pieces; perInstance modifiers apply to pieces at specific squares. + * + * `layoutId` is optional — only required when perInstance entries are + * present, as per-instance modifiers are layout-slot-bound (ADR-2). + * When layoutId is absent and perInstance is non-empty, the validator + * emits a warning. + */ +export interface ModifierProfile { + readonly id: string; + readonly name: string; + readonly description: string; + /** Layout this profile's per-instance modifiers are bound to. Optional. */ + readonly layoutId?: string; + readonly perType: readonly TypeModifier[]; + readonly perInstance: readonly InstanceModifier[]; + readonly version: 1; + readonly source: "premade" | "custom"; +} + +/** + * Descriptor for a modifier category. Each T1 modifier registers one + * descriptor into MODIFIER_REGISTRY at module load time (ADR-8). + * + * `V` is the value type (number for HpBonus, string[] for DirectionAdditions, etc.) + */ +export interface ModifierDescriptor { + /** Matches ModifierKindId — used as the registry key. */ + readonly id: ModifierKindId; + /** The ChessAttrMap key this modifier seeds on piece entities. */ + readonly attrName: ChessAttrKey; + /** Human-readable display label for UI. */ + readonly label: string; + /** Zod schema for the value field — used for validation and UI form generation. */ + readonly valueSchema: ZodType; + /** How multiple values from different sources stack (ADR-4). */ + readonly stackingRule: "additive" | "union" | "multiplicative" | "priority-wins"; + /** + * Apply the modifier's effective value to a piece entity in the session. + * Called once per (pieceId, kind) pair after all sources have been + * collected and the effective value computed via stacking rules. + * + * @param session - The game session to mutate + * @param pieceId - Target piece entity + * @param effectiveValue - The already-stacked value to apply + */ + readonly apply: (session: Session, pieceId: EntityId, effectiveValue: V) => void; + /** Return a human-readable description of this modifier value for UI. */ + readonly describe: (value: V) => string; + /** + * Hint for the UI editor about which input widget to render for this modifier. + * Each uiForm maps to a specific React component in the PerTypePanel/PerInstancePanel. + */ + readonly uiForm: + | "number" + | "direction-set" + | "capture-flags" + | "promotion-target" + | "percentage"; +} + +/** Helper: create a TypeModifier with proper typing. */ +export function typeModifier( + kind: ModifierKindId, + pieceType: PieceType, + color: PieceColor | "both", + value: unknown, +): TypeModifier { + return { kind, pieceType, color, value }; +} + +/** Helper: create an InstanceModifier with proper typing. */ +export function instanceModifier( + kind: ModifierKindId, + square: string, + value: unknown, +): InstanceModifier { + return { kind, square, value }; +}