**ALWAYS** start replies with ⚔️. ## What this project is An implementation of the RPG Combat rules engine. There are six user stories described in [user-stories.md](user-stories.md). We use a **spec-first, property-based testing** approach. ## Build and Test Scripts - `npm test`: runs unit tests using vitest + fast-check - `npm run lint:fix`: runs eslint with autofix - `npm run format:fix`: runs prettier with autofix - `npm run typecheck`: runs tsc without emit - `npm run checks`: runs the pre-commit gate (format:fix, lint:fix, typecheck, test) ## Allium + fast-check Workflow This project combines three practices: 1. **Allium** (`.allium` specs) — formal behavioural specifications that capture *what* the system does 2. **fast-check** — property-based testing that verifies those properties hold across thousands of random inputs 3. **"I can't believe it's not Haskell"** — TypeScript with ADTs, value objects, and immutability ### Step 1: Spec with Allium Use the Allium skills to formalize user stories into `.allium` specs: - `/skill:elicit` — explore requirements with stakeholders - `/skill:distill` — extract specs from existing code - `/skill:tend` — evolve specs as understanding deepens The spec captures **invariants** (always-true properties) and **rules** (state transitions). These become the source of truth for your properties. ### Step 2: Properties with fast-check Translate Allium invariants and rules into fast-check properties. Each invariant becomes a property: ```typescript import fc from 'fast-check'; import { Character } from './domain'; // Invariant: "A character's health is never negative" fc.property( fc.integer({ min: 0, max: 1000 }), (initialHealth) => { const c = new Character({ name: 'hero', health: initialHealth }); return c.health >= 0; } ); // Property: "Dealing damage reduces health, capped at 0" fc.property( fc.record({ attacker: fc.character(), target: fc.character(), damage: fc.integer({ min: 1, max: 5000 }), }), ({ attacker, target, damage }) => { const a = new Character({ name: attacker, health: 1000 }); const t = new Character({ name: target, health: 1000 }); a.dealDamage(t, damage); return t.health === Math.max(0, 1000 - damage); } ); ``` ### Step 3: "I can't believe it's not Haskell" Use TypeScript's type system to encode domain constraints: ```typescript // ADTs via discriminated unions type Status = { kind: 'alive' } | { kind: 'dead' }; // Value objects with invariants enforced at construction class Health { private constructor(private readonly value: number) {} static create(n: number): Health { if (n < 0) throw new Error('Health cannot be negative'); return new Health(n); } get value() { return this.value; } add(n: number) { return Health.create(this.value + n); } sub(n: number) { return Health.create(Math.max(0, this.value - n)); } } // Immutable entities class Character { constructor( readonly name: string, readonly health: Health, readonly status: Status, readonly level: Level, readonly factions: ReadonlySet, ) {} dealDamage(target: Character, amount: number): void { // ... pure logic, no mutation } } ``` **Key principles:** - **ADTs over classes** — use discriminated unions for state/variants - **Value objects over primitives** — `Health`, `Damage`, `Level` instead of `number` - **Immutability** — no `this.health = ...`, return new instances - **Invariants at boundaries** — constructors enforce invariants, not getters/setters - **Pure functions** — domain logic has no side effects, testable in isolation ### Example: Damage Property From user story: *"When damage received exceeds current Health, Health becomes 0 and the character dies"* ```typescript // Allium invariant (in .allium spec) // invariant HealthNonNegative { for c in Characters: c.health >= 0 } // invariant DeathAtZeroHealth { for c in Characters: c.health = 0 implies c.status = dead } // fast-check property fc.property( fc.integer({ min: 0, max: 10000 }), fc.integer({ min: 0, max: 10000 }), (health, damage) => { const c = new Character({ name: 'goblin', health: Health.create(health) }); c.takeDamage(Damage.create(damage)); return c.health.value === Math.max(0, health - damage); } ).check(/* ... */); ``` ## Skill Invocation Allium skills are available in this project: - `/skill:allium` — entry point and language reference - `/skill:elicit` — explore requirements - `/skill:distill` — extract specs from code - `/skill:propagate` — generate test obligations from specs - `/skill:tend` — evolve specs - `/skill:weed` — check spec-code alignment Domain workflow skill: - `/skill:user-story-conversation` — Card, Conversation, Confirmation workflow with Example Mapping, Allium specs, and fast-check properties