162 lines
3.9 KiB
Go
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
|
|
})
|
|
}
|