mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
325 lines
11 KiB
Go
325 lines
11 KiB
Go
// Copyright 2022 Team 254. All Rights Reserved.
|
|
// Author: pat@patfairbank.com (Patrick Fairbank)
|
|
//
|
|
// Models and logic encapsulating a group of one or more matches between the same two alliances at a given point in a
|
|
// playoff tournament.
|
|
|
|
package bracket
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/Team254/cheesy-arena-lite/model"
|
|
"strconv"
|
|
)
|
|
|
|
// Conveys how a given alliance should be populated -- either directly from alliance selection or based on the results
|
|
// of a prior matchup.
|
|
type allianceSource struct {
|
|
allianceId int
|
|
matchupKey matchupKey
|
|
useWinner bool
|
|
}
|
|
|
|
// Key for uniquely identifying a matchup. Round IDs are arbitrary and in descending order with "1" always representing
|
|
// the playoff finals. Group IDs are 1-indexed within a round and increasing in order of play.
|
|
type matchupKey struct {
|
|
Round int
|
|
Group int
|
|
}
|
|
|
|
// Conveys the complete generic information about a matchup required to construct it. In aggregate, the full list of
|
|
// match templates describing a bracket format can be used to construct an empty playoff bracket for a given number of
|
|
// alliances.
|
|
type matchupTemplate struct {
|
|
matchupKey
|
|
displayName string
|
|
NumWinsToAdvance int
|
|
redAllianceSource allianceSource
|
|
blueAllianceSource allianceSource
|
|
}
|
|
|
|
// Encapsulates the format and state of a group of one or more matches between the same two alliances at a given point
|
|
// in a playoff tournament.
|
|
type Matchup struct {
|
|
matchupTemplate
|
|
redAllianceSourceMatchup *Matchup
|
|
blueAllianceSourceMatchup *Matchup
|
|
RedAllianceId int
|
|
BlueAllianceId int
|
|
RedAllianceWins int
|
|
BlueAllianceWins int
|
|
}
|
|
|
|
// Convenience method to quickly create an alliance source that points to the winner of a different matchup.
|
|
func newWinnerAllianceSource(round, group int) allianceSource {
|
|
return allianceSource{matchupKey: newMatchupKey(round, group), useWinner: true}
|
|
}
|
|
|
|
// Convenience method to quickly create an alliance source that points to the loser of a different matchup.
|
|
func newLoserAllianceSource(round, group int) allianceSource {
|
|
return allianceSource{matchupKey: newMatchupKey(round, group), useWinner: false}
|
|
}
|
|
|
|
// Convenience method to quickly create a matchup key.
|
|
func newMatchupKey(round, group int) matchupKey {
|
|
return matchupKey{Round: round, Group: group}
|
|
}
|
|
|
|
// Returns the display name for a specific match within a matchup.
|
|
func (matchupTemplate *matchupTemplate) matchDisplayName(instance int) string {
|
|
displayName := matchupTemplate.displayName
|
|
if matchupTemplate.NumWinsToAdvance > 1 || instance > 1 {
|
|
// Append the instance if there is always more than one match in the series, or in exceptional circumstances
|
|
// like a tie in double-elimination unresolved by tiebreakers.
|
|
displayName += fmt.Sprintf("-%d", instance)
|
|
}
|
|
return displayName
|
|
}
|
|
|
|
// Returns the display name for the overall matchup.
|
|
func (matchup *Matchup) LongDisplayName() string {
|
|
if matchup.isFinal() {
|
|
return "Finals"
|
|
}
|
|
if _, err := strconv.Atoi(matchup.displayName); err == nil {
|
|
return "Match " + matchup.displayName
|
|
}
|
|
return matchup.displayName
|
|
}
|
|
|
|
// Returns the display name for the linked matchup from which the red alliance is populated.
|
|
func (matchup *Matchup) RedAllianceSourceDisplayName() string {
|
|
if matchup.redAllianceSourceMatchup == nil {
|
|
return ""
|
|
}
|
|
if matchup.redAllianceSource.useWinner {
|
|
return "W " + matchup.redAllianceSourceMatchup.displayName
|
|
}
|
|
return "L " + matchup.redAllianceSourceMatchup.displayName
|
|
}
|
|
|
|
// Returns the display name for the linked matchup from which the blue alliance is populated.
|
|
func (matchup *Matchup) BlueAllianceSourceDisplayName() string {
|
|
if matchup.blueAllianceSourceMatchup == nil {
|
|
return ""
|
|
}
|
|
if matchup.blueAllianceSource.useWinner {
|
|
return "W " + matchup.blueAllianceSourceMatchup.displayName
|
|
}
|
|
return "L " + matchup.blueAllianceSourceMatchup.displayName
|
|
}
|
|
|
|
// Returns a pair of strings indicating the leading alliance and a readable status of the matchup.
|
|
func (matchup *Matchup) StatusText() (string, string) {
|
|
var leader, status string
|
|
winText := "Advances"
|
|
if matchup.isFinal() {
|
|
winText = "Wins"
|
|
}
|
|
if matchup.RedAllianceWins >= matchup.NumWinsToAdvance {
|
|
leader = "red"
|
|
status = fmt.Sprintf("Red %s %d-%d", winText, matchup.RedAllianceWins, matchup.BlueAllianceWins)
|
|
} else if matchup.BlueAllianceWins >= matchup.NumWinsToAdvance {
|
|
leader = "blue"
|
|
status = fmt.Sprintf("Blue %s %d-%d", winText, matchup.BlueAllianceWins, matchup.RedAllianceWins)
|
|
} else if matchup.RedAllianceWins > matchup.BlueAllianceWins {
|
|
leader = "red"
|
|
status = fmt.Sprintf("Red Leads %d-%d", matchup.RedAllianceWins, matchup.BlueAllianceWins)
|
|
} else if matchup.BlueAllianceWins > matchup.RedAllianceWins {
|
|
leader = "blue"
|
|
status = fmt.Sprintf("Blue Leads %d-%d", matchup.BlueAllianceWins, matchup.RedAllianceWins)
|
|
} else if matchup.RedAllianceWins > 0 {
|
|
status = fmt.Sprintf("Series Tied %d-%d", matchup.RedAllianceWins, matchup.BlueAllianceWins)
|
|
}
|
|
return leader, status
|
|
}
|
|
|
|
// Returns the winning alliance ID of the matchup, or 0 if it is not yet known.
|
|
func (matchup *Matchup) winner() int {
|
|
if matchup.RedAllianceWins >= matchup.NumWinsToAdvance {
|
|
return matchup.RedAllianceId
|
|
}
|
|
if matchup.BlueAllianceWins >= matchup.NumWinsToAdvance {
|
|
return matchup.BlueAllianceId
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Returns the losing alliance ID of the matchup, or 0 if it is not yet known.
|
|
func (matchup *Matchup) loser() int {
|
|
if matchup.RedAllianceWins >= matchup.NumWinsToAdvance {
|
|
return matchup.BlueAllianceId
|
|
}
|
|
if matchup.BlueAllianceWins >= matchup.NumWinsToAdvance {
|
|
return matchup.RedAllianceId
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Returns true if the matchup has been won, and false if it is still to be determined.
|
|
func (matchup *Matchup) isComplete() bool {
|
|
return matchup.winner() > 0
|
|
}
|
|
|
|
// Returns true if the matchup represents the final matchup in the bracket.
|
|
func (matchup *Matchup) isFinal() bool {
|
|
return matchup.displayName == "F"
|
|
}
|
|
|
|
// Recursively traverses the matchup graph to update the state of this matchup and all of its children based on match
|
|
// results, counting wins and creating or deleting matches as required.
|
|
func (matchup *Matchup) update(database *model.Database) error {
|
|
// Update child matchups first. Only recurse down winner links to avoid visiting a node twice.
|
|
if matchup.redAllianceSourceMatchup != nil && matchup.redAllianceSource.useWinner {
|
|
if err := matchup.redAllianceSourceMatchup.update(database); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if matchup.blueAllianceSourceMatchup != nil && matchup.blueAllianceSource.useWinner {
|
|
if err := matchup.blueAllianceSourceMatchup.update(database); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Populate the alliance IDs from the lower matchups (or with a zero value if they are not yet complete).
|
|
if matchup.redAllianceSourceMatchup != nil {
|
|
if matchup.redAllianceSource.useWinner {
|
|
matchup.RedAllianceId = matchup.redAllianceSourceMatchup.winner()
|
|
} else {
|
|
matchup.RedAllianceId = matchup.redAllianceSourceMatchup.loser()
|
|
}
|
|
}
|
|
if matchup.blueAllianceSourceMatchup != nil {
|
|
if matchup.blueAllianceSource.useWinner {
|
|
matchup.BlueAllianceId = matchup.blueAllianceSourceMatchup.winner()
|
|
} else {
|
|
matchup.BlueAllianceId = matchup.blueAllianceSourceMatchup.loser()
|
|
}
|
|
}
|
|
|
|
matches, err := database.GetMatchesByElimRoundGroup(matchup.Round, matchup.Group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Bail if we do not yet know both alliances.
|
|
if matchup.RedAllianceId == 0 || matchup.BlueAllianceId == 0 {
|
|
// Ensure the current state is reset; it may have previously been populated if a match result was edited.
|
|
matchup.RedAllianceWins = 0
|
|
matchup.BlueAllianceWins = 0
|
|
|
|
// Delete any previously created matches.
|
|
for _, match := range matches {
|
|
if err = database.DeleteMatch(match.Id); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Create, update, and/or delete unplayed matches as required.
|
|
redAlliance, err := database.GetAllianceById(matchup.RedAllianceId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if redAlliance == nil {
|
|
return fmt.Errorf("alliance %d does not exist in the database", matchup.RedAllianceId)
|
|
}
|
|
blueAlliance, err := database.GetAllianceById(matchup.BlueAllianceId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if blueAlliance == nil {
|
|
return fmt.Errorf("alliance %d does not exist in the database", matchup.BlueAllianceId)
|
|
}
|
|
matchup.RedAllianceWins = 0
|
|
matchup.BlueAllianceWins = 0
|
|
var unplayedMatches []model.Match
|
|
for _, match := range matches {
|
|
if !match.IsComplete() {
|
|
// Update the teams in the match if they are not yet set or are incorrect.
|
|
changed := false
|
|
if match.Red1 != redAlliance.Lineup[0] || match.Red2 != redAlliance.Lineup[1] ||
|
|
match.Red3 != redAlliance.Lineup[2] {
|
|
positionRedTeams(&match, redAlliance)
|
|
match.ElimRedAlliance = redAlliance.Id
|
|
changed = true
|
|
if err = database.UpdateMatch(&match); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if match.Blue1 != blueAlliance.Lineup[0] || match.Blue2 != blueAlliance.Lineup[1] ||
|
|
match.Blue3 != blueAlliance.Lineup[2] {
|
|
positionBlueTeams(&match, blueAlliance)
|
|
match.ElimBlueAlliance = blueAlliance.Id
|
|
changed = true
|
|
}
|
|
if changed {
|
|
if err = database.UpdateMatch(&match); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
unplayedMatches = append(unplayedMatches, match)
|
|
continue
|
|
}
|
|
|
|
// Check who won.
|
|
if match.Status == model.RedWonMatch {
|
|
matchup.RedAllianceWins++
|
|
} else if match.Status == model.BlueWonMatch {
|
|
matchup.BlueAllianceWins++
|
|
}
|
|
}
|
|
|
|
maxWins := matchup.RedAllianceWins
|
|
if matchup.BlueAllianceWins > maxWins {
|
|
maxWins = matchup.BlueAllianceWins
|
|
}
|
|
numUnplayedMatchesNeeded := matchup.NumWinsToAdvance - maxWins
|
|
if len(unplayedMatches) > numUnplayedMatchesNeeded {
|
|
// Delete any superfluous matches off the end of the list.
|
|
for i := 0; i < len(unplayedMatches)-numUnplayedMatchesNeeded; i++ {
|
|
if err = database.DeleteMatch(unplayedMatches[len(unplayedMatches)-i-1].Id); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if len(unplayedMatches) < numUnplayedMatchesNeeded {
|
|
// Create initial set of matches or any additional required matches due to tie matches or ties in the round.
|
|
for i := 0; i < numUnplayedMatchesNeeded-len(unplayedMatches); i++ {
|
|
instance := len(matches) + i + 1
|
|
match := model.Match{
|
|
Type: "elimination",
|
|
DisplayName: matchup.matchDisplayName(instance),
|
|
ElimRound: matchup.Round,
|
|
ElimGroup: matchup.Group,
|
|
ElimInstance: instance,
|
|
ElimRedAlliance: redAlliance.Id,
|
|
ElimBlueAlliance: blueAlliance.Id,
|
|
}
|
|
positionRedTeams(&match, redAlliance)
|
|
positionBlueTeams(&match, blueAlliance)
|
|
if err = database.CreateMatch(&match); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Assigns the lineup from the alliance into the red team slots for the match.
|
|
func positionRedTeams(match *model.Match, alliance *model.Alliance) {
|
|
match.Red1 = alliance.Lineup[0]
|
|
match.Red2 = alliance.Lineup[1]
|
|
match.Red3 = alliance.Lineup[2]
|
|
}
|
|
|
|
// Assigns the lineup from the alliance into the blue team slots for the match.
|
|
func positionBlueTeams(match *model.Match, alliance *model.Alliance) {
|
|
match.Blue1 = alliance.Lineup[0]
|
|
match.Blue2 = alliance.Lineup[1]
|
|
match.Blue3 = alliance.Lineup[2]
|
|
}
|