Refactor tournament progression methods into separate package.

This commit is contained in:
Patrick Fairbank
2017-08-27 18:20:44 -07:00
parent 53d34ae82a
commit f0fe7df2b3
16 changed files with 550 additions and 488 deletions

View File

@@ -0,0 +1,248 @@
// 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/model"
"strconv"
"time"
)
const ElimMatchSpacingSec = 600
// Incrementally creates any elimination matches that can be created, based on the results of alliance
// selection or prior elimination rounds. Returns the winning alliance once it has been determined.
func UpdateEliminationSchedule(database *model.Database, startTime time.Time) ([]model.AllianceTeam, error) {
alliances, err := database.GetAllAlliances()
if err != nil {
return []model.AllianceTeam{}, err
}
winner, err := buildEliminationMatchSet(database, 1, 1, len(alliances))
if err != nil {
return []model.AllianceTeam{}, err
}
// Update the scheduled time for all matches that have yet to be run.
matches, err := database.GetMatchesByType("elimination")
if err != nil {
return []model.AllianceTeam{}, err
}
matchIndex := 0
for _, match := range matches {
if match.Status == "complete" {
continue
}
match.Time = startTime.Add(time.Duration(matchIndex*ElimMatchSpacingSec) * time.Second)
database.SaveMatch(&match)
matchIndex++
}
return winner, err
}
// 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.AllianceTeam, error) {
if numAlliances < 2 {
return []model.AllianceTeam{}, fmt.Errorf("Must have at least 2 alliances")
}
roundName, ok := model.ElimRoundNames[round]
if !ok {
return []model.AllianceTeam{}, 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.AllianceTeam
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.GetTeamsByAlliance(redAllianceNumber)
if err != nil {
return []model.AllianceTeam{}, 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.GetTeamsByAlliance(blueAllianceNumber)
if err != nil {
return []model.AllianceTeam{}, err
}
}
}
// If the alliances aren't known yet, get them from one round down in the bracket.
if len(redAlliance) == 0 {
redAlliance, err = buildEliminationMatchSet(database, round*2, group*2-1, numAlliances)
if err != nil {
return []model.AllianceTeam{}, err
}
}
if len(blueAlliance) == 0 {
blueAlliance, err = buildEliminationMatchSet(database, round*2, group*2, numAlliances)
if err != nil {
return []model.AllianceTeam{}, err
}
}
// Bail if the rounds below are not yet complete and we don't know either alliance competing this round.
if len(redAlliance) == 0 && len(blueAlliance) == 0 {
return []model.AllianceTeam{}, nil
}
// Check if the match set exists already and if it has been won.
var redWins, blueWins, numIncomplete int
var ties []*model.Match
matches, err := database.GetMatchesByElimRoundGroup(round, group)
if err != nil {
return []model.AllianceTeam{}, err
}
var unplayedMatches []*model.Match
for _, match := range matches {
// Update the teams in the match if they are not yet set or are incorrect.
if len(redAlliance) != 0 && !(teamInAlliance(match.Red1, redAlliance) &&
teamInAlliance(match.Red2, redAlliance) && teamInAlliance(match.Red3, redAlliance)) {
positionRedTeams(&match, redAlliance)
database.SaveMatch(&match)
} else if len(blueAlliance) != 0 && !(teamInAlliance(match.Blue1, blueAlliance) &&
teamInAlliance(match.Blue2, blueAlliance) && teamInAlliance(match.Blue3, blueAlliance)) {
positionBlueTeams(&match, blueAlliance)
database.SaveMatch(&match)
}
if match.Status != "complete" {
unplayedMatches = append(unplayedMatches, &match)
numIncomplete += 1
continue
}
// Check who won.
switch match.Winner {
case "R":
redWins += 1
case "B":
blueWins += 1
case "T":
ties = append(ties, &match)
default:
return []model.AllianceTeam{}, fmt.Errorf("Completed match %d has invalid winner '%s'", match.Id,
match.Winner)
}
}
// Delete any superfluous matches if the round is won.
if redWins == 2 || blueWins == 2 {
for _, match := range unplayedMatches {
err = database.DeleteMatch(match)
if err != nil {
return []model.AllianceTeam{}, err
}
}
// Bail out and announce the winner of this round.
if redWins == 2 {
return redAlliance, nil
} else {
return blueAlliance, nil
}
}
// Create initial set of matches or recreate any superfluous matches that were deleted but now are needed
// due to a revision in who won.
if len(matches) == 0 || len(ties) == 0 && numIncomplete == 0 {
// Fill in zeroes if only one alliance is known.
if len(redAlliance) == 0 {
redAlliance = []model.AllianceTeam{{}, {}, {}}
} else if len(blueAlliance) == 0 {
blueAlliance = []model.AllianceTeam{{}, {}, {}}
}
if len(redAlliance) < 3 || len(blueAlliance) < 3 {
// Raise an error if the alliance selection process gave us less than 3 teams per alliance.
return []model.AllianceTeam{}, fmt.Errorf("Alliances must consist of at least 3 teams")
}
if len(matches) < 1 {
err = database.CreateMatch(createMatch(roundName, round, group, 1, redAlliance, blueAlliance))
if err != nil {
return []model.AllianceTeam{}, err
}
}
if len(matches) < 2 {
err = database.CreateMatch(createMatch(roundName, round, group, 2, redAlliance, blueAlliance))
if err != nil {
return []model.AllianceTeam{}, err
}
}
if len(matches) < 3 {
err = database.CreateMatch(createMatch(roundName, round, group, 3, redAlliance, blueAlliance))
if err != nil {
return []model.AllianceTeam{}, err
}
}
}
// Duplicate any ties if we have run out of matches. Don't change the team positions, so queueing
// personnel can reuse any tied matches without having to print new schedules.
if numIncomplete == 0 {
for index, tie := range ties {
match := createMatch(roundName, round, group, len(matches)+index+1, redAlliance, blueAlliance)
match.Red1, match.Red2, match.Red3 = tie.Red1, tie.Red2, tie.Red3
match.Blue1, match.Blue2, match.Blue3 = tie.Blue1, tie.Blue2, tie.Blue3
err = database.CreateMatch(match)
if err != nil {
return []model.AllianceTeam{}, err
}
}
}
return []model.AllianceTeam{}, 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 []model.AllianceTeam,
blueAlliance []model.AllianceTeam) *model.Match {
match := model.Match{Type: "elimination", DisplayName: fmt.Sprintf("%s-%d", roundName, instance),
ElimRound: round, ElimGroup: group, ElimInstance: instance}
positionRedTeams(&match, redAlliance)
positionBlueTeams(&match, blueAlliance)
return &match
}
// Assigns the first three teams from the alliance into the red team slots for the match.
func positionRedTeams(match *model.Match, alliance []model.AllianceTeam) {
// For the 2015 game, the alliance captain is in the middle, first pick on the left, second on the right.
match.Red1 = alliance[1].TeamId
match.Red2 = alliance[0].TeamId
match.Red3 = alliance[2].TeamId
}
// Assigns the first three teams from the alliance into the blue team slots for the match.
func positionBlueTeams(match *model.Match, alliance []model.AllianceTeam) {
// For the 2015 game, the alliance captain is in the middle, first pick on the left, second on the right.
match.Blue1 = alliance[1].TeamId
match.Blue2 = alliance[0].TeamId
match.Blue3 = alliance[2].TeamId
}
// Returns true if the given team is part of the given alliance.
func teamInAlliance(teamId int, alliance []model.AllianceTeam) bool {
for _, allianceTeam := range alliance {
if teamId == allianceTeam.TeamId {
return true
}
}
return false
}

