Files
gogobee/gogobee_expedition_difficulty.md
prosolis 5ef10e35dc Phase 5b: player power floor + Phase-3 winners shipped to live
Closes the 'fairly breezy with some death' target the user picked
for Phase 5. Five-piece ship; Phase 1 matrix lands T1 88%, T2 74%,
T4 72%, T5 ~57% in or above band. T3 remains the design hump at
~45% (manor 39, underforge 47) — Wraith promotion to elite was
already done in Phase 4-B, the remaining standard-pool deaths are
the irreducible part of T3.

Pieces:
  1. computeMaxHP × 1.5 (phase5BHPMult in dnd.go). Uniform across
     class/level so the class-balance harness's in-tier parity
     assertion stays green. Bootstrap (bootstrap_phase5b_hp.go)
     refreshes hp_max for existing characters at startup;
     idempotent via db.JobCompleted. hp_current is bumped by the
     same delta so a full-HP character stays at full.
  2. applyPhase5BPlayerFloor (dnd_combat.go): +3 AC, +3 AttackBonus,
     +3 weapon.MagicBonus (damage). Applied at the END of
     applyDnDEquipmentLayer (after computeArmorAC's AC override)
     and inside buildHarnessPlayer so live and harness measurement
     match.
  3. Elite bracket 19 → 23 (resolveCombatInterrupt). Case order
     puts Elite (≥23) before Patrol (≥22) so a 23+ total prefers
     the single dangerous fight. Elite is now effectively a
     high-threat event reachable only via the +1-per-20-threat-
     above-40 mod — Phase 4-B's elite-pool monsters still appear,
     just less often.
  4. dailyThreatDrift base 3 → 1. Slows the threat clock so
     players have the days they need before threat tips zones
     into the new 23+ elite band.
  5. applyDailyBurn default → 50% (phase5BDailyBurnRatePct). Also
     applied in the temporal-override branch in
     dnd_expedition_cycle.go so tidal / unraveling days scale by
     the same 0.5× — otherwise those days would be
     disproportionately harsh against the new baseline.

The harness's expedition_balance.go reads phase5BDailyBurnRatePct
as the default-burn fallback when the override knob is zero, so
Phase 1 matrix measurements now reflect what live players
experience.

Test debt: 13 pinned-numbers unit tests across combat_stats_test,
dnd_test, dnd_xp_test, dnd_equipment_profiles_test,
dnd_expedition_supplies_test, dnd_expedition_cycle_test,
dnd_expedition_extract_test, dnd_expedition_region_cmd_test,
dnd_expedition_combat_test, dnd_expedition_threat_test,
dnd_expedition_temporal_test, expedition_balance_test were
pinning pre-Phase-5-B baselines; updated with comments noting
the cause. Class-balance suite stayed green (uniform buff
preserves spread).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:11:27 -07:00

28 KiB
Raw Blame History

Expedition Difficulty Tuning Pass

Started 2026-05-15, after the Phase D audit close-out. Sibling to the class-balance pass (gogobee_class_balance.md) — same Monte-Carlo philosophy, different target metric.

Class balance answers "is my class viable?" (parity between classes vs. the same monster tier). This pass answers "is the dungeon fair?" (does a tier-appropriate character complete a tier-N expedition at the expected rate?).

Same combat engine (simulateCombatWithRNG), orthogonal knobs.


Method

Sim harness, not telemetry-first. A new internal/plugin/dnd_expedition_balance.go will run thousands of seeded expeditions through the real autopilot / threat / encounter / combat code and report:

  • completion %
  • death %
  • median days-to-extract
  • median threat at extract
  • supply curve (% expeditions that hit Rationing / Severe / Starvation)
  • combat-encounter count per run
  • coin/XP earned

The harness produces these per (zone, player-level) cell; the regression test asserts the per-tier completion band.

Tier target band (steered by feedback_difficulty_target "relatively easy but not too easy", and reflecting that expeditions are multi-day commitments players plan around — losing one should sting but not feel arbitrary):

Tier Target completion % Acceptable band
T1 80% 7090%
T2 72% 6282%
T3 65% 5575%
T4 55% 4565%
T5 45% 3555%

Bands are ±10pp around the centerline. Sim is asserted against the band, not the centerline (no overfit). Player-level for each cell = "tier appropriate" per the class-balance kit ladder: T1 → L1-4, T2 → L3-7, T3 → L5-10, T4 → L7-15, T5 → L10-20 (median level per tier).

Out of scope:

  • Per-encounter combat parity (covered by gogobee_class_balance.md).
  • Arena difficulty (separate system, separate balance).
  • Live telemetry tables — deferred; sim is the primary truth source for this pass. We can add counters later if the sim diverges from observed live numbers.

Phases

Each phase ≈ 1 session, ships green + committed.

Phase 0 — sim harness spike

Build the minimum harness:

  • New file internal/plugin/dnd_expedition_balance.go exposing simulateExpeditionWithRNG(profile, seed) → result struct.
  • Critical de-risk: replay the autopilot deterministically. The ticker is wall-clock driven (dnd_expedition_cycle.go:38,56 at 06:00 / 21:00 UTC; expedition_ambient.go:22 every 3h). Harness must inject a virtual clock or call the per-phase advance functions directly (deliverBriefing, deliverRecap, ambient nudge, harvest autopilot) in tight loop, bypassing the goroutine ticker. Settle the clock-injection seam in Phase 0 — if this is messy, the whole pass collapses.
  • DB-touching layers stubbed/in-memory (same trick as class-balance Phase 0).
  • Sanity test: one cell — e.g. T2 zone at L5 Fighter — runs to completion or death deterministically, returns sensible numbers, 100 trials in <10s.

Exit: TestExpeditionBalance_Phase0_Spike green; commit.

Phase 1 — full matrix measurement

  • Build the cell list: every zone × tier-appropriate-level. Per the exploration map, expeditions span zones tagged T1T5; we measure every zone at its centerline level.
  • 200 trials/cell (same density as class-balance Phase 1).
  • Matrix log: per-cell completion%, death%, median days, median extract-threat, supply-curve histogram.
  • Per-tier mean + spread diagnostic across zones (analogue of the class-balance per-(level,tier) spread diagnostic).
  • TestClassBalance_Phase1_FullMatrix-style permanent test, but only gating on harness-broken pathologies for now (0% completion anywhere at T1, 100% completion at T5).

Exit: matrix numbers logged; commit with "Phase 1 baseline" note.

Phase 2b — wounded-entrant nick clamp (shipped)

clampSurpriseNick(rawNick, hpCurrent, hpMax) in dnd_expedition_combat.go: at full HP the raw nick stands; when wounded the nick is capped at max(1, hpCurrent/5) so a single nick on a low-HP fighter can't shave more than ~20% of remaining HP. The existing "nick can't KO" guard is preserved.

Motivation came from the post-2a tier-lethality trace (TestExpeditionBalance_Phase2_TierLethality): every tier had a fingerprint where an over-tier elite left the fighter at 25-50% HP + retreat, then the next standard's surprise nick (3-7 HP, often exceeding the fighter's headroom) pre-empted the combat engine entirely. Capping the wounded-entrant nick separates that cascade from genuine elite-one-shot deaths so Phase 2c can tune the elites with a clean read.

