From e05c44442ae8261b5c1446324bc57c70103c5142 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 1 Jul 2023 11:53:56 -0700 Subject: [PATCH] Refactor match play page to be completely dynamic. --- field/arena.go | 20 +- field/arena_notifiers.go | 7 + field/arena_test.go | 38 ++-- static/css/cheesy-arena.css | 3 + static/js/audience_display.js | 1 - static/js/match_play.js | 119 ++++++++---- templates/match_play.html | 114 +++-------- templates/match_play_match_load.html | 41 ++++ web/match_play.go | 273 ++++++++++++--------------- web/match_play_test.go | 216 +++++++++++---------- web/queueing_display.go | 2 +- web/web.go | 4 +- 12 files changed, 427 insertions(+), 411 deletions(-) create mode 100644 templates/match_play_match_load.html diff --git a/field/arena.go b/field/arena.go index 1e4ecab3..44bd4f10 100644 --- a/field/arena.go +++ b/field/arena.go @@ -215,7 +215,7 @@ func (arena *Arena) UpdatePlayoffTournament() error { // Sets up the arena for the given match. func (arena *Arena) LoadMatch(match *model.Match) error { if arena.MatchState != PreMatch { - return fmt.Errorf("Cannot load match while there is a match still in progress or with results pending.") + return fmt.Errorf("cannot load match while there is a match still in progress or with results pending") } arena.CurrentMatch = match @@ -373,7 +373,7 @@ func (arena *Arena) StartMatch() error { // Kills the current match or timeout if it is underway. func (arena *Arena) AbortMatch() error { if arena.MatchState == PreMatch || arena.MatchState == PostMatch || arena.MatchState == PostTimeout { - return fmt.Errorf("Cannot abort match when it is not in progress.") + return fmt.Errorf("cannot abort match when it is not in progress") } if arena.MatchState == TimeoutActive { @@ -397,7 +397,7 @@ func (arena *Arena) AbortMatch() error { // Clears out the match and resets the arena state unless there is a match underway. func (arena *Arena) ResetMatch() error { if arena.MatchState != PostMatch && arena.MatchState != PreMatch { - return fmt.Errorf("Cannot reset match while it is in progress.") + return fmt.Errorf("cannot reset match while it is in progress") } arena.MatchState = PreMatch arena.matchAborted = false @@ -414,7 +414,7 @@ func (arena *Arena) ResetMatch() error { // Starts a timeout of the given duration. func (arena *Arena) StartTimeout(description string, durationSec int) error { if arena.MatchState != PreMatch { - return fmt.Errorf("Cannot start timeout while there is a match still in progress or with results pending.") + return fmt.Errorf("cannot start timeout while there is a match still in progress or with results pending") } game.MatchTiming.TimeoutDurationSec = durationSec @@ -728,7 +728,7 @@ func (arena *Arena) setupNetwork(teams [6]*model.Team) { // Returns nil if the match can be started, and an error otherwise. func (arena *Arena) checkCanStartMatch() error { if arena.MatchState != PreMatch { - return fmt.Errorf("Cannot start match while there is a match still in progress or with results pending.") + return fmt.Errorf("cannot start match while there is a match still in progress or with results pending") } err := arena.checkAllianceStationsReady("R1", "R2", "R3", "B1", "B2", "B3") @@ -738,14 +738,14 @@ func (arena *Arena) checkCanStartMatch() error { if arena.Plc.IsEnabled() { if !arena.Plc.IsHealthy() { - return fmt.Errorf("Cannot start match while PLC is not healthy.") + return fmt.Errorf("cannot start match while PLC is not healthy") } if arena.Plc.GetFieldEstop() { - return fmt.Errorf("Cannot start match while field emergency stop is active.") + return fmt.Errorf("cannot start match while field emergency stop is active") } for name, status := range arena.Plc.GetArmorBlockStatuses() { if !status { - return fmt.Errorf("Cannot start match while PLC ArmorBlock '%s' is not connected.", name) + return fmt.Errorf("cannot start match while PLC ArmorBlock %q is not connected", name) } } } @@ -757,11 +757,11 @@ func (arena *Arena) checkAllianceStationsReady(stations ...string) error { for _, station := range stations { allianceStation := arena.AllianceStations[station] if allianceStation.Estop { - return fmt.Errorf("Cannot start match while an emergency stop is active.") + return fmt.Errorf("cannot start match while an emergency stop is active") } if !allianceStation.Bypass { if allianceStation.DsConn == nil || !allianceStation.DsConn.RobotLinked { - return fmt.Errorf("Cannot start match until all robots are connected or bypassed.") + return fmt.Errorf("cannot start match until all robots are connected or bypassed") } } } diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index b38d7c7a..48df2207 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -137,6 +137,9 @@ func (arena *Arena) GenerateMatchLoadMessage() any { } } + matchResult, _ := arena.Database.GetMatchResultForMatch(arena.CurrentMatch.Id) + isReplay := matchResult != nil + var matchup *playoff.Matchup redOffFieldTeams := []*model.Team{} blueOffFieldTeams := []*model.Team{} @@ -156,6 +159,8 @@ func (arena *Arena) GenerateMatchLoadMessage() any { return &struct { Match *model.Match + AllowSubstitution bool + IsReplay bool Teams map[string]*model.Team Rankings map[string]*game.Ranking Matchup *playoff.Matchup @@ -164,6 +169,8 @@ func (arena *Arena) GenerateMatchLoadMessage() any { BreakDescription string }{ arena.CurrentMatch, + arena.CurrentMatch.ShouldAllowSubstitution(), + isReplay, teams, rankings, matchup, diff --git a/field/arena_test.go b/field/arena_test.go index d4a6493d..742416a8 100644 --- a/field/arena_test.go +++ b/field/arena_test.go @@ -61,7 +61,7 @@ func TestArenaCheckCanStartMatch(t *testing.T) { // Check robot state constraints. err := arena.checkCanStartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match until all robots are connected or bypassed") + assert.Contains(t, err.Error(), "cannot start match until all robots are connected or bypassed") } arena.AllianceStations["R1"].Bypass = true arena.AllianceStations["R2"].Bypass = true @@ -70,7 +70,7 @@ func TestArenaCheckCanStartMatch(t *testing.T) { arena.AllianceStations["B2"].Bypass = true err = arena.checkCanStartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match until all robots are connected or bypassed") + assert.Contains(t, err.Error(), "cannot start match until all robots are connected or bypassed") } arena.AllianceStations["B3"].Bypass = true assert.Nil(t, arena.checkCanStartMatch()) @@ -79,7 +79,7 @@ func TestArenaCheckCanStartMatch(t *testing.T) { arena.Plc.SetAddress("1.2.3.4") err = arena.checkCanStartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while PLC is not healthy") + assert.Contains(t, err.Error(), "cannot start match while PLC is not healthy") } arena.Plc.SetAddress("") assert.Nil(t, arena.checkCanStartMatch()) @@ -218,73 +218,73 @@ func TestArenaStateEnforcement(t *testing.T) { assert.Nil(t, err) err = arena.AbortMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot abort match when") + assert.Contains(t, err.Error(), "cannot abort match when") } err = arena.StartMatch() assert.Nil(t, err) err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = AutoPeriod err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = PausePeriod err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = TeleopPeriod err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = PostMatch err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.AbortMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot abort match when") + assert.Contains(t, err.Error(), "cannot abort match when") } err = arena.ResetMatch() diff --git a/static/css/cheesy-arena.css b/static/css/cheesy-arena.css index a0e5f656..564e84d1 100644 --- a/static/css/cheesy-arena.css +++ b/static/css/cheesy-arena.css @@ -130,3 +130,6 @@ input[data-changed="true"], select[data-changed="true"] { top: 0; background-color: #fff; } +#testMatchSettings { + display: none; +} \ No newline at end of file diff --git a/static/js/audience_display.js b/static/js/audience_display.js index ddb8b283..86a68ffa 100644 --- a/static/js/audience_display.js +++ b/static/js/audience_display.js @@ -235,7 +235,6 @@ const handleScorePosted = function(data) { $("#" + blueSide + "FinalWins").text(data.BlueWins); const blueFinalDestination = $("#" + blueSide + "FinalDestination"); blueFinalDestination.html(data.BlueDestination.replace("Advances to ", "Advances to
")); - console.log(data); blueFinalDestination.toggle(data.BlueDestination !== ""); blueFinalDestination.attr("data-won", data.BlueWon); diff --git a/static/js/match_play.js b/static/js/match_play.js index ffd53396..90f45c89 100644 --- a/static/js/match_play.js +++ b/static/js/match_play.js @@ -4,72 +4,82 @@ // Client-side logic for the match play page. var websocket; -var currentMatchId; -var scoreIsReady; -var lowBatteryThreshold = 8; +let scoreIsReady; +let isReplay; +const lowBatteryThreshold = 8; + +// Sends a websocket message to load the specified match. +const loadMatch = function(matchId) { + websocket.send("loadMatch", { matchId: matchId }); +} + +// Sends a websocket message to load the results for the specified match into the display buffer. +const showResult = function(matchId) { + websocket.send("showResult", { matchId: matchId }); +} // Sends a websocket message to load a team into an alliance station. -var substituteTeam = function(team, position) { +const substituteTeam = function(team, position) { websocket.send("substituteTeam", { team: parseInt(team), position: position }) }; // Sends a websocket message to toggle the bypass status for an alliance station. -var toggleBypass = function(station) { +const toggleBypass = function(station) { websocket.send("toggleBypass", station); }; // Sends a websocket message to start the match. -var startMatch = function() { +const startMatch = function() { websocket.send("startMatch", { muteMatchSounds: $("#muteMatchSounds").prop("checked") }); }; // Sends a websocket message to abort the match. -var abortMatch = function() { +const abortMatch = function() { websocket.send("abortMatch"); }; // Sends a websocket message to signal to the volunteers that they may enter the field. -var signalVolunteers = function() { +const signalVolunteers = function() { websocket.send("signalVolunteers"); }; // Sends a websocket message to signal to the teams that they may enter the field. -var signalReset = function() { +const signalReset = function() { websocket.send("signalReset"); }; // Sends a websocket message to commit the match score and load the next match. -var commitResults = function() { +const commitResults = function() { websocket.send("commitResults"); }; // Sends a websocket message to discard the match score and load the next match. -var discardResults = function() { +const discardResults = function() { websocket.send("discardResults"); }; // Sends a websocket message to change what the audience display is showing. -var setAudienceDisplay = function() { +const setAudienceDisplay = function() { websocket.send("setAudienceDisplay", $("input[name=audienceDisplay]:checked").val()); }; // Sends a websocket message to change what the alliance station display is showing. -var setAllianceStationDisplay = function() { +const setAllianceStationDisplay = function() { websocket.send("setAllianceStationDisplay", $("input[name=allianceStationDisplay]:checked").val()); }; // Sends a websocket message to start the timeout. -var startTimeout = function() { - var duration = $("#timeoutDuration").val().split(":"); - var durationSec = parseFloat(duration[0]); +const startTimeout = function() { + const duration = $("#timeoutDuration").val().split(":"); + let durationSec = parseFloat(duration[0]); if (duration.length > 1) { durationSec = durationSec * 60 + parseFloat(duration[1]); } websocket.send("startTimeout", durationSec); }; -var confirmCommit = function(isReplay) { +const confirmCommit = function() { if (isReplay || !scoreIsReady) { // Show the appropriate message(s) in the confirmation dialog. $("#confirmCommitReplay").css("display", isReplay ? "block" : "none"); @@ -81,27 +91,20 @@ var confirmCommit = function(isReplay) { }; // Sends a websocket message to specify a custom name for the current test match. -var setTestMatchName = function() { +const setTestMatchName = function() { websocket.send("setTestMatchName", $("#testMatchName").val()); }; // Handles a websocket message to update the team connection status. -var handleArenaStatus = function(data) { - // If getting data for the wrong match (e.g. after a server restart), reload the page. - if (currentMatchId == null) { - currentMatchId = data.MatchId; - } else if (currentMatchId !== data.MatchId) { - location.reload(); - } - +const handleArenaStatus = function(data) { // Update the team status view. $.each(data.AllianceStations, function(station, stationStatus) { - var wifiStatus = data.TeamWifiStatuses[station]; + const wifiStatus = data.TeamWifiStatuses[station]; $("#status" + station + " .radio-status").text(wifiStatus.TeamId); if (stationStatus.DsConn) { // Format the driver station status box. - var dsConn = stationStatus.DsConn; + const dsConn = stationStatus.DsConn; $("#status" + station + " .ds-status").attr("data-status-ok", dsConn.DsLinked); if (dsConn.DsLinked) { $("#status" + station + " .ds-status").text(wifiStatus.MBits.toFixed(2) + "Mb"); @@ -109,11 +112,11 @@ var handleArenaStatus = function(data) { $("#status" + station + " .ds-status").text(""); } // Format the radio status box according to the connection status of the robot radio. - var radioOkay = stationStatus.Team && stationStatus.Team.Id === wifiStatus.TeamId && wifiStatus.RadioLinked; + const radioOkay = stationStatus.Team && stationStatus.Team.Id === wifiStatus.TeamId && wifiStatus.RadioLinked; $("#status" + station + " .radio-status").attr("data-status-ok", radioOkay); // Format the robot status box. - var robotOkay = dsConn.BatteryVoltage > lowBatteryThreshold && dsConn.RobotLinked; + const robotOkay = dsConn.BatteryVoltage > lowBatteryThreshold && dsConn.RobotLinked; $("#status" + station + " .robot-status").attr("data-status-ok", robotOkay); if (stationStatus.DsConn.SecondsSinceLastRobotLink > 1 && stationStatus.DsConn.SecondsSinceLastRobotLink < 1000) { $("#status" + station + " .robot-status").text(stationStatus.DsConn.SecondsSinceLastRobotLink.toFixed()); @@ -126,7 +129,7 @@ var handleArenaStatus = function(data) { $("#status" + station + " .robot-status").text(""); // Format the robot status box according to whether the AP is configured with the correct SSID. - var expectedTeamId = stationStatus.Team ? stationStatus.Team.Id : 0; + const expectedTeamId = stationStatus.Team ? stationStatus.Team.Id : 0; if (wifiStatus.TeamId === expectedTeamId) { if (wifiStatus.RadioLinked) { $("#status" + station + " .radio-status").attr("data-status-ok", true); @@ -226,8 +229,28 @@ var handleArenaStatus = function(data) { }); }; +// Handles a websocket message to update the teams for the current match. +const handleMatchLoad = function(data) { + isReplay = data.IsReplay; + + fetch("/match_play/match_load") + .then(response => response.text()) + .then(html => $("#matchListColumn").html(html)); + + $("#matchName").text(data.Match.LongName); + $("#testMatchName").val(data.Match.LongName); + $("#testMatchSettings").toggle(data.Match.Type === matchTypeTest); + $.each(data.Teams, function(station, team) { + const teamId = $(`#status${station} .team-number`); + teamId.val(team ? team.Id : ""); + teamId.prop("disabled", !data.AllowSubstitution); + }); + $("#playoffRedAllianceInfo").html(formatPlayoffAllianceInfo(data.Match.PlayoffRedAlliance, data.RedOffFieldTeams)); + $("#playoffBlueAllianceInfo").html(formatPlayoffAllianceInfo(data.Match.PlayoffBlueAlliance, data.BlueOffFieldTeams)); +} + // Handles a websocket message to update the match time countdown. -var handleMatchTime = function(data) { +const handleMatchTime = function(data) { translateMatchTime(data, function(matchState, matchStateText, countdownSec) { $("#matchState").text(matchStateText); $("#matchTime").text(countdownSec); @@ -235,19 +258,28 @@ var handleMatchTime = function(data) { }; // Handles a websocket message to update the match score. -var handleRealtimeScore = function(data) { +const handleRealtimeScore = function(data) { $("#redScore").text(data.Red.ScoreSummary.Score); $("#blueScore").text(data.Blue.ScoreSummary.Score); }; +// Handles a websocket message to populate the final score data. +const handleScorePosted = function(data) { + let matchName = data.Match.LongName; + if (!matchName) { + matchName = "None" + } + $("#savedMatchName").html(matchName); +} + // Handles a websocket message to update the audience display screen selector. -var handleAudienceDisplayMode = function(data) { +const handleAudienceDisplayMode = function(data) { $("input[name=audienceDisplay]:checked").prop("checked", false); $("input[name=audienceDisplay][value=" + data + "]").prop("checked", true); }; // Handles a websocket message to signal whether the referee and scorers have committed after the match. -var handleScoringStatus = function(data) { +const handleScoringStatus = function(data) { scoreIsReady = data.RefereeScoreReady && data.RedScoreReady && data.BlueScoreReady; $("#refereeScoreStatus").attr("data-ready", data.RefereeScoreReady); $("#redScoreStatus").text("Red Scoring " + data.NumRedScoringPanelsReady + "/" + data.NumRedScoringPanels); @@ -257,13 +289,13 @@ var handleScoringStatus = function(data) { }; // Handles a websocket message to update the alliance station display screen selector. -var handleAllianceStationDisplayMode = function(data) { +const handleAllianceStationDisplayMode = function(data) { $("input[name=allianceStationDisplay]:checked").prop("checked", false); $("input[name=allianceStationDisplay][value=" + data + "]").prop("checked", true); }; // Handles a websocket message to update the event status message. -var handleEventStatus = function(data) { +const handleEventStatus = function(data) { if (data.CycleTime === "") { $("#cycleTimeMessage").text("Last cycle time: Unknown"); } else { @@ -272,6 +304,17 @@ var handleEventStatus = function(data) { $("#earlyLateMessage").text(data.EarlyLateMessage); }; +const formatPlayoffAllianceInfo = function(allianceNumber, offFieldTeams) { + if (allianceNumber === 0) { + return ""; + } + let allianceInfo = `Alliance ${allianceNumber}`; + if (offFieldTeams.length > 0) { + allianceInfo += ` (not on field: ${offFieldTeams.map(team => team.Id).join(", ")})`; + } + return allianceInfo; +} + $(function() { // Activate tooltips above the status headers. $("[data-toggle=tooltip]").tooltip({"placement": "top"}); @@ -282,9 +325,11 @@ $(function() { arenaStatus: function(event) { handleArenaStatus(event.data); }, audienceDisplayMode: function(event) { handleAudienceDisplayMode(event.data); }, eventStatus: function(event) { handleEventStatus(event.data); }, + matchLoad: function(event) { handleMatchLoad(event.data); }, matchTime: function(event) { handleMatchTime(event.data); }, matchTiming: function(event) { handleMatchTiming(event.data); }, realtimeScore: function(event) { handleRealtimeScore(event.data); }, + scorePosted: function(event) { handleScorePosted(event.data); }, scoringStatus: function(event) { handleScoringStatus(event.data); }, }); }); diff --git a/templates/match_play.html b/templates/match_play.html index 863fa30e..f42fe9e7 100644 --- a/templates/match_play.html +++ b/templates/match_play.html @@ -7,58 +7,10 @@ {{define "title"}}Match Play{{end}} {{define "body"}}
-
- Load Test Match

- -
- {{range $type, $matches := .MatchesByType}} -
- - - - - - - - - - {{range $match := $matches}} - - - - - - {{end}} - -
MatchTimeAction
{{$match.ShortName}}{{$match.Time}} - - Load - - {{if ne $match.Status matchScheduled}} - - Show Result - - {{end}} -
-
- {{end}} -
-
+
-
- {{.Match.LongName}} -
+
 
 
 
 
@@ -73,17 +25,10 @@
Rbt
Byp
- {{template "matchPlayTeam" dict "team" .Match.Blue1 "color" "B" "position" 1 "data" .}} - {{template "matchPlayTeam" dict "team" .Match.Blue2 "color" "B" "position" 2 "data" .}} - {{template "matchPlayTeam" dict "team" .Match.Blue3 "color" "B" "position" 3 "data" .}} - {{if eq .Match.Type playoffMatch}} -
- Alliance {{.Match.PlayoffBlueAlliance}} - {{if .BlueOffFieldTeams}} - (not on field: {{range $i, $team := .BlueOffFieldTeams}}{{if $i}}, {{end}}{{$team}}{{end}}) - {{end}} -
- {{end}} + {{template "matchPlayTeam" dict "color" "B" "position" 1}} + {{template "matchPlayTeam" dict "color" "B" "position" 2}} + {{template "matchPlayTeam" dict "color" "B" "position" 3}} +
@@ -93,17 +38,10 @@
Rbt
Byp
- {{template "matchPlayTeam" dict "team" .Match.Red3 "color" "R" "position" 3 "data" .}} - {{template "matchPlayTeam" dict "team" .Match.Red2 "color" "R" "position" 2 "data" .}} - {{template "matchPlayTeam" dict "team" .Match.Red1 "color" "R" "position" 1 "data" .}} - {{if eq .Match.Type playoffMatch}} -
- Alliance {{.Match.PlayoffRedAlliance}} - {{if .RedOffFieldTeams}} - (not on field: {{range $i, $team := .RedOffFieldTeams}}{{if $i}}, {{end}}{{$team}}{{end}}) - {{end}} -
- {{end}} + {{template "matchPlayTeam" dict "color" "R" "position" 3}} + {{template "matchPlayTeam" dict "color" "R" "position" 2}} + {{template "matchPlayTeam" dict "color" "R" "position" 1}} +
@@ -126,7 +64,7 @@

Shown Match Result

- - {{if .SavedMatch.LongName}}{{.SavedMatch.LongName}}{{else}}None{{end}} - + None   - - Clear - + Clear

Match Sounds

@@ -270,11 +204,11 @@ - {{if eq .Match.Type testMatch}} +


Match Name

- - {{end}} + +
@@ -293,15 +227,16 @@
@@ -320,7 +255,9 @@ @@ -335,9 +272,8 @@
{{.position}}
- +
diff --git a/templates/match_play_match_load.html b/templates/match_play_match_load.html new file mode 100644 index 00000000..3f679adc --- /dev/null +++ b/templates/match_play_match_load.html @@ -0,0 +1,41 @@ +Load Test Match

+ +
+ {{range $type, $matches := .MatchesByType}} +
+ + + + + + + + + + {{range $match := $matches}} + + + + + + {{end}} + +
MatchTimeAction
{{$match.ShortName}}{{$match.Time}} + Load + {{if ne $match.Status matchScheduled}} + Show Result + {{end}} +
+
+ {{end}} +
diff --git a/web/match_play.go b/web/match_play.go index f02c6240..85604591 100644 --- a/web/match_play.go +++ b/web/match_play.go @@ -11,7 +11,6 @@ import ( "log" "net/http" "sort" - "strconv" "time" "github.com/Team254/cheesy-arena/field" @@ -19,7 +18,6 @@ import ( "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/tournament" "github.com/Team254/cheesy-arena/websocket" - "github.com/gorilla/mux" "github.com/mitchellh/mapstructure" ) @@ -39,72 +37,18 @@ func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) { return } - practiceMatches, err := web.buildMatchPlayList(model.Practice) - if err != nil { - handleWebErr(w, err) - return - } - qualificationMatches, err := web.buildMatchPlayList(model.Qualification) - if err != nil { - handleWebErr(w, err) - return - } - playoffMatches, err := web.buildMatchPlayList(model.Playoff) - if err != nil { - handleWebErr(w, err) - return - } - template, err := web.parseFiles("templates/match_play.html", "templates/base.html") if err != nil { handleWebErr(w, err) return } - matchesByType := map[model.MatchType]MatchPlayList{ - model.Practice: practiceMatches, - model.Qualification: qualificationMatches, - model.Playoff: playoffMatches, - } - currentMatchType := web.arena.CurrentMatch.Type - if currentMatchType == model.Test { - currentMatchType = model.Practice - } - redOffFieldTeams, blueOffFieldTeams, err := web.arena.Database.GetOffFieldTeamIds(web.arena.CurrentMatch) - if err != nil { - handleWebErr(w, err) - return - } - matchResult, err := web.arena.Database.GetMatchResultForMatch(web.arena.CurrentMatch.Id) - if err != nil { - handleWebErr(w, err) - return - } - isReplay := matchResult != nil data := struct { *model.EventSettings PlcIsEnabled bool - MatchesByType map[model.MatchType]MatchPlayList - CurrentMatchType model.MatchType - Match *model.Match - RedOffFieldTeams []int - BlueOffFieldTeams []int - AllowSubstitution bool - IsReplay bool - SavedMatchType string - SavedMatch *model.Match PlcArmorBlockStatuses map[string]bool }{ web.arena.EventSettings, web.arena.Plc.IsEnabled(), - matchesByType, - currentMatchType, - web.arena.CurrentMatch, - redOffFieldTeams, - blueOffFieldTeams, - web.arena.CurrentMatch.ShouldAllowSubstitution(), - isReplay, - web.arena.SavedMatch.Type.String(), - web.arena.SavedMatch, web.arena.Plc.GetArmorBlockStatuses(), } err = template.ExecuteTemplate(w, "base", data) @@ -114,92 +58,55 @@ func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) { } } -// Loads the given match onto the arena in preparation for playing it. -func (web *Web) matchPlayLoadHandler(w http.ResponseWriter, r *http.Request) { +// Renders a partial template containing the list of matches. +func (web *Web) matchPlayMatchLoadHandler(w http.ResponseWriter, r *http.Request) { if !web.userIsAdmin(w, r) { return } - vars := mux.Vars(r) - matchId, _ := strconv.Atoi(vars["matchId"]) - var match *model.Match - var err error - if matchId == 0 { - err = web.arena.LoadTestMatch() - } else { - match, err = web.arena.Database.GetMatchById(matchId) - if err != nil { - handleWebErr(w, err) - return - } - if match == nil { - handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId)) - return - } - err = web.arena.LoadMatch(match) - } + practiceMatches, err := web.buildMatchPlayList(model.Practice) if err != nil { handleWebErr(w, err) return } - - http.Redirect(w, r, "/match_play", 303) -} - -// Loads the results for the given match into the display buffer. -func (web *Web) matchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) { - if !web.userIsAdmin(w, r) { + qualificationMatches, err := web.buildMatchPlayList(model.Qualification) + if err != nil { + handleWebErr(w, err) return } - - vars := mux.Vars(r) - matchId, _ := strconv.Atoi(vars["matchId"]) - match, err := web.arena.Database.GetMatchById(matchId) + playoffMatches, err := web.buildMatchPlayList(model.Playoff) if err != nil { handleWebErr(w, err) return } - if match == nil { - handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId)) - return + + matchesByType := map[model.MatchType]MatchPlayList{ + model.Practice: practiceMatches, + model.Qualification: qualificationMatches, + model.Playoff: playoffMatches, } - matchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id) + currentMatchType := web.arena.CurrentMatch.Type + if currentMatchType == model.Test { + currentMatchType = model.Practice + } + + template, err := web.parseFiles("templates/match_play_match_load.html") if err != nil { handleWebErr(w, err) return } - if matchResult == nil { - handleWebErr(w, fmt.Errorf("No result found for match ID %d.", matchId)) - return - } - if match.ShouldUpdateRankings() { - web.arena.SavedRankings, err = web.arena.Database.GetAllRankings() - if err != nil { - handleWebErr(w, err) - return - } - } else { - web.arena.SavedRankings = game.Rankings{} + data := struct { + MatchesByType map[model.MatchType]MatchPlayList + CurrentMatchType model.MatchType + }{ + matchesByType, + currentMatchType, } - web.arena.SavedMatch = match - web.arena.SavedMatchResult = matchResult - web.arena.ScorePostedNotifier.Notify() - - 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) { + err = template.ExecuteTemplate(w, "match_play_match_load.html", data) + if err != nil { + handleWebErr(w, err) 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. @@ -216,9 +123,18 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request defer ws.Close() // Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine. - go ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.ArenaStatusNotifier, web.arena.MatchTimeNotifier, - web.arena.RealtimeScoreNotifier, web.arena.ScoringStatusNotifier, web.arena.AudienceDisplayModeNotifier, - web.arena.AllianceStationDisplayModeNotifier, web.arena.EventStatusNotifier) + go ws.HandleNotifiers( + web.arena.MatchTimingNotifier, + web.arena.AllianceStationDisplayModeNotifier, + web.arena.ArenaStatusNotifier, + web.arena.AudienceDisplayModeNotifier, + web.arena.EventStatusNotifier, + web.arena.MatchLoadNotifier, + web.arena.MatchTimeNotifier, + web.arena.RealtimeScoreNotifier, + web.arena.ScorePostedNotifier, + web.arena.ScoringStatusNotifier, + ) // Loop, waiting for commands and responding to them, until the client closes the connection. for { @@ -233,6 +149,84 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request } switch messageType { + case "loadMatch": + args := struct { + MatchId int + }{} + err = mapstructure.Decode(data, &args) + if err != nil { + ws.WriteError(err.Error()) + continue + } + err = web.arena.ResetMatch() + if err != nil { + ws.WriteError(err.Error()) + continue + } + if args.MatchId == 0 { + err = web.arena.LoadTestMatch() + } else { + match, err := web.arena.Database.GetMatchById(args.MatchId) + if err != nil { + ws.WriteError(err.Error()) + continue + } + if match == nil { + ws.WriteError(fmt.Sprintf("invalid match ID %d", args.MatchId)) + continue + } + err = web.arena.LoadMatch(match) + } + if err != nil { + ws.WriteError(err.Error()) + continue + } + case "showResult": + args := struct { + MatchId int + }{} + err = mapstructure.Decode(data, &args) + if err != nil { + ws.WriteError(err.Error()) + continue + } + if args.MatchId == 0 { + // Load an empty match to effectively clear the buffer. + web.arena.SavedMatch = &model.Match{} + web.arena.SavedMatchResult = model.NewMatchResult() + web.arena.ScorePostedNotifier.Notify() + continue + } + match, err := web.arena.Database.GetMatchById(args.MatchId) + if err != nil { + ws.WriteError(err.Error()) + continue + } + if match == nil { + ws.WriteError(fmt.Sprintf("invalid match ID %d", args.MatchId)) + continue + } + matchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id) + if err != nil { + ws.WriteError(err.Error()) + continue + } + if matchResult == nil { + ws.WriteError(fmt.Sprintf("No result found for match ID %d.", args.MatchId)) + continue + } + if match.ShouldUpdateRankings() { + web.arena.SavedRankings, err = web.arena.Database.GetAllRankings() + if err != nil { + ws.WriteError(err.Error()) + continue + } + } else { + web.arena.SavedRankings = game.Rankings{} + } + web.arena.SavedMatch = match + web.arena.SavedMatchResult = matchResult + web.arena.ScorePostedNotifier.Notify() case "substituteTeam": args := struct { Team int @@ -259,6 +253,9 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request continue } web.arena.AllianceStations[station].Bypass = !web.arena.AllianceStations[station].Bypass + if err = ws.WriteNotifier(web.arena.ArenaStatusNotifier); err != nil { + log.Println(err) + } case "startMatch": args := struct { MuteMatchSounds bool @@ -286,7 +283,7 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request continue } web.arena.FieldVolunteers = true - continue // Don't reload. + continue // Skip sending an arena status update. case "signalReset": if web.arena.MatchState != field.PostMatch && web.arena.MatchState != field.PreMatch { // Don't allow clearing the field until the match is over. @@ -295,8 +292,11 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request web.arena.FieldReset = true web.arena.AllianceStationDisplayMode = "fieldReset" web.arena.AllianceStationDisplayModeNotifier.Notify() - continue // Don't reload. case "commitResults": + if web.arena.MatchState != field.PostMatch { + ws.WriteError("cannot commit match while it is in progress") + continue + } err = web.commitCurrentMatchScore() if err != nil { ws.WriteError(err.Error()) @@ -312,12 +312,6 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request ws.WriteError(err.Error()) continue } - err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier) - if err != nil { - log.Println(err) - return - } - continue // Skip sending the status update, as the client is about to terminate and reload. case "discardResults": err = web.arena.ResetMatch() if err != nil { @@ -329,12 +323,6 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request ws.WriteError(err.Error()) continue } - err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier) - if err != nil { - log.Println(err) - return - } - continue // Skip sending the status update, as the client is about to terminate and reload. case "setAudienceDisplay": mode, ok := data.(string) if !ok { @@ -342,7 +330,6 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request continue } web.arena.SetAudienceDisplayMode(mode) - continue case "setAllianceStationDisplay": mode, ok := data.(string) if !ok { @@ -350,7 +337,6 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request continue } web.arena.SetAllianceStationDisplayMode(mode) - continue case "startTimeout": durationSec, ok := data.(float64) if !ok { @@ -374,17 +360,8 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request } web.arena.CurrentMatch.LongName = name web.arena.MatchLoadNotifier.Notify() - continue default: ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) - continue - } - - // Send out the status again after handling the command, as it most likely changed as a result. - err = ws.WriteNotifier(web.arena.ArenaStatusNotifier) - if err != nil { - log.Println(err) - return } } } diff --git a/web/match_play_test.go b/web/match_play_test.go index 4bbaf52a..b7fd7198 100644 --- a/web/match_play_test.go +++ b/web/match_play_test.go @@ -5,7 +5,6 @@ package web import ( "bytes" - "fmt" "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" @@ -22,6 +21,15 @@ import ( func TestMatchPlay(t *testing.T) { web := setupTestWeb(t) + // Check that some text near the bottom of the page is present. + recorder := web.getHttpResponse("/match_play") + assert.Equal(t, 200, recorder.Code) + assert.Contains(t, recorder.Body.String(), "Are you sure you want to discard the results for this match?") +} + +func TestMatchPlayMatchList(t *testing.T) { + web := setupTestWeb(t) + match1 := model.Match{Type: model.Practice, ShortName: "P1", Status: game.RedWonMatch} match2 := model.Match{Type: model.Practice, ShortName: "P2"} match3 := model.Match{Type: model.Qualification, ShortName: "Q1", Status: game.BlueWonMatch} @@ -34,7 +42,7 @@ func TestMatchPlay(t *testing.T) { web.arena.Database.CreateMatch(&match5) // Check that all matches are listed on the page. - recorder := web.getHttpResponse("/match_play") + recorder := web.getHttpResponse("/match_play/match_load") assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Body.String(), "P1") assert.Contains(t, recorder.Body.String(), "P2") @@ -43,86 +51,6 @@ func TestMatchPlay(t *testing.T) { assert.Contains(t, recorder.Body.String(), "SF1-2") } -func TestMatchPlayLoad(t *testing.T) { - web := setupTestWeb(t) - tournament.CreateTestAlliances(web.arena.Database, 8) - web.arena.CreatePlayoffTournament() - - web.arena.Database.CreateTeam(&model.Team{Id: 101}) - web.arena.Database.CreateTeam(&model.Team{Id: 102}) - web.arena.Database.CreateTeam(&model.Team{Id: 103}) - web.arena.Database.CreateTeam(&model.Team{Id: 104}) - web.arena.Database.CreateTeam(&model.Team{Id: 105}) - web.arena.Database.CreateTeam(&model.Team{Id: 106}) - match := model.Match{Type: model.Playoff, ShortName: "QF4-3", Status: game.RedWonMatch, Red1: 101, - Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} - web.arena.Database.CreateMatch(&match) - recorder := web.getHttpResponse("/match_play") - assert.Equal(t, 200, recorder.Code) - assert.NotContains(t, recorder.Body.String(), "101") - assert.NotContains(t, recorder.Body.String(), "102") - assert.NotContains(t, recorder.Body.String(), "103") - assert.NotContains(t, recorder.Body.String(), "104") - assert.NotContains(t, recorder.Body.String(), "105") - assert.NotContains(t, recorder.Body.String(), "106") - - // Load the match and check for the team numbers again. - recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/load", match.Id)) - assert.Equal(t, 303, recorder.Code) - recorder = web.getHttpResponse("/match_play") - assert.Equal(t, 200, recorder.Code) - assert.Contains(t, recorder.Body.String(), "101") - assert.Contains(t, recorder.Body.String(), "102") - assert.Contains(t, recorder.Body.String(), "103") - assert.Contains(t, recorder.Body.String(), "104") - assert.Contains(t, recorder.Body.String(), "105") - assert.Contains(t, recorder.Body.String(), "106") - - // Load a test match. - recorder = web.getHttpResponse("/match_play/0/load") - assert.Equal(t, 303, recorder.Code) - recorder = web.getHttpResponse("/match_play") - assert.Equal(t, 200, recorder.Code) - assert.NotContains(t, recorder.Body.String(), "101") - assert.NotContains(t, recorder.Body.String(), "102") - assert.NotContains(t, recorder.Body.String(), "103") - assert.NotContains(t, recorder.Body.String(), "104") - assert.NotContains(t, recorder.Body.String(), "105") - assert.NotContains(t, recorder.Body.String(), "106") -} - -func TestMatchPlayShowAndClearResult(t *testing.T) { - web := setupTestWeb(t) - - recorder := web.getHttpResponse("/match_play/1/show_result") - assert.Equal(t, 500, recorder.Code) - assert.Contains(t, recorder.Body.String(), "Invalid match") - match := model.Match{Type: model.Qualification, ShortName: "Q1", Status: game.TieMatch} - web.arena.Database.CreateMatch(&match) - recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id)) - assert.Equal(t, 500, recorder.Code) - assert.Contains(t, recorder.Body.String(), "No result found") - web.arena.Database.CreateMatchResult(model.BuildTestMatchResult(match.Id, 1)) - recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id)) - 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) { - web := setupTestWeb(t) - - // Load an invalid match. - recorder := web.getHttpResponse("/match_play/1114/load") - assert.Equal(t, 500, recorder.Code) - assert.Contains(t, recorder.Body.String(), "Invalid match") -} - func TestCommitMatch(t *testing.T) { web := setupTestWeb(t) @@ -322,13 +250,15 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { // Should get a few status updates right after connection. readWebsocketType(t, ws, "matchTiming") + readWebsocketType(t, ws, "allianceStationDisplayMode") readWebsocketType(t, ws, "arenaStatus") + readWebsocketType(t, ws, "audienceDisplayMode") + readWebsocketType(t, ws, "eventStatus") + readWebsocketType(t, ws, "matchLoad") readWebsocketType(t, ws, "matchTime") readWebsocketType(t, ws, "realtimeScore") + readWebsocketType(t, ws, "scorePosted") readWebsocketType(t, ws, "scoringStatus") - readWebsocketType(t, ws, "audienceDisplayMode") - readWebsocketType(t, ws, "allianceStationDisplayMode") - readWebsocketType(t, ws, "eventStatus") // Test that a server-side error is communicated to the client. ws.Write("nonexistenttype", nil) @@ -340,10 +270,10 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { ws.Write("substituteTeam", map[string]any{"team": 254, "position": "B5"}) assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station") ws.Write("substituteTeam", map[string]any{"team": 254, "position": "B1"}) - readWebsocketType(t, ws, "arenaStatus") + readWebsocketType(t, ws, "matchLoad") assert.Equal(t, 254, web.arena.CurrentMatch.Blue1) ws.Write("substituteTeam", map[string]any{"team": 0, "position": "B1"}) - readWebsocketType(t, ws, "arenaStatus") + readWebsocketType(t, ws, "matchLoad") assert.Equal(t, 0, web.arena.CurrentMatch.Blue1) ws.Write("toggleBypass", nil) assert.Contains(t, readWebsocketError(t, ws), "Failed to parse") @@ -358,9 +288,9 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { // Go through match flow. ws.Write("abortMatch", nil) - assert.Contains(t, readWebsocketError(t, ws), "Cannot abort match") + assert.Contains(t, readWebsocketError(t, ws), "cannot abort match") ws.Write("startMatch", nil) - assert.Contains(t, readWebsocketError(t, ws), "Cannot start match") + assert.Contains(t, readWebsocketError(t, ws), "cannot start match") web.arena.AllianceStations["R1"].Bypass = true web.arena.AllianceStations["R2"].Bypass = true web.arena.AllianceStations["R3"].Bypass = true @@ -368,27 +298,25 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { web.arena.AllianceStations["B2"].Bypass = true web.arena.AllianceStations["B3"].Bypass = true ws.Write("startMatch", nil) - readWebsocketType(t, ws, "arenaStatus") readWebsocketType(t, ws, "eventStatus") assert.Equal(t, field.StartMatch, web.arena.MatchState) ws.Write("commitResults", nil) - assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match") + assert.Contains(t, readWebsocketError(t, ws), "cannot commit match while it is in progress") ws.Write("discardResults", nil) - assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match") + assert.Contains(t, readWebsocketError(t, ws), "cannot reset match while it is in progress") ws.Write("abortMatch", nil) - readWebsocketType(t, ws, "arenaStatus") readWebsocketType(t, ws, "audienceDisplayMode") readWebsocketType(t, ws, "allianceStationDisplayMode") assert.Equal(t, field.PostMatch, web.arena.MatchState) web.arena.RedRealtimeScore.CurrentScore.AutoDockStatuses = [3]bool{false, true, true} web.arena.BlueRealtimeScore.CurrentScore.MobilityStatuses = [3]bool{true, false, true} ws.Write("commitResults", nil) - readWebsocketMultiple(t, ws, 4) // reload, realtimeScore, setAllianceStationDisplay, scoringStatus + readWebsocketMultiple(t, ws, 5) // scorePosted, matchLoad, realtimeScore, allianceStationDisplayMode, scoringStatus assert.Equal(t, [3]bool{false, true, true}, web.arena.SavedMatchResult.RedScore.AutoDockStatuses) assert.Equal(t, [3]bool{true, false, true}, web.arena.SavedMatchResult.BlueScore.MobilityStatuses) assert.Equal(t, field.PreMatch, web.arena.MatchState) ws.Write("discardResults", nil) - readWebsocketMultiple(t, ws, 4) // reload, realtimeScore, setAllianceStationDisplay, scoringStatus + readWebsocketMultiple(t, ws, 4) // matchLoad, realtimeScore, allianceStationDisplayMode, scoringStatus assert.Equal(t, field.PreMatch, web.arena.MatchState) // Test changing the displays. @@ -400,6 +328,95 @@ func TestMatchPlayWebsocketCommands(t *testing.T) { assert.Equal(t, "logo", web.arena.AllianceStationDisplayMode) } +func TestMatchPlayWebsocketLoadMatch(t *testing.T) { + web := setupTestWeb(t) + tournament.CreateTestAlliances(web.arena.Database, 8) + web.arena.CreatePlayoffTournament() + + server, wsUrl := web.startTestServer() + defer server.Close() + conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil) + assert.Nil(t, err) + defer conn.Close() + ws := websocket.NewTestWebsocket(conn) + + // Should get a few status updates right after connection. + readWebsocketMultiple(t, ws, 10) + + web.arena.Database.CreateTeam(&model.Team{Id: 101}) + web.arena.Database.CreateTeam(&model.Team{Id: 102}) + web.arena.Database.CreateTeam(&model.Team{Id: 103}) + web.arena.Database.CreateTeam(&model.Team{Id: 104}) + web.arena.Database.CreateTeam(&model.Team{Id: 105}) + web.arena.Database.CreateTeam(&model.Team{Id: 106}) + match := model.Match{Type: model.Playoff, ShortName: "QF4-3", Status: game.RedWonMatch, Red1: 101, + Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106} + web.arena.Database.CreateMatch(&match) + + matchIdMessage := struct{ MatchId int }{match.Id} + ws.Write("loadMatch", matchIdMessage) + readWebsocketType(t, ws, "matchLoad") + readWebsocketMultiple(t, ws, 3) + assert.Equal(t, 101, web.arena.CurrentMatch.Red1) + assert.Equal(t, 102, web.arena.CurrentMatch.Red2) + assert.Equal(t, 103, web.arena.CurrentMatch.Red3) + assert.Equal(t, 104, web.arena.CurrentMatch.Blue1) + assert.Equal(t, 105, web.arena.CurrentMatch.Blue2) + assert.Equal(t, 106, web.arena.CurrentMatch.Blue3) + + // Load a test match. + matchIdMessage.MatchId = 0 + ws.Write("loadMatch", matchIdMessage) + readWebsocketType(t, ws, "matchLoad") + readWebsocketMultiple(t, ws, 3) + assert.Equal(t, 0, web.arena.CurrentMatch.Red1) + assert.Equal(t, 0, web.arena.CurrentMatch.Red2) + assert.Equal(t, 0, web.arena.CurrentMatch.Red3) + assert.Equal(t, 0, web.arena.CurrentMatch.Blue1) + assert.Equal(t, 0, web.arena.CurrentMatch.Blue2) + assert.Equal(t, 0, web.arena.CurrentMatch.Blue3) + + // Load a nonexistent match. + matchIdMessage.MatchId = 254 + ws.Write("loadMatch", matchIdMessage) + assert.Contains(t, readWebsocketError(t, ws), "invalid match ID 254") +} + +func TestMatchPlayWebsocketShowAndClearResult(t *testing.T) { + web := setupTestWeb(t) + + server, wsUrl := web.startTestServer() + defer server.Close() + conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil) + assert.Nil(t, err) + defer conn.Close() + ws := websocket.NewTestWebsocket(conn) + + // Should get a few status updates right after connection. + readWebsocketMultiple(t, ws, 10) + + matchIdMessage := struct{ MatchId int }{1} + ws.Write("showResult", matchIdMessage) + assert.Contains(t, readWebsocketError(t, ws), "invalid match ID 1") + + match := model.Match{Type: model.Qualification, ShortName: "Q1", Status: game.TieMatch} + web.arena.Database.CreateMatch(&match) + ws.Write("showResult", matchIdMessage) + assert.Contains(t, readWebsocketError(t, ws), "No result found") + + web.arena.Database.CreateMatchResult(model.BuildTestMatchResult(match.Id, 1)) + ws.Write("showResult", matchIdMessage) + readWebsocketType(t, ws, "scorePosted") + assert.Equal(t, match.Id, web.arena.SavedMatch.Id) + assert.Equal(t, match.Id, web.arena.SavedMatchResult.MatchId) + + matchIdMessage.MatchId = 0 + ws.Write("showResult", matchIdMessage) + readWebsocketType(t, ws, "scorePosted") + assert.Equal(t, model.Match{}, *web.arena.SavedMatch) + assert.Equal(t, *model.NewMatchResult(), *web.arena.SavedMatchResult) +} + func TestMatchPlayWebsocketNotifications(t *testing.T) { web := setupTestWeb(t) @@ -413,14 +430,7 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) { ws := websocket.NewTestWebsocket(conn) // Should get a few status updates right after connection. - readWebsocketType(t, ws, "matchTiming") - readWebsocketType(t, ws, "arenaStatus") - readWebsocketType(t, ws, "matchTime") - readWebsocketType(t, ws, "realtimeScore") - readWebsocketType(t, ws, "scoringStatus") - readWebsocketType(t, ws, "audienceDisplayMode") - readWebsocketType(t, ws, "allianceStationDisplayMode") - readWebsocketType(t, ws, "eventStatus") + readWebsocketMultiple(t, ws, 10) web.arena.AllianceStations["R1"].Bypass = true web.arena.AllianceStations["R2"].Bypass = true diff --git a/web/queueing_display.go b/web/queueing_display.go index 5282f750..5279a152 100644 --- a/web/queueing_display.go +++ b/web/queueing_display.go @@ -42,7 +42,7 @@ func (web *Web) queueingDisplayHandler(w http.ResponseWriter, r *http.Request) { } } -// Renders the queueing display that shows upcoming matches and timing information. +// Renders a partial template containing the list of matches. func (web *Web) queueingDisplayMatchLoadHandler(w http.ResponseWriter, r *http.Request) { matches, err := web.arena.Database.GetMatchesByType(web.arena.CurrentMatch.Type, false) if err != nil { diff --git a/web/web.go b/web/web.go index 32b9d2ce..9d5d7088 100644 --- a/web/web.go +++ b/web/web.go @@ -167,9 +167,7 @@ func (web *Web) newHandler() http.Handler { router.HandleFunc("/login", web.loginHandler).Methods("GET") router.HandleFunc("/login", web.loginPostHandler).Methods("POST") 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/match_load", web.matchPlayMatchLoadHandler).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")