View File

@@ -0,0 +1,751 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package tournament
import (
"github.com/Team254/cheesy-arena/model"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestEliminationScheduleInitial(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 2)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 3, len(matches)) {
assertMatch(t, matches[0], "F-1", 1, 2)
assertMatch(t, matches[1], "F-2", 1, 2)
assertMatch(t, matches[2], "F-3", 1, 2)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 3)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
assertMatch(t, matches[0], "SF2-1", 2, 3)
assertMatch(t, matches[1], "SF2-2", 2, 3)
assertMatch(t, matches[2], "SF2-3", 2, 3)
assertMatch(t, matches[3], "F-1", 1, 0)
assertMatch(t, matches[4], "F-2", 1, 0)
assertMatch(t, matches[5], "F-3", 1, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 4)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
assertMatch(t, matches[0], "SF1-1", 1, 4)
assertMatch(t, matches[1], "SF2-1", 2, 3)
assertMatch(t, matches[2], "SF1-2", 1, 4)
assertMatch(t, matches[3], "SF2-2", 2, 3)
assertMatch(t, matches[4], "SF1-3", 1, 4)
assertMatch(t, matches[5], "SF2-3", 2, 3)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 5)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 9, len(matches)) {
assertMatch(t, matches[0], "QF2-1", 4, 5)
assertMatch(t, matches[1], "QF2-2", 4, 5)
assertMatch(t, matches[2], "QF2-3", 4, 5)
assertMatch(t, matches[3], "SF1-1", 1, 0)
assertMatch(t, matches[4], "SF2-1", 2, 3)
assertMatch(t, matches[5], "SF1-2", 1, 0)
assertMatch(t, matches[6], "SF2-2", 2, 3)
assertMatch(t, matches[7], "SF1-3", 1, 0)
assertMatch(t, matches[8], "SF2-3", 2, 3)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 6)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 12, len(matches)) {
assertMatch(t, matches[0], "QF2-1", 4, 5)
assertMatch(t, matches[1], "QF4-1", 3, 6)
assertMatch(t, matches[2], "QF2-2", 4, 5)
assertMatch(t, matches[3], "QF4-2", 3, 6)
assertMatch(t, matches[4], "QF2-3", 4, 5)
assertMatch(t, matches[5], "QF4-3", 3, 6)
assertMatch(t, matches[6], "SF1-1", 1, 0)
assertMatch(t, matches[7], "SF2-1", 2, 0)
assertMatch(t, matches[8], "SF1-2", 1, 0)
assertMatch(t, matches[9], "SF2-2", 2, 0)
assertMatch(t, matches[10], "SF1-3", 1, 0)
assertMatch(t, matches[11], "SF2-3", 2, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 7)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 12, len(matches)) {
assertMatch(t, matches[0], "QF2-1", 4, 5)
assertMatch(t, matches[1], "QF3-1", 2, 7)
assertMatch(t, matches[2], "QF4-1", 3, 6)
assertMatch(t, matches[3], "QF2-2", 4, 5)
assertMatch(t, matches[4], "QF3-2", 2, 7)
assertMatch(t, matches[5], "QF4-2", 3, 6)
assertMatch(t, matches[6], "QF2-3", 4, 5)
assertMatch(t, matches[7], "QF3-3", 2, 7)
assertMatch(t, matches[8], "QF4-3", 3, 6)
assertMatch(t, matches[9], "SF1-1", 1, 0)
assertMatch(t, matches[10], "SF1-2", 1, 0)
assertMatch(t, matches[11], "SF1-3", 1, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 8)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 12, len(matches)) {
assertMatch(t, matches[0], "QF1-1", 1, 8)
assertMatch(t, matches[1], "QF2-1", 4, 5)
assertMatch(t, matches[2], "QF3-1", 2, 7)
assertMatch(t, matches[3], "QF4-1", 3, 6)
assertMatch(t, matches[4], "QF1-2", 1, 8)
assertMatch(t, matches[5], "QF2-2", 4, 5)
assertMatch(t, matches[6], "QF3-2", 2, 7)
assertMatch(t, matches[7], "QF4-2", 3, 6)
assertMatch(t, matches[8], "QF1-3", 1, 8)
assertMatch(t, matches[9], "QF2-3", 4, 5)
assertMatch(t, matches[10], "QF3-3", 2, 7)
assertMatch(t, matches[11], "QF4-3", 3, 6)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 9)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 15, len(matches)) {
assertMatch(t, matches[0], "EF2-1", 8, 9)
assertMatch(t, matches[1], "EF2-2", 8, 9)
assertMatch(t, matches[2], "EF2-3", 8, 9)
assertMatch(t, matches[3], "QF1-1", 1, 0)
assertMatch(t, matches[4], "QF2-1", 4, 5)
assertMatch(t, matches[5], "QF3-1", 2, 7)
assertMatch(t, matches[6], "QF4-1", 3, 6)
assertMatch(t, matches[7], "QF1-2", 1, 0)
assertMatch(t, matches[8], "QF2-2", 4, 5)
assertMatch(t, matches[9], "QF3-2", 2, 7)
assertMatch(t, matches[10], "QF4-2", 3, 6)
assertMatch(t, matches[11], "QF1-3", 1, 0)
assertMatch(t, matches[12], "QF2-3", 4, 5)
assertMatch(t, matches[13], "QF3-3", 2, 7)
assertMatch(t, matches[14], "QF4-3", 3, 6)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 10)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 18, len(matches)) {
assertMatch(t, matches[0], "EF2-1", 8, 9)
assertMatch(t, matches[1], "EF6-1", 7, 10)
assertMatch(t, matches[2], "EF2-2", 8, 9)
assertMatch(t, matches[3], "EF6-2", 7, 10)
assertMatch(t, matches[4], "EF2-3", 8, 9)
assertMatch(t, matches[5], "EF6-3", 7, 10)
assertMatch(t, matches[6], "QF1-1", 1, 0)
assertMatch(t, matches[7], "QF2-1", 4, 5)
assertMatch(t, matches[8], "QF3-1", 2, 0)
assertMatch(t, matches[9], "QF4-1", 3, 6)
assertMatch(t, matches[10], "QF1-2", 1, 0)
assertMatch(t, matches[11], "QF2-2", 4, 5)
assertMatch(t, matches[12], "QF3-2", 2, 0)
assertMatch(t, matches[13], "QF4-2", 3, 6)
assertMatch(t, matches[14], "QF1-3", 1, 0)
assertMatch(t, matches[15], "QF2-3", 4, 5)
assertMatch(t, matches[16], "QF3-3", 2, 0)
assertMatch(t, matches[17], "QF4-3", 3, 6)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 11)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 21, len(matches)) {
assertMatch(t, matches[0], "EF2-1", 8, 9)
assertMatch(t, matches[1], "EF6-1", 7, 10)
assertMatch(t, matches[2], "EF8-1", 6, 11)
assertMatch(t, matches[3], "EF2-2", 8, 9)
assertMatch(t, matches[4], "EF6-2", 7, 10)
assertMatch(t, matches[5], "EF8-2", 6, 11)
assertMatch(t, matches[6], "EF2-3", 8, 9)
assertMatch(t, matches[7], "EF6-3", 7, 10)
assertMatch(t, matches[8], "EF8-3", 6, 11)
assertMatch(t, matches[9], "QF1-1", 1, 0)
assertMatch(t, matches[10], "QF2-1", 4, 5)
assertMatch(t, matches[11], "QF3-1", 2, 0)
assertMatch(t, matches[12], "QF4-1", 3, 0)
assertMatch(t, matches[13], "QF1-2", 1, 0)
assertMatch(t, matches[14], "QF2-2", 4, 5)
assertMatch(t, matches[15], "QF3-2", 2, 0)
assertMatch(t, matches[16], "QF4-2", 3, 0)
assertMatch(t, matches[17], "QF1-3", 1, 0)
assertMatch(t, matches[18], "QF2-3", 4, 5)
assertMatch(t, matches[19], "QF3-3", 2, 0)
assertMatch(t, matches[20], "QF4-3", 3, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 12)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 24, len(matches)) {
assertMatch(t, matches[0], "EF2-1", 8, 9)
assertMatch(t, matches[1], "EF4-1", 5, 12)
assertMatch(t, matches[2], "EF6-1", 7, 10)
assertMatch(t, matches[3], "EF8-1", 6, 11)
assertMatch(t, matches[4], "EF2-2", 8, 9)
assertMatch(t, matches[5], "EF4-2", 5, 12)
assertMatch(t, matches[6], "EF6-2", 7, 10)
assertMatch(t, matches[7], "EF8-2", 6, 11)
assertMatch(t, matches[8], "EF2-3", 8, 9)
assertMatch(t, matches[9], "EF4-3", 5, 12)
assertMatch(t, matches[10], "EF6-3", 7, 10)
assertMatch(t, matches[11], "EF8-3", 6, 11)
assertMatch(t, matches[12], "QF1-1", 1, 0)
assertMatch(t, matches[13], "QF2-1", 4, 0)
assertMatch(t, matches[14], "QF3-1", 2, 0)
assertMatch(t, matches[15], "QF4-1", 3, 0)
assertMatch(t, matches[16], "QF1-2", 1, 0)
assertMatch(t, matches[17], "QF2-2", 4, 0)
assertMatch(t, matches[18], "QF3-2", 2, 0)
assertMatch(t, matches[19], "QF4-2", 3, 0)
assertMatch(t, matches[20], "QF1-3", 1, 0)
assertMatch(t, matches[21], "QF2-3", 4, 0)
assertMatch(t, matches[22], "QF3-3", 2, 0)
assertMatch(t, matches[23], "QF4-3", 3, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 13)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 24, len(matches)) {
assertMatch(t, matches[0], "EF2-1", 8, 9)
assertMatch(t, matches[1], "EF3-1", 4, 13)
assertMatch(t, matches[2], "EF4-1", 5, 12)
assertMatch(t, matches[3], "EF6-1", 7, 10)
assertMatch(t, matches[4], "EF8-1", 6, 11)
assertMatch(t, matches[5], "EF2-2", 8, 9)
assertMatch(t, matches[6], "EF3-2", 4, 13)
assertMatch(t, matches[7], "EF4-2", 5, 12)
assertMatch(t, matches[8], "EF6-2", 7, 10)
assertMatch(t, matches[9], "EF8-2", 6, 11)
assertMatch(t, matches[10], "EF2-3", 8, 9)
assertMatch(t, matches[11], "EF3-3", 4, 13)
assertMatch(t, matches[12], "EF4-3", 5, 12)
assertMatch(t, matches[13], "EF6-3", 7, 10)
assertMatch(t, matches[14], "EF8-3", 6, 11)
assertMatch(t, matches[15], "QF1-1", 1, 0)
assertMatch(t, matches[16], "QF3-1", 2, 0)
assertMatch(t, matches[17], "QF4-1", 3, 0)
assertMatch(t, matches[18], "QF1-2", 1, 0)
assertMatch(t, matches[19], "QF3-2", 2, 0)
assertMatch(t, matches[20], "QF4-2", 3, 0)
assertMatch(t, matches[21], "QF1-3", 1, 0)
assertMatch(t, matches[22], "QF3-3", 2, 0)
assertMatch(t, matches[23], "QF4-3", 3, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 14)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 24, len(matches)) {
assertMatch(t, matches[0], "EF2-1", 8, 9)
assertMatch(t, matches[1], "EF3-1", 4, 13)
assertMatch(t, matches[2], "EF4-1", 5, 12)
assertMatch(t, matches[3], "EF6-1", 7, 10)
assertMatch(t, matches[4], "EF7-1", 3, 14)
assertMatch(t, matches[5], "EF8-1", 6, 11)
assertMatch(t, matches[6], "EF2-2", 8, 9)
assertMatch(t, matches[7], "EF3-2", 4, 13)
assertMatch(t, matches[8], "EF4-2", 5, 12)
assertMatch(t, matches[9], "EF6-2", 7, 10)
assertMatch(t, matches[10], "EF7-2", 3, 14)
assertMatch(t, matches[11], "EF8-2", 6, 11)
assertMatch(t, matches[12], "EF2-3", 8, 9)
assertMatch(t, matches[13], "EF3-3", 4, 13)
assertMatch(t, matches[14], "EF4-3", 5, 12)
assertMatch(t, matches[15], "EF6-3", 7, 10)
assertMatch(t, matches[16], "EF7-3", 3, 14)
assertMatch(t, matches[17], "EF8-3", 6, 11)
assertMatch(t, matches[18], "QF1-1", 1, 0)
assertMatch(t, matches[19], "QF3-1", 2, 0)
assertMatch(t, matches[20], "QF1-2", 1, 0)
assertMatch(t, matches[21], "QF3-2", 2, 0)
assertMatch(t, matches[22], "QF1-3", 1, 0)
assertMatch(t, matches[23], "QF3-3", 2, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 15)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 24, len(matches)) {
assertMatch(t, matches[0], "EF2-1", 8, 9)
assertMatch(t, matches[1], "EF3-1", 4, 13)
assertMatch(t, matches[2], "EF4-1", 5, 12)
assertMatch(t, matches[3], "EF5-1", 2, 15)
assertMatch(t, matches[4], "EF6-1", 7, 10)
assertMatch(t, matches[5], "EF7-1", 3, 14)
assertMatch(t, matches[6], "EF8-1", 6, 11)
assertMatch(t, matches[7], "EF2-2", 8, 9)
assertMatch(t, matches[8], "EF3-2", 4, 13)
assertMatch(t, matches[9], "EF4-2", 5, 12)
assertMatch(t, matches[10], "EF5-2", 2, 15)
assertMatch(t, matches[11], "EF6-2", 7, 10)
assertMatch(t, matches[12], "EF7-2", 3, 14)
assertMatch(t, matches[13], "EF8-2", 6, 11)
assertMatch(t, matches[14], "EF2-3", 8, 9)
assertMatch(t, matches[15], "EF3-3", 4, 13)
assertMatch(t, matches[16], "EF4-3", 5, 12)
assertMatch(t, matches[17], "EF5-3", 2, 15)
assertMatch(t, matches[18], "EF6-3", 7, 10)
assertMatch(t, matches[19], "EF7-3", 3, 14)
assertMatch(t, matches[20], "EF8-3", 6, 11)
assertMatch(t, matches[21], "QF1-1", 1, 0)
assertMatch(t, matches[22], "QF1-2", 1, 0)
assertMatch(t, matches[23], "QF1-3", 1, 0)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
CreateTestAlliances(database, 16)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 24, len(matches)) {
assertMatch(t, matches[0], "EF1-1", 1, 16)
assertMatch(t, matches[1], "EF2-1", 8, 9)
assertMatch(t, matches[2], "EF3-1", 4, 13)
assertMatch(t, matches[3], "EF4-1", 5, 12)
assertMatch(t, matches[4], "EF5-1", 2, 15)
assertMatch(t, matches[5], "EF6-1", 7, 10)
assertMatch(t, matches[6], "EF7-1", 3, 14)
assertMatch(t, matches[7], "EF8-1", 6, 11)
assertMatch(t, matches[8], "EF1-2", 1, 16)
assertMatch(t, matches[9], "EF2-2", 8, 9)
assertMatch(t, matches[10], "EF3-2", 4, 13)
assertMatch(t, matches[11], "EF4-2", 5, 12)
assertMatch(t, matches[12], "EF5-2", 2, 15)
assertMatch(t, matches[13], "EF6-2", 7, 10)
assertMatch(t, matches[14], "EF7-2", 3, 14)
assertMatch(t, matches[15], "EF8-2", 6, 11)
assertMatch(t, matches[16], "EF1-3", 1, 16)
assertMatch(t, matches[17], "EF2-3", 8, 9)
assertMatch(t, matches[18], "EF3-3", 4, 13)
assertMatch(t, matches[19], "EF4-3", 5, 12)
assertMatch(t, matches[20], "EF5-3", 2, 15)
assertMatch(t, matches[21], "EF6-3", 7, 10)
assertMatch(t, matches[22], "EF7-3", 3, 14)
assertMatch(t, matches[23], "EF8-3", 6, 11)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
}
func TestEliminationScheduleErrors(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 1)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.NotNil(t, err) {
assert.Equal(t, "Must have at least 2 alliances", err.Error())
}
database.TruncateAllianceTeams()
CreateTestAlliances(database, 17)
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.NotNil(t, err) {
assert.Equal(t, "Round of depth 32 is not supported", err.Error())
}
database.TruncateAllianceTeams()
database.CreateAllianceTeam(&model.AllianceTeam{0, 1, 0, 1})
database.CreateAllianceTeam(&model.AllianceTeam{0, 1, 1, 2})
database.CreateAllianceTeam(&model.AllianceTeam{0, 2, 0, 3})
database.CreateAllianceTeam(&model.AllianceTeam{0, 2, 1, 4})
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.NotNil(t, err) {
assert.Equal(t, "Alliances must consist of at least 3 teams", err.Error())
}
database.TruncateAllianceTeams()
}
func TestEliminationSchedulePopulatePartialMatch(t *testing.T) {
database := setupTestDb(t)
// Final should be updated after semifinal is concluded.
CreateTestAlliances(database, 3)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "SF2-1", "B")
scoreMatch(database, "SF2-2", "B")
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 5, len(matches)) {
assertMatch(t, matches[2], "F-1", 1, 3)
assertMatch(t, matches[3], "F-2", 1, 3)
assertMatch(t, matches[4], "F-3", 1, 3)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
database.TruncateMatchResults()
// Final should be generated and populated as both semifinals conclude.
CreateTestAlliances(database, 4)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "SF2-1", "R")
scoreMatch(database, "SF2-2", "R")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 8, len(matches)) {
assertMatch(t, matches[5], "F-1", 0, 2)
assertMatch(t, matches[6], "F-2", 0, 2)
assertMatch(t, matches[7], "F-3", 0, 2)
}
scoreMatch(database, "SF1-1", "R")
scoreMatch(database, "SF1-2", "R")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 7, len(matches)) {
assertMatch(t, matches[4], "F-1", 1, 2)
assertMatch(t, matches[5], "F-2", 1, 2)
assertMatch(t, matches[6], "F-3", 1, 2)
}
database.TruncateAllianceTeams()
database.TruncateMatches()
database.TruncateMatchResults()
}
func TestEliminationScheduleCreateNextRound(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 4)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "SF1-1", "B")
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, _ := database.GetMatchesByType("elimination")
assert.Equal(t, 6, len(matches))
scoreMatch(database, "SF2-1", "B")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 6, len(matches))
scoreMatch(database, "SF1-2", "B")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 8, len(matches))
scoreMatch(database, "SF2-2", "B")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 7, len(matches)) {
assertMatch(t, matches[4], "F-1", 4, 3)
assertMatch(t, matches[5], "F-2", 4, 3)
assertMatch(t, matches[6], "F-3", 4, 3)
}
}
func TestEliminationScheduleDetermineWinner(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", "T")
winner, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ := database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-2", "B")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-3", "B")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.Nil(t, err) {
if assert.Equal(t, 3, len(winner)) {
assert.Equal(t, 2, winner[0].TeamId)
}
}
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
database.TruncateAllianceTeams()
database.TruncateMatches()
database.TruncateMatchResults()
// Round with one tie and a split.
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", "R")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-2", "T")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-3", "B")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 4, len(matches))
assert.Equal(t, "F-4", matches[3].DisplayName)
scoreMatch(database, "F-4", "T")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
scoreMatch(database, "F-5", "R")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.Nil(t, err) {
if assert.Equal(t, 3, len(winner)) {
assert.Equal(t, 1, winner[0].TeamId)
}
}
database.TruncateAllianceTeams()
database.TruncateMatches()
database.TruncateMatchResults()
// Round with two ties.
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", "T")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-2", "B")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 3, len(matches))
scoreMatch(database, "F-3", "T")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
assert.Empty(t, winner)
matches, _ = database.GetMatchesByType("elimination")
assert.Equal(t, 5, len(matches))
assert.Equal(t, "F-4", matches[3].DisplayName)
assert.Equal(t, "F-5", matches[4].DisplayName)
scoreMatch(database, "F-4", "B")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.Nil(t, err) {
if assert.Equal(t, 3, len(winner)) {
assert.Equal(t, 2, winner[0].TeamId)
}
}
database.TruncateAllianceTeams()
database.TruncateMatches()
database.TruncateMatchResults()
// Round with repeated ties.
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", "T")
scoreMatch(database, "F-2", "T")
scoreMatch(database, "F-3", "T")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-4", "T")
scoreMatch(database, "F-5", "T")
scoreMatch(database, "F-6", "T")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-7", "R")
scoreMatch(database, "F-8", "B")
scoreMatch(database, "F-9", "R")
winner, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.Nil(t, err) {
if assert.Equal(t, 3, len(winner)) {
assert.Equal(t, 1, winner[0].TeamId)
}
}
}
func TestEliminationScheduleRemoveUnneededMatches(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", "R")
scoreMatch(database, "F-2", "R")
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, _ := database.GetMatchesByType("elimination")
assert.Equal(t, 2, len(matches))
// Check that the deleted match is recreated if the score is changed.
scoreMatch(database, "F-2", "B")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, _ = database.GetMatchesByType("elimination")
if assert.Equal(t, 3, len(matches)) {
assert.Equal(t, "F-3", matches[2].DisplayName)
}
}
func TestEliminationScheduleChangePreviousRoundResult(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 4)
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
scoreMatch(database, "SF2-1", "R")
scoreMatch(database, "SF2-2", "B")
scoreMatch(database, "SF2-3", "R")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
scoreMatch(database, "SF2-3", "B")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err := database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 9, len(matches)) {
assertMatch(t, matches[6], "F-1", 0, 3)
assertMatch(t, matches[7], "F-2", 0, 3)
assertMatch(t, matches[8], "F-3", 0, 3)
}
scoreMatch(database, "SF1-1", "R")
scoreMatch(database, "SF1-2", "R")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
scoreMatch(database, "SF1-2", "B")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
scoreMatch(database, "SF1-3", "B")
_, err = UpdateEliminationSchedule(database, time.Unix(0, 0))
assert.Nil(t, err)
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 9, len(matches)) {
assertMatch(t, matches[6], "F-1", 4, 3)
assertMatch(t, matches[7], "F-2", 4, 3)
assertMatch(t, matches[8], "F-3", 4, 3)
}
}
func TestEliminationScheduleUnscoredMatch(t *testing.T) {
database := setupTestDb(t)
CreateTestAlliances(database, 2)
UpdateEliminationSchedule(database, time.Unix(0, 0))
scoreMatch(database, "F-1", "blorpy")
_, err := UpdateEliminationSchedule(database, time.Unix(0, 0))
if assert.NotNil(t, err) {
assert.Equal(t, "Completed match 1 has invalid winner 'blorpy'", err.Error())
}
}
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, 6, len(matches)) {
assert.True(t, time.Unix(1000, 0).Equal(matches[0].Time))
assert.True(t, time.Unix(1600, 0).Equal(matches[1].Time))
assert.True(t, time.Unix(2200, 0).Equal(matches[2].Time))
assert.True(t, time.Unix(2800, 0).Equal(matches[3].Time))
assert.True(t, time.Unix(3400, 0).Equal(matches[4].Time))
assert.True(t, time.Unix(4000, 0).Equal(matches[5].Time))
}
scoreMatch(database, "SF1-1", "R")
scoreMatch(database, "SF1-3", "B")
UpdateEliminationSchedule(database, time.Unix(5000, 0))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)
if assert.Equal(t, 6, len(matches)) {
assert.True(t, time.Unix(1000, 0).Equal(matches[0].Time))
assert.True(t, time.Unix(5000, 0).Equal(matches[1].Time))
assert.True(t, time.Unix(5600, 0).Equal(matches[2].Time))
assert.True(t, time.Unix(6200, 0).Equal(matches[3].Time))
assert.True(t, time.Unix(3400, 0).Equal(matches[4].Time))
assert.True(t, time.Unix(6800, 0).Equal(matches[5].Time))
}
}
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.Red1)
assert.Equal(t, blueAlliance, match.Blue1)
}
func scoreMatch(database *model.Database, displayName string, winner string) {
match, _ := database.GetMatchByName("elimination", displayName)
match.Status = "complete"
match.Winner = winner
database.SaveMatch(match)
}

