137 lines
3.9 KiB
Go
137 lines
3.9 KiB
Go
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
|
|
}
|