mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
225 lines
7.3 KiB
Go
225 lines
7.3 KiB
Go
// Copyright 2022 Team 254. All Rights Reserved.
|
|
// Author: pat@patfairbank.com (Patrick Fairbank)
|
|
//
|
|
// Model and logic encapsulating a playoff elimination bracket.
|
|
|
|
package bracket
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/Team254/cheesy-arena-lite/model"
|
|
"time"
|
|
)
|
|
|
|
type Bracket struct {
|
|
FinalsMatchup *Matchup
|
|
}
|
|
|
|
const ElimMatchSpacingSec = 600
|
|
|
|
// Creates an unpopulated bracket with a format that is defined by the given matchup templates and number of alliances.
|
|
func newBracket(matchupTemplates []matchupTemplate, finalsMatchupKey matchupKey, numAlliances int) (*Bracket, error) {
|
|
// Create a map of matchup templates by key for easy lookup while creating the bracket.
|
|
matchupTemplateMap := make(map[matchupKey]matchupTemplate, len(matchupTemplates))
|
|
for _, matchupTemplate := range matchupTemplates {
|
|
matchupTemplateMap[matchupTemplate.matchupKey] = matchupTemplate
|
|
}
|
|
|
|
// Recursively build the bracket, starting with the finals matchup.
|
|
finalsMatchup, _, err := createMatchupGraph(
|
|
finalsMatchupKey, true, matchupTemplateMap, numAlliances, make(map[matchupKey]*Matchup),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Bracket{FinalsMatchup: finalsMatchup}, nil
|
|
}
|
|
|
|
// Recursive helper method to create the current matchup node and all of its children.
|
|
func createMatchupGraph(
|
|
matchupKey matchupKey,
|
|
useWinner bool,
|
|
matchupTemplateMap map[matchupKey]matchupTemplate,
|
|
numAlliances int,
|
|
matchupMap map[matchupKey]*Matchup,
|
|
) (*Matchup, int, error) {
|
|
matchupTemplate, ok := matchupTemplateMap[matchupKey]
|
|
if !ok {
|
|
return nil, 0, fmt.Errorf("could not find template for matchup %+v in the list of templates", matchupKey)
|
|
}
|
|
|
|
redAllianceIdFromSelection := matchupTemplate.redAllianceSource.allianceId
|
|
blueAllianceIdFromSelection := matchupTemplate.blueAllianceSource.allianceId
|
|
if redAllianceIdFromSelection > 0 || blueAllianceIdFromSelection > 0 {
|
|
// This is a leaf node in the matchup graph; the alliances will come from the alliance selection.
|
|
if redAllianceIdFromSelection == 0 || blueAllianceIdFromSelection == 0 {
|
|
return nil, 0, fmt.Errorf("both alliances must be populated either from selection or a lower round")
|
|
}
|
|
|
|
// Zero out alliance IDs that don't exist at this tournament to signal that this matchup doesn't need to be
|
|
// played.
|
|
if redAllianceIdFromSelection > numAlliances {
|
|
redAllianceIdFromSelection = 0
|
|
}
|
|
if blueAllianceIdFromSelection > numAlliances {
|
|
blueAllianceIdFromSelection = 0
|
|
}
|
|
|
|
if redAllianceIdFromSelection > 0 && blueAllianceIdFromSelection > 0 {
|
|
// This is a real matchup that will be played out.
|
|
matchup, ok := matchupMap[matchupKey]
|
|
if !ok {
|
|
matchup = &Matchup{
|
|
matchupTemplate: matchupTemplate,
|
|
RedAllianceId: redAllianceIdFromSelection,
|
|
BlueAllianceId: blueAllianceIdFromSelection,
|
|
}
|
|
matchupMap[matchupKey] = matchup
|
|
}
|
|
return matchup, 0, nil
|
|
}
|
|
if redAllianceIdFromSelection == 0 && blueAllianceIdFromSelection == 0 {
|
|
// This matchup should be pruned from the bracket since neither alliance has a valid source; this tournament
|
|
// is too small for this matchup to be played.
|
|
return nil, 0, nil
|
|
}
|
|
if useWinner {
|
|
if redAllianceIdFromSelection > 0 {
|
|
// The red alliance has a bye.
|
|
return nil, redAllianceIdFromSelection, nil
|
|
} else {
|
|
// The blue alliance has a bye.
|
|
return nil, blueAllianceIdFromSelection, nil
|
|
}
|
|
} else {
|
|
// There is no losing alliance to return; prune this matchup.
|
|
return nil, 0, nil
|
|
}
|
|
}
|
|
|
|
// Recurse to determine the lower-round red and blue matchups that will feed into this one, or the alliances that
|
|
// have a bye to this round.
|
|
redAllianceSourceMatchup, redByeAllianceId, err := createMatchupGraph(
|
|
matchupTemplate.redAllianceSource.matchupKey,
|
|
matchupTemplate.redAllianceSource.useWinner,
|
|
matchupTemplateMap,
|
|
numAlliances,
|
|
matchupMap,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
blueAllianceSourceMatchup, blueByeAllianceId, err := createMatchupGraph(
|
|
matchupTemplate.blueAllianceSource.matchupKey,
|
|
matchupTemplate.blueAllianceSource.useWinner,
|
|
matchupTemplateMap,
|
|
numAlliances,
|
|
matchupMap,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if redAllianceSourceMatchup == nil && redByeAllianceId == 0 &&
|
|
blueAllianceSourceMatchup == nil && blueByeAllianceId == 0 {
|
|
// This matchup should be pruned from the bracket since neither alliance has a valid source; this tournament is
|
|
// too small for this matchup to be played.
|
|
return nil, 0, nil
|
|
}
|
|
if redByeAllianceId > 0 && blueAllianceSourceMatchup == nil && blueByeAllianceId == 0 {
|
|
if useWinner {
|
|
// The red alliance has a bye.
|
|
return nil, redByeAllianceId, nil
|
|
} else {
|
|
// There is no losing alliance to return; prune this matchup.
|
|
return nil, 0, nil
|
|
}
|
|
}
|
|
if blueByeAllianceId > 0 && redAllianceSourceMatchup == nil && redByeAllianceId == 0 {
|
|
if useWinner {
|
|
// The blue alliance has a bye.
|
|
return nil, blueByeAllianceId, nil
|
|
} else {
|
|
// There is no losing alliance to return; prune this matchup.
|
|
return nil, 0, nil
|
|
}
|
|
}
|
|
|
|
// This is a real matchup that will be played out.
|
|
matchup, ok := matchupMap[matchupKey]
|
|
if !ok {
|
|
matchup = &Matchup{
|
|
matchupTemplate: matchupTemplate,
|
|
RedAllianceId: redByeAllianceId,
|
|
BlueAllianceId: blueByeAllianceId,
|
|
RedAllianceSourceMatchup: redAllianceSourceMatchup,
|
|
BlueAllianceSourceMatchup: blueAllianceSourceMatchup,
|
|
}
|
|
matchupMap[matchupKey] = matchup
|
|
}
|
|
return matchup, 0, nil
|
|
}
|
|
|
|
// Returns the winning alliance ID of the entire bracket, or 0 if it is not yet known.
|
|
func (bracket *Bracket) Winner() int {
|
|
return bracket.FinalsMatchup.winner()
|
|
}
|
|
|
|
// Returns the finalist alliance ID of the entire bracket, or 0 if it is not yet known.
|
|
func (bracket *Bracket) Finalist() int {
|
|
return bracket.FinalsMatchup.loser()
|
|
}
|
|
|
|
// Returns true if the bracket has been won, and false if it is still to be determined.
|
|
func (bracket *Bracket) IsComplete() bool {
|
|
return bracket.FinalsMatchup.isComplete()
|
|
}
|
|
|
|
// Traverses the bracket to update the state of each matchup based on match results, counting wins and creating or
|
|
// deleting matches as required.
|
|
func (bracket *Bracket) Update(database *model.Database, startTime *time.Time) error {
|
|
if err := bracket.FinalsMatchup.update(database); err != nil {
|
|
return err
|
|
}
|
|
|
|
if startTime != nil {
|
|
// Update the scheduled time for all matches that have yet to be run.
|
|
matches, err := database.GetMatchesByType("elimination")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
matchIndex := 0
|
|
for _, match := range matches {
|
|
if match.IsComplete() {
|
|
continue
|
|
}
|
|
match.Time = startTime.Add(time.Duration(matchIndex*ElimMatchSpacingSec) * time.Second)
|
|
if err = database.UpdateMatch(&match); err != nil {
|
|
return err
|
|
}
|
|
matchIndex++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Prints out each matchup within the bracket in level order, backwards from finals to earlier rounds, for debugging.
|
|
func (bracket *Bracket) print() {
|
|
matchupQueue := []*Matchup{bracket.FinalsMatchup}
|
|
for len(matchupQueue) > 0 {
|
|
matchup := matchupQueue[0]
|
|
fmt.Printf("%+v\n\n", matchup)
|
|
matchupQueue = matchupQueue[1:]
|
|
if matchup != nil {
|
|
if matchup.RedAllianceSourceMatchup != nil && matchup.redAllianceSource.useWinner {
|
|
matchupQueue = append(matchupQueue, matchup.RedAllianceSourceMatchup)
|
|
}
|
|
if matchup.BlueAllianceSourceMatchup != nil && matchup.blueAllianceSource.useWinner {
|
|
matchupQueue = append(matchupQueue, matchup.BlueAllianceSourceMatchup)
|
|
}
|
|
}
|
|
}
|
|
}
|