18 Commits

Author SHA1 Message Date
Patrick Fairbank
642116bd62 Shrink team numbers on alliance display to fit iPads. 2022-09-22 06:38:28 -07:00
Patrick Fairbank
3d566f4866 Set display name when pushing double-elimination matches to TBA. 2022-09-11 13:01:50 -07:00
Patrick Fairbank
d7c3b9df6c Swap red/blue in double-elimination Match 11 to match FIRST rules. 2022-09-08 15:59:36 -07:00
unknown
8e46617706 Update match 11 input arrows on bracket 2022-09-08 15:59:33 -07:00
Patrick Fairbank
cf58a2174e Implement translation of double-elimination match keys when pushing to TBA. 2022-09-03 09:10:52 -07:00
Patrick Fairbank
4bee40e66b Fix test match name on match play. 2022-08-24 18:04:06 -07:00
Patrick Fairbank
baaf3b694b Add query parameter to bracket SVG API to hide active match. 2022-08-24 17:55:50 -07:00
unknown
1acf221ac8 Add current match display 2022-08-24 17:55:45 -07:00
Patrick Fairbank
600b50d840 Change 'Saved Match Result' to 'Shown Match Result'. 2022-08-24 17:55:41 -07:00
Patrick Fairbank
1c2197e690 Identify saved match on Match Play screen and provide mechanism to clear it. 2022-08-23 18:30:32 -07:00
Patrick Fairbank
3ea4e26af3 Fix bug in match scheduling redirect. 2022-08-23 17:53:28 -07:00
unknown
a23d675ebb Fade out losers on completed matches 2022-08-22 18:35:17 -07:00
unknown
0b864d37dd Style complete matches 2022-08-22 18:35:12 -07:00
unknown
0eefc3ba40 Expose Matchup isComplete method in preparation for incorporating into bracket graphic 2022-08-22 18:35:07 -07:00
Patrick Fairbank
0aa69340f0 Add queueing mechanism for audience display transitions to prevent glitches. 2022-08-22 18:28:35 -07:00
Patrick Fairbank
e1fe4f660d Redirect to match play after finalizing alliance selection. 2022-08-22 18:28:12 -07:00
Patrick Fairbank
1896136abd Don't show 0-0 scores on match review page for matches that haven't been played yet. 2022-08-22 18:27:59 -07:00
Patrick Fairbank
4d3d270a0f Update alliance selection error message when picking a team who didn't play any qualification matches. 2022-08-22 18:27:43 -07:00
21 changed files with 235 additions and 99 deletions

View File

@@ -90,8 +90,8 @@ var doubleEliminationBracketMatchupTemplates = []matchupTemplate{
matchupKey: newMatchupKey(4, 1),
displayName: "11",
NumWinsToAdvance: 1,
redAllianceSource: newWinnerAllianceSource(3, 1),
blueAllianceSource: newWinnerAllianceSource(3, 2),
redAllianceSource: newWinnerAllianceSource(3, 2),
blueAllianceSource: newWinnerAllianceSource(3, 1),
},
{
matchupKey: newMatchupKey(4, 2),

View File

@@ -122,11 +122,11 @@ func TestDoubleEliminationProgression(t *testing.T) {
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[10], "11", 7, 8)
assertMatch(t, matches[11], "12", 4, 3)
}
scoreMatch(database, "11", game.BlueWonMatch)
scoreMatch(database, "11", game.RedWonMatch)
assert.Nil(t, bracket.Update(database, &dummyStartTime))
matches, err = database.GetMatchesByType("elimination")
assert.Nil(t, err)

View File

