# Balancing Process

How balancing is done in this project: the pass order, the tuning
discipline, and the tools. Refer to this when starting the next
balancing round.

## The pass order

Each pass depends on the ones before it; a change in an earlier pass
invalidates the later ones (but not vice versa). Redo from the earliest
pass whose inputs changed.

1. **Targets** (`targets.md`) — choose the root numbers: run shape,
   factory curve, threat-cost ladder, fleet size, block roots, combat
   anchors, pacing anchors. These are design decisions, not
   measurements. Everything else is derived from them.
2. **Tree structure** (`../content_design.md`) — items, chains,
   what-consumes-what, per the production tree rules (one input per
   phase transition, generic parts, archetypes, refactorability).
   Structure only, no quantities.
3. **Numbers** (`derived.md`, recipes/materials in the configs) —
   quantities and durations so every fitted ship sums to its ladder
   value, the ratio curve is realized, and the belt/buffer guardrails
   hold. Verified computationally by `tools/threat_report.py`.
4. **Calculator/tooling parity** — the game's `ThreatCostCalculator`
   and `tools/threat_report.py` must produce identical values; the
   Python tool is the design reference. Any semantic change to
   REQ-THREAT-* needs both updated plus tests.
5. **Combat stats** (arena-driven) — derive stats from the combat
   anchors, then iterate against the arena suite
   (`bin/balancing/data/balancing.toml`) until equal-threat matchups are
   near-draws. Threat costs are stat-independent, so arena ship counts
   stay valid across stat changes.
6. **Pacing** — unlock ladder, `unlock_requires` edges, threat rate,
   block/artifact/expansion values, per the pacing anchors.

Then: **full-game playtests**, which are the only check for the pacing
pass and feed back into targets.

## Tuning discipline (learned in arena rounds 1–5)

- **Change anchors, not symptoms.** When a class of results is off,
  adjust the anchor that explains all of them (e.g. the concentration
  tax) rather than individual stats.
- **Fewest knobs per round.** Attribution dies when many knobs move at
  once. Prefer one anchor change plus its mechanical compensations.
- **Shared vs. local knobs.** Guns and module stats are shared across
  many hulls — changing them moves many matchups. Per-hull HP moves
  exactly one matchup; it is the designated per-ship trim knob on top of
  the HP-per-threat prior.
- **Mind the ride-alongs.** A module buff lands on every default loadout
  containing it (e.g. an armor buff strengthens the destroyer swarm that
  opposes the dreadnought). Compute the net effect per matchup before
  choosing step sizes.
- **Two-round signal policy.** Single arena runs re-roll by ~±10% EHP
  margin; a margin inside ±20% counts as converged for v1. Only act on
  signals that persist across two rounds.
- **Arena geometry is part of the fixture.** Lane width/height changes
  the results (full engagement vs. fleets slipping past); margins are
  only comparable within the same geometry.
- **Accept mechanical texture.** Not every deviation is a bug: a margin
  that survives a stat change is mechanical (usually range/kiting under
  the orbit AI) and may be desirable doctrine texture. Document the
  acceptance in `targets.md` instead of chasing it.
- **Range is the strongest stat** under the orbit AI — free approach
  fire. Price range modifiers conservatively; station dominance is
  controlled via range, not HP.

## Tools

- `tools/threat_report.py` — item threats, module contributions,
  hull/fitted ship threats, producer:consumer ratios, belt feasibility;
  reads the real configs. The design reference for threat semantics.
- `tools/verify_recipes.py` — recipe tree closure, visuals coverage,
  orphans, reprocessing-only items.
- `tools/verify_layouts.py` — module footprint gating matrix per hull.
- **Balancing tool** (`balancing` target) — parallel arena simulation of
  `bin/balancing/data/balancing.toml`; logs winner, surviving counts,
  team EHP %, and fight duration per arena. The suite covers: class
  mirrors (expect near-mutual annihilation, symmetric winners),
  equal-threat cross-tier matchups (expect near-draws — power-per-threat
  made empirical), a 2:1 decisiveness check, doctrine matchups
  (armored-vs-glass, repair-escort), and station assault.

## Checklist for the next balancing round

1. Pull; run `verify_recipes.py`, `verify_layouts.py`,
   `threat_report.py`; compare against the tables in `derived.md`.
2. If recipes/materials changed: re-check fitted threats vs. the ladder
   in `targets.md`; update arena suite ship counts if fitted values
   moved.
3. Run the arena suite; read EHP margins and durations against the
   expectations noted in `balancing.toml` and the anchors.
4. Apply changes per the tuning discipline (two-round signals only);
   record the round and its knob changes in `history.md`.
5. Update `derived.md` where values moved; if an anchor moved, update
   `targets.md` and state why.
6. Commit and push (the review workflow reads the remote).