Matrix delta vs Phase 2a baseline is small (median encs +0.1-0.2 per cell) — completion% still 0% across every tier — but the tier-lethality trace stretched substantively: T1 trial 0 went 5 → 8 encs / 5 → 7 days; T3 trial 1 ran 18 → 10 encs but visibly survived multiple chained interrupts at low HP that pre-2b would have ended on the nick alone. The remaining deaths are now legible as elite one-shot fights on fresh entries (Hobgoblin Warchief, Green Hag, Roper, Young Red Dragon) — that's the Phase 2c roster signal.

Tuning surface: the /5 divisor in clampSurpriseNick is the wounded-fighter lethality knob.

Phase 2 lever sweep (shipped) — negative result

Before adding Phase 2c, the two surfaced knobs (retreatThreatBump, clampSurpriseNick divisor) got swept across a full 3×4 grid (bump ∈ {2, 5, 10} × divisor ∈ {3, 5, 8, 12}) at 200 trials/cell across every zone in the matrix. Wired via expeditionBalanceProfile.{RetreatThreatBumpOverride, SurpriseNickDivisorOverride} threaded into expeditionHarness; the live caller (runHarvestInterrupt) is untouched.

Sweep file: TestExpeditionBalance_Phase2_LeverSweep in expedition_balance_test.go. -short skips.

