**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. **The example below shows the complete domain shape — not what to implement per story.** Implement only what a property forces you to write.

```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<Faction>,
  ) {}

  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** — when a property reveals that a bare `number` or `string` is insufficient, introduce a value object. Don't anticipate — react.
- **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

**YAGNI discipline:** Write only the minimum code necessary to make the current property pass.

Before writing a method or class, ask:

1. **Does a story property require this?** If no → don't write it.
2. **Does this touch a concept from a different story?** If yes → it's scope creep.
3. **Am I implementing this because it feels useful, not because a property forces it?** If yes → stop.

> **The litmus test:** If you can't point to a failing property that demands this code, don't write it. Future stories will reveal what abstractions are actually needed — and they'll look different than you expect.

### What to skip per story (common traps)

| Story        | Don't implement                  | Belongs to                    |
| ------------ | -------------------------------- | ----------------------------- |
| 1 (Creation) | `isAllyOf`, `isAlive`, `isDead`  | Stories 3+                    |
| 1 (Creation) | `Health.add()`, `Health.isMax()` | Stories 3/4                   |
| 1 (Creation) | `Level.next()`, `Level.diff()`   | Story 5                       |
| 2 (Damage)   | `Damage` value object            | Only if a property demands it |
| 2 (Damage)   | Level modifier (±50%)            | Story 3                       |
| 2 (Damage)   | Faction/ally checks              | Story 3                       |
| 3 (Levels)   | MagicalObjects                   | Story 4                       |
| 3 (Levels)   | Level-up tracking                | Story 5                       |
| 4 (Objects)  | `joinFaction` / `leaveFaction`   | Story 3                       |

### 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
