Add a playoff alliances report.

This commit is contained in:
Patrick Fairbank
2022-08-21 11:10:51 -07:00
parent 8e86206168
commit b1dfe59369
7 changed files with 154 additions and 27 deletions

View File

@@ -164,17 +164,17 @@ func createMatchupGraph(
// Returns the winning alliance ID of the entire bracket, or 0 if it is not yet known. // Returns the winning alliance ID of the entire bracket, or 0 if it is not yet known.
func (bracket *Bracket) Winner() int { func (bracket *Bracket) Winner() int {
return bracket.FinalsMatchup.winner() return bracket.FinalsMatchup.Winner()
} }
// Returns the finalist alliance ID of the entire bracket, or 0 if it is not yet known. // Returns the finalist alliance ID of the entire bracket, or 0 if it is not yet known.
func (bracket *Bracket) Finalist() int { func (bracket *Bracket) Finalist() int {
return bracket.FinalsMatchup.loser() return bracket.FinalsMatchup.Loser()
} }
// Returns true if the bracket has been won, and false if it is still to be determined. // Returns true if the bracket has been won, and false if it is still to be determined.
func (bracket *Bracket) IsComplete() bool { func (bracket *Bracket) IsComplete() bool {
return bracket.FinalsMatchup.isComplete() return bracket.FinalsMatchup.IsComplete()
} }
// Returns a slice of all matchups contained within the bracket. // Returns a slice of all matchups contained within the bracket.
@@ -230,12 +230,20 @@ func (bracket *Bracket) Update(database *model.Database, startTime *time.Time) e
return nil return nil
} }
// Prints out each matchup within the bracket in level order, backwards from finals to earlier rounds, for debugging. // Performs a traversal of the bracket in reverse order of rounds and invokes the given function for each visited
func (bracket *Bracket) print() { // matchup.
func (bracket *Bracket) ReverseRoundOrderTraversal(visitFunction func(*Matchup)) {
matchupQueue := []*Matchup{bracket.FinalsMatchup} matchupQueue := []*Matchup{bracket.FinalsMatchup}
for len(matchupQueue) > 0 { for len(matchupQueue) > 0 {
// Reorder the queue since graph depth doesn't necessarily equate to round.
sort.Slice(matchupQueue, func(i, j int) bool {
if matchupQueue[i].Round == matchupQueue[j].Round {
return matchupQueue[i].Group < matchupQueue[j].Group
}
return matchupQueue[i].Round > matchupQueue[j].Round
})
matchup := matchupQueue[0] matchup := matchupQueue[0]
fmt.Printf("%+v\n\n", matchup) visitFunction(matchup)
matchupQueue = matchupQueue[1:] matchupQueue = matchupQueue[1:]
if matchup != nil { if matchup != nil {
if matchup.redAllianceSourceMatchup != nil && matchup.redAllianceSource.useWinner { if matchup.redAllianceSourceMatchup != nil && matchup.redAllianceSource.useWinner {
@@ -247,3 +255,10 @@ func (bracket *Bracket) print() {
} }
} }
} }
// Prints out each matchup within the bracket in level order, backwards from finals to earlier rounds, for debugging.
func (bracket *Bracket) print() {
bracket.ReverseRoundOrderTraversal(func(matchup *Matchup) {
fmt.Printf("%+v\n\n", matchup)
})
}

View File

@@ -209,3 +209,25 @@ func TestBracketGetMatchup(t *testing.T) {
} }
assert.Nil(t, matchup) assert.Nil(t, matchup)
} }
func TestBracketLevelOrderTraversal(t *testing.T) {
database := setupTestDb(t)
tournament.CreateTestAlliances(database, 8)
bracket, err := NewSingleEliminationBracket(8)
assert.Nil(t, err)
var displayNames []string
bracket.ReverseRoundOrderTraversal(func(matchup *Matchup) {
displayNames = append(displayNames, matchup.displayName)
})
assert.Equal(t, []string{"F", "SF1", "SF2", "QF1", "QF2", "QF3", "QF4"}, displayNames)
bracket, err = NewDoubleEliminationBracket(8)
assert.Nil(t, err)
displayNames = nil
bracket.ReverseRoundOrderTraversal(func(matchup *Matchup) {
displayNames = append(displayNames, matchup.displayName)
})
assert.Equal(t, []string{"F", "13", "11", "12", "9", "10", "5", "6", "7", "8", "1", "2", "3", "4"}, displayNames)
}

View File

