Refactor event status to include cycle time calculation.

This commit is contained in:
Patrick Fairbank
2020-03-29 18:40:11 -07:00
parent 6cef7112ae
commit 7c7b90f8cc
14 changed files with 256 additions and 177 deletions

View File

@@ -13,7 +13,6 @@ import (
"github.com/Team254/cheesy-arena/partner"
"github.com/Team254/cheesy-arena/plc"
"log"
"math"
"time"
)
@@ -64,7 +63,7 @@ type Arena struct {
BlueRealtimeScore *RealtimeScore
lastDsPacketTime time.Time
lastPeriodicTaskTime time.Time
EventStatusMessage string
EventStatus EventStatus
FieldVolunteers bool
FieldReset bool
AudienceDisplayMode string
@@ -289,6 +288,7 @@ func (arena *Arena) StartMatch() error {
if arena.CurrentMatch.Type != "test" {
arena.Database.SaveMatch(arena.CurrentMatch)
}
arena.updateCycleTime(arena.CurrentMatch.StartedAt)
// Save the missed packet count to subtract it from the running count.
for _, allianceStation := range arena.AllianceStations {
@@ -867,73 +867,6 @@ func (arena *Arena) alliancePostMatchScoreReady(alliance string) bool {
// Performs any actions that need to run at the interval specified by periodicTaskPeriodSec.
func (arena *Arena) runPeriodicTasks() {
// Check how early or late the event is running and publish an update to the displays that show it.
newEventStatusMessage := arena.getEventStatusMessage()
if newEventStatusMessage != arena.EventStatusMessage {
arena.EventStatusMessage = newEventStatusMessage
arena.EventStatusNotifier.Notify()
}
// Clean up the list of displays.
arena.updateEarlyLateMessage()
arena.purgeDisconnectedDisplays()
}
// Updates the string that indicates how early or late the event is running.
func (arena *Arena) getEventStatusMessage() string {
currentMatch := arena.CurrentMatch
if currentMatch.Type != "practice" && currentMatch.Type != "qualification" {
// Only practice and qualification matches have a strict schedule.
return ""
}
if currentMatch.IsComplete() {
// This is a replay or otherwise unpredictable situation.
return ""
}
var minutesLate float64
if arena.MatchState > PreMatch && arena.MatchState < PostMatch {
// The match is in progress; simply calculate lateness from its start time.
minutesLate = currentMatch.StartedAt.Sub(currentMatch.Time).Minutes()
} else {
// We need to check the adjacent matches to accurately determine lateness.
matches, _ := arena.Database.GetMatchesByType(currentMatch.Type)
previousMatchIndex := -1
nextMatchIndex := len(matches)
for i, match := range matches {
if match.Id == currentMatch.Id {
previousMatchIndex = i - 1
nextMatchIndex = i + 1
break
}
}
if arena.MatchState == PreMatch {
currentMinutesLate := time.Now().Sub(currentMatch.Time).Minutes()
if previousMatchIndex >= 0 &&
currentMatch.Time.Sub(matches[previousMatchIndex].Time).Minutes() <= MaxMatchGapMin {
previousMatch := matches[previousMatchIndex]
previousMinutesLate := previousMatch.StartedAt.Sub(previousMatch.Time).Minutes()
minutesLate = math.Max(previousMinutesLate, currentMinutesLate)
} else {
minutesLate = math.Max(currentMinutesLate, 0)
}
} else if arena.MatchState == PostMatch {
currentMinutesLate := currentMatch.StartedAt.Sub(currentMatch.Time).Minutes()
if nextMatchIndex < len(matches) {
nextMatch := matches[nextMatchIndex]
nextMinutesLate := time.Now().Sub(nextMatch.Time).Minutes()
minutesLate = math.Max(currentMinutesLate, nextMinutesLate)
} else {
minutesLate = currentMinutesLate
}
}
}
if minutesLate > earlyLateThresholdMin {
return fmt.Sprintf("Event is running %d minutes late", int(minutesLate))
} else if minutesLate < -earlyLateThresholdMin {
return fmt.Sprintf("Event is running %d minutes early", int(-minutesLate))
}
return "Event is running on schedule"
}

View File

@@ -113,7 +113,7 @@ func (arena *Arena) generateDisplayConfigurationMessage() interface{} {
}
func (arena *Arena) generateEventStatusMessage() interface{} {
return arena.EventStatusMessage
return arena.EventStatus
}
func (arena *Arena) generateLowerThirdMessage() interface{} {

View File

@@ -644,100 +644,3 @@ func TestSaveTeamHasConnected(t *testing.T) {
assert.Equal(t, "San Jose", teams[5].City)
}
}
func TestEventStatusMessage(t *testing.T) {
arena := setupTestArena(t)
arena.LoadTestMatch()
assert.Equal(t, "", arena.getEventStatusMessage())
arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "1"})
arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "2"})
matches, _ := arena.Database.GetMatchesByType("qualification")
assert.Equal(t, 2, len(matches))
setMatch(arena.Database, &matches[0], time.Now().Add(300*time.Second), time.Time{}, false)
arena.CurrentMatch = &matches[0]
arena.MatchState = PreMatch
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-120*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(181*time.Second), time.Now(), false)
arena.MatchState = AutoPeriod
assert.Equal(t, "Event is running 3 minutes early", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), false)
setMatch(arena.Database, &matches[1], time.Now().Add(481*time.Second), time.Time{}, false)
arena.MatchState = PostMatch
assert.Equal(t, "Event is running 5 minutes early", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(181*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes early", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), true)
assert.Equal(t, "", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(900*time.Second), time.Time{}, false)
arena.CurrentMatch = &matches[1]
arena.MatchState = PreMatch
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(899*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 5 minutes early", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-120*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Now().Add(-541*time.Second), false)
arena.MatchState = TeleopPeriod
assert.Equal(t, "Event is running 6 minutes early", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), false)
arena.MatchState = PostMatch
assert.Equal(t, "Event is running 8 minutes late", arena.getEventStatusMessage())
setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), true)
assert.Equal(t, "", arena.getEventStatusMessage())
// Check other match types.
arena.MatchState = PreMatch
arena.CurrentMatch = &model.Match{Type: "practice", Time: time.Now().Add(-181 * time.Second)}
assert.Equal(t, "Event is running 3 minutes late", arena.getEventStatusMessage())
arena.CurrentMatch = &model.Match{Type: "elimination", Time: time.Now().Add(-181 * time.Second)}
assert.Equal(t, "", arena.getEventStatusMessage())
}
func setMatch(database *model.Database, match *model.Match, matchTime time.Time, startedAt time.Time, isComplete bool) {
match.Time = matchTime
match.StartedAt = startedAt
if isComplete {
match.Status = model.TieMatch
} else {
match.Status = model.MatchNotPlayed
}
_ = database.SaveMatch(match)
}

