Phase 2 (sweep): lever-tuning sweep, negative result

Sweep the two knobs surfaced by Phases 2a/2b — retreatThreatBump
and clampSurpriseNick's wounded-entrant divisor — across a full
3×4 grid (bump ∈ {2, 5, 10} × divisor ∈ {3, 5, 8, 12}) at 200
trials/cell across every matrix zone.

Wiring is harness-only: clampSurpriseNick keeps its live shape and
delegates to a new clampSurpriseNickD(divisor) variant; the harness
profile gains RetreatThreatBumpOverride/SurpriseNickDivisorOverride
fields threaded onto expeditionHarness; resolvedRetreatBump and
resolvedNickDivisor pick override-or-live. Zero on either field
falls back to the shipped value so live runHarvestInterrupt is
untouched.

Sweep test: TestExpeditionBalance_Phase2_LeverSweep, -short skipped,
mirrors Phase2_CadenceCalibration's per-tier digest shape.

Outcome: across 24,000 trial-cells (12 lever combos × 10 zones
× 200 trials), every cell reports 0.0% completion / ~100% death.
The knobs are inert on the headline metric — even (b=2, d=12)
can't lift any tier off the floor. Confirms the post-2b
tier-lethality trace: remaining deaths are fresh-entry elite
one-shots (Warchief, Hag, Roper, Young Red Dragon), not chained-
interrupt cascades. Justifies Phase 2c (roster dilution) rather
than further tuning of these two levers.

Plan doc updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-15 10:39:45 -07:00
parent 159daf8fc8
commit 8cdd64b383
4 changed files with 198 additions and 8 deletions

View File

@@ -97,6 +97,12 @@ type expeditionBalanceProfile struct {
// calibration sweep sets this per cell; everyday callers leave it
// zero.
HarvestRollsPerDay int
// Phase 2 lever overrides (sweep-only). Zero means "use the live
// shipped value." Wired into the harness day loop / runHarnessFight
// path; the live runHarvestInterrupt is untouched. See
// TestExpeditionBalance_Phase2_LeverSweep.
RetreatThreatBumpOverride int
SurpriseNickDivisorOverride int
}
// expeditionTrialResult is the outcome of one simulated expedition.
@@ -274,7 +280,7 @@ func (h *expeditionHarness) advanceExpeditionOneDay() expeditionTrialResult {
// dnd_expedition_combat.go (Phase 2). Run continues
// with carryover HP and a threat bump; the harvest
// slot's loot is just forfeit.
h.exp.ThreatLevel += retreatThreatBump
h.exp.ThreatLevel += h.resolvedRetreatBump()
if h.exp.ThreatLevel > 100 {
h.exp.ThreatLevel = 100
}
@@ -333,6 +339,25 @@ func (h *expeditionHarness) advanceExpeditionOneDay() expeditionTrialResult {
return expeditionTrialResult{}
}
// resolvedRetreatBump returns the per-harness lever override or the
// shipped retreatThreatBump if no override is set. Zero is the "use
// live" sentinel so the field's zero value remains safe.
func (h *expeditionHarness) resolvedRetreatBump() int {
if h.retreatThreatBumpOverride > 0 {
return h.retreatThreatBumpOverride
}
return retreatThreatBump
}
// resolvedNickDivisor returns the per-harness override or the shipped
// liveSurpriseNickDivisor. Same zero-sentinel contract as above.
func (h *expeditionHarness) resolvedNickDivisor() int {
if h.surpriseNickDivisorOverride > 0 {
return h.surpriseNickDivisorOverride
}
return liveSurpriseNickDivisor
}
// terminate stamps the final trial result with shared bookkeeping
// (days elapsed, threat at end, encounter count, HP%).
func (h *expeditionHarness) terminate(reason string, completed, died, starved bool) expeditionTrialResult {
@@ -376,7 +401,11 @@ func (h *expeditionHarness) runHarnessFight(zone ZoneDefinition, elite bool) Com
// (clampSurpriseNick) that breaks the chained-interrupt death
// spiral. Mirror live exactly so the harness measures the same
// lever the live caller applies.
nick := clampSurpriseNick(surpriseRoundNick(monster, int(zone.Tier)), h.char.HPCurrent, h.char.HPMax)
nick := clampSurpriseNickD(
surpriseRoundNick(monster, int(zone.Tier)),
h.char.HPCurrent, h.char.HPMax,
h.resolvedNickDivisor(),
)
h.char.HPCurrent -= nick
player := buildHarnessPlayer(h.char)
@@ -487,6 +516,10 @@ type expeditionHarness struct {
rng *harnessRNG
encounters int
rollsPerDay int // resolved from profile + default; never zero
// Lever overrides for the Phase 2 sweep. Zero on both fields ⇒
// live behavior (retreatThreatBump=5, /5 surprise-nick divisor).
retreatThreatBumpOverride int
surpriseNickDivisorOverride int
// traceFight, if non-nil, is invoked once per fight inside
// runHarnessFight with a human-readable summary. Used by the
// Phase 2 lethality probe to spot whether the nick, the picked
@@ -531,10 +564,12 @@ func runExpeditionBalanceTrial(p expeditionBalanceProfile, seed uint64) expediti
rolls = harnessHarvestRollsPerDay
}
h := &expeditionHarness{
exp: exp,
char: char,
rng: newHarnessRNG(seed),
rollsPerDay: rolls,
exp: exp,
char: char,
rng: newHarnessRNG(seed),
rollsPerDay: rolls,
retreatThreatBumpOverride: p.RetreatThreatBumpOverride,
surpriseNickDivisorOverride: p.SurpriseNickDivisorOverride,
}
for {
res := h.advanceExpeditionOneDay()