Skip to content

Commit

Permalink
Add a configurable, optional timer to the alliance selection audience…
Browse files Browse the repository at this point in the history
… overlay.
  • Loading branch information
patfair committed May 31, 2024
1 parent df30be6 commit e22d214
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 45 deletions.
50 changes: 26 additions & 24 deletions field/arena.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,32 @@ type Arena struct {
ScoringPanelRegistry
ArenaNotifiers
MatchState
lastMatchState MatchState
CurrentMatch *model.Match
MatchStartTime time.Time
LastMatchTimeSec float64
RedRealtimeScore *RealtimeScore
BlueRealtimeScore *RealtimeScore
lastDsPacketTime time.Time
lastPeriodicTaskTime time.Time
EventStatus EventStatus
FieldReset bool
AudienceDisplayMode string
SavedMatch *model.Match
SavedMatchResult *model.MatchResult
SavedRankings game.Rankings
AllianceStationDisplayMode string
AllianceSelectionAlliances []model.Alliance
PlayoffTournament *playoff.PlayoffTournament
LowerThird *model.LowerThird
ShowLowerThird bool
MuteMatchSounds bool
matchAborted bool
soundsPlayed map[*game.MatchSound]struct{}
breakDescription string
preloadedTeams *[6]*model.Team
lastMatchState MatchState
CurrentMatch *model.Match
MatchStartTime time.Time
LastMatchTimeSec float64
RedRealtimeScore *RealtimeScore
BlueRealtimeScore *RealtimeScore
lastDsPacketTime time.Time
lastPeriodicTaskTime time.Time
EventStatus EventStatus
FieldReset bool
AudienceDisplayMode string
SavedMatch *model.Match
SavedMatchResult *model.MatchResult
SavedRankings game.Rankings
AllianceStationDisplayMode string
AllianceSelectionAlliances []model.Alliance
AllianceSelectionShowTimer bool
AllianceSelectionTimeRemainingSec int
PlayoffTournament *playoff.PlayoffTournament
LowerThird *model.LowerThird
ShowLowerThird bool
MuteMatchSounds bool
matchAborted bool
soundsPlayed map[*game.MatchSound]struct{}
breakDescription string
preloadedTeams *[6]*model.Team
}

type AllianceStation struct {
Expand Down
10 changes: 9 additions & 1 deletion field/arena_notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,15 @@ func (arena *Arena) configureNotifiers() {
}

func (arena *Arena) generateAllianceSelectionMessage() any {
return &arena.AllianceSelectionAlliances
return &struct {
Alliances []model.Alliance
ShowTimer bool
TimeRemainingSec int
}{
arena.AllianceSelectionAlliances,
arena.AllianceSelectionShowTimer,
arena.AllianceSelectionTimeRemainingSec,
}
}

func (arena *Arena) generateAllianceStationDisplayModeMessage() any {
Expand Down
32 changes: 32 additions & 0 deletions static/js/alliance_selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2024 Team 254. All Rights Reserved.
// Author: [email protected] (Patrick Fairbank)
//
// Client-side logic for the alliance selection page.

var websocket;

// Sends a websocket message to show the timer.
const showTimer = function() {
websocket.send("showTimer");
};

// Sends a websocket message to hide the timer.
const hideTimer = function() {
websocket.send("hideTimer");
}

// Handles a websocket message to update the alliance selection status.
const handleAllianceSelection = function(data) {
$("#timer").text(getCountdownString(data.TimeRemainingSec));
};

$(function() {
// Activate playoff tournament datetime picker.
const startTime = moment(new Date()).hour(13).minute(0).second(0);
newDateTimePicker("startTimePicker", startTime.toDate());

// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/alliance_selection/websocket", {
allianceSelection: function(event) { handleAllianceSelection(event.data); },
});
});
16 changes: 9 additions & 7 deletions static/js/audience_display.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,7 @@ const handleMatchLoad = function(data) {
// Handles a websocket message to update the match time countdown.
const handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
let countdownString = String(countdownSec % 60);
if (countdownString.length === 1) {
countdownString = "0" + countdownString;
}
countdownString = Math.floor(countdownSec / 60) + ":" + countdownString;
$("#matchTime").text(countdownString);
$("#matchTime").text(getCountdownString(countdownSec));
});
};

Expand Down Expand Up @@ -325,14 +320,21 @@ const handlePlaySound = function(sound) {
};

