Remove game-specific scoring

This commit is contained in:
Ken Schenke
2020-04-14 19:38:14 -05:00
parent 53caa27208
commit f075b7bb8d
59 changed files with 298 additions and 3004 deletions

152
field/arena.go Normal file → Executable file
View File

@@ -52,15 +52,14 @@ type Arena struct {
TbaClient *partner.TbaClient
AllianceStations map[string]*AllianceStation
Displays map[string]*Display
ScoringPanelRegistry
ArenaNotifiers
MatchState
lastMatchState MatchState
CurrentMatch *model.Match
MatchStartTime time.Time
LastMatchTimeSec float64
RedRealtimeScore *RealtimeScore
BlueRealtimeScore *RealtimeScore
RedScore *game.Score
BlueScore *game.Score
lastDsPacketTime time.Time
lastPeriodicTaskTime time.Time
EventStatus EventStatus
@@ -113,8 +112,6 @@ func NewArena(dbPath string) (*Arena, error) {
arena.Displays = make(map[string]*Display)
arena.ScoringPanelRegistry.initialize()
// Load empty match as current.
arena.MatchState = PreMatch
arena.LoadTestMatch()
@@ -164,10 +161,6 @@ func (arena *Arena) LoadSettings() error {
game.UpdateMatchSounds()
arena.MatchTimingNotifier.Notify()
game.StageCapacities[game.Stage1] = settings.Stage1Capacity
game.StageCapacities[game.Stage2] = settings.Stage2Capacity
game.StageCapacities[game.Stage3] = settings.Stage3Capacity
return nil
}
@@ -207,13 +200,12 @@ func (arena *Arena) LoadMatch(match *model.Match) error {
arena.AllianceStations["R3"].Team, arena.AllianceStations["B1"].Team, arena.AllianceStations["B2"].Team,
arena.AllianceStations["B3"].Team})
// Reset the arena state and realtime scores.
// Reset the arena state and game scores.
arena.soundsPlayed = make(map[*game.MatchSound]struct{})
arena.RedRealtimeScore = NewRealtimeScore()
arena.BlueRealtimeScore = NewRealtimeScore()
arena.RedScore = new(game.Score)
arena.BlueScore = new(game.Score)
arena.FieldVolunteers = false
arena.FieldReset = false
arena.ScoringPanelRegistry.resetScoreCommitted()
// Notify any listeners about the new match.
arena.MatchLoadNotifier.Notify()
@@ -522,14 +514,12 @@ func (arena *Arena) Run() {
// Calculates the red alliance score summary for the given realtime snapshot.
func (arena *Arena) RedScoreSummary() *game.ScoreSummary {
return arena.RedRealtimeScore.CurrentScore.Summarize(arena.BlueRealtimeScore.CurrentScore.Fouls,
arena.MatchState >= TeleopPeriod)
return arena.RedScore.Summarize()
}
// Calculates the blue alliance score summary for the given realtime snapshot.
func (arena *Arena) BlueScoreSummary() *game.ScoreSummary {
return arena.BlueRealtimeScore.CurrentScore.Summarize(arena.RedRealtimeScore.CurrentScore.Fouls,
arena.MatchState >= TeleopPeriod)
return arena.BlueScore.Summarize()
}
// Loads a team into an alliance station, cleaning up the previous team there if there is one.
@@ -704,23 +694,6 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
arena.lastDsPacketTime = time.Now()
}
// Sends a game data packet encoded with the given Control Panel target color to the given stations.
func (arena *Arena) sendGameDataPacket(color game.ControlPanelColor, stations ...string) {
gameData := game.GetGameDataForColor(color)
log.Printf("Sending game data packet '%s' to stations %v", gameData, stations)
for _, station := range stations {
if allianceStation, ok := arena.AllianceStations[station]; ok {
dsConn := allianceStation.DsConn
if dsConn != nil {
err := dsConn.sendGameDataPacket(gameData)
if err != nil {
log.Printf("Error sending game data packet to Team %d: %v", dsConn.TeamId, err)
}
}
}
}
}
// Returns the alliance station identifier for the given team, or the empty string if the team is not present
// in the current match.
func (arena *Arena) getAssignedAllianceStation(teamId int) string {
@@ -759,79 +732,10 @@ func (arena *Arena) handlePlcInput() {
// Don't do anything if we're outside the match, otherwise we may overwrite manual edits.
return
}
redScore := &arena.RedRealtimeScore.CurrentScore
oldRedScore := *redScore
blueScore := &arena.BlueRealtimeScore.CurrentScore
oldBlueScore := *blueScore
matchStartTime := arena.MatchStartTime
currentTime := time.Now()
teleopStarted := arena.MatchState >= TeleopPeriod
if arena.Plc.IsEnabled() {
// Handle power ports.
redPortCells, bluePortCells := arena.Plc.GetPowerPorts()
redPowerPort := &arena.RedRealtimeScore.powerPort
redPowerPort.UpdateState(redPortCells, redScore.CellCountingStage(teleopStarted), matchStartTime, currentTime)
redScore.AutoCellsBottom = redPowerPort.AutoCellsBottom
redScore.AutoCellsOuter = redPowerPort.AutoCellsOuter
redScore.AutoCellsInner = redPowerPort.AutoCellsInner
redScore.TeleopCellsBottom = redPowerPort.TeleopCellsBottom
redScore.TeleopCellsOuter = redPowerPort.TeleopCellsOuter
redScore.TeleopCellsInner = redPowerPort.TeleopCellsInner
bluePowerPort := &arena.BlueRealtimeScore.powerPort
bluePowerPort.UpdateState(bluePortCells, blueScore.CellCountingStage(teleopStarted), matchStartTime,
currentTime)
blueScore.AutoCellsBottom = bluePowerPort.AutoCellsBottom
blueScore.AutoCellsOuter = bluePowerPort.AutoCellsOuter
blueScore.AutoCellsInner = bluePowerPort.AutoCellsInner
blueScore.TeleopCellsBottom = bluePowerPort.TeleopCellsBottom
blueScore.TeleopCellsOuter = bluePowerPort.TeleopCellsOuter
blueScore.TeleopCellsInner = bluePowerPort.TeleopCellsInner
// Handle control panel.
redColor, redSegmentCount, blueColor, blueSegmentCount := arena.Plc.GetControlPanels()
redControlPanel := &arena.RedRealtimeScore.ControlPanel
redControlPanel.CurrentColor = redColor
redControlPanel.UpdateState(redSegmentCount, redScore.StageAtCapacity(game.Stage2, teleopStarted),
redScore.StageAtCapacity(game.Stage3, teleopStarted), currentTime)
redScore.ControlPanelStatus = redControlPanel.ControlPanelStatus
blueControlPanel := &arena.BlueRealtimeScore.ControlPanel
blueControlPanel.CurrentColor = blueColor
blueControlPanel.UpdateState(blueSegmentCount, blueScore.StageAtCapacity(game.Stage2, teleopStarted),
blueScore.StageAtCapacity(game.Stage3, teleopStarted), currentTime)
blueScore.ControlPanelStatus = blueControlPanel.ControlPanelStatus
// Handle shield generator rungs.
if game.ShouldAssessRung(matchStartTime, currentTime) {
redScore.RungIsLevel, blueScore.RungIsLevel = arena.Plc.GetRungs()
}
}
// Check if either alliance has reached Stage 3 capacity.
if redScore.StageAtCapacity(game.Stage3, arena.MatchState >= TeleopPeriod) &&
redScore.PositionControlTargetColor == game.ColorUnknown ||
blueScore.StageAtCapacity(game.Stage3, arena.MatchState >= TeleopPeriod) &&
blueScore.PositionControlTargetColor == game.ColorUnknown {
// Determine the position control target colors and send packets to inform the driver stations.
redScore.PositionControlTargetColor = arena.RedRealtimeScore.ControlPanel.GetPositionControlTargetColor()
blueScore.PositionControlTargetColor = arena.BlueRealtimeScore.ControlPanel.GetPositionControlTargetColor()
arena.sendGameDataPacket(redScore.PositionControlTargetColor, "R1", "R2", "R3")
arena.sendGameDataPacket(blueScore.PositionControlTargetColor, "B1", "B2", "B3")
}
if !oldRedScore.Equals(redScore) || !oldBlueScore.Equals(blueScore) {
arena.RealtimeScoreNotifier.Notify()
}
}
// Updates the PLC's coils based on its inputs and the current scoring state.
func (arena *Arena) handlePlcOutput() {
matchStartTime := arena.MatchStartTime
currentTime := time.Now()
redScore := &arena.RedRealtimeScore.CurrentScore
blueScore := &arena.BlueRealtimeScore.CurrentScore
switch arena.MatchState {
case PreMatch:
if arena.lastMatchState != PreMatch {
@@ -857,51 +761,18 @@ func (arena *Arena) handlePlcOutput() {
if arena.FieldReset {
arena.Plc.SetFieldResetLight(true)
}
scoreReady := arena.RedRealtimeScore.FoulsCommitted && arena.BlueRealtimeScore.FoulsCommitted &&
arena.alliancePostMatchScoreReady("red") && arena.alliancePostMatchScoreReady("blue")
arena.Plc.SetStackLights(false, false, !scoreReady, false)
arena.Plc.SetStackLights(false, false, false, false)
if arena.lastMatchState != PostMatch {
go func() {
time.Sleep(time.Second * game.PowerPortTeleopGracePeriodSec)
arena.Plc.SetPowerPortMotors(false)
time.Sleep(time.Second)
}()
}
arena.Plc.SetStageActivatedLights([3]bool{false, false, false}, [3]bool{false, false, false})
arena.Plc.SetControlPanelLights(false, false)
case AutoPeriod:
arena.Plc.SetPowerPortMotors(true)
fallthrough
case PausePeriod:
fallthrough
case TeleopPeriod:
arena.Plc.SetStageActivatedLights(arena.RedScoreSummary().StagesActivated,
arena.BlueScoreSummary().StagesActivated)
controlPanelLightState := func(state game.ControlPanelLightState) bool {
switch state {
case game.ControlPanelLightOn:
return true
case game.ControlPanelLightFlashing:
return arena.Plc.GetCycleState(2, 0, 2)
default:
return false
}
}
arena.Plc.SetControlPanelLights(
controlPanelLightState(arena.RedRealtimeScore.ControlPanel.ControlPanelLightState),
controlPanelLightState(arena.BlueRealtimeScore.ControlPanel.ControlPanelLightState))
// If the PLC reports a ball jam, blink the orange light and the power port color.
redJam, blueJam := arena.Plc.GetPowerPortJams()
blink := arena.Plc.GetCycleState(2, 0, 2)
arena.Plc.SetStackLights(redJam && blink, blueJam && blink, (redJam || blueJam) && !blink, true)
}
if game.ShouldAssessRung(matchStartTime, currentTime) {
arena.Plc.SetShieldGeneratorLights(redScore.RungIsLevel, blueScore.RungIsLevel)
} else {
arena.Plc.SetShieldGeneratorLights(false, false)
}
}
@@ -950,11 +821,6 @@ func (arena *Arena) playSound(name string) {
}
}
func (arena *Arena) alliancePostMatchScoreReady(alliance string) bool {
numPanels := arena.ScoringPanelRegistry.GetNumPanels(alliance)
return numPanels > 0 && arena.ScoringPanelRegistry.GetNumScoreCommitted(alliance) >= numPanels
}
// Performs any actions that need to run at the interval specified by periodicTaskPeriodSec.
func (arena *Arena) runPeriodicTasks() {
arena.updateEarlyLateMessage()

52
field/arena_notifiers.go Normal file → Executable file
View File

@@ -29,7 +29,6 @@ type ArenaNotifiers struct {
RealtimeScoreNotifier *websocket.Notifier
ReloadDisplaysNotifier *websocket.Notifier
ScorePostedNotifier *websocket.Notifier
ScoringStatusNotifier *websocket.Notifier
}
type MatchTimeMessage struct {
@@ -40,7 +39,6 @@ type MatchTimeMessage struct {
type audienceAllianceScoreFields struct {
Score *game.Score
ScoreSummary *game.ScoreSummary
ControlPanel *game.ControlPanel
}
// Instantiates notifiers and configures their message producing methods.
@@ -62,7 +60,6 @@ func (arena *Arena) configureNotifiers() {
arena.RealtimeScoreNotifier = websocket.NewNotifier("realtimeScore", arena.generateRealtimeScoreMessage)
arena.ReloadDisplaysNotifier = websocket.NewNotifier("reload", nil)
arena.ScorePostedNotifier = websocket.NewNotifier("scorePosted", arena.generateScorePostedMessage)
arena.ScoringStatusNotifier = websocket.NewNotifier("scoringStatus", arena.generateScoringStatusMessage)
}
func (arena *Arena) generateAllianceSelectionMessage() interface{} {
@@ -160,8 +157,8 @@ func (arena *Arena) generateRealtimeScoreMessage() interface{} {
Blue *audienceAllianceScoreFields
MatchState
}{}
fields.Red = getAudienceAllianceScoreFields(arena.RedRealtimeScore, arena.RedScoreSummary())
fields.Blue = getAudienceAllianceScoreFields(arena.BlueRealtimeScore, arena.BlueScoreSummary())
fields.Red = getAudienceAllianceScoreFields(arena.RedScore, arena.RedScoreSummary())
fields.Blue = getAudienceAllianceScoreFields(arena.BlueScore, arena.BlueScoreSummary())
fields.MatchState = arena.MatchState
return &fields
}
@@ -208,53 +205,18 @@ func (arena *Arena) generateScorePostedMessage() interface{} {
RedScoreSummary *game.ScoreSummary
BlueScoreSummary *game.ScoreSummary
Rankings map[int]game.Ranking
RedFouls []game.Foul
BlueFouls []game.Foul
RulesViolated map[int]*game.Rule
RedCards map[string]string
BlueCards map[string]string
SeriesStatus string
SeriesLeader string
}{arena.SavedMatch.CapitalizedType(), arena.SavedMatch, arena.SavedMatchResult.RedScoreSummary(true),
arena.SavedMatchResult.BlueScoreSummary(true), rankings, arena.SavedMatchResult.RedScore.Fouls,
arena.SavedMatchResult.BlueScore.Fouls,
getRulesViolated(arena.SavedMatchResult.RedScore.Fouls, arena.SavedMatchResult.BlueScore.Fouls),
arena.SavedMatchResult.RedCards, arena.SavedMatchResult.BlueCards, seriesStatus, seriesLeader}
}
func (arena *Arena) generateScoringStatusMessage() interface{} {
return &struct {
RefereeScoreReady bool
RedScoreReady bool
BlueScoreReady bool
NumRedScoringPanels int
NumRedScoringPanelsReady int
NumBlueScoringPanels int
NumBlueScoringPanelsReady int
}{arena.RedRealtimeScore.FoulsCommitted && arena.BlueRealtimeScore.FoulsCommitted,
arena.alliancePostMatchScoreReady("red"), arena.alliancePostMatchScoreReady("blue"),
arena.ScoringPanelRegistry.GetNumPanels("red"), arena.ScoringPanelRegistry.GetNumScoreCommitted("red"),
arena.ScoringPanelRegistry.GetNumPanels("blue"), arena.ScoringPanelRegistry.GetNumScoreCommitted("blue")}
}{arena.SavedMatch.CapitalizedType(), arena.SavedMatch, arena.SavedMatchResult.RedScoreSummary(),
arena.SavedMatchResult.BlueScoreSummary(), rankings,
seriesStatus, seriesLeader}
}
// Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay.
func getAudienceAllianceScoreFields(allianceScore *RealtimeScore,
func getAudienceAllianceScoreFields(allianceScore *game.Score,
allianceScoreSummary *game.ScoreSummary) *audienceAllianceScoreFields {
fields := new(audienceAllianceScoreFields)
fields.Score = &allianceScore.CurrentScore
fields.Score = allianceScore
fields.ScoreSummary = allianceScoreSummary
fields.ControlPanel = &allianceScore.ControlPanel
return fields
}
// Produce a map of rules that were violated by either alliance so that they are available to the announcer.
func getRulesViolated(redFouls, blueFouls []game.Foul) map[int]*game.Rule {
rules := make(map[int]*game.Rule)
for _, foul := range redFouls {
rules[foul.RuleId] = game.GetRuleById(foul.RuleId)
}
for _, foul := range blueFouls {
rules[foul.RuleId] = game.GetRuleById(foul.RuleId)
}
return rules
}

View File

@@ -1,20 +0,0 @@
// Copyright 2017 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model representing the current state of the score during a match.
package field
import "github.com/Team254/cheesy-arena/game"
type RealtimeScore struct {
CurrentScore game.Score
Cards map[string]string
FoulsCommitted bool
powerPort game.PowerPort
ControlPanel game.ControlPanel
}
func NewRealtimeScore() *RealtimeScore {
return &RealtimeScore{Cards: make(map[string]string)}
}

View File

@@ -1,78 +0,0 @@
// Copyright 2019 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model representing and methods for tracking the state of a realtime scoring panel.
package field
import (
"github.com/Team254/cheesy-arena/websocket"
"sync"
)
type ScoringPanelRegistry struct {
scoringPanels map[string]map[*websocket.Websocket]bool // The score committed state for each panel.
mutex sync.Mutex
}
func (registry *ScoringPanelRegistry) initialize() {
registry.scoringPanels = map[string]map[*websocket.Websocket]bool{"red": {}, "blue": {}}
}
// Resets the score committed state for each registered panel to false.
func (registry *ScoringPanelRegistry) resetScoreCommitted() {
registry.mutex.Lock()
defer registry.mutex.Unlock()
for _, alliancePanels := range registry.scoringPanels {
for key := range alliancePanels {
alliancePanels[key] = false
}
}
}
// Returns the number of registered panels for the given alliance.
func (registry *ScoringPanelRegistry) GetNumPanels(alliance string) int {
registry.mutex.Lock()
defer registry.mutex.Unlock()
return len(registry.scoringPanels[alliance])
}
// Returns the number of registered panels whose score is committed for the given alliance.
func (registry *ScoringPanelRegistry) GetNumScoreCommitted(alliance string) int {
registry.mutex.Lock()
defer registry.mutex.Unlock()
numCommitted := 0
for _, panel := range registry.scoringPanels[alliance] {
if panel {
numCommitted++
}
}
return numCommitted
}
// Adds a panel to the registry, referenced by its websocket pointer.
func (registry *ScoringPanelRegistry) RegisterPanel(alliance string, ws *websocket.Websocket) {
registry.mutex.Lock()
defer registry.mutex.Unlock()
registry.scoringPanels[alliance][ws] = false
}
// Sets the score committed state to true for the given panel, referenced by its websocket pointer.
func (registry *ScoringPanelRegistry) SetScoreCommitted(alliance string, ws *websocket.Websocket) {
registry.mutex.Lock()
defer registry.mutex.Unlock()
registry.scoringPanels[alliance][ws] = true
}
// Removes a panel from the registry, referenced by its websocket pointer.
func (registry *ScoringPanelRegistry) UnregisterPanel(alliance string, ws *websocket.Websocket) {
registry.mutex.Lock()
defer registry.mutex.Unlock()
delete(registry.scoringPanels[alliance], ws)
}

View File

@@ -1,51 +0,0 @@
// Copyright 2019 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package field
import (
"github.com/Team254/cheesy-arena/websocket"
"github.com/stretchr/testify/assert"
"testing"
)
func TestScoringPanelRegistry(t *testing.T) {
var registry ScoringPanelRegistry
registry.initialize()
assert.Equal(t, 0, registry.GetNumPanels("red"))
assert.Equal(t, 0, registry.GetNumScoreCommitted("red"))
assert.Equal(t, 0, registry.GetNumPanels("blue"))
assert.Equal(t, 0, registry.GetNumScoreCommitted("blue"))
ws1 := new(websocket.Websocket)
ws2 := new(websocket.Websocket)
ws3 := new(websocket.Websocket)
registry.RegisterPanel("red", ws1)
registry.RegisterPanel("blue", ws2)
registry.RegisterPanel("red", ws3)
assert.Equal(t, 2, registry.GetNumPanels("red"))
assert.Equal(t, 0, registry.GetNumScoreCommitted("red"))
assert.Equal(t, 1, registry.GetNumPanels("blue"))
assert.Equal(t, 0, registry.GetNumScoreCommitted("blue"))
registry.SetScoreCommitted("red", ws3)
registry.SetScoreCommitted("blue", ws2)
registry.SetScoreCommitted("blue", ws2)
assert.Equal(t, 2, registry.GetNumPanels("red"))
assert.Equal(t, 1, registry.GetNumScoreCommitted("red"))
assert.Equal(t, 1, registry.GetNumPanels("blue"))
assert.Equal(t, 1, registry.GetNumScoreCommitted("blue"))
registry.UnregisterPanel("red", ws1)
registry.UnregisterPanel("blue", ws2)
assert.Equal(t, 1, registry.GetNumPanels("red"))
assert.Equal(t, 1, registry.GetNumScoreCommitted("red"))
assert.Equal(t, 0, registry.GetNumPanels("blue"))
assert.Equal(t, 0, registry.GetNumScoreCommitted("blue"))
registry.resetScoreCommitted()
assert.Equal(t, 1, registry.GetNumPanels("red"))
assert.Equal(t, 0, registry.GetNumScoreCommitted("red"))
assert.Equal(t, 0, registry.GetNumPanels("blue"))
assert.Equal(t, 0, registry.GetNumScoreCommitted("blue"))
}