Outcome: every one of the 24,000 trial-cells reports 0.0% completion / ~100% death. The two knobs are inert on the headline metric — even the gentlest combo (b=2, d=12) cannot lift any tier off the floor. Median days-to-end stays at 23 across the grid with median 2.67.3 encounters before death.

Read: confirms the post-2b tier-lethality trace — deaths are fresh-entry elite one-shots (Warchief @ HP19/20, Hag @ HP41, Roper, Young Red Dragon), not chained-interrupt cascades. The two levers target the cascade path that's now mostly resolved; they have no remaining death-fraction to convert. Phase 2c (roster dilution) is justified rather than premature.

Phase 2c — roster gate (shipped)

Promoted one mid-tier alt to IsElite: true in every zone whose elite pool was a single SpawnWeight=1 boss (internal/plugin/dnd_zone.go). The boss stays in the pool; weighted dilution means the InterruptElite bracket no longer auto-picks the over-tier boss. pickZoneEnemy also excludes the promoted entry from the standard pool, lowering standard encounter difficulty by removing one mid-difficulty option from non-elite brackets (acceptable: each zone still has 3-4 standard entries left). Crypt-Valdris was already dual-elite (wight + flameskull) and was left alone.

zone promoted alt boss (untouched)
goblin_warrens worg (SW=3) hobgoblin_warchief
forest_shadows owlbear (SW=4) green_hag
sunken_temple aboleth_thrall (SW=3) water_elemental
manor_blackspire vampire_spawn (SW=3) revenant
underforge salamander (SW=3) + fire_elemental (SW=3) helmed_horror
underdark drow_elite_warrior (SW=4) roper
feywild_crossing night_hag (SW=3) fomorian
dragons_lair dragonborn_cultist (SW=3) young_red_dragon
abyss_portal hezrou (SW=4) marilith

Phase 1 matrix delta (200 trials/cell, Fighter, tier centerline): goblin_warrens 0% → 3.0% completion (first non-zero T1 reading since Phase 0); sunken_temple 0% → 0.5%; dragons_lair death% 100% → 60% with 40% now starve (fighter survives the elite-spawn path long enough to run out of supplies — different failure mode); underdark death% 100% → 90% (10% starve); feywild_crossing 100% → 92% death (8% starve). T3 manor_blackspire / underforge still 100% death — the remaining death pressure there is the post-2b chained-standard path, not the elite roll.

Tier-lethality trace cross-check (Phase2_TierLethality) shows the new alt-elites actually spawning and being winnable: underdark trial 1 now opens with Drow Elite Warrior (won), reaches the Roper at turn 6, retreats, then dies on a Hook Horror nick — a substantively longer arc than the pre-2c fresh-entry Roper one-shot. Dragons_lair trials similarly show Dragonborn Cultist as a winnable elite stop before the dragon roll lands.

Stat-block tuning of the bosses themselves is off-limits per the Phase 3 constraint (bestiary is shared across systems).

Phase 3 — global lever tuning

