feat(football): implement World Cup 2026 simulator and Empirical-Poisson Mixture Model
Build and Push Docker Image / build (push) Successful in 3m38s
Build and Push Docker Image / build (push) Successful in 3m38s
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user