Refactor playoff bracket logic to more easily support alternative formats.

This commit is contained in:
Patrick Fairbank
2022-08-03 20:53:16 -07:00
parent 737972f494
commit bfc5e56fc9
15 changed files with 995 additions and 530 deletions

181
bracket/bracket.go Normal file
View File

@@ -0,0 +1,181 @@
// 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, 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 := createMatchupTree(newMatchupKey(1, 1), matchupTemplateMap, numAlliances)
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 createMatchupTree(
matchupKey matchupKey, matchupTemplateMap map[matchupKey]matchupTemplate, numAlliances int,
) (*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 tree; 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.
return &Matchup{
matchupTemplate: matchupTemplate,
RedAllianceId: redAllianceIdFromSelection,
BlueAllianceId: blueAllianceIdFromSelection,
}, 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 redAllianceIdFromSelection > 0 {
// The red alliance has a bye.
return nil, redAllianceIdFromSelection, nil
} else {
// The blue alliance has a bye.
return nil, blueAllianceIdFromSelection, 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 := createMatchupTree(
matchupTemplate.redAllianceSource.matchupKey, matchupTemplateMap, numAlliances,
)
if err != nil {
return nil, 0, err
}
blueAllianceSourceMatchup, blueByeAllianceId, err := createMatchupTree(
matchupTemplate.blueAllianceSource.matchupKey, matchupTemplateMap, numAlliances,
)
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 {
// The red alliance has a bye.
return nil, redByeAllianceId, nil
}
if blueByeAllianceId > 0 && redAllianceSourceMatchup == nil && redByeAllianceId == 0 {
// The blue alliance has a bye.
return nil, blueByeAllianceId, nil
}
// This is a real matchup that will be played out.
return &Matchup{
matchupTemplate: matchupTemplate,
RedAllianceId: redByeAllianceId,
BlueAllianceId: blueByeAllianceId,
RedAllianceSourceMatchup: redAllianceSourceMatchup,
BlueAllianceSourceMatchup: blueAllianceSourceMatchup,
}, 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 {
matchupQueue = append(matchupQueue, matchup.RedAllianceSourceMatchup)
matchupQueue = append(matchupQueue, matchup.BlueAllianceSourceMatchup)
}
}
}

171
bracket/bracket_test.go Normal file
View File

@@ -0,0 +1,171 @@
// Copyright 2022 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package bracket
import (
"github.com/Team254/cheesy-arena-lite/model"
"github.com/Team254/cheesy-arena-lite/tournament"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewBracketErrors(t *testing.T) {
_, err := newBracket([]matchupTemplate{}, 8)
if assert.NotNil(t, err) {
assert.Equal(t, "could not find template for matchup {round:1 group:1} in the list of templates", err.Error())
}
matchTemplate := matchupTemplate{
matchupKey: newMatchupKey(1, 1),
redAllianceSource: allianceSource{allianceId: 1},
blueAllianceSource: newMatchupAllianceSource(2, 2),
}
_, err = newBracket([]matchupTemplate{matchTemplate}, 8)
if assert.NotNil(t, err) {
assert.Equal(t, "both alliances must be populated either from selection or a lower round", err.Error())
}
}
func TestNewBracketInverseSeeding(t *testing.T) {
database := setupTestDb(t)
matchupTemplates := []matchupTemplate{
{
matchupKey: newMatchupKey(1, 1),
displayNameFormat: "F-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(2, 1),
blueAllianceSource: newMatchupAllianceSource(2, 2),
},
{
matchupKey: newMatchupKey(2, 1),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(4, 2),
blueAllianceSource: newMatchupAllianceSource(4, 1),
},
{
matchupKey: newMatchupKey(2, 2),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 3},
blueAllianceSource: allianceSource{allianceId: 2},
},
{
matchupKey: newMatchupKey(4, 1),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 8},
blueAllianceSource: allianceSource{allianceId: 1},
},
{
matchupKey: newMatchupKey(4, 2),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 5},
blueAllianceSource: allianceSource{allianceId: 4},
},
}
tournament.CreateTestAlliances(database, 2)
bracket, err := newBracket(matchupTemplates, 2)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 2, len(matches)) {
assertMatch(t, matches[0], "F-1", 1, 2)
assertMatch(t, matches[1], "F-2", 1, 2)
}
}
func TestBracketUpdateTiming(t *testing.T) {
database := setupTestDb(t)
tournament.CreateTestAlliances(database, 4)
bracket, err := NewSingleEliminationBracket(4)
assert.Nil(t, err)
startTime := time.Unix(1000, 0)
assert.Nil(t, bracket.Update(database, &startTime))
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 4, len(matches)) {
assert.Equal(t, int64(1000), matches[0].Time.Unix())
assert.Equal(t, int64(1600), matches[1].Time.Unix())
assert.Equal(t, int64(2200), matches[2].Time.Unix())
assert.Equal(t, int64(2800), matches[3].Time.Unix())
}
scoreMatch(database, "SF1-1", model.RedWonMatch)
scoreMatch(database, "SF1-2", model.BlueWonMatch)
startTime = time.Unix(5000, 0)
assert.Nil(t, bracket.Update(database, &startTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 5, len(matches)) {
assert.Equal(t, int64(1000), matches[0].Time.Unix())
assert.Equal(t, int64(5000), matches[1].Time.Unix())
assert.Equal(t, int64(2200), matches[2].Time.Unix())
assert.Equal(t, int64(5600), matches[3].Time.Unix())
assert.Equal(t, int64(6200), matches[4].Time.Unix())
}
}
func TestBracketUpdateTeamPositions(t *testing.T) {
database := setupTestDb(t)
tournament.CreateTestAlliances(database, 4)
bracket, err := NewSingleEliminationBracket(4)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ := database.GetMatchesByType("elimination")
match1 := matches[0]
match2 := matches[1]
assert.Equal(t, 102, match1.Red1)
assert.Equal(t, 101, match1.Red2)
assert.Equal(t, 103, match1.Red3)
assert.Equal(t, 302, match2.Blue1)
assert.Equal(t, 301, match2.Blue2)
assert.Equal(t, 303, match2.Blue3)
// Shuffle the team positions and check that the subsequent matches in the same round have the same ones.
match1.Red1, match1.Red2 = match1.Red2, 104
match2.Blue1, match2.Blue3 = 305, match2.Blue1
database.UpdateMatch(&match1)
database.UpdateMatch(&match2)
scoreMatch(database, "SF1-1", model.RedWonMatch)
scoreMatch(database, "SF2-1", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 4, len(matches)) {
assert.Equal(t, match1.Red1, matches[0].Red1)
assert.Equal(t, match1.Red2, matches[0].Red2)
assert.Equal(t, match1.Red3, matches[0].Red3)
assert.Equal(t, match1.Red1, matches[2].Red1)
assert.Equal(t, match1.Red2, matches[2].Red2)
assert.Equal(t, match1.Red3, matches[2].Red3)
assert.Equal(t, match2.Blue1, matches[1].Blue1)
assert.Equal(t, match2.Blue2, matches[1].Blue2)
assert.Equal(t, match2.Blue3, matches[1].Blue3)
assert.Equal(t, match2.Blue1, matches[3].Blue1)
assert.Equal(t, match2.Blue2, matches[3].Blue2)
assert.Equal(t, match2.Blue3, matches[3].Blue3)
}
// Advance them to the finals and verify that the team position updates have been propagated.
scoreMatch(database, "SF1-2", model.RedWonMatch)
scoreMatch(database, "SF2-2", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 6, len(matches)) {
for i := 4; i < 6; i++ {
assert.Equal(t, match1.Red1, matches[i].Red1)
assert.Equal(t, match1.Red2, matches[i].Red2)
assert.Equal(t, match1.Red3, matches[i].Red3)
assert.Equal(t, match2.Blue1, matches[i].Blue1)
assert.Equal(t, match2.Blue2, matches[i].Blue2)
assert.Equal(t, match2.Blue3, matches[i].Blue3)
}
}
}

