Files
CosmicClash/server/domain/teams.go
T
2026-08-31 20:29:11 +01:00

117 lines
2.9 KiB
Go

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
}