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
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
|
|
}
|