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 { if slot.PlayerID == "" || seen[slot.PlayerID] { return fmt.Errorf("%w: roster players must be unique", ErrInvalid) } seen[slot.PlayerID] = true counts[slot.Role]++ } 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 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 } if r.Teams[teamIndex].ID == teamBID && slot.PlayerID == playerBID { b = slot } } } 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 r.Version++ return r.Validate(false) } 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) 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 }