package domain import ( "fmt" "sort" "time" ) type PreparedProposal struct { Proposal Proposal CasualLineup []CasualSlot } // PrepareProposal is the boundary between matchmaking and proposal state. It // never creates a proposal for a casual formation without one human per team, // or for a ranked formation whose verified identity/arena metadata fails the // ranked admission policy. func PrepareProposal(id string, playlist Playlist, formation MatchFormation, rankedParticipants []RankedParticipant, arena RankedArena, now time.Time) (PreparedProposal, error) { if len(formation.Selection.Players) < 2 || len(formation.Selection.Players) > 6 { return PreparedProposal{}, fmt.Errorf("invalid formed player count") } playerIDs := make([]string, 0, len(formation.Selection.Players)) seen := make(map[string]bool, len(formation.Selection.Players)) for _, player := range formation.Selection.Players { if player.PlayerID == "" || seen[player.PlayerID] { return PreparedProposal{}, fmt.Errorf("invalid formed player identity") } seen[player.PlayerID] = true playerIDs = append(playerIDs, player.PlayerID) } var lineup []CasualSlot switch playlist { case Casual: participants := make([]ConnectParticipant, 0, len(formation.Selection.Players)) for index, player := range formation.Teams.Team0 { participants = append(participants, ConnectParticipant{PlayerID: player.PlayerID, Team: 0, Slot: index}) } for index, player := range formation.Teams.Team1 { participants = append(participants, ConnectParticipant{PlayerID: player.PlayerID, Team: 1, Slot: 3 + index}) } var err error lineup, err = BuildCasualLineup(participants) if err != nil { return PreparedProposal{}, err } case Ranked: if len(rankedParticipants) != len(playerIDs) { return PreparedProposal{}, fmt.Errorf("ranked metadata does not match formed players") } metadata := make(map[string]bool, len(rankedParticipants)) for _, participant := range rankedParticipants { metadata[participant.PlayerID] = true } if len(metadata) != len(playerIDs) { return PreparedProposal{}, fmt.Errorf("ranked metadata has duplicate or unknown players") } for _, playerID := range playerIDs { if !metadata[playerID] { return PreparedProposal{}, fmt.Errorf("ranked metadata missing formed player") } } if err := ValidateRankedAdmission(rankedParticipants, arena); err != nil { return PreparedProposal{}, err } default: return PreparedProposal{}, fmt.Errorf("unsupported playlist") } proposal, err := NewProposal(id, playlist, playerIDs, now) if err != nil { return PreparedProposal{}, err } if formation.Selection.Region == "" || len(formation.Selection.Players) == 0 || formation.Selection.Players[0].ProtocolVersion < 1 { return PreparedProposal{}, fmt.Errorf("formed match metadata is incomplete") } proposal.Region = formation.Selection.Region proposal.Protocol = formation.Selection.Players[0].ProtocolVersion if playlist == Ranked { proposal.ArenaPath = arena.Path } for _, player := range formation.Selection.Players { if player.ProtocolVersion != proposal.Protocol { return PreparedProposal{}, fmt.Errorf("formed match has mixed protocols") } } assignProposalSlots(&proposal, formation.Teams) return PreparedProposal{Proposal: proposal, CasualLineup: lineup}, nil } func assignProposalSlots(proposal *Proposal, teams Teams) { assign := func(team int, players []Candidate) { ordered := append([]Candidate(nil), players...) sort.Slice(ordered, func(i, j int) bool { return ordered[i].PlayerID < ordered[j].PlayerID }) for index, player := range ordered { for participant := range proposal.Participants { if proposal.Participants[participant].PlayerID == player.PlayerID { proposal.Participants[participant].Team = team proposal.Participants[participant].Slot = team*3 + index break } } } } assign(0, teams.Team0) assign(1, teams.Team1) }