187 lines
6.2 KiB
Go
187 lines
6.2 KiB
Go
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
|
|
}
|