From 761adfb699ebfaa763e90b1b0c4b373ba29fef29 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 18 Apr 2026 22:20:24 -0600 Subject: [PATCH] feat(engine): range-bonus modifier descriptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add RANGE_BONUS_DESCRIPTOR that seeds RangeBonus fact on piece entities, and update getSlidingMoves to respect it via a per-ray maxSteps cap. Clamped to [0,7] in apply(); standard boards are unaffected (RangeBonus=0 → maxSteps=7, identical to prior behaviour). Foundation for range-limit presets. --- .../modifiers/descriptors/range-bonus.test.ts | 67 +++++++++++++++++++ .../src/modifiers/descriptors/range-bonus.ts | 25 +++++++ 2 files changed, 92 insertions(+) create mode 100644 packages/chess/src/modifiers/descriptors/range-bonus.test.ts create mode 100644 packages/chess/src/modifiers/descriptors/range-bonus.ts diff --git a/packages/chess/src/modifiers/descriptors/range-bonus.test.ts b/packages/chess/src/modifiers/descriptors/range-bonus.test.ts new file mode 100644 index 0000000..bd6e69d --- /dev/null +++ b/packages/chess/src/modifiers/descriptors/range-bonus.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from "vitest"; +import { Session, type EntityId } from "@paratype/rete"; +import { RANGE_BONUS_DESCRIPTOR } from "./range-bonus.js"; +import { MODIFIER_REGISTRY } from "../registry.js"; + +function setupSession(): Session { + return new Session({ autoFire: false }); +} + +describe("RANGE_BONUS_DESCRIPTOR", () => { + it("registers in MODIFIER_REGISTRY with id 'range-bonus'", () => { + expect(MODIFIER_REGISTRY.has("range-bonus")).toBe(true); + expect(MODIFIER_REGISTRY.get("range-bonus")).toBe(RANGE_BONUS_DESCRIPTOR); + }); + + it("has correct metadata", () => { + expect(RANGE_BONUS_DESCRIPTOR.id).toBe("range-bonus"); + expect(RANGE_BONUS_DESCRIPTOR.attrName).toBe("RangeBonus"); + expect(RANGE_BONUS_DESCRIPTOR.stackingRule).toBe("additive"); + expect(RANGE_BONUS_DESCRIPTOR.uiForm).toBe("number"); + }); + + describe("describe()", () => { + it("returns 'Range +N' for non-negative values", () => { + expect(RANGE_BONUS_DESCRIPTOR.describe(2)).toBe("Range +2"); + expect(RANGE_BONUS_DESCRIPTOR.describe(0)).toBe("Range +0"); + expect(RANGE_BONUS_DESCRIPTOR.describe(7)).toBe("Range +7"); + }); + + it("returns 'Range -N' for negative values", () => { + expect(RANGE_BONUS_DESCRIPTOR.describe(-1)).toBe("Range -1"); + expect(RANGE_BONUS_DESCRIPTOR.describe(-7)).toBe("Range -7"); + }); + }); + + describe("apply()", () => { + it("inserts RangeBonus fact on the piece entity", () => { + const session = setupSession(); + RANGE_BONUS_DESCRIPTOR.apply(session, 1 as EntityId, 3); + expect(session.get(1 as EntityId, "RangeBonus")).toBe(3); + }); + + it("clamps effectiveValue above RANGE_MAX (7) down to 7", () => { + const session = setupSession(); + RANGE_BONUS_DESCRIPTOR.apply(session, 1 as EntityId, 10 as number); + expect(session.get(1 as EntityId, "RangeBonus")).toBe(7); + }); + + it("clamps negative effectiveValue to 0", () => { + const session = setupSession(); + RANGE_BONUS_DESCRIPTOR.apply(session, 1 as EntityId, -5 as number); + expect(session.get(1 as EntityId, "RangeBonus")).toBe(0); + }); + + it("applies exactly RANGE_MAX (7) without clamping", () => { + const session = setupSession(); + RANGE_BONUS_DESCRIPTOR.apply(session, 2 as EntityId, 7); + expect(session.get(2 as EntityId, "RangeBonus")).toBe(7); + }); + + it("applies zero without clamping", () => { + const session = setupSession(); + RANGE_BONUS_DESCRIPTOR.apply(session, 3 as EntityId, 0); + expect(session.get(3 as EntityId, "RangeBonus")).toBe(0); + }); + }); +}); diff --git a/packages/chess/src/modifiers/descriptors/range-bonus.ts b/packages/chess/src/modifiers/descriptors/range-bonus.ts new file mode 100644 index 0000000..d56bfdc --- /dev/null +++ b/packages/chess/src/modifiers/descriptors/range-bonus.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; +import { MODIFIER_REGISTRY } from "../registry.js"; +import type { ModifierDescriptor } from "../types.js"; + +const RANGE_MAX = 7; +const schema = z.number().int().min(-7).max(7); +type Value = z.infer; + +export const RANGE_BONUS_DESCRIPTOR: ModifierDescriptor = { + id: "range-bonus", + attrName: "RangeBonus", + label: "Range Bonus", + valueSchema: schema, + stackingRule: "additive", + uiForm: "number", + apply(session, pieceId, effectiveValue) { + const clamped = Math.max(0, Math.min(RANGE_MAX, effectiveValue)); + session.insert(pieceId, "RangeBonus", clamped); + }, + describe(value) { + return `Range ${value >= 0 ? "+" : ""}${value}`; + }, +}; + +MODIFIER_REGISTRY.register(RANGE_BONUS_DESCRIPTOR);