View File

@@ -0,0 +1,147 @@
// Copyright 2017 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Functions for calculating the qualification rankings.
package tournament
import (
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"sort"
"strconv"
)
// Determines the rankings from the stored match results, and saves them to the database.
func CalculateRankings(database *model.Database) error {
matches, err := database.GetMatchesByType("qualification")
if err != nil {
return err
}
rankings := make(map[int]*game.Ranking)
for _, match := range matches {
if match.Status != "complete" {
continue
}
matchResult, err := database.GetMatchResultForMatch(match.Id)
if err != nil {
return err
}
if !match.Red1IsSurrogate {
addMatchResultToRankings(rankings, match.Red1, matchResult, true)
}
if !match.Red2IsSurrogate {
addMatchResultToRankings(rankings, match.Red2, matchResult, true)
}
if !match.Red3IsSurrogate {
addMatchResultToRankings(rankings, match.Red3, matchResult, true)
}
if !match.Blue1IsSurrogate {
addMatchResultToRankings(rankings, match.Blue1, matchResult, false)
}
if !match.Blue2IsSurrogate {
addMatchResultToRankings(rankings, match.Blue2, matchResult, false)
}
if !match.Blue3IsSurrogate {
addMatchResultToRankings(rankings, match.Blue3, matchResult, false)
}
}
sortedRankings := sortRankings(rankings)
for rank, ranking := range sortedRankings {
ranking.Rank = rank + 1
}
err = database.ReplaceAllRankings(sortedRankings)
if err != nil {
return nil
}
return nil
}
// Checks all the match results for yellow and red cards, and updates the team model accordingly.
func CalculateTeamCards(database *model.Database, matchType string) error {
teams, err := database.GetAllTeams()
if err != nil {
return err
}
teamsMap := make(map[string]model.Team)
for _, team := range teams {
team.YellowCard = false
teamsMap[strconv.Itoa(team.Id)] = team
}
matches, err := database.GetMatchesByType(matchType)
if err != nil {
return err
}
for _, match := range matches {
if match.Status != "complete" {
continue
}
matchResult, err := database.GetMatchResultForMatch(match.Id)
if err != nil {
return err
}
// Mark the team as having a yellow card if they got either a yellow or red in a previous match.
for teamId, card := range matchResult.RedCards {
if team, ok := teamsMap[teamId]; ok && card != "" {
team.YellowCard = true
teamsMap[teamId] = team
}
}
for teamId, card := range matchResult.BlueCards {
if team, ok := teamsMap[teamId]; ok && card != "" {
team.YellowCard = true
teamsMap[teamId] = team
}
}
}
// Save the teams to the database.
for _, team := range teamsMap {
err = database.SaveTeam(&team)
if err != nil {
return err
}
}
return nil
}
// Incrementally accounts for the given match result in the set of rankings that are being built.
func addMatchResultToRankings(rankings map[int]*game.Ranking, teamId int, matchResult *model.MatchResult, isRed bool) {
ranking := rankings[teamId]
if ranking == nil {
ranking = &game.Ranking{TeamId: teamId}
rankings[teamId] = ranking
}
// Determine whether the team was disqualified.
var cards map[string]string
if isRed {
cards = matchResult.RedCards
} else {
cards = matchResult.BlueCards
}
disqualified := false
if card, ok := cards[strconv.Itoa(teamId)]; ok && card == "red" {
disqualified = true
}
if isRed {
ranking.AddScoreSummary(matchResult.RedScoreSummary(), matchResult.BlueScoreSummary(), disqualified)
} else {
ranking.AddScoreSummary(matchResult.BlueScoreSummary(), matchResult.RedScoreSummary(), disqualified)
}
}
func sortRankings(rankings map[int]*game.Ranking) game.Rankings {
var sortedRankings game.Rankings
for _, ranking := range rankings {
sortedRankings = append(sortedRankings, ranking)
}
sort.Sort(sortedRankings)
return sortedRankings
}