// Handles a websocket message to update the alliance selection screen.
const handleAllianceSelection = function(alliances) {
const handleAllianceSelection = function(data) {
const alliances = data.Alliances;
if (alliances && alliances.length > 0) {
const numColumns = alliances[0].TeamIds.length + 1;
$.each(alliances, function(k, v) {
v.Index = k + 1;
});
$("#allianceSelection").html(allianceSelectionTemplate({alliances: alliances, numColumns: numColumns}));
}

if (data.ShowTimer) {
$("#allianceSelectionTimer").text(getCountdownString(data.TimeRemainingSec));
} else {
$("#allianceSelectionTimer").html(" ");
}
};

// Handles a websocket message to populate and/or show/hide a lower third.
Expand Down
19 changes: 14 additions & 5 deletions static/js/match_timing.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const matchTypePractice = 1;
const matchTypeQualification = 2;
const matchTypePlayoff = 3;

var matchStates = {
const matchStates = {
0: "PRE_MATCH",
1: "START_MATCH",
2: "WARMUP_PERIOD",
Expand All @@ -20,16 +20,16 @@ var matchStates = {
7: "TIMEOUT_ACTIVE",
8: "POST_TIMEOUT"
};
var matchTiming;
let matchTiming;

// Handles a websocket message containing the length of each period in the match.
var handleMatchTiming = function(data) {
const handleMatchTiming = function(data) {
matchTiming = data;
};

// Converts the raw match state and time into a human-readable state and per-period time. Calls the provided
// callback with the result.
var translateMatchTime = function(data, callback) {
const translateMatchTime = function(data, callback) {
var matchStateText;
switch (matchStates[data.MatchState]) {
case "PRE_MATCH":
Expand Down Expand Up @@ -60,7 +60,7 @@ var translateMatchTime = function(data, callback) {
};

// Returns the per-period countdown for the given match state and overall time into the match.
var getCountdown = function(matchState, matchTimeSec) {
const getCountdown = function(matchState, matchTimeSec) {
switch (matchStates[matchState]) {
case "PRE_MATCH":
case "START_MATCH":
Expand All @@ -77,3 +77,12 @@ var getCountdown = function(matchState, matchTimeSec) {
return 0;
}
};

// Converts the given countdown in seconds to a string with a colon separator and leading zero padding.
const getCountdownString = function(countdownSec) {
let countdownString = String(countdownSec % 60);
if (countdownString.length === 1) {
countdownString = "0" + countdownString;
}
return Math.floor(countdownSec / 60) + ":" + countdownString;
};
7 changes: 1 addition & 6 deletions static/js/wall_display.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,7 @@ const handleMatchLoad = function(data) {
// Handles a websocket message to update the match time countdown.
const handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
let countdownString = String(countdownSec % 60);
if (countdownString.length === 1) {
countdownString = "0" + countdownString;
}
countdownString = Math.floor(countdownSec / 60) + ":" + countdownString;
$("#matchTime").text(countdownString);
$("#matchTime").text(getCountdownString(countdownSec));
});
};

Expand Down
31 changes: 31 additions & 0 deletions templates/alliance_selection.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,35 @@
</tbody>
</table>
Hint: Press 'Enter' after entering each team number for easiest use.
<div class="card card-body bg-body-secondary mt-4">
<div class="row">
<div class="col-lg-8">
<legend>Selection Timer</legend>
</div>
<div class="col-lg-4 text-end">
<legend id="timer"></legend>
</div>
</div>
<p>
Timer starts automatically after each non-captain assignment and is hidden on the audience overlay until
the button below is pressed.
</p>
<div class="row">
<label class="col-lg-5 control-label">Time limit in seconds<br />(0 = disabled)</label>
<div class="col-lg-3">
<input type="text" class="form-control" name="timeLimitSec" value="{{.TimeLimitSec}}">
</div>
<div class="col-lg-4 text-end">
<button type="submit" class="btn btn-primary">Save/Reset</button>
</div>
</div>
<div class="mt-3 row justify-content-center">
<div class="col-lg-8 text-center">
<button type="button" class="btn btn-success" onclick="showTimer();">Show Timer</button>
<button type="button" class="btn btn-secondary" onclick="hideTimer();">Hide Timer</button>
</div>
</div>
</div>
</form>
</div>
<div class="col-lg-2">
Expand Down Expand Up @@ -149,6 +178,8 @@ <h4 class="modal-title">Confirm</h4>
</div>
{{end}}
{{define "script"}}
<script src="/static/js/match_timing.js"></script>
<script src="/static/js/alliance_selection.js"></script>
<script>
$(function() {
var startTime = moment(new Date()).hour(13).minute(0).second(0);
Expand Down
3 changes: 3 additions & 0 deletions templates/audience_display.html
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@
{{"{{/each}}"}}
</tr>
{{"{{/each}}"}}
<tr>
<td id="allianceSelectionTimer" colspan="{{"{{numColumns}}"}}"></td>
</tr>
</table>
</script>
<script id="sponsorImageTemplate" type="text/x-handlebars-template">
Expand Down
83 changes: 82 additions & 1 deletion web/alliance_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"fmt"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/tournament"
"github.com/Team254/cheesy-arena/websocket"
"io"
"log"
"net/http"
"strconv"
"time"
Expand All @@ -23,6 +26,12 @@ type RankedTeam struct {
// Global var to hold the team rankings during the alliance selection.
var cachedRankedTeams []*RankedTeam

// Global var to hold configurable time limit for selections. A value of zero disables the timer.
var allianceSelectionTimeLimitSec = 0

// Global var to hold a ticker used for the alliance selection timer.
var allianceSelectionTicker *time.Ticker

// Shows the alliance selection page.
func (web *Web) allianceSelectionGetHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsAdmin(w, r) {
Expand All @@ -43,6 +52,9 @@ func (web *Web) allianceSelectionPostHandler(w http.ResponseWriter, r *http.Requ
return
}

// Update time limit value.
allianceSelectionTimeLimitSec, _ = strconv.Atoi(r.PostFormValue("timeLimitSec"))

// Reset picked state for each team in preparation for reconstructing it.
newRankedTeams := make([]*RankedTeam, len(cachedRankedTeams))
for i, team := range cachedRankedTeams {
Expand Down Expand Up @@ -90,6 +102,25 @@ func (web *Web) allianceSelectionPostHandler(w http.ResponseWriter, r *http.Requ
}
cachedRankedTeams = newRankedTeams

if allianceSelectionTicker != nil {
allianceSelectionTicker.Stop()
web.arena.AllianceSelectionShowTimer = false
web.arena.AllianceSelectionTimeRemainingSec = 0
}
if _, nextCol := web.determineNextCell(); nextCol > 0 && allianceSelectionTimeLimitSec > 0 {
web.arena.AllianceSelectionTimeRemainingSec = allianceSelectionTimeLimitSec
allianceSelectionTicker = time.NewTicker(time.Second)
go func() {
for range allianceSelectionTicker.C {
web.arena.AllianceSelectionTimeRemainingSec--
web.arena.AllianceSelectionNotifier.Notify()
if web.arena.AllianceSelectionTimeRemainingSec == 0 {
allianceSelectionTicker.Stop()
}
}
}()
}

web.arena.AllianceSelectionNotifier.Notify()
http.Redirect(w, r, "/alliance_selection", 303)
}
Expand Down Expand Up @@ -254,6 +285,47 @@ func (web *Web) allianceSelectionFinalizeHandler(w http.ResponseWriter, r *http.
http.Redirect(w, r, "/match_play", 303)
}

// The websocket endpoint for the alliance selection client to send control commands and receive status updates.
func (web *Web) allianceSelectionWebsocketHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsAdmin(w, r) {
return
}

ws, err := websocket.NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
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.AllianceSelectionNotifier)

// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
messageType, _, err := ws.Read()
if err != nil {
if err == io.EOF {
// Client has closed the connection; nothing to do here.
return
}
log.Println(err)
return
}

switch messageType {
case "showTimer":
web.arena.AllianceSelectionShowTimer = true
web.arena.AllianceSelectionNotifier.Notify()
case "hideTimer":
web.arena.AllianceSelectionShowTimer = false
web.arena.AllianceSelectionNotifier.Notify()
default:
ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
}
}
}

func (web *Web) renderAllianceSelection(w http.ResponseWriter, r *http.Request, errorMessage string) {
if len(web.arena.AllianceSelectionAlliances) == 0 {
// The application may have been restarted since the alliance selection was conducted; try reloading the
Expand All @@ -279,7 +351,16 @@ func (web *Web) renderAllianceSelection(w http.ResponseWriter, r *http.Request,
NextRow int
NextCol int
ErrorMessage string
}{web.arena.EventSettings, web.arena.AllianceSelectionAlliances, cachedRankedTeams, nextRow, nextCol, errorMessage}
TimeLimitSec int
}{
web.arena.EventSettings,
web.arena.AllianceSelectionAlliances,
cachedRankedTeams,
nextRow,
nextCol,
errorMessage,
allianceSelectionTimeLimitSec,
}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
Expand Down
Loading

0 comments on commit e22d214

Please sign in to comment.