228
bracket/matchup.go Normal file
View File

@@ -0,0 +1,228 @@
// 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 (
"github.com/Team254/cheesy-arena-lite/model"
"strconv"
"strings"
)
// 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
}
// 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
displayNameFormat 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 a different matchup.
func newMatchupAllianceSource(round, group int) allianceSource {
return allianceSource{matchupKey: newMatchupKey(round, group)}
}
// 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) displayName(instance int) string {
displayName := matchupTemplate.displayNameFormat
displayName = strings.Replace(displayName, "${group}", strconv.Itoa(matchupTemplate.group), -1)
displayName = strings.Replace(displayName, "${instance}", strconv.Itoa(instance), -1)
return displayName
}
// 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
}
// Recursively traverses the matchup tree 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 {
if matchup.RedAllianceSourceMatchup != nil {
if err := matchup.RedAllianceSourceMatchup.update(database); err != nil {
return err
}
}
if matchup.BlueAllianceSourceMatchup != nil {
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 {
matchup.RedAllianceId = matchup.RedAllianceSourceMatchup.winner()
}
if matchup.BlueAllianceSourceMatchup != nil {
matchup.BlueAllianceId = matchup.BlueAllianceSourceMatchup.winner()
}
// 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
return nil
}
// Create, update, and/or delete unplayed matches as required.
redAlliance, err := database.GetAllianceById(matchup.RedAllianceId)
if err != nil {
return err
}
blueAlliance, err := database.GetAllianceById(matchup.BlueAllianceId)
if err != nil {
return err
}
matches, err := database.GetMatchesByElimRoundGroup(matchup.round, matchup.group)
if err != nil {
return err
}
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.displayName(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]
}

View File

