mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Phase 4a (diag): per-zone outlier attribution
The Phase 3-B sweep wrung out the global lever surface and named four
zones that read ~0% completion under every (elite-threshold, drift,
nick-floor, supply-burn) combo: crypt_valdris T1, forest_shadows T2,
manor_blackspire T3, abyss_portal T5. Each has a healthy sibling at
the same tier (goblin_warrens 44%, sunken_temple 13.5%, underforge
2.5%, dragons_lair 57.5%) so the gap is per-zone, not tier-wide.
Adds a structured per-fight trace hook (traceFightStruct + the
harnessFightTrace struct that mirrors the existing string trace) so
diagnostics can aggregate without parsing the formatted log line.
Mirror-format with traceFight; if a field is added, update both
paths.
TestExpeditionBalance_Phase4A_OutlierDiagnostic walks the four
outlier-vs-sibling pairs at 200 trials each on the Phase 3-A/3-B best
cell (e=23 d=1 burn=50) and reports per-monster appearances /
win-rate / avg HP loss / kill attribution + day-of-end histogram +
elite-vs-standard fight mix.
Findings:
- crypt_valdris: dual-killer elite pool (Wight 99 kills, Flameskull
68). Phase 2c left this zone untouched ("already dual-elite") but
both elites are over-tier for T1.
- forest_shadows: standard pool too lethal — Displacer Beast (53
kills, 38% win standard) + Bandit Captain (48 kills, 57% win).
- manor_blackspire: Wraith on the standard slot is dragging the
floor (85 kills, 45hp loss per win). Vampire Spawn + Revenant
elite pair is also rough.
- abyss_portal: Nalfeshnee mis-classified standard (86 kills, 2.8%
win at L17). Vrock at 79% win is borderline.
Phase 4-B applies per-zone roster tweaks (IsElite re-flag, drop a
deadly entry, soften a SpawnWeight) — no monster stat-block changes.
T3 may need a follow-up tier-wide pass since the sibling underforge
also sits at 2.5%; out of scope here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -504,6 +504,21 @@ func (h *expeditionHarness) runHarnessFight(zone ZoneDefinition, elite bool) Com
|
||||
monster.Name, h.char.HPMax, nick, hpBeforeFight, result.PlayerEndHP,
|
||||
enemy.Stats.AC, enemy.Stats.AttackBonus, outcome))
|
||||
}
|
||||
if h.traceFightStruct != nil {
|
||||
h.traceFightStruct(harnessFightTrace{
|
||||
Day: h.exp.CurrentDay,
|
||||
Tier: int(zone.Tier),
|
||||
Elite: elite,
|
||||
MonsterName: monster.Name,
|
||||
HPMax: h.char.HPMax,
|
||||
Nick: nick,
|
||||
HPPre: hpBeforeFight,
|
||||
HPPost: result.PlayerEndHP,
|
||||
EnemyAC: enemy.Stats.AC,
|
||||
EnemyAtk: enemy.Stats.AttackBonus,
|
||||
Won: result.PlayerWon,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -608,6 +623,30 @@ type expeditionHarness struct {
|
||||
// monster, or the combat fold itself is driving deaths. Nil in
|
||||
// production runs — has zero cost when unused.
|
||||
traceFight func(line string)
|
||||
// traceFightStruct, if non-nil, fires alongside traceFight with the
|
||||
// same fight's data as a parsed struct. Lets aggregate diagnostics
|
||||
// (e.g. Phase 4-A's per-monster attribution) skip the fragile parse
|
||||
// of the formatted line. Retreat events are not surfaced here — the
|
||||
// retreat trace is one-line only via traceFight. Nil in production.
|
||||
traceFightStruct func(harnessFightTrace)
|
||||
}
|
||||
|
||||
// harnessFightTrace is the structured per-fight record exposed via
|
||||
// traceFightStruct. Mirrors the fields the formatted traceFight line
|
||||
// already emits; if you add a field, update both paths so the human
|
||||
// log and the structured aggregate stay in sync.
|
||||
type harnessFightTrace struct {
|
||||
Day int
|
||||
Tier int
|
||||
Elite bool
|
||||
MonsterName string
|
||||
HPMax int
|
||||
Nick int
|
||||
HPPre int
|
||||
HPPost int
|
||||
EnemyAC int
|
||||
EnemyAtk int
|
||||
Won bool
|
||||
}
|
||||
|
||||
// harnessRNG is a thin wrapper around math/rand/v2 so the d20 helper
|
||||
|
||||
Reference in New Issue
Block a user