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.
95 lines
2.8 KiB
Go
95 lines
2.8 KiB
Go
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)),
|
|
}
|
|
}
|