View File

@@ -0,0 +1,91 @@
// Copyright 2017 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package tournament
import (
"github.com/Team254/cheesy-arena/model"
"github.com/stretchr/testify/assert"
"testing"
)
func TestCalculateRankings(t *testing.T) {
database := setupTestDb(t)
setupMatchResultsForRankings(database)
err := CalculateRankings(database)
assert.Nil(t, err)
rankings, err := database.GetAllRankings()
assert.Nil(t, err)
if assert.Equal(t, 6, len(rankings)) {
assert.Equal(t, 4, rankings[0].TeamId)
assert.Equal(t, 5, rankings[1].TeamId)
assert.Equal(t, 6, rankings[2].TeamId)
assert.Equal(t, 1, rankings[3].TeamId)
assert.Equal(t, 3, rankings[4].TeamId)
assert.Equal(t, 2, rankings[5].TeamId)
}
// Test after changing a match result.
matchResult3 := model.BuildTestMatchResult(3, 3)
matchResult3.RedScore, matchResult3.BlueScore = matchResult3.BlueScore, matchResult3.RedScore
err = database.CreateMatchResult(matchResult3)
assert.Nil(t, err)
err = CalculateRankings(database)
assert.Nil(t, err)
rankings, err = database.GetAllRankings()
assert.Nil(t, err)
if assert.Equal(t, 6, len(rankings)) {
assert.Equal(t, 6, rankings[0].TeamId)
assert.Equal(t, 5, rankings[1].TeamId)
assert.Equal(t, 4, rankings[2].TeamId)
assert.Equal(t, 1, rankings[3].TeamId)
assert.Equal(t, 3, rankings[4].TeamId)
assert.Equal(t, 2, rankings[5].TeamId)
}
}
// Sets up a schedule and results that touches on all possible variables.
func setupMatchResultsForRankings(database *model.Database) {
match1 := model.Match{Type: "qualification", DisplayName: "1", Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5,
Blue3: 6, Status: "complete"}
database.CreateMatch(&match1)
matchResult1 := model.BuildTestMatchResult(match1.Id, 1)
matchResult1.RedCards = map[string]string{"2": "red"}
database.CreateMatchResult(matchResult1)
match2 := model.Match{Type: "qualification", DisplayName: "2", Red1: 1, Red2: 3, Red3: 5, Blue1: 2, Blue2: 4,
Blue3: 6, Status: "complete", Red2IsSurrogate: true, Blue3IsSurrogate: true}
database.CreateMatch(&match2)
matchResult2 := model.BuildTestMatchResult(match2.Id, 1)
matchResult2.BlueScore = matchResult2.RedScore
database.CreateMatchResult(matchResult2)
match3 := model.Match{Type: "qualification", DisplayName: "3", Red1: 6, Red2: 5, Red3: 4, Blue1: 3, Blue2: 2,
Blue3: 1, Status: "complete", Red3IsSurrogate: true}
database.CreateMatch(&match3)
matchResult3 := model.BuildTestMatchResult(match3.Id, 1)
database.CreateMatchResult(matchResult3)
matchResult3 = model.NewMatchResult()
matchResult3.MatchId = match3.Id
matchResult3.PlayNumber = 2
database.CreateMatchResult(matchResult3)
match4 := model.Match{Type: "practice", DisplayName: "1", Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5,
Blue3: 6, Status: "complete"}
database.CreateMatch(&match4)
matchResult4 := model.BuildTestMatchResult(match4.Id, 1)
database.CreateMatchResult(matchResult4)
match5 := model.Match{Type: "elimination", DisplayName: "F-1", Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5,
Blue3: 6, Status: "complete"}
database.CreateMatch(&match5)
matchResult5 := model.BuildTestMatchResult(match5.Id, 1)
database.CreateMatchResult(matchResult5)
match6 := model.Match{Type: "qualification", DisplayName: "4", Red1: 7, Red2: 8, Red3: 9, Blue1: 10, Blue2: 11,
Blue3: 12, Status: ""}
database.CreateMatch(&match6)
matchResult6 := model.BuildTestMatchResult(match6.Id, 1)
database.CreateMatchResult(matchResult6)
}

