mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-10 14:16:47 -04:00
Refactor playoff bracket logic to more easily support alternative formats.
This commit is contained in:
181
bracket/bracket.go
Normal file
181
bracket/bracket.go
Normal 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
171
bracket/bracket_test.go
Normal 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
228
bracket/matchup.go
Normal 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]
|
||||
}
|
||||
128
bracket/single_elimination.go
Normal file
128
bracket/single_elimination.go
Normal 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},
|
||||
},
|
||||
}
|
||||
@@ -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
36
bracket/test_helpers.go
Normal 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})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user