Commit Graph

4 Commits

Author SHA1 Message Date
prosolis
159daf8fc8 Phase 2b (lever): wounded-entrant surprise-nick clamp
The post-2a tier-lethality trace showed the remaining 0% completion
was driven by chained surprise-round nicks on already-wounded fighters,
not by the elites themselves. Pattern from the trace:

  fight day=5 Hobgoblin Warchief: hp_pre=24 → hp_post=14  WON
  fight day=5 Goblin Archer:      nick=6, hp_pre=8 → hp_post=0  LOST

The Warchief left the fighter at HP 14; the Goblin Archer's surprise
nick (6 HP) dropped them to 8 before combat resolved — and a standard
goblin then finished a fighter who should have survived. Same shape at
T2 (Dire Wolf nick 4 on hp=3), T3 (Fire Elemental nick 4 on hp=5),
T4-T5. The nick was acting as a hidden cascade multiplier, pre-empting
the combat engine on wounded entries.

clampSurpriseNick caps the nick at max(1, hpCurrent/5) when the
fighter enters wounded (HPCurrent < HPMax); at full HP the raw nick
stands. The existing 'nick < HPCurrent' KO-guard is preserved as a
backstop. /5 is the wounded-fighter lethality knob; tighter (/10) is
gentler, looser (/3) re-opens the cascade.

Live caller (runHarvestInterrupt) and harness (runHarnessFight) both
route through the new helper so the sim measures the same lever the
live caller applies.

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

Push-back on the original lever-order: the Phase 2a recap put roster
gate first, but the trace fingerprint named the wounded-entry nick
as the dominant cause-of-death in 4 of 5 tier traces. Doing nick-cap
first keeps Phase 2c's diagnostic clean and avoids re-tuning rosters
after another lever changes the shape under us.

Pre-existing failures unrelated to this change:
- TestAdv2Scenario_ZoneRunGoblinWarrens (advance regression, prior)
- TestMageSpellbookLineInRender (render assertion, prior)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:27:29 -07:00
prosolis
04aa887d18 Phase 2a (lever): expedition retreat semantics split
Phase 2 diagnostics named the InterruptElite bracket as the likely
first lever; tier-walking the lethality probe at the matrix cadence
told a different story.  Phase 1's uniform-0% baseline isn't an
elite-bracket calibration issue at all — every tier reads 0% because
the engine's TimedOut contract was being ignored by every expedition
caller.

combat_engine.go:451 says: "Timeout = retreat, not lethal blow.
Caller treats a timeout loss as 'fight ended, no character death'".
But runHarvestInterrupt / tryPatrolEncounter / resolveCombatRoom all
called abandonZoneRun + retireAllRegionRuns on any !PlayerWon —
ending the expedition outright on a retreat.  The retreat flavor line
("X outlasts you. You retreat from the expedition, wounded but alive")
was already in the code, just stapled to an actual run-abort.

Splits the policy by caller:

  • runHarvestInterrupt   — autopilot daytime interrupt.  TimedOut →
    retreat: threat +5, HP carries over, run continues, harvest slot
    forfeit (no kill / loot).  HP<=0 still ends the run + marks dead.
  • tryPatrolEncounter   — !advance pre-room patrol roll.  Same
    retreat policy: patrols don't gate progress, so retreating from
    one and walking into the next room is the right shape.
  • resolveCombatRoom    — !advance room/elite combat.  Unchanged —
    this path gates room progression; a retreat has nowhere to go, so
    any loss still ends the run.  (Manual zone runs were always
    intended to end here.)

Harness mirrored: daytime interrupt timeout → carry HP + threat bump
+ continue day; night-encounter loss → terminate (mirrors
resolveCombatRoom, since live night encounters defer to !advance).

retreatThreatBump = 5 is the per-retreat threat penalty.  Low enough
not to compound brutally with chained retreats, high enough that 3–4
retreats noticeably walks the threat clock toward Stirring.  Easy to
dial in Phase 3 if zones go off-band.

Phase 1 matrix after the change still reads 0% completion at every
cell — but the encounter counts and survival shape are dramatically
different (T4 underdark 3.6→7.5 encs; T3 underforge trial saw 18
encounters across 10 days where the pre-change run died on day 2/3).
Adds TestExpeditionBalance_Phase2_TierLethality, a tier-walking
companion to the T1/rolls=1 probe, that traces every fight at the
matrix cadence across one zone per tier — the actual Phase 2b lever
work picks from this data, not the old T1-only probe.

The remaining 0% is now legibly driven by tier-disproportionate elite
rosters (Hobgoblin Warchief at T1, Green Hag at T2, Roper/Helmed
Horror higher up) that one-shot or two-shot tier-appropriate
fighters.  Phase 2b's lever shortlist:

  1. Roster gate / SpawnWeight tuning to dilute over-tier elites.
  2. Surprise-nick floor reduction on chained interrupts (carryover
     HP + nick is the death-spiral fingerprint at T1 specifically).
  3. Per-day cadence reduction if 1+2 don't carry T1 to band.