103
tournament/schedule.go Normal file
View File

@@ -0,0 +1,103 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Functions for creating practice and qualification match schedules.
package tournament
import (
"encoding/csv"
"fmt"
"github.com/Team254/cheesy-arena/model"
"math"
"math/rand"
"os"
"strconv"
"time"
)
var schedulesDir = "schedules"
const TeamsPerMatch = 6
type ScheduleBlock struct {
StartTime time.Time
NumMatches int
MatchSpacingSec int
}
// Creates a random schedule for the given parameters and returns it as a list of matches.
func BuildRandomSchedule(teams []model.Team, scheduleBlocks []ScheduleBlock, matchType string) ([]model.Match, error) {
// Load the anonymized, pre-randomized match schedule for the given number of teams and matches per team.
numTeams := len(teams)
numMatches := countMatches(scheduleBlocks)
matchesPerTeam := int(float32(numMatches*TeamsPerMatch) / float32(numTeams))
// Adjust the number of matches to remove any excess from non-perfect block scheduling.
numMatches = int(math.Ceil(float64(numTeams) * float64(matchesPerTeam) / TeamsPerMatch))
file, err := os.Open(fmt.Sprintf("%s/%d_%d.csv", schedulesDir, numTeams, matchesPerTeam))
if err != nil {
return nil, fmt.Errorf("No schedule template exists for %d teams and %d matches", numTeams, matchesPerTeam)
}
defer file.Close()
reader := csv.NewReader(file)
csvLines, err := reader.ReadAll()
if err != nil {
return nil, err
}
if len(csvLines) != numMatches {
return nil, fmt.Errorf("Schedule file contains %d matches, expected %d", len(csvLines), numMatches)
}
// Convert string fields from schedule to integers.
anonSchedule := make([][12]int, numMatches)
for i := 0; i < numMatches; i++ {
for j := 0; j < 12; j++ {
anonSchedule[i][j], err = strconv.Atoi(csvLines[i][j])
if err != nil {
return nil, err
}
}
}
// Generate a random permutation of the team ordering to fill into the pre-randomized schedule.
teamShuffle := rand.Perm(numTeams)
matches := make([]model.Match, numMatches)
for i, anonMatch := range anonSchedule {
matches[i].Type = matchType
matches[i].DisplayName = strconv.Itoa(i + 1)
matches[i].Red1 = teams[teamShuffle[anonMatch[0]-1]].Id
matches[i].Red1IsSurrogate = (anonMatch[1] == 1)
matches[i].Red2 = teams[teamShuffle[anonMatch[2]-1]].Id
matches[i].Red2IsSurrogate = (anonMatch[3] == 1)
matches[i].Red3 = teams[teamShuffle[anonMatch[4]-1]].Id
matches[i].Red3IsSurrogate = (anonMatch[5] == 1)
matches[i].Blue1 = teams[teamShuffle[anonMatch[6]-1]].Id
matches[i].Blue1IsSurrogate = (anonMatch[7] == 1)
matches[i].Blue2 = teams[teamShuffle[anonMatch[8]-1]].Id
matches[i].Blue2IsSurrogate = (anonMatch[9] == 1)
matches[i].Blue3 = teams[teamShuffle[anonMatch[10]-1]].Id
matches[i].Blue3IsSurrogate = (anonMatch[11] == 1)
}
// Fill in the match times.
matchIndex := 0
for _, block := range scheduleBlocks {
for i := 0; i < block.NumMatches && matchIndex < numMatches; i++ {
matches[matchIndex].Time = block.StartTime.Add(time.Duration(i*block.MatchSpacingSec) * time.Second)
matchIndex++
}
}
return matches, nil
}
// Returns the total number of matches that can be run within the given schedule blocks.
func countMatches(scheduleBlocks []ScheduleBlock) int {
numMatches := 0
for _, block := range scheduleBlocks {
numMatches += block.NumMatches
}
return numMatches
}

