package main import ( "fmt" "github.com/denisovdennis/autohero/internal/game" "github.com/denisovdennis/autohero/internal/model" ) // gearCheckScenariosForTemplate builds one fair-tier cell per level in the intersection of // [t.MinLevel..t.MaxLevel] with [levelMin..levelMax] when those flags are > 0 (0 = no bound from that side). func gearCheckScenariosForTemplate(t model.Enemy, levelMin, levelMax int) []gridScenario { minL := t.MinLevel maxL := t.MaxLevel if minL <= 0 || maxL < minL { lvl := (t.MinLevel + t.MaxLevel) / 2 if lvl < 1 { lvl = 1 } if t.BaseLevel > 0 { lvl = t.BaseLevel } if levelMin > 0 && lvl < levelMin { lvl = levelMin } if levelMax > 0 && lvl > levelMax { lvl = levelMax } if levelMin > 0 && levelMax > 0 && (lvl < levelMin || lvl > levelMax) { return nil } return []gridScenario{{heroLv: lvl, enemyLv: lvl, gearIdx: 0}} } if levelMin > 0 && levelMin > minL { minL = levelMin } if levelMax > 0 && levelMax < maxL { maxL = levelMax } if minL > maxL { return nil } out := make([]gridScenario, 0, maxL-minL+1) for lv := minL; lv <= maxL; lv++ { out = append(out, gridScenario{heroLv: lv, enemyLv: lv, gearIdx: 0}) } return out } // runGearCheck compares baseline (common) vs max (legendary) reference gear at each level. // Rules (best gear vs baseline): // - Kill time must not improve by more than maxSpeedupPct (median win duration max >= baseline * (1 - maxSpeedupPct/100)). // - Median hero HP%% on wins with best gear must be <= maxHeroHpPct/100. func runGearCheck( tmpl model.Enemy, et string, scenarios []gridScenario, n int, seedBase int64, maxSpeedupPct float64, maxHeroHpPct float64, strict bool, ) (failCells int, lines []string) { minDurRatio := 1.0 - maxSpeedupPct/100.0 maxHpFrac := maxHeroHpPct / 100.0 if minDurRatio < 0 || minDurRatio > 1 { minDurRatio = 0.80 } for _, sc := range scenarios { h := hashGridScenario(et, sc) heroB := game.NewReferenceHeroForBalance(sc.heroLv, game.ReferenceGearBaseline, nil) heroM := game.NewReferenceHeroForBalance(sc.heroLv, game.ReferenceGearMax, nil) rB := runOneGridScenario(tmpl, heroB, sc.enemyLv, n, seedBase, h) rM := runOneGridScenario(tmpl, heroM, sc.enemyLv, n, seedBase, h+0x100000) if rB.medianWinSec <= 0 { if strict { failCells++ lines = append(lines, fmt.Sprintf( "FAIL %s L%d: baseline had no median win duration (strict; winRate=%.1f%%)", et, sc.heroLv, 100*rB.winRate)) } else { lines = append(lines, fmt.Sprintf("# %s L%d: SKIP — baseline had no median win duration (winRate=%.1f%%)", et, sc.heroLv, 100*rB.winRate)) } continue } if rM.medianWinSec <= 0 { failCells++ lines = append(lines, fmt.Sprintf("FAIL %s L%d: max gear had no median win duration (winRate=%.1f%%)", et, sc.heroLv, 100*rM.winRate)) continue } // Faster kill = lower duration. Allow at most maxSpeedupPct faster => durM >= durB * minDurRatio. failDur := rM.medianWinSec < rB.medianWinSec*minDurRatio-1e-6 failHp := rM.medianHeroHp > maxHpFrac+1e-9 if failDur || failHp { failCells++ speedupPct := (1.0 - rM.medianWinSec/rB.medianWinSec) * 100.0 lines = append(lines, fmt.Sprintf( "FAIL %s L%d: baseline dur=%.1fs hp=%.1f%% | max dur=%.1fs (~%.1f%% faster) need dur>=%.1fs | max hp=%.1f%% need <=%.1f%% | dur_ok=%v hp_ok=%v", et, sc.heroLv, rB.medianWinSec, 100*rB.medianHeroHp, rM.medianWinSec, speedupPct, rB.medianWinSec*minDurRatio, 100*rM.medianHeroHp, maxHeroHpPct, !failDur, !failHp, )) } } return failCells, lines }