mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Add definition of a double-elimination bracket.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
117
bracket/double_elimination.go
Normal file
117
bracket/double_elimination.go
Normal 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),
|
||||
},
|
||||
}
|
||||
280
bracket/double_elimination_test.go
Normal file
280
bracket/double_elimination_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user