mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
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>
This commit is contained in:
@@ -428,6 +428,73 @@ func TestExpeditionBalance_Phase2_LethalityProbe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExpeditionBalance_Phase2_TierLethality is the tier-walking
|
||||
// companion to the T1-only lethality probe. Phase 1's matrix shows
|
||||
// uniform 0% across every tier at the default rolls=4 cadence, which
|
||||
// the T1/rolls=1 probe alone can't explain (its bimodal-warchief
|
||||
// finding wouldn't show at T5 vs an L17 fighter). This probe traces
|
||||
// every fight at the matrix cadence across one zone per tier so we
|
||||
// can read whether the deaths are elite-driven, standard-fight
|
||||
// chained, or something the harness combat fold itself is doing.
|
||||
//
|
||||
// Diagnostic-only — no assertions.
|
||||
func TestExpeditionBalance_Phase2_TierLethality(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("phase 2 tier-lethality probe writes verbose log; -short skips it")
|
||||
}
|
||||
|
||||
const trialsPerTier = 3
|
||||
const baseSeed uint64 = 0x7E11A1
|
||||
cells := []struct {
|
||||
zone ZoneID
|
||||
tier ZoneTier
|
||||
}{
|
||||
{ZoneGoblinWarrens, ZoneTierBeginner},
|
||||
{ZoneForestShadows, ZoneTierApprentice},
|
||||
{ZoneUnderforge, ZoneTierJourneyman},
|
||||
{ZoneUnderdark, ZoneTierVeteran},
|
||||
{ZoneDragonsLair, ZoneTierLegendary},
|
||||
}
|
||||
for _, cell := range cells {
|
||||
level := phase1TierCenterline[cell.tier]
|
||||
profile := expeditionBalanceProfile{
|
||||
ZoneID: cell.zone,
|
||||
Class: ClassFighter,
|
||||
Level: level,
|
||||
Supplies: makeSupplies(cell.tier, SupplyPurchase{StandardPacks: 3}),
|
||||
CampType: CampTypeStandard,
|
||||
HarvestRollsPerDay: 4, // matrix default, not the sparse probe
|
||||
}
|
||||
t.Logf("═══ %s T%d L%d Fighter rolls=4 ═══", cell.zone, cell.tier, level)
|
||||
for trial := 0; trial < trialsPerTier; trial++ {
|
||||
exp := newHarnessExpedition(profile)
|
||||
char := buildHarnessCharacter(classBalanceProfile{
|
||||
Class: profile.Class,
|
||||
Level: profile.Level,
|
||||
})
|
||||
t.Logf("─── %s trial %d: hp_max=%d ───", cell.zone, trial, char.HPMax)
|
||||
h := &expeditionHarness{
|
||||
exp: exp,
|
||||
char: char,
|
||||
rng: newHarnessRNG(baseSeed + uint64(trial) + uint64(cell.tier)*101),
|
||||
rollsPerDay: profile.HarvestRollsPerDay,
|
||||
traceFight: func(line string) {
|
||||
t.Logf(" %s", line)
|
||||
},
|
||||
}
|
||||
for {
|
||||
res := h.advanceExpeditionOneDay()
|
||||
if res.EndedReason != "" {
|
||||
t.Logf("END %s trial %d: reason=%s days=%d threat=%d encs=%d hp_left_pct=%.1f%%",
|
||||
cell.zone, trial, res.EndedReason, res.DaysElapsed, res.ThreatAtEnd,
|
||||
res.CombatEncounters, res.HPRemainingPct*100)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// joinZones is a tiny helper kept local to the test file so the
|
||||
// per-tier log line reads in one logical chunk without pulling in
|
||||
// strings.Join's import for production code.
|
||||
|
||||
Reference in New Issue
Block a user