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.

161 lines
4.9 KiB
Go

package game
import (
"math"
"testing"
"time"
"github.com/denisovdennis/autohero/internal/model"
"github.com/denisovdennis/autohero/internal/tuning"
)
func TestBandIndexForHeroLevel(t *testing.T) {
cases := []struct {
L int
want int
}{
{1, 0}, {9, 0},
{10, 1}, {19, 1},
{20, 2}, {29, 2},
{30, 3}, {39, 3},
{40, 4}, {49, 4},
{0, -1}, {50, -1},
}
for _, tc := range cases {
if got := bandIndexForHeroLevel(tc.L); got != tc.want {
t.Fatalf("bandIndexForHeroLevel(%d)=%d want %d", tc.L, got, tc.want)
}
}
}
func TestTemplateProgressionBand(t *testing.T) {
if TemplateProgressionBand(model.Enemy{MinLevel: 1, MaxLevel: 5, BaseLevel: 1}) != 0 {
t.Fatal("low-tier template should be band 0")
}
// Midpoint (25+35)/2 = 30 → tier band index 2 (see TemplateProgressionBand).
if TemplateProgressionBand(model.Enemy{MinLevel: 25, MaxLevel: 35, BaseLevel: 25}) != 2 {
t.Fatalf("mid 30 should map to template band 2, got %d", TemplateProgressionBand(model.Enemy{MinLevel: 25, MaxLevel: 35}))
}
if TemplateProgressionBand(model.Enemy{MinLevel: 41, MaxLevel: 45, BaseLevel: 41}) != 4 {
t.Fatalf("late template should map to band 4, got %d", TemplateProgressionBand(model.Enemy{MinLevel: 41, MaxLevel: 45}))
}
}
func TestAggregateBandDurationsPure(t *testing.T) {
// One second per level-up; 9+10+10+10+10 = 49 levels
sec := make([]float64, 49)
for i := range sec {
sec[i] = 1
}
var band [5]time.Duration
for idx, L := range levelRange(1, 49) {
bi := bandIndexForHeroLevel(L)
if bi >= 0 {
band[bi] += time.Duration(sec[idx] * float64(time.Second))
}
}
if band[0] != 9*time.Second {
t.Fatalf("band0: %v", band[0])
}
for i := 1; i <= 4; i++ {
if band[i] != 10*time.Second {
t.Fatalf("band %d: %v", i, band[i])
}
}
}
func TestGlobalScaleMonotonicTotalTime(t *testing.T) {
ensureTestEnemyTemplates()
cfg := tuning.DefaultValues()
tuning.Set(cfg)
t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })
base := CloneEnemyTemplates(EnemyTemplatesFromSlice(model.EnemyTemplates))
params := ProgressionSimParams{
IterationsPerLevel: 6,
Seed: 42,
RestAfterCombat: 10 * time.Second,
Gear: ReferenceGearMedian,
AccountLosses: false,
MinHeroLevel: 1,
MaxHeroLevelInclusive: 15,
}
slow := ApplyXPRewardScaleSpec(base, XPRewardScaleSpec{Global: 0.2, Elite: 1, PerBand: [5]float64{1, 1, 1, 1, 1}})
fast := ApplyXPRewardScaleSpec(base, XPRewardScaleSpec{Global: 5.0, Elite: 1, PerBand: [5]float64{1, 1, 1, 1, 1}})
rSlow, err := SimulateProgressionBands(params, slow)
if err != nil {
t.Fatal(err)
}
rFast, err := SimulateProgressionBands(params, fast)
if err != nil {
t.Fatal(err)
}
if rSlow.Total <= rFast.Total {
t.Fatalf("expected higher XP scale to reduce total time: slow=%s fast=%s", rSlow.Total, rFast.Total)
}
}
func TestProratedBandTargets(t *testing.T) {
full := DefaultProgressionBandTargets
// Through L29: full bands 0,1,2 → 1+3+6 weeks
pr := ProratedBandTargets(29, full)
sum := SumBandTargets(pr)
wantWeeks := 10 // 1+3+6
if got := int(sum / (7 * 24 * time.Hour)); got != wantWeeks {
t.Fatalf("ProratedBandTargets(29) sum weeks=%d want %d (%v)", got, wantWeeks, pr)
}
// Through L9: only band 0 full
pr2 := ProratedBandTargets(9, full)
if SumBandTargets(pr2) != full[0] {
t.Fatalf("band0 only: %v", pr2)
}
}
func TestEnforceMonotonicXPRewardByTier(t *testing.T) {
m := map[string]model.Enemy{
"a": {Slug: "a", MinLevel: 1, MaxLevel: 5, XPReward: 10},
"b": {Slug: "b", MinLevel: 20, MaxLevel: 30, XPReward: 5},
}
out := EnforceMonotonicXPRewardByTier(m)
if out["b"].XPReward <= out["a"].XPReward {
t.Fatalf("higher tier should have strictly higher xp: a=%d b=%d", out["a"].XPReward, out["b"].XPReward)
}
}
func TestOptimizePerTypeScalesRuns(t *testing.T) {
ensureTestEnemyTemplates()
cfg := tuning.DefaultValues()
tuning.Set(cfg)
t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })
base := CloneEnemyTemplates(EnemyTemplatesFromSlice(model.EnemyTemplates))
params := ProgressionSimParams{
IterationsPerLevel: 3,
Seed: 1,
RestAfterCombat: 5 * time.Second,
Gear: ReferenceGearMedian,
MinHeroLevel: 1,
MaxHeroLevelInclusive: 6,
}
targets := ProratedBandTargets(6, DefaultProgressionBandTargets)
m, _, res, sq := OptimizePerTypeScales(base, params, targets, 1, 6, false)
if len(m) != len(base) {
t.Fatalf("len(perType)=%d len(base)=%d", len(m), len(base))
}
if math.IsNaN(sq) {
t.Fatal("sqErr nan")
}
if math.IsInf(res.TotalSec, 1) {
t.Skip("combat unwinnable in testdata")
}
}
func TestSquaredErrorSum(t *testing.T) {
sim := [5]time.Duration{10 * time.Second, 20 * time.Second, 30 * time.Second, 40 * time.Second, 50 * time.Second}
tg := [5]time.Duration{10 * time.Second, 10 * time.Second, 10 * time.Second, 10 * time.Second, 10 * time.Second}
if SquaredErrorSum(sim, tg) <= 0 {
t.Fatal("expected positive error")
}
}