Tune the knobs that affect every zone uniformly:

  • Daily threat drift (dnd_expedition_threat.go:87 — base +3/day).
  • Threat band thresholds (Quiet/Stirring/Alert/Hostile/Siege — dnd_expedition_threat.go:70-82).
  • Supply burn multipliers (dnd_expedition_supplies.go:43-56 — T1×1 → T5×3).
  • Surprise-round damage floor (dnd_expedition_combat.go:184).
  • Encounter bracket thresholds (dnd_expedition_combat.go:39-44 None/Noise/Standard/Elite/Patrol).
  • Camp mods (dnd_expedition_night.go:85-92).

Goal: every tier centerline lands within ±5pp of target. Bands stay ±10pp.

Phase 3-A — elite-bracket + drift sweep (shipped)

Wired two harness overrides (EliteInterruptThresholdOverride, ThreatDriftBaseOverride) and swept a 3×3 grid (elite ∈ {17, 19, 23} × drift ∈ {1, 3, 5}) over the Phase 1 matrix. Test: TestExpeditionBalance_Phase3_GlobalLeverSweep.

Strong partial positive. The elite-bracket threshold is the dominant lever for T1T3; threat drift is a small modulator on top.

(elite, drift) T1 mean T2 mean T3 mean T4 mean T5 mean
17, 1 (more elites) 0.5% 0.0% 0.0% 0.0% 0.0%
19, 3 (live) 3.0% 0.2% 0.0% 0.0% 0.0%
23, 1 (rare elites) 24.0% 7.2% 1.8% 0.0% 0.0%
23, 3 16.5% 4.0% 2.0% 0.0% 0.0%
23, 5 11.8% 3.2% 0.2% 0.0% 0.0%

Best cell at e=23/d=1: goblin_warrens 40.5%, sunken_temple 14.5%. Still well below the T1 70-90% / T2 62-82% target bands — the lever moves the needle in the right direction but cannot land any tier on-band alone.

T4/T5 fingerprint changed but didn't lift. At e=23 the dragons_lair death-fraction drops from ~60% → 24% but starvation climbs to 75% — the fighter now survives elites long enough to run out of supplies. T4 (underdark/feywild) likewise sees death drop and starvation rise. Indicates a second lever is needed for the higher tiers: either standard-fight survivability (surprise-nick floor / standard roster density) or supply margin.

Next: Phase 3-B sweeps surprise-round nick and supply-burn knobs on top of e=23/d=1 to push T1-T3 toward target band and lift T4-T5 off the floor.

Phase 3-B — nick-floor + supply-burn sweep (shipped)

Wired two more harness overrides (SurpriseNickFloorOverride, SupplyBurnRatePctOverride) and swept a 3×3 grid (floor ∈ {0, 1, tier (=live)} × burn% ∈ {50, 75, 100 (=live)}) on top of the Phase 3-A best cell (e=23, d=1). Test: TestExpeditionBalance_Phase3B_NickSupplySweep.

Strong partial positive for T5 supply margin; nick-floor lever inert.

Tier-mean completion (Fighter, centerline level, 10 zones × 200 trials):

(floor, burn%) T1 mean T2 T3 T4 T5
0, 50 20.8% 5.0% 3.8% 7.5% 29.5%
0, 75 26.5% 6.0% 2.8% 9.0% 0.0%
0, 100 25.8% 5.0% 3.0% 0.0% 0.0%
1, 50 25.5% 6.5% 1.8% 8.5% 27.8%
1, 75 28.5% 6.2% 4.0% 11.5% 0.0%
1, 100 26.8% 7.5% 2.5% 0.0% 0.0%
tier, 50 26.8% 6.2% 2.5% 7.8% 27.5%
tier, 75 (live floor) 29.5% 7.8% 3.0% 12.8% 0.0%
tier,100 (live) 28.5% 6.8% 3.0% 0.0% 0.0%