@@ -0,0 +1,128 @@
// Copyright 2022 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Defines the tournament structure for a single-elimination, best-of-three bracket.
package bracket
import "fmt"
// Creates an unpopulated single-elimination bracket containing only the required matchups for the given number of
// alliances.
func NewSingleEliminationBracket(numAlliances int) (*Bracket, error) {
if numAlliances < 2 {
return nil, fmt.Errorf("Must have at least 2 alliances")
}
if numAlliances > 16 {
return nil, fmt.Errorf("Must have at most 16 alliances")
}
return newBracket(singleEliminationBracketMatchupTemplates, numAlliances)
}
var singleEliminationBracketMatchupTemplates = []matchupTemplate{
{
matchupKey: newMatchupKey(1, 1),
displayNameFormat: "F-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(2, 1),
blueAllianceSource: newMatchupAllianceSource(2, 2),
},
{
matchupKey: newMatchupKey(2, 1),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(4, 1),
blueAllianceSource: newMatchupAllianceSource(4, 2),
},
{
matchupKey: newMatchupKey(2, 2),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(4, 3),
blueAllianceSource: newMatchupAllianceSource(4, 4),
},
{
matchupKey: newMatchupKey(4, 1),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 1),
blueAllianceSource: newMatchupAllianceSource(8, 2),
},
{
matchupKey: newMatchupKey(4, 2),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 3),
blueAllianceSource: newMatchupAllianceSource(8, 4),
},
{
matchupKey: newMatchupKey(4, 3),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 5),
blueAllianceSource: newMatchupAllianceSource(8, 6),
},
{
matchupKey: newMatchupKey(4, 4),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 7),
blueAllianceSource: newMatchupAllianceSource(8, 8),
},
{
matchupKey: newMatchupKey(8, 1),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 1},
blueAllianceSource: allianceSource{allianceId: 16},
},
{
matchupKey: newMatchupKey(8, 2),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 8},
blueAllianceSource: allianceSource{allianceId: 9},
},
{
matchupKey: newMatchupKey(8, 3),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 4},
blueAllianceSource: allianceSource{allianceId: 13},
},
{
matchupKey: newMatchupKey(8, 4),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 5},
blueAllianceSource: allianceSource{allianceId: 12},
},
{
matchupKey: newMatchupKey(8, 5),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 2},
blueAllianceSource: allianceSource{allianceId: 15},
},
{
matchupKey: newMatchupKey(8, 6),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 7},
blueAllianceSource: allianceSource{allianceId: 10},
},
{
matchupKey: newMatchupKey(8, 7),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 3},
blueAllianceSource: allianceSource{allianceId: 14},
},
{
matchupKey: newMatchupKey(8, 8),
displayNameFormat: "EF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: allianceSource{allianceId: 6},
blueAllianceSource: allianceSource{allianceId: 11},
},
}

View File

