diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index 61f5b73..83babed 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -201,14 +201,16 @@ func (arena *Arena) generateScorePostedMessage() interface{} { BlueScoreSummary *game.ScoreSummary 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(), - arena.SavedMatchResult.BlueScoreSummary(), populateFoulDescriptions(arena.SavedMatchResult.RedScore.Fouls), - populateFoulDescriptions(arena.SavedMatchResult.BlueScore.Fouls), arena.SavedMatchResult.RedCards, - arena.SavedMatchResult.BlueCards, seriesStatus, seriesLeader} + arena.SavedMatchResult.BlueScoreSummary(), 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{} { @@ -236,17 +238,14 @@ func getAudienceAllianceScoreFields(allianceScore *RealtimeScore, return fields } -// Copy the description from the rules to the fouls so that they are available to the announcer. -func populateFoulDescriptions(fouls []game.Foul) []game.Foul { - foulsCopy := make([]game.Foul, len(fouls)) - copy(foulsCopy, fouls) - for i := range foulsCopy { - for _, rule := range game.Rules { - if foulsCopy[i].RuleNumber == rule.RuleNumber { - foulsCopy[i].Description = rule.Description - break - } - } +// 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) } - return foulsCopy + for _, foul := range blueFouls { + rules[foul.RuleId] = game.GetRuleById(foul.RuleId) + } + return rules } diff --git a/game/foul.go b/game/foul.go index 4f82748..7b54412 100644 --- a/game/foul.go +++ b/game/foul.go @@ -1,56 +1,27 @@ // Copyright 2017 Team 254. All Rights Reserved. // Author: pat@patfairbank.com (Patrick Fairbank) // -// Model of a foul and game-specific rules. +// Model of a foul. package game type Foul struct { - Rule + RuleId int TeamId int TimeInMatchSec float64 } -type Rule struct { - RuleNumber string - IsTechnical bool - IsRankingPoint bool - Description string -} - -// All rules from the 2018 game that carry point penalties. -var Rules = []Rule{ - {"S6", false, false, "DRIVE TEAMS may not extend any body part into the CARGO Chute. Momentary encroachment into the Chute is an exception to this rule."}, - {"C8", false, false, "Strategies clearly aimed at forcing the opposing ALLIANCE to violate a rule are not in the spirit of FIRST Robotics Competition and not allowed."}, - {"G3", true, false, "During the SANDSTORM PERIOD, a ROBOT may not cross the FIELD such that its BUMPERS break the plane defined by their opponent’s CARGO SHIP LINE."}, - {"G4", false, false, "ROBOTS may not have greater-than-momentary or repeated control, i.e. exercise greater-than-momentary or repeated influence, of more than one (1) GAME PIECE at a time, either directly or transitively through other objects."}, - {"G5", false, true, "A ROBOT may not remove a GAME PIECE from an opponents’ ROCKET/CARGO SHIP."}, - {"G7", false, false, "ROBOTS may not intentionally eject GAME PIECES from the FIELD."}, - {"G8", false, false, "ROBOTS may not deliberately use GAME PIECES in an attempt to ease or amplify the challenge associated with FIELD elements."}, - {"G9", false, false, "No more than one ROBOT may be positioned such that its BUMPERS are completely beyond the opponent’s CARGO SHIP LINE."}, - {"G9", true, false, "No more than one ROBOT may be positioned such that its BUMPERS are completely beyond the opponent’s CARGO SHIP LINE."}, - {"G10", false, false, "No part of a ROBOT, except its BUMPERS, may be outside its FRAME PERIMETER if its BUMPERS are completely beyond its opponent’s CARGO SHIP LINE."}, - {"G10", true, false, "No part of a ROBOT, except its BUMPERS, may be outside its FRAME PERIMETER if its BUMPERS are completely beyond its opponent’s CARGO SHIP LINE."}, - {"G12", false, false, "A ROBOT may not break the vertical plane above the ALLIANCE STATION WALL or damage the SANDSTORM."}, - {"G13", false, false, "A ROBOT may not contact an opponent ROBOT if that opponent ROBOT’S BUMPERS are fully in their HAB ZONE."}, - {"G15", false, false, "DRIVE TEAMS, ROBOTS, and OPERATOR CONSOLES are prohibited from the following actions with regards to interaction with ARENA elements: grabbing, grasping, attaching to, hanging, deforming, becoming entangled, damaging, and repositioning GAME PIECE holders."}, - {"G16", false, true, "During Qualification MATCHES, ROBOTS may not contact opponents’ ROCKETS starting at T-minus 20s."}, - {"G17", false, false, "Fallen (i.e. tipped over) ROBOTS attempting to right themselves (either by themselves or with assistance from a partner ROBOT) have one ten (10) second grace period in which they may not be contacted by an opponent ROBOT."}, - {"G18", false, false, "ROBOTS may not pin an opponent’s ROBOT for more than five (5) seconds."}, - {"G18", true, false, "ROBOTS may not pin an opponent’s ROBOT for more than five (5) seconds."}, - {"G19", true, false, "Strategies aimed at the destruction or inhibition of ROBOTS via attachment, damage, tipping, or entanglements are not allowed."}, - {"G20", true, false, "Initiating deliberate or damaging contact with an opponent ROBOT on or inside the vertical extension of its FRAME PERIMETER, including transitively through a GAME PIECE, is not allowed."}, - {"G23", false, false, "BUMPERS must be in the BUMPER ZONE during the MATCH unless a ROBOT is completely in its HAB ZONE or supported by a ROBOT completely in its HAB ZONE."}, - {"G24", false, false, "ROBOTS may not extend more than 30 in (~76 cm). beyond their FRAME PERIMETER."}, - {"H6", false, false, "During the MATCH, DRIVERS, COACHES, and HUMAN PLAYERS may not contact anything outside the ALLIANCE STATION and TECHNICIANS may not contact anything outside their designated area."}, - {"H7", false, false, "During the MATCH, team members may only enter GAME PIECES on to the FIELD through their LOADING STATIONS."}, - {"H8", false, false, "During a MATCH, COACHES may not touch GAME PIECES unless for safety purposes."}, - {"H9", true, false, "During the SANDSTORM PERIOD, COACHES, DRIVERS, HUMAN PLAYERS, and any part of the OPERATOR CONSOLE may not break the vertical planes defined by the STARTING LINES, unless for safety purposes."}, - {"H10", true, false, "During the SANDSTORM PERIOD, COACHES, DRIVERS, and HUMAN PLAYERS may not look over the top of the ALLIANCE WALL down to the FIELD to overcome the effect of the SANDSTORM."}, +// Returns the rule for which the foul was assigned. +func (foul *Foul) Rule() *Rule { + return GetRuleById(foul.RuleId) } +// Returns the number of points that the foul adds to the opposing alliance's score. func (foul *Foul) PointValue() int { - if foul.IsTechnical { + if foul.Rule() == nil { + return 0 + } + if foul.Rule().IsTechnical { return 10 } else { return 3 diff --git a/game/rule.go b/game/rule.go new file mode 100644 index 0000000..7ef608b --- /dev/null +++ b/game/rule.go @@ -0,0 +1,62 @@ +// Copyright 2020 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Model of a game-specific rule. + +package game + +type Rule struct { + Id int + RuleNumber string + IsTechnical bool + IsRankingPoint bool + Description string +} + +// All rules from the 2018 game that carry point penalties. +var rules = []*Rule{ + {1, "S6", false, false, "DRIVE TEAMS may not extend any body part into the CARGO Chute. Momentary encroachment into the Chute is an exception to this rule."}, + {2, "C8", false, false, "Strategies clearly aimed at forcing the opposing ALLIANCE to violate a rule are not in the spirit of FIRST Robotics Competition and not allowed."}, + {3, "G3", true, false, "During the SANDSTORM PERIOD, a ROBOT may not cross the FIELD such that its BUMPERS break the plane defined by their opponent’s CARGO SHIP LINE."}, + {4, "G4", false, false, "ROBOTS may not have greater-than-momentary or repeated control, i.e. exercise greater-than-momentary or repeated influence, of more than one (1) GAME PIECE at a time, either directly or transitively through other objects."}, + {5, "G5", false, true, "A ROBOT may not remove a GAME PIECE from an opponents’ ROCKET/CARGO SHIP."}, + {6, "G7", false, false, "ROBOTS may not intentionally eject GAME PIECES from the FIELD."}, + {7, "G8", false, false, "ROBOTS may not deliberately use GAME PIECES in an attempt to ease or amplify the challenge associated with FIELD elements."}, + {8, "G9", false, false, "No more than one ROBOT may be positioned such that its BUMPERS are completely beyond the opponent’s CARGO SHIP LINE."}, + {9, "G9", true, false, "No more than one ROBOT may be positioned such that its BUMPERS are completely beyond the opponent’s CARGO SHIP LINE."}, + {10, "G10", false, false, "No part of a ROBOT, except its BUMPERS, may be outside its FRAME PERIMETER if its BUMPERS are completely beyond its opponent’s CARGO SHIP LINE."}, + {11, "G10", true, false, "No part of a ROBOT, except its BUMPERS, may be outside its FRAME PERIMETER if its BUMPERS are completely beyond its opponent’s CARGO SHIP LINE."}, + {12, "G12", false, false, "A ROBOT may not break the vertical plane above the ALLIANCE STATION WALL or damage the SANDSTORM."}, + {13, "G13", false, false, "A ROBOT may not contact an opponent ROBOT if that opponent ROBOT’S BUMPERS are fully in their HAB ZONE."}, + {14, "G15", false, false, "DRIVE TEAMS, ROBOTS, and OPERATOR CONSOLES are prohibited from the following actions with regards to interaction with ARENA elements: grabbing, grasping, attaching to, hanging, deforming, becoming entangled, damaging, and repositioning GAME PIECE holders."}, + {15, "G16", false, true, "During Qualification MATCHES, ROBOTS may not contact opponents’ ROCKETS starting at T-minus 20s."}, + {16, "G17", false, false, "Fallen (i.e. tipped over) ROBOTS attempting to right themselves (either by themselves or with assistance from a partner ROBOT) have one ten (10) second grace period in which they may not be contacted by an opponent ROBOT."}, + {17, "G18", false, false, "ROBOTS may not pin an opponent’s ROBOT for more than five (5) seconds."}, + {18, "G18", true, false, "ROBOTS may not pin an opponent’s ROBOT for more than five (5) seconds."}, + {19, "G19", true, false, "Strategies aimed at the destruction or inhibition of ROBOTS via attachment, damage, tipping, or entanglements are not allowed."}, + {20, "G20", true, false, "Initiating deliberate or damaging contact with an opponent ROBOT on or inside the vertical extension of its FRAME PERIMETER, including transitively through a GAME PIECE, is not allowed."}, + {21, "G23", false, false, "BUMPERS must be in the BUMPER ZONE during the MATCH unless a ROBOT is completely in its HAB ZONE or supported by a ROBOT completely in its HAB ZONE."}, + {22, "G24", false, false, "ROBOTS may not extend more than 30 in (~76 cm). beyond their FRAME PERIMETER."}, + {23, "H6", false, false, "During the MATCH, DRIVERS, COACHES, and HUMAN PLAYERS may not contact anything outside the ALLIANCE STATION and TECHNICIANS may not contact anything outside their designated area."}, + {24, "H7", false, false, "During the MATCH, team members may only enter GAME PIECES on to the FIELD through their LOADING STATIONS."}, + {25, "H8", false, false, "During a MATCH, COACHES may not touch GAME PIECES unless for safety purposes."}, + {26, "H9", true, false, "During the SANDSTORM PERIOD, COACHES, DRIVERS, HUMAN PLAYERS, and any part of the OPERATOR CONSOLE may not break the vertical planes defined by the STARTING LINES, unless for safety purposes."}, + {27, "H10", true, false, "During the SANDSTORM PERIOD, COACHES, DRIVERS, and HUMAN PLAYERS may not look over the top of the ALLIANCE WALL down to the FIELD to overcome the effect of the SANDSTORM."}, +} +var ruleMap map[int]*Rule + +// Returns the rule having the given ID, or nil if no such rule exists. +func GetRuleById(id int) *Rule { + return GetAllRules()[id] +} + +// Returns a slice of all defined rules that carry point penalties. +func GetAllRules() map[int]*Rule { + if ruleMap == nil { + ruleMap = make(map[int]*Rule, len(rules)) + for _, rule := range rules { + ruleMap[rule.Id] = rule + } + } + return ruleMap +} diff --git a/game/rule_test.go b/game/rule_test.go new file mode 100644 index 0000000..e7e4734 --- /dev/null +++ b/game/rule_test.go @@ -0,0 +1,24 @@ +// Copyright 2020 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package game + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetRuleById(t *testing.T) { + assert.Nil(t, GetRuleById(0)) + assert.Equal(t, rules[0], GetRuleById(1)) + assert.Equal(t, rules[20], GetRuleById(21)) + assert.Nil(t, GetRuleById(1000)) +} + +func TestGetAllRules(t *testing.T) { + allRules := GetAllRules() + assert.Equal(t, len(rules), len(allRules)) + for _, rule := range rules { + assert.Equal(t, rule, allRules[rule.Id]) + } +} diff --git a/game/score.go b/game/score.go index b4e4c34..d95d9ef 100644 --- a/game/score.go +++ b/game/score.go @@ -96,7 +96,10 @@ func (score *Score) Summarize(opponentFouls []Foul) *ScoreSummary { } else { // Check for the opponent fouls that automatically trigger the ranking point. for _, foul := range opponentFouls { - if foul.IsRankingPoint { + if foul.Rule() == nil { + continue + } + if foul.Rule().IsRankingPoint { summary.CompleteRocket = true break } diff --git a/game/score_test.go b/game/score_test.go index 707485d..64505e9 100644 --- a/game/score_test.go +++ b/game/score_test.go @@ -32,6 +32,10 @@ func TestScoreSummary(t *testing.T) { assert.Equal(t, false, blueSummary.CompleteRocket) assert.Equal(t, true, blueSummary.HabDocking) + // Test invalid foul. + redScore.Fouls[0].RuleId = 0 + assert.Equal(t, 13, blueScore.Summarize(redScore.Fouls).FoulPoints) + // Test rocket completion boundary conditions. assert.Equal(t, true, redScore.Summarize(blueScore.Fouls).CompleteRocket) redScore.RocketFarLeftBays[1] = BayHatch @@ -41,7 +45,7 @@ func TestScoreSummary(t *testing.T) { assert.Equal(t, true, redScore.Summarize(blueScore.Fouls).CompleteRocket) redScore.RocketNearLeftBays[2] = BayHatch assert.Equal(t, false, redScore.Summarize(blueScore.Fouls).CompleteRocket) - redScore.Fouls[1].IsRankingPoint = true + redScore.Fouls[2].RuleId = 15 assert.Equal(t, true, redScore.Summarize(redScore.Fouls).CompleteRocket) // Test hab docking boundary conditions. @@ -119,12 +123,7 @@ func TestScoreEquals(t *testing.T) { 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 + score2.Fouls[0].RuleId = 1 assert.False(t, score1.Equals(score2)) assert.False(t, score2.Equals(score1)) diff --git a/game/test_helpers.go b/game/test_helpers.go index aa474bf..8c6b665 100644 --- a/game/test_helpers.go +++ b/game/test_helpers.go @@ -7,9 +7,9 @@ package game func TestScore1() *Score { fouls := []Foul{ - {Rule{"G18", true, false, ""}, 25, 150}, - {Rule{"G20", true, false, ""}, 1868, 0}, - {Rule{"G22", false, false, ""}, 25, 25.2}, + {18, 25, 150}, + {20, 1868, 0}, + {21, 25, 25.2}, } return &Score{ RobotStartLevels: [3]int{2, 1, 2}, diff --git a/static/js/announcer_display.js b/static/js/announcer_display.js index 71a167a..aa889b9 100644 --- a/static/js/announcer_display.js +++ b/static/js/announcer_display.js @@ -50,11 +50,18 @@ var handleRealtimeScore = function(data) { // Handles a websocket message to populate the final score data. var handleScorePosted = function(data) { + $.each(data.RedFouls, function(i, foul) { + Object.assign(foul, data.RulesViolated[foul.RuleId]); + }); + $.each(data.BlueFouls, function(i, foul) { + Object.assign(foul, data.RulesViolated[foul.RuleId]); + }); + $("#scoreMatchName").text(data.MatchType + " Match " + data.Match.DisplayName); $("#redScoreDetails").html(matchResultTemplate({score: data.RedScoreSummary, fouls: data.RedFouls, - cards: data.RedCards})); + rulesViolated: data.RulesViolated, cards: data.RedCards})); $("#blueScoreDetails").html(matchResultTemplate({score: data.BlueScoreSummary, fouls: data.BlueFouls, - cards: data.BlueCards})); + rulesViolated: data.RulesViolated, cards: data.BlueCards})); $("#matchResult").modal("show"); // Activate tooltips above the foul listings. diff --git a/static/js/match_review.js b/static/js/match_review.js index 0afc095..ea17034 100644 --- a/static/js/match_review.js +++ b/static/js/match_review.js @@ -51,9 +51,7 @@ var renderResults = function(alliance) { if (result.score.Fouls != null) { $.each(result.score.Fouls, function(k, v) { getInputElement(alliance, "Foul" + k + "Team", v.TeamId).prop("checked", true); - getInputElement(alliance, "Foul" + k + "RuleNumber").val(v.RuleNumber); - getInputElement(alliance, "Foul" + k + "IsTechnical").prop("checked", v.IsTechnical); - getInputElement(alliance, "Foul" + k + "IsRankingPoint").prop("checked", v.IsRankingPoint); + getSelectElement(alliance, "Foul" + k + "RuleId").val(v.RuleId); getInputElement(alliance, "Foul" + k + "Time").val(v.TimeInMatchSec); }); } @@ -99,9 +97,7 @@ var updateResults = function(alliance) { result.score.Fouls = []; for (var i = 0; formData[alliance + "Foul" + i + "Time"]; i++) { var prefix = alliance + "Foul" + i; - var foul = {TeamId: parseInt(formData[prefix + "Team"]), RuleNumber: formData[prefix + "RuleNumber"], - IsTechnical: formData[prefix + "IsTechnical"] === "on", - IsRankingPoint: formData[prefix + "IsRankingPoint"] === "on", + var foul = {TeamId: parseInt(formData[prefix + "Team"]), RuleId: parseInt(formData[prefix + "RuleId"]), TimeInMatchSec: parseFloat(formData[prefix + "Time"])}; result.score.Fouls.push(foul); } diff --git a/static/js/referee_panel.js b/static/js/referee_panel.js index 4b4f5d1..e72bc55 100644 --- a/static/js/referee_panel.js +++ b/static/js/referee_panel.js @@ -46,15 +46,13 @@ var clearFoul = function() { // Sends the foul to the server to add it to the list. var commitFoul = function() { websocket.send("addFoul", {Alliance: foulTeamButton.attr("data-alliance"), - TeamId: parseInt(foulTeamButton.attr("data-team")), Rule: foulRuleButton.attr("data-rule"), - IsTechnical: foulRuleButton.attr("data-is-technical") === "true", - IsRankingPoint: foulRuleButton.attr("data-is-ranking-point") === "true"}); + TeamId: parseInt(foulTeamButton.attr("data-team")), RuleId: parseInt(foulRuleButton.attr("data-rule-id"))}); }; // Removes the foul with the given parameters from the list. -var deleteFoul = function(alliance, team, rule, isTechnical, isRankingPoint, timeSec) { - websocket.send("deleteFoul", {Alliance: alliance, TeamId: parseInt(team), Rule: rule, - IsTechnical: isTechnical, IsRankingPoint: isRankingPoint, TimeInMatchSec: timeSec}); +var deleteFoul = function(alliance, team, ruleId, timeSec) { + websocket.send("deleteFoul", {Alliance: alliance, TeamId: parseInt(team), RuleId: parseInt(ruleId), + TimeInMatchSec: timeSec}); }; // Cycles through no card, yellow card, and red card. diff --git a/templates/announcer_display.html b/templates/announcer_display.html index 556ea11..a96a4cb 100644 --- a/templates/announcer_display.html +++ b/templates/announcer_display.html @@ -96,7 +96,9 @@