mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"sort"
|
||||
)
|
||||
@@ -90,6 +91,12 @@ type expeditionBalanceProfile struct {
|
||||
// CampType is the camp the player establishes each night.
|
||||
// Standard is the spike default; Phase 3 may sweep this per cell.
|
||||
CampType string
|
||||
// HarvestRollsPerDay overrides the harness's default combat-interrupt
|
||||
// roll count for the daytime phase. Zero means "use the package
|
||||
// default" (harnessHarvestRollsPerDay). Phase 2's cadence
|
||||
// calibration sweep sets this per cell; everyday callers leave it
|
||||
// zero.
|
||||
HarvestRollsPerDay int
|
||||
}
|
||||
|
||||
// expeditionTrialResult is the outcome of one simulated expedition.
|
||||
@@ -244,7 +251,7 @@ func (h *expeditionHarness) advanceExpeditionOneDay() expeditionTrialResult {
|
||||
}
|
||||
|
||||
// 4. Daytime combat-interrupt rolls.
|
||||
for i := 0; i < harnessHarvestRollsPerDay; i++ {
|
||||
for i := 0; i < h.rollsPerDay; i++ {
|
||||
kind, _ := resolveCombatInterrupt(
|
||||
h.exp.ThreatLevel, int(zone.Tier), h.char.Class, h.exp.ZoneID, h.rng.d20,
|
||||
)
|
||||
@@ -366,7 +373,20 @@ func (h *expeditionHarness) runHarnessFight(zone ZoneDefinition, elite bool) Com
|
||||
player.Stats.StartHP = h.char.HPCurrent
|
||||
}
|
||||
enemy := buildHarnessZoneEnemy(monster, int(zone.Tier))
|
||||
return simulateCombatWithRNG(player, enemy, dungeonCombatPhases, h.rng.r)
|
||||
hpBeforeFight := h.char.HPCurrent
|
||||
result := simulateCombatWithRNG(player, enemy, dungeonCombatPhases, h.rng.r)
|
||||
if h.traceFight != nil {
|
||||
outcome := "WON"
|
||||
if !result.PlayerWon {
|
||||
outcome = "LOST"
|
||||
}
|
||||
h.traceFight(fmt.Sprintf(
|
||||
"fight day=%d zone=%s tier=%d elite=%v monster=%s hp_max=%d nick=%d hp_pre=%d hp_post=%d enemy_ac=%d enemy_atk=%d → %s",
|
||||
h.exp.CurrentDay, h.exp.ZoneID, int(zone.Tier), elite,
|
||||
monster.Name, h.char.HPMax, nick, hpBeforeFight, result.PlayerEndHP,
|
||||
enemy.Stats.AC, enemy.Stats.AttackBonus, outcome))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// pickHarnessZoneEnemy is the harness's RNG-driven analogue of
|
||||
@@ -447,10 +467,17 @@ func buildHarnessZoneEnemy(monster DnDMonsterTemplate, tier int) Combatant {
|
||||
// expeditionHarness threads per-trial mutable state (RNG, expedition,
|
||||
// character HP carryover, encounter count) through the day loop.
|
||||
type expeditionHarness struct {
|
||||
exp *Expedition
|
||||
char *DnDCharacter
|
||||
rng *harnessRNG
|
||||
encounters int
|
||||
exp *Expedition
|
||||
char *DnDCharacter
|
||||
rng *harnessRNG
|
||||
encounters int
|
||||
rollsPerDay int // resolved from profile + default; never zero
|
||||
// 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
|
||||
// monster, or the combat fold itself is driving deaths. Nil in
|
||||
// production runs — has zero cost when unused.
|
||||
traceFight func(line string)
|
||||
}
|
||||
|
||||
// harnessRNG is a thin wrapper around math/rand/v2 so the d20 helper
|
||||
@@ -484,10 +511,15 @@ func runExpeditionBalanceTrial(p expeditionBalanceProfile, seed uint64) expediti
|
||||
Subclass: p.Subclass,
|
||||
Level: p.Level,
|
||||
})
|
||||
rolls := p.HarvestRollsPerDay
|
||||
if rolls <= 0 {
|
||||
rolls = harnessHarvestRollsPerDay
|
||||
}
|
||||
h := &expeditionHarness{
|
||||
exp: exp,
|
||||
char: char,
|
||||
rng: newHarnessRNG(seed),
|
||||
exp: exp,
|
||||
char: char,
|
||||
rng: newHarnessRNG(seed),
|
||||
rollsPerDay: rolls,
|
||||
}
|
||||
for {
|
||||
res := h.advanceExpeditionOneDay()
|
||||
|
||||
Reference in New Issue
Block a user