package domain import ( "fmt" "sort" ) type Teams struct { Team0 []Candidate Team1 []Candidate } // PartitionTeams exhaustively evaluates balanced two-team assignments. The // first team is anchored to the lexically smallest player to remove the // equivalent team-0/team-1 mirror; this makes the result stable across worker // order and database row order. func PartitionTeams(players []Candidate) (Teams, error) { if len(players) < 2 || len(players) > 6 || len(players)%2 != 0 { return Teams{}, fmt.Errorf("team partition requires an even player count from 2 through 6") } ordered := append([]Candidate(nil), players...) sort.Slice(ordered, func(i, j int) bool { return ordered[i].PlayerID < ordered[j].PlayerID }) teamSize := len(ordered) / 2 anchor := ordered[0].PlayerID best := Teams{} found := false chosen := make([]Candidate, 0, teamSize) var visit func(int) visit = func(start int) { if len(chosen) == teamSize { if !containsPlayer(chosen, anchor) { return } team1 := make([]Candidate, 0, teamSize) for _, player := range ordered { if !containsPlayer(chosen, player.PlayerID) { team1 = append(team1, player) } } if !found || betterTeams(chosen, team1, best) { best = Teams{Team0: append([]Candidate(nil), chosen...), Team1: team1} found = true } return } for i := start; i < len(ordered); i++ { chosen = append(chosen, ordered[i]) visit(i + 1) chosen = chosen[:len(chosen)-1] } } visit(0) if !found { return Teams{}, fmt.Errorf("no balanced team partition") } return best, nil } func betterTeams(team0, team1 []Candidate, best Teams) bool { if best.Team0 == nil { return true } meanDelta, maxOpposing := teamScore(team0, team1) bestMean, bestMaxOpposing := teamScore(best.Team0, best.Team1) if meanDelta != bestMean { return meanDelta < bestMean } if maxOpposing != bestMaxOpposing { return maxOpposing < bestMaxOpposing } return playerIDs(team0) < playerIDs(best.Team0) } func teamScore(team0, team1 []Candidate) (float64, float64) { mean0, mean1 := meanRating(team0), meanRating(team1) maxOpposing := 0.0 for _, left := range team0 { for _, right := range team1 { delta := abs(left.Rating - right.Rating) if delta > maxOpposing { maxOpposing = delta } } } return abs(mean0 - mean1), maxOpposing } func meanRating(players []Candidate) float64 { total := 0.0 for _, player := range players { total += player.Rating } return total / float64(len(players)) } func containsPlayer(players []Candidate, playerID string) bool { for _, player := range players { if player.PlayerID == playerID { return true } } return false } func playerIDs(players []Candidate) string { ids := make([]string, 0, len(players)) for _, player := range players { ids = append(ids, player.PlayerID) } sort.Strings(ids) result := "" for _, id := range ids { result += id + "\x00" } return result }