@@ -135,7 +135,7 @@ func (matchup *Matchup) StatusText() (string, string) {
} }
// Returns the winning alliance ID of the matchup, or 0 if it is not yet known. // Returns the winning alliance ID of the matchup, or 0 if it is not yet known.
func (matchup *Matchup) winner() int { func (matchup *Matchup) Winner() int {
if matchup.RedAllianceWins >= matchup.NumWinsToAdvance { if matchup.RedAllianceWins >= matchup.NumWinsToAdvance {
return matchup.RedAllianceId return matchup.RedAllianceId
} }
@@ -146,7 +146,7 @@ func (matchup *Matchup) winner() int {
} }
// Returns the losing alliance ID of the matchup, or 0 if it is not yet known. // Returns the losing alliance ID of the matchup, or 0 if it is not yet known.
func (matchup *Matchup) loser() int { func (matchup *Matchup) Loser() int {
if matchup.RedAllianceWins >= matchup.NumWinsToAdvance { if matchup.RedAllianceWins >= matchup.NumWinsToAdvance {
return matchup.BlueAllianceId return matchup.BlueAllianceId
} }
@@ -157,8 +157,8 @@ func (matchup *Matchup) loser() int {
} }
// Returns true if the matchup has been won, and false if it is still to be determined. // Returns true if the matchup has been won, and false if it is still to be determined.
func (matchup *Matchup) isComplete() bool { func (matchup *Matchup) IsComplete() bool {
return matchup.winner() > 0 return matchup.Winner() > 0
} }
// Returns true if the matchup represents the final matchup in the bracket. // Returns true if the matchup represents the final matchup in the bracket.
@@ -184,16 +184,16 @@ 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). // Populate the alliance IDs from the lower matchups (or with a zero value if they are not yet complete).
if matchup.redAllianceSourceMatchup != nil { if matchup.redAllianceSourceMatchup != nil {
if matchup.redAllianceSource.useWinner { if matchup.redAllianceSource.useWinner {
matchup.RedAllianceId = matchup.redAllianceSourceMatchup.winner() matchup.RedAllianceId = matchup.redAllianceSourceMatchup.Winner()
} else { } else {
matchup.RedAllianceId = matchup.redAllianceSourceMatchup.loser() matchup.RedAllianceId = matchup.redAllianceSourceMatchup.Loser()
} }
} }
if matchup.blueAllianceSourceMatchup != nil { if matchup.blueAllianceSourceMatchup != nil {
if matchup.blueAllianceSource.useWinner { if matchup.blueAllianceSource.useWinner {
matchup.BlueAllianceId = matchup.blueAllianceSourceMatchup.winner() matchup.BlueAllianceId = matchup.blueAllianceSourceMatchup.Winner()
} else { } else {
matchup.BlueAllianceId = matchup.blueAllianceSourceMatchup.loser() matchup.BlueAllianceId = matchup.blueAllianceSourceMatchup.Loser()
} }
} }

View File

@@ -56,7 +56,8 @@
<li><a target="_blank" href="/reports/pdf/schedule/qualification">Qualification Schedule</a></li> <li><a target="_blank" href="/reports/pdf/schedule/qualification">Qualification Schedule</a></li>
<li><a target="_blank" href="/reports/pdf/schedule/elimination">Playoff Schedule</a></li> <li><a target="_blank" href="/reports/pdf/schedule/elimination">Playoff Schedule</a></li>
<li><a target="_blank" href="/reports/pdf/rankings">Standings</a></li> <li><a target="_blank" href="/reports/pdf/rankings">Standings</a></li>
<li><a target="_blank" href="/reports/pdf/bracket">Bracket</a></li> <li><a target="_blank" href="/reports/pdf/alliances">Playoff Alliances</a></li>
<li><a target="_blank" href="/reports/pdf/bracket">Playoff Bracket</a></li>
<li><a target="_blank" href="/reports/pdf/backups">Backup Teams</a></li> <li><a target="_blank" href="/reports/pdf/backups">Backup Teams</a></li>
<li><a target="_blank" href="/reports/pdf/coupons">Playoff Alliance Coupons</a></li> <li><a target="_blank" href="/reports/pdf/coupons">Playoff Alliance Coupons</a></li>
<li><a target="_blank" href="/reports/pdf/teams?showHasConnected=true">Team Connection Status</a></li> <li><a target="_blank" href="/reports/pdf/teams?showHasConnected=true">Team Connection Status</a></li>

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,9 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"github.com/Team254/cheesy-arena-lite/bracket"
"github.com/Team254/cheesy-arena-lite/game" "github.com/Team254/cheesy-arena-lite/game"
"github.com/Team254/cheesy-arena-lite/model"
"github.com/Team254/cheesy-arena-lite/tournament" "github.com/Team254/cheesy-arena-lite/tournament"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jung-kurt/gofpdf" "github.com/jung-kurt/gofpdf"
@@ -101,19 +103,19 @@ func (web *Web) rankingsPdfReportHandler(w http.ResponseWriter, r *http.Request)
func (web *Web) findBackupTeams(rankings game.Rankings) (game.Rankings, map[int]bool, error) { func (web *Web) findBackupTeams(rankings game.Rankings) (game.Rankings, map[int]bool, error) {
var pruned game.Rankings var pruned game.Rankings
picks, err := web.arena.Database.GetAllAlliances() alliances, err := web.arena.Database.GetAllAlliances()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if len(picks) == 0 { if len(alliances) == 0 {
return nil, nil, errors.New("backup teams report is unavailable until alliances have been selected") return nil, nil, errors.New("backup teams report is unavailable until alliances have been selected")
} }
pickedTeams := make(map[int]bool) pickedTeams := make(map[int]bool)
pickedBackups := make(map[int]bool) pickedBackups := make(map[int]bool)
for _, alliance := range picks { for _, alliance := range alliances {
for i, allianceTeamId := range alliance.TeamIds { for i, allianceTeamId := range alliance.TeamIds {
// Teams in third in an alliance are backups at events that use 3 team alliances. // Teams in third in an alliance are backups at events that use 3 team alliances.
if i == 3 { if i == 3 {
@@ -589,6 +591,92 @@ func (web *Web) wpaKeysCsvReportHandler(w http.ResponseWriter, r *http.Request)
} }
} }
// Generates a PDF-formatted report of the playoff alliances and the teams contained within.
func (web *Web) alliancesPdfReportHandler(w http.ResponseWriter, r *http.Request) {
alliances, err := web.arena.Database.GetAllAlliances()
if err != nil {
handleWebErr(w, err)
return
}
// Traverse the bracket to register the furthest level that the alliance has achieved.
allianceStatuses := make(map[int]string)
if web.arena.PlayoffBracket.IsComplete() {
allianceStatuses[web.arena.PlayoffBracket.Winner()] = "Winner\n "
allianceStatuses[web.arena.PlayoffBracket.Finalist()] = "Finalist\n "
}
web.arena.PlayoffBracket.ReverseRoundOrderTraversal(func(matchup *bracket.Matchup) {
if matchup.IsComplete() {
if _, ok := allianceStatuses[matchup.Loser()]; !ok {
allianceStatuses[matchup.Loser()] = fmt.Sprintf("Eliminated in\n%s", matchup.LongDisplayName())
}
} else {
if matchup.RedAllianceId > 0 {
allianceStatuses[matchup.RedAllianceId] = fmt.Sprintf("Playing in\n%s", matchup.LongDisplayName())
}
if matchup.BlueAllianceId > 0 {
allianceStatuses[matchup.BlueAllianceId] = fmt.Sprintf("Playing in\n%s", matchup.LongDisplayName())
}
}
})
teams, err := web.arena.Database.GetAllTeams()
if err != nil {
handleWebErr(w, err)
return
}
teamsMap := make(map[int]model.Team, len(teams))
for _, team := range teams {
teamsMap[team.Id] = team
}
// The widths of the table columns in mm, stored here so that they can be referenced for each row.
colWidths := map[string]float64{"Alliance": 23, "Id": 12, "Name": 80, "Location": 80}
rowHeight := 6.5
pdf := gofpdf.New("P", "mm", "Letter", "font")
pdf.AddPage()
pdf.SetFont("Arial", "B", 10)
pdf.SetFillColor(220, 220, 220)
// Render table header row.
pdf.CellFormat(195, rowHeight, "Playoff Alliances - "+web.arena.EventSettings.Name, "", 1, "C", false, 0, "")
pdf.CellFormat(colWidths["Alliance"], rowHeight, "Alliance", "1", 0, "C", true, 0, "")
pdf.CellFormat(colWidths["Id"], rowHeight, "Team", "1", 0, "C", true, 0, "")
pdf.CellFormat(colWidths["Name"], rowHeight, "Name", "1", 0, "C", true, 0, "")
pdf.CellFormat(colWidths["Location"], rowHeight, "Location", "1", 1, "C", true, 0, "")
pdf.SetFont("Arial", "", 10)
xStart := pdf.GetX()
for _, alliance := range alliances {
yStart := pdf.GetY()
pdf.MultiCell(
colWidths["Alliance"],
rowHeight*float64(len(alliance.TeamIds))/5,
fmt.Sprintf(" \n%d\n%s\n ", alliance.Id, allianceStatuses[alliance.Id]),
"1",
"C",
false,
)
pdf.SetY(yStart)
for _, teamId := range alliance.TeamIds {
pdf.SetX(xStart + colWidths["Alliance"])
team := teamsMap[teamId]
pdf.CellFormat(colWidths["Id"], rowHeight, strconv.Itoa(team.Id), "1", 0, "L", false, 0, "")
pdf.CellFormat(colWidths["Name"], rowHeight, team.Nickname, "1", 0, "L", false, 0, "")
location := fmt.Sprintf("%s, %s, %s", team.City, team.StateProv, team.Country)
pdf.CellFormat(colWidths["Location"], rowHeight, location, "1", 1, "L", false, 0, "")
}
}
// Write out the PDF file as the HTTP response.
w.Header().Set("Content-Type", "application/pdf")
err = pdf.Output(w)
if err != nil {
handleWebErr(w, err)
return
}
}
// Generates a PDF-formatted report of the playoff bracket, relying on the browser to convert SVG to PDF (since no // Generates a PDF-formatted report of the playoff bracket, relying on the browser to convert SVG to PDF (since no
// suitable Go library for doing so appears to exist). // suitable Go library for doing so appears to exist).
func (web *Web) bracketPdfReportHandler(w http.ResponseWriter, r *http.Request) { func (web *Web) bracketPdfReportHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -149,17 +149,18 @@ func (web *Web) newHandler() http.Handler {
router.HandleFunc("/match_review", web.matchReviewHandler).Methods("GET") router.HandleFunc("/match_review", web.matchReviewHandler).Methods("GET")
router.HandleFunc("/match_review/{matchId}/edit", web.matchReviewEditGetHandler).Methods("GET") router.HandleFunc("/match_review/{matchId}/edit", web.matchReviewEditGetHandler).Methods("GET")
router.HandleFunc("/match_review/{matchId}/edit", web.matchReviewEditPostHandler).Methods("POST") router.HandleFunc("/match_review/{matchId}/edit", web.matchReviewEditPostHandler).Methods("POST")
router.HandleFunc("/reports/csv/rankings", web.rankingsCsvReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/rankings", web.rankingsPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/bracket", web.bracketPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/csv/backups", web.backupTeamsCsvReportHandler).Methods("GET") router.HandleFunc("/reports/csv/backups", web.backupTeamsCsvReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/backups", web.backupsPdfReportHandler).Methods("GET") router.HandleFunc("/reports/csv/rankings", web.rankingsCsvReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/coupons", web.couponsPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/csv/schedule/{type}", web.scheduleCsvReportHandler).Methods("GET") router.HandleFunc("/reports/csv/schedule/{type}", web.scheduleCsvReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/schedule/{type}", web.schedulePdfReportHandler).Methods("GET")
router.HandleFunc("/reports/csv/teams", web.teamsCsvReportHandler).Methods("GET") router.HandleFunc("/reports/csv/teams", web.teamsCsvReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/teams", web.teamsPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/csv/wpa_keys", web.wpaKeysCsvReportHandler).Methods("GET") router.HandleFunc("/reports/csv/wpa_keys", web.wpaKeysCsvReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/alliances", web.alliancesPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/backups", web.backupsPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/bracket", web.bracketPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/coupons", web.couponsPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/rankings", web.rankingsPdfReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/schedule/{type}", web.schedulePdfReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/teams", web.teamsPdfReportHandler).Methods("GET")
router.HandleFunc("/setup/awards", web.awardsGetHandler).Methods("GET") router.HandleFunc("/setup/awards", web.awardsGetHandler).Methods("GET")
router.HandleFunc("/setup/awards", web.awardsPostHandler).Methods("POST") router.HandleFunc("/setup/awards", web.awardsPostHandler).Methods("POST")
router.HandleFunc("/setup/awards/publish", web.awardsPublishHandler).Methods("POST") router.HandleFunc("/setup/awards/publish", web.awardsPublishHandler).Methods("POST")