126 lines
3.5 KiB
Go
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
|
|
}
|