Add event management features to API, including event cancellation, deletion, registration closure, and workflow balance generation. Introduce new roster management endpoints and enhance participant handling with avoided player preferences. Update database schema and service logic to support these changes.
This commit is contained in:
186
backend/internal/domain/series_fsm.go
Normal file
186
backend/internal/domain/series_fsm.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SeriesPhase string
|
||||
|
||||
const (
|
||||
CoinTossPending SeriesPhase = "CoinTossPending"
|
||||
MapBanPhase SeriesPhase = "MapBan"
|
||||
MapPickPhase SeriesPhase = "MapPick"
|
||||
HeroBanPhase SeriesPhase = "HeroBan"
|
||||
PlayingPhase SeriesPhase = "Playing"
|
||||
SeriesComplete SeriesPhase = "Completed"
|
||||
)
|
||||
|
||||
type SeriesAction struct {
|
||||
Kind string `json:"kind"`
|
||||
Summary string `json:"summary"`
|
||||
TeamID string `json:"teamId,omitempty"`
|
||||
ActorAccountID string `json:"actorAccountId"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
type SeriesMap struct {
|
||||
Number int `json:"number"`
|
||||
Name string `json:"name"`
|
||||
HeroBans []DraftAction `json:"heroBans"`
|
||||
Result *MapResult `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func NewSeries(id, eventID, tournamentID string, teams [2]string, rules Ruleset) (*Series, error) {
|
||||
if id == "" || eventID == "" || teams[0] == "" || teams[1] == "" || teams[0] == teams[1] {
|
||||
return nil, fmt.Errorf("%w: invalid series", ErrInvalid)
|
||||
}
|
||||
if err := rules.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Series{
|
||||
ID: id, EventID: eventID, TournamentID: tournamentID,
|
||||
TeamAID: teams[0], TeamBID: teams[1], BestOf: rules.BestOf,
|
||||
RulesetID: rules.ID, Phase: CoinTossPending,
|
||||
SeriesBans: map[string][]string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Series) Toss(seed, actor string, now time.Time, rules Ruleset) error {
|
||||
if s.Phase != CoinTossPending {
|
||||
return fmt.Errorf("%w: coin toss is unavailable", ErrConflict)
|
||||
}
|
||||
toss, err := TossCoin(s.TeamAID, s.TeamBID, seed, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.CoinToss = &toss
|
||||
first := toss.WinnerTeamID
|
||||
if rules.InitialMapBanner == 1 {
|
||||
first = s.otherTeam(first)
|
||||
}
|
||||
draft, err := NewMapDraft(s.availablePool(rules, 0), first, [2]string{s.TeamAID, s.TeamBID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.MapDraft, s.Phase = draft, MapBanPhase
|
||||
s.Version++
|
||||
s.Audit = append(s.Audit, SeriesAction{Kind: "coin_toss", Summary: "Coin toss won by " + toss.WinnerTeamID, TeamID: toss.WinnerTeamID, ActorAccountID: actor, At: now})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Series) BanMap(teamID, name, actor string, now time.Time, rules Ruleset) error {
|
||||
if s.Phase != MapBanPhase || s.MapDraft == nil {
|
||||
return fmt.Errorf("%w: map ban is unavailable", ErrConflict)
|
||||
}
|
||||
if err := s.MapDraft.Ban(teamID, name, actor, now); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Version++
|
||||
s.Audit = append(s.Audit, SeriesAction{Kind: "map_ban", Summary: name + " banned", TeamID: teamID, ActorAccountID: actor, At: now})
|
||||
selected, complete := s.MapDraft.Selected()
|
||||
if !complete {
|
||||
return nil
|
||||
}
|
||||
return s.startHeroDraft(selected, actor, now, rules)
|
||||
}
|
||||
|
||||
func (s *Series) PickMap(teamID, name, actor string, now time.Time, rules Ruleset) error {
|
||||
if s.Phase != MapPickPhase {
|
||||
return fmt.Errorf("%w: map pick is unavailable", ErrConflict)
|
||||
}
|
||||
if teamID != s.NextMapPickerID {
|
||||
return fmt.Errorf("%w: only the previous map loser can pick", ErrConflict)
|
||||
}
|
||||
if !slices.Contains(s.AvailableMaps, name) {
|
||||
return fmt.Errorf("%w: map unavailable", ErrInvalid)
|
||||
}
|
||||
s.Version++
|
||||
s.Audit = append(s.Audit, SeriesAction{Kind: "map_selected", Summary: name + " picked", TeamID: teamID, ActorAccountID: actor, At: now})
|
||||
return s.startHeroDraft(name, actor, now, rules)
|
||||
}
|
||||
|
||||
func (s *Series) startHeroDraft(selected, actor string, now time.Time, rules Ruleset) error {
|
||||
s.CurrentMap = selected
|
||||
first := s.otherTeam(s.CoinToss.WinnerTeamID)
|
||||
if rules.InitialHeroBanner == 0 {
|
||||
first = s.CoinToss.WinnerTeamID
|
||||
}
|
||||
draft, err := NewHeroDraft(rules.Heroes, [2]string{s.TeamAID, s.TeamBID}, first, rules.HeroBansPerTeam, s.SeriesBans)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.HeroDraft, s.Phase = draft, HeroBanPhase
|
||||
s.NextMapPickerID, s.AvailableMaps = "", nil
|
||||
s.Maps = append(s.Maps, SeriesMap{Number: len(s.Maps) + 1, Name: selected})
|
||||
if len(s.Audit) == 0 || s.Audit[len(s.Audit)-1].Kind != "map_selected" {
|
||||
s.Audit = append(s.Audit, SeriesAction{Kind: "map_selected", Summary: selected + " selected", ActorAccountID: actor, At: now})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Series) BanHero(teamID, hero, actor string, now time.Time) error {
|
||||
if s.Phase != HeroBanPhase || s.HeroDraft == nil {
|
||||
return fmt.Errorf("%w: hero ban is unavailable", ErrConflict)
|
||||
}
|
||||
if err := s.HeroDraft.Ban(teamID, hero, actor, now); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Version++
|
||||
s.Maps[len(s.Maps)-1].HeroBans = slices.Clone(s.HeroDraft.CurrentBans)
|
||||
s.Audit = append(s.Audit, SeriesAction{Kind: "hero_ban", Summary: hero + " banned", TeamID: teamID, ActorAccountID: actor, At: now})
|
||||
if s.HeroDraft.Complete() {
|
||||
s.SeriesBans = s.HeroDraft.SeriesBans
|
||||
s.Phase = PlayingPhase
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Series) RecordCurrentMap(outcome MapOutcome, actor string, now time.Time, rules Ruleset) error {
|
||||
if s.Phase != PlayingPhase || s.CurrentMap == "" {
|
||||
return fmt.Errorf("%w: map result is unavailable", ErrConflict)
|
||||
}
|
||||
result := MapResult{MapName: s.CurrentMap, Outcome: outcome, ActorAccountID: actor, RecordedAt: now}
|
||||
if err := s.RecordResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
s.PlayedMaps = append(s.PlayedMaps, s.CurrentMap)
|
||||
s.Maps[len(s.Maps)-1].Result = &result
|
||||
s.Audit = append(s.Audit, SeriesAction{Kind: "result", Summary: s.CurrentMap + ": " + string(outcome), ActorAccountID: actor, At: now})
|
||||
if s.WinnerTeamID != "" {
|
||||
s.Phase = SeriesComplete
|
||||
return nil
|
||||
}
|
||||
poolIndex := len(s.Maps)
|
||||
if poolIndex >= len(rules.MapPools) {
|
||||
poolIndex = len(rules.MapPools) - 1
|
||||
}
|
||||
switch outcome {
|
||||
case TeamAWin:
|
||||
s.NextMapPickerID = s.TeamBID
|
||||
case TeamBWin:
|
||||
s.NextMapPickerID = s.TeamAID
|
||||
default:
|
||||
// A draw has no loser, so the initial coin-toss loser receives the pick.
|
||||
s.NextMapPickerID = s.otherTeam(s.CoinToss.WinnerTeamID)
|
||||
}
|
||||
s.AvailableMaps = s.availablePool(rules, poolIndex)
|
||||
if len(s.AvailableMaps) == 0 {
|
||||
return fmt.Errorf("%w: no unplayed maps remain in pool", ErrInvalid)
|
||||
}
|
||||
s.MapDraft, s.HeroDraft, s.CurrentMap, s.Phase = nil, nil, "", MapPickPhase
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Series) availablePool(rules Ruleset, index int) []string {
|
||||
pool := slices.Clone(rules.MapPools[index])
|
||||
return slices.DeleteFunc(pool, func(name string) bool { return slices.Contains(s.PlayedMaps, name) })
|
||||
}
|
||||
|
||||
func (s Series) otherTeam(teamID string) string {
|
||||
if teamID == s.TeamAID {
|
||||
return s.TeamBID
|
||||
}
|
||||
return s.TeamAID
|
||||
}
|
||||
Reference in New Issue
Block a user