# 0004 — Encounter resolver as pure library with seeded PRNG

- **Status:** Accepted
- **Date:** 2026-05-06

## Context and problem statement

The mission engine resolves an encounter every 60 seconds for each active mission. The resolver takes survivor stats, perk modifiers, group context, and difficulty, then produces an outcome (success, failure, injury, sacrifice) with associated log text.

Where in the architecture should this logic live, and how should randomness be handled?

The choice has cascading implications: testability, replayability for debugging, the feasibility of automated balance testing, and how easily the logic can be exercised in CI without spinning up the full Nest application.

## Decision drivers

- **Testability.** The encounter resolver is the most game-critical and balance-sensitive code in the project. It must be exhaustively testable.
- **Determinism.** When a viewer reports "my mission desynced at tick 7," we need to reproduce the exact roll that occurred. Without determinism, the bug report is useless.
- **Balance validation.** AI-assisted development tends to invent plausible-looking probability numbers that produce degenerate gameplay. We need automated checks that catch this.
- **Pluggability.** Future iterations may want to substitute the resolver (different game modes, A/B tests, content versions).
- **Performance.** A 60-second tick is forgiving, but the resolver shouldn't allocate database connections or perform I/O.

## Considered options

1. **Resolver as a NestJS service.** Lives in the API, dependency-injected, can call other services.
2. **Resolver as a static method on a domain class.** Pure but tightly coupled to specific entity types.
3. **Resolver as a pure function in a shared library, with `Math.random()` for randomness.** Simple, but non-deterministic.
4. **Resolver as a pure function in a shared library, with a seeded PRNG injected as input.** Pure and deterministic.

## Decision outcome

**Chosen: Pure function in `libs/mission-logic`, accepting a seed as input and using a seeded PRNG (`seedrandom`).**

Signature:

```typescript
function resolveEncounter(
  survivor: SurvivorStats,
  perks: PerkModifier[],
  difficulty: Difficulty,
  groupContext: GroupContext | null,
  seed: string
): EncounterResult
```

The seed is generated at the API layer (via cryptographic randomness), passed to the resolver, and persisted in `mission_logs` alongside the result. To replay any tick, fetch its seed and modifiers from the database and call the resolver with identical inputs.

`Math.random()` is forbidden anywhere in `libs/mission-logic`. This is enforced via lint rule and called out as a hard rule in `CLAUDE.md`.

## Consequences

### Positive

- **Property-based testing** with `fast-check` exercises the full input space — empty perks, modifier overflow, negative modifiers, extreme difficulty — in a way that wasn't feasible with stateful or non-pure designs.
- **Monte Carlo balance harness** can simulate 10,000 missions across perk loadouts in CI as a snapshot test, catching balance regressions before merge.
- **Replayable debugging.** Any reported mission desync can be reproduced exactly by replaying the seed.
- **No database or HTTP dependencies in the resolver** means tests run in milliseconds, not seconds.
- **Future SWF (group) logic** layers cleanly on top — the group context is just another input to the same pure function.

### Negative

- **Seed management discipline required.** Every tick must persist its seed, or replay capability is lost. Discoverable, but easy to forget.
- **Slightly more boilerplate at the call site** — the API has to generate and pass a seed, where with `Math.random()` it would be implicit.
- **`Math.random()` in dependencies** can sneak in transitively. The lint rule and code review must guard against this.

### Neutral

- This design also enables **deterministic frontend preview/demo modes** in the future — the overlay could replay a canned mission with a known seed for documentation or marketing.
- Pure-library placement means the resolver can be reused if we ever build a CLI tool, a Discord bot variant, or an offline simulator without Nest.
