Files
own-tools/pkg/football/logic/simulator.go
T
2026-06-19 16:15:13 -07:00

126 lines
3.5 KiB
Go

package logic
import (
"math/rand"
"sort"
"time"
)
// RunMonteCarlo runs a Monte Carlo simulation of the remaining matches.
// Returns a slice of TeamProbability sorted by Group and then by TotalQualProb descending.
func RunMonteCarlo(teams []Team, matches []Match, scoreOutcomes []ScoreOutcome, numSimulations int) []TeamProbability {
if numSimulations <= 0 {
numSimulations = 50000
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
sm := NewScoreModel(scoreOutcomes)
// Map teams to groups
teamGroups := make(map[string]string)
groupsTeams := make(map[string][]string)
for _, t := range teams {
teamGroups[t.Name] = t.Group
groupsTeams[t.Group] = append(groupsTeams[t.Group], t.Name)
}
directCounts := make(map[string]int)
thirdCounts := make(map[string]int)
// Separate completed and uncompleted matches
var completed []Match
var uncompleted []Match
for _, m := range matches {
if m.IsCompleted {
completed = append(completed, m)
} else {
uncompleted = append(uncompleted, m)
}
}
for sim := 0; sim < numSimulations; sim++ {
// Clone completed matches and simulate uncompleted ones
simMatches := make([]Match, len(completed)+len(uncompleted))
copy(simMatches, completed)
for i, m := range uncompleted {
outcome := sm.RandomScore(r)
simMatches[len(completed)+i] = Match{
TeamA: m.TeamA,
TeamB: m.TeamB,
IsCompleted: true,
ScoreA: outcome.ScoreA,
ScoreB: outcome.ScoreB,
}
}
// Calculate standings for each group
thirdPlaced := make([]*TeamStats, 0, 12)
for grp, grpTeams := range groupsTeams {
var grpMatches []Match
for _, m := range simMatches {
if teamGroups[m.TeamA] == grp && teamGroups[m.TeamB] == grp {
grpMatches = append(grpMatches, m)
}
}
// Shuffle group teams to randomize tie-breaks in SortGroupStandings
shuffledGrpTeams := make([]string, len(grpTeams))
copy(shuffledGrpTeams, grpTeams)
r.Shuffle(len(shuffledGrpTeams), func(i, j int) {
shuffledGrpTeams[i], shuffledGrpTeams[j] = shuffledGrpTeams[j], shuffledGrpTeams[i]
})
standings := CalculateStandings(shuffledGrpTeams, grpMatches)
// Top 2 qualify directly
if len(standings) > 0 {
directCounts[standings[0].TeamName]++
}
if len(standings) > 1 {
directCounts[standings[1].TeamName]++
}
// 3rd placed team goes to the pool
if len(standings) > 2 {
thirdPlaced = append(thirdPlaced, standings[2])
}
}
// Shuffle third-placed list to randomize cross-group tie-breaks in CompareThirdPlaced
r.Shuffle(len(thirdPlaced), func(i, j int) {
thirdPlaced[i], thirdPlaced[j] = thirdPlaced[j], thirdPlaced[i]
})
CompareThirdPlaced(thirdPlaced)
limit := 8
if len(thirdPlaced) < limit {
limit = len(thirdPlaced)
}
for i := 0; i < limit; i++ {
thirdCounts[thirdPlaced[i].TeamName]++
}
}
// Build result slice
res := make([]TeamProbability, 0, len(teams))
for _, t := range teams {
dQual := float64(directCounts[t.Name]) / float64(numSimulations)
tQual := float64(thirdCounts[t.Name]) / float64(numSimulations)
res = append(res, TeamProbability{
TeamName: t.Name,
Group: t.Group,
DirectQualProb: dQual,
ThirdQualProb: tQual,
TotalQualProb: dQual + tQual,
})
}
// Sort results by Group name first, then by TotalQualProb descending
sort.Slice(res, func(i, j int) bool {
if res[i].Group != res[j].Group {
return res[i].Group < res[j].Group
}
return res[i].TotalQualProb > res[j].TotalQualProb
})
return res
}