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

162 lines
3.9 KiB
Go

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
})
}