diff --git a/field/arena.go b/field/arena.go index 527331a..087dd53 100644 --- a/field/arena.go +++ b/field/arena.go @@ -76,11 +76,11 @@ type Arena struct { ScaleLeds led.Controller RedSwitchLeds led.Controller BlueSwitchLeds led.Controller - scale *game.Seesaw - redSwitch *game.Seesaw - blueSwitch *game.Seesaw - redVault *game.Vault - blueVault *game.Vault + Scale *game.Seesaw + RedSwitch *game.Seesaw + BlueSwitch *game.Seesaw + RedVault *game.Vault + BlueVault *game.Vault } type ArenaStatus struct { @@ -226,17 +226,17 @@ func (arena *Arena) LoadMatch(match *model.Match) error { arena.RedRealtimeScore = NewRealtimeScore() arena.BlueRealtimeScore = NewRealtimeScore() arena.FieldReset = false - arena.scale = new(game.Seesaw) - arena.redSwitch = new(game.Seesaw) - arena.blueSwitch = new(game.Seesaw) - arena.redVault = new(game.Vault) - arena.blueVault = new(game.Vault) + arena.Scale = &game.Seesaw{Kind: game.NeitherAlliance} + arena.RedSwitch = &game.Seesaw{Kind: game.RedAlliance} + arena.BlueSwitch = &game.Seesaw{Kind: game.BlueAlliance} + arena.RedVault = &game.Vault{Alliance: game.RedAlliance} + arena.BlueVault = &game.Vault{Alliance: game.BlueAlliance} game.ResetPowerUps() // Set a consistent initial value for field element sidedness. - arena.scale.SetSidedness(true) - arena.redSwitch.SetSidedness(true) - arena.blueSwitch.SetSidedness(true) + arena.Scale.SetSidedness(true) + arena.RedSwitch.SetSidedness(true) + arena.BlueSwitch.SetSidedness(true) arena.ScaleLeds.SetSidedness(true) arena.RedSwitchLeds.SetSidedness(true) arena.BlueSwitchLeds.SetSidedness(true) @@ -321,9 +321,9 @@ func (arena *Arena) StartMatch() error { // Configure the field elements with the game-specific data. switchNearIsRed := arena.CurrentMatch.GameSpecificData[0] == 'L' scaleNearIsRed := arena.CurrentMatch.GameSpecificData[1] == 'L' - arena.scale.SetSidedness(scaleNearIsRed) - arena.redSwitch.SetSidedness(switchNearIsRed) - arena.blueSwitch.SetSidedness(switchNearIsRed) + arena.Scale.SetSidedness(scaleNearIsRed) + arena.RedSwitch.SetSidedness(switchNearIsRed) + arena.BlueSwitch.SetSidedness(switchNearIsRed) arena.ScaleLeds.SetSidedness(scaleNearIsRed) arena.RedSwitchLeds.SetSidedness(switchNearIsRed) arena.BlueSwitchLeds.SetSidedness(switchNearIsRed) @@ -701,35 +701,45 @@ func (arena *Arena) handlePlcInput() { // Handle scale and switch ownership. scale, redSwitch, blueSwitch := arena.Plc.GetScaleAndSwitches() - arena.scale.UpdateState(scale, currentTime) - arena.redSwitch.UpdateState(redSwitch, currentTime) - arena.blueSwitch.UpdateState(blueSwitch, currentTime) + arena.Scale.UpdateState(scale, currentTime) + arena.RedSwitch.UpdateState(redSwitch, currentTime) + arena.BlueSwitch.UpdateState(blueSwitch, currentTime) if arena.MatchState == AutoPeriod { - redScore.AutoOwnershipPoints = 2 * int(arena.redSwitch.GetRedSeconds(matchStartTime, currentTime)+ - arena.scale.GetRedSeconds(matchStartTime, currentTime)) - blueScore.AutoOwnershipPoints = 2 * int(arena.blueSwitch.GetBlueSeconds(matchStartTime, currentTime)+ - arena.scale.GetBlueSeconds(matchStartTime, currentTime)) + redScore.AutoOwnershipPoints = 2 * int(arena.RedSwitch.GetRedSeconds(matchStartTime, currentTime)+ + arena.Scale.GetRedSeconds(matchStartTime, currentTime)) + blueScore.AutoOwnershipPoints = 2 * int(arena.BlueSwitch.GetBlueSeconds(matchStartTime, currentTime)+ + arena.Scale.GetBlueSeconds(matchStartTime, currentTime)) } else { - redScore.TeleopOwnershipPoints = int(arena.redSwitch.GetRedSeconds(teleopStartTime, currentTime) + - arena.scale.GetRedSeconds(teleopStartTime, currentTime)) - blueScore.TeleopOwnershipPoints = int(arena.blueSwitch.GetBlueSeconds(teleopStartTime, currentTime) + - arena.scale.GetBlueSeconds(teleopStartTime, currentTime)) + redScore.TeleopOwnershipPoints = int(arena.RedSwitch.GetRedSeconds(teleopStartTime, currentTime) + + arena.Scale.GetRedSeconds(teleopStartTime, currentTime)) + blueScore.TeleopOwnershipPoints = int(arena.BlueSwitch.GetBlueSeconds(teleopStartTime, currentTime) + + arena.Scale.GetBlueSeconds(teleopStartTime, currentTime)) } // Handle vaults. redForceDistance, redLevitateDistance, redBoostDistance, blueForceDistance, blueLevitateDistance, blueBoostDistance := arena.Plc.GetVaults() - arena.redVault.UpdateCubes(redForceDistance, redLevitateDistance, redBoostDistance) - arena.blueVault.UpdateCubes(blueForceDistance, blueLevitateDistance, blueBoostDistance) + arena.RedVault.UpdateCubes(redForceDistance, redLevitateDistance, redBoostDistance) + arena.BlueVault.UpdateCubes(blueForceDistance, blueLevitateDistance, blueBoostDistance) redForce, redLevitate, redBoost, blueForce, blueLevitate, blueBoost := arena.Plc.GetPowerUpButtons() - arena.redVault.UpdateButtons(redForce, redLevitate, redBoost, currentTime) - arena.blueVault.UpdateButtons(blueForce, blueLevitate, blueBoost, currentTime) - redScore.ForceCubes, redScore.ForcePlayed = arena.redVault.ForceCubes, arena.redVault.ForcePowerUp != nil - redScore.LevitateCubes, redScore.LevitatePlayed = arena.redVault.LevitateCubes, arena.redVault.LevitatePlayed - redScore.BoostCubes, redScore.BoostPlayed = arena.redVault.BoostCubes, arena.redVault.BoostPowerUp != nil - blueScore.ForceCubes, blueScore.ForcePlayed = arena.blueVault.ForceCubes, arena.blueVault.ForcePowerUp != nil - blueScore.LevitateCubes, blueScore.LevitatePlayed = arena.blueVault.LevitateCubes, arena.blueVault.LevitatePlayed - blueScore.BoostCubes, blueScore.BoostPlayed = arena.blueVault.BoostCubes, arena.blueVault.BoostPowerUp != nil + arena.RedVault.UpdateButtons(redForce, redLevitate, redBoost, currentTime) + arena.BlueVault.UpdateButtons(blueForce, blueLevitate, blueBoost, currentTime) + redScore.ForceCubes, redScore.ForcePlayed = arena.RedVault.ForceCubes, arena.RedVault.ForcePowerUp != nil + redScore.LevitateCubes, redScore.LevitatePlayed = arena.RedVault.LevitateCubes, arena.RedVault.LevitatePlayed + redScore.BoostCubes, redScore.BoostPlayed = arena.RedVault.BoostCubes, arena.RedVault.BoostPowerUp != nil + blueScore.ForceCubes, blueScore.ForcePlayed = arena.BlueVault.ForceCubes, arena.BlueVault.ForcePowerUp != nil + blueScore.LevitateCubes, blueScore.LevitatePlayed = arena.BlueVault.LevitateCubes, arena.BlueVault.LevitatePlayed + blueScore.BoostCubes, blueScore.BoostPlayed = arena.BlueVault.BoostCubes, arena.BlueVault.BoostPowerUp != nil + + // Check if a power up has been newly played and trigger the accompanying sound effect if so. + newRedPowerUp := arena.RedVault.CheckForNewlyPlayedPowerUp() + if newRedPowerUp != "" && !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-" + newRedPowerUp) + } + newBluePowerUp := arena.BlueVault.CheckForNewlyPlayedPowerUp() + if newBluePowerUp != "" && !arena.MuteMatchSounds { + arena.PlaySoundNotifier.Notify("match-" + newBluePowerUp) + } if !oldRedScore.Equals(redScore) || !oldBlueScore.Equals(blueScore) { arena.RealtimeScoreNotifier.Notify(nil) diff --git a/game/power_up.go b/game/power_up.go index 8f5688d..ac7524a 100644 --- a/game/power_up.go +++ b/game/power_up.go @@ -20,16 +20,17 @@ const ( ) // Power up state enum. -type state int +type PowerUpState int const ( - queued state = iota - active - expired + Unplayed PowerUpState = iota + Queued + Active + Expired ) type PowerUp struct { - alliance + Alliance effect level int startTime time.Time @@ -41,14 +42,14 @@ func ResetPowerUps() { powerUpUses = powerUpUses[:0] } -func (powerUp *PowerUp) GetState(currentTime time.Time) state { +func (powerUp *PowerUp) GetState(currentTime time.Time) PowerUpState { if powerUp.startTime.After(currentTime) { - return queued + return Queued } if powerUp.getEndTime().After(currentTime) { - return active + return Active } - return expired + return Expired } func (powerUp *PowerUp) getEndTime() time.Time { @@ -58,7 +59,7 @@ func (powerUp *PowerUp) getEndTime() time.Time { // Returns the current active power up, or nil if there isn't one. func getActivePowerUp(currentTime time.Time) *PowerUp { for _, powerUp := range powerUpUses { - if powerUp.GetState(currentTime) == active { + if powerUp.GetState(currentTime) == Active { return powerUp } } @@ -75,10 +76,10 @@ func maybeActivatePowerUp(powerUp *PowerUp, currentTime time.Time) *PowerUp { } else { lastPowerUp := powerUpUses[len(powerUpUses)-1] lastPowerUpState := lastPowerUp.GetState(currentTime) - if lastPowerUpState == expired { + if lastPowerUpState == Expired { canActivate = true powerUp.startTime = currentTime - } else if lastPowerUpState == active && lastPowerUp.alliance != powerUp.alliance { + } else if lastPowerUpState == Active && lastPowerUp.Alliance != powerUp.Alliance { canActivate = true powerUp.startTime = lastPowerUp.getEndTime() } diff --git a/game/power_up_test.go b/game/power_up_test.go index cdf9907..53cfca8 100644 --- a/game/power_up_test.go +++ b/game/power_up_test.go @@ -13,11 +13,11 @@ var matchStartTime = time.Unix(10, 0) func TestPowerUpGetState(t *testing.T) { powerUp := PowerUp{startTime: timeAfterStart(30)} - assert.Equal(t, queued, powerUp.GetState(timeAfterStart(25))) - assert.Equal(t, queued, powerUp.GetState(timeAfterStart(29.9))) - assert.Equal(t, active, powerUp.GetState(timeAfterStart(30.1))) - assert.Equal(t, active, powerUp.GetState(timeAfterStart(39.9))) - assert.Equal(t, expired, powerUp.GetState(timeAfterStart(40.1))) + assert.Equal(t, Queued, powerUp.GetState(timeAfterStart(25))) + assert.Equal(t, Queued, powerUp.GetState(timeAfterStart(29.9))) + assert.Equal(t, Active, powerUp.GetState(timeAfterStart(30.1))) + assert.Equal(t, Active, powerUp.GetState(timeAfterStart(39.9))) + assert.Equal(t, Expired, powerUp.GetState(timeAfterStart(40.1))) } func TestPowerUpActivate(t *testing.T) { @@ -41,16 +41,21 @@ func TestPowerUpActivate(t *testing.T) { } func TestPowerUpQueue(t *testing.T) { - powerUp1 := &PowerUp{alliance: redAlliance} - maybeActivatePowerUp(powerUp1, timeAfterStart(60)) + ResetPowerUps() - powerUp2 := &PowerUp{alliance: redAlliance} + powerUp1 := &PowerUp{Alliance: RedAlliance} + assert.NotNil(t, maybeActivatePowerUp(powerUp1, timeAfterStart(60))) + + powerUp2 := &PowerUp{Alliance: RedAlliance} assert.Nil(t, maybeActivatePowerUp(powerUp2, timeAfterStart(65))) - powerUp2.alliance = blueAlliance + powerUp2.Alliance = BlueAlliance if assert.NotNil(t, maybeActivatePowerUp(powerUp2, timeAfterStart(65))) { assert.Equal(t, timeAfterStart(70), powerUp2.startTime) } + powerUp3 := &PowerUp{Alliance: RedAlliance} + assert.NotNil(t, maybeActivatePowerUp(powerUp3, timeAfterStart(81))) + assert.Equal(t, powerUp1, getActivePowerUp(timeAfterStart(69.9))) assert.Equal(t, powerUp2, getActivePowerUp(timeAfterStart(70.1))) } diff --git a/game/seesaw.go b/game/seesaw.go index fd3dbf4..1256c88 100644 --- a/game/seesaw.go +++ b/game/seesaw.go @@ -9,23 +9,23 @@ import ( "time" ) -type alliance int +type Alliance int const ( - neitherAlliance alliance = iota - redAlliance - blueAlliance + NeitherAlliance Alliance = iota + RedAlliance + BlueAlliance ) type Seesaw struct { - kind alliance // Red or blue indicates that it is a switch; neither indicates the scale. + Kind Alliance // Red or blue indicates that it is a switch; neither indicates the scale. nearIsRed bool ownerships []*Ownership } type Ownership struct { seesaw *Seesaw - ownedBy alliance + ownedBy Alliance startTime time.Time endTime *time.Time } @@ -38,46 +38,55 @@ func (seesaw *Seesaw) SetSidedness(nearIsRed bool) { // Updates the internal timing state of the scale or switch given the current state of the sensors. func (seesaw *Seesaw) UpdateState(state [2]bool, currentTime time.Time) { - ownedBy := neitherAlliance + ownedBy := NeitherAlliance // Check if there is an active force power up for this seesaw. currentPowerUp := getActivePowerUp(currentTime) if currentPowerUp != nil && currentPowerUp.effect == force && - (seesaw.kind == neitherAlliance && currentPowerUp.level >= 2 || - (seesaw.kind == currentPowerUp.alliance && (currentPowerUp.level == 1 || currentPowerUp.level == 3))) { - ownedBy = currentPowerUp.alliance + (seesaw.Kind == NeitherAlliance && currentPowerUp.level >= 2 || + (seesaw.Kind == currentPowerUp.Alliance && (currentPowerUp.level == 1 || currentPowerUp.level == 3))) { + ownedBy = currentPowerUp.Alliance } else { // Determine current ownership from sensor state. if state[0] && !state[1] && seesaw.nearIsRed || state[1] && !state[0] && !seesaw.nearIsRed { - ownedBy = redAlliance + ownedBy = RedAlliance } else if state[0] && !state[1] && !seesaw.nearIsRed || state[1] && !state[0] && seesaw.nearIsRed { - ownedBy = blueAlliance + ownedBy = BlueAlliance } } // Update data if ownership has changed since last cycle. currentOwnership := seesaw.getCurrentOwnership() if currentOwnership != nil && ownedBy != currentOwnership.ownedBy || - currentOwnership == nil && ownedBy != neitherAlliance { + currentOwnership == nil && ownedBy != NeitherAlliance { if currentOwnership != nil { currentOwnership.endTime = ¤tTime } - if ownedBy != neitherAlliance { + if ownedBy != NeitherAlliance { newOwnership := &Ownership{seesaw: seesaw, ownedBy: ownedBy, startTime: currentTime} seesaw.ownerships = append(seesaw.ownerships, newOwnership) } } } +func (seesaw *Seesaw) GetOwnedBy() Alliance { + ownership := seesaw.getCurrentOwnership() + if ownership == nil { + return NeitherAlliance + } else { + return ownership.ownedBy + } +} + // Returns the auto and teleop period scores for the red alliance. func (seesaw *Seesaw) GetRedSeconds(startTime, endTime time.Time) float64 { - return seesaw.getAllianceSeconds(redAlliance, startTime, endTime) + return seesaw.getAllianceSeconds(RedAlliance, startTime, endTime) } // Returns the auto and teleop period scores for the blue alliance. func (seesaw *Seesaw) GetBlueSeconds(startTime, endTime time.Time) float64 { - return seesaw.getAllianceSeconds(blueAlliance, startTime, endTime) + return seesaw.getAllianceSeconds(BlueAlliance, startTime, endTime) } func (seesaw *Seesaw) getCurrentOwnership() *Ownership { @@ -90,7 +99,7 @@ func (seesaw *Seesaw) getCurrentOwnership() *Ownership { return nil } -func (seesaw *Seesaw) getAllianceSeconds(ownedBy alliance, startTime, endTime time.Time) float64 { +func (seesaw *Seesaw) getAllianceSeconds(ownedBy Alliance, startTime, endTime time.Time) float64 { var seconds float64 for _, ownership := range seesaw.ownerships { if ownership.ownedBy == ownedBy { @@ -122,9 +131,9 @@ func (ownership *Ownership) getSeconds(startTime, endTime time.Time, ignoreBoost // Find the boost power up applicable to this seesaw and alliance, if it exists. var boostPowerUp *PowerUp for _, powerUp := range powerUpUses { - if powerUp.effect == boost && ownership.ownedBy == powerUp.alliance { - if ownership.seesaw.kind == neitherAlliance && powerUp.level >= 2 || - ownership.seesaw.kind != neitherAlliance && (powerUp.level == 1 || powerUp.level == 3) { + if powerUp.effect == boost && ownership.ownedBy == powerUp.Alliance { + if ownership.seesaw.Kind == NeitherAlliance && powerUp.level >= 2 || + ownership.seesaw.Kind != NeitherAlliance && (powerUp.level == 1 || powerUp.level == 3) { boostPowerUp = powerUp break } diff --git a/game/seesaw_test.go b/game/seesaw_test.go index 8326ab6..e3c5aa5 100644 --- a/game/seesaw_test.go +++ b/game/seesaw_test.go @@ -9,7 +9,7 @@ import ( ) func TestOwnership(t *testing.T) { - ownership := Ownership{nil, redAlliance, timeAfterStart(1), nil} + ownership := Ownership{nil, RedAlliance, timeAfterStart(1), nil} assert.Equal(t, 0.0, ownership.getSeconds(timeAfterStart(0), timeAfterStart(0), true)) assert.Equal(t, 0.5, ownership.getSeconds(timeAfterStart(0), timeAfterStart(1.5), true)) assert.Equal(t, 8.75, ownership.getSeconds(timeAfterStart(0), timeAfterStart(9.75), true)) @@ -31,7 +31,7 @@ func TestOwnership(t *testing.T) { func TestSecondCounting(t *testing.T) { ResetPowerUps() - redSwitch := &Seesaw{kind: redAlliance} + redSwitch := &Seesaw{Kind: RedAlliance} redSwitch.SetSidedness(true) // Test that there is no accumulation before the start of the match. @@ -69,15 +69,15 @@ func TestSecondCounting(t *testing.T) { func TestForce(t *testing.T) { ResetPowerUps() - blueSwitch := &Seesaw{kind: blueAlliance} + blueSwitch := &Seesaw{Kind: BlueAlliance} blueSwitch.SetSidedness(true) - scale := &Seesaw{kind: neitherAlliance} + scale := &Seesaw{Kind: NeitherAlliance} scale.SetSidedness(true) // Force switch only. blueSwitch.UpdateState([2]bool{true, false}, timeAfterStart(0)) scale.UpdateState([2]bool{true, false}, timeAfterStart(0)) - powerUp := &PowerUp{alliance: blueAlliance, effect: force, level: 1} + powerUp := &PowerUp{Alliance: BlueAlliance, effect: force, level: 1} maybeActivatePowerUp(powerUp, timeAfterStart(2.5)) blueSwitch.UpdateState([2]bool{true, false}, timeAfterStart(2.5)) scale.UpdateState([2]bool{true, false}, timeAfterStart(2.5)) @@ -91,7 +91,7 @@ func TestForce(t *testing.T) { assert.Equal(t, 0.0, scale.GetBlueSeconds(timeAfterStart(0), timeAfterStart(15))) // Force scale only. - powerUp = &PowerUp{alliance: blueAlliance, effect: force, level: 2} + powerUp = &PowerUp{Alliance: BlueAlliance, effect: force, level: 2} maybeActivatePowerUp(powerUp, timeAfterStart(20)) blueSwitch.UpdateState([2]bool{true, false}, timeAfterStart(20)) scale.UpdateState([2]bool{true, false}, timeAfterStart(20)) @@ -101,7 +101,7 @@ func TestForce(t *testing.T) { assert.Equal(t, 10.0, scale.GetBlueSeconds(timeAfterStart(20), timeAfterStart(40))) // Force both switch and scale. - powerUp = &PowerUp{alliance: blueAlliance, effect: force, level: 3} + powerUp = &PowerUp{Alliance: BlueAlliance, effect: force, level: 3} maybeActivatePowerUp(powerUp, timeAfterStart(50)) blueSwitch.UpdateState([2]bool{true, false}, timeAfterStart(50)) scale.UpdateState([2]bool{true, false}, timeAfterStart(50)) @@ -114,15 +114,15 @@ func TestForce(t *testing.T) { func TestBoost(t *testing.T) { ResetPowerUps() - blueSwitch := &Seesaw{kind: blueAlliance} + blueSwitch := &Seesaw{Kind: BlueAlliance} blueSwitch.SetSidedness(true) - scale := &Seesaw{kind: neitherAlliance} + scale := &Seesaw{Kind: NeitherAlliance} scale.SetSidedness(false) // Test within continuous ownership period. blueSwitch.UpdateState([2]bool{false, true}, timeAfterStart(20)) scale.UpdateState([2]bool{true, false}, timeAfterStart(20)) - powerUp := &PowerUp{alliance: blueAlliance, effect: boost, level: 2} + powerUp := &PowerUp{Alliance: BlueAlliance, effect: boost, level: 2} maybeActivatePowerUp(powerUp, timeAfterStart(25)) assert.Equal(t, 5.0, scale.GetBlueSeconds(timeAfterStart(0), timeAfterStart(25))) assert.Equal(t, 6.0, scale.GetBlueSeconds(timeAfterStart(0), timeAfterStart(25.5))) @@ -136,7 +136,7 @@ func TestBoost(t *testing.T) { ResetPowerUps() blueSwitch.UpdateState([2]bool{false, false}, timeAfterStart(44)) scale.UpdateState([2]bool{false, false}, timeAfterStart(44)) - powerUp = &PowerUp{alliance: blueAlliance, effect: boost, level: 3} + powerUp = &PowerUp{Alliance: BlueAlliance, effect: boost, level: 3} maybeActivatePowerUp(powerUp, timeAfterStart(45)) assert.Equal(t, 0.0, blueSwitch.GetBlueSeconds(timeAfterStart(45), timeAfterStart(50))) assert.Equal(t, 0.0, scale.GetBlueSeconds(timeAfterStart(45), timeAfterStart(50))) @@ -151,7 +151,7 @@ func TestBoost(t *testing.T) { ResetPowerUps() scale.UpdateState([2]bool{false, true}, timeAfterStart(65)) assert.Equal(t, 5.0, scale.GetRedSeconds(timeAfterStart(65), timeAfterStart(70))) - powerUp = &PowerUp{alliance: redAlliance, effect: boost, level: 2} + powerUp = &PowerUp{Alliance: RedAlliance, effect: boost, level: 2} maybeActivatePowerUp(powerUp, timeAfterStart(70)) scale.UpdateState([2]bool{false, false}, timeAfterStart(72.5)) assert.Equal(t, 10.0, scale.GetRedSeconds(timeAfterStart(65), timeAfterStart(72.5))) @@ -163,7 +163,7 @@ func TestBoost(t *testing.T) { // Test with just the switch. blueSwitch.UpdateState([2]bool{false, true}, timeAfterStart(100)) scale.UpdateState([2]bool{true, false}, timeAfterStart(100)) - powerUp = &PowerUp{alliance: blueAlliance, effect: boost, level: 1} + powerUp = &PowerUp{Alliance: BlueAlliance, effect: boost, level: 1} maybeActivatePowerUp(powerUp, timeAfterStart(100)) assert.Equal(t, 20.0, blueSwitch.GetBlueSeconds(timeAfterStart(100), timeAfterStart(110))) assert.Equal(t, 10.0, scale.GetBlueSeconds(timeAfterStart(100), timeAfterStart(110))) diff --git a/game/vault.go b/game/vault.go index a4fd78f..10d779a 100644 --- a/game/vault.go +++ b/game/vault.go @@ -10,13 +10,14 @@ import ( ) type Vault struct { - alliance alliance - ForceCubes int - LevitateCubes int - BoostCubes int - LevitatePlayed bool - ForcePowerUp *PowerUp - BoostPowerUp *PowerUp + Alliance + ForceCubes int + LevitateCubes int + BoostCubes int + LevitatePlayed bool + ForcePowerUp *PowerUp + BoostPowerUp *PowerUp + newlyPlayedPowerUp string } // Updates the state of the vault given the state of the individual power cube sensors. @@ -30,19 +31,33 @@ func (vault *Vault) UpdateCubes(forceDistance, levitateDistance, boostDistance u func (vault *Vault) UpdateButtons(forceButton, levitateButton, boostButton bool, currentTime time.Time) { if levitateButton && vault.LevitateCubes == 3 && !vault.LevitatePlayed { vault.LevitatePlayed = true + vault.newlyPlayedPowerUp = "levitate" } if forceButton && vault.ForceCubes > 0 && vault.ForcePowerUp == nil { - vault.ForcePowerUp = maybeActivatePowerUp(&PowerUp{effect: force, alliance: vault.alliance, + vault.ForcePowerUp = maybeActivatePowerUp(&PowerUp{effect: force, Alliance: vault.Alliance, level: vault.ForceCubes}, currentTime) + if vault.ForcePowerUp != nil { + vault.newlyPlayedPowerUp = "force" + } } if boostButton && vault.BoostCubes > 0 && vault.BoostPowerUp == nil { - vault.BoostPowerUp = maybeActivatePowerUp(&PowerUp{effect: boost, alliance: vault.alliance, + vault.BoostPowerUp = maybeActivatePowerUp(&PowerUp{effect: boost, Alliance: vault.Alliance, level: vault.BoostCubes}, currentTime) + if vault.BoostPowerUp != nil { + vault.newlyPlayedPowerUp = "boost" + } } } +// Returns the name of the newly-played power up if there is one, or an empty string otherwise, and resets the state. +func (vault *Vault) CheckForNewlyPlayedPowerUp() string { + powerUp := vault.newlyPlayedPowerUp + vault.newlyPlayedPowerUp = "" + return powerUp +} + func countCubes(distance uint16) int { // TODO(patrick): Update with real values once there is a physical setup to test with. if distance >= 3000 { diff --git a/game/vault_test.go b/game/vault_test.go index 1639019..010d267 100644 --- a/game/vault_test.go +++ b/game/vault_test.go @@ -71,7 +71,7 @@ func TestVaultLevitate(t *testing.T) { } func TestVaultForce(t *testing.T) { - vault := Vault{alliance: blueAlliance} + vault := Vault{Alliance: BlueAlliance} ResetPowerUps() vault.UpdateCubes(0, 0, 0) @@ -86,30 +86,30 @@ func TestVaultForce(t *testing.T) { vault.UpdateCubes(1000, 0, 0) vault.UpdateButtons(true, false, false, time.Now()) if assert.NotNil(t, vault.ForcePowerUp) { - assert.Equal(t, blueAlliance, vault.ForcePowerUp.alliance) + assert.Equal(t, BlueAlliance, vault.ForcePowerUp.Alliance) assert.Equal(t, force, vault.ForcePowerUp.effect) assert.Equal(t, 1, vault.ForcePowerUp.level) } // Activation with two cubes. - vault = Vault{alliance: redAlliance} + vault = Vault{Alliance: RedAlliance} ResetPowerUps() vault.UpdateCubes(2000, 0, 0) vault.UpdateButtons(true, false, false, time.Now()) if assert.NotNil(t, vault.ForcePowerUp) { - assert.Equal(t, redAlliance, vault.ForcePowerUp.alliance) + assert.Equal(t, RedAlliance, vault.ForcePowerUp.Alliance) assert.Equal(t, force, vault.ForcePowerUp.effect) assert.Equal(t, 2, vault.ForcePowerUp.level) } // Activation with three cubes. - vault = Vault{alliance: blueAlliance} + vault = Vault{Alliance: BlueAlliance} ResetPowerUps() vault.UpdateCubes(3000, 0, 0) vault.UpdateButtons(true, false, false, time.Now()) assert.NotNil(t, vault.ForcePowerUp) if assert.NotNil(t, vault.ForcePowerUp) { - assert.Equal(t, blueAlliance, vault.ForcePowerUp.alliance) + assert.Equal(t, BlueAlliance, vault.ForcePowerUp.Alliance) assert.Equal(t, force, vault.ForcePowerUp.effect) assert.Equal(t, 3, vault.ForcePowerUp.level) } @@ -120,7 +120,7 @@ func TestVaultForce(t *testing.T) { } func TestVaultBoost(t *testing.T) { - vault := Vault{alliance: blueAlliance} + vault := Vault{Alliance: BlueAlliance} ResetPowerUps() vault.UpdateCubes(0, 0, 0) @@ -135,30 +135,30 @@ func TestVaultBoost(t *testing.T) { vault.UpdateCubes(0, 0, 1000) vault.UpdateButtons(false, false, true, time.Now()) if assert.NotNil(t, vault.BoostPowerUp) { - assert.Equal(t, blueAlliance, vault.BoostPowerUp.alliance) + assert.Equal(t, BlueAlliance, vault.BoostPowerUp.Alliance) assert.Equal(t, boost, vault.BoostPowerUp.effect) assert.Equal(t, 1, vault.BoostPowerUp.level) } // Activation with two cubes. - vault = Vault{alliance: redAlliance} + vault = Vault{Alliance: RedAlliance} ResetPowerUps() vault.UpdateCubes(0, 0, 2000) vault.UpdateButtons(false, false, true, time.Now()) if assert.NotNil(t, vault.BoostPowerUp) { - assert.Equal(t, redAlliance, vault.BoostPowerUp.alliance) + assert.Equal(t, RedAlliance, vault.BoostPowerUp.Alliance) assert.Equal(t, boost, vault.BoostPowerUp.effect) assert.Equal(t, 2, vault.BoostPowerUp.level) } // Activation with three cubes. - vault = Vault{alliance: blueAlliance} + vault = Vault{Alliance: BlueAlliance} ResetPowerUps() vault.UpdateCubes(0, 0, 3000) vault.UpdateButtons(false, false, true, time.Now()) assert.NotNil(t, vault.BoostPowerUp) if assert.NotNil(t, vault.BoostPowerUp) { - assert.Equal(t, blueAlliance, vault.BoostPowerUp.alliance) + assert.Equal(t, BlueAlliance, vault.BoostPowerUp.Alliance) assert.Equal(t, boost, vault.BoostPowerUp.effect) assert.Equal(t, 3, vault.BoostPowerUp.level) } @@ -167,3 +167,44 @@ func TestVaultBoost(t *testing.T) { vault.UpdateButtons(false, false, false, time.Now()) assert.NotNil(t, vault.BoostPowerUp) } + +func TestVaultMultipleActivations(t *testing.T) { + redVault := Vault{Alliance: RedAlliance} + redVault.UpdateCubes(1000, 3000, 1000) + blueVault := Vault{Alliance: BlueAlliance} + blueVault.UpdateCubes(1000, 3000, 1000) + ResetPowerUps() + + redVault.UpdateButtons(true, false, false, timeAfterStart(0)) + redVault.UpdateButtons(false, false, false, timeAfterStart(1)) + if assert.NotNil(t, redVault.ForcePowerUp) { + assert.Equal(t, Active, redVault.ForcePowerUp.GetState(timeAfterStart(0.5))) + } + assert.Equal(t, "force", redVault.CheckForNewlyPlayedPowerUp()) + assert.Equal(t, "", redVault.CheckForNewlyPlayedPowerUp()) + + redVault.UpdateButtons(false, true, false, timeAfterStart(2)) + redVault.UpdateButtons(false, false, false, timeAfterStart(3)) + assert.True(t, redVault.LevitatePlayed) + assert.Equal(t, "levitate", redVault.CheckForNewlyPlayedPowerUp()) + assert.Equal(t, "", redVault.CheckForNewlyPlayedPowerUp()) + + blueVault.UpdateButtons(false, false, true, timeAfterStart(4)) + blueVault.UpdateButtons(false, false, false, timeAfterStart(5)) + if assert.NotNil(t, blueVault.BoostPowerUp) { + assert.Equal(t, Queued, blueVault.BoostPowerUp.GetState(timeAfterStart(4.5))) + } + assert.Equal(t, "boost", blueVault.CheckForNewlyPlayedPowerUp()) + assert.Equal(t, "", blueVault.CheckForNewlyPlayedPowerUp()) + assert.Equal(t, Expired, redVault.ForcePowerUp.GetState(timeAfterStart(11))) + assert.Equal(t, Active, blueVault.BoostPowerUp.GetState(timeAfterStart(11))) + assert.Equal(t, Expired, blueVault.BoostPowerUp.GetState(timeAfterStart(21))) + + redVault.UpdateButtons(false, false, true, timeAfterStart(25)) + redVault.UpdateButtons(false, false, false, timeAfterStart(26)) + if assert.NotNil(t, redVault.BoostPowerUp) { + assert.Equal(t, Active, redVault.BoostPowerUp.GetState(timeAfterStart(25.5))) + } + assert.Equal(t, "boost", redVault.CheckForNewlyPlayedPowerUp()) + assert.Equal(t, "", redVault.CheckForNewlyPlayedPowerUp()) +} diff --git a/static/audio/match_boost.wav b/static/audio/match_boost.wav new file mode 100644 index 0000000..35a9614 Binary files /dev/null and b/static/audio/match_boost.wav differ diff --git a/static/audio/match_force.wav b/static/audio/match_force.wav new file mode 100644 index 0000000..4bb3d74 Binary files /dev/null and b/static/audio/match_force.wav differ diff --git a/static/audio/match_levitate.wav b/static/audio/match_levitate.wav new file mode 100644 index 0000000..290fddb Binary files /dev/null and b/static/audio/match_levitate.wav differ diff --git a/static/audio/match_powerup.wav b/static/audio/match_powerup.wav deleted file mode 100644 index 98b95ce..0000000 Binary files a/static/audio/match_powerup.wav and /dev/null differ diff --git a/static/css/audience_display.css b/static/css/audience_display.css index cdd7d4b..3e8e87f 100644 --- a/static/css/audience_display.css +++ b/static/css/audience_display.css @@ -104,37 +104,92 @@ html { color: #fff; opacity: 0; } -.score-fields-icons { - line-height: 99px; - text-align: center; - width: 30px; -} -.score-fields-icons span { - display: inline-block; - line-height: 33px; -} -.score-fields-text { - width: 19px; -} -.score-fields-text span { +.score-fields span { display: inline-block; line-height: 34px; padding: 0; } +.power-up[data-state="0"] { + opacity: 1; +} +.power-up[data-state="1"] { + color: #ffff00; +} +.power-up[data-state="2"] { + color: #ffff00; + animation: power-up-blinker 0.5s linear infinite; +} +.power-up[data-state="3"] { + opacity: 0.4; +} +@keyframes power-up-blinker { + 50% { + opacity: 0.5; + } +} +.score-fields-icons { + text-align: center; + width: 28px; +} +.score-fields-text { + width: 19px; +} +.powerup-progress { + float: left; + height: 100%; + margin: 0 4px 0 4px; + width: 6px; +} +.powerup-progress>div { + position: absolute; + top: 0; + bottom: 0; + width: 6px; + height: 0; + margin: auto; + background-color: #fff; + opacity: 0.7; + border-radius: 3px; +} #logo { position: relative; - top: 30px; - height: 85px; + top: 35px; + height: 75px; } #matchTime { position: relative; - top: 6px; + top: 4px; height: 60px; color: #222; font-family: "FuturaLTBold"; font-size: 32px; opacity: 0; } +.seesaw-indicator { + position: absolute; + bottom: 8px; + border-radius: 3px; + width: 11px; + height: 16px; + background-color: #ccc; + border: 1px solid #333; + opacity: 0; +} +.seesaw-indicator[data-owned-by="1"] { + background-color: #ff4444; +} +.seesaw-indicator[data-owned-by="2"] { + background-color: #2080ff; +} +#redSwitchIndicator { + left: 50px; +} +#scaleIndicator { + left: 68px; +} +#blueSwitchIndicator { + left: 86px; +} #blindsContainer { position: fixed; top: 0; @@ -406,11 +461,3 @@ html { font-family: "FuturaLTBold"; line-height: 87px; } -.score-icon { - position: relative; - top: -4px; - height: 23px; -} -.score-icon-right { - -webkit-transform: scaleX(-1); -} diff --git a/static/img/pressure.svg b/static/img/pressure.svg deleted file mode 100644 index f9a1756..0000000 --- a/static/img/pressure.svg +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/img/rotor.svg b/static/img/rotor.svg deleted file mode 100644 index 2af8e86..0000000 --- a/static/img/rotor.svg +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/img/takeoff.svg b/static/img/takeoff.svg deleted file mode 100644 index 995cbe4..0000000 --- a/static/img/takeoff.svg +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/js/audience_display.js b/static/js/audience_display.js index 934f0bb..7507760 100644 --- a/static/js/audience_display.js +++ b/static/js/audience_display.js @@ -52,14 +52,36 @@ var handleMatchTime = function(data) { // Handles a websocket message to update the match score. var handleRealtimeScore = function(data) { - $("#redScoreNumber").text(data.RedScoreSummary.Score); - $("#redOwnershipPoints").text(data.RedScoreSummary.OwnershipPoints); - $("#redRotors").text(data.RedScore.AutoRotors + data.RedScore.Rotors); - $("#redTakeoffs").text(data.RedScore.Takeoffs); - $("#blueScoreNumber").text(data.BlueScoreSummary.Score); - $("#blueOwnershipPoints").text(data.BlueScoreSummary.OwnershipPoints); - $("#blueRotors").text(data.BlueScore.AutoRotors + data.BlueScore.Rotors); - $("#blueTakeoffs").text(data.BlueScore.Takeoffs); + $("#redScoreNumber").text(data.Red.Score); + $("#redForceCubesIcon").attr("data-state", data.Red.ForceState); + $("#redForceCubes").text(data.Red.ForceCubes).attr("data-state", data.Red.ForceState); + $("#redLevitateCubesIcon").attr("data-state", data.Red.LevitateState); + $("#redLevitateCubes").text(data.Red.LevitateCubes).attr("data-state", data.Red.LevitateState); + $("#redBoostCubesIcon").attr("data-state", data.Red.BoostState); + $("#redBoostCubes").text(data.Red.BoostCubes).attr("data-state", data.Red.BoostState); + + $("#blueScoreNumber").text(data.Blue.Score); + $("#blueForceCubesIcon").attr("data-state", data.Blue.ForceState); + $("#blueForceCubes").text(data.Blue.ForceCubes).attr("data-state", data.Blue.ForceState); + $("#blueLevitateCubesIcon").attr("data-state", data.Blue.LevitateState); + $("#blueLevitateCubes").text(data.Blue.LevitateCubes).attr("data-state", data.Blue.LevitateState); + $("#blueBoostCubesIcon").attr("data-state", data.Blue.BoostState); + $("#blueBoostCubes").text(data.Blue.BoostCubes).attr("data-state", data.Blue.BoostState); + + // Switch/scale indicators. + $("#scaleIndicator").attr("data-owned-by", data.ScaleOwnedBy); + $("#redSwitchIndicator").attr("data-owned-by", data.Red.SwitchOwnedBy); + $("#blueSwitchIndicator").attr("data-owned-by", data.Blue.SwitchOwnedBy); + + // Power up progress bars. + if ((data.Red.ForceState === 2 || data.Red.BoostState === 2) && $("#redProgress").height() === 0) { + $("#redProgress").height(85); + $("#redProgress").transition({queue: false, height: 0}, 10000, "linear"); + } + if ((data.Blue.ForceState === 2 || data.Blue.BoostState === 2) && $("#blueProgress").height() === 0) { + $("#blueProgress").height(85); + $("#blueProgress").transition({queue: false, height: 0}, 10000, "linear"); + } }; // Handles a websocket message to populate the final score data. @@ -144,9 +166,10 @@ var transitionBlankToIntro = function(callback) { var transitionIntroToInMatch = function(callback) { $("#logo").transition({queue: false, top: "10px"}, 500, "ease"); - $(".score").transition({queue: false, width: "250px"}, 500, "ease", function() { + $(".score").transition({queue: false, width: "275px"}, 500, "ease", function() { $(".score-number").transition({queue: false, opacity: 1}, 750, "ease"); $(".score-fields").transition({queue: false, opacity: 1}, 750, "ease"); + $(".seesaw-indicator").transition({queue: false, opacity: 1}, 750, "ease"); $("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback); }); }; @@ -165,10 +188,11 @@ var transitionBlankToInMatch = function(callback) { $("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() { $(".teams").transition({queue: false, width: "65px"}, 100, "linear", function() { $("#logo").transition({queue: false, top: "10px"}, 500, "ease"); - $(".score").transition({queue: false, width: "250px"}, 500, "ease", function() { + $(".score").transition({queue: false, width: "275px"}, 500, "ease", function() { $("#eventMatchInfo").show(); $(".score-number").transition({queue: false, opacity: 1}, 750, "ease"); $(".score-fields").transition({queue: false, opacity: 1}, 750, "ease"); + $(".seesaw-indicator").transition({queue: false, opacity: 1}, 750, "ease"); $("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback); var height = -$("#eventMatchInfo").height(); $("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback); @@ -180,8 +204,9 @@ var transitionBlankToInMatch = function(callback) { var transitionInMatchToIntro = function(callback) { $(".score-number").transition({queue: false, opacity: 0}, 300, "linear"); $(".score-fields").transition({queue: false, opacity: 0}, 300, "linear"); + $(".seesaw-indicator").transition({queue: false, opacity: 0}, 300, "linear"); $("#matchTime").transition({queue: false, opacity: 0}, 300, "linear", function() { - $("#logo").transition({queue: false, top: "30px"}, 500, "ease"); + $("#logo").transition({queue: false, top: "35px"}, 500, "ease"); $(".score").transition({queue: false, width: "120px"}, 500, "ease"); $(".teams").transition({queue: false, width: "65px"}, 500, "ease", callback); }); @@ -191,9 +216,10 @@ var transitionInMatchToBlank = function(callback) { $("#eventMatchInfo").transition({queue: false, bottom: "0px"}, 500, "ease"); $("#matchTime").transition({queue: false, opacity: 0}, 300, "linear"); $(".score-fields").transition({queue: false, opacity: 0}, 300, "linear"); + $(".seesaw-indicator").transition({queue: false, opacity: 0}, 300, "linear"); $(".score-number").transition({queue: false, opacity: 0}, 300, "linear", function() { $("#eventMatchInfo").hide(); - $("#logo").transition({queue: false, top: "30px"}, 500, "ease"); + $("#logo").transition({queue: false, top: "35px"}, 500, "ease"); $(".score").transition({queue: false, width: "0px"}, 500, "ease"); $(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() { $("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback); diff --git a/templates/audience_display.html b/templates/audience_display.html index b007459..fb040cf 100644 --- a/templates/audience_display.html +++ b/templates/audience_display.html @@ -26,35 +26,41 @@
-
-
- + F
+ L
+ B
-
-
- +
+
+
+
+
+
 
-
-
- + F
+ L
+ B
-
-
- +
+
+
+
+
+
 
@@ -72,6 +78,9 @@
+
+
+
@@ -163,6 +172,9 @@ + + + diff --git a/web/audience_display.go b/web/audience_display.go index 07a2d95..f371291 100644 --- a/web/audience_display.go +++ b/web/audience_display.go @@ -11,8 +11,26 @@ import ( "io" "log" "net/http" + "time" ) +type audienceScoreFields struct { + Red *audienceAllianceScoreFields + Blue *audienceAllianceScoreFields + ScaleOwnedBy game.Alliance +} + +type audienceAllianceScoreFields struct { + Score int + ForceCubes int + LevitateCubes int + BoostCubes int + ForceState game.PowerUpState + LevitateState game.PowerUpState + BoostState game.PowerUpState + SwitchOwnedBy game.Alliance +} + // Renders the audience display to be chroma keyed over the video feed. func (web *Web) audienceDisplayHandler(w http.ResponseWriter, r *http.Request) { if !web.userIsReader(w, r) { @@ -93,13 +111,7 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R log.Printf("Websocket error: %s", err) return } - data = struct { - RedScore *game.Score - BlueScore *game.Score - RedScoreSummary *game.ScoreSummary - BlueScoreSummary *game.ScoreSummary - }{&web.arena.RedRealtimeScore.CurrentScore, &web.arena.BlueRealtimeScore.CurrentScore, - web.arena.RedScoreSummary(), web.arena.BlueScoreSummary()} + data = web.getAudienceScoreFields() err = websocket.Write("realtimeScore", data) if err != nil { log.Printf("Websocket error: %s", err) @@ -155,13 +167,7 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R return } messageType = "realtimeScore" - message = struct { - RedScore *game.Score - BlueScore *game.Score - RedScoreSummary *game.ScoreSummary - BlueScoreSummary *game.ScoreSummary - }{&web.arena.RedRealtimeScore.CurrentScore, &web.arena.BlueRealtimeScore.CurrentScore, - web.arena.RedScoreSummary(), web.arena.BlueScoreSummary()} + message = web.getAudienceScoreFields() case _, ok := <-scorePostedListener: if !ok { return @@ -220,3 +226,41 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R } } } + +// Constructs the data object sent to the audience display for the realtime scoring overlay. +func (web *Web) getAudienceScoreFields() *audienceScoreFields { + fields := new(audienceScoreFields) + fields.Red = getAudienceAllianceScoreFields(&web.arena.RedRealtimeScore.CurrentScore, web.arena.RedScoreSummary(), + web.arena.RedVault, web.arena.RedSwitch) + fields.Blue = getAudienceAllianceScoreFields(&web.arena.BlueRealtimeScore.CurrentScore, + web.arena.BlueScoreSummary(), web.arena.BlueVault, web.arena.BlueSwitch) + fields.ScaleOwnedBy = web.arena.Scale.GetOwnedBy() + return fields +} + +// Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay. +func getAudienceAllianceScoreFields(allianceScore *game.Score, allianceScoreSummary *game.ScoreSummary, + allianceVault *game.Vault, allianceSwitch *game.Seesaw) *audienceAllianceScoreFields { + fields := new(audienceAllianceScoreFields) + fields.Score = allianceScoreSummary.Score + fields.ForceCubes = allianceScore.ForceCubes + fields.LevitateCubes = allianceScore.LevitateCubes + fields.BoostCubes = allianceScore.BoostCubes + if allianceVault.ForcePowerUp != nil { + fields.ForceState = allianceVault.ForcePowerUp.GetState(time.Now()) + } else { + fields.ForceState = game.Unplayed + } + if allianceVault.LevitatePlayed { + fields.LevitateState = game.Expired + } else { + fields.LevitateState = game.Unplayed + } + if allianceVault.BoostPowerUp != nil { + fields.BoostState = allianceVault.BoostPowerUp.GetState(time.Now()) + } else { + fields.BoostState = game.Unplayed + } + fields.SwitchOwnedBy = allianceSwitch.GetOwnedBy() + return fields +}