@@ -1,21 +1,25 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Copyright 2022 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package tournament
package bracket
import (
"github.com/Team254/cheesy-arena-lite/model"
"github.com/Team254/cheesy-arena-lite/tournament"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestEliminationScheduleInitial(t *testing.T) {
var dummyStartTime = time.Unix(0, 0)
func TestSingleEliminationInitial(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 2)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 2)
bracket, err := NewSingleEliminationBracket(2)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 2, len(matches)) {
@@ -25,9 +29,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 3)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 3)
bracket, err = NewSingleEliminationBracket(3)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 2, len(matches)) {
@@ -37,9 +42,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 4)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 4)
bracket, err = NewSingleEliminationBracket(4)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 4, len(matches)) {
@@ -51,9 +57,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 5)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 5)
bracket, err = NewSingleEliminationBracket(5)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 4, len(matches)) {
@@ -65,9 +72,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 6)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 6)
bracket, err = NewSingleEliminationBracket(6)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 4, len(matches)) {
@@ -79,9 +87,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 7)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 7)
bracket, err = NewSingleEliminationBracket(7)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
@@ -95,9 +104,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 8)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 8)
bracket, err = NewSingleEliminationBracket(8)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
@@ -113,9 +123,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 9)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 9)
bracket, err = NewSingleEliminationBracket(9)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
@@ -131,9 +142,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 10)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 10)
bracket, err = NewSingleEliminationBracket(10)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
@@ -149,9 +161,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 11)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 11)
bracket, err = NewSingleEliminationBracket(11)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
@@ -167,9 +180,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 12)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 12)
bracket, err = NewSingleEliminationBracket(12)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
@@ -185,9 +199,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 13)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 13)
bracket, err = NewSingleEliminationBracket(13)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 10, len(matches)) {
@@ -205,9 +220,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 14)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 14)
bracket, err = NewSingleEliminationBracket(14)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 12, len(matches)) {
@@ -227,9 +243,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 15)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 15)
bracket, err = NewSingleEliminationBracket(15)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 14, len(matches)) {
@@ -251,9 +268,10 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateAlliances()
database.TruncateMatches()
CreateTestAlliances(database, 16)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 16)
bracket, err = NewSingleEliminationBracket(16)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 16, len(matches)) {
@@ -278,34 +296,29 @@ func TestEliminationScheduleInitial(t *testing.T) {
database.TruncateMatches()
}
func TestEliminationScheduleErrors(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 1)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
func TestSingleEliminationErrors(t *testing.T) {
_, err := NewSingleEliminationBracket(1)
if assert.NotNil(t, err) {
assert.Equal(t, "Must have at least 2 alliances", err.Error())
}
database.TruncateAlliances()
CreateTestAlliances(database, 17)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
_, err = NewSingleEliminationBracket(17)
if assert.NotNil(t, err) {
assert.Equal(t, "Round of depth 32 is not supported", err.Error())
assert.Equal(t, "Must have at most 16 alliances", err.Error())
}
database.TruncateAlliances()
}
func TestEliminationSchedulePopulatePartialMatch(t *testing.T) {
func TestSingleEliminationPopulatePartialMatch(t *testing.T) {
database := setupTestDb(t)
// Final should be updated after semifinal is concluded.
CreateTestAlliances(database, 3)
UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 3)
bracket, err := NewSingleEliminationBracket(3)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF2-1", model.BlueWonMatch)
scoreMatch(database, "SF2-2", model.BlueWonMatch)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 4, len(matches)) {
@@ -317,19 +330,19 @@ func TestEliminationSchedulePopulatePartialMatch(t *testing.T) {
database.TruncateMatchResults()
// Final should be generated and populated as both semifinals conclude.
CreateTestAlliances(database, 4)
UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 4)
bracket, err = NewSingleEliminationBracket(4)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF2-1", model.RedWonMatch)
scoreMatch(database, "SF2-2", model.RedWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 4, len(matches))
scoreMatch(database, "SF1-1", model.RedWonMatch)
scoreMatch(database, "SF1-2", model.RedWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
@@ -341,29 +354,27 @@ func TestEliminationSchedulePopulatePartialMatch(t *testing.T) {
database.TruncateMatchResults()
}
func TestEliminationScheduleCreateNextRound(t *testing.T) {
func TestSingleEliminationCreateNextRound(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 4)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "SF1-1", model.BlueWonMatch)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 4)
bracket, err := NewSingleEliminationBracket(4)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF1-1", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ := database.GetMatchesByType("elimination")
assert.Equal(t, 4, len(matches))
scoreMatch(database, "SF2-1", model.BlueWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 4, len(matches))
scoreMatch(database, "SF1-2", model.BlueWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 4, len(matches))
scoreMatch(database, "SF2-2", model.BlueWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 6, len(matches)) {
assertMatch(t, matches[4], "F-1", 4, 3)
@@ -371,29 +382,31 @@ func TestEliminationScheduleCreateNextRound(t *testing.T) {
}
}
func TestEliminationScheduleDetermineWinner(t *testing.T) {
func TestSingleEliminationDetermineWinner(t *testing.T) {
database := setupTestDb(t)
// Round with one tie and a sweep.
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", model.TieMatch)
won, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 2)
bracket, err := NewSingleEliminationBracket(2)
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "F-1", model.TieMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
assert.Equal(t, 0, bracket.Winner())
assert.Equal(t, 0, bracket.Finalist())
matches, _ := database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-2", model.BlueWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-3", model.BlueWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.Nil(t, err) {
assert.True(t, won)
}
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.True(t, bracket.IsComplete())
assert.Equal(t, 2, bracket.Winner())
assert.Equal(t, 1, bracket.Finalist())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
database.TruncateAlliances()
@@ -401,78 +414,72 @@ func TestEliminationScheduleDetermineWinner(t *testing.T) {
database.TruncateMatchResults()
// Round with one tie and a split.
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", model.RedWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 2)
bracket, err = NewSingleEliminationBracket(2)
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "F-1", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 2, len(matches))
scoreMatch(database, "F-2", model.TieMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-3", model.BlueWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 4, len(matches))
assert.Equal(t, "F-4", matches[3].DisplayName)
scoreMatch(database, "F-4", model.TieMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
scoreMatch(database, "F-5", model.RedWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.Nil(t, err) {
assert.True(t, won)
}
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.True(t, bracket.IsComplete())
assert.Equal(t, 1, bracket.Winner())
assert.Equal(t, 2, bracket.Finalist())
database.TruncateAlliances()
database.TruncateMatches()
database.TruncateMatchResults()
// Round with two ties.
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", model.TieMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 2)
bracket, err = NewSingleEliminationBracket(2)
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "F-1", model.TieMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-2", model.BlueWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-3", model.TieMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 4, len(matches)) {
assert.Equal(t, "F-4", matches[3].DisplayName)
}
scoreMatch(database, "F-4", model.BlueWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.Nil(t, err) {
assert.True(t, won)
}
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.True(t, bracket.IsComplete())
database.TruncateAlliances()
database.TruncateMatches()
database.TruncateMatchResults()
// Round with repeated ties.
CreateTestAlliances(database, 2)
tournament.CreateTestAlliances(database, 2)
updateAndAssertSchedule := func(expectedNumMatches int, expectedWon bool) {
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Equal(t, expectedWon, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.Equal(t, expectedWon, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, expectedNumMatches, len(matches))
}
@@ -497,65 +504,59 @@ func TestEliminationScheduleDetermineWinner(t *testing.T) {
updateAndAssertSchedule(9, true)
}
func TestEliminationScheduleRemoveUnneededMatches(t *testing.T) {
func TestSingleEliminationRemoveUnneededMatches(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 2)
bracket, err := NewSingleEliminationBracket(2)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "F-1", model.RedWonMatch)
scoreMatch(database, "F-2", model.TieMatch)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, _ := database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
// Check that the third match is deleted if the score is changed.
scoreMatch(database, "F-2", model.RedWonMatch)
won, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.True(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.True(t, bracket.IsComplete())
// Check that the deleted match is recreated if the score is changed.
scoreMatch(database, "F-2", model.BlueWonMatch)
won, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.False(t, won)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
assert.False(t, bracket.IsComplete())
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 3, len(matches)) {
assert.Equal(t, "F-3", matches[2].DisplayName)
}
}
func TestEliminationScheduleChangePreviousRoundResult(t *testing.T) {
func TestSingleEliminationChangePreviousRoundResult(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 4)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
tournament.CreateTestAlliances(database, 4)
bracket, err := NewSingleEliminationBracket(4)
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF2-1", model.RedWonMatch)
scoreMatch(database, "SF2-2", model.BlueWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF2-3", model.RedWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF2-3", model.BlueWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 5, len(matches))
scoreMatch(database, "SF1-1", model.RedWonMatch)
scoreMatch(database, "SF1-2", model.RedWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF1-2", model.BlueWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
scoreMatch(database, "SF1-3", model.BlueWonMatch)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
@@ -563,107 +564,3 @@ func TestEliminationScheduleChangePreviousRoundResult(t *testing.T) {
assertMatch(t, matches[7], "F-2", 4, 3)
}
}
func TestEliminationScheduleTiming(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 4)
UpdateEliminationSchedule(database, time.Unix(1000, 0))
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 4, len(matches)) {
assert.Equal(t, int64(1000), matches[0].Time.Unix())
assert.Equal(t, int64(1600), matches[1].Time.Unix())
assert.Equal(t, int64(2200), matches[2].Time.Unix())
assert.Equal(t, int64(2800), matches[3].Time.Unix())
}
scoreMatch(database, "SF1-1", model.RedWonMatch)
scoreMatch(database, "SF1-2", model.BlueWonMatch)
UpdateEliminationSchedule(database, time.Unix(5000, 0))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 5, len(matches)) {
assert.Equal(t, int64(1000), matches[0].Time.Unix())
assert.Equal(t, int64(5000), matches[1].Time.Unix())
assert.Equal(t, int64(2200), matches[2].Time.Unix())
assert.Equal(t, int64(5600), matches[3].Time.Unix())
assert.Equal(t, int64(6200), matches[4].Time.Unix())
}
}
func TestEliminationScheduleTeamPositions(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 4)
UpdateEliminationSchedule(database, time.Unix(1000, 0))
matches, _ := database.GetMatchesByType("elimination")
match1 := matches[0]
match2 := matches[1]
assert.Equal(t, 102, match1.Red1)
assert.Equal(t, 101, match1.Red2)
assert.Equal(t, 103, match1.Red3)
assert.Equal(t, 302, match2.Blue1)
assert.Equal(t, 301, match2.Blue2)
assert.Equal(t, 303, match2.Blue3)
// Shuffle the team positions and check that the subsequent matches in the same round have the same ones.
match1.Red1, match1.Red2 = match1.Red2, 104
match2.Blue1, match2.Blue3 = 305, match2.Blue1
database.UpdateMatch(&match1)
database.UpdateMatch(&match2)
scoreMatch(database, "SF1-1", model.RedWonMatch)
scoreMatch(database, "SF2-1", model.BlueWonMatch)
UpdateEliminationSchedule(database, time.Unix(1000, 0))
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 4, len(matches)) {
assert.Equal(t, match1.Red1, matches[0].Red1)
assert.Equal(t, match1.Red2, matches[0].Red2)
assert.Equal(t, match1.Red3, matches[0].Red3)
assert.Equal(t, match1.Red1, matches[2].Red1)
assert.Equal(t, match1.Red2, matches[2].Red2)
assert.Equal(t, match1.Red3, matches[2].Red3)
assert.Equal(t, match2.Blue1, matches[1].Blue1)
assert.Equal(t, match2.Blue2, matches[1].Blue2)
assert.Equal(t, match2.Blue3, matches[1].Blue3)
assert.Equal(t, match2.Blue1, matches[3].Blue1)
assert.Equal(t, match2.Blue2, matches[3].Blue2)
assert.Equal(t, match2.Blue3, matches[3].Blue3)
}
// Advance them to the finals and verify that the team position updates have been propagated.
scoreMatch(database, "SF1-2", model.RedWonMatch)
scoreMatch(database, "SF2-2", model.BlueWonMatch)
UpdateEliminationSchedule(database, time.Unix(5000, 0))
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 6, len(matches)) {
for i := 4; i < 6; i++ {
assert.Equal(t, match1.Red1, matches[i].Red1)
assert.Equal(t, match1.Red2, matches[i].Red2)
assert.Equal(t, match1.Red3, matches[i].Red3)
assert.Equal(t, match2.Blue1, matches[i].Blue1)
assert.Equal(t, match2.Blue2, matches[i].Blue2)
assert.Equal(t, match2.Blue3, matches[i].Blue3)
}
}
}
func assertMatch(t *testing.T, match model.Match, displayName string, redAlliance int, blueAlliance int) {
assert.Equal(t, displayName, match.DisplayName)
assert.Equal(t, redAlliance, match.ElimRedAlliance)
assert.Equal(t, blueAlliance, match.ElimBlueAlliance)
assert.Equal(t, 100*redAlliance+2, match.Red1)
assert.Equal(t, 100*redAlliance+1, match.Red2)
assert.Equal(t, 100*redAlliance+3, match.Red3)
assert.Equal(t, 100*blueAlliance+2, match.Blue1)
assert.Equal(t, 100*blueAlliance+1, match.Blue2)
assert.Equal(t, 100*blueAlliance+3, match.Blue3)
}
func scoreMatch(database *model.Database, displayName string, winner model.MatchStatus) {
match, _ := database.GetMatchByName("elimination", displayName)
match.Status = winner
database.UpdateMatch(match)
UpdateAlliance(database, [3]int{match.Red1, match.Red2, match.Red3}, match.ElimRedAlliance)
UpdateAlliance(database, [3]int{match.Blue1, match.Blue2, match.Blue3}, match.ElimBlueAlliance)
}

36
bracket/test_helpers.go Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2022 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Helper methods for use in tests in this package and others.
package bracket
import (
"github.com/Team254/cheesy-arena-lite/model"
"github.com/stretchr/testify/assert"
"testing"
)
func setupTestDb(t *testing.T) *model.Database {
return model.SetupTestDb(t, "bracket")
}
func assertMatch(t *testing.T, match model.Match, displayName string, redAlliance int, blueAlliance int) {
assert.Equal(t, displayName, match.DisplayName)
assert.Equal(t, redAlliance, match.ElimRedAlliance)
assert.Equal(t, blueAlliance, match.ElimBlueAlliance)
assert.Equal(t, 100*redAlliance+2, match.Red1)
assert.Equal(t, 100*redAlliance+1, match.Red2)
assert.Equal(t, 100*redAlliance+3, match.Red3)
assert.Equal(t, 100*blueAlliance+2, match.Blue1)
assert.Equal(t, 100*blueAlliance+1, match.Blue2)
assert.Equal(t, 100*blueAlliance+3, match.Blue3)
}
func scoreMatch(database *model.Database, displayName string, winner model.MatchStatus) {
match, _ := database.GetMatchByName("elimination", displayName)
match.Status = winner
database.UpdateMatch(match)
database.UpdateAllianceFromMatch(match.ElimRedAlliance, [3]int{match.Red1, match.Red2, match.Red3})
database.UpdateAllianceFromMatch(match.ElimBlueAlliance, [3]int{match.Blue1, match.Blue2, match.Blue3})
}

View File

@@ -7,6 +7,7 @@ package field
import (
"fmt"
"github.com/Team254/cheesy-arena-lite/bracket"
"github.com/Team254/cheesy-arena-lite/game"
"github.com/Team254/cheesy-arena-lite/model"
"github.com/Team254/cheesy-arena-lite/network"
@@ -71,6 +72,7 @@ type Arena struct {
SavedRankings game.Rankings
AllianceStationDisplayMode string
AllianceSelectionAlliances []model.Alliance
PlayoffBracket *bracket.Bracket
LowerThird *model.LowerThird
ShowLowerThird bool
MuteMatchSounds bool
@@ -112,6 +114,14 @@ func NewArena(dbPath string) (*Arena, error) {
arena.Displays = make(map[string]*Display)
// Reconstruct the playoff bracket in memory.
if err = arena.CreatePlayoffBracket(); err != nil {
return nil, err
}
if err = arena.UpdatePlayoffBracket(nil); err != nil {
return nil, err
}
// Load empty match as current.
arena.MatchState = PreMatch
arena.LoadTestMatch()
@@ -164,6 +174,30 @@ func (arena *Arena) LoadSettings() error {
return nil
}
// Constructs an empty playoff bracket in memory, based only on the number of alliances.
func (arena *Arena) CreatePlayoffBracket() error {
alliances, err := arena.Database.GetAllAlliances()
if err != nil {
return err
}
if len(alliances) > 0 {
arena.PlayoffBracket, err = bracket.NewSingleEliminationBracket(len(alliances))
if err != nil {
return err
}
}
return nil
}
// Traverses the in-memory playoff bracket to populate alliances, create matches, and assess winners. Does nothing if
// the bracket has not been created.are
func (arena *Arena) UpdatePlayoffBracket(startTime *time.Time) error {
if arena.PlayoffBracket == nil {
return nil
}
return arena.PlayoffBracket.Update(arena.Database, startTime)
}
// Sets up the arena for the given match.
func (arena *Arena) LoadMatch(match *model.Match) error {
if arena.MatchState != PreMatch {

View File

@@ -44,6 +44,39 @@ func (database *Database) GetAllAlliances() ([]Alliance, error) {
return alliances, nil
}
// Updates the alliance, if necessary, to include whoever played in the match, in case there was a substitute.
func (database *Database) UpdateAllianceFromMatch(allianceId int, matchTeamIds [3]int) error {
alliance, err := database.GetAllianceById(allianceId)
if err != nil {
return err
}
changed := false
if matchTeamIds != alliance.Lineup {
alliance.Lineup = matchTeamIds
changed = true
}
for _, teamId := range matchTeamIds {
found := false
for _, allianceTeamId := range alliance.TeamIds {
if teamId == allianceTeamId {
found = true
break
}
}
if !found {
alliance.TeamIds = append(alliance.TeamIds, teamId)
changed = true
}
}
if changed {
return database.UpdateAlliance(alliance)
}
return nil
}
// Returns two arrays containing the IDs of any teams for the red and blue alliances, respectively, who are part of the
// elimination alliance but are not playing in the given match.
// If the given match isn't an elimination match, empty arrays are returned.

View File

@@ -39,6 +39,19 @@ func TestAllianceCrud(t *testing.T) {
assert.Nil(t, alliance2)
}
func TestUpdateAllianceFromMatch(t *testing.T) {
db := setupTestDb(t)
defer db.Close()
alliance := Alliance{Id: 3, TeamIds: []int{254, 1114, 296, 1503}, Lineup: [3]int{1114, 254, 296}}
assert.Nil(t, db.CreateAlliance(&alliance))
assert.Nil(t, db.UpdateAllianceFromMatch(3, [3]int{1503, 188, 296}))
alliance2, err := db.GetAllianceById(3)
assert.Nil(t, err)
assert.Equal(t, []int{254, 1114, 296, 1503, 188}, alliance2.TeamIds)
assert.Equal(t, [3]int{1503, 188, 296}, alliance2.Lineup)
}
func TestTruncateAllianceTeams(t *testing.T) {
db := setupTestDb(t)
defer db.Close()

View File

@@ -48,7 +48,7 @@ const (
MatchNotPlayed MatchStatus = ""
)
var ElimRoundNames = map[int]string{1: "F", 2: "SF", 4: "QF", 8: "EF"}
var elimRoundNames = map[int]string{1: "F", 2: "SF", 4: "QF", 8: "EF"}
func (database *Database) CreateMatch(match *Match) error {
return database.matchTable.create(match)
@@ -153,7 +153,7 @@ func (match *Match) TbaCode() string {
if match.Type == "qualification" {
return fmt.Sprintf("qm%s", match.DisplayName)
} else if match.Type == "elimination" {
return fmt.Sprintf("%s%dm%d", strings.ToLower(ElimRoundNames[match.ElimRound]), match.ElimGroup,
return fmt.Sprintf("%s%dm%d", strings.ToLower(elimRoundNames[match.ElimRound]), match.ElimGroup,
match.ElimInstance)
}
return ""

View File

@@ -1,262 +0,0 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Functions for creating and updating the elimination match schedule.
package tournament
import (
"fmt"
"github.com/Team254/cheesy-arena-lite/model"
"strconv"
"time"
)
const ElimMatchSpacingSec = 600
const numWinsToAdvance = 2
// Incrementally creates any elimination matches that can be created, based on the results of alliance
// selection or prior elimination rounds. Returns true if the tournament is won.
func UpdateEliminationSchedule(database *model.Database, startTime time.Time) (bool, error) {
alliances, err := database.GetAllAlliances()
if err != nil {
return false, err
}
winner, err := buildEliminationMatchSet(database, 1, 1, len(alliances))
if err != nil {
return false, err
}
// Update the scheduled time for all matches that have yet to be run.
matches, err := database.GetMatchesByType("elimination")
if err != nil {
return false, 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 false, err
}
matchIndex++
}
return winner != nil, err
}
// Updates the alliance, if necessary, to include whoever played in the match, in case there was a substitute.
func UpdateAlliance(database *model.Database, matchTeamIds [3]int, allianceId int) error {
alliance, err := database.GetAllianceById(allianceId)
if err != nil {
return err
}
changed := false
if matchTeamIds != alliance.Lineup {
alliance.Lineup = matchTeamIds
changed = true
}
for _, teamId := range matchTeamIds {
found := false
for _, allianceTeamId := range alliance.TeamIds {
if teamId == allianceTeamId {
found = true
break
}
}
if !found {
alliance.TeamIds = append(alliance.TeamIds, teamId)
changed = true
}
}
if changed {
return database.UpdateAlliance(alliance)
}
return nil
}
// Recursively traverses the elimination bracket downwards, creating matches as necessary. Returns the winner
// of the given round if known.
func buildEliminationMatchSet(
database *model.Database, round int, group int, numAlliances int,
) (*model.Alliance, error) {
if numAlliances < 2 {
return nil, fmt.Errorf("Must have at least 2 alliances")
}
roundName, ok := model.ElimRoundNames[round]
if !ok {
return nil, fmt.Errorf("Round of depth %d is not supported", round*2)
}
if round != 1 {
roundName += strconv.Itoa(group)
}
// Recurse to figure out who the involved alliances are.
var redAlliance, blueAlliance *model.Alliance
var err error
if numAlliances < 4*round {
// This is the first round for some or all alliances and will be at least partially populated from the
// alliance selection results.
matchups := []int{1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11}
factor := len(matchups) / round
redAllianceNumber := matchups[(group-1)*factor]
blueAllianceNumber := matchups[(group-1)*factor+factor/2]
numDirectAlliances := 4*round - numAlliances
if redAllianceNumber <= numDirectAlliances {
// The red alliance has a bye or the number of alliances is a power of 2; get from alliance selection.
redAlliance, err = database.GetAllianceById(redAllianceNumber)
if err != nil {
return nil, err
}
}
if blueAllianceNumber <= numDirectAlliances {
// The blue alliance has a bye or the number of alliances is a power of 2; get from alliance selection.
blueAlliance, err = database.GetAllianceById(blueAllianceNumber)
if err != nil {
return nil, err
}
}
}
// If the alliances aren't known yet, get them from one round down in the bracket.
if redAlliance == nil {
redAlliance, err = buildEliminationMatchSet(database, round*2, group*2-1, numAlliances)
if err != nil {
return nil, err
}
}
if blueAlliance == nil {
blueAlliance, err = buildEliminationMatchSet(database, round*2, group*2, numAlliances)
if err != nil {
return nil, err
}
}
// Bail if the rounds below are not yet complete and we don't know both alliances competing this round.
if redAlliance == nil || blueAlliance == nil {
return nil, nil
}
// Create, update, and/or delete unplayed matches as necessary.
matches, err := database.GetMatchesByElimRoundGroup(round, group)
if err != nil {
return nil, err
}
var redAllianceWins, blueAllianceWins int
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 nil, 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 nil, err
}
}
unplayedMatches = append(unplayedMatches, match)
continue
}
// Check who won.
if match.Status == model.RedWonMatch {
redAllianceWins++
} else if match.Status == model.BlueWonMatch {
blueAllianceWins++
}
}
maxWins := redAllianceWins
if blueAllianceWins > maxWins {
maxWins = blueAllianceWins
}
numUnplayedMatchesNeeded := 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 nil, 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++ {
err = database.CreateMatch(
createMatch(roundName, round, group, len(matches)+i+1, redAlliance, blueAlliance),
)
if err != nil {
return nil, err
}
}
}
// Determine the winner of the round or if it is still in progress.
if redAllianceWins >= numWinsToAdvance {
return redAlliance, nil
}
if blueAllianceWins >= numWinsToAdvance {
return blueAlliance, nil
}
return nil, nil
}
// Creates a match at the given point in the elimination bracket and populates the teams.
func createMatch(
roundName string,
round int,
group int,
instance int,
redAlliance,
blueAlliance *model.Alliance,
) *model.Match {
match := model.Match{
Type: "elimination",
DisplayName: fmt.Sprintf("%s-%d", roundName, instance),
ElimRound: round,
ElimGroup: group,
ElimInstance: instance,
}
if redAlliance != nil {
match.ElimRedAlliance = redAlliance.Id
positionRedTeams(&match, redAlliance)
}
if blueAlliance != nil {
match.ElimBlueAlliance = blueAlliance.Id
positionBlueTeams(&match, blueAlliance)
}
return &match
}
// 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]
}

View File

@@ -8,7 +8,6 @@ package web
import (
"fmt"
"github.com/Team254/cheesy-arena-lite/model"
"github.com/Team254/cheesy-arena-lite/tournament"
"net/http"
"strconv"
"time"
@@ -159,6 +158,12 @@ func (web *Web) allianceSelectionResetHandler(w http.ResponseWriter, r *http.Req
return
}
// Replace the current in-memory bracket if it was populated with teams.
if err = web.arena.CreatePlayoffBracket(); err != nil {
handleWebErr(w, err)
return
}
web.arena.AllianceSelectionAlliances = []model.Alliance{}
cachedRankedTeams = []*RankedTeam{}
web.arena.AllianceSelectionNotifier.Notify()
@@ -209,8 +214,11 @@ func (web *Web) allianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.
}
// Generate the first round of elimination matches.
_, err = tournament.UpdateEliminationSchedule(web.arena.Database, startTime)
if err != nil {
if err = web.arena.CreatePlayoffBracket(); err != nil {
handleWebErr(w, err)
return
}
if err = web.arena.UpdatePlayoffBracket(&startTime); err != nil {
handleWebErr(w, err)
return
}

View File

@@ -7,6 +7,7 @@ package web
import (
"fmt"
"github.com/Team254/cheesy-arena-lite/bracket"
"github.com/Team254/cheesy-arena-lite/field"
"github.com/Team254/cheesy-arena-lite/game"
"github.com/Team254/cheesy-arena-lite/model"
@@ -436,36 +437,30 @@ func (web *Web) commitMatchScore(match *model.Match, matchResult *model.MatchRes
}
if match.ShouldUpdateEliminationMatches() {
if err = tournament.UpdateAlliance(
web.arena.Database, [3]int{match.Red1, match.Red2, match.Red3}, match.ElimRedAlliance,
if err = web.arena.Database.UpdateAllianceFromMatch(
match.ElimRedAlliance, [3]int{match.Red1, match.Red2, match.Red3},
); err != nil {
return err
}
if err = tournament.UpdateAlliance(
web.arena.Database, [3]int{match.Blue1, match.Blue2, match.Blue3}, match.ElimBlueAlliance,
if err = web.arena.Database.UpdateAllianceFromMatch(
match.ElimBlueAlliance, [3]int{match.Blue1, match.Blue2, match.Blue3},
); err != nil {
return err
}
// Generate any subsequent elimination matches.
isTournamentWon, err := tournament.UpdateEliminationSchedule(web.arena.Database,
time.Now().Add(time.Second*tournament.ElimMatchSpacingSec))
if err != nil {
nextMatchTime := time.Now().Add(time.Second * bracket.ElimMatchSpacingSec)
if err = web.arena.UpdatePlayoffBracket(&nextMatchTime); err != nil {
return err
}
// Generate awards if the tournament is over.
if isTournamentWon {
var winnerAllianceId, finalistAllianceId int
if match.Status == model.RedWonMatch {
winnerAllianceId = match.ElimRedAlliance
finalistAllianceId = match.ElimBlueAlliance
} else if match.Status == model.BlueWonMatch {
winnerAllianceId = match.ElimBlueAlliance
finalistAllianceId = match.ElimRedAlliance
}
if err = tournament.CreateOrUpdateWinnerAndFinalistAwards(web.arena.Database, winnerAllianceId,
finalistAllianceId); err != nil {
if web.arena.PlayoffBracket.IsComplete() {
winnerAllianceId := web.arena.PlayoffBracket.Winner()
finalistAllianceId := web.arena.PlayoffBracket.Finalist()
if err = tournament.CreateOrUpdateWinnerAndFinalistAwards(
web.arena.Database, winnerAllianceId, finalistAllianceId,
); err != nil {
return err
}
}

View File

@@ -184,6 +184,7 @@ func TestCommitEliminationTie(t *testing.T) {
assert.Equal(t, model.TieMatch, match.Status)
tournament.CreateTestAlliances(web.arena.Database, 2)
web.arena.CreatePlayoffBracket()
match.Type = "elimination"
match.ElimRedAlliance = 1
match.ElimBlueAlliance = 2

View File

@@ -45,6 +45,7 @@ func TestMatchReviewEditExistingResult(t *testing.T) {
matchResult.MatchType = match.Type
assert.Nil(t, web.arena.Database.CreateMatchResult(matchResult))
tournament.CreateTestAlliances(web.arena.Database, 2)
web.arena.CreatePlayoffBracket()
recorder := web.getHttpResponse("/match_review")
assert.Equal(t, 200, recorder.Code)
@@ -85,6 +86,7 @@ func TestMatchReviewCreateNewResult(t *testing.T) {
Red2: 1002, Red3: 1003, Blue1: 1004, Blue2: 1005, Blue3: 1006, ElimRedAlliance: 1, ElimBlueAlliance: 2}
web.arena.Database.CreateMatch(&match)
tournament.CreateTestAlliances(web.arena.Database, 2)
web.arena.CreatePlayoffBracket()
recorder := web.getHttpResponse("/match_review")
assert.Equal(t, 200, recorder.Code)