Pre-existing test failures (TestAdv2Scenario_ZoneRunGoblinWarrens,
TestMageSpellbookLineInRender) verified to fail identically on HEAD;
no new test regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:01:07 -07:00
prosolis
0f09a421bc Phase 2 (diag): cadence sweep, gear-tier fix, lethality probe
Three Phase 2 diagnostic artifacts. No tuning knob has moved on
production code yet — these tests calibrate the harness and surface
the real first lever for next session.

1) Cadence calibration sweep
   TestExpeditionBalance_Phase2_CadenceCalibration sweeps
   HarvestRollsPerDay ∈ {1,2,3,4} across the full Phase 1 matrix and
   logs per-cell + per-tier completion. Required a new
   HarvestRollsPerDay field on expeditionBalanceProfile so cells can
   override the package-default constant. Finding: cadence is NOT the
   dominant lever — at rolls=1 the T1 cell only reaches 2%, with
   bimodal hp_left (100% survivors / 0% deaths). Killed the cadence
   hypothesis from Phase 1's commit message.

2) Gear-tier centerline fix
   phase1TierCenterline bumped for T3/T4/T5 (8→9, 11→13, 15→17). The
   shared gearTier ladder (5/9/13/17 boundaries) was placing T3/T4/T5
   centerlines one gear bracket *below* the zone's tier, so those
   cells fought with under-spec'd weapons/armor. New centerlines are
   the lowest level in each tier's design-doc range where gearTier ==
   tier. All centerlines still inside their design-doc ranges. Effect
   in the sweep at rolls=1: underforge T3 1.0% → 10.5% comp, underdark
   T4 flipped from pure combat-death to 14% starve (i.e. fighter now
   survives combat, runs out of food). Real bug, but small — the
   structural lethality problem remains.

3) Lethality probe + traceFight hook
   TestExpeditionBalance_Phase2_LethalityProbe runs 5 trials at the
   cleanest cell (T1 goblin_warrens L3 fighter, rolls=1) with a new
   optional traceFight hook on expeditionHarness that logs
   monster/AC/atk/HP-pre/HP-post/outcome per fight. Hook is nil in
   production runs, zero cost when unused. Finding: at T1, the
   InterruptElite branch keeps drawing Hobgoblin Warchief (AC 18,
   atk +5) from goblin_warrens' elite roster, and an L3 fighter has
   ~coin-flip odds against a CR-6-ish elite. One bad draw = dead;
   that's the bimodal hp_left fingerprint from the sweep. Non-elite
   draws (Worg, AC 13) play out as normal multi-round combats and
   are winnable.

Next-session lever choices, in order of suspected impact:
  - Roster gate: Hobgoblin Warchief out of (or weighted down in)
    the T1 elite pool — it's tier-disproportionate for goblin_warrens.
  - InterruptElite threshold: rarer elite-bracket draws at low threat
    so a single d20 swing doesn't equal expedition end.
  - Tier-floor cap on already-over-tier bestiary entries.

Plan doc: gogobee_expedition_difficulty.md §Phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:39:31 -07:00
prosolis
53d1608490 Phase 0: expedition-difficulty sim harness spike
New file internal/plugin/expedition_balance.go — sibling to
dnd_class_balance.go, no dnd_ prefix per feedback_avoid_dnd_naming.
Plan doc: gogobee_expedition_difficulty.md.

Critical de-risk settled: clock-injection seam. The harness does not
run the wall-clock tickers (expeditionBriefingTicker @ 06:00,
expeditionRecapTicker @ 21:00, expedition_ambient @ 3h) and does not
inject a virtual time.Now. Instead, advanceExpeditionOneDay
reimplements the morning→day→night pipeline by calling the math-pure
helpers directly: applyDailyBurn, dailyThreatDrift,
resolveCombatInterrupt, resolveWanderingCheck, simulateCombatWithRNG.
No DB, no goroutines, no clock.

Phase 0 trade-offs (documented in the file header):
- Boss completion deferred; survive-N-days is the proxy.
- Per-region zones, loot/XP, pardon/Sovereign, babysit, temporal
  stack effects all skipped — Phase 1+ scope.
- Encounter cadence (harnessHarvestRollsPerDay=4) is a placeholder
  to be calibrated against live traces in Phase 1.

Tests:
- TestExpeditionBalance_Phase0_Spike — T2 Crypt Valdris × L5 Fighter,
  100 trials, asserts only degenerate sentinels (no 0%/100%, days>0,
  days<=cap). Runs in <3s.
- TestExpeditionBalance_Phase0_SeedSpread — confirms the RNG seam is
  actually wired by showing two distinct seeds produce different
  trials. Full byte-for-byte reproducibility under same seed is not
  asserted at Phase 0; surpriseRoundNick + pickWanderingMonster
  draw from package-global rand (same caveat as class balance).

Spike numbers (calibration baseline, NOT a target): 1% completion,
99% deaths, median 4 days, ~6 encounters. The headline death rate is
loud but expected — Phase 1's full matrix will diagnose whether it's
HP-carryover punishment, bestiary-vs-abstract-monster delta, or kit
ladder mismatch. Phase 2 is where the centerline gets tuned.

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