Files
mixmaker/backend/internal/domain/event_workflow.go

267 lines
7.6 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 {
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
}