diff --git a/src/software/ai/hl/stp/play/offense/offense_play_test.py b/src/software/ai/hl/stp/play/offense/offense_play_test.py index 3bc0668499..b865ebe2c2 100644 --- a/src/software/ai/hl/stp/play/offense/offense_play_test.py +++ b/src/software/ai/hl/stp/play/offense/offense_play_test.py @@ -1,5 +1,7 @@ import software.python_bindings as tbots_cpp from proto.play_pb2 import Play, PlayName + +from software.simulated_tests.excessive_dribbling import NeverExcessivelyDribbles from software.simulated_tests.friendly_team_scored import * from software.simulated_tests.ball_enters_region import * from software.simulated_tests.friendly_has_ball_possession import * @@ -76,7 +78,8 @@ def setup(start_point): # Always Validation inv_always_validation_sequence_set = [ - [BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])] + [BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])], + [NeverExcessivelyDribbles()], ] ag_always_validation_sequence_set = [[FriendlyAlwaysHasBallPossession()]] diff --git a/src/software/ai/hl/stp/tactic/dribble/BUILD b/src/software/ai/hl/stp/tactic/dribble/BUILD index 3c5565efb3..67199a889f 100644 --- a/src/software/ai/hl/stp/tactic/dribble/BUILD +++ b/src/software/ai/hl/stp/tactic/dribble/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -60,3 +62,20 @@ cc_test( "//software/world", ], ) + +py_test( + name = "excessive_dribble_test", + srcs = [ + "excessive_dribble_test.py", + ], + # TODO (#2619) Remove tag to run in parallel + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests:speed_threshold_helpers", + "//software/simulated_tests:validation", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/dribble/excessive_dribble_test.py b/src/software/ai/hl/stp/tactic/dribble/excessive_dribble_test.py new file mode 100644 index 0000000000..443cc06825 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/dribble/excessive_dribble_test.py @@ -0,0 +1,184 @@ +import pytest + +import software.python_bindings as tbots_cpp +from software.simulated_tests.excessive_dribbling import ( + NeverExcessivelyDribbles, + EventuallyStartsExcessivelyDribbling, +) +from proto.message_translation.tbots_protobuf import ( + WorldState, + AssignedTacticPlayControlParams, + DribbleTactic, +) +from software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) +from proto.message_translation.tbots_protobuf import create_world_state + + +@pytest.mark.parametrize( + "initial_location,dribble_destination,final_dribble_orientation, should_excessively_dribble, blue_robot_location", + [ + # The following tests check that DribbleTactic does not false trigger for distances within regulations + # Dribble Destination for the ball < 1.0 from its starting position + ( + tbots_cpp.Point(0.5, 0), + tbots_cpp.Point(1.02, 0), + tbots_cpp.Angle(), + False, + tbots_cpp.Point(0, 1), + ), + # Dribble Testing diagonally + ( + tbots_cpp.Point(0.25, 0.25), + tbots_cpp.Point(0.80, 0.50), + tbots_cpp.Angle.fromRadians(50), + False, + tbots_cpp.Point(0, 1), + ), + # Boundary Testing, because of the autoref implementation (initial of position Bot to final of Ball), + # a conservative max dribble distance (0.95 m) is used + # Test vertical dribbling + ( + tbots_cpp.Point(0.01, 0), + tbots_cpp.Point(0.96, 0), + tbots_cpp.Angle(), + False, + tbots_cpp.Point(0, 1), + ), + # Test horizontal dribbling + ( + tbots_cpp.Point(1, 1.5), + tbots_cpp.Point(1.95, 1.5), + tbots_cpp.Angle(), + False, + tbots_cpp.Point(0, 1), + ), + # Test bot and ball in same position + ( + tbots_cpp.Point(0, 1), + tbots_cpp.Point(0.95, 1), + tbots_cpp.Angle(), + False, + tbots_cpp.Point(0, 1), + ), + # The following tests check that DribbleTactic correctly triggers for distances outside regulation + # Dribble Destination for the ball > 1.0 from its starting position + ( + tbots_cpp.Point(0, 2), + tbots_cpp.Point(0, 0.5), + tbots_cpp.Angle(), + True, + tbots_cpp.Point(0, 0), + ), + # Dribble Testing diagonally + ( + tbots_cpp.Point(0.1, 1.1), + tbots_cpp.Point(1.1, 0.1), + tbots_cpp.Angle.fromRadians(50), + True, + tbots_cpp.Point(0, 0), + ), + # Boundary Testing, due to the conservative implementation a dribble distance of 1 m should fail + # Test Vertical Dribbling + ( + tbots_cpp.Point(0, 1), + tbots_cpp.Point(0, 2), + tbots_cpp.Angle(), + True, + tbots_cpp.Point(0, 0), + ), + # Test Horizontal Dribbling + ( + tbots_cpp.Point(1, 2), + tbots_cpp.Point(0, 2), + tbots_cpp.Angle(), + True, + tbots_cpp.Point(0, 0), + ), + # Test Diagonal Dribbling + ( + tbots_cpp.Point(0, 1), + tbots_cpp.Point(0.6, 1.8), + tbots_cpp.Angle(), + True, + tbots_cpp.Point(0, 0), + ), + # Test robot and ball at same position (affects dribbling orientation and therefore perceived dribble distance) + ( + tbots_cpp.Point(0, 0), + tbots_cpp.Point(0, 1), + tbots_cpp.Angle(), + True, + tbots_cpp.Point(0, 0), + ), + ( + tbots_cpp.Point(0.0, 0.01), + tbots_cpp.Point(0.81, 0.61), + tbots_cpp.Angle(), + True, + tbots_cpp.Point(0, 0), + ), + ], +) +def test_excessive_dribbling( + initial_location, + dribble_destination, + final_dribble_orientation, + should_excessively_dribble, + simulated_test_runner, + blue_robot_location, +): + if should_excessively_dribble: + # Always and Eventually validation sets for excessive dribbling + always_validation_sequence_set = [[]] + eventually_validation_sequence_set = [[EventuallyStartsExcessivelyDribbling()]] + else: + # Always and Eventually validation sets for not excessive dribbling + always_validation_sequence_set = [[NeverExcessivelyDribbles()]] + eventually_validation_sequence_set = [[]] + + blue_robot_locations = [blue_robot_location] + + simulated_test_runner.simulator_proto_unix_io.send_proto( + WorldState, + create_world_state( + [], + blue_robot_locations=blue_robot_locations, + ball_location=initial_location, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + # Setup Tactic + params = AssignedTacticPlayControlParams() + params.assigned_tactics[0].dribble.CopyFrom( + DribbleTactic( + dribble_destination=tbots_cpp.createPointProto(dribble_destination), + final_dribble_orientation=tbots_cpp.createAngleProto( + final_dribble_orientation + ), + allow_excessive_dribbling=True, + ) + ) + + simulated_test_runner.blue_full_system_proto_unix_io.send_proto( + AssignedTacticPlayControlParams, params + ) + + # Setup no tactics on the enemy side + params = AssignedTacticPlayControlParams() + simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( + AssignedTacticPlayControlParams, params + ) + + simulated_test_runner.run_test( + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + inv_always_validation_sequence_set=always_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_always_validation_sequence_set=always_validation_sequence_set, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/simulated_tests/excessive_dribbling.py b/src/software/simulated_tests/excessive_dribbling.py index d310ba1346..b62045bb7c 100644 --- a/src/software/simulated_tests/excessive_dribbling.py +++ b/src/software/simulated_tests/excessive_dribbling.py @@ -1,5 +1,5 @@ import software.python_bindings as tbots_cpp -from proto.import_all_protos import * +from proto.import_all_protos import ValidationStatus, ValidationGeometry from software.simulated_tests.validation import ( Validation, @@ -13,11 +13,15 @@ class ExcessivelyDribbling(Validation): """Checks if any friendly robot is excessively dribbling the ball, i.e. for over 1m.""" def __init__(self): - self.continous_dribbling_start_point = None + self.continuous_dribbling_start_point = None + self.dribbler_tolerance = 0.05 + self.max_dribbling_displacement = 1.00 + self.dribbling_error_margin = 0.05 @override def get_validation_status(self, world) -> ValidationStatus: - """Checks if any friendly robot is excessively dribbling the ball, i.e. for over 1m. + """Checks if any friendly robot is excessively dribbling the ball past the max dribble displacement + minus the dribbling error margin :param world: The world msg to validate :return: FAILING when the robot is excessively dribbling @@ -25,30 +29,45 @@ def get_validation_status(self, world) -> ValidationStatus: """ ball_position = tbots_cpp.createPoint(world.ball.current_state.global_position) for robot in world.friendly_team.team_robots: - if not tbots_cpp.Robot(robot).isNearDribbler(ball_position, 0.01): - # if ball is not near dribbler then de-activate this validation - self.continous_dribbling_start_point = None - elif ( - ball_position - (self.continous_dribbling_start_point or ball_position) - ).length() > 1.0: - return ValidationStatus.FAILING - elif self.continous_dribbling_start_point is None: - # ball is in dribbler, but previously wasn't in dribbler, so set continuous dribbling start point - self.continous_dribbling_start_point = ball_position + if tbots_cpp.Robot(robot).isNearDribbler( + ball_position, self.dribbler_tolerance + ): + if self.continuous_dribbling_start_point is None: + # Set the dribbling validation start point to the current ball position + self.continuous_dribbling_start_point = ball_position + elif ( + ball_position - self.continuous_dribbling_start_point + ).length() > ( + self.max_dribbling_displacement - self.dribbling_error_margin + ): + return ValidationStatus.FAILING + return ValidationStatus.PASSING + + # Reset the dribbling validation start point if no robots are near the ball + self.continuous_dribbling_start_point = None return ValidationStatus.PASSING @override def get_validation_geometry(self, world) -> ValidationGeometry: """(override) Shows the max allowed dribbling circle""" return create_validation_geometry( - [tbots_cpp.Circle(self.continous_dribbling_start_point, 1.0)] - if self.continous_dribbling_start_point is not None + [ + tbots_cpp.Circle( + self.continuous_dribbling_start_point, + self.max_dribbling_displacement, + ), + tbots_cpp.Circle( + self.continuous_dribbling_start_point, + self.max_dribbling_displacement - self.dribbling_error_margin, + ), + ] + if self.continuous_dribbling_start_point is not None else [] ) @override def __repr__(self): - return "Check that the dribbling robot has not dribbled for more than 1m" + return f"Check that the dribbling robot has not dribbled for more than {self.max_dribbling_displacement} m minus error margin ({self.dribbling_error_margin} m)" (