Three readings:

  1. Supply burn is the T5 unlock. dragons_lair completes ~55% at burn=50 (versus 0% at burn=75/100) — the fighter survives elites but starves en route. burn=75 isn't enough margin; burn=50 converts the starvation-out into a completion.
  2. Supply burn is the T4 modulator. underdark/feywild step from 0% (burn=100) → 12% (burn=75) → 8% (burn=50). The burn=75 peak is the sweet spot; halving further leaves the fighter alive into more elite fights, where it dies in combat instead.
  3. Nick-floor lever is inert. Across all burn levels, dropping the surprise-round floor from tier10 moves T1-T3 means by at most ~3pp. The wounded-clamp (Phase 2b, clampSurpriseNick) already eats the chip-damage budget on repeat entries; the raw floor only matters on the first fresh-HP swing, which the engine recovers from. Recommendation: discard this lever from the live-tuning candidate list.

T2-T3 wall persists. Even at the best (floor=tier, burn=75) cell: T2 reads 7.8% (target 62-82%) and T3 reads 3.0% (target 54-74%). forest_shadows T2, manor_blackspire T3, and abyss_portal T5 sit at ~0% across every combo — these are zone-specific outliers, not addressable by any global lever. Phase 4 (per-zone outlier pass) is the next move.

Next: Phase 4 walks the matrix zone-by-zone. The shipped Phase 3 sweep already names the worst offenders (forest_shadows T2, manor_blackspire T3, abyss_portal T5, crypt_valdris T1); each needs a diagnostic trace (boss stat-block, roster mix, supply DC) to pick the right per-zone tool from Phase 4's list. Optional: revisit shipping the burn=75 global if Phase 4 outlier fixes leave T4 still stuck.

Phase 4 — per-zone outlier pass

After globals settle, the matrix names outlier zones — tiers where one zone reads dramatically off-band while sibling zones are fine. Per-zone tools:

  • SpawnWeight / IsElite flags in zone rosters (dnd_zone.go:100).
  • Boss stat tuning in zone definitions.
  • Resource DCs (affects harvest interrupt roll).
  • Per-zone loot table density (downstream economy impact — flag if significant).

Constraint: do not touch monster stat blocks (those belong to the bestiary, shared across systems). Tune the roster + zone-level knobs, not the monsters themselves.

Phase 4-A — outlier diagnostic (shipped)

Added a structured per-fight trace hook (traceFightStruct + harnessFightTrace) alongside the existing string traceFight; both fire from the same runHarnessFight site so the human log and the structured aggregate stay in sync. Test: TestExpeditionBalance_Phase4A_OutlierDiagnostic. For each of the four outlier zones it pairs against the healthy sibling at the same tier (goblin_warrens, sunken_temple, underforge, dragons_lair), running 200 trials each on the Phase 3-B best cell (e=23, d=1, burn=50) and reporting per-monster appearances / win-rate / avg HP loss / kill attribution + day-of-end histogram.

Findings — each outlier had one or two miscategorised entries that Phase 2c's single-boss-elite dilution sweep had skipped:

  • crypt_valdris: dual-killer elite pool (Wight 99k, Flameskull 68k); Phase 2c had skipped this zone as "already dual-elite," but both elites are over-tier for T1.
  • forest_shadows: standard pool too lethal — Displacer Beast (38% win, 53k) + Bandit Captain (57% win, 48k).
  • manor_blackspire: Wraith on standard slot (45hp loss per win, 85k).
  • abyss_portal: Nalfeshnee mis-classified standard (CR-13, 2.8% win, 86k) + Vrock standard at 43hp loss per win.

Phase 4-B — roster re-classification (shipped)

Per-zone roster tweaks based on Phase 4-A's attribution. No monster stat-block touches.

  • crypt_valdris: dropped flameskull (lives in underforge T3); promoted specter to IsElite SW=3 as the soft dilutor.
  • forest_shadows: promoted Displacer Beast + Bandit Captain to IsElite; Owlbear SW 4→2 so the 4-elite pool dilutes evenly.
  • manor_blackspire: promoted Wraith to IsElite (joins Spawn+Revenant).
  • abyss_portal: promoted Nalfeshnee + Vrock to IsElite (collapses standard pool to Quasit alone, ≥99% win).

