Files
gogobee/gogobee_class_balance.md
prosolis 76f814c0c9 D&D: class-balance Phase 2 — passive + L5 subclass tuning + in-tier parity assertion
The Phase 1 per-class-mean summary was hiding the truth — most cells are
floor/ceiling-saturated (L10+ pinned at 1.0, L1-4 caster cells at high
tier pinned at 0.0), so means barely budge when you tune passives. Added
a per-(level, tier) cross-class spread diagnostic to the matrix log,
then tuned with the levers from doc §6 in priority order:

1. Class passives (dnd_passives.go) — caster trailers (Bard, Mage,
   Warlock, Sorcerer) gained level + casting-stat-scaled FlatDmgStart
   bursts so the L1-4 chassis isn't a quarterstaff + one weak spell
   against a T2-T3 monster; small DamageBonus riders (Mage/Bard/
   Sorcerer/Rogue +5%, Warlock 10→12%) and +1 attack for Bard/Warlock
   close the steady-DPS gap. Added clampNonNeg so ability-mod-scaled
   additions never go negative on sub-10-stat sheets.

2. Subclass L5 tiers (dnd_subclass_combat.go) — the three Sorcerer L5
   picks (Wild/Storm/Draconic) and Warlock Great Old One were defense-
   only or near-inert pre-tune; each gained a small bite (DamageBonus
   +0.10, or a FlatDmgStart burst for Storm) so the L5 chassis can press
   through a T4 monster.

Parity band locked in TestClassBalance_Phase1_FullMatrix: cross-class
spread ≤ 35pp on the in-tier diagonal — (level, tier) cells where the
level is appropriate for the tier (T1: L1-4, T2: L3-7, T3: L5-10, T4:
L7-15, T5: L10-20). Off-tier cells (L1 mage at T3 dungeon etc.) are
still logged but not asserted: those are level-vs-tier mismatches and
casters at L1-4 can't muscle through a T3 monster on a single L1-slot
spell the way martials muscle through with weapon dice. Worst in-tier
cell after tuning: ~26pp at L3/T2. The 35pp band gives ~9pp Monte-Carlo
headroom over the worst signal at 200 trials/cell.

TestApplyClassPassives expectations updated to match the new passives.
Phase 0 spike still green, full plugin suite (-short) clean.
2026-05-14 20:15:16 -07:00

139 lines
6.9 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.
- **Phase 2 — tuning pass. Done.** First insight: per-class-mean is dominated
by floor+ceiling saturation (L10+ pinned at 1.0; L1-4 caster cells at high
tier pinned at 0.0), masking the actual gaps. The matrix log now also
emits a per-(level, tier) cross-class spread — the cells with real signal.
Tuned levers, in priority order:
- **Class passives** (`dnd_passives.go`): caster trailers (Bard, Mage,
Warlock, Sorcerer) gained level + casting-stat scaled FlatDmgStart bursts
so their L1-4 chassis isn't a quarterstaff + one weak spell against a T2-T3
monster; small DamageBonus riders (Mage/Bard/Sorcerer/Rogue +5%, Warlock
10→12%) and +1 attack for Bard/Warlock close the steady-DPS gap. Added a
`clampNonNeg` helper so ability-mod-scaled additions never go negative on
sheets with sub-10 casting stats.
- **Subclass L5 tiers** (`dnd_subclass_combat.go`): the three Sorcerer
L5 picks (Wild/Storm/Draconic) and Warlock Great Old One were defense-only
or near-inert pre-tune; each gained a DamageBonus +0.10 (or FlatDmgStart
burst for Storm) so the L5 chassis can press through a T4 monster.
**Parity band locked** in `TestClassBalance_Phase1_FullMatrix`: cross-class
spread ≤ 35pp on the *in-tier* diagonal — the (level, tier) cells where the
level is appropriate for the tier (T1: L1-4, T2: L3-7, T3: L5-10, T4: L7-15,
T5: L10-20). Off-tier cells (e.g. L1 mage at T3 dungeon) are still logged but
not asserted: those are level-vs-tier mismatches, and casters at L1-4 can't
muscle through a T3 monster on a single L1-slot spell the way martials muscle
through with weapon dice. Observed worst in-tier cell after tuning: L3/T2 at
~26pp, L1/T1 at ~24pp, L5/T3 at ~20pp; the 35pp band gives ~9pp Monte-Carlo
headroom at 200 trials/cell. **← current**
- **Phase 3 (if needed) — second-order.** Subclass-vs-subclass spread within a
class; the Paladin/Rogue MAD question if the data shows it bites. The L7/T5
cell (47pp spread, *off-tier*) is the most obvious next target if the
underleveled-cell experience needs lifting.
## 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)