Add definition of a double-elimination bracket.

This commit is contained in:
Patrick Fairbank
2022-08-16 19:47:38 -07:00
parent 1a1c62ae49
commit b6050d8cfc
7 changed files with 544 additions and 63 deletions

View File

@@ -26,7 +26,13 @@ func newBracket(matchupTemplates []matchupTemplate, numAlliances int) (*Bracket,
}
// Recursively build the bracket, starting with the finals matchup.
finalsMatchup, _, err := createMatchupTree(newMatchupKey(1, 1), matchupTemplateMap, numAlliances)
finalsMatchup, _, err := createMatchupGraph(
newMatchupKey(1, 1),
true,
matchupTemplateMap,
numAlliances,
make(map[matchupKey]*Matchup),
)
if err != nil {
return nil, err
}
@@ -35,8 +41,12 @@ func newBracket(matchupTemplates []matchupTemplate, numAlliances int) (*Bracket,
}
// Recursive helper method to create the current matchup node and all of its children.
func createMatchupTree(
matchupKey matchupKey, matchupTemplateMap map[matchupKey]matchupTemplate, numAlliances int,
func createMatchupGraph(
matchupKey matchupKey,
useWinner bool,
matchupTemplateMap map[matchupKey]matchupTemplate,
numAlliances int,
matchupMap map[matchupKey]*Matchup,
) (*Matchup, int, error) {
matchupTemplate, ok := matchupTemplateMap[matchupKey]
if !ok {
@@ -46,7 +56,7 @@ func createMatchupTree(
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.
// This is a leaf node in the matchup graph; the alliances will come from the alliance selection.
if redAllianceIdFromSelection == 0 || blueAllianceIdFromSelection == 0 {
return nil, 0, fmt.Errorf("both alliances must be populated either from selection or a lower round")
}
@@ -62,36 +72,54 @@ func createMatchupTree(
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
matchup, ok := matchupMap[matchupKey]
if !ok {
matchup = &Matchup{
matchupTemplate: matchupTemplate,
RedAllianceId: redAllianceIdFromSelection,
BlueAllianceId: blueAllianceIdFromSelection,
}
matchupMap[matchupKey] = matchup
}
return matchup, 0, nil
}
if redAllianceIdFromSelection == 0 && blueAllianceIdFromSelection == 0 {
// This matchup should be pruned from the bracket since neither alliance has a valid source; this tournament
// is too small for this matchup to be played.
return nil, 0, nil
}
if redAllianceIdFromSelection > 0 {
// The red alliance has a bye.
return nil, redAllianceIdFromSelection, nil
if useWinner {
if redAllianceIdFromSelection > 0 {
// The red alliance has a bye.
return nil, redAllianceIdFromSelection, nil
} else {
// The blue alliance has a bye.
return nil, blueAllianceIdFromSelection, nil
}
} else {
// The blue alliance has a bye.
return nil, blueAllianceIdFromSelection, nil
// There is no losing alliance to return; prune this matchup.
return nil, 0, nil
}
}
// Recurse to determine the lower-round red and blue matchups that will feed into this one, or the alliances that
// have a bye to this round.
redAllianceSourceMatchup, redByeAllianceId, err := createMatchupTree(
matchupTemplate.redAllianceSource.matchupKey, matchupTemplateMap, numAlliances,
redAllianceSourceMatchup, redByeAllianceId, err := createMatchupGraph(
matchupTemplate.redAllianceSource.matchupKey,
matchupTemplate.redAllianceSource.useWinner,
matchupTemplateMap,
numAlliances,
matchupMap,
)
if err != nil {
return nil, 0, err
}
blueAllianceSourceMatchup, blueByeAllianceId, err := createMatchupTree(
matchupTemplate.blueAllianceSource.matchupKey, matchupTemplateMap, numAlliances,
blueAllianceSourceMatchup, blueByeAllianceId, err := createMatchupGraph(
matchupTemplate.blueAllianceSource.matchupKey,
matchupTemplate.blueAllianceSource.useWinner,
matchupTemplateMap,
numAlliances,
matchupMap,
)
if err != nil {
return nil, 0, err
@@ -104,22 +132,37 @@ func createMatchupTree(
return nil, 0, nil
}
if redByeAllianceId > 0 && blueAllianceSourceMatchup == nil && blueByeAllianceId == 0 {
// The red alliance has a bye.
return nil, redByeAllianceId, nil
if useWinner {
// The red alliance has a bye.
return nil, redByeAllianceId, nil
} else {
// There is no losing alliance to return; prune this matchup.
return nil, 0, nil
}
}
if blueByeAllianceId > 0 && redAllianceSourceMatchup == nil && redByeAllianceId == 0 {
// The blue alliance has a bye.
return nil, blueByeAllianceId, nil
if useWinner {
// The blue alliance has a bye.
return nil, blueByeAllianceId, nil
} else {
// There is no losing alliance to return; prune this matchup.
return nil, 0, nil
}
}
// This is a real matchup that will be played out.
return &Matchup{
matchupTemplate: matchupTemplate,
RedAllianceId: redByeAllianceId,
BlueAllianceId: blueByeAllianceId,
RedAllianceSourceMatchup: redAllianceSourceMatchup,
BlueAllianceSourceMatchup: blueAllianceSourceMatchup,
}, 0, nil
matchup, ok := matchupMap[matchupKey]
if !ok {
matchup = &Matchup{
matchupTemplate: matchupTemplate,
RedAllianceId: redByeAllianceId,
BlueAllianceId: blueByeAllianceId,
RedAllianceSourceMatchup: redAllianceSourceMatchup,
BlueAllianceSourceMatchup: blueAllianceSourceMatchup,
}
matchupMap[matchupKey] = matchup
}
return matchup, 0, nil
}
// Returns the winning alliance ID of the entire bracket, or 0 if it is not yet known.
@@ -174,8 +217,12 @@ func (bracket *Bracket) print() {
fmt.Printf("%+v\n\n", matchup)
matchupQueue = matchupQueue[1:]
if matchup != nil {
matchupQueue = append(matchupQueue, matchup.RedAllianceSourceMatchup)
matchupQueue = append(matchupQueue, matchup.BlueAllianceSourceMatchup)
if matchup.RedAllianceSourceMatchup != nil && matchup.redAllianceSource.useWinner {
matchupQueue = append(matchupQueue, matchup.RedAllianceSourceMatchup)
}
if matchup.BlueAllianceSourceMatchup != nil && matchup.blueAllianceSource.useWinner {
matchupQueue = append(matchupQueue, matchup.BlueAllianceSourceMatchup)
}
}
}
}

View File

@@ -20,7 +20,7 @@ func TestNewBracketErrors(t *testing.T) {
matchTemplate := matchupTemplate{
matchupKey: newMatchupKey(1, 1),
redAllianceSource: allianceSource{allianceId: 1},
blueAllianceSource: newMatchupAllianceSource(2, 2),
blueAllianceSource: newWinnerAllianceSource(2, 2),
}
_, err = newBracket([]matchupTemplate{matchTemplate}, 8)
if assert.NotNil(t, err) {
@@ -35,15 +35,15 @@ func TestNewBracketInverseSeeding(t *testing.T) {
matchupKey: newMatchupKey(1, 1),
displayNameFormat: "F-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(2, 1),
blueAllianceSource: newMatchupAllianceSource(2, 2),
redAllianceSource: newWinnerAllianceSource(2, 1),
blueAllianceSource: newWinnerAllianceSource(2, 2),
},
{
matchupKey: newMatchupKey(2, 1),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(4, 2),
blueAllianceSource: newMatchupAllianceSource(4, 1),
redAllianceSource: newWinnerAllianceSource(4, 2),
blueAllianceSource: newWinnerAllianceSource(4, 1),
},
{
matchupKey: newMatchupKey(2, 2),

View File

@@ -0,0 +1,117 @@
// Copyright 2022 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Defines the tournament structure for a double-elimination bracket culminating in a best-of-three final.
package bracket
import "fmt"
// Creates an unpopulated double-elimination bracket. Only supports having exactly eight alliances.
func NewDoubleEliminationBracket(numAlliances int) (*Bracket, error) {
if numAlliances != 8 {
return nil, fmt.Errorf("Must have exactly 8 alliances")
}
return newBracket(doubleEliminationBracketMatchupTemplates, newMatchupKey(6, 1), numAlliances)
}
var doubleEliminationBracketMatchupTemplates = []matchupTemplate{
{
matchupKey: newMatchupKey(1, 1),
displayNameFormat: "1",
numWinsToAdvance: 1,
redAllianceSource: allianceSource{allianceId: 1},
blueAllianceSource: allianceSource{allianceId: 8},
},
{
matchupKey: newMatchupKey(1, 2),
displayNameFormat: "2",
numWinsToAdvance: 1,
redAllianceSource: allianceSource{allianceId: 4},
blueAllianceSource: allianceSource{allianceId: 5},
},
{
matchupKey: newMatchupKey(1, 3),
displayNameFormat: "3",
numWinsToAdvance: 1,
redAllianceSource: allianceSource{allianceId: 3},
blueAllianceSource: allianceSource{allianceId: 6},
},
{
matchupKey: newMatchupKey(1, 4),
displayNameFormat: "4",
numWinsToAdvance: 1,
redAllianceSource: allianceSource{allianceId: 2},
blueAllianceSource: allianceSource{allianceId: 7},
},
{
matchupKey: newMatchupKey(2, 1),
displayNameFormat: "5",
numWinsToAdvance: 1,
redAllianceSource: newLoserAllianceSource(1, 1),
blueAllianceSource: newLoserAllianceSource(1, 2),
},
{
matchupKey: newMatchupKey(2, 2),
displayNameFormat: "6",
numWinsToAdvance: 1,
redAllianceSource: newLoserAllianceSource(1, 3),
blueAllianceSource: newLoserAllianceSource(1, 4),
},
{
matchupKey: newMatchupKey(2, 3),
displayNameFormat: "7",
numWinsToAdvance: 1,
redAllianceSource: newWinnerAllianceSource(1, 1),
blueAllianceSource: newWinnerAllianceSource(1, 2),
},
{
matchupKey: newMatchupKey(2, 4),
displayNameFormat: "8",
numWinsToAdvance: 1,
redAllianceSource: newWinnerAllianceSource(1, 3),
blueAllianceSource: newWinnerAllianceSource(1, 4),
},
{
matchupKey: newMatchupKey(3, 1),
displayNameFormat: "9",
numWinsToAdvance: 1,
redAllianceSource: newLoserAllianceSource(2, 3),
blueAllianceSource: newWinnerAllianceSource(2, 2),
},
{
matchupKey: newMatchupKey(3, 2),
displayNameFormat: "10",
numWinsToAdvance: 1,
redAllianceSource: newLoserAllianceSource(2, 4),
blueAllianceSource: newWinnerAllianceSource(2, 1),
},
{
matchupKey: newMatchupKey(4, 1),
displayNameFormat: "11",
numWinsToAdvance: 1,
redAllianceSource: newWinnerAllianceSource(3, 1),
blueAllianceSource: newWinnerAllianceSource(3, 2),
},
{
matchupKey: newMatchupKey(4, 2),
displayNameFormat: "12",
numWinsToAdvance: 1,
redAllianceSource: newWinnerAllianceSource(2, 3),
blueAllianceSource: newWinnerAllianceSource(2, 4),
},
{
matchupKey: newMatchupKey(5, 1),
displayNameFormat: "13",
numWinsToAdvance: 1,
redAllianceSource: newLoserAllianceSource(4, 2),
blueAllianceSource: newWinnerAllianceSource(4, 1),
},
{
matchupKey: newMatchupKey(6, 1),
displayNameFormat: "F-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newWinnerAllianceSource(4, 2),
blueAllianceSource: newWinnerAllianceSource(5, 1),
},
}

View File

@@ -0,0 +1,280 @@
// 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"
)
func TestDoubleEliminationInitial(t *testing.T) {
database := setupTestDb(t)
tournament.CreateTestAlliances(database, 8)
bracket, err := NewDoubleEliminationBracket(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, 4, len(matches)) {
assertMatch(t, matches[0], "1", 1, 8)
assertMatch(t, matches[1], "2", 4, 5)
assertMatch(t, matches[2], "3", 3, 6)
assertMatch(t, matches[3], "4", 2, 7)
}
}
func TestDoubleEliminationErrors(t *testing.T) {
_, err := NewDoubleEliminationBracket(7)
if assert.NotNil(t, err) {
assert.Equal(t, "Must have exactly 8 alliances", err.Error())
}
_, err = NewDoubleEliminationBracket(9)
if assert.NotNil(t, err) {
assert.Equal(t, "Must have exactly 8 alliances", err.Error())
}
}
func TestDoubleEliminationProgression(t *testing.T) {
database := setupTestDb(t)
tournament.CreateTestAlliances(database, 8)
bracket, err := NewDoubleEliminationBracket(8)
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, "1", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 4, len(matches))
scoreMatch(database, "2", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
assertMatch(t, matches[4], "5", 1, 5)
assertMatch(t, matches[5], "7", 8, 4)
}
scoreMatch(database, "3", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 6, len(matches))
scoreMatch(database, "4", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
assertMatch(t, matches[4], "5", 1, 5)
assertMatch(t, matches[5], "6", 6, 2)
assertMatch(t, matches[6], "7", 8, 4)
assertMatch(t, matches[7], "8", 3, 7)
}
scoreMatch(database, "5", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 8, len(matches))
scoreMatch(database, "6", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 8, len(matches))
scoreMatch(database, "7", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 9, len(matches)) {
assertMatch(t, matches[8], "9", 8, 2)
}
scoreMatch(database, "8", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 11, len(matches)) {
assertMatch(t, matches[9], "10", 7, 5)
assertMatch(t, matches[10], "12", 4, 3)
}
scoreMatch(database, "9", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 11, len(matches))
scoreMatch(database, "10", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 12, len(matches)) {
assertMatch(t, matches[10], "11", 8, 7)
assertMatch(t, matches[11], "12", 4, 3)
}
scoreMatch(database, "11", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 12, len(matches))
scoreMatch(database, "12", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 13, len(matches)) {
assertMatch(t, matches[12], "13", 3, 7)
}
scoreMatch(database, "13", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 15, len(matches)) {
assertMatch(t, matches[13], "F-1", 4, 7)
assertMatch(t, matches[14], "F-2", 4, 7)
}
assert.False(t, bracket.IsComplete())
assert.Equal(t, 0, bracket.Winner())
assert.Equal(t, 0, bracket.Finalist())
scoreMatch(database, "F-1", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 15, len(matches))
assert.False(t, bracket.IsComplete())
assert.Equal(t, 0, bracket.Winner())
assert.Equal(t, 0, bracket.Finalist())
scoreMatch(database, "F-2", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 16, len(matches)) {
assertMatch(t, matches[15], "F-3", 4, 7)
}
assert.False(t, bracket.IsComplete())
assert.Equal(t, 0, bracket.Winner())
assert.Equal(t, 0, bracket.Finalist())
scoreMatch(database, "F-3", model.TieMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 17, len(matches)) {
assertMatch(t, matches[16], "F-4", 4, 7)
}
assert.False(t, bracket.IsComplete())
assert.Equal(t, 0, bracket.Winner())
assert.Equal(t, 0, bracket.Finalist())
scoreMatch(database, "F-4", model.TieMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 18, len(matches)) {
assertMatch(t, matches[17], "F-5", 4, 7)
}
assert.False(t, bracket.IsComplete())
assert.Equal(t, 0, bracket.Winner())
assert.Equal(t, 0, bracket.Finalist())
scoreMatch(database, "F-5", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 18, len(matches))
assert.True(t, bracket.IsComplete())
assert.Equal(t, 7, bracket.Winner())
assert.Equal(t, 4, bracket.Finalist())
}
func TestDoubleEliminationTie(t *testing.T) {
database := setupTestDb(t)
tournament.CreateTestAlliances(database, 8)
bracket, err := NewDoubleEliminationBracket(8)
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, "1", model.TieMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 5, len(matches)) {
assertMatch(t, matches[4], "1-2", 1, 8)
}
scoreMatch(database, "1-2", model.TieMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
assertMatch(t, matches[5], "1-3", 1, 8)
}
scoreMatch(database, "1-3", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 6, len(matches))
}
func TestDoubleEliminationChangeResult(t *testing.T) {
database := setupTestDb(t)
tournament.CreateTestAlliances(database, 8)
bracket, err := NewDoubleEliminationBracket(8)
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, "1", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 4, len(matches))
scoreMatch(database, "2", model.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
assertMatch(t, matches[4], "5", 1, 5)
assertMatch(t, matches[5], "7", 8, 4)
}
scoreMatch(database, "2", model.MatchNotPlayed)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Equal(t, 4, len(matches))
scoreMatch(database, "2", model.BlueWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
assertMatch(t, matches[4], "5", 1, 4)
assertMatch(t, matches[5], "7", 8, 5)
}
}

View File

@@ -7,6 +7,7 @@
package bracket
import (
"fmt"
"github.com/Team254/cheesy-arena-lite/model"
"strconv"
"strings"
@@ -17,6 +18,7 @@ import (
type allianceSource struct {
allianceId int
matchupKey matchupKey
useWinner bool
}
// Key for uniquely identifying a matchup. Round IDs are arbitrary and in descending order with "1" always representing
@@ -49,9 +51,14 @@ type Matchup struct {
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 an alliance source that points to the winner of a different matchup.
func newWinnerAllianceSource(round, group int) allianceSource {
return allianceSource{matchupKey: newMatchupKey(round, group), useWinner: true}
}
// Convenience method to quickly create an alliance source that points to the loser of a different matchup.
func newLoserAllianceSource(round, group int) allianceSource {
return allianceSource{matchupKey: newMatchupKey(round, group), useWinner: false}
}
// Convenience method to quickly create a matchup key.
@@ -63,7 +70,13 @@ func newMatchupKey(round, group int) matchupKey {
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)
if strings.Contains(displayName, "${instance}") {
displayName = strings.Replace(displayName, "${instance}", strconv.Itoa(instance), -1)
} else if instance > 1 {
// Special case to handle matchups that only have more than one instance under exceptional circumstances (like
// ties in double-elimination unresolved by tiebreakers).
displayName += fmt.Sprintf("-%d", instance)
}
return displayName
}
@@ -94,15 +107,16 @@ 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
// Recursively traverses the matchup graph to update the state of this matchup and all of its children based on match
// results, counting wins and creating or deleting matches as required.
func (matchup *Matchup) update(database *model.Database) error {
if matchup.RedAllianceSourceMatchup != nil {
// Update child matchups first. Only recurse down winner links to avoid visiting a node twice.
if matchup.RedAllianceSourceMatchup != nil && matchup.redAllianceSource.useWinner {
if err := matchup.RedAllianceSourceMatchup.update(database); err != nil {
return err
}
}
if matchup.BlueAllianceSourceMatchup != nil {
if matchup.BlueAllianceSourceMatchup != nil && matchup.blueAllianceSource.useWinner {
if err := matchup.BlueAllianceSourceMatchup.update(database); err != nil {
return err
}
@@ -110,10 +124,23 @@ func (matchup *Matchup) update(database *model.Database) error {
// 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.redAllianceSource.useWinner {
matchup.RedAllianceId = matchup.RedAllianceSourceMatchup.winner()
} else {
matchup.RedAllianceId = matchup.RedAllianceSourceMatchup.loser()
}
}
if matchup.BlueAllianceSourceMatchup != nil {
matchup.BlueAllianceId = matchup.BlueAllianceSourceMatchup.winner()
if matchup.blueAllianceSource.useWinner {
matchup.BlueAllianceId = matchup.BlueAllianceSourceMatchup.winner()
} else {
matchup.BlueAllianceId = matchup.BlueAllianceSourceMatchup.loser()
}
}
matches, err := database.GetMatchesByElimRoundGroup(matchup.round, matchup.group)
if err != nil {
return err
}
// Bail if we do not yet know both alliances.
@@ -121,6 +148,14 @@ func (matchup *Matchup) update(database *model.Database) error {
// Ensure the current state is reset; it may have previously been populated if a match result was edited.
matchup.RedAllianceWins = 0
matchup.BlueAllianceWins = 0
// Delete any previously created matches.
for _, match := range matches {
if err = database.DeleteMatch(match.Id); err != nil {
return err
}
}
return nil
}
@@ -133,10 +168,6 @@ func (matchup *Matchup) update(database *model.Database) error {
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

View File

@@ -24,50 +24,50 @@ var singleEliminationBracketMatchupTemplates = []matchupTemplate{
matchupKey: newMatchupKey(1, 1),
displayNameFormat: "F-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(2, 1),
blueAllianceSource: newMatchupAllianceSource(2, 2),
redAllianceSource: newWinnerAllianceSource(2, 1),
blueAllianceSource: newWinnerAllianceSource(2, 2),
},
{
matchupKey: newMatchupKey(2, 1),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(4, 1),
blueAllianceSource: newMatchupAllianceSource(4, 2),
redAllianceSource: newWinnerAllianceSource(4, 1),
blueAllianceSource: newWinnerAllianceSource(4, 2),
},
{
matchupKey: newMatchupKey(2, 2),
displayNameFormat: "SF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(4, 3),
blueAllianceSource: newMatchupAllianceSource(4, 4),
redAllianceSource: newWinnerAllianceSource(4, 3),
blueAllianceSource: newWinnerAllianceSource(4, 4),
},
{
matchupKey: newMatchupKey(4, 1),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 1),
blueAllianceSource: newMatchupAllianceSource(8, 2),
redAllianceSource: newWinnerAllianceSource(8, 1),
blueAllianceSource: newWinnerAllianceSource(8, 2),
},
{
matchupKey: newMatchupKey(4, 2),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 3),
blueAllianceSource: newMatchupAllianceSource(8, 4),
redAllianceSource: newWinnerAllianceSource(8, 3),
blueAllianceSource: newWinnerAllianceSource(8, 4),
},
{
matchupKey: newMatchupKey(4, 3),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 5),
blueAllianceSource: newMatchupAllianceSource(8, 6),
redAllianceSource: newWinnerAllianceSource(8, 5),
blueAllianceSource: newWinnerAllianceSource(8, 6),
},
{
matchupKey: newMatchupKey(4, 4),
displayNameFormat: "QF${group}-${instance}",
numWinsToAdvance: 2,
redAllianceSource: newMatchupAllianceSource(8, 7),
blueAllianceSource: newMatchupAllianceSource(8, 8),
redAllianceSource: newWinnerAllianceSource(8, 7),
blueAllianceSource: newWinnerAllianceSource(8, 8),
},
{
matchupKey: newMatchupKey(8, 1),

View File

@@ -563,4 +563,10 @@ func TestSingleEliminationChangePreviousRoundResult(t *testing.T) {
assertMatch(t, matches[6], "F-1", 4, 3)
assertMatch(t, matches[7], "F-2", 4, 3)
}
scoreMatch(database, "SF2-3", model.MatchNotPlayed)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
assert.Equal(t, 6, len(matches))
}