package domain import ( "fmt" "slices" ) type EventState string const ( RegistrationOpen EventState = "RegistrationOpen" RegistrationClosed EventState = "RegistrationClosed" Balancing EventState = "Balancing" RostersDraft EventState = "RostersDraft" RostersConfirmed EventState = "RostersConfirmed" Live EventState = "Live" Completed EventState = "Completed" Cancelled EventState = "Cancelled" ) func (e *Event) Transition(from []EventState, to EventState, expectedVersion int) error { if e.Version != expectedVersion { return fmt.Errorf("%w: stale event version", ErrConflict) } if !slices.Contains(from, e.State) { return fmt.Errorf("%w: event is %s", ErrConflict, e.State) } e.State = to e.Version++ return nil } type RosterDraft struct { EventID string `json:"eventId"` Teams []Team `json:"teams"` Reserve []string `json:"reserve"` Version int `json:"version"` Confirmed bool `json:"confirmed"` } func (r RosterDraft) Validate(requireCaptains bool) error { if r.EventID == "" || len(r.Teams) < 2 { return fmt.Errorf("%w: at least two teams are required", ErrInvalid) } seen := map[string]bool{} for _, team := range r.Teams { if len(team.Slots) != 5 { return fmt.Errorf("%w: every team must contain five players", ErrInvalid) } counts := map[Role]int{} for _, slot := range team.Slots { counts[slot.Role]++ if slot.PlayerID == "" { if requireCaptains { return fmt.Errorf("%w: every roster slot must be filled", ErrInvalid) } continue } if seen[slot.PlayerID] { return fmt.Errorf("%w: roster players must be unique", ErrInvalid) } seen[slot.PlayerID] = true } if counts[Tank] != 1 || counts[Damage] != 2 || counts[Support] != 2 { return fmt.Errorf("%w: every team must use 1/2/2", ErrInvalid) } if requireCaptains && !slices.ContainsFunc(team.Slots, func(slot Slot) bool { return slot.PlayerID == team.CaptainPlayerID }) { return fmt.Errorf("%w: every team needs a captain", ErrInvalid) } } for _, playerID := range r.Reserve { if playerID == "" || seen[playerID] { return fmt.Errorf("%w: reserve players must be unique", ErrInvalid) } seen[playerID] = true } return nil } func (r *RosterDraft) Swap(teamAID, playerAID, teamBID, playerBID string) error { if r.Confirmed { return fmt.Errorf("%w: rosters are locked", ErrConflict) } var a, b *Slot var teamA, teamB *Team for teamIndex := range r.Teams { for slotIndex := range r.Teams[teamIndex].Slots { slot := &r.Teams[teamIndex].Slots[slotIndex] if r.Teams[teamIndex].ID == teamAID && slot.PlayerID == playerAID { a = slot teamA = &r.Teams[teamIndex] } if r.Teams[teamIndex].ID == teamBID && slot.PlayerID == playerBID { b = slot teamB = &r.Teams[teamIndex] } } } if a == nil || b == nil { return ErrNotFound } if a.Role != b.Role { return fmt.Errorf("%w: only equal roles can be swapped", ErrInvalid) } a.PlayerID, b.PlayerID = b.PlayerID, a.PlayerID a.Rating, b.Rating = b.Rating, a.Rating clearMovedCaptains(teamA, playerAID, teamB, playerBID) r.Version++ return r.Validate(false) } func (r *RosterDraft) SwapAcrossRoles(teamAID, playerAID, teamBID, playerBID string, playerBRatingForA, playerARatingForB int) error { if r.Confirmed { return fmt.Errorf("%w: rosters are locked", ErrConflict) } var a, b *Slot var teamA, teamB *Team for teamIndex := range r.Teams { for slotIndex := range r.Teams[teamIndex].Slots { slot := &r.Teams[teamIndex].Slots[slotIndex] if r.Teams[teamIndex].ID == teamAID && slot.PlayerID == playerAID { a = slot teamA = &r.Teams[teamIndex] } if r.Teams[teamIndex].ID == teamBID && slot.PlayerID == playerBID { b = slot teamB = &r.Teams[teamIndex] } } } if a == nil || b == nil { return ErrNotFound } a.PlayerID, b.PlayerID = b.PlayerID, a.PlayerID a.Rating, b.Rating = playerBRatingForA, playerARatingForB clearMovedCaptains(teamA, playerAID, teamB, playerBID) r.Version++ return r.Validate(false) } func clearMovedCaptains(teamA *Team, playerAID string, teamB *Team, playerBID string) { if teamA == nil || teamB == nil || teamA.ID == teamB.ID { return } if teamA.CaptainPlayerID == playerAID { teamA.CaptainPlayerID = "" } if teamB.CaptainPlayerID == playerBID { teamB.CaptainPlayerID = "" } } func (r *RosterDraft) Substitute(teamID, outgoingID, reserveID string, rating int) error { if r.Confirmed { return fmt.Errorf("%w: rosters are locked", ErrConflict) } reserveIndex := slices.Index(r.Reserve, reserveID) if reserveIndex < 0 { return fmt.Errorf("%w: player is not in reserve", ErrInvalid) } for teamIndex := range r.Teams { if r.Teams[teamIndex].ID != teamID { continue } for slotIndex := range r.Teams[teamIndex].Slots { slot := &r.Teams[teamIndex].Slots[slotIndex] if slot.PlayerID == outgoingID { slot.PlayerID, slot.Rating = reserveID, rating r.Reserve[reserveIndex] = outgoingID if r.Teams[teamIndex].CaptainPlayerID == outgoingID { r.Teams[teamIndex].CaptainPlayerID = "" } r.Version++ return r.Validate(false) } } } return ErrNotFound } func (r *RosterDraft) PlaceReserve(teamID string, role Role, reserveID string, rating int) error { if r.Confirmed { return fmt.Errorf("%w: rosters are locked", ErrConflict) } reserveIndex := slices.Index(r.Reserve, reserveID) if reserveIndex < 0 { return fmt.Errorf("%w: player is not in reserve", ErrInvalid) } for teamIndex := range r.Teams { if r.Teams[teamIndex].ID != teamID { continue } for slotIndex := range r.Teams[teamIndex].Slots { slot := &r.Teams[teamIndex].Slots[slotIndex] if slot.Role == role && slot.PlayerID == "" { slot.PlayerID, slot.Rating = reserveID, rating r.Reserve = slices.Delete(r.Reserve, reserveIndex, reserveIndex+1) r.Version++ return r.Validate(false) } } } return fmt.Errorf("%w: empty role slot not found", ErrNotFound) } func (r *RosterDraft) MoveToEmpty(fromTeamID, playerID, toTeamID string, role Role, targetRating int) error { if r.Confirmed { return fmt.Errorf("%w: rosters are locked", ErrConflict) } var source, target *Slot var sourceTeam *Team for teamIndex := range r.Teams { team := &r.Teams[teamIndex] for slotIndex := range team.Slots { slot := &team.Slots[slotIndex] if team.ID == fromTeamID && slot.PlayerID == playerID { source, sourceTeam = slot, team } if team.ID == toTeamID && slot.Role == role && slot.PlayerID == "" { target = slot } } } if source == nil || target == nil { return ErrNotFound } target.PlayerID, target.Rating = source.PlayerID, targetRating source.PlayerID, source.Rating = "", 0 if sourceTeam.CaptainPlayerID == playerID { sourceTeam.CaptainPlayerID = "" } r.Version++ return r.Validate(false) } func (r *RosterDraft) MoveToReserve(teamID, playerID string) error { if r.Confirmed { return fmt.Errorf("%w: rosters are locked", ErrConflict) } for teamIndex := range r.Teams { if r.Teams[teamIndex].ID != teamID { continue } for slotIndex := range r.Teams[teamIndex].Slots { slot := &r.Teams[teamIndex].Slots[slotIndex] if slot.PlayerID == playerID { slot.PlayerID, slot.Rating = "", 0 r.Reserve = append(r.Reserve, playerID) if r.Teams[teamIndex].CaptainPlayerID == playerID { r.Teams[teamIndex].CaptainPlayerID = "" } r.Version++ return r.Validate(false) } } } return ErrNotFound } func (r *RosterDraft) EmergencySubstitute(teamID, outgoingID, reserveID string, rating int) error { confirmed := r.Confirmed r.Confirmed = false err := r.Substitute(teamID, outgoingID, reserveID, rating) r.Confirmed = confirmed return err }