package domain import ( "fmt" "time" ) type CasualPhase string const ( CasualKickoff CasualPhase = "KICKOFF" CasualLive CasualPhase = "LIVE" ) type CasualSlot struct { Slot int Team int PlayerID string IsBot bool } // BuildCasualLineup freezes the live six-slot shape. Missing humans become // explicit server bots; no human is inserted after live play begins by this // function. func BuildCasualLineup(participants []ConnectParticipant) ([]CasualSlot, error) { if len(participants) < 2 || len(participants) > 6 { return nil, fmt.Errorf("casual lineup needs 2 through 6 humans") } seen := make(map[string]bool, len(participants)) teamHuman := map[int]bool{} lineup := make([]CasualSlot, 6) usedSlots := make(map[int]bool) for _, participant := range participants { if participant.PlayerID == "" || participant.Team < 0 || participant.Team > 1 || participant.Slot < 0 || participant.Slot > 5 || participant.Slot/3 != participant.Team || seen[participant.PlayerID] || usedSlots[participant.Slot] { return nil, fmt.Errorf("invalid casual participant") } seen[participant.PlayerID] = true usedSlots[participant.Slot] = true teamHuman[participant.Team] = true lineup[participant.Slot] = CasualSlot{Slot: participant.Slot, Team: participant.Team, PlayerID: participant.PlayerID} } if !teamHuman[0] || !teamHuman[1] { return nil, fmt.Errorf("casual lineup requires one human on each team") } for i := range lineup { if lineup[i].PlayerID == "" { lineup[i] = CasualSlot{Slot: i, Team: i / 3, PlayerID: fmt.Sprintf("bot-slot-%d", i), IsBot: true} } } return lineup, nil } func CanCasualBackfill(phase CasualPhase, slot CasualSlot) bool { return phase == CasualKickoff && slot.IsBot } // CasualBackfillPenalty is intentionally zero: a kickoff-only backfill does // not receive a hidden-rating update or an abandon/decline cooldown. func CasualBackfillPenalty() time.Duration { return 0 }