@@ -6,7 +6,6 @@
package model
import (
"fmt"
"github.com/Team254/cheesy-arena-lite/game"
"sort"
"strings"
@@ -40,8 +39,6 @@ type Match struct {
Status game.MatchStatus
}
var elimRoundNames = map[int]string{1: "F", 2: "SF", 4: "QF", 8: "EF"}
func (database *Database) CreateMatch(match *Match) error {
return database.matchTable.create(match)
}
@@ -141,16 +138,6 @@ func (match *Match) TypePrefix() string {
return ""
}
func (match *Match) TbaCode() string {
if match.Type == "qualification" {
return fmt.Sprintf("qm%s", match.DisplayName)
} else if match.Type == "elimination" {
return fmt.Sprintf("%s%dm%d", strings.ToLower(elimRoundNames[match.ElimRound]), match.ElimGroup,
match.ElimInstance)
}
return ""
}
// Returns true if the match is of a type that allows substitution of teams.
func (match *Match) ShouldAllowSubstitution() bool {
return match.Type != "qualification"

View File

@@ -112,18 +112,3 @@ func TestGetMatchesByType(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 1, len(matches))
}
func TestTbaCode(t *testing.T) {
match := Match{Type: "practice", DisplayName: "3"}
assert.Equal(t, "", match.TbaCode())
match = Match{Type: "qualification", DisplayName: "26"}
assert.Equal(t, "qm26", match.TbaCode())
match = Match{Type: "elimination", DisplayName: "EF2-1", ElimRound: 8, ElimGroup: 2, ElimInstance: 1}
assert.Equal(t, "ef2m1", match.TbaCode())
match = Match{Type: "elimination", DisplayName: "QF3-2", ElimRound: 4, ElimGroup: 3, ElimInstance: 2}
assert.Equal(t, "qf3m2", match.TbaCode())
match = Match{Type: "elimination", DisplayName: "SF1-3", ElimRound: 2, ElimGroup: 1, ElimInstance: 3}
assert.Equal(t, "sf1m3", match.TbaCode())
match = Match{Type: "elimination", DisplayName: "F2", ElimRound: 1, ElimGroup: 1, ElimInstance: 2}
assert.Equal(t, "f1m2", match.TbaCode())
}

View File

@@ -15,6 +15,7 @@ import (
"io/ioutil"
"net/http"
"strconv"
"strings"
)
const (
@@ -38,6 +39,7 @@ type TbaMatch struct {
Alliances map[string]*TbaAlliance `json:"alliances"`
TimeString string `json:"time_string"`
TimeUtc string `json:"time_utc"`
DisplayName string `json:"display_name"`
}
type TbaAlliance struct {
@@ -48,15 +50,17 @@ type TbaAlliance struct {
}
type TbaRanking struct {
TeamKey string `json:"team_key"`
Rank int `json:"rank"`
RP float32
Auto int
Endgame int
Teleop int
WinLossTie string
Dqs int `json:"dqs"`
Played int `json:"played"`
TeamKey string `json:"team_key"`
Rank int `json:"rank"`
RP float32
Auto int
Endgame int
Teleop int
Wins int `json:"wins"`
Losses int `json:"losses"`
Ties int `json:"ties"`
Dqs int `json:"dqs"`
Played int `json:"played"`
}
type TbaRankings struct {
@@ -101,6 +105,33 @@ type TbaPublishedAward struct {
Awardee string `json:"awardee"`
}
type elimMatchKey struct {
elimRound int
elimGroup int
}
type tbaElimMatchKey struct {
compLevel string
setNumber int
}
var doubleEliminationMatchKeyMapping = map[elimMatchKey]tbaElimMatchKey{
{1, 1}: {"ef", 1},
{1, 2}: {"ef", 2},
{1, 3}: {"ef", 3},
{1, 4}: {"ef", 4},
{2, 1}: {"ef", 5},
{2, 2}: {"ef", 6},
{2, 3}: {"qf", 1},
{2, 4}: {"qf", 2},
{3, 1}: {"qf", 3},
{3, 2}: {"qf", 4},
{4, 1}: {"sf", 1},
{4, 2}: {"sf", 2},
{5, 1}: {"f", 1},
{6, 1}: {"f", 2},
}
func NewTbaClient(eventCode, secretId, secret string) *TbaClient {
return &TbaClient{BaseUrl: tbaBaseUrl, eventCode: eventCode, secretId: secretId, secret: secret,
eventNamesCache: make(map[string]string)}
@@ -266,6 +297,10 @@ func (client *TbaClient) PublishMatches(database *model.Database) error {
if err != nil {
return err
}
eventSettings, err := database.GetEventSettings()
if err != nil {
return err
}
matches := append(qualMatches, elimMatches...)
tbaMatches := make([]TbaMatch, len(matches))
@@ -298,12 +333,16 @@ func (client *TbaClient) PublishMatches(database *model.Database) error {
alliances["blue"] = createTbaAlliance([3]int{match.Blue1, match.Blue2, match.Blue3},
[3]bool{match.Blue1IsSurrogate, match.Blue2IsSurrogate, match.Blue3IsSurrogate}, blueScore)
tbaMatches[i] = TbaMatch{"qm", 0, matchNumber, alliances, match.Time.Local().Format("3:04 PM"),
match.Time.UTC().Format("2006-01-02T15:04:05")}
tbaMatches[i] = TbaMatch{
CompLevel: "qm",
SetNumber: 0,
MatchNumber: matchNumber,
Alliances: alliances,
TimeString: match.Time.Local().Format("3:04 PM"),
TimeUtc: match.Time.UTC().Format("2006-01-02T15:04:05"),
}
if match.Type == "elimination" {
tbaMatches[i].CompLevel = map[int]string{1: "f", 2: "sf", 4: "qf", 8: "ef"}[match.ElimRound]
tbaMatches[i].SetNumber = match.ElimGroup
tbaMatches[i].MatchNumber = match.ElimInstance
setElimMatchKey(&tbaMatches[i], &match, eventSettings.ElimType)
}
}
jsonBody, err := json.Marshal(tbaMatches)
@@ -331,14 +370,21 @@ func (client *TbaClient) PublishRankings(database *model.Database) error {
}
// Build a JSON object of TBA-format rankings.
breakdowns := []string{"RP", "Auto", "Endgame", "Teleop", "WinLossTie"}
breakdowns := []string{"RP", "Auto", "Endgame", "Teleop"}
tbaRankings := make([]TbaRanking, len(rankings))
for i, ranking := range rankings {
tbaRankings[i] = TbaRanking{getTbaTeam(ranking.TeamId), ranking.Rank,
float32(ranking.RankingPoints) / float32(ranking.Played), ranking.AutoPoints, ranking.EndgamePoints,
ranking.TeleopPoints,
fmt.Sprintf("%d-%d-%d", ranking.Wins, ranking.Losses, ranking.Ties), 0,
ranking.Played}
tbaRankings[i] = TbaRanking{
TeamKey: getTbaTeam(ranking.TeamId),
Rank: ranking.Rank,
RP: float32(ranking.RankingPoints) / float32(ranking.Played),
Auto: ranking.AutoPoints,
Endgame: ranking.EndgamePoints,
Teleop: ranking.TeleopPoints,
Wins: ranking.Wins,
Losses: ranking.Losses,
Ties: ranking.Ties,
Played: ranking.Played,
}
}
jsonBody, err := json.Marshal(TbaRankings{breakdowns, tbaRankings})
if err != nil {
@@ -503,11 +549,20 @@ func (client *TbaClient) PublishAwards(database *model.Database) error {
return nil
}
// Returns the sum of all values in the slice representing different stages for a power cell goal.
func sumPowerCells(cells []int) int {
var total int
for _, cell := range cells {
total += cell
// Sets the match key attributes on TbaMatch based on the match and bracket type.
func setElimMatchKey(tbaMatch *TbaMatch, match *model.Match, elimType string) {
if elimType == "single" {
tbaMatch.CompLevel = map[int]string{1: "ef", 2: "qf", 3: "sf", 4: "f"}[match.ElimRound]
tbaMatch.SetNumber = match.ElimGroup
tbaMatch.MatchNumber = match.ElimInstance
} else if elimType == "double" {
if tbaKey, ok := doubleEliminationMatchKeyMapping[elimMatchKey{match.ElimRound, match.ElimGroup}]; ok {
tbaMatch.CompLevel = tbaKey.compLevel
tbaMatch.SetNumber = tbaKey.setNumber
}
tbaMatch.MatchNumber = match.ElimInstance
if !strings.HasPrefix(match.DisplayName, "F") {
tbaMatch.DisplayName = "Match " + match.DisplayName
}
}
return total
}

View File

@@ -43,7 +43,7 @@ func TestPublishMatches(t *testing.T) {
match1 := model.Match{Type: "qualification", DisplayName: "2", Time: time.Unix(600, 0), Red1: 7, Red2: 8, Red3: 9,
Blue1: 10, Blue2: 11, Blue3: 12, Status: game.RedWonMatch}
match2 := model.Match{Type: "elimination", DisplayName: "SF2-2", ElimRound: 2, ElimGroup: 2, ElimInstance: 2}
match2 := model.Match{Type: "elimination", DisplayName: "SF2-2", ElimRound: 3, ElimGroup: 2, ElimInstance: 2}
database.CreateMatch(&match1)
database.CreateMatch(&match2)
matchResult1 := model.BuildTestMatchResult(match1.Id, 1)

View File

@@ -120,8 +120,8 @@ body[data-position=right] #inMatch #blueScore {
left: 0;
right: 0;
margin: 0 auto;
font-size: 550px;
line-height: 550px;
font-size: 450px;
line-height: 450px;
text-align: center;
}
#preMatch .databar {

View File

@@ -80,6 +80,9 @@
.label-scoring[data-ready=true] {
background-color: #0c6;
}
.label-saved-match {
background-color: #999;
}
.nowrap {
white-space: nowrap;
}

View File

@@ -6,6 +6,8 @@
var websocket;
var transitionMap;
const transitionQueue = [];
let transitionInProgress = false;
var currentScreen = "blank";
var redSide;
var blueSide;
@@ -36,24 +38,47 @@ const bracketLogoScale = 0.75;
// Handles a websocket message to change which screen is displayed.
var handleAudienceDisplayMode = function(targetScreen) {
if (targetScreen === currentScreen) {
transitionQueue.push(targetScreen);
executeTransitionQueue();
};
// Sequentially executes all transitions in the queue. Returns without doing anything if another invocation is already
// in progress.
const executeTransitionQueue = function() {
if (transitionInProgress) {
// There is an existing invocation of this method which will execute all transitions in the queue.
return;
}
if (targetScreen === "sponsor") {
initializeSponsorDisplay();
}
transitions = transitionMap[currentScreen][targetScreen];
if (transitions == null) {
// There is no direct transition defined; need to go to the blank screen first.
transitions = function() {
transitionMap[currentScreen]["blank"](transitionMap["blank"][targetScreen]);
if (transitionQueue.length > 0) {
transitionInProgress = true;
const targetScreen = transitionQueue.shift();
const callback = function() {
// When the current transition is complete, call this method again to invoke the next one in the queue.
currentScreen = targetScreen;
transitionInProgress = false;
setTimeout(executeTransitionQueue, 100); // A small delay is needed to avoid visual glitches.
};
}
transitions();
currentScreen = targetScreen;
if (targetScreen === currentScreen) {
callback();
return;
}
if (targetScreen === "sponsor") {
initializeSponsorDisplay();
}
let transitions = transitionMap[currentScreen][targetScreen];
if (transitions !== undefined) {
transitions(callback);
} else {
// There is no direct transition defined; need to go to the blank screen first.
transitionMap[currentScreen]["blank"](function() {
transitionMap["blank"][targetScreen](callback);
});
}
}
};
// Handles a websocket message to update the teams for the current match.

View File

@@ -138,6 +138,9 @@
stroke-miterlimit:10;
}
.matchblock.complete #background {
fill:#e8e8e8;
}
.matchblock.active #background {
fill:#444444;
}
@@ -178,6 +181,21 @@
text-anchor:middle;
}
.matchblock.complete.red-win .teamnum.b,
.matchblock.complete.blue-win .teamnum.r {
fill:#888888;
}
.matchblock.complete.red-win.active .teamnum.b,
.matchblock.complete.blue-win.active .teamnum.r {
fill:#cccccc;
}
.matchblock.complete.blue-win .alliancenum.r {
fill:#ffc8c8;
}
.matchblock.complete.red-win .alliancenum.b {
fill:#A9D6FF;
}
<!-- Match Positioning -->
.bracket_double #match_1_1 {transform: translate(114px, 158px);}
@@ -364,9 +382,8 @@
<line class="loser" x1="1246.485" y1="746.485" x2="1229.515" y2="729.515"/>
<text transform="translate(926.2617 775.3125)">W</text>
<text transform="translate(926.2617 585.3125)">W</text>
<polyline points="1008,725 984,725 954.488,593.142"/>
<polyline class="coverup" points="1008,656 984,656 955.118,784.297"/>
<polyline points="1008,655 984,655 955.118,783 898,783"/>
<polyline points="955,593 955,655 1008,655"/>
<polyline points="955,783 955,725 1008,725"/>
</g>
<g id="connectors_4_2"{{if (index .Matchups "4_2").IsActive}} class="active"{{end}}>
<rect class="coverup" x="699.216" y="241.054" width="312.117" height="199.156"/>
@@ -461,7 +478,7 @@
{{end}}
{{define "matchup"}}
<g id="match_{{.Round}}_{{.Group}}" class="matchblock {{if .IsActive}}active{{end}}">
<g id="match_{{.Round}}_{{.Group}}" class="matchblock {{if .IsActive}}active{{end}} {{if .IsComplete}}complete {{.SeriesLeader}}-win{{end}}">
<rect class="structure" id="background" y="23" width="205" height="130.452"/>
<rect class="red" y="23" width="45.567" height="66.319"/>
<rect class="blue" y="89.133" width="45.567" height="64.319"/>
@@ -470,27 +487,27 @@
<text id="series_status" x="203.9999" y="170.5669" class="{{.SeriesLeader}}">{{.SeriesStatus}}</text>
<text id="match_title" x="0" y="17.3691">{{.DisplayName}}</text>
{{if .RedAlliance}}
<text x="22" y="70" class="alliancenum">{{.RedAlliance.Id}}</text>
<text x="22" y="70" class="alliancenum r">{{.RedAlliance.Id}}</text>
{{if ge (len .RedAlliance.TeamIds) 3}}
<text x="86.7247" y="54.0281" class="teamnum">{{index .RedAlliance.TeamIds 0}}</text>
<text x="162.8365" y="54.0281" class="teamnum">{{index .RedAlliance.TeamIds 1}}</text>
<text x="86.7247" y="81.2683" class="teamnum">{{index .RedAlliance.TeamIds 2}}</text>
<text x="86.7247" y="54.0281" class="teamnum r">{{index .RedAlliance.TeamIds 0}}</text>
<text x="162.8365" y="54.0281" class="teamnum r">{{index .RedAlliance.TeamIds 1}}</text>
<text x="86.7247" y="81.2683" class="teamnum r">{{index .RedAlliance.TeamIds 2}}</text>
{{end}}
{{if ge (len .RedAlliance.TeamIds) 4}}
<text x="162.8365" y="81.2683" class="teamnum">{{index .RedAlliance.TeamIds 3}}</text>
<text x="162.8365" y="81.2683" class="teamnum r">{{index .RedAlliance.TeamIds 3}}</text>
{{end}}
{{else}}
<text class="placeholder" x="101.1501" y="66.5769">{{.RedAllianceSource}}</text>
{{end}}
{{if .BlueAlliance}}
<text x="22" y="135" class="alliancenum">{{.BlueAlliance.Id}}</text>
<text x="22" y="135" class="alliancenum b">{{.BlueAlliance.Id}}</text>
{{if ge (len .BlueAlliance.TeamIds) 3}}
<text x="86.7247" y="119.1797" class="teamnum">{{index .BlueAlliance.TeamIds 0}}</text>
<text x="162.8365" y="119.1797" class="teamnum">{{index .BlueAlliance.TeamIds 1}}</text>
<text x="86.7247" y="146.4199" class="teamnum">{{index .BlueAlliance.TeamIds 2}}</text>
<text x="86.7247" y="119.1797" class="teamnum b">{{index .BlueAlliance.TeamIds 0}}</text>
<text x="162.8365" y="119.1797" class="teamnum b">{{index .BlueAlliance.TeamIds 1}}</text>
<text x="86.7247" y="146.4199" class="teamnum b">{{index .BlueAlliance.TeamIds 2}}</text>
{{end}}
{{if ge (len .BlueAlliance.TeamIds) 4}}
<text x="162.8365" y="146.4199" class="teamnum">{{index .BlueAlliance.TeamIds 3}}</text>
<text x="162.8365" y="146.4199" class="teamnum b">{{index .BlueAlliance.TeamIds 3}}</text>
{{end}}
{{else}}
<text class="placeholder" x="101.1501" y="130.4177">{{.BlueAllianceSource}}</text>

View File

@@ -56,7 +56,11 @@
</div>
<div class="col-lg-8">
<div class="row text-center">
<div id="matchState" class="col-lg-2 col-lg-offset-2 well well-sm text-center">&nbsp;</div>
<div class="col-lg-3 well well-sm text-center" style="text-transform: uppercase;">
{{if eq .Match.Type "elimination"}}playoff{{else}}{{.Match.Type}}{{end}}
{{if ne .Match.Type "test" }}{{.Match.DisplayName}}{{end}}
</div>
<div id="matchState" class="col-lg-3 well well-sm text-center">&nbsp;</div>
<div id="matchTime" class="col-lg-2 well well-sm text-center">&nbsp;</div>
<div id="redScore" class="col-lg-2 well well-sm well-red text-center">&nbsp;</div>
<div id="blueScore" class="col-lg-2 well well-sm well-blue text-center">&nbsp;</div>
@@ -290,6 +294,14 @@
</label>
</div>
</div>
<p>Shown Match Result</p>
<span class="label label-saved-match">
{{if .SavedMatch.DisplayName}}{{.SavedMatchType}} {{.SavedMatch.DisplayName}}{{else}}None{{end}}
</span>
&nbsp;
<a href="/match_play/clear_result">
<b class="btn btn-info btn-xs">Clear</b>
</a>
</div>
<div class="col-lg-3">
<p>Match Sounds</p>

View File

@@ -44,8 +44,8 @@
<td class="text-center blue-text">
{{index $match.BlueTeams 0}}, {{index $match.BlueTeams 1}}, {{index $match.BlueTeams 2}}
</td>
<td class="text-center red-text">{{$match.RedScore}}</td>
<td class="text-center blue-text">{{$match.BlueScore}}</td>
<td class="text-center red-text">{{if $match.IsComplete}}{{$match.RedScore}}{{end}}</td>
<td class="text-center blue-text">{{if $match.IsComplete}}{{$match.BlueScore}}{{end}}</td>
<td class="text-center nowrap">
<a href="/match_review/{{$match.Id}}/edit"><b class="btn btn-info btn-xs">Edit</b></a>
</td>

View File

@@ -76,7 +76,13 @@ func (web *Web) allianceSelectionPostHandler(w http.ResponseWriter, r *http.Requ
}
}
if !found {
web.renderAllianceSelection(w, r, fmt.Sprintf("Team %d is not present at this event.", teamId))
web.renderAllianceSelection(
w,
r,
fmt.Sprintf(
"Team %d has not played any matches at this event and is ineligible for selection.", teamId,
),
)
return
}
}
@@ -248,7 +254,13 @@ func (web *Web) allianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.
// Signal displays of the bracket to update themselves.
web.arena.ScorePostedNotifier.Notify()
http.Redirect(w, r, "/alliance_selection", 303)
// Load the first playoff match.
matches, err := web.arena.Database.GetMatchesByType("elimination")
if err == nil && len(matches) > 0 {
_ = web.arena.LoadMatch(&matches[0])
}
http.Redirect(w, r, "/match_play", 303)
}
// Publishes the alliances to the web.

View File

@@ -118,7 +118,7 @@ func TestAllianceSelectionErrors(t *testing.T) {
assert.Contains(t, recorder.Body.String(), "Invalid team number")
recorder = web.postHttpResponse("/alliance_selection", "selection0_0=100")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "not present at this event")
assert.Contains(t, recorder.Body.String(), "ineligible for selection")
recorder = web.postHttpResponse("/alliance_selection", "selection0_0=101&selection1_1=101")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "already part of an alliance")

View File

@@ -46,6 +46,7 @@ type allianceMatchup struct {
IsActive bool
SeriesLeader string
SeriesStatus string
IsComplete bool
}
// Generates a JSON dump of the matches and results.
@@ -227,19 +228,27 @@ func (web *Web) teamAvatarsApiHandler(w http.ResponseWriter, r *http.Request) {
}
func (web *Web) bracketSvgApiHandler(w http.ResponseWriter, r *http.Request) {
hideActive := false
if hideActiveValue, ok := r.URL.Query()["hideActive"]; ok {
hideActive = hideActiveValue[0] == "true"
}
w.Header().Set("Content-Type", "image/svg+xml")
if err := web.generateBracketSvg(w); err != nil {
if err := web.generateBracketSvg(w, hideActive); err != nil {
handleWebErr(w, err)
return
}
}
func (web *Web) generateBracketSvg(w io.Writer) error {
func (web *Web) generateBracketSvg(w io.Writer, hideActive bool) error {
alliances, err := web.arena.Database.GetAllAlliances()
if err != nil {
return err
}
activeMatch := web.arena.SavedMatch
var activeMatch *model.Match
if !hideActive {
activeMatch = web.arena.SavedMatch
}
matchups := make(map[string]*allianceMatchup)
if web.arena.PlayoffBracket != nil {
@@ -250,6 +259,7 @@ func (web *Web) generateBracketSvg(w io.Writer) error {
DisplayName: matchup.LongDisplayName(),
RedAllianceSource: matchup.RedAllianceSourceDisplayName(),
BlueAllianceSource: matchup.BlueAllianceSourceDisplayName(),
IsComplete: matchup.IsComplete(),
}
if matchup.RedAllianceId > 0 {
if len(alliances) > 0 {

View File

@@ -89,6 +89,8 @@ func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) {
BlueScore *game.Score
AllowSubstitution bool
IsReplay bool
SavedMatchType string
SavedMatch *model.Match
PlcArmorBlockStatuses map[string]bool
}{
web.arena.EventSettings,
@@ -102,6 +104,8 @@ func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) {
web.arena.BlueScore,
web.arena.CurrentMatch.ShouldAllowSubstitution(),
isReplay,
web.arena.SavedMatch.CapitalizedType(),
web.arena.SavedMatch,
web.arena.Plc.GetArmorBlockStatuses(),
}
err = template.ExecuteTemplate(w, "base", data)
@@ -185,6 +189,20 @@ func (web *Web) matchPlayShowResultHandler(w http.ResponseWriter, r *http.Reques
http.Redirect(w, r, "/match_play", 303)
}
// Clears the match results display buffer.
func (web *Web) matchPlayClearResultHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsAdmin(w, r) {
return
}
// Load an empty match to effectively clear the buffer.
web.arena.SavedMatch = &model.Match{}
web.arena.SavedMatchResult = model.NewMatchResult()
web.arena.ScorePostedNotifier.Notify()
http.Redirect(w, r, "/match_play", 303)
}
// The websocket endpoint for the match play client to send control commands and receive status updates.
func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsAdmin(w, r) {

View File

@@ -91,7 +91,7 @@ func TestMatchPlayLoad(t *testing.T) {
assert.NotContains(t, recorder.Body.String(), "106")
}
func TestMatchPlayShowResult(t *testing.T) {
func TestMatchPlayShowAndClearResult(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/match_play/1/show_result")
@@ -107,6 +107,11 @@ func TestMatchPlayShowResult(t *testing.T) {
assert.Equal(t, 303, recorder.Code)
assert.Equal(t, match.Id, web.arena.SavedMatch.Id)
assert.Equal(t, match.Id, web.arena.SavedMatchResult.MatchId)
recorder = web.getHttpResponse("/match_play/clear_result")
assert.Equal(t, 303, recorder.Code)
assert.Equal(t, model.Match{}, *web.arena.SavedMatch)
assert.Equal(t, *model.NewMatchResult(), *web.arena.SavedMatchResult)
}
func TestMatchPlayErrors(t *testing.T) {

View File

@@ -24,6 +24,7 @@ type MatchReviewListItem struct {
RedScore int
BlueScore int
ColorClass string
IsComplete bool
}
// Shows the match review interface.
@@ -196,12 +197,16 @@ func (web *Web) buildMatchReviewList(matchType string) ([]MatchReviewListItem, e
switch match.Status {
case game.RedWonMatch:
matchReviewList[i].ColorClass = "danger"
matchReviewList[i].IsComplete = true
case game.BlueWonMatch:
matchReviewList[i].ColorClass = "info"
matchReviewList[i].IsComplete = true
case game.TieMatch:
matchReviewList[i].ColorClass = "warning"
matchReviewList[i].IsComplete = true
default:
matchReviewList[i].ColorClass = ""
matchReviewList[i].IsComplete = false
}
}

View File

@@ -681,7 +681,7 @@ func (web *Web) alliancesPdfReportHandler(w http.ResponseWriter, r *http.Request
// suitable Go library for doing so appears to exist).
func (web *Web) bracketPdfReportHandler(w http.ResponseWriter, r *http.Request) {
buffer := new(bytes.Buffer)
err := web.generateBracketSvg(buffer)
err := web.generateBracketSvg(buffer, true)
if err != nil {
handleWebErr(w, err)
return

View File

@@ -27,6 +27,7 @@ func (web *Web) scheduleGetHandler(w http.ResponseWriter, r *http.Request) {
matchType := getMatchType(r)
if matchType == "" {
http.Redirect(w, r, "/setup/schedule?matchType=practice", 302)
return
}
if matchType != "practice" && matchType != "qualification" {

View File

@@ -145,6 +145,7 @@ func (web *Web) newHandler() http.Handler {
router.HandleFunc("/match_play", web.matchPlayHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/load", web.matchPlayLoadHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/show_result", web.matchPlayShowResultHandler).Methods("GET")
router.HandleFunc("/match_play/clear_result", web.matchPlayClearResultHandler).Methods("GET")
router.HandleFunc("/match_play/websocket", web.matchPlayWebsocketHandler).Methods("GET")
router.HandleFunc("/match_review", web.matchReviewHandler).Methods("GET")
router.HandleFunc("/match_review/{matchId}/edit", web.matchReviewEditGetHandler).Methods("GET")