diff --git a/game/boiler.go b/game/boiler.go new file mode 100644 index 0000000..0a973da --- /dev/null +++ b/game/boiler.go @@ -0,0 +1,42 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Scoring logic for the 2017 boiler element. + +package game + +import ( + "time" +) + +const ( + BoilerAutoGracePeriodSec = 5 + BoilerTeleopGracePeriodSec = 5 +) + +type Boiler struct { + AutoFuelLow int + AutoFuelHigh int + FuelLow int + FuelHigh int +} + +// Updates the internal counting state of the boiler given the current state of the hardware counts. Allows the score to +// accumulate before the match, since the counters will be reset in hardware. +func (boiler *Boiler) UpdateState(lowCount, highCount int, matchStartTime, currentTime time.Time) { + autoValidityDuration := time.Duration(MatchTiming.AutoDurationSec+BoilerAutoGracePeriodSec) * time.Second + autoValidityCutoff := matchStartTime.Add(autoValidityDuration) + teleopValidityDuration := time.Duration(MatchTiming.AutoDurationSec+MatchTiming.PauseDurationSec+ + MatchTiming.TeleopDurationSec+BoilerTeleopGracePeriodSec) * time.Second + teleopValidityCutoff := matchStartTime.Add(teleopValidityDuration) + + if currentTime.Before(autoValidityCutoff) { + boiler.AutoFuelLow = lowCount + boiler.AutoFuelHigh = highCount + boiler.FuelLow = 0 + boiler.FuelHigh = 0 + } else if currentTime.Before(teleopValidityCutoff) { + boiler.FuelLow = lowCount + boiler.FuelHigh = highCount + } +} diff --git a/game/boiler_test.go b/game/boiler_test.go new file mode 100644 index 0000000..92deedf --- /dev/null +++ b/game/boiler_test.go @@ -0,0 +1,64 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package game + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +var matchStartTime = time.Unix(10, 0) + +func TestFuelBeforeMatch(t *testing.T) { + boiler := Boiler{} + + boiler.UpdateState(1, 2, matchStartTime, timeAfterStart(-1)) + checkBoilerCounts(t, 1, 2, 0, 0, &boiler) +} + +func TestAutoFuel(t *testing.T) { + boiler := Boiler{} + + boiler.UpdateState(3, 4, matchStartTime, timeAfterStart(1)) + checkBoilerCounts(t, 3, 4, 0, 0, &boiler) + boiler.UpdateState(5, 6, matchStartTime, timeAfterStart(10)) + checkBoilerCounts(t, 5, 6, 0, 0, &boiler) + boiler.UpdateState(7, 8, matchStartTime, timeAfterStart(19.9)) + checkBoilerCounts(t, 7, 8, 0, 0, &boiler) + boiler.UpdateState(9, 10, matchStartTime, timeAfterStart(20.1)) + checkBoilerCounts(t, 7, 8, 9, 10, &boiler) +} + +func TestTeleopFuel(t *testing.T) { + boiler := Boiler{} + + boiler.UpdateState(1, 2, matchStartTime, timeAfterStart(1)) + boiler.UpdateState(3, 4, matchStartTime, timeAfterStart(21)) + checkBoilerCounts(t, 1, 2, 3, 4, &boiler) + boiler.UpdateState(5, 6, matchStartTime, timeAfterStart(120)) + checkBoilerCounts(t, 1, 2, 5, 6, &boiler) + boiler.UpdateState(7, 8, matchStartTime, timeAfterEnd(-1)) + checkBoilerCounts(t, 1, 2, 7, 8, &boiler) + boiler.UpdateState(9, 10, matchStartTime, timeAfterEnd(4.9)) + checkBoilerCounts(t, 1, 2, 9, 10, &boiler) + boiler.UpdateState(11, 12, matchStartTime, timeAfterEnd(5.1)) + checkBoilerCounts(t, 1, 2, 9, 10, &boiler) +} + +func checkBoilerCounts(t *testing.T, autoLow, autoHigh, low, high int, boiler *Boiler) { + assert.Equal(t, autoLow, boiler.AutoFuelLow) + assert.Equal(t, autoHigh, boiler.AutoFuelHigh) + assert.Equal(t, low, boiler.FuelLow) + assert.Equal(t, high, boiler.FuelHigh) +} +func timeAfterStart(sec float32) time.Time { + return matchStartTime.Add(time.Duration(1000*sec) * time.Millisecond) +} + +func timeAfterEnd(sec float32) time.Time { + matchDuration := time.Duration(MatchTiming.AutoDurationSec+MatchTiming.PauseDurationSec+ + MatchTiming.TeleopDurationSec) * time.Second + return matchStartTime.Add(matchDuration).Add(time.Duration(1000*sec) * time.Millisecond) +} diff --git a/game/match_timing.go b/game/match_timing.go index e9581af..0a75fe8 100644 --- a/game/match_timing.go +++ b/game/match_timing.go @@ -5,9 +5,16 @@ package game +import "time" + var MatchTiming = struct { AutoDurationSec int PauseDurationSec int TeleopDurationSec int EndgameTimeLeftSec int }{15, 2, 135, 30} + +func GetMatchEndTime(matchStartTime time.Time) time.Time { + return matchStartTime.Add(time.Duration(MatchTiming.AutoDurationSec+MatchTiming.PauseDurationSec+ + MatchTiming.TeleopDurationSec) * time.Second) +} diff --git a/game/rotor_set.go b/game/rotor_set.go new file mode 100644 index 0000000..0a03976 --- /dev/null +++ b/game/rotor_set.go @@ -0,0 +1,50 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Scoring logic for the 2017 rotor elements. + +package game + +import ( + "time" +) + +type RotorSet struct { + AutoRotors int + Rotors int +} + +// Updates the internal counting state of the rotors given the current state of the sensors. +func (rotorSet *RotorSet) UpdateState(rotors [4]bool, matchStartTime, currentTime time.Time) { + autoValidityCutoff := matchStartTime.Add(time.Duration(MatchTiming.AutoDurationSec) * time.Second) + teleopValidityCutoff := autoValidityCutoff.Add(time.Duration(MatchTiming.PauseDurationSec+ + MatchTiming.TeleopDurationSec) * time.Second) + + if currentTime.After(matchStartTime) { + if currentTime.Before(autoValidityCutoff) { + if rotorSet.AutoRotors == 0 && rotors[0] { + rotorSet.AutoRotors++ + } + if rotorSet.AutoRotors == 1 && rotors[1] { + rotorSet.AutoRotors++ + } + } else if currentTime.Before(teleopValidityCutoff) { + if rotorSet.totalRotors() == 0 && rotors[0] { + rotorSet.Rotors++ + } + if rotorSet.totalRotors() == 1 && rotors[1] { + rotorSet.Rotors++ + } + if rotorSet.totalRotors() == 2 && rotors[2] { + rotorSet.Rotors++ + } + if rotorSet.totalRotors() == 3 && rotors[3] { + rotorSet.Rotors++ + } + } + } +} + +func (rotorSet *RotorSet) totalRotors() int { + return rotorSet.AutoRotors + rotorSet.Rotors +} diff --git a/game/rotor_set_test.go b/game/rotor_set_test.go new file mode 100644 index 0000000..88b749d --- /dev/null +++ b/game/rotor_set_test.go @@ -0,0 +1,103 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package game + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRotorsBeforeMatch(t *testing.T) { + rotorSet := RotorSet{} + + rotorSet.UpdateState([4]bool{true, true, true, false}, matchStartTime, timeAfterStart(-1)) + checkRotorCounts(t, 0, 0, &rotorSet) +} + +func TestAutoRotors(t *testing.T) { + rotorSet := RotorSet{} + + rotorSet.UpdateState([4]bool{false, false, false, false}, matchStartTime, timeAfterStart(1)) + checkRotorCounts(t, 0, 0, &rotorSet) + rotorSet.UpdateState([4]bool{false, true, true, true}, matchStartTime, timeAfterStart(1)) + checkRotorCounts(t, 0, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, false, false, false}, matchStartTime, timeAfterStart(1)) + checkRotorCounts(t, 1, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, false, false}, matchStartTime, timeAfterStart(5)) + checkRotorCounts(t, 2, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, true, false}, matchStartTime, timeAfterStart(11)) + checkRotorCounts(t, 2, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, true, false}, matchStartTime, timeAfterStart(20)) + checkRotorCounts(t, 2, 1, &rotorSet) + + // Check going straight to two. + rotorSet = RotorSet{} + rotorSet.UpdateState([4]bool{true, true, false, false}, matchStartTime, timeAfterStart(5)) + checkRotorCounts(t, 2, 0, &rotorSet) + + // Check timing threshold. + rotorSet = RotorSet{} + rotorSet.UpdateState([4]bool{true, false, false, false}, matchStartTime, timeAfterStart(5)) + checkRotorCounts(t, 1, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, false, false}, matchStartTime, timeAfterStart(15.1)) + checkRotorCounts(t, 1, 1, &rotorSet) +} + +func TestTeleopRotors(t *testing.T) { + rotorSet := RotorSet{} + + rotorSet.UpdateState([4]bool{false, false, false, false}, matchStartTime, timeAfterStart(14)) + checkRotorCounts(t, 0, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, false, false, false}, matchStartTime, timeAfterStart(20)) + checkRotorCounts(t, 0, 1, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, false, false}, matchStartTime, timeAfterStart(30)) + checkRotorCounts(t, 0, 2, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, true, false}, matchStartTime, timeAfterStart(100)) + checkRotorCounts(t, 0, 3, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, true, true}, matchStartTime, timeAfterStart(120)) + checkRotorCounts(t, 0, 4, &rotorSet) +} + +func TestRotorsAfterMatch(t *testing.T) { + rotorSet := RotorSet{} + + rotorSet.UpdateState([4]bool{true, false, false, false}, matchStartTime, timeAfterEnd(1)) + checkRotorCounts(t, 0, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, false, false}, matchStartTime, timeAfterEnd(2)) + checkRotorCounts(t, 0, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, true, false}, matchStartTime, timeAfterEnd(3)) + checkRotorCounts(t, 0, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, true, true}, matchStartTime, timeAfterEnd(4)) + checkRotorCounts(t, 0, 0, &rotorSet) +} + +func TestRotorLatching(t *testing.T) { + rotorSet := RotorSet{} + + rotorSet.UpdateState([4]bool{false, true, false, false}, matchStartTime, timeAfterStart(1)) + checkRotorCounts(t, 0, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, false, false, false}, matchStartTime, timeAfterStart(2)) + checkRotorCounts(t, 1, 0, &rotorSet) + rotorSet.UpdateState([4]bool{false, false, false, false}, matchStartTime, timeAfterStart(5)) + checkRotorCounts(t, 1, 0, &rotorSet) + rotorSet.UpdateState([4]bool{false, true, false, false}, matchStartTime, timeAfterStart(10)) + checkRotorCounts(t, 2, 0, &rotorSet) + rotorSet.UpdateState([4]bool{true, true, false, false}, matchStartTime, timeAfterStart(10)) + checkRotorCounts(t, 2, 0, &rotorSet) + rotorSet.UpdateState([4]bool{false, false, false, true}, matchStartTime, timeAfterStart(20)) + checkRotorCounts(t, 2, 0, &rotorSet) + rotorSet.UpdateState([4]bool{false, false, true, false}, matchStartTime, timeAfterStart(30)) + checkRotorCounts(t, 2, 1, &rotorSet) + rotorSet.UpdateState([4]bool{false, false, false, true}, matchStartTime, timeAfterStart(50)) + checkRotorCounts(t, 2, 2, &rotorSet) + rotorSet.UpdateState([4]bool{false, false, false, false}, matchStartTime, timeAfterEnd(-1)) + checkRotorCounts(t, 2, 2, &rotorSet) + rotorSet.UpdateState([4]bool{false, false, false, false}, matchStartTime, timeAfterEnd(1)) + checkRotorCounts(t, 2, 2, &rotorSet) +} + +func checkRotorCounts(t *testing.T, autoRotors, rotors int, rotorSet *RotorSet) { + assert.Equal(t, autoRotors, rotorSet.AutoRotors) + assert.Equal(t, rotors, rotorSet.Rotors) +} diff --git a/game/score.go b/game/score.go index ad6de13..06deab7 100644 --- a/game/score.go +++ b/game/score.go @@ -74,3 +74,20 @@ func (score *Score) Summarize(opponentFouls []Foul, matchType string) *ScoreSumm return summary } + +func (score *Score) Equals(other *Score) bool { + if score.AutoMobility != other.AutoMobility || score.AutoRotors != other.AutoRotors || + score.AutoFuelLow != other.AutoFuelLow || score.AutoFuelHigh != other.AutoFuelHigh || + score.Rotors != other.Rotors || score.FuelLow != other.FuelLow || score.FuelHigh != other.FuelHigh || + score.Takeoffs != other.Takeoffs || score.ElimDq != other.ElimDq || len(score.Fouls) != len(other.Fouls) { + return false + } + + for i, foul := range score.Fouls { + if foul != other.Fouls[i] { + return false + } + } + + return true +} diff --git a/game/score_test.go b/game/score_test.go index c73bd87..a22638e 100644 --- a/game/score_test.go +++ b/game/score_test.go @@ -71,3 +71,83 @@ func TestScoreSummary(t *testing.T) { assert.Equal(t, 0, redScore.Summarize(blueScore.Fouls, "elimination").Score) assert.Equal(t, 0, blueScore.Summarize(redScore.Fouls, "elimination").Score) } + +func TestScoreEquals(t *testing.T) { + score1 := TestScore1() + score2 := TestScore1() + assert.True(t, score1.Equals(score2)) + assert.True(t, score2.Equals(score1)) + + score3 := TestScore2() + assert.False(t, score1.Equals(score3)) + assert.False(t, score3.Equals(score1)) + + score2.AutoMobility += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.AutoRotors += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.AutoFuelLow += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.AutoFuelHigh += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.Rotors += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.FuelLow += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.FuelHigh += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.Takeoffs += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.Fouls = []Foul{} + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.Fouls[0].RuleNumber = "G1000" + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.Fouls[0].IsTechnical = !score2.Fouls[0].IsTechnical + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.Fouls[0].TeamId += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.Fouls[0].TimeInMatchSec += 1 + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) + + score2 = TestScore1() + score2.ElimDq = !score2.ElimDq + assert.False(t, score1.Equals(score2)) + assert.False(t, score2.Equals(score1)) +} diff --git a/game/touchpad.go b/game/touchpad.go new file mode 100644 index 0000000..a13a458 --- /dev/null +++ b/game/touchpad.go @@ -0,0 +1,67 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Scoring logic for the 2017 touchpad element. + +package game + +import ( + "time" +) + +const ( + NotTriggered = iota + Triggered + Held +) + +type Touchpad struct { + lastTriggered bool + triggeredTime *time.Time + untriggeredTime *time.Time +} + +// Updates the internal timing state of the touchpad given the current state of the sensor. +func (touchpad *Touchpad) UpdateState(triggered bool, currentTime time.Time) { + if triggered && !touchpad.lastTriggered { + touchpad.triggeredTime = ¤tTime + touchpad.untriggeredTime = nil + } else if !triggered && touchpad.lastTriggered { + touchpad.untriggeredTime = ¤tTime + } + touchpad.lastTriggered = triggered +} + +// Determines the scoring status of the touchpad. Returns 0 if not triggered, 1 if triggered but not yet for a full +// second, and 2 if triggered and counting for points. +func (touchpad *Touchpad) GetState(matchStartTime, currentTime time.Time) int { + matchEndTime := GetMatchEndTime(matchStartTime) + + if touchpad.triggeredTime != nil && touchpad.triggeredTime.Before(matchEndTime) { + if touchpad.untriggeredTime == nil { + if currentTime.Sub(*touchpad.triggeredTime) >= time.Second { + return Held + } else { + return Triggered + } + } else if touchpad.untriggeredTime.Sub(*touchpad.triggeredTime) >= time.Second && + touchpad.untriggeredTime.After(matchEndTime) { + return Held + } + } + + return NotTriggered +} + +func CountTouchpads(touchpads *[3]Touchpad, matchStartTime, currentTime time.Time) int { + matchEndTime := GetMatchEndTime(matchStartTime) + + count := 0 + for _, touchpad := range touchpads { + if touchpad.GetState(matchEndTime, currentTime) == 2 { + count++ + } + } + + return count +} diff --git a/game/touchpad_test.go b/game/touchpad_test.go new file mode 100644 index 0000000..fd8ab8e --- /dev/null +++ b/game/touchpad_test.go @@ -0,0 +1,100 @@ +// Copyright 2017 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package game + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNotTriggered(t *testing.T) { + touchpad := Touchpad{} + touchpad.UpdateState(false, timeAfterEnd(-10)) + + touchpad.UpdateState(false, timeAfterEnd(-1)) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(-1))) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(2))) +} + +func TestTriggeredReleasedEarly(t *testing.T) { + touchpad := Touchpad{} + touchpad.UpdateState(false, timeAfterEnd(-10)) + + touchpad.UpdateState(true, timeAfterEnd(-5)) + assert.Equal(t, Triggered, touchpad.GetState(matchStartTime, timeAfterEnd(-4.9))) + assert.Equal(t, Held, touchpad.GetState(matchStartTime, timeAfterEnd(-3))) + touchpad.UpdateState(false, timeAfterEnd(-1)) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(-1.1))) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(2))) +} + +func TestTriggeredTooShort(t *testing.T) { + touchpad := Touchpad{} + touchpad.UpdateState(false, timeAfterEnd(-10)) + + touchpad.UpdateState(true, timeAfterEnd(-0.5)) + touchpad.UpdateState(true, timeAfterEnd(0)) + assert.Equal(t, Triggered, touchpad.GetState(matchStartTime, timeAfterEnd(0.2))) + touchpad.UpdateState(false, timeAfterEnd(0.4)) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(0.5))) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(2))) +} + +func TestTriggeredHeld(t *testing.T) { + touchpad := Touchpad{} + touchpad.UpdateState(false, timeAfterEnd(-10)) + + touchpad.UpdateState(true, timeAfterEnd(-5)) + touchpad.UpdateState(true, timeAfterEnd(-3)) + touchpad.UpdateState(true, timeAfterEnd(1)) + assert.Equal(t, Held, touchpad.GetState(matchStartTime, timeAfterEnd(2))) +} + +func TestTriggeredReleased(t *testing.T) { + touchpad := Touchpad{} + touchpad.UpdateState(false, timeAfterEnd(-10)) + + touchpad.UpdateState(true, timeAfterEnd(-5)) + touchpad.UpdateState(true, timeAfterEnd(-3)) + touchpad.UpdateState(true, timeAfterEnd(1)) + assert.Equal(t, Held, touchpad.GetState(matchStartTime, timeAfterEnd(2))) + touchpad.UpdateState(false, timeAfterEnd(3)) + assert.Equal(t, Held, touchpad.GetState(matchStartTime, timeAfterEnd(4))) +} + +func TestReTriggered(t *testing.T) { + touchpad := Touchpad{} + touchpad.UpdateState(false, timeAfterEnd(-10)) + + touchpad.UpdateState(true, timeAfterEnd(-5)) + assert.Equal(t, Held, touchpad.GetState(matchStartTime, timeAfterEnd(-3))) + touchpad.UpdateState(false, timeAfterEnd(-1)) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(-1.1))) + touchpad.UpdateState(true, timeAfterEnd(-0.1)) + assert.Equal(t, Triggered, touchpad.GetState(matchStartTime, timeAfterEnd(0.1))) + assert.Equal(t, Held, touchpad.GetState(matchStartTime, timeAfterEnd(2))) +} + +func TestTriggeredLate(t *testing.T) { + touchpad := Touchpad{} + touchpad.UpdateState(false, timeAfterEnd(-10)) + + touchpad.UpdateState(true, timeAfterEnd(0.1)) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(0.2))) + assert.Equal(t, NotTriggered, touchpad.GetState(matchStartTime, timeAfterEnd(2))) +} + +func TestCountTouchpads(t *testing.T) { + var touchpads [3]Touchpad + touchpads[0].UpdateState(true, timeAfterEnd(-5)) + touchpads[1].UpdateState(true, timeAfterEnd(-2)) + touchpads[2].UpdateState(true, timeAfterEnd(-0.1)) + + assert.Equal(t, 0, CountTouchpads(&touchpads, matchStartTime, timeAfterEnd(-6))) + assert.Equal(t, 0, CountTouchpads(&touchpads, matchStartTime, timeAfterEnd(-5.5))) + assert.Equal(t, 1, CountTouchpads(&touchpads, matchStartTime, timeAfterEnd(-3))) + assert.Equal(t, 1, CountTouchpads(&touchpads, matchStartTime, timeAfterEnd(-1.5))) + assert.Equal(t, 2, CountTouchpads(&touchpads, matchStartTime, timeAfterEnd(0))) + assert.Equal(t, 3, CountTouchpads(&touchpads, matchStartTime, timeAfterEnd(1))) +}