package domain import ( "fmt" "reflect" "testing" ) func TestBalanceProducesDeterministicFullTeamsAndReserve(t *testing.T) { var players []Player for i := 0; i < 23; i++ { players = append(players, Player{ ID: fmt.Sprintf("p%02d", i), Ratings: Ratings{ Tank: 5 + i%36, Damage: 5 + (i*3)%36, Support: 5 + (i*7)%36, }, }) } first, err := Balance("event", players) if err != nil { t.Fatal(err) } second, _ := Balance("event", players) if !reflect.DeepEqual(first, second) { t.Fatal("balancer output is not deterministic") } if len(first) != 3 { t.Fatalf("got %d candidates", len(first)) } for _, candidate := range first { if len(candidate.Teams) != 4 || len(candidate.Reserve) != 3 { t.Fatalf("unexpected allocation: %d teams, %d reserve", len(candidate.Teams), len(candidate.Reserve)) } for _, team := range candidate.Teams { counts := map[Role]int{} for _, slot := range team.Slots { counts[slot.Role]++ } if counts[Tank] != 1 || counts[Damage] != 2 || counts[Support] != 2 { t.Fatalf("invalid composition: %#v", counts) } } } } func TestBalanceKeepsOddNumberOfFullTeams(t *testing.T) { players := make([]Player, 15) for i := range players { players[i] = Player{ ID: fmt.Sprintf("p%02d", i), Ratings: Ratings{Tank: 10 + i, Damage: 15 + i%20, Support: 8 + (i*3)%30}, } } candidates, err := Balance("event", players) if err != nil { t.Fatal(err) } for _, candidate := range candidates { if len(candidate.Teams) != 3 || len(candidate.Reserve) != 0 { t.Fatalf("expected three full teams, got %d teams and %d reserve", len(candidate.Teams), len(candidate.Reserve)) } } } func TestBalanceUsesSoftRoleAndTeammatePreferences(t *testing.T) { players := make([]Player, 10) for i := range players { players[i] = Player{ ID: fmt.Sprintf("p%02d", i), Ratings: Ratings{Tank: 20, Damage: 20, Support: 20}, } } players[0].PreferredRoles = []Role{Support} players[0].PreferredPlayerIDs = []string{"p01"} candidates, err := Balance("event", players) if err != nil { t.Fatal(err) } best := candidates[0] var teamForP0, teamForP1 int = -1, -1 var roleForP0 Role for teamIndex, team := range best.Teams { for _, slot := range team.Slots { switch slot.PlayerID { case "p00": teamForP0, roleForP0 = teamIndex, slot.Role case "p01": teamForP1 = teamIndex } } } if teamForP0 != teamForP1 || roleForP0 != Support { t.Fatalf("preferences were not reflected in best candidate: role=%s teams=%d/%d", roleForP0, teamForP0, teamForP1) } }