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,161 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// CalculateStandings computes the stats for each team in a group given the match results.
|
||||
func CalculateStandings(teams []string, matches []Match) []*TeamStats {
|
||||
statsMap := make(map[string]*TeamStats)
|
||||
for _, t := range teams {
|
||||
statsMap[t] = &TeamStats{TeamName: t}
|
||||
}
|
||||
|
||||
for _, m := range matches {
|
||||
if !m.IsCompleted {
|
||||
continue
|
||||
}
|
||||
sa, sb := statsMap[m.TeamA], statsMap[m.TeamB]
|
||||
if sa == nil || sb == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sa.GoalsScored += m.ScoreA
|
||||
sa.GoalDifference += m.ScoreA - m.ScoreB
|
||||
sb.GoalsScored += m.ScoreB
|
||||
sb.GoalDifference += m.ScoreB - m.ScoreA
|
||||
|
||||
if m.ScoreA > m.ScoreB {
|
||||
sa.Points += 3
|
||||
} else if m.ScoreA < m.ScoreB {
|
||||
sb.Points += 3
|
||||
} else {
|
||||
sa.Points += 1
|
||||
sb.Points += 1
|
||||
}
|
||||
}
|
||||
|
||||
res := make([]*TeamStats, 0, len(teams))
|
||||
for _, s := range statsMap {
|
||||
res = append(res, s)
|
||||
}
|
||||
|
||||
// Sort the standings
|
||||
SortGroupStandings(res, matches)
|
||||
return res
|
||||
}
|
||||
|
||||
// SortGroupStandings sorts the teams in a group based on the tie-breaking rules.
|
||||
func SortGroupStandings(teams []*TeamStats, matches []Match) {
|
||||
sort.Slice(teams, func(i, j int) bool {
|
||||
ti, tj := teams[i], teams[j]
|
||||
|
||||
// 1. Points
|
||||
if ti.Points != tj.Points {
|
||||
return ti.Points > tj.Points
|
||||
}
|
||||
|
||||
// 2. Goal Difference
|
||||
if ti.GoalDifference != tj.GoalDifference {
|
||||
return ti.GoalDifference > tj.GoalDifference
|
||||
}
|
||||
|
||||
// 3. Goals Scored
|
||||
if ti.GoalsScored != tj.GoalsScored {
|
||||
return ti.GoalsScored > tj.GoalsScored
|
||||
}
|
||||
|
||||
// 4-6. Head-to-Head (H2H)
|
||||
// We dynamically compute H2H stats for all teams that are tied on Points, GD, and GS.
|
||||
// Since we are sorting, we can check the relationship between ti and tj.
|
||||
// First find all teams tied with ti and tj.
|
||||
tiedTeams := []string{}
|
||||
for _, t := range teams {
|
||||
if t.Points == ti.Points && t.GoalDifference == ti.GoalDifference && t.GoalsScored == ti.GoalsScored {
|
||||
tiedTeams = append(tiedTeams, t.TeamName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tiedTeams) > 1 {
|
||||
h2hStats := computeH2H(tiedTeams, matches)
|
||||
h2hi := h2hStats[ti.TeamName]
|
||||
h2hj := h2hStats[tj.TeamName]
|
||||
|
||||
if h2hi != nil && h2hj != nil {
|
||||
// H2H Points
|
||||
if h2hi.Points != h2hj.Points {
|
||||
return h2hi.Points > h2hj.Points
|
||||
}
|
||||
// H2H GD
|
||||
if h2hi.GoalDifference != h2hj.GoalDifference {
|
||||
return h2hi.GoalDifference > h2hj.GoalDifference
|
||||
}
|
||||
// H2H GS
|
||||
if h2hi.GoalsScored != h2hj.GoalsScored {
|
||||
return h2hi.GoalsScored > h2hj.GoalsScored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. No fallback (retains the shuffled input order to randomize tie-breaks in Monte Carlo)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// computeH2H calculates Points, GD, and GS only considering matches between the specified tied teams.
|
||||
func computeH2H(tiedTeams []string, matches []Match) map[string]*TeamStats {
|
||||
statsMap := make(map[string]*TeamStats)
|
||||
isTied := make(map[string]bool)
|
||||
for _, t := range tiedTeams {
|
||||
statsMap[t] = &TeamStats{TeamName: t}
|
||||
isTied[t] = true
|
||||
}
|
||||
|
||||
for _, m := range matches {
|
||||
if !m.IsCompleted {
|
||||
continue
|
||||
}
|
||||
if isTied[m.TeamA] && isTied[m.TeamB] {
|
||||
sa, sb := statsMap[m.TeamA], statsMap[m.TeamB]
|
||||
sa.GoalsScored += m.ScoreA
|
||||
sa.GoalDifference += m.ScoreA - m.ScoreB
|
||||
sb.GoalsScored += m.ScoreB
|
||||
sb.GoalDifference += m.ScoreB - m.ScoreA
|
||||
|
||||
if m.ScoreA > m.ScoreB {
|
||||
sa.Points += 3
|
||||
} else if m.ScoreA < m.ScoreB {
|
||||
sb.Points += 3
|
||||
} else {
|
||||
sa.Points += 1
|
||||
sb.Points += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return statsMap
|
||||
}
|
||||
|
||||
// CompareThirdPlaced ranks the third-placed teams across groups.
|
||||
func CompareThirdPlaced(thirdTeams []*TeamStats) {
|
||||
sort.Slice(thirdTeams, func(i, j int) bool {
|
||||
ti, tj := thirdTeams[i], thirdTeams[j]
|
||||
|
||||
// 1. Points
|
||||
if ti.Points != tj.Points {
|
||||
return ti.Points > tj.Points
|
||||
}
|
||||
|
||||
// 2. Goal Difference
|
||||
if ti.GoalDifference != tj.GoalDifference {
|
||||
return ti.GoalDifference > tj.GoalDifference
|
||||
}
|
||||
|
||||
// 3. Goals Scored
|
||||
if ti.GoalsScored != tj.GoalsScored {
|
||||
return ti.GoalsScored > tj.GoalsScored
|
||||
}
|
||||
|
||||
// 4. No fallback (retains the shuffled input order to randomize tie-breaks in Monte Carlo)
|
||||
return false
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user