107
field/event_status.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright 2020 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model and functions for reporting on event status.
package field
import (
"fmt"
"math"
"time"
)
type EventStatus struct {
CycleTime string
EarlyLateMessage string
lastMatchStartTime time.Time
}
// Calculates the last cycle time and publishes an update to the displays that show it.
func (arena *Arena) updateCycleTime(matchStartTime time.Time) {
if arena.EventStatus.lastMatchStartTime.IsZero() {
// We don't know when the previous match was started.
arena.EventStatus.CycleTime = ""
} else {
cycleTimeSec := int(matchStartTime.Sub(arena.EventStatus.lastMatchStartTime).Seconds())
hours := cycleTimeSec / 3600
minutes := cycleTimeSec % 3600 / 60
seconds := cycleTimeSec % 60
if hours > 0 {
arena.EventStatus.CycleTime = fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
} else {
arena.EventStatus.CycleTime = fmt.Sprintf("%d:%02d", minutes, seconds)
}
}
arena.EventStatus.lastMatchStartTime = matchStartTime
arena.EventStatusNotifier.Notify()
}
// Checks how early or late the event is running and publishes an update to the displays that show it.
func (arena *Arena) updateEarlyLateMessage() {
newEarlyLateMessage := arena.getEarlyLateMessage()
if newEarlyLateMessage != arena.EventStatus.EarlyLateMessage {
arena.EventStatus.EarlyLateMessage = newEarlyLateMessage
arena.EventStatusNotifier.Notify()
}
}
// Updates the string that indicates how early or late the event is running.
func (arena *Arena) getEarlyLateMessage() string {
currentMatch := arena.CurrentMatch
if currentMatch.Type != "practice" && currentMatch.Type != "qualification" {
// Only practice and qualification matches have a strict schedule.
return ""
}
if currentMatch.IsComplete() {
// This is a replay or otherwise unpredictable situation.
return ""
}
var minutesLate float64
if arena.MatchState > PreMatch && arena.MatchState < PostMatch {
// The match is in progress; simply calculate lateness from its start time.
minutesLate = currentMatch.StartedAt.Sub(currentMatch.Time).Minutes()
} else {
// We need to check the adjacent matches to accurately determine lateness.
matches, _ := arena.Database.GetMatchesByType(currentMatch.Type)
previousMatchIndex := -1
nextMatchIndex := len(matches)
for i, match := range matches {
if match.Id == currentMatch.Id {
previousMatchIndex = i - 1
nextMatchIndex = i + 1
break
}
}
if arena.MatchState == PreMatch {
currentMinutesLate := time.Now().Sub(currentMatch.Time).Minutes()
if previousMatchIndex >= 0 &&
currentMatch.Time.Sub(matches[previousMatchIndex].Time).Minutes() <= MaxMatchGapMin {
previousMatch := matches[previousMatchIndex]
previousMinutesLate := previousMatch.StartedAt.Sub(previousMatch.Time).Minutes()
minutesLate = math.Max(previousMinutesLate, currentMinutesLate)
} else {
minutesLate = math.Max(currentMinutesLate, 0)
}
} else if arena.MatchState == PostMatch {
currentMinutesLate := currentMatch.StartedAt.Sub(currentMatch.Time).Minutes()
if nextMatchIndex < len(matches) {
nextMatch := matches[nextMatchIndex]
nextMinutesLate := time.Now().Sub(nextMatch.Time).Minutes()
minutesLate = math.Max(currentMinutesLate, nextMinutesLate)
} else {
minutesLate = currentMinutesLate
}
}
}
if minutesLate > earlyLateThresholdMin {
return fmt.Sprintf("Event is running %d minutes late", int(minutesLate))
} else if minutesLate < -earlyLateThresholdMin {
return fmt.Sprintf("Event is running %d minutes early", int(-minutesLate))
}
return "Event is running on schedule"
}

