package game import ( "math/rand" "sort" "time" "github.com/denisovdennis/autohero/internal/model" ) // BalanceEnemyMode selects how the opponent is chosen for balance Monte Carlo. type BalanceEnemyMode int const ( // BalanceEnemyWolfOnly uses a single scaled Forest Wolf (canonical curve check). BalanceEnemyWolfOnly BalanceEnemyMode = iota // BalanceEnemyMixedSpawn matches PickEnemyForLevelWithRNG (weighted random template in band). BalanceEnemyMixedSpawn ) // BalanceMonteCarloResult aggregates outcomes from RunBalanceMonteCarlo. type BalanceMonteCarloResult struct { Iterations int Wins int WinRate float64 MedianDur time.Duration P90Dur time.Duration MeanDur time.Duration } var balanceSimStart = time.Unix(1_700_000_000, 0) // RunBalanceMonteCarlo runs N independent fights at hero level against scaled enemies. // Per-iteration RNG is derived from seed so results are reproducible. // Global math/rand is re-seeded per fight for damage/crit/dodge rolls (same as legacy combat). func RunBalanceMonteCarlo(level int, iterations int, seed int64, gearProfile ReferenceGearProfile, enemyMode BalanceEnemyMode) BalanceMonteCarloResult { if iterations <= 0 { return BalanceMonteCarloResult{} } var wins int durations := make([]time.Duration, 0, iterations) var sumDur time.Duration for i := 0; i < iterations; i++ { var gearRng *rand.Rand if gearProfile == ReferenceGearRolled { gearRng = rand.New(rand.NewSource(seed + int64(i)*1_000_003)) } baseHero := NewReferenceHeroForBalance(level, gearProfile, gearRng) hero := CloneHeroForCombatSim(baseHero) // Combat RNG (damage rolls, dodge, crit, debuff procs). rand.Seed(seed + int64(i)*9_999_983) var enemy model.Enemy switch enemyMode { case BalanceEnemyWolfOnly: tmpl := model.EnemyTemplates[model.EnemyWolf] enemy = ScaleEnemyTemplate(tmpl, level) case BalanceEnemyMixedSpawn: pickRNG := rand.New(rand.NewSource(seed + int64(i)*2_000_001)) enemy = PickEnemyForLevelWithRNG(level, pickRNG) default: tmpl := model.EnemyTemplates[model.EnemyWolf] enemy = ScaleEnemyTemplate(tmpl, level) } survived, elapsed := ResolveCombatToEndWithDuration(hero, &enemy, balanceSimStart, CombatSimOptions{ TickRate: 100 * time.Millisecond, }) if survived { wins++ } durations = append(durations, elapsed) sumDur += elapsed } sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] }) median := durations[len(durations)/2] p90idx := int(0.9 * float64(len(durations)-1)) if p90idx < 0 { p90idx = 0 } p90 := durations[p90idx] return BalanceMonteCarloResult{ Iterations: iterations, Wins: wins, WinRate: float64(wins) / float64(iterations), MedianDur: median, P90Dur: p90, MeanDur: time.Duration(int64(sumDur) / int64(iterations)), } }