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