mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Wired two harness lever overrides — EliteInterruptThresholdOverride
(live=19) and ThreatDriftBaseOverride (live=3) — into the day-loop in
expedition_balance.go. Live runHarvestInterrupt / dailyThreatDrift are
untouched; the harness re-buckets Standard↔Elite after the live call.
TestExpeditionBalance_Phase3_GlobalLeverSweep walks a 3×3 grid
(elite ∈ {17,19,23} × drift ∈ {1,3,5}) over the Phase 1 matrix at
200 trials/cell. -short skips.
Elite-bracket threshold is the dominant lever for T1–T3. At
e=23/d=1: T1 mean 24.0% (goblin_warrens 40.5%), T2 7.2%
(sunken_temple 14.5%), T3 1.8%. Still well below target bands
(T1 70-90%, T2 62-82%) — 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 dragons_lair
death drops 60% → 24% but starvation climbs to 75% — the fighter
now survives elites long enough to run out of supplies. T4 cells
shift the same way. Indicates a second lever is needed for the
higher tiers (standard-fight survivability or supply margin), to
be swept in Phase 3-B.
Plan doc updated. Renumbered the trailing "per-zone outlier pass"
to Phase 4 and "MAD / second-order" to Phase 5 so the test names
align with phase numbers going forward.
-short suite: same 2 pre-existing failures
(TestAdv2Scenario_ZoneRunGoblinWarrens, TestMageSpellbookLineInRender).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
313 lines
14 KiB
Markdown
313 lines
14 KiB
Markdown
# 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% | 70–90% |
|
||
| T2 | 72% | 62–82% |
|
||
| T3 | 65% | 55–75% |
|
||
| T4 | 55% | 45–65% |
|
||
| T5 | 45% | 35–55% |
|
||
|
||
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 T1–T5; 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 2–3 across the grid
|
||
with median 2.6–7.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 T1–T3; 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 4 — per-zone outlier pass
|
||
|
||
After globals settle, the matrix will name 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.
|
||
|
||
**Exit:** all per-zone cells within band; commit.
|
||
|
||
### Phase 5 — 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
|