123
tournament/schedule_test.go Normal file
View File

@@ -0,0 +1,123 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package tournament
import (
"fmt"
"github.com/Team254/cheesy-arena/model"
"github.com/stretchr/testify/assert"
"math/rand"
"os"
"testing"
"time"
)
func TestMain(m *testing.M) {
schedulesDir = "../schedules"
os.Exit(m.Run())
}
func TestNonExistentSchedule(t *testing.T) {
teams := make([]model.Team, 6)
scheduleBlocks := []ScheduleBlock{{time.Unix(0, 0).UTC(), 2, 60}}
_, err := BuildRandomSchedule(teams, scheduleBlocks, "test")
expectedErr := "No schedule template exists for 6 teams and 2 matches"
if assert.NotNil(t, err) {
assert.Equal(t, expectedErr, err.Error())
}
}
func TestMalformedSchedule(t *testing.T) {
filename := fmt.Sprintf("%s/6_1.csv", schedulesDir)
scheduleFile, _ := os.Create(filename)
defer os.Remove(filename)
scheduleFile.WriteString("1,0,2,0,3,0,4,0,5,0,6,0\n6,0,5,0,4,0,3,0,2,0,1,0\n")
scheduleFile.Close()
teams := make([]model.Team, 6)
scheduleBlocks := []ScheduleBlock{{time.Unix(0, 0).UTC(), 1, 60}}
_, err := BuildRandomSchedule(teams, scheduleBlocks, "test")
expectedErr := "Schedule file contains 2 matches, expected 1"
if assert.NotNil(t, err) {
assert.Equal(t, expectedErr, err.Error())
}
os.Remove(filename)
scheduleFile, _ = os.Create(filename)
scheduleFile.WriteString("1,0,asdf,0,3,0,4,0,5,0,6,0\n")
scheduleFile.Close()
_, err = BuildRandomSchedule(teams, scheduleBlocks, "test")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "strconv.Atoi")
}
}
func TestScheduleTeams(t *testing.T) {
rand.Seed(0)
numTeams := 18
teams := make([]model.Team, numTeams)
for i := 0; i < numTeams; i++ {
teams[i].Id = i + 101
}
scheduleBlocks := []ScheduleBlock{{time.Unix(0, 0).UTC(), 6, 60}}
matches, err := BuildRandomSchedule(teams, scheduleBlocks, "test")
assert.Nil(t, err)
assert.Equal(t, model.Match{Type: "test", DisplayName: "1", Time: time.Unix(0, 0).UTC(), Red1: 115, Red2: 111,
Red3: 108, Blue1: 109, Blue2: 116, Blue3: 117}, matches[0])
assert.Equal(t, model.Match{Type: "test", DisplayName: "2", Time: time.Unix(60, 0).UTC(), Red1: 114, Red2: 112,
Red3: 103, Blue1: 101, Blue2: 104, Blue3: 118}, matches[1])
assert.Equal(t, model.Match{Type: "test", DisplayName: "3", Time: time.Unix(120, 0).UTC(), Red1: 110, Red2: 107,
Red3: 105, Blue1: 106, Blue2: 113, Blue3: 102}, matches[2])
assert.Equal(t, model.Match{Type: "test", DisplayName: "4", Time: time.Unix(180, 0).UTC(), Red1: 112, Red2: 108,
Red3: 109, Blue1: 101, Blue2: 111, Blue3: 103}, matches[3])
assert.Equal(t, model.Match{Type: "test", DisplayName: "5", Time: time.Unix(240, 0).UTC(), Red1: 113, Red2: 117,
Red3: 115, Blue1: 110, Blue2: 114, Blue3: 102}, matches[4])
assert.Equal(t, model.Match{Type: "test", DisplayName: "6", Time: time.Unix(300, 0).UTC(), Red1: 118, Red2: 105,
Red3: 106, Blue1: 107, Blue2: 104, Blue3: 116}, matches[5])
// Check with excess room for matches in the schedule.
scheduleBlocks = []ScheduleBlock{{time.Unix(0, 0).UTC(), 7, 60}}
matches, err = BuildRandomSchedule(teams, scheduleBlocks, "test")
assert.Nil(t, err)
}
func TestScheduleTiming(t *testing.T) {
teams := make([]model.Team, 18)
scheduleBlocks := []ScheduleBlock{{time.Unix(100, 0).UTC(), 10, 75},
{time.Unix(20000, 0).UTC(), 5, 1000},
{time.Unix(100000, 0).UTC(), 15, 29}}
matches, err := BuildRandomSchedule(teams, scheduleBlocks, "test")
assert.Nil(t, err)
assert.Equal(t, time.Unix(100, 0).UTC(), matches[0].Time)
assert.Equal(t, time.Unix(775, 0).UTC(), matches[9].Time)
assert.Equal(t, time.Unix(20000, 0).UTC(), matches[10].Time)
assert.Equal(t, time.Unix(24000, 0).UTC(), matches[14].Time)
assert.Equal(t, time.Unix(100000, 0).UTC(), matches[15].Time)
assert.Equal(t, time.Unix(100406, 0).UTC(), matches[29].Time)
}
func TestScheduleSurrogates(t *testing.T) {
rand.Seed(0)
numTeams := 38
teams := make([]model.Team, numTeams)
for i := 0; i < numTeams; i++ {
teams[i].Id = i + 101
}
scheduleBlocks := []ScheduleBlock{{time.Unix(0, 0).UTC(), 64, 60}}
matches, _ := BuildRandomSchedule(teams, scheduleBlocks, "test")
for i, match := range matches {
if i == 13 || i == 14 {
if !match.Red1IsSurrogate || match.Red2IsSurrogate || match.Red3IsSurrogate ||
!match.Blue1IsSurrogate || match.Blue2IsSurrogate || match.Blue3IsSurrogate {
t.Errorf("Surrogates wrong for match %d", i+1)
}
} else {
if match.Red1IsSurrogate || match.Red2IsSurrogate || match.Red3IsSurrogate ||
match.Blue1IsSurrogate || match.Blue2IsSurrogate || match.Blue3IsSurrogate {
t.Errorf("Expected match %d to be free of surrogates", i+1)
}
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright 2017 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Helper methods for use in tests in this package and others.
package tournament
import (
"github.com/Team254/cheesy-arena/model"
"testing"
)
func CreateTestAlliances(database *model.Database, allianceCount int) {
for i := 1; i <= allianceCount; i++ {
database.CreateAllianceTeam(&model.AllianceTeam{0, i, 0, i})
database.CreateAllianceTeam(&model.AllianceTeam{0, i, 1, i})
database.CreateAllianceTeam(&model.AllianceTeam{0, i, 2, i})
}
}
func setupTestDb(t *testing.T) *model.Database {
return model.SetupTestDb(t, "tournament", "..")
}