Outlier-vs-sibling completion (Phase 3-B best cell, 200 trials):

Zone Before After Sibling
crypt_valdris T1 8.5% 46.0% 45.0%
forest_shadows T2 0.0% 7.5% 13.0%
manor_blackspire T3 0.5% 14.5% 3.0%
abyss_portal T5 0.0% 25.0% 57.5%

Phase 3-B tier mean (b=50, f=tier) before → after: T1 26.8% → 44.0% (+17pp), T2 6.2% → 10.0% (+4), T3 2.5% → 8.5% (+6), T4 unchanged, T5 27.5% → 41.0% (+14).

Exit (partial): the four off-band outliers are no longer outliers — every per-zone cell is within the spread of its tier siblings. The Phase 4 plan's stricter exit ("all per-zone cells within target band") is not met: T2/T3/T5 sibling pairs still sit below band (T2 7-13% vs 62-82%; T3 3-14% vs 54-74%; T5 25-57% vs 36-56%). That gap is tier-wide, not zone-specific — both zones at each problem tier underperform together — so no further per-zone tool will close it. Phase 5 (or a follow-up tier-wide pass) needs to take over.

Next: Phase 5. Candidate moves are tier-wide: revisit player gear-tier mapping at T2/T3 (the centerline level vs gear ladder, see phase1TierCenterline), reconsider the elite-bracket threshold per-tier instead of global, or ship burn=75 as the new live global since Phase 4-B already converted T4 to playable at b=50.

Phase 5 — tier-wide pass (T2/T3/T5 under-band)

Phase 4-B closed the per-zone outliers but left T2/T3/T5 sibling pairs below the target band as a tier-wide gap. The plan doc's three candidate moves (gear-tier centerline remap, per-tier elite threshold, ship burn=75) needed disambiguation before any of them got pulled — hence a measure-first sub-phase.

Phase 5-A — tier-wide sensitivity sweep (shipped)

TestExpeditionBalance_Phase5A_TierWideSensitivity runs the 6 under- band zones (T2: forest_shadows + sunken_temple; T3: manor_blackspire + underforge; T5: abyss_portal + dragons_lair) through three one-axis sweeps with the other two levers held at Phase 3-B best (e=23, d=1, burn=50): player level ±2 around tier centerline, elite threshold ∈ {18, 23, 28}, supply burn pct ∈ {40, 50, 60}. 200 trials/cell × 6 zones × 9 cells = 10.8k trials; runs in <1s.

Numbers (200 trials/cell, comp%):

T2 (band 62-82%)
  L  forest=0→6.5→19    sunken=0→13.5→34.5     STRONG slope
  E  forest=0.5→9→8     sunken=0→14.5→13       saturated @23; 18 collapses
  B  forest=3.5→7→6.5   sunken=14→15.5→11      inert
T3 (band 54-74%)
  L  manor=3→10.5→13.5  under=0→4→8.5          clear slope
  E  manor=0→9.5→9.5    under=0→3→4            saturated @23; 18 collapses
  B  manor=13→9.5→9.5   under=2.5→4→2.5        inert
T5 (band 36-56%)
  L  abyss=21.5→28.5→32 dragons=60→60.5→53     gentle; dragons in-band
  E  abyss=1→28.5→31    dragons=10→60.5→57.5   saturated @23; 18 collapses
  B  abyss=27.5→28.5→0  dragons=59→60.5→0      burn=60 STARVE CLIFF

