Files
gogobee/gogobee_class_balance.md
prosolis ddfa89e7a7 D&D: class-balance Phase 1 — full 10×30 measurement matrix
Generalizes the Phase 0 spike harness to the full build matrix the
class-balance doc plans for. No tuning yet — just measurement.

- classBalanceProfile gains Subclass; buildHarnessCharacter sets it on
  the synthetic DnDCharacter; buildHarnessPlayer now calls
  applySubclassPassives after class+race passives, matching live order
  (combat_bridge.go, combat_session_build.go). Subclass="" is a no-op,
  so L1–L4 pre-unlock rows are unaffected.
- buildPhase1Profiles yields 190 rows: 10 classes × 4 pre-subclass
  levels (L1–L4) + 10 classes × 3 subclasses × 5 post-unlock checkpoints
  (L5/7/10/15/20). Order is registry order so output reads like the
  design doc / !class help.
- TestClassBalance_Phase1_FullMatrix runs the matrix at 200 trials/cell
  (~5.5s) and logs every cell plus a per-class tier-mean summary with
  min/max range. Only harness-broken pathologies fail the test (0% at
  T1 anywhere, or 100% at T5 for an L1 build); per-tier parity bands
  land in Phase 2 once we have data to calibrate the tolerance.

Phase-2 baseline from this run: at T4 the cross-class spread of mean
win rate runs Bard 0.62 → Fighter 0.80 (~18pp); at T5 0.48 → 0.64
(~16pp); casters trail martials at the post-unlock tier (T3) by ~20pp.

Phase 0 test (TestClassBalance_Phase0_FighterVsMage) still green with
identical numbers — the additional applySubclassPassives call is a
no-op for Subclass=="".
2026-05-14 20:00:00 -07:00

114 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GogoBee — D&D Class Balance Pass
> **Companion to:** `gogobee_dnd_design_doc_v1.1.md`, `gogobee_spell_system.md`, `gogobee_subclass_system.md`
> **Version:** 0.1 (planning draft)
> **Status:** Pre-implementation — Phase 0 spike in progress
> **Sibling work:** the race-balance pass (`internal/plugin/dnd_race_balance.go`, `TestRaceBalance`) — same spirit, different method (see §1).
---
## 0. Why this doc exists
The race-balance pass tuned the seven races to equal *effective power* via a
hand-weighted scoring model. That model is a **proxy** — races don't fight, so
there is nothing to measure directly.
Classes are different: combat is **one-shot auto-resolved** through a single
`simulateCombatWithRNG` call (`combat_engine.go:338`), and the RNG is
**seedable**. So class balance does not need a proxy model — we can run
thousands of real, reproducible fights and read the win rates directly.
This doc plans that work. Endgame: a `TestClassBalance` regression fixture that
holds per-tier win-rate parity across all 10 classes × 30 subclasses, plus the
tuning pass that gets us there.
**Non-goals.** Not a combat-engine redesign. Not adding classes/subclasses.
"Balanced" means balanced *in gogobee's one-shot engine* — the model collapses
tabletop action economy, and that is accepted.
---
## 1. Method — measure, don't model
| | Race pass | Class pass |
|---|---|---|
| Power source | ability mods only | full combat: HP/AC, weapons, spells, passives, subclass tiers |
| Approach | hand-weighted scoring proxy | Monte Carlo over the real engine |
| Trust | heuristic weights (estimated) | empirical win rates (measured) |
| Artifact | `dnd_race_balance.go` + `TestRaceBalance` | `dnd_class_balance.go` + `TestClassBalance` |
---
## 2. The matrix (full)
For each of the 10 classes:
- `subclass = none` at L1L4
- each of its 5 subclasses at the L5 / L7 / L10 / L15 / L20 tier-unlock checkpoints
→ ~60 build profiles × level checkpoints × ~6 monster tiers. Each cell = N
seeded fights through `simulateCombatWithRNG`. One-shot sims are cheap; compute
is not the constraint.
**Opponents:** the existing per-tier monster scaling curves
(`dnd_combat.go:3551`) — dungeon Tier and arena ThreatLevel — used as a fixed
gauntlet.
**Builds:** standard array assigned by each class's stat priority
(`dnd_combat.go:185218`), Human race (neutral baseline, +1 all), plus a
standardized equipment loadout (see §3).
---
## 3. The two hard parts (Phase 0 de-risks these)
1. **Equipment loadout policy.** Combat uses equipment-driven AC and real weapon
dice. The per-class, per-level kit must be standardized *fairly* or every
downstream number is garbage. Derive from existing starting-gear / level
tables if they exist; otherwise define a canonical tiered kit.
2. **Spell-selection policy.** Caster damage flows through `applyPendingCast`.
Without a "what would a player cast here" heuristic per class/level, all 8
casters fight as naked weapon-users and read as broken — a measurement
artifact, not real imbalance.
---
## 4. Balance rule
Per-tier **win-rate parity**: at each difficulty tier, every class within a
tolerance band of the cross-class mean win rate. Band set empirically after the
first full run (the race pass's ±0.5 was set the same way). Damage-dealt,
HP-remaining, and near-death-rate are logged as diagnostics, not asserted.
---
## 5. Phases (one phase ≈ one session; each ends green + committed)
- **Phase 0 — spike.** Harness skeleton; equipment + spell-selection policies;
run *Fighter vs. Mage only* across tiers and sanity-check plausibility
(both win something; casters not at 0%). If the numbers are implausible, fix
the policies before trusting anything. **Done — commit 0878b4e.**
- **Phase 1 — harness + matrix.** Generalize to all 10 classes × 30 subclasses;
`TestClassBalance` logs the full report. No tuning yet — just measurement.
**Done.** Subclass field plumbed through the harness, `applySubclassPassives`
wired in to match live combat order, `buildPhase1Profiles` produces 190 rows
(10 × 4 pre-subclass + 10 × 3 × 5 post-subclass), `TestClassBalance_Phase1_FullMatrix`
logs the cells plus per-class tier means and ranges. Phase-2 calibration baseline:
at T4 the cross-class spread of *mean* win rate runs Bard 0.62 → Fighter 0.80
(~18pp); at T5 0.48 → 0.64 (~16pp); casters trail martials at the post-unlock
tier (T3) by ~20pp. **← current**
- **Phase 2 — tuning pass.** Adjust the levers (class passives → subclass tiers
→ spell dice → AC floor → attack bonus, in that order) until the parity band
holds. Lock the band into the test assertion.
- **Phase 3 (if needed) — second-order.** Subclass-vs-subclass spread within a
class; the Paladin/Rogue MAD question if the data shows it bites.
## 6. Tuning levers (priority order)
1. Class passives — `dnd_passives.go`
2. Subclass L5/7/10/15 tiers — `dnd_subclass_combat.go`
3. Spell damage dice & upcast — `dnd_spells_data.go`, `dnd_spells_srd_data.go`
4. Spell slot progression — `dnd_spells.go`
5. AC floor / armor proficiency — `dnd.go:164178`
6. Attack bonus class matrix — `dnd_combat.go:2233`
7. Monster scaling curves — `dnd_combat.go:3551` (last resort; affects PvE feel)