You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

113 lines
3.5 KiB
Go

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
}