Reading:

  • Player level is the dominant lever at T2/T3. Every +2 levels lifts completion meaningfully; the elite gate and supply burn are inert or actively harmful around the Phase 3-B baseline.
  • Elite threshold is already at its sweet spot at e=23. Going to e=18 floods elites and collapses every tier; e=28 is flat. The per- tier-threshold candidate from Phase 4 close-out is killed: there's no per-tier value to be found in the explored range.
  • burn=75 globally is killed. burn=60 alone produces 36% / 61% starve at T5; burn=75 would be catastrophic. The Phase 3-B negative result holds tier-wide. burn must stay ≤50.
  • T5 dragons_lair is already in-band at the baseline (60.5%). The T5 tier-wide gap is really an abyss_portal gap (28.5%); the Phase 4-B re-class lifted it from outlier to "low-but-not-isolated," but it still sits below band by itself.
  • The level-bump path has a catch. L7 at T2 (still inside the L3-7 design range and gearTier=2) only reaches ~27% mean — still well below the 62-82% band. Closing T2 to band likely requires a cross-gearTier-boundary centerline (T2 → L9 = gearTier 3) and/or a player-side combat-math adjustment, not just a within-bracket centerline shift. That's a design call, not a knob twist.

Next: Phase 5-B is a design choice between three concrete moves that the measurement now backs:

  1. Cross-bracket centerline bump for T2 (→ L9) and T3 (→ L13), plus T5 abyss-only level tweak. Lifts player gear bracket into the next tier; closes the band gap directly but is a player-side power move that affects the live game (chars at the centerline level keep their existing gear; centerline only governs the harness's measurement cell). The change is in the harness, but it admits the live difficulty target was wrong for these tiers — Phase 5-C would then carry it back to the live gear-acquisition curve.
  2. Player combat-math retune at T2/T3 (HP scaling, proficiency curve, attack-bonus floor) — bigger lift, touches the class-balance harness too, deserves its own pass.
  3. Lower the band target for T2/T3/T5 to ~30-50% mean and accept that completion is meant to be hard. This is the "do nothing" option dressed up as a design decision; surfaces the question of whether the original 70% T2 target was ever the right number.

Recommend option (1) for Phase 5-B since the harness already measures it cleanly and the data points at it; option (3) is the fallback if the cross-bracket numbers still don't close the band.

Phase 5-B — player power floor + Phase-3 winners shipped

The Phase 5-A read named player level / gear-tier centerline as the dominant lever at T2/T3 but flagged that even max-of-range within the gear bracket missed the band. Three design options were on the table: cross-bracket centerline bump, player combat-math retune, or lower the band target. The user picked combat-math retune ("fairly breezy with some death — not a ton") — option (2). On reflection this was the only one of the three that actually moves live difficulty (cross-bracket centerline only moves the harness number; lowering the band is the "do-nothing" fallback).

The shipped lift, in five small pieces:

  1. computeMaxHP ×1.5 (dnd.go: phase5BHPMult). The HP curve is uniformly scaled so every class/level lifts together — preserves the class-balance harness's in-tier spread assertion. Migration bootstrapPhase5BHPRefresh walks dnd_character once at startup to refresh existing rows; idempotent via db.JobCompleted.
  2. Combat-stat player floor (dnd_combat.go: applyPhase5BPlayerFloor): +3 AC, +3 AttackBonus, +3 weapon.MagicBonus (damage). Applied at the END of applyDnDEquipmentLayer (so the AC override from computeArmorAC doesn't stomp it) AND inside buildHarnessPlayer so live and harness measurement agree.
  3. Elite bracket 19 → 23 (dnd_expedition_combat.go: resolveCombatInterrupt). Case order: Elite (≥23) before Patrol (≥22) so a 23+ total prefers the single dangerous fight over the patrol-flavored standard pick. Elite is now effectively a high-threat event reachable only via the +1-per-20-threat-above-40 mod — base d20+tier+ranger maxes at ~21. Phase 4-B's elite-pool monsters still appear; just less often.
  4. Threat drift base 3 → 1 (dnd_expedition_threat.go: dailyThreatDrift). Slows the threat clock so players have the days they need to extract before threat tips zones into the new 23+ elite band.
  5. Daily supply burn × 0.5 (dnd_expedition_supplies.go: phase5BDailyBurnRatePct). applyDailyBurn's default now passes 50; applyDailyBurnP keeps its rate-parameterized form for the harness. Also applied in the temporal-override branch (dnd_expedition_cycle.go) so tidal / unraveling days scale by the same 0.5× — otherwise those days would be disproportionately harsh against the new baseline.

The harness reflects all five (expedition_balance.go reads phase5BDailyBurnRatePct as the default-burn fallback when the override knob is zero). Phase 1 matrix on the shipped baseline, Fighter @ tier-centerline, 200 trials:

T1  87.8%  spread 1.5   (crypt 89, goblin 87)           ⬆ band 70-90 — IN BAND
T2  74.0%  spread 12    (forest 80, sunken 68)          ⬆ band 62-82 — IN BAND
T3  43.2%  spread 8.5   (manor 39, underforge 47)        ⬇ band 55-75 — UNDER
T4  71.8%  spread 25.5  (feywild 59, underdark 88)      ⬆ band 45-65 — OVER (breezy)
T5  ~57%   (dragons 78, abyss 38)                       ⬆ band 35-55 — borderline

Four of five tier means land at or above band-center, matching the "fairly breezy with some death" target. T3 remains the hump — both manor and underforge sit ~40-50% completion, killing 50-60% of the time. Phase 4-B already promoted T3's worst killers (Wraith) to elite, so the standard-pool deaths are now the irreducible part. T4's wide spread (feywild 59 vs underdark 88) means feywild still has a roster-side asymmetry that warrants a Phase 4-B-style pass.

Test debt cleaned: 13 unit tests in combat_stats_test.go, dnd_test.go, dnd_xp_test.go, dnd_equipment_profiles_test.go, dnd_expedition_supplies_test.go, dnd_expedition_cycle_test.go, dnd_expedition_extract_test.go, dnd_expedition_region_cmd_test.go, dnd_expedition_combat_test.go, dnd_expedition_threat_test.go, dnd_expedition_temporal_test.go, expedition_balance_test.go were pinning the pre-Phase-5-B numbers; updated to the shipped values with comments noting the cause.

Exit: Phase 5-B closed-out the user's primary target ("make it breezy"). T1/T2/T4/T5 mean in or above band; T3 remains the design hump and is fine for "some death" tier shape. Remaining work (Phase 5-C+) is T3 roster polish + feywild T4-spread fix — separate session.

Phase 6 — optional MAD / second-order

If post-Phase-3 the bands hold but feel wrong subjectively (e.g. "median days too short", "no expedition ever hits Siege"), add a secondary assertion on median-days-to-extract or extract-threat. Phase 3 may also surface a request to expose a difficulty knob to operators (env var or admin command); scope that here if it comes up.


Open questions

  • Should the babysit perk be on or off in the sim? Pick one and document; both as separate cells doubles matrix size for diminishing insight. Default proposed: off, since it's a discovery-mechanic buff (feedback_npc_buffs_are_secret) — players who find it earn extra margin, not the baseline.
  • Pardon proc (33% @ chat 20+): include in sim, but at a fixed chat-level assumption (chat 15 = no pardon, since most fresh runs haven't hit the threshold). Decide in Phase 0.
  • Sovereign reprieve: skip in sim — it's gated on full set equipped and is a one-shot per cooldown. End-game-only; not part of the baseline difficulty.
  • Multi-region zones (E4): does the sim need to walk all regions in one expedition, or pick one region per run? Probably full walk for realism. Settle in Phase 0.
  • DM mood: starts at 50; drifts via combat outcomes. Let it drift naturally in the sim, do not pin.

Deliverables checklist

  • internal/plugin/dnd_expedition_balance.go (sim harness)
  • TestExpeditionBalance_Phase0_Spike
  • TestExpeditionBalance_Phase1_FullMatrix (becomes the permanent regression gate after Phase 2 tightens it)
  • Tuning commits per phase
  • One-line update to MEMORY.md + project memory pointer