126
field/event_status_test.go Normal file
View File

@@ -0,0 +1,126 @@
// Copyright 2020 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package field
import (
"github.com/Team254/cheesy-arena/model"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestCycleTime(t *testing.T) {
arena := setupTestArena(t)
assert.Equal(t, "", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Time{})
assert.Equal(t, "", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(-125 * time.Second))
assert.Equal(t, "", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now())
assert.Equal(t, "2:05", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(3456 * time.Second))
assert.Equal(t, "57:36", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(5 * time.Hour))
assert.Equal(t, "4:02:24", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(123*time.Hour + 1256*time.Second))
assert.Equal(t, "118:20:56", arena.EventStatus.CycleTime)
}
func TestEarlyLateMessage(t *testing.T) {
arena := setupTestArena(t)
arena.LoadTestMatch()
assert.Equal(t, "", arena.getEarlyLateMessage())
arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "1"})
arena.Database.CreateMatch(&model.Match{Type: "qualification", DisplayName: "2"})
matches, _ := arena.Database.GetMatchesByType("qualification")
assert.Equal(t, 2, len(matches))
setMatch(arena.Database, &matches[0], time.Now().Add(300*time.Second), time.Time{}, false)
arena.CurrentMatch = &matches[0]
arena.MatchState = PreMatch
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-120*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(181*time.Second), time.Now(), false)
arena.MatchState = AutoPeriod
assert.Equal(t, "Event is running 3 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), false)
setMatch(arena.Database, &matches[1], time.Now().Add(481*time.Second), time.Time{}, false)
arena.MatchState = PostMatch
assert.Equal(t, "Event is running 5 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(181*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), true)
assert.Equal(t, "", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(900*time.Second), time.Time{}, false)
arena.CurrentMatch = &matches[1]
arena.MatchState = PreMatch
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(899*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 5 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-120*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Now().Add(-541*time.Second), false)
arena.MatchState = TeleopPeriod
assert.Equal(t, "Event is running 6 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), false)
arena.MatchState = PostMatch
assert.Equal(t, "Event is running 8 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), true)
assert.Equal(t, "", arena.getEarlyLateMessage())
// Check other match types.
arena.MatchState = PreMatch
arena.CurrentMatch = &model.Match{Type: "practice", Time: time.Now().Add(-181 * time.Second)}
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
arena.CurrentMatch = &model.Match{Type: "elimination", Time: time.Now().Add(-181 * time.Second)}
assert.Equal(t, "", arena.getEarlyLateMessage())
}
func setMatch(database *model.Database, match *model.Match, matchTime time.Time, startedAt time.Time, isComplete bool) {
match.Time = matchTime
match.StartedAt = startedAt
if isComplete {
match.Status = model.TieMatch
} else {
match.Status = model.MatchNotPlayed
}
_ = database.SaveMatch(match)
}

View File

@@ -75,7 +75,7 @@ body {
padding-left: 0;
padding-right: 0;
}
#eventStatusMessage {
#earlyLateMessage {
margin-top: 10px;
font-size: 25px;
font-family: "FuturaLTBold";

View File

@@ -54,7 +54,7 @@ h1 {
.avatars {
line-height: 51px;
}
#eventStatusMessage {
#earlyLateMessage {
font-size: 25px;
font-family: "FuturaLTBold";
color: #fff;

View File

@@ -227,7 +227,12 @@ var handleAllianceStationDisplayMode = function(data) {
// Handles a websocket message to update the event status message.
var handleEventStatus = function(data) {
$("#eventStatusMessage").text(data);
if (data.CycleTime === "") {
$("#cycleTimeMessage").text("Last cycle time: Unknown");
} else {
$("#cycleTimeMessage").text("Last cycle time: " + data.CycleTime);
}
$("#earlyLateMessage").text(data.EarlyLateMessage);
};
$(function() {

View File

@@ -81,7 +81,7 @@ var setHighestPlayedMatch = function(highestPlayedMatch) {
// Handles a websocket message to update the event status message.
var handleEventStatus = function(data) {
$("#eventStatusMessage").text(data);
$("#earlyLateMessage").text(data.EarlyLateMessage);
};
$(function() {

View File

@@ -30,7 +30,7 @@ var handleMatchTime = function(data) {
// Handles a websocket message to update the event status message.
var handleEventStatus = function(data) {
$("#eventStatusMessage").text(data);
$("#earlyLateMessage").text(data.EarlyLateMessage);
};
$(function() {

View File

@@ -217,7 +217,9 @@
</div>
</div>
<div class="row">
<div id="eventStatusMessage" class="col-lg-12"></div>
<div id="cycleTimeMessage" class="col-lg-5 col-lg-offset-1"></div>
<div id="earlyLateMessage" class="col-lg-5 text-right"></div>
<div class="col-lg-1"></div>
</div>
</div>
</div>

View File

@@ -46,7 +46,7 @@
<span id="highestPlayedMatch"></span>
</div>
</div>
<div id="eventStatusMessage"></div>
<div id="earlyLateMessage"></div>
</div>
<script id="standingsTemplate" type="text/x-handlebars-template">
<tbody>

View File

@@ -70,7 +70,7 @@
</div>
</div>
{{end}}
<div id="eventStatusMessage" class="col-lg-10 col-lg-offset-1"></div>
<div id="earlyLateMessage" class="col-lg-10 col-lg-offset-1"></div>
</body>
<script src="/static/js/lib/jquery.min.js"></script>
<script src="/static/js/lib/jquery.json-2.4.min.js"></script>

View File

@@ -299,6 +299,7 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
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")
@@ -359,13 +360,15 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
web.arena.AllianceStations["B3"].Bypass = true
assert.Nil(t, web.arena.StartMatch())
web.arena.Update()
messages := readWebsocketMultiple(t, ws, 4)
messages := readWebsocketMultiple(t, ws, 5)
_, ok := messages["matchTime"]
assert.True(t, ok)
_, ok = messages["audienceDisplayMode"]
assert.True(t, ok)
_, ok = messages["allianceStationDisplayMode"]
assert.True(t, ok)
_, ok = messages["eventStatus"]
assert.True(t, ok)
web.arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
web.arena.Update()
messages = readWebsocketMultiple(t, ws, 2)