diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000000..4c66e31a57 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,32 @@ +Found using: `rg SimulatedErForceSimPlayTestFixture` and manual sorting + +TACTIC TESTS TO PORT + +src/software/ai/hl/stp/tactic/pivot_kick/pivot_kick_tactic_test.cpp (DONE 99% ACCURACY) +src/software/ai/hl/stp/tactic/crease_defender/crease_defender_tactic_test.cpp (DONE 99% ACCURACY + ADDITIONAL) +src/software/ai/hl/stp/tactic/halt/halt_tactic_test.cpp (DONE 99% ACCURACY) +src/software/ai/hl/stp/tactic/move/move_tactic_test.cpp (DONE 99%) +src/software/ai/hl/stp/tactic/kick/kick_tactic_test.cpp (DONE 99%) +src/software/ai/hl/stp/tactic/penalty_kick/penalty_kick_tactic_test.cpp (DISABLED) +src/software/ai/hl/stp/tactic/chip/chip_tactic_test.cpp (DONE 99%) +src/software/ai/hl/stp/tactic/dribble/dribble_tactic_test.cpp (DONE 98% need to implement thing) +src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.cpp (DONE 99%) +src/software/ai/hl/stp/tactic/attacker/attacker_tactic_passing_test.cpp (DONE 98% need to implement thing) +src/software/ai/hl/stp/tactic/attacker/attacker_tactic_keep_away_test.cpp (VERY COMPLEX VALIDATIONS) +src/software/ai/hl/stp/tactic/attacker/attacker_tactic_shoot_goal_test.cpp (DONE 98% need to implement thing) +src/software/ai/hl/stp/tactic/dribble/dribble_tactic_push_enemy_test.cpp (DONE 99%) +src/software/ai/hl/stp/tactic/goalie/goalie_tactic_chip_test.cpp + +PLAY TESTS TO PORT + +src/software/ai/hl/stp/play/kickoff_friendly_play_test.cpp +src/software/ai/hl/stp/play/kickoff_enemy_play_test.cpp +src/software/ai/hl/stp/play/stop_play_test.cpp +src/software/ai/hl/stp/play/free_kick/free_kick_play_test.cpp +src/software/ai/hl/stp/play/shoot_or_chip_play_test.cpp +src/software/ai/hl/stp/play/crease_defense/crease_defense_play_test.cpp +src/software/ai/hl/stp/play/shoot_or_pass/shoot_or_pass_play_test.cpp +src/software/ai/hl/stp/play/example/example_play_test.cpp + +src/software/ai/hl/stp/play/penalty_kick_enemy/penalty_kick_enemy_play_test.cpp (GRAYSONS PR PERHAPS?) +src/software/ai/hl/stp/play/penalty_kick/penalty_kick_play_test.cpp (GRAYSONS PR) diff --git a/src/proto/message_translation/tbots_protobuf.py b/src/proto/message_translation/tbots_protobuf.py index a573922f5c..a9f980786e 100644 --- a/src/proto/message_translation/tbots_protobuf.py +++ b/src/proto/message_translation/tbots_protobuf.py @@ -11,7 +11,8 @@ def create_world_state( blue_robot_locations: list[tbots_cpp.Point], ball_location: tbots_cpp.Point, ball_velocity: tbots_cpp.Vector, - blue_robot_orientations: list[float] = [], + blue_robot_orientations: list[tbots_cpp.Angle] = [], + blue_robot_velocities: list[tbots_cpp.Vector] = [], ) -> WorldState: """Initializes the world from a list of robot locations and ball location/velocity. @@ -22,31 +23,41 @@ def create_world_state( :param ball_location: Location of the ball :param ball_velocity: Velocity of the ball :param blue_robot_orientations: A list of blue robots orientations + :param blue_robot_velocities: A list of blue robots velocities """ world_state = WorldState() + # TODO (#2558): add param for robot angular velocity since its used for some tests + for robot_id, robot_location in enumerate(yellow_robot_locations): world_state.yellow_robots[robot_id].CopyFrom( RobotState( global_position=Point( x_meters=robot_location.x(), y_meters=robot_location.y() ), + # Initialize angle of yellow robots the same cause its specified in any tests (yet) global_orientation=Angle(radians=math.pi), ) ) for robot_id, robot_location in enumerate(blue_robot_locations): - orientation = 0 try: - orientation = blue_robot_orientations[robot_id] + orientation = blue_robot_orientations[robot_id].toRadians() except IndexError: - pass + orientation = 0 + + try: + velocity = blue_robot_velocities[robot_id] + except IndexError: + velocity = tbots_cpp.Vector(0, 0) + world_state.blue_robots[robot_id].CopyFrom( RobotState( global_position=Point( x_meters=robot_location.x(), y_meters=robot_location.y() ), global_orientation=Angle(radians=orientation), + global_velocity=tbots_cpp.createVectorProto(velocity), ) ) diff --git a/src/software/ai/hl/stp/play/BUILD b/src/software/ai/hl/stp/play/BUILD index db672cbc01..a3deeb5167 100644 --- a/src/software/ai/hl/stp/play/BUILD +++ b/src/software/ai/hl/stp/play/BUILD @@ -169,7 +169,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) @@ -214,7 +214,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) @@ -270,7 +270,7 @@ py_test( "//proto:software_py_proto", "//proto:tbots_py_proto", "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/ball_placement/BUILD b/src/software/ai/hl/stp/play/ball_placement/BUILD index c59998362e..219650ea12 100644 --- a/src/software/ai/hl/stp/play/ball_placement/BUILD +++ b/src/software/ai/hl/stp/play/ball_placement/BUILD @@ -40,7 +40,7 @@ py_test( tags = ["exclusive"], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py b/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py index eba1625288..fbb09deebc 100644 --- a/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py +++ b/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py @@ -1,16 +1,74 @@ import pytest - import software.python_bindings as tbots_cpp from software.py_constants import ENEMY_BALL_PLACEMENT_DISTANCE_METERS -from proto.play_pb2 import Play, PlayName -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.ball_stops_in_region import * -from software.simulated_tests.robot_enters_region import * + +from proto.import_all_protos import * +from proto.ssl_gc_common_pb2 import Team +from proto.message_translation.tbots_protobuf import create_world_state +from software.simulated_tests.pytest_validations.ball_enters_region import ( + BallAlwaysStaysInRegion, + BallEventuallyEntersRegion, +) +from software.simulated_tests.pytest_validations.robot_enters_region import ( + RobotEventuallyExitsRegion, +) from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) -from proto.message_translation.tbots_protobuf import create_world_state -from proto.ssl_gc_common_pb2 import Team + + +@pytest.mark.parametrize( + "ball_start_point, ball_placement_point", + [ + # test normal ball placement (not edge case) + (tbots_cpp.Point(2, 2), tbots_cpp.Point(0, 1.5)), + # test when ball starting point is outside of the goal line + (tbots_cpp.Point(-4.7, 2.0), tbots_cpp.Point(0, 0.5)), + # test when ball starting point is outside of the side lines + (tbots_cpp.Point(-2.0, 3.2), tbots_cpp.Point(0, -0.5)), + # test when ball starting point is inside of enemy net + (tbots_cpp.Point(4.0, 0), tbots_cpp.Point(2.0, 2.0)), + # test when ball starting point is in friendly net + (tbots_cpp.Point(-4.0, 0), tbots_cpp.Point(-2.0, 2.0)), + ], +) +def test_two_ai_ball_placement( + simulated_test_runner, ball_start_point, ball_placement_point +): + run_ball_placement_scenario( + simulated_test_runner, ball_start_point, ball_placement_point + ) + + +@pytest.mark.parametrize( + "ball_start_point, ball_placement_point", + [ + # TODO: Remove these ball placement tests until #3561 is resolved + # # 2023 RoboCup ball placement scenarios + # # Scenario 1 + # (tbots_cpp.Point(-0.2, -2.8), tbots_cpp.Point(-0.2, 2.8)), + # # Scenario 2 + # (tbots_cpp.Point(-3.5, -2.25), tbots_cpp.Point(0, 0)), + # # Scenario 3 + # (tbots_cpp.Point(-1.5, -2.25), tbots_cpp.Point(-0.2, -2.8)), + # # Scenario 4 + # (tbots_cpp.Point(-4.4, -2.9), tbots_cpp.Point(-0.2, 2.8)), + # # Scenario 5 + # (tbots_cpp.Point(-0.5, -0), tbots_cpp.Point(-4.3, 2.8)), + # # Scenario 6 + # (tbots_cpp.Point(-1, -3.15), tbots_cpp.Point(-3.5, -2.8)), + # # Scenario 7 + # (tbots_cpp.Point(-1, 3.15), tbots_cpp.Point(-3.5, 2.8)), + # # Scenario 8 + # (tbots_cpp.Point(-4.45, -0.1), tbots_cpp.Point(-0.5, 2.8)), + ], +) +def test_robocup_technical_challenge_placement( + simulated_test_runner, ball_start_point, ball_placement_point +): + run_ball_placement_scenario( + simulated_test_runner, ball_start_point, ball_placement_point, blue_only=True + ) def ball_placement_play_setup( @@ -54,12 +112,22 @@ def ball_placement_play_setup( .negXPosYCorner(), ] + # Create world state + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_start_point, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) # Pass in placement point here - not required for all play tests - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.BALL_PLACEMENT, team=Team.BLUE, final_ball_placement_point=ball_placement_point, @@ -72,76 +140,9 @@ def ball_placement_play_setup( yellow_play = Play() yellow_play.name = PlayName.HaltPlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) + simulated_test_runner.set_play(blue_play, is_friendly=True) if not blue_only: - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - Play, yellow_play - ) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_start_point, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) - - -@pytest.mark.parametrize( - "ball_start_point, ball_placement_point", - [ - # test normal ball placement (not edge case) - (tbots_cpp.Point(2, 2), tbots_cpp.Point(0, 1.5)), - # test when ball starting point is outside of the goal line - (tbots_cpp.Point(-4.7, 2.0), tbots_cpp.Point(0, 0.5)), - # test when ball starting point is outside of the side lines - (tbots_cpp.Point(-2.0, 3.2), tbots_cpp.Point(0, -0.5)), - # test when ball starting point is inside of enemy net - (tbots_cpp.Point(4.0, 0), tbots_cpp.Point(2.0, 2.0)), - # test when ball starting point is in friendly net - (tbots_cpp.Point(-4.0, 0), tbots_cpp.Point(-2.0, 2.0)), - ], -) -def test_two_ai_ball_placement( - simulated_test_runner, ball_start_point, ball_placement_point -): - run_ball_placement_scenario( - simulated_test_runner, ball_start_point, ball_placement_point - ) - - -@pytest.mark.parametrize( - "ball_start_point, ball_placement_point", - [ - # TODO: Remove these ball placement tests until #3561 is resolved - # # 2023 RoboCup ball placement scenarios - # # Scenario 1 - # (tbots_cpp.Point(-0.2, -2.8), tbots_cpp.Point(-0.2, 2.8)), - # # Scenario 2 - # (tbots_cpp.Point(-3.5, -2.25), tbots_cpp.Point(0, 0)), - # # Scenario 3 - # (tbots_cpp.Point(-1.5, -2.25), tbots_cpp.Point(-0.2, -2.8)), - # # Scenario 4 - # (tbots_cpp.Point(-4.4, -2.9), tbots_cpp.Point(-0.2, 2.8)), - # # Scenario 5 - # (tbots_cpp.Point(-0.5, -0), tbots_cpp.Point(-4.3, 2.8)), - # # Scenario 6 - # (tbots_cpp.Point(-1, -3.15), tbots_cpp.Point(-3.5, -2.8)), - # # Scenario 7 - # (tbots_cpp.Point(-1, 3.15), tbots_cpp.Point(-3.5, 2.8)), - # # Scenario 8 - # (tbots_cpp.Point(-4.45, -0.1), tbots_cpp.Point(-0.5, 2.8)), - ], -) -def test_robocup_technical_challenge_placement( - simulated_test_runner, ball_start_point, ball_placement_point -): - run_ball_placement_scenario( - simulated_test_runner, ball_start_point, ball_placement_point, blue_only=True - ) + simulated_test_runner.set_play(yellow_play, is_friendly=False) def run_ball_placement_scenario( @@ -150,8 +151,8 @@ def run_ball_placement_scenario( """Runs a ball placement test scenario with the specified parameters. :param simulated_test_runner: The test runner used to simulate robot and ball behavior. - :param ball_start_point: The initial position of the ball (provided by pytest parameterization). - :param ball_placement_point: The target position where the ball should be placed (provided by pytest parameterization). + :param ball_start_point: The initial position of the ball. + :param ball_placement_point: The target position where the ball should be placed. :param blue_only: If True, only the blue team is active; the yellow team is ignored. """ # Placement Eventually Validation diff --git a/src/software/ai/hl/stp/play/crease_defense/BUILD b/src/software/ai/hl/stp/play/crease_defense/BUILD index f166ec7122..bb6402127a 100644 --- a/src/software/ai/hl/stp/play/crease_defense/BUILD +++ b/src/software/ai/hl/stp/play/crease_defense/BUILD @@ -39,7 +39,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/crease_defense/crease_defense_play_test.py b/src/software/ai/hl/stp/play/crease_defense/crease_defense_play_test.py index 54c258ba29..d3ae9e561c 100644 --- a/src/software/ai/hl/stp/play/crease_defense/crease_defense_play_test.py +++ b/src/software/ai/hl/stp/play/crease_defense/crease_defense_play_test.py @@ -9,68 +9,64 @@ def test_crease_defense_play(simulated_test_runner): - # starting point must be Point - ball_initial_pos = tbots_cpp.Point(0.9, 2.85) - # placement point must be Vector2 to work with game controller - tbots_cpp.Point(-3, -2) + def setup(*args): + ball_initial_pos = tbots_cpp.Point(0.9, 2.85) - # Setup Bots - blue_bots = [ - tbots_cpp.Point(-4.5, 0), - tbots_cpp.Point(-3, 1.5), - tbots_cpp.Point(-3, 0.5), - tbots_cpp.Point(-3, -0.5), - tbots_cpp.Point(-3, -1.5), - tbots_cpp.Point(-3, -3.0), - ] + blue_bots = [ + tbots_cpp.Point(-4.5, 0), + tbots_cpp.Point(-3, 1.5), + tbots_cpp.Point(-3, 0.5), + tbots_cpp.Point(-3, -0.5), + tbots_cpp.Point(-3, -1.5), + tbots_cpp.Point(-3, -3.0), + ] - yellow_bots = [ - tbots_cpp.Point(1, 3), - tbots_cpp.Point(1, -0.25), - tbots_cpp.Point(1, -1.25), - tbots_cpp.Field.createSSLDivisionBField().enemyGoalCenter(), - tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXNegYCorner(), - tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXPosYCorner(), - ] + yellow_bots = [ + tbots_cpp.Point(1, 3), + tbots_cpp.Point(1, -0.25), + tbots_cpp.Point(1, -1.25), + tbots_cpp.Field.createSSLDivisionBField().enemyGoalCenter(), + tbots_cpp.Field.createSSLDivisionBField() + .enemyDefenseArea() + .negXNegYCorner(), + tbots_cpp.Field.createSSLDivisionBField() + .enemyDefenseArea() + .negXPosYCorner(), + ] - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.STOP, team=Team.UNKNOWN - ) - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.FORCE_START, team=Team.BLUE - ) + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) - # Force play override here - blue_play = Play() - blue_play.name = PlayName.CreaseDefensePlay + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.STOP, team=Team.UNKNOWN + ) + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.FORCE_START, team=Team.BLUE + ) - yellow_play = Play() - yellow_play.name = PlayName.HaltPlay + blue_play = Play() + blue_play.name = PlayName.CreaseDefensePlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto(Play, yellow_play) + yellow_play = Play() + yellow_play.name = PlayName.HaltPlay - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) + simulated_test_runner.set_play(yellow_play, is_friendly=False) - # Always Validation - # TODO- #2778 Validation + # TODO (#2778): actually add validations always_validation_sequence_set = [[]] - # Eventually Validation - # TODO- #2778 Validation + # TODO (#2778): actually add validations eventually_validation_sequence_set = [[]] simulated_test_runner.run_test( + setup=setup, inv_eventually_validation_sequence_set=eventually_validation_sequence_set, inv_always_validation_sequence_set=always_validation_sequence_set, test_timeout_s=25, diff --git a/src/software/ai/hl/stp/play/defense/BUILD b/src/software/ai/hl/stp/play/defense/BUILD index ea875f597c..012f16b25c 100644 --- a/src/software/ai/hl/stp/play/defense/BUILD +++ b/src/software/ai/hl/stp/play/defense/BUILD @@ -42,7 +42,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/defense/defense_play_test.py b/src/software/ai/hl/stp/play/defense/defense_play_test.py index b17da5777c..03ec7724a5 100644 --- a/src/software/ai/hl/stp/play/defense/defense_play_test.py +++ b/src/software/ai/hl/stp/play/defense/defense_play_test.py @@ -2,8 +2,8 @@ import software.python_bindings as tbots_cpp from proto.play_pb2 import Play, PlayName -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.friendly_has_ball_possession import ( +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.friendly_has_ball_possession import ( FriendlyEventuallyHasBallPossession, ) from proto.message_translation.tbots_protobuf import create_world_state @@ -37,40 +37,30 @@ ) def test_defense_play_ball_steal(simulated_test_runner, blue_bots, yellow_bots): def setup(*args): - # Starting point must be Point ball_initial_pos = tbots_cpp.Point(0.93, 0) - # Placement point must be Vector2 to work with game controller - tbots_cpp.Point(-3, -2) - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.FORCE_START, team=Team.BLUE ) - # Force play override here blue_play = Play() blue_play.name = PlayName.DefensePlay - params = AssignedTacticPlayControlParams() - - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - AssignedTacticPlayControlParams, params - ) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + # TODO: fix having to set tactics first since it sets both teams + simulated_test_runner.set_tactics() + simulated_test_runner.set_play(blue_play, is_friendly=True) simulated_test_runner.run_test( setup=setup, @@ -115,41 +105,32 @@ def setup(*args): ) def test_defense_play(simulated_test_runner, blue_bots, yellow_bots): def setup(*args): - # Starting point must be Point ball_initial_pos = tbots_cpp.Point(0.9, 2.85) - # Placement point must be Vector2 to work with game controller - tbots_cpp.Point(-3, -2) - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.FORCE_START, team=Team.BLUE ) - # Force play override here blue_play = Play() blue_play.name = PlayName.DefensePlay yellow_play = Play() yellow_play.name = PlayName.ShootOrPassPlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - Play, yellow_play - ) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) + simulated_test_runner.set_play(yellow_play, is_friendly=False) simulated_test_runner.run_test( setup=setup, diff --git a/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD b/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD index e8a02bfdc6..c76845ca98 100644 --- a/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD +++ b/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD @@ -31,7 +31,7 @@ py_test( tags = ["exclusive"], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/enemy_ball_placement/enemy_ball_placement_play_test.py b/src/software/ai/hl/stp/play/enemy_ball_placement/enemy_ball_placement_play_test.py index 815582f104..01cb4f16cd 100644 --- a/src/software/ai/hl/stp/play/enemy_ball_placement/enemy_ball_placement_play_test.py +++ b/src/software/ai/hl/stp/play/enemy_ball_placement/enemy_ball_placement_play_test.py @@ -2,7 +2,7 @@ import software.python_bindings as tbots_cpp from proto.play_pb2 import Play, PlayName -from software.simulated_tests.robot_enters_placement_region import * +from software.simulated_tests.pytest_validations.robot_enters_placement_region import * from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) @@ -23,7 +23,6 @@ def test_two_ai_ball_placement( simulated_test_runner, ball_start_point, ball_placement_point ): def setup(*args): - # Setup Bots blue_bots = [ tbots_cpp.Point(-4.5, 0), tbots_cpp.Point(-4, 0.5), @@ -46,40 +45,33 @@ def setup(*args): .negXPosYCorner(), ] - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_start_point, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - # Pass in placement point here - not required for all play tests - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.BALL_PLACEMENT, team=Team.YELLOW, final_ball_placement_point=ball_placement_point, ) - # Force play override here blue_play = Play() blue_play.name = PlayName.EnemyBallPlacementPlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) + simulated_test_runner.set_play(blue_play, is_friendly=True) yellow_play = Play() yellow_play.name = PlayName.BallPlacementPlay - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - Play, yellow_play - ) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_start_point, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + simulated_test_runner.set_play(yellow_play, is_friendly=False) always_validation_sequence_set = [ [RobotNeverEntersPlacementRegion(ball_placement_point)] diff --git a/src/software/ai/hl/stp/play/enemy_free_kick/BUILD b/src/software/ai/hl/stp/play/enemy_free_kick/BUILD index 2e6193787a..c538123c6e 100644 --- a/src/software/ai/hl/stp/play/enemy_free_kick/BUILD +++ b/src/software/ai/hl/stp/play/enemy_free_kick/BUILD @@ -45,7 +45,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/enemy_free_kick/enemy_free_kick_play_test.py b/src/software/ai/hl/stp/play/enemy_free_kick/enemy_free_kick_play_test.py index b296df1b45..ccab4d5d37 100644 --- a/src/software/ai/hl/stp/play/enemy_free_kick/enemy_free_kick_play_test.py +++ b/src/software/ai/hl/stp/play/enemy_free_kick/enemy_free_kick_play_test.py @@ -3,14 +3,11 @@ import software.python_bindings as tbots_cpp from proto.play_pb2 import Play, PlayName -from software.simulated_tests.or_validation import OrValidation +from software.simulated_tests.pytest_validations.or_validation import OrValidation -from software.simulated_tests.ball_moves_from_rest import ( - BallEventuallyMovesFromRest, -) -from software.simulated_tests.friendly_team_scored import * -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.robot_enters_region import ( +from software.simulated_tests.pytest_validations.friendly_team_scored import * +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.robot_enters_region import ( RobotEventuallyEntersRegion, RobotNeverEntersRegion, ) @@ -102,38 +99,31 @@ def test_enemy_free_kick_play( simulated_test_runner, blue_bots, yellow_bots, ball_initial_pos ): - # Setup Bots def setup(*args): - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.DIRECT, team=Team.YELLOW ) - # Force play override here blue_play = Play() blue_play.name = PlayName.EnemyFreeKickPlay yellow_play = Play() yellow_play.name = PlayName.FreeKickPlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - Play, yellow_play - ) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) + simulated_test_runner.set_play(yellow_play, is_friendly=False) # Always Validation always_validation_sequence_set = [ @@ -143,11 +133,14 @@ def setup(*args): RobotNeverEntersRegion( regions=[tbots_cpp.Circle(ball_initial_pos, 0.05)] ), - BallEventuallyMovesFromRest(position=ball_initial_pos), + BallEventuallyExitsRegion( + regions=[tbots_cpp.Circle(ball_initial_pos, 0.05)] + ), ] ) ] ] + # Eventually Validation eventually_validation_sequence_set = [ [RobotEventuallyEntersRegion(regions=[tbots_cpp.Circle(ball_initial_pos, 1)])] diff --git a/src/software/ai/hl/stp/play/example/BUILD b/src/software/ai/hl/stp/play/example/BUILD index faa3db67d5..b3ff15e029 100644 --- a/src/software/ai/hl/stp/play/example/BUILD +++ b/src/software/ai/hl/stp/play/example/BUILD @@ -56,8 +56,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:speed_threshold_helpers", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/example/example_play_test.py b/src/software/ai/hl/stp/play/example/example_play_test.py index e17a8da9b3..20ab8c1e07 100644 --- a/src/software/ai/hl/stp/play/example/example_play_test.py +++ b/src/software/ai/hl/stp/play/example/example_play_test.py @@ -1,9 +1,9 @@ import software.python_bindings as tbots_cpp -from software.simulated_tests.robot_enters_region import ( +from software.simulated_tests.pytest_validations.robot_enters_region import ( NumberOfRobotsEventuallyExitsRegion, NumberOfRobotsEventuallyEntersRegion, ) -from software.simulated_tests.robot_speed_threshold import * +from software.simulated_tests.pytest_validations.robot_speed_threshold import * from proto.message_translation.tbots_protobuf import create_world_state from proto.ssl_gc_common_pb2 import Team from proto.play_pb2 import Play, PlayName @@ -16,7 +16,6 @@ def test_example_play(simulated_test_runner): ball_initial_pos = tbots_cpp.Point(0, 0) def setup(*args): - # Setup Bots blue_bots = [ tbots_cpp.Point(-3, 2.5), tbots_cpp.Point(-3, 1.5), @@ -39,40 +38,34 @@ def setup(*args): .negXPosYCorner(), ] - # Force play override here + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + blue_play = Play() blue_play.name = PlayName.ExamplePlay yellow_play = Play() yellow_play.name = PlayName.HaltPlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - Play, yellow_play - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) + simulated_test_runner.set_play(yellow_play, is_friendly=False) - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.NORMAL_START, team=Team.BLUE ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.DIRECT, team=Team.BLUE ) - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) - # params just have to be a list of length 1 to ensure the test runs at least once simulated_test_runner.run_test( setup=setup, diff --git a/src/software/ai/hl/stp/play/free_kick/BUILD b/src/software/ai/hl/stp/play/free_kick/BUILD index df9375a693..88815024af 100644 --- a/src/software/ai/hl/stp/play/free_kick/BUILD +++ b/src/software/ai/hl/stp/play/free_kick/BUILD @@ -54,7 +54,7 @@ py_test( tags = ["exclusive"], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/free_kick/free_kick_play_test.py b/src/software/ai/hl/stp/play/free_kick/free_kick_play_test.py index 4e24bc392c..5187df630b 100644 --- a/src/software/ai/hl/stp/play/free_kick/free_kick_play_test.py +++ b/src/software/ai/hl/stp/play/free_kick/free_kick_play_test.py @@ -2,8 +2,8 @@ import software.python_bindings as tbots_cpp from proto.play_pb2 import Play, PlayName -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.friendly_team_scored import * +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.friendly_team_scored import * from proto.message_translation.tbots_protobuf import create_world_state from proto.ssl_gc_common_pb2 import Team from software.simulated_tests.simulated_test_fixture import ( @@ -14,45 +14,33 @@ def free_kick_play_setup( blue_bots, yellow_bots, ball_initial_pos, play_name, simulated_test_runner ): - """Sets up the free kick play test + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) - :param blue_bots: positions of blue robots - :param yellow_bots: positions of yellow robots - :param ball_initial_pos: initial position of the ball - :param play_name: current play being used for blue robots - :param simulated_test_runner: the current test runner - """ - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.NORMAL_START, team=Team.BLUE ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.DIRECT, team=Team.BLUE ) - # Force play override here blue_play = Play() blue_play.name = play_name yellow_play = Play() yellow_play.name = PlayName.HaltPlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto(Play, yellow_play) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) + simulated_test_runner.set_play(yellow_play, is_friendly=False) # We want to test friendly half, enemy half, and at the border of the field diff --git a/src/software/ai/hl/stp/play/halt_play/BUILD b/src/software/ai/hl/stp/play/halt_play/BUILD index a491d9dcb6..d76695e405 100644 --- a/src/software/ai/hl/stp/play/halt_play/BUILD +++ b/src/software/ai/hl/stp/play/halt_play/BUILD @@ -43,8 +43,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:speed_threshold_helpers", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/play/halt_play/halt_play_test.py b/src/software/ai/hl/stp/play/halt_play/halt_play_test.py index 830f88f5f2..a6ce7d2f5c 100644 --- a/src/software/ai/hl/stp/play/halt_play/halt_play_test.py +++ b/src/software/ai/hl/stp/play/halt_play/halt_play_test.py @@ -1,5 +1,5 @@ import software.python_bindings as tbots_cpp -from software.simulated_tests.robot_speed_threshold import * +from software.simulated_tests.pytest_validations.robot_speed_threshold import * from proto.message_translation.tbots_protobuf import create_world_state from proto.ssl_gc_common_pb2 import Team from software.simulated_tests.simulated_test_fixture import ( @@ -11,10 +11,8 @@ # @pytest.mark.parametrize("run_enemy_ai,test_duration", [(False, 20), (True, 20)]) def test_halt_play(simulated_test_runner): def setup(*args): - # starting point must be Point ball_initial_pos = tbots_cpp.Point(0, 0) - # Setup Bots blue_bots = [ tbots_cpp.Point(-3, 2.5), tbots_cpp.Point(-3, 1.5), @@ -37,20 +35,7 @@ def setup(*args): .negXPosYCorner(), ] - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.STOP, team=Team.UNKNOWN - ) - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.FORCE_START, team=Team.UNKNOWN - ) - - # No plays to override. AI does whatever for 3 seconds before HALT CMD - # is issued - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, + simulated_test_runner.set_world_state( create_world_state( yellow_robot_locations=yellow_bots, blue_robot_locations=blue_bots, @@ -59,6 +44,13 @@ def setup(*args): ), ) + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.STOP, team=Team.UNKNOWN + ) + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.FORCE_START, team=Team.UNKNOWN + ) + # params just have to be a list of length 1 to ensure the test runs at least once simulated_test_runner.run_test( setup=setup, diff --git a/src/software/ai/hl/stp/play/kickoff_play_test.py b/src/software/ai/hl/stp/play/kickoff_play_test.py index 521805bc2d..0d3438ef4f 100644 --- a/src/software/ai/hl/stp/play/kickoff_play_test.py +++ b/src/software/ai/hl/stp/play/kickoff_play_test.py @@ -2,86 +2,85 @@ import software.python_bindings as tbots_cpp from proto.play_pb2 import Play, PlayName -from software.simulated_tests.robot_enters_region import * -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.ball_moves_from_rest import * +from software.simulated_tests.pytest_validations.robot_enters_region import * +from software.simulated_tests.pytest_validations.ball_enters_region import * from proto.import_all_protos import * from proto.message_translation.tbots_protobuf import create_world_state from proto.ssl_gc_common_pb2 import Team from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) -from software.simulated_tests.or_validation import OrValidation +from software.simulated_tests.pytest_validations.or_validation import OrValidation @pytest.mark.parametrize("is_friendly_test", [True, False]) def test_kickoff_play(simulated_test_runner, is_friendly_test): ball_initial_pos = tbots_cpp.Point(0, 0) - # Setup Bots - blue_bots = [ - tbots_cpp.Point(-3, 2.5), - tbots_cpp.Point(-3, 1.5), - tbots_cpp.Point(-3, 0.5), - tbots_cpp.Point(-3, -0.5), - tbots_cpp.Point(-3, -1.5), - tbots_cpp.Point(-3, -2.5), - ] + def setup(*args): + blue_bots = [ + tbots_cpp.Point(-3, 2.5), + tbots_cpp.Point(-3, 1.5), + tbots_cpp.Point(-3, 0.5), + tbots_cpp.Point(-3, -0.5), + tbots_cpp.Point(-3, -1.5), + tbots_cpp.Point(-3, -2.5), + ] + + yellow_bots = [ + tbots_cpp.Point(1, 0), + tbots_cpp.Point(1, 2.5), + tbots_cpp.Point(1, -2.5), + tbots_cpp.Field.createSSLDivisionBField().enemyGoalCenter(), + tbots_cpp.Field.createSSLDivisionBField() + .enemyDefenseArea() + .negXNegYCorner(), + tbots_cpp.Field.createSSLDivisionBField() + .enemyDefenseArea() + .negXPosYCorner(), + ] + + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) - yellow_bots = [ - tbots_cpp.Point(1, 0), - tbots_cpp.Point(1, 2.5), - tbots_cpp.Point(1, -2.5), - tbots_cpp.Field.createSSLDivisionBField().enemyGoalCenter(), - tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXNegYCorner(), - tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXPosYCorner(), - ] + blue_play = Play() + yellow_play = Play() - blue_play = Play() - yellow_play = Play() + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.STOP, team=Team.UNKNOWN + ) - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.STOP, team=Team.UNKNOWN - ) + if is_friendly_test: + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.KICKOFF, team=Team.BLUE + ) + blue_play.name = PlayName.KickoffFriendlyPlay + yellow_play.name = PlayName.KickoffEnemyPlay + else: + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.KICKOFF, team=Team.YELLOW + ) + blue_play.name = PlayName.KickoffEnemyPlay + yellow_play.name = PlayName.KickoffFriendlyPlay - if is_friendly_test: - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.KICKOFF, team=Team.BLUE - ) - blue_play.name = PlayName.KickoffFriendlyPlay - yellow_play.name = PlayName.KickoffEnemyPlay - else: - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.KICKOFF, team=Team.YELLOW + simulated_test_runner.send_gamecontroller_command( + gc_command=Command.Type.NORMAL_START, team=Team.BLUE ) - blue_play.name = PlayName.KickoffEnemyPlay - yellow_play.name = PlayName.KickoffFriendlyPlay - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.NORMAL_START, team=Team.BLUE - ) - - # Force play override here - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto(Play, yellow_play) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) + simulated_test_runner.set_play(yellow_play, is_friendly=False) # Always Validation always_validation_sequence_set = [[]] - ball_moves_at_rest_validation = BallAlwaysMovesFromRest( - position=tbots_cpp.Point(0, 0), threshold=0.05 + ball_moves_at_rest_validation = BallNeverEntersRegion( + regions=[tbots_cpp.Circle(tbots_cpp.Point(0, 0), 0.05)] ) expected_center_circle_or_validation_set = [ @@ -138,6 +137,7 @@ def test_kickoff_play(simulated_test_runner, is_friendly_test): ) simulated_test_runner.run_test( + setup=setup, inv_eventually_validation_sequence_set=eventually_validation_sequence_set, inv_always_validation_sequence_set=always_validation_sequence_set, test_timeout_s=10, diff --git a/src/software/ai/hl/stp/play/offense/BUILD b/src/software/ai/hl/stp/play/offense/BUILD index 4e2658fa63..b01c38acad 100644 --- a/src/software/ai/hl/stp/play/offense/BUILD +++ b/src/software/ai/hl/stp/play/offense/BUILD @@ -39,7 +39,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) 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 b865ebe2c2..a9b17ee3ff 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,10 +1,9 @@ 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 * +from software.simulated_tests.pytest_validations.friendly_team_scored import * +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.friendly_has_ball_possession import * +from software.simulated_tests.pytest_validations.excessive_dribbling import * from proto.message_translation.tbots_protobuf import create_world_state from proto.ssl_gc_common_pb2 import Team from software.simulated_tests.simulated_test_fixture import ( @@ -14,13 +13,8 @@ def test_offense_play(simulated_test_runner): def setup(start_point): - # starting point must be Point ball_initial_pos = start_point - # placement point must be Vector2 to work with game controller - tbots_cpp.Point(-3, -2) - tbots_cpp.Field.createSSLDivisionBField() - # Setup Bots blue_bots = [ tbots_cpp.Point(-4.5, 3.0), tbots_cpp.Point(-2, 1.5), @@ -43,36 +37,30 @@ def setup(start_point): .negXPosYCorner(), ] - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=yellow_bots, + blue_robot_locations=blue_bots, + ball_location=ball_initial_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.FORCE_START, team=Team.BLUE ) - # Force play override here blue_play = Play() blue_play.name = PlayName.OffensePlay yellow_play = Play() yellow_play.name = PlayName.HaltPlay - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - Play, yellow_play - ) - - # Create world state - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=yellow_bots, - blue_robot_locations=blue_bots, - ball_location=ball_initial_pos, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) + simulated_test_runner.set_play(yellow_play, is_friendly=False) field = tbots_cpp.Field.createSSLDivisionBField() diff --git a/src/software/ai/hl/stp/play/passing_sim_test.py b/src/software/ai/hl/stp/play/passing_sim_test.py index b3821d363f..54baa96d17 100644 --- a/src/software/ai/hl/stp/play/passing_sim_test.py +++ b/src/software/ai/hl/stp/play/passing_sim_test.py @@ -1,19 +1,20 @@ import pytest -import math import software.python_bindings as tbots_cpp from proto.import_all_protos import * from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) from proto.message_translation.tbots_protobuf import create_world_state -from software.simulated_tests.friendly_receives_ball_slow import ( +from software.simulated_tests.pytest_validations.friendly_receives_ball_slow import ( FriendlyAlwaysReceivesBallSlow, ) -from software.simulated_tests.friendly_has_ball_possession import ( +from software.simulated_tests.pytest_validations.friendly_has_ball_possession import ( FriendlyEventuallyHasBallPossession, ) -from software.simulated_tests.ball_moves_in_direction import BallMovesForwardInRegions -from software.simulated_tests.ball_enters_region import ( +from software.simulated_tests.pytest_validations.ball_moves_in_direction import ( + BallMovesForwardInRegions, +) +from software.simulated_tests.pytest_validations.ball_enters_region import ( BallEventuallyExitsRegion, BallEventuallyEntersRegion, ) @@ -45,7 +46,7 @@ def setup_pass_and_robots( blue_robot_locations = [attacker_robot_position, *receiver_robot_positions] # Setup the world state - simulated_test_runner.set_worldState( + simulated_test_runner.set_world_state( create_world_state( yellow_robot_locations=enemy_robot_positions, blue_robot_locations=blue_robot_locations, @@ -108,9 +109,8 @@ def setup_pass_and_robots( # Setup the passer's tactic # We use KickTactic since AttackerTactic shoots towards the goal instead if open # KickTactic just does the kick we want - params = AssignedTacticPlayControlParams() - params.assigned_tactics[0].kick.CopyFrom( - KickTactic( + blue_tactics = { + 0: KickTactic( kick_origin=Point( x_meters=best_pass.passerPoint().x(), y_meters=best_pass.passerPoint().y(), @@ -118,7 +118,7 @@ def setup_pass_and_robots( kick_direction=Angle(radians=kick_vec.orientation().toRadians()), kick_speed_meters_per_second=best_pass.speed(), ) - ) + } # if we want a friendly robot to receive the pass if receive_pass: @@ -138,12 +138,9 @@ def setup_pass_and_robots( "disable_one_touch_shot": True, } - params.assigned_tactics[1].receiver.CopyFrom(ReceiverTactic(**receiver_args)) - simulated_test_runner.set_tactics(params, True) + blue_tactics[1] = ReceiverTactic(**receiver_args) - # Setup no tactics on the enemy side - params = AssignedTacticPlayControlParams() - simulated_test_runner.set_tactics(params, False) + simulated_test_runner.set_tactics(blue_tactics=blue_tactics, yellow_tactics={}) return best_pass @@ -158,7 +155,7 @@ def setup_pass_and_robots( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(-1.0, 0.0), [tbots_cpp.Point(1.0, 0.0)], - [0, math.pi], + [tbots_cpp.Angle.zero(), tbots_cpp.Angle.half()], [], ), # pass between 2 robots on opposite ends of the field @@ -167,7 +164,7 @@ def setup_pass_and_robots( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(-3.5, 0.0), [tbots_cpp.Point(3.5, 0.0)], - [0, math.pi], + [tbots_cpp.Angle.zero(), tbots_cpp.Angle.half()], [], ), # TODO: Make Interception Better @@ -178,7 +175,7 @@ def setup_pass_and_robots( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(0.0, -1.0), [tbots_cpp.Point(0.0, 3.0)], - [0, math.pi], + [tbots_cpp.Angle.zero(), tbots_cpp.Angle.half()], [], ), # pass between 2 robots on opposite ends of the field's diagonal @@ -187,7 +184,7 @@ def setup_pass_and_robots( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(-3.5, 2.5), [tbots_cpp.Point(3.5, -2.5)], - [0, math.pi], + [tbots_cpp.Angle.zero(), tbots_cpp.Angle.half()], [], ), # straight pass with an enemy in between the 2 robots @@ -196,7 +193,7 @@ def setup_pass_and_robots( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(-1.0, 0.0), [tbots_cpp.Point(1.5, 0.0)], - [0, math.pi], + [tbots_cpp.Angle.zero(), tbots_cpp.Angle.half()], [tbots_cpp.Point(0.5, 0.0)], ), # pass with a sparse wall of enemy robots in between the 2 robots @@ -205,7 +202,7 @@ def setup_pass_and_robots( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(-2.0, 0.0), [tbots_cpp.Point(1.0, 0.0)], - [0, math.pi], + [tbots_cpp.Angle.zero(), tbots_cpp.Angle.half()], [ tbots_cpp.Point(0.5, 2.0), tbots_cpp.Point(0.5, 1.0), @@ -277,7 +274,7 @@ def test_passing_receive_speed( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(2.0, -2.0), [tbots_cpp.Point(-2.5, 2.0)], - [math.pi, 0], + [tbots_cpp.Angle.half(), tbots_cpp.Angle.zero()], [], ), ( @@ -285,7 +282,7 @@ def test_passing_receive_speed( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(0.5, 0), [tbots_cpp.Point(-0.5, 0)], - [math.pi, 0], + [tbots_cpp.Angle.half(), tbots_cpp.Angle.zero()], [], ), ( @@ -293,7 +290,7 @@ def test_passing_receive_speed( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(0.6, 0), [tbots_cpp.Point(-0.6, 0)], - [math.pi, 0], + [tbots_cpp.Angle.half(), tbots_cpp.Angle.zero()], [], ), ( @@ -306,7 +303,7 @@ def test_passing_receive_speed( tbots_cpp.Point(1, -1), tbots_cpp.Point(2, 0), ], - [math.pi, 0], + [tbots_cpp.Angle.half(), tbots_cpp.Angle.zero()], [], ), ( @@ -314,7 +311,7 @@ def test_passing_receive_speed( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(1, 0), [tbots_cpp.Point(2.5, 2.5), tbots_cpp.Point(-1, 0)], - [math.pi, 0], + [tbots_cpp.Angle.half(), tbots_cpp.Angle.zero()], [], ), ( @@ -322,7 +319,7 @@ def test_passing_receive_speed( tbots_cpp.Vector(0.0, 0.0), tbots_cpp.Point(0.5, 0), [tbots_cpp.Point(-1, 0)], - [math.pi, 0], + [tbots_cpp.Angle.half(), tbots_cpp.Angle.zero()], [ tbots_cpp.Point(0.5, 0.5), tbots_cpp.Point(0.5, -0.5), diff --git a/src/software/ai/hl/stp/play/shoot_or_chip_play_test.py b/src/software/ai/hl/stp/play/shoot_or_chip_play_test.py index 8b1d8ea44f..5584002067 100644 --- a/src/software/ai/hl/stp/play/shoot_or_chip_play_test.py +++ b/src/software/ai/hl/stp/play/shoot_or_chip_play_test.py @@ -3,7 +3,7 @@ import software.python_bindings as tbots_cpp from proto.play_pb2 import Play, PlayName -from software.simulated_tests.ball_enters_region import * +from software.simulated_tests.pytest_validations.ball_enters_region import * from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) @@ -52,22 +52,18 @@ def setup(*args): world_state.yellow_robots[5].CopyFrom(last_robot) - # Game Controller Setup - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.set_world_state(world_state) + + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.STOP, team=Team.UNKNOWN ) - simulated_test_runner.gamecontroller.send_gc_command( + simulated_test_runner.send_gamecontroller_command( gc_command=Command.Type.FORCE_START, team=Team.BLUE ) blue_play = Play() blue_play.name = PlayName.ShootOrChipPlay - - simulated_test_runner.blue_full_system_proto_unix_io.send_proto(Play, blue_play) - - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, world_state - ) + simulated_test_runner.set_play(blue_play, is_friendly=True) simulated_test_runner.run_test( setup=setup, diff --git a/src/software/ai/hl/stp/tactic/BUILD b/src/software/ai/hl/stp/tactic/BUILD index c69db8f0a3..253bdeaad6 100644 --- a/src/software/ai/hl/stp/tactic/BUILD +++ b/src/software/ai/hl/stp/tactic/BUILD @@ -1,15 +1,5 @@ package(default_visibility = ["//visibility:public"]) -cc_test( - name = "penalty_setup_tactic_test", - srcs = ["penalty_setup_tactic_test.cpp"], - deps = [ - "//shared/test_util:tbots_gtest_main", - "//software/ai/hl/stp/tactic/move:move_tactic", - "//software/test_util", - ], -) - cc_library( name = "all_tactics", hdrs = ["all_tactics.h"], diff --git a/src/software/ai/hl/stp/tactic/attacker/BUILD b/src/software/ai/hl/stp/tactic/attacker/BUILD index 6b1300c8cd..6c72220060 100644 --- a/src/software/ai/hl/stp/tactic/attacker/BUILD +++ b/src/software/ai/hl/stp/tactic/attacker/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -76,3 +78,14 @@ cc_test( "//software/world", ], ) + +py_test( + name = "attacker_tactic_test", + srcs = ["attacker_tactic_test.py"], + tags = ["exclusive"], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/attacker/attacker_tactic_test.py b/src/software/ai/hl/stp/tactic/attacker/attacker_tactic_test.py new file mode 100644 index 0000000000..5dfe7891c7 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/attacker/attacker_tactic_test.py @@ -0,0 +1,415 @@ +import pytest +import software.python_bindings as tbots_cpp +from software.py_constants import ROBOT_MAX_RADIUS_METERS + +from proto.import_all_protos import AttackerTactic, Pass +from proto.message_translation.tbots_protobuf import create_world_state +from software.simulated_tests.pytest_validations.ball_kicked_in_direction import ( + BallEventuallyKickedInDirection, +) +from software.simulated_tests.pytest_validations.excessive_dribbling import ( + NeverExcessivelyDribbles, +) +from software.simulated_tests.pytest_validations.friendly_team_scored import ( + FriendlyTeamEventuallyScored, +) +from software.simulated_tests.pytest_validations.robot_at_orientation import ( + RobotEventuallyAtOrientation, +) +from software.simulated_tests.pytest_validations.robot_at_position import ( + RobotEventuallyAtPosition, +) +from software.simulated_tests.simulated_test_fixture import pytest_main + + +def calculate_ball_velocity(passer_point, receiver_point, speed): + """Calculate ball velocity based on pass trajectory""" + direction = receiver_point - passer_point + if direction.length() > 0: + direction = direction.normalize() + return direction * speed + + +# Passing Tests +@pytest.mark.parametrize( + "passer_point, receiver_point, pass_speed, robot_pos, ball_pos, ball_velocity", + [ + # Stationary Ball Tests + # Attacker point != Balls location & Balls location != Robots Location + ( + tbots_cpp.Point(0, 0.5), + tbots_cpp.Point(0, 0), + 5, + tbots_cpp.Point(0, 0), + tbots_cpp.Point(0.5, 0.5), + tbots_cpp.Vector(0, 0), + ), + # Attacker point == Balls location & Balls location != Robots Location + ( + tbots_cpp.Point(-0.5, -0.5), + tbots_cpp.Point(0, 0), + 5, + tbots_cpp.Point(0, 0), + tbots_cpp.Point(-0.5, -0.5), + tbots_cpp.Vector(0, 0), + ), + # Attacker point != Balls location & Balls location == Robots Location + ( + tbots_cpp.Point(0.5, 0.5), + tbots_cpp.Point(0, 1), + 5, + tbots_cpp.Point(0.5, 0.5), + tbots_cpp.Point(-0.5, 0.5), + tbots_cpp.Vector(0, 0), + ), + # TODO(#2909): Enable test once the robot can turn faster and hits the ball with + # the ball at the exact robot location + # Attacker point == Balls location & Balls location == Robots Location + # ( + # tbots_cpp.Point(0.0, 0.0), + # tbots_cpp.Point(0, 0), + # 5, + # tbots_cpp.Point(0, 0), + # tbots_cpp.Point(0.0, 0.0), + # tbots_cpp.Vector(0, 0), + # ), + # Attacker point far away (not a normal use case, but just to sanity check) + ( + tbots_cpp.Point(0, 0), + tbots_cpp.Point(0, 0), + 5, + tbots_cpp.Point(3.5, 2.5), + tbots_cpp.Point(0.0, 0.0), + tbots_cpp.Vector(0, 0), + ), + # Attacker point != Balls location & Balls location != Robots Location (duplicate) + ( + tbots_cpp.Point(0, 0.5), + tbots_cpp.Point(0, 0), + 5, + tbots_cpp.Point(0, 0), + tbots_cpp.Point(0.5, 0.5), + tbots_cpp.Vector(0, 0), + ), + # Moving Ball Tests + # Attacker point == Balls location & Balls location != Robots Location + ( + tbots_cpp.Point(-0.5, -0.5), + tbots_cpp.Point(0, 0), + 5, + tbots_cpp.Point(0, 0), + tbots_cpp.Point(-0.5, -0.5), + tbots_cpp.Vector(1, 0), + ), + # TODO (#2859): Robot does not kick ball when dribbler is off since it is + # too far away. + # Attacker point != Balls location & Balls location == Robots Location + # ( + # tbots_cpp.Point(0.4, 0.4), + # tbots_cpp.Point(0, 1), + # 5, + # tbots_cpp.Point(0.5, 0.5), + # tbots_cpp.Point(-0.4, 0.4), + # tbots_cpp.Vector(0, 1), + # ), + # Attacker point == Balls location & Balls location == Robots Location + ( + tbots_cpp.Point(0, 0), + tbots_cpp.Point(0, 0), + 5, + tbots_cpp.Point(0, 0), + tbots_cpp.Point(ROBOT_MAX_RADIUS_METERS, ROBOT_MAX_RADIUS_METERS), + tbots_cpp.Vector(1, 0), + ), + ], +) +def test_attacker_passing( + passer_point, + receiver_point, + pass_speed, + robot_pos, + ball_pos, + ball_velocity, + simulated_test_runner, +): + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + robot_pos, + ], + # TODO (#2558) + # In C++ test: Uses ai_config to set min_open_angle_for_shot_deg to force passing + # Temp fix by blocking off the enemy goal + yellow_robot_locations=[ + tbots_cpp.Point(4, -0.4), + tbots_cpp.Point(4, -0.2), + tbots_cpp.Point(4, 0), + tbots_cpp.Point(4, 0.2), + tbots_cpp.Point(4, 0.4), + ], + ball_location=ball_pos, + ball_velocity=ball_velocity, + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: AttackerTactic( + best_pass_so_far=Pass( + passer_point=tbots_cpp.createPointProto(passer_point), + receiver_point=tbots_cpp.createPointProto(receiver_point), + pass_speed_m_per_s=pass_speed, + ), + pass_committed=True, + ) + } + ) + + pass_orientation = (receiver_point - passer_point).orientation() + + eventually_validation_sequence_set = [ + [ + RobotEventuallyAtOrientation(robot_id=1, orientation=pass_orientation), + # Larger threshold since techically ball is at passer point, not the robot + RobotEventuallyAtPosition( + robot_id=1, position=passer_point, threshold=ROBOT_MAX_RADIUS_METERS + ), + BallEventuallyKickedInDirection(kick_direction=pass_orientation), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + run_till_end=False, + test_timeout_s=7, + ) + + +# Keep Away Tests +@pytest.mark.parametrize( + "passer_point, receiver_point, pass_speed, robot_pos, robot_orientation, ball_pos, ball_velocity, enemy_positions, ignore_score_checks", + [ + # Basic keep away with enemy robots around + ( + tbots_cpp.Point(-0.2, 0.0), + tbots_cpp.Point(-3, 2.5), + 5, + tbots_cpp.Point(0.25, 0), + tbots_cpp.Angle.half(), + tbots_cpp.Point(0.0, 0.0), + tbots_cpp.Vector(0, 0), + [ + tbots_cpp.Point(-0.6, 0.25), + tbots_cpp.Point(0.0, 0.6), + tbots_cpp.Point(-0.25, 0.5), + tbots_cpp.Point(0.6, -0.25), + ], + True, # This test is too unstable, so we ignore the checks + ), + # TODO(#2909): Enable test once the robot can turn faster and hits the ball with + # the ball at the exact robot location + # ( + # tbots_cpp.Point(0.0, 0.0), + # tbots_cpp.Point(-3, 2.5), + # 5, + # tbots_cpp.Point(0.25, 0), + # tbots_cpp.Angle.zero(), + # tbots_cpp.Point(0.0, 0.0), + # tbots_cpp.Vector(0, 0), + # [tbots_cpp.Point(-0.5, 0.5)], + # False, + # ), + ], +) +def test_attacker_keep_away( + passer_point, + receiver_point, + pass_speed, + robot_pos, + robot_orientation, + ball_pos, + ball_velocity, + enemy_positions, + ignore_score_checks, + simulated_test_runner, +): + # TODO(#2558): Port C++ validation functions that don't exist in Python yet + # In C++ test: + # - Uses ai_config to set min_open_angle_for_shot_deg to 90 and + # enemy_about_to_steal_ball_radius to 0.01 + # - Validates calculateProximityRisk improves over time + # - Validates ratePassEnemyRisk improves over time + # - Validates ball stays in field boundaries + # This test is skipped in C++ due to instability + + field = tbots_cpp.Field.createSSLDivisionBField() + field_top_left = field.fieldLines().negXPosYCorner() + + # Keep away near field corner (second test case from C++) + def setup(*args): + robot_pos = field_top_left + ball_pos = tbots_cpp.Point(field_top_left.x() + 0.05, field_top_left.y() - 0.2) + enemy_positions = [ + tbots_cpp.Point(-4, 2), + tbots_cpp.Point(-4, 2.25), + tbots_cpp.Point(-4, 2.5), + tbots_cpp.Point(-4, 2.75), + ] + + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + robot_pos, + ], + yellow_robot_locations=enemy_positions, + ball_location=ball_pos, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + pass_point = tbots_cpp.Point( + field_top_left.x() + 0.05, field_top_left.y() - 0.05 + ) + receiver_point = tbots_cpp.Point(0, 0) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: AttackerTactic( + best_pass_so_far=Pass( + passer_point=tbots_cpp.createPointProto(pass_point), + receiver_point=tbots_cpp.createPointProto(receiver_point), + pass_speed_m_per_s=5, + ), + pass_committed=False, + ) + } + ) + + always_validation_sequence_set = [ + [ + NeverExcessivelyDribbles(), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_always_validation_sequence_set=always_validation_sequence_set, + ag_always_validation_sequence_set=always_validation_sequence_set, + ) + + +# Shoot Goal Tests +@pytest.mark.parametrize( + "ball_pos, ball_velocity, robot_pos, enemy_positions", + [ + # # TODO (#2693): Fix FSM looping endlessly + # # enemy goal blocked by enemy robots with enemy threat right + # ( + # tbots_cpp.Point(2, 1), + # tbots_cpp.Vector(0, 0), + # tbots_cpp.Point(1, 1), + # [ + # tbots_cpp.Point(2.4, 1), + # tbots_cpp.Point(3, 0.4), + # tbots_cpp.Point(3, 0.8), + # tbots_cpp.Point(3.1, 0.6), + # tbots_cpp.Point(3.1, 1), + # tbots_cpp.Point(4.2, 1.2), + # ], + # ), + # small opening in enemy formation + ( + tbots_cpp.Point(2, 1), + tbots_cpp.Vector(0, 0), + tbots_cpp.Point(1, 1), + [ + tbots_cpp.Point(1, 0), + tbots_cpp.Point(3, 0.2), + tbots_cpp.Point(3, 0.8), + tbots_cpp.Point(3.1, 0), + tbots_cpp.Point(3.1, 1), + tbots_cpp.Point(4.2, 1.2), + ], + ), + # extreme angle shot + ( + tbots_cpp.Point(3.8, -1.9), + tbots_cpp.Vector(0, 0), + tbots_cpp.Point(1, 1), + [ + tbots_cpp.Point(1, 0), + tbots_cpp.Point(3, 1.2), + tbots_cpp.Point(3, 0.8), + tbots_cpp.Point(3.1, 0.6), + tbots_cpp.Point(3.1, 1), + tbots_cpp.Point(4.2, 0.5), + ], + ), + # enemy trying to steal + ( + tbots_cpp.Point(2.5, -1), + tbots_cpp.Vector(0, 0), + tbots_cpp.Point(1, 1), + [ + tbots_cpp.Point(2.5, -1.4), + tbots_cpp.Point(3, 0.4), + tbots_cpp.Point(3, 0.8), + tbots_cpp.Point(3.1, 0.6), + tbots_cpp.Point(3.1, 1), + tbots_cpp.Point(4.2, 1.2), + ], + ), + ], +) +def test_attacker_shoot_goal( + ball_pos, ball_velocity, robot_pos, enemy_positions, simulated_test_runner +): + # TODO (#2558) + # In C++ test: + # - Uses ai_config to set pass_delay_sec to 0.0 + # - Uses motion constraint FRIENDLY_DEFENSE_AREA + + field = tbots_cpp.Field.createSSLDivisionBField() + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[robot_pos], + yellow_robot_locations=enemy_positions, + ball_location=ball_pos, + ball_velocity=ball_velocity, + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 0: AttackerTactic( + chip_target=tbots_cpp.createPointProto( + tbots_cpp.Point(0, field.fieldLines().yMin()) + ), + ) + } + ) + + eventually_validation_sequence_set = [ + [ + FriendlyTeamEventuallyScored(), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + run_till_end=False, + test_timeout_s=9, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/hl/stp/tactic/chip/BUILD b/src/software/ai/hl/stp/tactic/chip/BUILD index a4f05afb17..4ceb832f6a 100644 --- a/src/software/ai/hl/stp/tactic/chip/BUILD +++ b/src/software/ai/hl/stp/tactic/chip/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -30,7 +32,7 @@ cc_test( ) cc_test( - name = "chip_tactic_test", + name = "chip_tactic_cpp_test", srcs = ["chip_tactic_test.cpp"], deps = [ ":chip_tactic", @@ -41,3 +43,16 @@ cc_test( "//software/test_util", ], ) + +py_test( + name = "chip_tactic_test", + srcs = ["chip_tactic_test.py"], + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/chip/chip_tactic_test.py b/src/software/ai/hl/stp/tactic/chip/chip_tactic_test.py new file mode 100644 index 0000000000..53b82d3bcb --- /dev/null +++ b/src/software/ai/hl/stp/tactic/chip/chip_tactic_test.py @@ -0,0 +1,93 @@ +import pytest + +import software.python_bindings as tbots_cpp + +from software.py_constants import ROBOT_MAX_RADIUS_METERS +from software.simulated_tests.pytest_validations.ball_is_off_ground import ( + BallIsEventuallyOffGround, +) +from software.simulated_tests.pytest_validations.ball_kicked_in_direction import ( + BallEventuallyKickedInDirection, +) +from software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) +from proto.message_translation.tbots_protobuf import create_world_state +from proto.import_all_protos import ChipTactic + + +@pytest.mark.parametrize( + "ball_offset_from_robot, angle_to_chip_at", + [ + # place the ball directly to the left of the robot + (tbots_cpp.Vector(0, 0.5), tbots_cpp.Angle.zero()), + # place the ball directly to the right of the robot + (tbots_cpp.Vector(0, -0.5), tbots_cpp.Angle.zero()), + # place the ball directly infront of the robot + (tbots_cpp.Vector(0.5, 0), tbots_cpp.Angle.zero()), + # place the ball directly behind the robot + (tbots_cpp.Vector(-0.5, 0), tbots_cpp.Angle.zero()), + # place the ball in the robots dribbler + ( + tbots_cpp.Vector(ROBOT_MAX_RADIUS_METERS, 0), + tbots_cpp.Angle.zero(), + ), + # Repeat the same tests but kick in the opposite direction + # place the ball directly to the left of the robot + (tbots_cpp.Vector(0, 0.5), tbots_cpp.Angle.half()), + # place the ball directly to the right of the robot + (tbots_cpp.Vector(0, -0.5), tbots_cpp.Angle.half()), + # place the ball directly infront of the robot + (tbots_cpp.Vector(0.5, 0), tbots_cpp.Angle.half()), + # place the ball directly behind the robot + (tbots_cpp.Vector(-0.5, 0), tbots_cpp.Angle.half()), + # place the ball in the robots dribbler + ( + tbots_cpp.Vector(ROBOT_MAX_RADIUS_METERS, 0), + tbots_cpp.Angle.zero(), + ), + ], +) +def test_chip(ball_offset_from_robot, angle_to_chip_at, simulated_test_runner): + robot_position = tbots_cpp.Point(0, 0) + ball_position = robot_position + ball_offset_from_robot + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + robot_position, + ], + yellow_robot_locations=[tbots_cpp.Point(4, 0)], + ball_location=ball_position, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: ChipTactic( + chip_origin=tbots_cpp.createPointProto(ball_position), + chip_direction=tbots_cpp.createAngleProto(angle_to_chip_at), + chip_distance_meters=2.0, + ) + } + ) + + eventually_validations = [ + [ + BallIsEventuallyOffGround(), + BallEventuallyKickedInDirection(angle_to_chip_at), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/hl/stp/tactic/crease_defender/BUILD b/src/software/ai/hl/stp/tactic/crease_defender/BUILD index 8cabfce189..c75d4a6e30 100644 --- a/src/software/ai/hl/stp/tactic/crease_defender/BUILD +++ b/src/software/ai/hl/stp/tactic/crease_defender/BUILD @@ -59,8 +59,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:speed_threshold_helpers", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/tactic/crease_defender/crease_defender_tactic_test.py b/src/software/ai/hl/stp/tactic/crease_defender/crease_defender_tactic_test.py index d470146178..f04e82282c 100644 --- a/src/software/ai/hl/stp/tactic/crease_defender/crease_defender_tactic_test.py +++ b/src/software/ai/hl/stp/tactic/crease_defender/crease_defender_tactic_test.py @@ -1,20 +1,172 @@ import pytest import software.python_bindings as tbots_cpp -from software.simulated_tests.robot_enters_region import * -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.ball_moves_in_direction import * -from software.simulated_tests.friendly_has_ball_possession import * -from software.simulated_tests.ball_speed_threshold import * -from software.simulated_tests.robot_speed_threshold import * -from software.simulated_tests.excessive_dribbling import * +from proto.import_all_protos import ( + CreaseDefenderTactic, + CreaseDefenderAlignment, + MaxAllowedSpeedMode, + BallStealMode, +) +from software.simulated_tests.pytest_validations.robot_enters_region import * +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.ball_moves_in_direction import * +from software.simulated_tests.pytest_validations.friendly_has_ball_possession import * +from software.simulated_tests.pytest_validations.ball_speed_threshold import * +from software.simulated_tests.pytest_validations.robot_speed_threshold import * +from software.simulated_tests.pytest_validations.excessive_dribbling import * from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) -from software.simulated_tests.ball_is_off_ground import * +from software.simulated_tests.pytest_validations.ball_is_off_ground import * from proto.message_translation.tbots_protobuf import create_world_state +def test_not_bumping_ball_towards_net(simulated_test_runner): + enemy_threat_point = tbots_cpp.Point(3, 0) + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[tbots_cpp.Point(0, 0)], + yellow_robot_locations=[tbots_cpp.Point(4, 0)], + ball_location=enemy_threat_point, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 0: CreaseDefenderTactic( + enemy_threat_origin=tbots_cpp.createPointProto(enemy_threat_point), + crease_defender_alignment=CreaseDefenderAlignment.CENTRE, + ) + } + ) + + simulated_test_runner.run_test( + setup=setup, + inv_always_validation_sequence_set=[[BallSpeedAlwaysBelowThreshold(0.001)]], + ag_always_validation_sequence_set=[[BallSpeedAlwaysBelowThreshold(0.001)]], + ) + + +@pytest.mark.parametrize( + "enemy_threat_point,crease_alignment,region_index", + [ + # Enemy threat in front of crease, LEFT + (tbots_cpp.Point(1, 2.5), CreaseDefenderAlignment.LEFT, 2), + # Enemy threat in front of crease, CENTRE + (tbots_cpp.Point(1, -2.5), CreaseDefenderAlignment.CENTRE, 3), + # Enemy threat in front of crease, RIGHT + (tbots_cpp.Point(1.5, 2), CreaseDefenderAlignment.RIGHT, 2), + # Enemy threat left side of crease, RIGHT + (tbots_cpp.Point(-3.5, 2.5), CreaseDefenderAlignment.RIGHT, 1), + # Enemy threat left side of crease, CENTRE + (tbots_cpp.Point(-4, 2.5), CreaseDefenderAlignment.CENTRE, 0), + # goal Enemy threat right side of crease, RIGHT + (tbots_cpp.Point(-4, -2), CreaseDefenderAlignment.RIGHT, 5), + # Enemy threat right side of crease, LEFT + (tbots_cpp.Point(-4.25, -2), CreaseDefenderAlignment.LEFT, 5), + ], +) +def test_crease_region_positioning( + enemy_threat_point, crease_alignment, region_index, simulated_test_runner +): + field = tbots_cpp.Field.createSSLDivisionBField() + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[tbots_cpp.Point(-3, 1.5)], + yellow_robot_locations=[ + tbots_cpp.Point(1, 0), + enemy_threat_point, + tbots_cpp.Point(1, -1.5), + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + ], + ball_location=tbots_cpp.Point(4.5, -3), + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 0: CreaseDefenderTactic( + enemy_threat_origin=tbots_cpp.createPointProto(enemy_threat_point), + crease_defender_alignment=crease_alignment, + ) + } + ) + + # We check if the robot is in one of the following regions + # ┌───┬───┬────────────────┐ + # │ │ │ │ + # │ 0 │ 1 │ C + # │ │ │ E + # │ │ │ N + # ├───┴───┤ 2 T + # E │ R + # N Def. │ E + # D │ │ + # L ├────────────────┤ + # I Area │ │ + # N │ │ + # E │ L + # ├───┬───┤ I + # │ │ │ 3 N + # │ 5 │ 4 │ E + # │ │ │ │ + # │ │ │ │ + # └───┴───┴────────────────┘ + + defense_area = field.friendlyDefenseArea() + field_lines = field.fieldLines() + + defense_area_half_width = tbots_cpp.Vector(defense_area.xLength() / 2, 0) + + defender_regions = [ + tbots_cpp.Rectangle( + field.friendlyCornerPos(), + defense_area.negXPosYCorner() + defense_area_half_width, + ), + tbots_cpp.Rectangle( + field.friendlyCornerPos() + defense_area_half_width, + defense_area.posXPosYCorner(), + ), + tbots_cpp.Rectangle( + tbots_cpp.Point(defense_area.xMax(), field_lines.yMax()), + field.centerPoint(), + ), + tbots_cpp.Rectangle( + tbots_cpp.Point(defense_area.xMax(), field_lines.yMin()), + field.centerPoint(), + ), + tbots_cpp.Rectangle( + field.friendlyCornerNeg() + defense_area_half_width, + defense_area.posXNegYCorner(), + ), + tbots_cpp.Rectangle( + field.friendlyCornerNeg(), + defense_area.negXNegYCorner() + defense_area_half_width, + ), + ] + + eventually_validations = [ + [ + # TODO: check that it stays for 1000 ticks + RobotEventuallyEntersRegion(regions=[defender_regions[region_index]]) + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + ) + + @pytest.mark.parametrize( "blue_bots, yellow_bots, ball_initial_pos, ball_initial_velocity", [ @@ -46,41 +198,27 @@ def test_crease_positioning( ball_initial_velocity, simulated_test_runner, ): - # Setup Robot def setup(*args): - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, + simulated_test_runner.set_world_state( create_world_state( blue_robot_locations=[blue_bots], yellow_robot_locations=[yellow_bots], ball_location=ball_initial_pos, ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - - params.assigned_tactics[0].crease_defender.CopyFrom( - CreaseDefenderTactic( - enemy_threat_origin=tbots_cpp.createPointProto(ball_initial_pos), - crease_defender_alignment=CreaseDefenderAlignment.CENTRE, - max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, - ball_steal_mode=BallStealMode.STEAL, ) ) - 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.set_tactics( + blue_tactics={ + 0: CreaseDefenderTactic( + enemy_threat_origin=tbots_cpp.createPointProto(ball_initial_pos), + crease_defender_alignment=CreaseDefenderAlignment.CENTRE, + max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, + ball_steal_mode=BallStealMode.STEAL, + ) + } ) - # Always Validation always_validation_sequence_set = [ [ RobotNeverEntersRegion( @@ -94,10 +232,8 @@ def setup(*args): ] ] - # Eventually Validation eventually_validation_sequence_set = [ [ - # Crease defender should be near the defense area RobotEventuallyEntersRegion( regions=[ tbots_cpp.Field.createSSLDivisionBField() @@ -172,54 +308,37 @@ def test_crease_autochip( should_chip, simulated_test_runner, ): - # Setup Robot def setup(*args): - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, + simulated_test_runner.set_world_state( create_world_state( blue_robot_locations=[blue_bots], yellow_robot_locations=[yellow_bots], ball_location=ball_initial_pos, ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - - params.assigned_tactics[0].crease_defender.CopyFrom( - CreaseDefenderTactic( - enemy_threat_origin=tbots_cpp.createPointProto(ball_initial_pos), - crease_defender_alignment=CreaseDefenderAlignment.CENTRE, - max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, - ball_steal_mode=BallStealMode.STEAL, ) ) - 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.set_tactics( + blue_tactics={ + 0: CreaseDefenderTactic( + enemy_threat_origin=tbots_cpp.createPointProto(ball_initial_pos), + crease_defender_alignment=CreaseDefenderAlignment.CENTRE, + max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, + ball_steal_mode=BallStealMode.STEAL, + ) + } ) - # Always Validation always_validation_sequence_set = [ [ BallIsAlwaysOnGround(), NeverExcessivelyDribbles(), ] ] - # Eventually Validation eventually_validation_sequence_set = [[]] if should_chip: - # Always Validation for chipping always_validation_sequence_set = [[]] - # Eventually Validation for chipping eventually_validation_sequence_set = [[BallIsEventuallyOffGround()]] simulated_test_runner.run_test( @@ -261,48 +380,33 @@ def test_crease_get_ball( should_dribble, simulated_test_runner, ): - # Setup Robot def setup(*args): - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, + simulated_test_runner.set_world_state( create_world_state( blue_robot_locations=[blue_bots], yellow_robot_locations=[yellow_bots], ball_location=ball_initial_pos, ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - - params.assigned_tactics[0].crease_defender.CopyFrom( - CreaseDefenderTactic( - enemy_threat_origin=tbots_cpp.createPointProto(ball_initial_pos), - crease_defender_alignment=CreaseDefenderAlignment.CENTRE, - max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, - ball_steal_mode=BallStealMode.STEAL, ) ) - 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.set_tactics( + blue_tactics={ + 0: CreaseDefenderTactic( + enemy_threat_origin=tbots_cpp.createPointProto(ball_initial_pos), + crease_defender_alignment=CreaseDefenderAlignment.CENTRE, + max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, + ball_steal_mode=BallStealMode.STEAL, + ) + } ) - # Always Validation always_validation_sequence_set = [ [ BallIsAlwaysOnGround(), NeverExcessivelyDribbles(), ] ] - # Eventually Validation eventually_validation_sequence_set = [[]] if should_dribble: diff --git a/src/software/ai/hl/stp/tactic/dribble/BUILD b/src/software/ai/hl/stp/tactic/dribble/BUILD index 67199a889f..77ddbae1ba 100644 --- a/src/software/ai/hl/stp/tactic/dribble/BUILD +++ b/src/software/ai/hl/stp/tactic/dribble/BUILD @@ -32,7 +32,7 @@ cc_test( ) cc_test( - name = "dribble_tactic_test", + name = "dribble_tactic_cpp_test", srcs = ["dribble_tactic_test.cpp"], deps = [ ":dribble_tactic", @@ -47,6 +47,19 @@ cc_test( ], ) +py_test( + name = "dribble_tactic_test", + srcs = ["dribble_tactic_test.py"], + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) + cc_test( name = "dribble_tactic_push_enemy_test", srcs = ["dribble_tactic_push_enemy_test.cpp"], @@ -74,8 +87,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:speed_threshold_helpers", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/tactic/dribble/dribble_tactic_test.py b/src/software/ai/hl/stp/tactic/dribble/dribble_tactic_test.py new file mode 100644 index 0000000000..1cb5924195 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/dribble/dribble_tactic_test.py @@ -0,0 +1,329 @@ +import pytest +import software.python_bindings as tbots_cpp +from software.py_constants import DIST_TO_FRONT_OF_ROBOT_METERS, ROBOT_MAX_RADIUS_METERS + +from proto.import_all_protos import DribbleTactic +from proto.message_translation.tbots_protobuf import create_world_state +from software.simulated_tests.pytest_validations.ball_enters_region import ( + BallAlwaysStaysInRegion, + BallEventuallyEntersRegion, +) +from software.simulated_tests.pytest_validations.excessive_dribbling import ( + EventuallyStartsExcessivelyDribbling, +) +from software.simulated_tests.pytest_validations.robot_at_orientation import ( + RobotEventuallyAtOrientation, +) +from software.simulated_tests.pytest_validations.robot_received_ball import ( + RobotEventuallyReceivedBall, +) +from software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) + + +def get_enemy_robot_positions(): + field = tbots_cpp.Field.createSSLDivisionBField() + return [ + tbots_cpp.Point(1, 0), + tbots_cpp.Point(1, 2.5), + tbots_cpp.Point(1, -2.5), + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + ] + + +@pytest.mark.parametrize( + "initial_pos, dribble_dest, dribble_orientation, ball_pos, ball_vel", + [ + # test_intercept_ball_behind_enemy_robot + ( + tbots_cpp.Point(-3, 1.5), + tbots_cpp.Point(0, 0), + None, + tbots_cpp.Point(3, -2), + tbots_cpp.Vector(-0.5, 1), + ), + # test_stopped_ball + ( + tbots_cpp.Point(-3, 1.5), + tbots_cpp.Point(0, 0), + None, + tbots_cpp.Point(-1, 1.5), + tbots_cpp.Vector(0, 0), + ), + # test_ball_bounce_off_of_enemy_robot + ( + tbots_cpp.Point(-3, 1.5), + tbots_cpp.Point(0, 0), + None, + tbots_cpp.Point(0, 0), + tbots_cpp.Vector(2.5, 0), + ), + # test_moving_ball_dribble_dest + ( + tbots_cpp.Point(-3, 1.5), + tbots_cpp.Point(-3, 1), + None, + tbots_cpp.Point(3, -2), + tbots_cpp.Vector(-1, 2), + ), + # test_moving_ball_dribble_orientation + ( + tbots_cpp.Point(-3, 1.5), + None, + tbots_cpp.Angle.quarter(), + tbots_cpp.Point(3, -2), + tbots_cpp.Vector(-1, 2), + ), + # test_moving_ball_dribble_dest_and_orientation + ( + tbots_cpp.Point(-2, 1.5), + tbots_cpp.Point(-1, 2), + tbots_cpp.Angle.zero(), + tbots_cpp.Point(1, 0), + tbots_cpp.Vector(1, 2), + ), + # test_dribble_dest_and_orientation_around_rectangle + ( + tbots_cpp.Point(3, -3), + tbots_cpp.Point(4, 2.5), + tbots_cpp.Angle.half(), + tbots_cpp.Point(4, -2.5), + tbots_cpp.Vector(0, 0), + ), + # test_steal_ball_from_behind_enemy 1 + ( + tbots_cpp.Point(-2, 2.5), + tbots_cpp.Point(0, 2), + tbots_cpp.Angle.zero(), + tbots_cpp.Point(1 + DIST_TO_FRONT_OF_ROBOT_METERS, 2.5), + tbots_cpp.Vector(0, 0), + ), + # test_steal_ball_from_behind_enemy 2 + ( + tbots_cpp.Point(3.5, 2.5), + tbots_cpp.Point(0, 2), + tbots_cpp.Angle.zero(), + tbots_cpp.Point(1 + DIST_TO_FRONT_OF_ROBOT_METERS, 2.5), + tbots_cpp.Vector(0, 0), + ), + # test_steal_ball_from_behind_enemy 3 + ( + tbots_cpp.Point(1, 0), + tbots_cpp.Point(0, 2), + tbots_cpp.Angle.zero(), + tbots_cpp.Point(1 + DIST_TO_FRONT_OF_ROBOT_METERS, 2.5), + tbots_cpp.Vector(0, 0), + ), + ], +) +def test_dribble( + initial_pos, + dribble_dest, + dribble_orientation, + ball_pos, + ball_vel, + simulated_test_runner, +): + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + initial_pos, + ], + yellow_robot_locations=get_enemy_robot_positions(), + ball_location=ball_pos, + ball_velocity=ball_vel, + ) + ) + + dribble_params = DribbleTactic( + allow_excessive_dribbling=False, + ) + if dribble_dest: + dribble_params.dribble_destination.CopyFrom( + tbots_cpp.createPointProto(dribble_dest) + ) + if dribble_orientation: + dribble_params.final_dribble_orientation.CopyFrom( + tbots_cpp.createAngleProto(dribble_orientation) + ) + + simulated_test_runner.set_tactics(blue_tactics={1: dribble_params}) + + eventually_validations = [[RobotEventuallyReceivedBall(1)]] + + if dribble_dest: + eventually_validations[0].append( + BallEventuallyEntersRegion([tbots_cpp.Circle(dribble_dest, 0.3)]) + ) + + if dribble_orientation: + eventually_validations[0].append( + RobotEventuallyAtOrientation(1, dribble_orientation) + ) + + eventually_validations[0].append(RobotEventuallyReceivedBall(1)) + + # TODO (#2514): tune dribbling and re-enable + # Robot always not excessively dribbling + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + test_timeout_s=25, + run_till_end=False, + ) + + +def test_dribble_with_excessive_dribbling(simulated_test_runner): + dribble_destination = tbots_cpp.Point(3, 2) + initial_position = tbots_cpp.Point(4.5, -3.0) + dribble_orientation = tbots_cpp.Angle.half() + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + initial_position, + ], + yellow_robot_locations=get_enemy_robot_positions(), + ball_location=tbots_cpp.Point(4.2, -2.5), + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: DribbleTactic( + dribble_destination=tbots_cpp.createPointProto(dribble_destination), + final_dribble_orientation=tbots_cpp.createAngleProto( + dribble_orientation + ), + allow_excessive_dribbling=True, + ) + } + ) + + eventually_validation_sequence_set = [ + [ + RobotEventuallyReceivedBall(1), + BallEventuallyEntersRegion([tbots_cpp.Circle(dribble_destination, 0.3)]), + RobotEventuallyAtOrientation(1, dribble_orientation), + ], + [EventuallyStartsExcessivelyDribbling()], + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + test_timeout_s=10, + ) + + +def test_run_into_enemy_robot_knock_ball_away( + simulated_test_runner, +): + initial_position = tbots_cpp.Point(-2, 1.5) + dribble_destination = tbots_cpp.Point(-1, 2) + dribble_orientation = tbots_cpp.Angle.half() + ball_pos = tbots_cpp.Point(2, -2) + ball_vel = tbots_cpp.Vector(2, 4) + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + initial_position, + ], + yellow_robot_locations=get_enemy_robot_positions() + + [tbots_cpp.Point(1, 1.1)], + ball_location=ball_pos, + ball_velocity=ball_vel, + ) + ) + + dribble_params = DribbleTactic( + allow_excessive_dribbling=False, + dribble_destination=tbots_cpp.createPointProto(dribble_destination), + final_dribble_orientation=tbots_cpp.createAngleProto(dribble_orientation), + ) + + simulated_test_runner.set_tactics(blue_tactics={1: dribble_params}) + + eventually_validations = [ + [ + RobotEventuallyReceivedBall(1), + BallEventuallyEntersRegion([tbots_cpp.Circle(dribble_destination, 0.3)]), + RobotEventuallyAtOrientation(1, dribble_orientation), + RobotEventuallyReceivedBall(1), + ] + ] + + # TODO (#2514): tune dribbling and re-enable + # Robot always not excessively dribbling + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + test_timeout_s=10, + ) + + +def test_robot_not_bumping_ball_when_turning( + simulated_test_runner, +): + # The ball is placed right behind the friendly robot. Verify that the robot + # does not bump the ball away when turning around to dribble it. + robot_location = tbots_cpp.Point(-1, 0) + ball_location = robot_location + tbots_cpp.Vector(ROBOT_MAX_RADIUS_METERS, 0) + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[tbots_cpp.Point(-3, 2.5), robot_location], + blue_robot_orientations=[ + tbots_cpp.Angle.zero(), + tbots_cpp.Angle.half(), + ], + yellow_robot_locations=get_enemy_robot_positions() + + [tbots_cpp.Point(1, 1.1)], + ball_location=ball_location, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + dribble_params = DribbleTactic() + simulated_test_runner.set_tactics(blue_tactics={1: dribble_params}) + + eventually_validations = [ + [ + RobotEventuallyReceivedBall(1), + ] + ] + + # TODO (#2514): tune dribbling and re-enable + # Robot always not excessively dribbling + always_validations = [ + [BallAlwaysStaysInRegion([tbots_cpp.Circle(ball_location, 0.05)])] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + inv_always_validation_sequence_set=always_validations, + ag_always_validation_sequence_set=always_validations, + ) + + +if __name__ == "__main__": + pytest_main(__file__) 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 index 443cc06825..7fdb463254 100644 --- a/src/software/ai/hl/stp/tactic/dribble/excessive_dribble_test.py +++ b/src/software/ai/hl/stp/tactic/dribble/excessive_dribble_test.py @@ -1,15 +1,11 @@ import pytest import software.python_bindings as tbots_cpp -from software.simulated_tests.excessive_dribbling import ( +from software.simulated_tests.pytest_validations.excessive_dribbling import ( NeverExcessivelyDribbles, EventuallyStartsExcessivelyDribbling, ) -from proto.message_translation.tbots_protobuf import ( - WorldState, - AssignedTacticPlayControlParams, - DribbleTactic, -) +from proto.message_translation.tbots_protobuf import DribbleTactic from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) @@ -129,6 +125,30 @@ def test_excessive_dribbling( simulated_test_runner, blue_robot_location, ): + def setup(*args): + blue_robot_locations = [blue_robot_location] + + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=[], + blue_robot_locations=blue_robot_locations, + ball_location=initial_location, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 0: DribbleTactic( + dribble_destination=tbots_cpp.createPointProto(dribble_destination), + final_dribble_orientation=tbots_cpp.createAngleProto( + final_dribble_orientation + ), + allow_excessive_dribbling=True, + ) + } + ) + if should_excessively_dribble: # Always and Eventually validation sets for excessive dribbling always_validation_sequence_set = [[]] @@ -138,41 +158,8 @@ def test_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( + setup=setup, 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, diff --git a/src/software/ai/hl/stp/tactic/goalie/BUILD b/src/software/ai/hl/stp/tactic/goalie/BUILD index abdb4d7636..5fbd6a27c4 100644 --- a/src/software/ai/hl/stp/tactic/goalie/BUILD +++ b/src/software/ai/hl/stp/tactic/goalie/BUILD @@ -58,8 +58,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:speed_threshold_helpers", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/tactic/goalie/goalie_tactic_test.py b/src/software/ai/hl/stp/tactic/goalie/goalie_tactic_test.py index ee0640db37..b2a271ed51 100644 --- a/src/software/ai/hl/stp/tactic/goalie/goalie_tactic_test.py +++ b/src/software/ai/hl/stp/tactic/goalie/goalie_tactic_test.py @@ -1,18 +1,18 @@ import pytest import software.python_bindings as tbots_cpp -from software.simulated_tests.robot_enters_region import * -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.ball_moves_in_direction import * -from software.simulated_tests.friendly_has_ball_possession import * -from software.simulated_tests.ball_speed_threshold import * -from software.simulated_tests.robot_speed_threshold import * -from software.simulated_tests.excessive_dribbling import * +from proto.import_all_protos import GoalieTactic, MaxAllowedSpeedMode +from software.simulated_tests.pytest_validations.robot_enters_region import * +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.ball_moves_in_direction import * +from software.simulated_tests.pytest_validations.friendly_has_ball_possession import * +from software.simulated_tests.pytest_validations.ball_speed_threshold import * +from software.simulated_tests.pytest_validations.robot_speed_threshold import * +from software.simulated_tests.pytest_validations.excessive_dribbling import * from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) from proto.message_translation.tbots_protobuf import create_world_state -from proto.ssl_gc_common_pb2 import Team @pytest.mark.parametrize( @@ -114,45 +114,24 @@ def test_goalie_blocks_shot( robot_initial_position, simulated_test_runner, ): - # Setup Robot - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - [], - blue_robot_locations=[robot_initial_position], - ball_location=ball_initial_position, - ball_velocity=ball_initial_velocity, - ), - ) - - # These aren't necessary for this test, but this is just an example - # of how to send commands to the simulator. - # - # NOTE: The gamecontroller responses are automatically handled by - # the gamecontroller context manager class - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.STOP, team=Team.UNKNOWN - ) - simulated_test_runner.gamecontroller.send_gc_command( - gc_command=Command.Type.FORCE_START, team=Team.BLUE - ) + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=[], + blue_robot_locations=[robot_initial_position], + ball_location=ball_initial_position, + ball_velocity=ball_initial_velocity, + ) + ) - # Setup Tactic - params = AssignedTacticPlayControlParams() - params.assigned_tactics[0].goalie.CopyFrom( - GoalieTactic(max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT) - ) - simulated_test_runner.blue_full_system_proto_unix_io.send_proto( - AssignedTacticPlayControlParams, params - ) + simulated_test_runner.set_tactics( + blue_tactics={ + 0: GoalieTactic( + max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT + ) + } + ) - # Setup no tactics on the enemy side - params = AssignedTacticPlayControlParams() - simulated_test_runner.yellow_full_system_proto_unix_io.send_proto( - AssignedTacticPlayControlParams, params - ) - - # Always Validation always_validation_sequence_set = [ [ RobotNeverEntersRegion( @@ -165,7 +144,6 @@ def test_goalie_blocks_shot( ] ] - # Eventually Validation eventually_validation_sequence_set = [ [ # Goalie should be in the defense area @@ -178,6 +156,7 @@ def test_goalie_blocks_shot( ] simulated_test_runner.run_test( + setup=setup, 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, @@ -212,35 +191,28 @@ def test_goalie_clears_from_dead_zone( should_clear, simulated_test_runner, ): - # Setup Robot - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - [], - blue_robot_locations=[ - tbots_cpp.Field.createSSLDivisionBField().friendlyDefenseArea().centre() - ], - ball_location=ball_position, - ball_velocity=tbots_cpp.Vector(0, 0), - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - params.assigned_tactics[0].goalie.CopyFrom( - GoalieTactic(max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT) - ) - simulated_test_runner.blue_full_system_proto_unix_io.send_proto( - AssignedTacticPlayControlParams, params - ) + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + [], + blue_robot_locations=[ + tbots_cpp.Field.createSSLDivisionBField() + .friendlyDefenseArea() + .centre() + ], + ball_location=ball_position, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) - # 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.set_tactics( + blue_tactics={ + 0: GoalieTactic( + max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT + ) + } + ) - # Always Validation always_validation_sequence_set = [ [ BallNeverEntersRegion( @@ -253,7 +225,6 @@ def test_goalie_clears_from_dead_zone( if should_clear: always_validation_sequence_set = [[]] - # Eventually Validation eventually_validation_sequence_set = [[]] if should_clear: eventually_validation_sequence_set = [ @@ -268,6 +239,7 @@ def test_goalie_clears_from_dead_zone( ] simulated_test_runner.run_test( + setup=setup, test_timeout_s=8, inv_eventually_validation_sequence_set=eventually_validation_sequence_set, inv_always_validation_sequence_set=always_validation_sequence_set, diff --git a/src/software/ai/hl/stp/tactic/halt/BUILD b/src/software/ai/hl/stp/tactic/halt/BUILD index 266edf3f11..463fdd978b 100644 --- a/src/software/ai/hl/stp/tactic/halt/BUILD +++ b/src/software/ai/hl/stp/tactic/halt/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -28,7 +30,7 @@ cc_test( ) cc_test( - name = "halt_tactic_test", + name = "halt_tactic_cpp_test", srcs = ["halt_tactic_test.cpp"], deps = [ ":halt_tactic", @@ -39,3 +41,16 @@ cc_test( "//software/test_util", ], ) + +py_test( + name = "halt_tactic_test", + srcs = ["halt_tactic_test.py"], + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/halt/halt_tactic_test.py b/src/software/ai/hl/stp/tactic/halt/halt_tactic_test.py new file mode 100644 index 0000000000..c039a89a62 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/halt/halt_tactic_test.py @@ -0,0 +1,75 @@ +import software.python_bindings as tbots_cpp + +from proto.import_all_protos import ( + HaltTactic, +) +from proto.message_translation.tbots_protobuf import create_world_state +from software.simulated_tests.pytest_validations.robot_speed_threshold import ( + RobotSpeedEventuallyBelowThreshold, +) +from software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) + + +def test_robot_already_stopped(simulated_test_runner): + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[tbots_cpp.Point(-3, 2.5), tbots_cpp.Point(0, 0)], + yellow_robot_locations=[tbots_cpp.Point(4, 0)], + ball_location=tbots_cpp.Point(0, 0.5), + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics(blue_tactics={1: HaltTactic()}) + + eventually_validations = [ + [ + # TODO: check for 1000 ticks + RobotSpeedEventuallyBelowThreshold(speed_threshold=0.001), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + inv_always_validation_sequence_set=[[]], + ag_always_validation_sequence_set=[[]], + ) + + +def test_robot_start_moving(simulated_test_runner): + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[tbots_cpp.Point(-3, 2.5), tbots_cpp.Point(0, 0)], + blue_robot_velocities=[tbots_cpp.Vector(0, 0), tbots_cpp.Vector(4, 4)], + yellow_robot_locations=[tbots_cpp.Point(4, 0)], + ball_location=tbots_cpp.Point(0, 0.5), + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics(blue_tactics={1: HaltTactic()}) + + eventually_validations = [ + [ + # TODO: check for 1000 ticks + RobotSpeedEventuallyBelowThreshold(speed_threshold=0.001), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + inv_always_validation_sequence_set=[[]], + ag_always_validation_sequence_set=[[]], + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/hl/stp/tactic/kick/BUILD b/src/software/ai/hl/stp/tactic/kick/BUILD index e95d29e14e..c0cf23b7a2 100644 --- a/src/software/ai/hl/stp/tactic/kick/BUILD +++ b/src/software/ai/hl/stp/tactic/kick/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -30,7 +32,7 @@ cc_test( ) cc_test( - name = "kick_tactic_test", + name = "kick_tactic_cpp_test", srcs = ["kick_tactic_test.cpp"], deps = [ ":kick_tactic", @@ -41,3 +43,16 @@ cc_test( "//software/test_util", ], ) + +py_test( + name = "kick_tactic_test", + srcs = ["kick_tactic_test.py"], + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/kick/kick_tactic_test.py b/src/software/ai/hl/stp/tactic/kick/kick_tactic_test.py new file mode 100644 index 0000000000..c9cdc26dfe --- /dev/null +++ b/src/software/ai/hl/stp/tactic/kick/kick_tactic_test.py @@ -0,0 +1,82 @@ +import pytest +import software.python_bindings as tbots_cpp +from software.py_constants import ROBOT_MAX_RADIUS_METERS + +from proto.import_all_protos import KickTactic +from proto.message_translation.tbots_protobuf import create_world_state +from software.simulated_tests.pytest_validations.ball_kicked_in_direction import ( + BallEventuallyKickedInDirection, +) +from software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) + + +@pytest.mark.parametrize( + "ball_offset_from_robot, angle_to_kick_at", + [ + # place the ball directly to the left of the robot + (tbots_cpp.Vector(0, 0.5), tbots_cpp.Angle.zero()), + # place the ball directly to the right of the robot + (tbots_cpp.Vector(0, -0.5), tbots_cpp.Angle.zero()), + # place the ball directly infront of the robot + (tbots_cpp.Vector(0.5, 0), tbots_cpp.Angle.zero()), + # place the ball directly behind the robot + (tbots_cpp.Vector(-0.5, 0), tbots_cpp.Angle.zero()), + # place the ball in the robots dribbler + (tbots_cpp.Vector(ROBOT_MAX_RADIUS_METERS, 0), tbots_cpp.Angle.zero()), + # Repeat the same tests but kick in the opposite direction + # place the ball directly to the left of the robot + (tbots_cpp.Vector(0, 0.5), tbots_cpp.Angle.half()), + # place the ball directly to the right of the robot + (tbots_cpp.Vector(0, -0.5), tbots_cpp.Angle.half()), + # place the ball directly infront of the robot + (tbots_cpp.Vector(0.5, 0), tbots_cpp.Angle.half()), + # place the ball directly behind the robot + (tbots_cpp.Vector(-0.5, 0), tbots_cpp.Angle.half()), + # place the ball in the robots dribbler + (tbots_cpp.Vector(ROBOT_MAX_RADIUS_METERS, 0), tbots_cpp.Angle.zero()), + ], +) +def test_kick(ball_offset_from_robot, angle_to_kick_at, simulated_test_runner): + robot_position = tbots_cpp.Point(0, 0) + ball_position = robot_position + ball_offset_from_robot + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + robot_position, + ], + yellow_robot_locations=[tbots_cpp.Point(4, 0)], + ball_location=ball_position, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: KickTactic( + kick_origin=tbots_cpp.createPointProto(ball_position), + kick_direction=tbots_cpp.createAngleProto(angle_to_kick_at), + kick_speed_meters_per_second=5.0, + ) + } + ) + + eventually_validations = [ + [ + BallEventuallyKickedInDirection(angle_to_kick_at), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validations, + ag_eventually_validation_sequence_set=eventually_validations, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/hl/stp/tactic/move/BUILD b/src/software/ai/hl/stp/tactic/move/BUILD index 82746f594b..2f0155cf34 100644 --- a/src/software/ai/hl/stp/tactic/move/BUILD +++ b/src/software/ai/hl/stp/tactic/move/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -29,7 +31,7 @@ cc_test( ) cc_test( - name = "move_tactic_test", + name = "move_tactic_cpp_test", srcs = ["move_tactic_test.cpp"], deps = [ ":move_tactic", @@ -42,3 +44,16 @@ cc_test( "//software/world", ], ) + +py_test( + name = "move_tactic_test", + srcs = ["move_tactic_test.py"], + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/move/move_tactic_test.py b/src/software/ai/hl/stp/tactic/move/move_tactic_test.py new file mode 100644 index 0000000000..21ca8d4da5 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/move/move_tactic_test.py @@ -0,0 +1,281 @@ +import pytest + +import software.python_bindings as tbots_cpp +from software.simulated_tests.pytest_validations.robot_at_position import ( + RobotEventuallyAtPosition, +) +from software.simulated_tests.pytest_validations.ball_kicked_in_direction import ( + BallEventuallyKickedInDirection, +) +from software.simulated_tests.pytest_validations.robot_at_orientation import ( + RobotEventuallyAtOrientation, +) +from software.simulated_tests.pytest_validations.ball_is_off_ground import ( + BallIsEventuallyOffGround, +) +from software.simulated_tests.pytest_validations.robot_at_angular_velocity import ( + RobotEventuallyAtAngularVelocity, +) +from software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) +from proto.message_translation.tbots_protobuf import create_world_state +from proto.import_all_protos import * + + +def test_move_across_field(simulated_test_runner): + initial_position = tbots_cpp.Point(-3, 1.5) + destination = tbots_cpp.Point(2.5, -1.1) + field = tbots_cpp.Field.createSSLDivisionBField() + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + initial_position, + ], + yellow_robot_locations=[ + tbots_cpp.Point(1, 0), + tbots_cpp.Point(1, 2.5), + tbots_cpp.Point(1, -2.5), + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + ], + ball_location=tbots_cpp.Point(4.5, -3), + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: MoveTactic( + destination=tbots_cpp.createPointProto(destination), + final_orientation=tbots_cpp.createAngleProto( + tbots_cpp.Angle.zero() + ), + ) + } + ) + + eventually_validation_sequence_set = [ + [ + # TODO: should also validate that the robot stays at destination for 1000 ticks after + RobotEventuallyAtPosition(1, destination), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + ) + + +def test_autochip_move(simulated_test_runner): + initial_position = tbots_cpp.Point(-3, 1.5) + destination = tbots_cpp.Point(0, 1.5) + field = tbots_cpp.Field.createSSLDivisionBField() + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + initial_position, + ], + yellow_robot_locations=[ + tbots_cpp.Point(1, 0), + tbots_cpp.Point(1, 2.5), + tbots_cpp.Point(1, -2.5), + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + ], + ball_location=destination, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: MoveTactic( + destination=tbots_cpp.createPointProto(destination), + final_orientation=tbots_cpp.createAngleProto( + tbots_cpp.Angle.zero() + ), + dribbler_mode=DribblerMode.OFF, + ball_collision_type=BallCollisionType.ALLOW, + auto_chip_or_kick=AutoChipOrKick(autochip_distance_meters=2.0), + max_allowed_speed_mode=MaxAllowedSpeedMode.COLLISIONS_ALLOWED, + obstacle_avoidance_mode=ObstacleAvoidanceMode.SAFE, + ) + } + ) + + eventually_validation_sequence_set = [ + [ + # TODO (#2558): should also validate that the robot stays at destination for 1000 ticks after + RobotEventuallyAtPosition(1, destination), + BallEventuallyKickedInDirection(tbots_cpp.Angle.zero()), + BallIsEventuallyOffGround(), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + test_timeout_s=10, + ) + + +def test_autokick_move(simulated_test_runner): + initial_position = tbots_cpp.Point(-1, -0.5) + destination = tbots_cpp.Point(-1, -1) + field = tbots_cpp.Field.createSSLDivisionBField() + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[initial_position], + blue_robot_orientations=[tbots_cpp.Angle.threeQuarter()], + yellow_robot_locations=[ + tbots_cpp.Point(1, 0), + tbots_cpp.Point(1, 2.5), + tbots_cpp.Point(1, -2.5), + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + ], + ball_location=destination, + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 0: MoveTactic( + destination=tbots_cpp.createPointProto(destination), + final_orientation=tbots_cpp.createAngleProto( + tbots_cpp.Angle.threeQuarter() + ), + dribbler_mode=DribblerMode.OFF, + ball_collision_type=BallCollisionType.ALLOW, + auto_chip_or_kick=AutoChipOrKick(autokick_speed_m_per_s=3.0), + max_allowed_speed_mode=MaxAllowedSpeedMode.COLLISIONS_ALLOWED, + obstacle_avoidance_mode=ObstacleAvoidanceMode.SAFE, + ) + } + ) + + eventually_validation_sequence_set = [ + [ + # TODO (#2558): should also validate that the robot stays at destination for 1000 ticks after + RobotEventuallyAtPosition(0, destination), + BallEventuallyKickedInDirection(tbots_cpp.Angle.threeQuarter()), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + test_timeout_s=5, + ) + + +@pytest.mark.parametrize( + "orientation, initial_position, destination, angular_velocity", + [ + # Robot facing right, should rotate counter clockwise to face up + ( + tbots_cpp.Angle.quarter(), + tbots_cpp.Point(2, 0), + tbots_cpp.Point(-2, 0), + tbots_cpp.Angle.fromDegrees(360), + ), + # Robot facing right, should rotate clockwise to face down + ( + tbots_cpp.Angle.threeQuarter(), + tbots_cpp.Point(2, 0), + tbots_cpp.Point(-2, 0), + tbots_cpp.Angle.fromDegrees(-360), + ), + # Robot facing right, should rotate counter clockwise to face up + ( + tbots_cpp.Angle.quarter(), + tbots_cpp.Point(0, 0), + tbots_cpp.Point(0, 0), + tbots_cpp.Angle.fromDegrees(360), + ), + # Robot facing right, should rotate clockwise to face down + ( + tbots_cpp.Angle.threeQuarter(), + tbots_cpp.Point(0, 0), + tbots_cpp.Point(0, 0), + tbots_cpp.Angle.fromDegrees(-360), + ), + # Robot facing right, should rotate counter clockwise to face left, slightly up + ( + tbots_cpp.Angle.fromDegrees(175), + tbots_cpp.Point(3, -1), + tbots_cpp.Point(0, 1), + tbots_cpp.Angle.fromDegrees(360), + ), + # Robot facing right, should rotate clockwise to face down + ( + tbots_cpp.Angle.fromDegrees(185), + tbots_cpp.Point(3, -1), + tbots_cpp.Point(0, 1), + tbots_cpp.Angle.fromDegrees(-360), + ), + ], +) +def test_spinning_move( + orientation, initial_position, destination, angular_velocity, simulated_test_runner +): + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[initial_position], + yellow_robot_locations=[tbots_cpp.Point(4, 0)], + ball_location=tbots_cpp.Point(1, 1), + ball_velocity=tbots_cpp.Vector(0, 0), + ), + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 0: MoveTactic( + destination=tbots_cpp.createPointProto(destination), + final_orientation=tbots_cpp.createAngleProto(orientation), + dribbler_mode=DribblerMode.OFF, + ball_collision_type=BallCollisionType.ALLOW, + auto_chip_or_kick=AutoChipOrKick(), + max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, + obstacle_avoidance_mode=ObstacleAvoidanceMode.SAFE, + ) + } + ) + + eventually_validation_sequence_set = [ + [ + # high threshold just to check direction of angular velocity + RobotEventuallyAtAngularVelocity(0, angular_velocity, 4), + RobotEventuallyAtPosition(0, destination), + RobotEventuallyAtOrientation(0, orientation), + # TODO: validate position and orientation for 1000 ticks + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/hl/stp/tactic/pass_defender/BUILD b/src/software/ai/hl/stp/tactic/pass_defender/BUILD index e87d8f757c..31b9d28f46 100644 --- a/src/software/ai/hl/stp/tactic/pass_defender/BUILD +++ b/src/software/ai/hl/stp/tactic/pass_defender/BUILD @@ -45,8 +45,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:speed_threshold_helpers", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/hl/stp/tactic/pass_defender/pass_defender_tactic_test.py b/src/software/ai/hl/stp/tactic/pass_defender/pass_defender_tactic_test.py index 021426462a..2414d7b3dd 100644 --- a/src/software/ai/hl/stp/tactic/pass_defender/pass_defender_tactic_test.py +++ b/src/software/ai/hl/stp/tactic/pass_defender/pass_defender_tactic_test.py @@ -1,13 +1,14 @@ import pytest import software.python_bindings as tbots_cpp -from software.simulated_tests.robot_enters_region import * -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.ball_moves_in_direction import * -from software.simulated_tests.friendly_has_ball_possession import * -from software.simulated_tests.ball_speed_threshold import * -from software.simulated_tests.robot_speed_threshold import * -from software.simulated_tests.excessive_dribbling import * +from proto.import_all_protos import PassDefenderTactic, BallStealMode +from software.simulated_tests.pytest_validations.robot_enters_region import * +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.ball_moves_in_direction import * +from software.simulated_tests.pytest_validations.friendly_has_ball_possession import * +from software.simulated_tests.pytest_validations.ball_speed_threshold import * +from software.simulated_tests.pytest_validations.robot_speed_threshold import * +from software.simulated_tests.pytest_validations.excessive_dribbling import * from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) @@ -35,36 +36,27 @@ def test_ball_chipped_on_intercept( position_to_block_from, simulated_test_runner, ): - # Setup Robot - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - [], - blue_robot_locations=[position_to_block_from], - ball_location=ball_initial_position, - ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - params.assigned_tactics[0].pass_defender.CopyFrom( - PassDefenderTactic( - position_to_block_from=tbots_cpp.createPointProto(position_to_block_from), - ball_steal_mode=BallStealMode.STEAL, + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + [], + blue_robot_locations=[position_to_block_from], + ball_location=ball_initial_position, + ball_velocity=ball_initial_velocity, + ) ) - ) - 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.set_tactics( + blue_tactics={ + 0: PassDefenderTactic( + position_to_block_from=tbots_cpp.createPointProto( + position_to_block_from + ), + ball_steal_mode=BallStealMode.STEAL, + ) + } + ) - # Always Validation always_validation_sequence_set = [ [ RobotNeverEntersRegion( @@ -79,10 +71,10 @@ def test_ball_chipped_on_intercept( ] ] - # Eventually Validation eventually_validation_sequence_set = [[BallSpeedEventuallyBelowThreshold(0.4)]] simulated_test_runner.run_test( + setup=setup, 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, @@ -117,36 +109,27 @@ def test_avoid_intercept_scenario( position_to_block_from, simulated_test_runner, ): - # Setup Robot - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - [], - blue_robot_locations=[position_to_block_from], - ball_location=ball_initial_position, - ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - params.assigned_tactics[0].pass_defender.CopyFrom( - PassDefenderTactic( - position_to_block_from=tbots_cpp.createPointProto(position_to_block_from), - ball_steal_mode=BallStealMode.STEAL, + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=[], + blue_robot_locations=[position_to_block_from], + ball_location=ball_initial_position, + ball_velocity=ball_initial_velocity, + ) ) - ) - 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.set_tactics( + blue_tactics={ + 0: PassDefenderTactic( + position_to_block_from=tbots_cpp.createPointProto( + position_to_block_from + ), + ball_steal_mode=BallStealMode.STEAL, + ) + } + ) - # Always Validation always_validation_sequence_set = [ [ # Defender should remain near position_to_block_from @@ -179,10 +162,10 @@ def test_avoid_intercept_scenario( ] ] - # Eventually Validation eventually_validation_sequence_set = [[BallSpeedEventuallyBelowThreshold(0.4)]] simulated_test_runner.run_test( + setup=setup, 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, @@ -259,35 +242,27 @@ def test_steal_ball( should_steal, simulated_test_runner, ): - # Setup Robot - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - blue_robot_locations=[position_to_block_from], - yellow_robot_locations=[enemy_kicker_position], - ball_location=ball_initial_position, - ball_velocity=ball_initial_velocity, - ), - ) + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[position_to_block_from], + yellow_robot_locations=[enemy_kicker_position], + ball_location=ball_initial_position, + ball_velocity=ball_initial_velocity, + ) + ) - # Setup Tactic - params = AssignedTacticPlayControlParams() - params.assigned_tactics[0].pass_defender.CopyFrom( - PassDefenderTactic( - position_to_block_from=tbots_cpp.createPointProto(position_to_block_from), - ball_steal_mode=BallStealMode.STEAL, + simulated_test_runner.set_tactics( + blue_tactics={ + 0: PassDefenderTactic( + position_to_block_from=tbots_cpp.createPointProto( + position_to_block_from + ), + ball_steal_mode=BallStealMode.STEAL, + ) + } ) - ) - 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 - ) - # Always Validation always_validation_sequence_set = [ [ RobotNeverEntersRegion( @@ -297,7 +272,7 @@ def test_steal_ball( ), NeverExcessivelyDribbles(), ] - ] # Eventually Validation + ] eventually_validation_sequence_set = [[BallSpeedEventuallyBelowThreshold(0.4)]] if should_steal: @@ -311,6 +286,7 @@ def test_steal_ball( ) simulated_test_runner.run_test( + setup=setup, 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, diff --git a/src/software/ai/hl/stp/tactic/penalty_setup_tactic_test.cpp b/src/software/ai/hl/stp/tactic/penalty_setup_tactic_test.cpp deleted file mode 100644 index 9535174636..0000000000 --- a/src/software/ai/hl/stp/tactic/penalty_setup_tactic_test.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include - -#include "software/ai/hl/stp/tactic/move/move_tactic.h" -#include "software/test_util/test_util.h" - -TEST(PenaltySetupTacticTest, constructor_test) -{ - PenaltySetupTactic tactic = - PenaltySetupTactic(std::make_shared()); -} diff --git a/src/software/ai/hl/stp/tactic/pivot_kick/BUILD b/src/software/ai/hl/stp/tactic/pivot_kick/BUILD index 34bc30121f..cd501b698a 100644 --- a/src/software/ai/hl/stp/tactic/pivot_kick/BUILD +++ b/src/software/ai/hl/stp/tactic/pivot_kick/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -32,7 +34,7 @@ cc_test( ) cc_test( - name = "pivot_kick_tactic_test", + name = "pivot_kick_tactic_cpp_test", srcs = ["pivot_kick_tactic_test.cpp"], deps = [ ":pivot_kick_tactic", @@ -45,3 +47,16 @@ cc_test( "//software/world", ], ) + +py_test( + name = "pivot_kick_tactic_test", + srcs = ["pivot_kick_tactic_test.py"], + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/pivot_kick/pivot_kick_tactic_test.py b/src/software/ai/hl/stp/tactic/pivot_kick/pivot_kick_tactic_test.py new file mode 100644 index 0000000000..727317c302 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/pivot_kick/pivot_kick_tactic_test.py @@ -0,0 +1,82 @@ +import pytest +import software.python_bindings as tbots_cpp +from software.py_constants import ROBOT_MAX_RADIUS_METERS + +from proto.import_all_protos import * +from proto.message_translation.tbots_protobuf import create_world_state +from software.simulated_tests.pytest_validations.ball_kicked_in_direction import ( + BallEventuallyKickedInDirection, +) +from software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) + + +@pytest.mark.parametrize( + "ball_offset_from_robot, angle_to_kick_at", + [ + # TODO (#2859): Flaky, the robot does not dribble far enough into the ball + # place the ball directly to the left of the robot + (tbots_cpp.Vector(0, 0.5), tbots_cpp.Angle.zero()), + # place the ball directly to the right of the robot + (tbots_cpp.Vector(0, -0.5), tbots_cpp.Angle.zero()), + # place the ball directly infront of the robot + (tbots_cpp.Vector(0.5, 0), tbots_cpp.Angle.zero()), + # place the ball directly behind the robot + (tbots_cpp.Vector(-0.5, 0), tbots_cpp.Angle.zero()), + # place the ball in the robots dribbler + (tbots_cpp.Vector(ROBOT_MAX_RADIUS_METERS, 0), tbots_cpp.Angle.zero()), + # Repeat test cases for kicking in opposite direction + (tbots_cpp.Vector(0, 0.5), tbots_cpp.Angle.half()), + (tbots_cpp.Vector(0, -0.5), tbots_cpp.Angle.half()), + (tbots_cpp.Vector(0.5, 0), tbots_cpp.Angle.half()), + # TODO (#2909): Enable test once the robot can turn faster and hits the ball with the dribbler + (tbots_cpp.Vector(-0.5, 0), tbots_cpp.Angle.half()), + (tbots_cpp.Vector(ROBOT_MAX_RADIUS_METERS, 0), tbots_cpp.Angle.half()), + ], +) +def test_pivot_kick(ball_offset_from_robot, angle_to_kick_at, simulated_test_runner): + robot_position = tbots_cpp.Point(0, 0) + ball_position = robot_position + ball_offset_from_robot + + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + robot_position, + ], + yellow_robot_locations=[tbots_cpp.Point(4, 0)], + ball_location=ball_position, + ball_velocity=tbots_cpp.Vector(0, 0), + ) + ) + + simulated_test_runner.set_tactics( + blue_tactics={ + 1: PivotKickTactic( + kick_origin=tbots_cpp.createPointProto(ball_position), + kick_direction=tbots_cpp.createAngleProto(angle_to_kick_at), + auto_chip_or_kick=AutoChipOrKick(autokick_speed_m_per_s=5.0), + ) + } + ) + + eventually_validation_sequence_set = [ + [ + BallEventuallyKickedInDirection(angle_to_kick_at), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + inv_always_validation_sequence_set=[[]], + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_always_validation_sequence_set=[[]], + test_timeout_s=5, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/hl/stp/tactic/receiver/BUILD b/src/software/ai/hl/stp/tactic/receiver/BUILD index 8548bfe80e..672482d2e5 100644 --- a/src/software/ai/hl/stp/tactic/receiver/BUILD +++ b/src/software/ai/hl/stp/tactic/receiver/BUILD @@ -1,3 +1,5 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + package(default_visibility = ["//visibility:public"]) cc_library( @@ -34,7 +36,7 @@ cc_test( ) cc_test( - name = "receiver_tactic_test", + name = "receiver_tactic_cpp_test", srcs = ["receiver_tactic_test.cpp"], deps = [ ":receiver_tactic", @@ -47,3 +49,14 @@ cc_test( "//software/world", ], ) + +py_test( + name = "receiver_tactic_test", + srcs = ["receiver_tactic_test.py"], + tags = ["exclusive"], + deps = [ + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py b/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py new file mode 100644 index 0000000000..d8350d40d5 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py @@ -0,0 +1,245 @@ +import pytest + +import software.python_bindings as tbots_cpp +from proto.import_all_protos import Pass, ReceiverTactic +from proto.message_translation.tbots_protobuf import create_world_state +from software.simulated_tests.pytest_validations.friendly_team_scored import ( + FriendlyTeamEventuallyScored, +) +from software.simulated_tests.pytest_validations.robot_at_orientation import ( + RobotEventuallyAtOrientation, +) +from software.simulated_tests.pytest_validations.robot_received_ball import ( + RobotEventuallyReceivedBall, +) +from software.simulated_tests.simulated_test_fixture import pytest_main + + +def calculate_ball_velocity(passer_point, receiver_point, speed): + """Calculate ball velocity based on pass trajectory""" + direction = receiver_point - passer_point + if direction.length() > 0: + direction = direction.normalize() + return direction * speed + + +@pytest.mark.parametrize( + "passer_point, receiver_point, pass_speed, robot_pos, robot_orientation, one_touch", + [ + # Robot already at receive point + ( + tbots_cpp.Point(0.0, 0.5), + tbots_cpp.Point(2, 2), + 4, + tbots_cpp.Point(2, 2), + tbots_cpp.Angle.zero(), + False, + ), + # Robot slightly off from receive point: test 1 + ( + tbots_cpp.Point(0.0, 0.4), + tbots_cpp.Point(2, 2), + 4, + tbots_cpp.Point(2, 1.5), + tbots_cpp.Angle.zero(), + False, + ), + # Robot slightly off from receive point: test 2 + ( + tbots_cpp.Point(0.0, 0.4), + tbots_cpp.Point(2, 2), + 4, + tbots_cpp.Point(2.5, 2.0), + tbots_cpp.Angle.zero(), + False, + ), + # Robot facing away from pass + ( + tbots_cpp.Point(0.0, 0.0), + tbots_cpp.Point(-3, 0), + 4, + tbots_cpp.Point(-3, 0), + tbots_cpp.Angle.half(), + False, + ), + # Robot facing towards pass + ( + tbots_cpp.Point(0.0, 0.0), + tbots_cpp.Point(-3, 0), + 4, + tbots_cpp.Point(-3, 0), + tbots_cpp.Angle.zero(), + False, + ), + # Robot facing towards pass speedy + ( + tbots_cpp.Point(0.0, 0.0), + tbots_cpp.Point(-3, 0), + 5, + tbots_cpp.Point(-3, 0), + tbots_cpp.Angle.zero(), + False, + ), + # Sharp angles, these are only a finite set of what + # sort of sharp angles we can achieve. + # + # If we are noticing issues with one-touch on the field, we should + # add more tests here and explore more of the "one-touch" space + # + # TODO (#2570): re-enable when one-touch aren't flaky for these tests + # TODO(#2909): re-enable once the robot can turn faster and hits the ball + # + # one touch robot on receiver point + # ( + # tbots_cpp.Point(2.0, 0.0), + # tbots_cpp.Point(3.5, 2.5), + # 3.5, + # tbots_cpp.Point(3.5, 2.5), + # tbots_cpp.Angle.zero(), + # True, + # ), + # ( + # tbots_cpp.Point(2.0, 0.0), + # tbots_cpp.Point(3.5, -2.5), + # 3.5, + # tbots_cpp.Point(3.5, -2.5), + # tbots_cpp.Angle.zero(), + # True, + # ), + # # one touch robot away from receiver point + # ( + # tbots_cpp.Point(1.5, 0.0), + # tbots_cpp.Point(2.5, 2.5), + # 3.5, + # tbots_cpp.Point(2.0, 2.5), + # tbots_cpp.Angle.zero(), + # True, + # ), + # ( + # tbots_cpp.Point(1.5, 0.0), + # tbots_cpp.Point(2.5, -2.5), + # 3.5, + # tbots_cpp.Point(2.0, -2.5), + # tbots_cpp.Angle.zero(), + # True, + # ), + ( + tbots_cpp.Point(4.0, 1.5), + tbots_cpp.Point(4, -1), + 5, + tbots_cpp.Point(4, -1), + tbots_cpp.Angle.half(), + True, + ), + # ( + # tbots_cpp.Point(4.0, 1.5), + # tbots_cpp.Point(3.5, -1), + # 5, + # tbots_cpp.Point(3.5, -1), + # tbots_cpp.Angle.zero(), + # True, + # ), + ( + tbots_cpp.Point(4.0, 1.5), + tbots_cpp.Point(3.0, -1), + 4.5, + tbots_cpp.Point(3.0, -1), + tbots_cpp.Angle.half(), + True, + ), + # ( + # tbots_cpp.Point(4.0, -1.5), + # tbots_cpp.Point(4, 1), + # 5, + # tbots_cpp.Point(4, 1), + # tbots_cpp.Angle.half(), + # True, + # ), + # ( + # tbots_cpp.Point(4.0, -1.5), + # tbots_cpp.Point(3.5, 1), + # 5, + # tbots_cpp.Point(3.5, 1), + # tbots_cpp.Angle.zero(), + # True, + # ), + ( + tbots_cpp.Point(4.0, -1.5), + tbots_cpp.Point(3, 1), + 4.5, + tbots_cpp.Point(3, 1), + tbots_cpp.Angle.zero(), + True, + ), + # ( + # tbots_cpp.Point(3.0, 0.0), + # tbots_cpp.Point(2, 0), + # 4, + # tbots_cpp.Point(2, 0), + # tbots_cpp.Angle.zero(), + # True, + # ), + ], +) +def test_receiver( + passer_point, + receiver_point, + pass_speed, + robot_pos, + robot_orientation, + one_touch, + simulated_test_runner, +): + def setup(*args): + ball_velocity = calculate_ball_velocity( + passer_point, receiver_point, pass_speed + ) + + simulated_test_runner.set_world_state( + create_world_state( + blue_robot_locations=[ + tbots_cpp.Point(-3, 2.5), + robot_pos, + ], + yellow_robot_locations=[], + ball_location=passer_point, + ball_velocity=ball_velocity, + ) + ) + + # Necessary since pass is a python keyword + receiver_args = { + "pass": Pass( + passer_point=tbots_cpp.createPointProto(passer_point), + receiver_point=tbots_cpp.createPointProto(receiver_point), + pass_speed_m_per_s=pass_speed, + ), + "disable_one_touch_shot": not one_touch, + } + + simulated_test_runner.set_tactics( + blue_tactics={1: ReceiverTactic(**receiver_args)} + ) + + eventually_validation_sequence_set = [ + [FriendlyTeamEventuallyScored()] + if one_touch + else [ + RobotEventuallyAtOrientation( + robot_id=1, + orientation=(passer_point - receiver_point).orientation(), + ), + RobotEventuallyReceivedBall(robot_id=1), + ] + ] + + simulated_test_runner.run_test( + setup=setup, + inv_eventually_validation_sequence_set=eventually_validation_sequence_set, + ag_eventually_validation_sequence_set=eventually_validation_sequence_set, + run_till_end=False, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/navigator/trajectory/BUILD b/src/software/ai/navigator/trajectory/BUILD index e3a243f39c..667735d3dd 100644 --- a/src/software/ai/navigator/trajectory/BUILD +++ b/src/software/ai/navigator/trajectory/BUILD @@ -157,7 +157,7 @@ py_test( deps = [ "//proto/message_translation:py_tbots_protobuf", "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/ai/navigator/trajectory/simulated_hrvo_test.py b/src/software/ai/navigator/trajectory/simulated_hrvo_test.py index cc5dc5d911..aef3cad55e 100644 --- a/src/software/ai/navigator/trajectory/simulated_hrvo_test.py +++ b/src/software/ai/navigator/trajectory/simulated_hrvo_test.py @@ -2,14 +2,15 @@ from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) -from software.simulated_tests.avoid_collisions import * +from software.simulated_tests.pytest_validations.avoid_collisions import * import software.python_bindings as tbots from software.py_constants import * from proto.message_translation.tbots_protobuf import create_world_state import math from proto.import_all_protos import * +from proto.ssl_gc_common_pb2 import Team from software.simulated_tests.simulated_test_fixture import SimulatedTestRunner -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( create_validation_types, create_validation_geometry, ) @@ -184,21 +185,29 @@ def hrvo_setup( ball_initial_vel = tbots.Point(0, 0) - # Game Controller Setup + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=enemy_robots_positions, + blue_robot_locations=friendly_robots_positions, + ball_location=ball_initial_pos, + ball_velocity=ball_initial_vel, + ) + ) + simulated_test_runner.send_gamecontroller_command( - gc_command=Command.Type.STOP, is_friendly=True + gc_command=Command.Type.STOP, team=Team.BLUE ) simulated_test_runner.send_gamecontroller_command( - gc_command=Command.Type.STOP, is_friendly=False + gc_command=Command.Type.STOP, team=Team.YELLOW ) simulated_test_runner.send_gamecontroller_command( - gc_command=Command.Type.FORCE_START, is_friendly=True + gc_command=Command.Type.FORCE_START, team=Team.BLUE ) - blue_params = AssignedTacticPlayControlParams() + blue_tactics = {} for index, destination in enumerate(friendly_robots_destinations): - blue_params = get_move_update_control_params( + blue_tactics[index] = get_move_tactics( index, destination, ( @@ -206,30 +215,17 @@ def hrvo_setup( if friendly_robots_final_orientations else desired_orientation ), - params=blue_params, ) - simulated_test_runner.set_tactics(blue_params, True) - - # Setup no tactics on the enemy side - yellow_params = AssignedTacticPlayControlParams() + yellow_tactics = {} for index, destination in enumerate(enemy_robots_destinations): - yellow_params = get_move_update_control_params( - index, destination, tbots.Angle.fromRadians(0), params=yellow_params + yellow_tactics[index] = get_move_tactics( + index, destination, tbots.Angle.fromRadians(0) ) - simulated_test_runner.set_tactics(yellow_params, False) - - # Setup Robots - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - yellow_robot_locations=enemy_robots_positions, - blue_robot_locations=friendly_robots_positions, - ball_location=ball_initial_pos, - ball_velocity=ball_initial_vel, - ), + simulated_test_runner.set_tactics( + blue_tactics=blue_tactics, yellow_tactics=yellow_tactics ) @@ -536,39 +532,28 @@ def get_reached_destination_validation(robot_destinations: list[tbots.Point]): ] -def get_move_update_control_params( +def get_move_tactics( robot_id: int, destination: tbots.Point, desired_orientation: tbots.Angle, - params: AssignedTacticPlayControlParams = None, ): - """Constructs the control params for a Move Tactic for a single robot - with the given data - And adds it to an existing or new AssignedTacticPlayControlParams message + """Constructs a Move Tactic for a single robot with the given data :param robot_id: the id of the robot who will be assigned these params :param destination: the destination of the robot :param desired_orientation: the desired orientation of the robot - :param params: AssignedTacticPlayControlParams message - if not None, add this robot's params to this - else, create a new message and add - :return: an AssignedTacticPlayControlParams message with this robot's params added + :return: a MoveTactic """ - params = params if params else AssignedTacticPlayControlParams() - params.assigned_tactics[robot_id].move.CopyFrom( - MoveTactic( - destination=Point(x_meters=destination.x(), y_meters=destination.y()), - final_orientation=Angle(radians=desired_orientation.toRadians()), - dribbler_mode=DribblerMode.OFF, - ball_collision_type=BallCollisionType.ALLOW, - auto_chip_or_kick=AutoChipOrKick(autokick_speed_m_per_s=0.0), - max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, - obstacle_avoidance_mode=ObstacleAvoidanceMode.AGGRESSIVE, - ) + return MoveTactic( + destination=Point(x_meters=destination.x(), y_meters=destination.y()), + final_orientation=Angle(radians=desired_orientation.toRadians()), + dribbler_mode=DribblerMode.OFF, + ball_collision_type=BallCollisionType.ALLOW, + auto_chip_or_kick=AutoChipOrKick(autokick_speed_m_per_s=0.0), + max_allowed_speed_mode=MaxAllowedSpeedMode.PHYSICAL_LIMIT, + obstacle_avoidance_mode=ObstacleAvoidanceMode.AGGRESSIVE, ) - return params - if __name__ == "__main__": pytest_main(__file__) diff --git a/src/software/field_tests/BUILD b/src/software/field_tests/BUILD index ad76156e99..b742fb70b5 100644 --- a/src/software/field_tests/BUILD +++ b/src/software/field_tests/BUILD @@ -16,7 +16,7 @@ py_library( "//software/networking/unix:threaded_unix_listener_py", "//software/networking/unix:threaded_unix_sender_py", "//software/simulated_tests:tbots_test_runner", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", "//software/thunderscope", "//software/thunderscope:constants", "//software/thunderscope:estop_helpers", @@ -39,7 +39,7 @@ py_test( deps = [ "//software:conftest", "//software/simulated_tests:tbots_test_runner", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) @@ -56,7 +56,7 @@ py_test( deps = [ "//software:conftest", "//software/simulated_tests:tbots_test_runner", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) @@ -68,7 +68,7 @@ py_test( ], deps = [ "//software:conftest", - "//software/simulated_tests:validation", + "//software/simulated_tests/pytest_validations:all_validations", requirement("pytest"), ], ) diff --git a/src/software/field_tests/field_test_fixture.py b/src/software/field_tests/field_test_fixture.py index 42c0c9f6ee..b8758292e7 100644 --- a/src/software/field_tests/field_test_fixture.py +++ b/src/software/field_tests/field_test_fixture.py @@ -7,7 +7,7 @@ import argparse from proto.import_all_protos import * -from software.simulated_tests import validation +from software.simulated_tests.pytest_validations import validation from software.thunderscope.constants import EstopMode, IndividualRobotMode from software.thunderscope.thunderscope import Thunderscope from software.thunderscope.proto_unix_io import ProtoUnixIO diff --git a/src/software/field_tests/movement_robot_field_test.py b/src/software/field_tests/movement_robot_field_test.py index bd436869bf..82f9698201 100644 --- a/src/software/field_tests/movement_robot_field_test.py +++ b/src/software/field_tests/movement_robot_field_test.py @@ -40,7 +40,7 @@ # ball_location=tbots_cpp.Point(1, 1), # ball_velocity=tbots_cpp.Point(0, 0), # ) -# simulated_test_runner.set_worldState(initial_worldstate) +# simulated_test_runner.set_world_state(initial_worldstate) # # # Setup Tactic # params = AssignedTacticPlayControlParams() @@ -103,11 +103,7 @@ def test_basic_rotation(field_test_runner): move_tactic.obstacle_avoidance_mode = ObstacleAvoidanceMode.SAFE # Setup Tactic - params = AssignedTacticPlayControlParams() - - params.assigned_tactics[id].move.CopyFrom(move_tactic) - - field_test_runner.set_tactics(params, True) + field_test_runner.set_tactics(blue_tactics={id: move_tactic}) field_test_runner.run_test( always_validation_sequence_set=[[]], eventually_validation_sequence_set=[[]], @@ -115,10 +111,8 @@ def test_basic_rotation(field_test_runner): ) # Send a halt tactic after the test finishes halt_tactic = HaltTactic() - params = AssignedTacticPlayControlParams() - params.assigned_tactics[id].stop.CopyFrom(halt_tactic) # send the halt tactic - field_test_runner.set_tactics(params, True) + field_test_runner.set_tactics(blue_tactics={id: halt_tactic}) # validate by eye logger.info(f"robot set to {angle} orientation") @@ -187,10 +181,7 @@ def test_one_robots_square(field_test_runner): for tactic in tactics: print(f"Going to {tactic.destination}") - params = AssignedTacticPlayControlParams() - params.assigned_tactics[id].move.CopyFrom(tactic) - - field_test_runner.set_tactics(params, True) + field_test_runner.set_tactics(blue_tactics={id: tactic}) field_test_runner.run_test( always_validation_sequence_set=[[]], eventually_validation_sequence_set=[[]], @@ -199,8 +190,7 @@ def test_one_robots_square(field_test_runner): # Send a halt tactic after the test finishes halt_tactic = HaltTactic() - params = AssignedTacticPlayControlParams() - params.assigned_tactics[id].stop.CopyFrom(halt_tactic) + field_test_runner.set_tactics(blue_tactics={id: halt_tactic}) if __name__ == "__main__": diff --git a/src/software/field_tests/passing_field_test.py b/src/software/field_tests/passing_field_test.py index 9899c6b63a..7f9f7abbb0 100644 --- a/src/software/field_tests/passing_field_test.py +++ b/src/software/field_tests/passing_field_test.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * from software.field_tests.field_test_fixture import * -from software.simulated_tests.friendly_receives_ball_slow import ( +from software.simulated_tests.pytest_validations.friendly_receives_ball_slow import ( FriendlyAlwaysReceivesBallSlow, ) from software.simulated_tests.simulated_test_fixture import ( @@ -41,9 +41,8 @@ def test_passing(field_test_runner): # Setup the passer's tactic # We use KickTactic since AttackerTactic shoots towards the goal instead if open # KickTactic just does the kick we want - params = AssignedTacticPlayControlParams() - params.assigned_tactics[passer_robot_id].kick.CopyFrom( - KickTactic( + tactics = { + passer_robot_id: KickTactic( kick_origin=Point( x_meters=pass_to_test.passerPoint().x(), y_meters=pass_to_test.passerPoint().y(), @@ -51,11 +50,11 @@ def test_passing(field_test_runner): kick_direction=Angle(radians=kick_vec.orientation().toRadians()), kick_speed_meters_per_second=pass_to_test.speed(), ) - ) + } # if we want a friendly robot to receive the pass if should_receive_pass: - # arguments for a ReceiverTactic + # arguments for a ReceiverTactic, required since pass is a python keyword receiver_args = { "pass": Pass( passer_point=Point( @@ -71,9 +70,7 @@ def test_passing(field_test_runner): "disable_one_touch_shot": True, } - params.assigned_tactics[receiver_robot_id].receiver.CopyFrom( - ReceiverTactic(**receiver_args) - ) + tactics[receiver_robot_id] = ReceiverTactic(**receiver_args) field = tbots_cpp.Field.createSSLDivisionBField() tbots_cpp.EighteenZonePitchDivision(field) @@ -89,7 +86,7 @@ def test_passing(field_test_runner): ] ] - field_test_runner.set_tactics(params, True) + field_test_runner.set_tactics(blue_tactics=tactics) field_test_runner.run_test( always_validation_sequence_set=always_validation_sequence_set, eventually_validation_sequence_set=[[]], @@ -98,10 +95,12 @@ def test_passing(field_test_runner): # Send a halt tactic after the test finishes halt_tactic = HaltTactic() - params = AssignedTacticPlayControlParams() - params.assigned_tactics[passer_robot_id].stop.CopyFrom(halt_tactic) - params.assigned_tactics[receiver_robot_id].stop.CopyFrom(halt_tactic) - field_test_runner.set_tactics(params, True) + field_test_runner.set_tactics( + blue_tactics={ + passer_robot_id: halt_tactic, + receiver_robot_id: halt_tactic, + } + ) if __name__ == "__main__": diff --git a/src/software/field_tests/pivot_kick_field_test.py b/src/software/field_tests/pivot_kick_field_test.py index 1b01d73c9c..884de63d1d 100644 --- a/src/software/field_tests/pivot_kick_field_test.py +++ b/src/software/field_tests/pivot_kick_field_test.py @@ -23,25 +23,25 @@ def test_pivot_kick(field_test_runner): ] ) - params = AssignedTacticPlayControlParams() - params.assigned_tactics[id].pivot_kick.CopyFrom( - PivotKickTactic( - kick_origin=Point(x_meters=-1.13, y_meters=0.75), - kick_direction=Angle(radians=-math.pi / 2), - auto_chip_or_kick=AutoChipOrKick(autokick_speed_m_per_s=5.0), - ) + field_test_runner.set_tactics( + blue_tactics={ + id: PivotKickTactic( + kick_origin=Point(x_meters=-1.13, y_meters=0.75), + kick_direction=Angle(radians=-math.pi / 2), + auto_chip_or_kick=AutoChipOrKick(autokick_speed_m_per_s=5.0), + ) + } ) - field_test_runner.set_tactics(params, True) field_test_runner.run_test( always_validation_sequence_set=[[]], eventually_validation_sequence_set=[[]], test_timeout_s=15, ) + # Send a halt tactic after the test finishes halt_tactic = HaltTactic() - params = AssignedTacticPlayControlParams() - params.assigned_tactics[id].stop.CopyFrom(halt_tactic) + field_test_runner.set_tactics(blue_tactics={id: halt_tactic}) if __name__ == "__main__": diff --git a/src/software/python_bindings.cpp b/src/software/python_bindings.cpp index 1b792a5dc3..8100a8143c 100644 --- a/src/software/python_bindings.cpp +++ b/src/software/python_bindings.cpp @@ -203,7 +203,13 @@ PYBIND11_MODULE(python_bindings, m) .def(py::self > Angle()) .def_static("fromRadians", &Angle::fromRadians) .def_static("fromDegrees", &Angle::fromDegrees) + .def_static("zero", &Angle::zero) + .def_static("quarter", &Angle::quarter) + .def_static("half", &Angle::half) + .def_static("threeQuarter", &Angle::threeQuarter) + .def_static("full", &Angle::full) .def("toRadians", &Angle::toRadians) + .def("minDiff", &Angle::minDiff) // Overloaded .def("__repr__", [](const Angle& a) diff --git a/src/software/simulated_tests/BUILD b/src/software/simulated_tests/BUILD index 2b33a53da5..87691949ec 100644 --- a/src/software/simulated_tests/BUILD +++ b/src/software/simulated_tests/BUILD @@ -9,23 +9,54 @@ compile_pip_requirements( requirements_txt = "requirements_lock.txt", ) -py_test( - name = "simulated_test_ball_model", +# TODO: move somewhere else since its use in field test fixture so this makes no sense in simulated_tests +py_library( + name = "tbots_test_runner", srcs = [ - "simulated_test_ball_model.py", + "tbots_test_runner.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", + data = [ + "//software:py_constants.so", ], deps = [ - "//software:conftest", - "//software/simulated_tests:speed_threshold_helpers", - "//software/simulated_tests:validation", - requirement("pytest"), + "//proto:import_all_protos", + "//software/logger:py_logger", + "//software/networking/unix:threaded_unix_listener_py", + "//software/networking/unix:threaded_unix_sender_py", + "//software/simulated_tests/pytest_validations:all_validations", + "//software/thunderscope", + "//software/thunderscope:config", + "//software/thunderscope:constants", + "//software/thunderscope:time_provider", + "//software/thunderscope/binary_context_managers:full_system", + "//software/thunderscope/binary_context_managers:game_controller", + "//software/thunderscope/binary_context_managers:simulator", + ], +) + +py_library( + name = "simulated_test_fixture", + srcs = [ + "simulated_test_fixture.py", + ], + data = [ + "//software:py_constants.so", + ], + deps = [ + ":tbots_test_runner", + "//proto:import_all_protos", + "//software/logger:py_logger", + "//software/networking/unix:threaded_unix_listener_py", + "//software/networking/unix:threaded_unix_sender_py", + "//software/simulated_tests/pytest_validations:all_validations", + "//software/thunderscope", + "//software/thunderscope/binary_context_managers:full_system", + "//software/thunderscope/binary_context_managers:game_controller", + "//software/thunderscope/binary_context_managers:simulator", ], ) +# TODO (#2581): remove cc_library( name = "simulated_er_force_sim_test_fixture", testonly = True, @@ -49,6 +80,7 @@ cc_library( ], ) +# TODO (#2581): remove cc_library( name = "simulated_er_force_sim_play_test_fixture", testonly = True, @@ -61,86 +93,19 @@ cc_library( ], ) -py_library( - name = "simulated_test_fixture", - srcs = [ - "simulated_test_fixture.py", - ], - data = [ - "//software:py_constants.so", - ], - deps = [ - ":tbots_test_runner", - "//proto:import_all_protos", - "//software/logger:py_logger", - "//software/networking/unix:threaded_unix_listener_py", - "//software/networking/unix:threaded_unix_sender_py", - "//software/simulated_tests:validation", - "//software/thunderscope", - "//software/thunderscope/binary_context_managers:full_system", - "//software/thunderscope/binary_context_managers:game_controller", - "//software/thunderscope/binary_context_managers:simulator", - ], -) - -py_library( - name = "tbots_test_runner", - srcs = [ - "tbots_test_runner.py", - ], - data = [ - "//software:py_constants.so", - ], - deps = [ - "//proto:import_all_protos", - "//software/logger:py_logger", - "//software/networking/unix:threaded_unix_listener_py", - "//software/networking/unix:threaded_unix_sender_py", - "//software/simulated_tests:validation", - "//software/thunderscope", - "//software/thunderscope:config", - "//software/thunderscope:constants", - "//software/thunderscope:time_provider", - "//software/thunderscope/binary_context_managers:full_system", - "//software/thunderscope/binary_context_managers:game_controller", - "//software/thunderscope/binary_context_managers:simulator", - ], -) - -py_library( - name = "validation", +# TODO (#2558): move this test somewhere else +py_test( + name = "simulated_test_ball_model", srcs = [ - "avoid_collisions.py", - "ball_enters_region.py", - "ball_is_off_ground.py", - "ball_moves_forward.py", - "ball_moves_from_rest.py", - "ball_moves_in_direction.py", - "ball_speed_threshold.py", - "ball_stops_in_region.py", - "excessive_dribbling.py", - "friendly_has_ball_possession.py", - "friendly_receives_ball_slow.py", - "friendly_team_scored.py", - "or_validation.py", - "robot_enters_placement_region.py", - "robot_enters_region.py", - "robot_enters_region_and_stops.py", - "robot_speed_threshold.py", - "validation.py", + "simulated_test_ball_model.py", ], - data = [ - "//software:py_constants.so", - "//software:python_bindings.so", + # TODO (#2619) Remove tag to run in parallel + tags = [ + "exclusive", ], deps = [ - "//proto:software_py_proto", - "//proto:tbots_py_proto", - "//software/logger:py_logger", + "//software:conftest", + "//software/simulated_tests/pytest_validations:all_validations", + requirement("pytest"), ], ) - -py_library( - name = "speed_threshold_helpers", - srcs = ["speed_threshold_helpers.py"], -) diff --git a/src/software/simulated_tests/ball_moves_forward.py b/src/software/simulated_tests/ball_moves_forward.py deleted file mode 100644 index 258b9db3bd..0000000000 --- a/src/software/simulated_tests/ball_moves_forward.py +++ /dev/null @@ -1,64 +0,0 @@ -import software.python_bindings as tbots_cpp -from proto.import_all_protos import * - -from software.simulated_tests.validation import ( - Validation, - create_validation_geometry, - create_validation_types, -) -from typing import override - - -class BallMovesForward(Validation): - """Checks if ball is moving forward, i.e. in the +x direction""" - - def __init__(self, initial_ball_position): - self.last_ball_position = initial_ball_position - - @override - def get_validation_status(self, world) -> ValidationStatus: - """Checks if ball is moving forward, i.e. in the +x direction - - :param world: The world msg to validate - :return: FAILING if ball doesn't move forward - PASSING if ball moves forward - """ - validation_status = ValidationStatus.FAILING - current_ball_position = tbots_cpp.createPoint( - world.ball.current_state.global_position - ) - - if current_ball_position.x() > self.last_ball_position.x(): - validation_status = ValidationStatus.PASSING - self.last_ball_position = current_ball_position - return validation_status - - @override - def get_validation_geometry(self, world) -> ValidationGeometry: - """(override) Shows the last ball position line""" - return create_validation_geometry( - [ - tbots_cpp.Rectangle( - tbots_cpp.Point( - self.last_ball_position.x(), - tbots_cpp.Field(world.field).fieldBoundary().yMin(), - ), - tbots_cpp.Point( - self.last_ball_position.x() + 0.01, - tbots_cpp.Field(world.field).fieldBoundary().yMax(), - ), - ) - ] - ) - - @override - def __repr__(self): - return "Check that the ball moves forward" - - -( - BallEventuallyMovesForward, - BallStopsMovingForward, - BallAlwaysMovesForward, - BallNeverMovesForward, -) = create_validation_types(BallMovesForward) diff --git a/src/software/simulated_tests/ball_moves_from_rest.py b/src/software/simulated_tests/ball_moves_from_rest.py deleted file mode 100644 index 11f182af15..0000000000 --- a/src/software/simulated_tests/ball_moves_from_rest.py +++ /dev/null @@ -1,61 +0,0 @@ -import software.python_bindings as tbots_cpp -from proto.import_all_protos import * - -from software.simulated_tests.validation import ( - Validation, - create_validation_geometry, - create_validation_types, -) -from typing import override - - -class BallMovesFromRest(Validation): - """Checks if ball has moved threshold meters from initial position""" - - def __init__(self, position, threshold=0.05): - """Constructor - - :param position: initial position of the ball - :param threshold: distance for which ball is considered to have moved - """ - self.initial_ball_position = position - self.threshold = threshold - - @override - def get_validation_status(self, world) -> ValidationStatus: - """Checks if ball has moved threshold meters from initial position. Default is 0.05m. - - :param world: The world msg to validate - :return: FAILING if ball doesn't move according to RoboCup rules - PASSING if ball moves according to RoboCup rules - """ - validation_status = ValidationStatus.FAILING - current_ball_position = tbots_cpp.createPoint( - world.ball.current_state.global_position - ) - - if ( - self.initial_ball_position - current_ball_position - ).length() > self.threshold: - validation_status = ValidationStatus.PASSING - - return validation_status - - @override - def get_validation_geometry(self, world) -> ValidationGeometry: - """(override) Shows the last ball position line""" - return create_validation_geometry( - [tbots_cpp.Circle(self.initial_ball_position, self.threshold)] - ) - - @override - def __repr__(self): - return "Check that the ball moves from rest" - - -( - BallEventuallyMovesFromRest, - BallStopsMovingFromRest, - BallAlwaysMovesFromRest, - BallNeverMovesFromRest, -) = create_validation_types(BallMovesFromRest) diff --git a/src/software/simulated_tests/pytest_validations/BUILD b/src/software/simulated_tests/pytest_validations/BUILD new file mode 100644 index 0000000000..dc1553e453 --- /dev/null +++ b/src/software/simulated_tests/pytest_validations/BUILD @@ -0,0 +1,38 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "all_validations", + srcs = [ + "avoid_collisions.py", + "ball_enters_region.py", + "ball_is_off_ground.py", + "ball_kicked_in_direction.py", + "ball_moves_in_direction.py", + "ball_speed_threshold.py", + "ball_stops_in_region.py", + "excessive_dribbling.py", + "friendly_has_ball_possession.py", + "friendly_receives_ball_slow.py", + "friendly_team_scored.py", + "or_validation.py", + "robot_at_angular_velocity.py", + "robot_at_orientation.py", + "robot_at_position.py", + "robot_enters_placement_region.py", + "robot_enters_region.py", + "robot_enters_region_and_stops.py", + "robot_received_ball.py", + "robot_speed_threshold.py", + "speed_threshold_helpers.py", + "validation.py", + ], + data = [ + "//software:py_constants.so", + "//software:python_bindings.so", + ], + deps = [ + "//proto:software_py_proto", + "//proto:tbots_py_proto", + "//software/logger:py_logger", + ], +) diff --git a/src/software/simulated_tests/pytest_validations/__init__.py b/src/software/simulated_tests/pytest_validations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/software/simulated_tests/avoid_collisions.py b/src/software/simulated_tests/pytest_validations/avoid_collisions.py similarity index 98% rename from src/software/simulated_tests/avoid_collisions.py rename to src/software/simulated_tests/pytest_validations/avoid_collisions.py index 28571474d4..9c057a23f0 100644 --- a/src/software/simulated_tests/avoid_collisions.py +++ b/src/software/simulated_tests/pytest_validations/avoid_collisions.py @@ -1,7 +1,7 @@ from software.py_constants import * import software.python_bindings as tbots from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_types, create_validation_geometry, diff --git a/src/software/simulated_tests/ball_enters_region.py b/src/software/simulated_tests/pytest_validations/ball_enters_region.py similarity index 96% rename from src/software/simulated_tests/ball_enters_region.py rename to src/software/simulated_tests/pytest_validations/ball_enters_region.py index 78c12c7895..3ba8310858 100644 --- a/src/software/simulated_tests/ball_enters_region.py +++ b/src/software/simulated_tests/pytest_validations/ball_enters_region.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/ball_is_off_ground.py b/src/software/simulated_tests/pytest_validations/ball_is_off_ground.py similarity index 97% rename from src/software/simulated_tests/ball_is_off_ground.py rename to src/software/simulated_tests/pytest_validations/ball_is_off_ground.py index 35b5b5b514..9272dcfc82 100644 --- a/src/software/simulated_tests/ball_is_off_ground.py +++ b/src/software/simulated_tests/pytest_validations/ball_is_off_ground.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/pytest_validations/ball_kicked_in_direction.py b/src/software/simulated_tests/pytest_validations/ball_kicked_in_direction.py new file mode 100644 index 0000000000..1d01f72309 --- /dev/null +++ b/src/software/simulated_tests/pytest_validations/ball_kicked_in_direction.py @@ -0,0 +1,70 @@ +import software.python_bindings as tbots_cpp +from proto.import_all_protos import * + +from software.simulated_tests.pytest_validations.validation import ( + Validation, + create_validation_geometry, + create_validation_types, +) +from typing import override + + +class BallKickedInDirection(Validation): + """Checks if the ball has been kicked in a specific direction""" + + LINE_LENGTH = 0.5 + + def __init__( + self, + kick_direction, + ): + """Constructs the validation object + + :param kick_direction: The expected direction the ball should be kicked (tbots_cpp.Angle) + """ + self.kick_direction = kick_direction + + @override + def get_validation_status(self, world) -> ValidationStatus: + """Checks if the ball has been kicked in the expected direction + + :param world: The world msg to validate + :return: FAILING if the ball has not been kicked in the expected direction + PASSING if the ball has been kicked in the expected direction + """ + # Convert ball proto to tbots_cpp ball + ball_pos = tbots_cpp.createPoint(world.ball.current_state.global_position) + ball_vel = tbots_cpp.createVector(world.ball.current_state.global_velocity) + + ball = tbots_cpp.Ball(ball_pos, ball_vel, tbots_cpp.Timestamp()) + + return ( + ValidationStatus.PASSING + if ball.hasBallBeenKicked(self.kick_direction) + else ValidationStatus.FAILING + ) + + @override + def get_validation_geometry(self, world) -> ValidationGeometry: + """Returns arrow geometry showing the expected kick direction + + :param world: The world msg to create validation geometry from + :return: ValidationGeometry containing an arrow in the kick direction + """ + ball_pos = world.ball.current_state.global_position + start = tbots_cpp.Point(ball_pos.x_meters, ball_pos.y_meters) + direction = tbots_cpp.Vector(1, 0).rotate(self.kick_direction) + end = start + direction * self.LINE_LENGTH + return create_validation_geometry([tbots_cpp.Segment(start, end)]) + + @override + def __repr__(self): + return f"Check that the ball is kicked in direction {self.kick_direction}" + + +( + BallEventuallyKickedInDirection, + _BallEventuallyNotKickedInDirection, # Doesn't make sense + _BallAlwaysKickedInDirection, # Doesn't make sense + BallNeverKickedInDirection, +) = create_validation_types(BallKickedInDirection) diff --git a/src/software/simulated_tests/ball_moves_in_direction.py b/src/software/simulated_tests/pytest_validations/ball_moves_in_direction.py similarity index 98% rename from src/software/simulated_tests/ball_moves_in_direction.py rename to src/software/simulated_tests/pytest_validations/ball_moves_in_direction.py index dff5046cd5..c85d278333 100644 --- a/src/software/simulated_tests/ball_moves_in_direction.py +++ b/src/software/simulated_tests/pytest_validations/ball_moves_in_direction.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/ball_speed_threshold.py b/src/software/simulated_tests/pytest_validations/ball_speed_threshold.py similarity index 94% rename from src/software/simulated_tests/ball_speed_threshold.py rename to src/software/simulated_tests/pytest_validations/ball_speed_threshold.py index 3fc61aecdb..b57cbf28b3 100644 --- a/src/software/simulated_tests/ball_speed_threshold.py +++ b/src/software/simulated_tests/pytest_validations/ball_speed_threshold.py @@ -1,9 +1,9 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * from software.py_constants import * -from software.simulated_tests.speed_threshold_helpers import * +from software.simulated_tests.pytest_validations.speed_threshold_helpers import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/ball_stops_in_region.py b/src/software/simulated_tests/pytest_validations/ball_stops_in_region.py similarity index 96% rename from src/software/simulated_tests/ball_stops_in_region.py rename to src/software/simulated_tests/pytest_validations/ball_stops_in_region.py index 369a1a70de..331e7f2f78 100644 --- a/src/software/simulated_tests/ball_stops_in_region.py +++ b/src/software/simulated_tests/pytest_validations/ball_stops_in_region.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/excessive_dribbling.py b/src/software/simulated_tests/pytest_validations/excessive_dribbling.py similarity index 97% rename from src/software/simulated_tests/excessive_dribbling.py rename to src/software/simulated_tests/pytest_validations/excessive_dribbling.py index b62045bb7c..9367874521 100644 --- a/src/software/simulated_tests/excessive_dribbling.py +++ b/src/software/simulated_tests/pytest_validations/excessive_dribbling.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import ValidationStatus, ValidationGeometry -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/friendly_has_ball_possession.py b/src/software/simulated_tests/pytest_validations/friendly_has_ball_possession.py similarity index 96% rename from src/software/simulated_tests/friendly_has_ball_possession.py rename to src/software/simulated_tests/pytest_validations/friendly_has_ball_possession.py index b63e4104fb..4af8c8da20 100644 --- a/src/software/simulated_tests/friendly_has_ball_possession.py +++ b/src/software/simulated_tests/pytest_validations/friendly_has_ball_possession.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/friendly_receives_ball_slow.py b/src/software/simulated_tests/pytest_validations/friendly_receives_ball_slow.py similarity index 97% rename from src/software/simulated_tests/friendly_receives_ball_slow.py rename to src/software/simulated_tests/pytest_validations/friendly_receives_ball_slow.py index 9b5b257ed1..e086a44296 100644 --- a/src/software/simulated_tests/friendly_receives_ball_slow.py +++ b/src/software/simulated_tests/pytest_validations/friendly_receives_ball_slow.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/friendly_team_scored.py b/src/software/simulated_tests/pytest_validations/friendly_team_scored.py similarity index 95% rename from src/software/simulated_tests/friendly_team_scored.py rename to src/software/simulated_tests/pytest_validations/friendly_team_scored.py index ecf68aa0d3..538485ad95 100644 --- a/src/software/simulated_tests/friendly_team_scored.py +++ b/src/software/simulated_tests/pytest_validations/friendly_team_scored.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/or_validation.py b/src/software/simulated_tests/pytest_validations/or_validation.py similarity index 96% rename from src/software/simulated_tests/or_validation.py rename to src/software/simulated_tests/pytest_validations/or_validation.py index efb96682ca..02c5259254 100644 --- a/src/software/simulated_tests/or_validation.py +++ b/src/software/simulated_tests/pytest_validations/or_validation.py @@ -1,5 +1,5 @@ from proto.validation_pb2 import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, ) from typing import override diff --git a/src/software/simulated_tests/pytest_validations/robot_at_angular_velocity.py b/src/software/simulated_tests/pytest_validations/robot_at_angular_velocity.py new file mode 100644 index 0000000000..86154ae10d --- /dev/null +++ b/src/software/simulated_tests/pytest_validations/robot_at_angular_velocity.py @@ -0,0 +1,66 @@ +import software.python_bindings as tbots_cpp +from proto.import_all_protos import * + +from software.simulated_tests.pytest_validations.validation import ( + Validation, + create_validation_geometry, + create_validation_types, +) +from typing import override + + +class RobotAtAngularVelocity(Validation): + """Checks if a robot is at a specific angular velocity""" + + def __init__(self, robot_id, angular_velocity, threshold=0.1): + """Constructs the validation object + + :param robot_id: The ID of the robot to check + :param angular_velocity: The expected angular velocity (tbots_cpp.Angle) + :param threshold: The tolerance in radians/s for the angular velocity check + """ + self.robot_id = robot_id + self.angular_velocity = angular_velocity + self.threshold = threshold + + @override + def get_validation_status(self, world) -> ValidationStatus: + """Checks if the robot is at the expected angular velocity + + :param world: The world msg to validate + :return: FAILING when the robot is not at the expected angular velocity + PASSING when the robot is at the expected angular velocity + """ + for robot in world.friendly_team.team_robots: + if robot.id == self.robot_id: + robot_ang_vel = tbots_cpp.Angle.fromRadians( + robot.current_state.global_angular_velocity.radians_per_second + ) + angle_diff = abs( + robot_ang_vel.toRadians() - self.angular_velocity.toRadians() + ) + if angle_diff < self.threshold: + return ValidationStatus.PASSING + return ValidationStatus.FAILING + + @override + def get_validation_geometry(self, world) -> ValidationGeometry: + """Returns the angular velocity of the robot for visualization + + :param world: The world msg to create validation geometry from + :return: ValidationGeometry showing the robot's angular velocity + """ + # TODO (#2558): create validation geometry + return create_validation_geometry() + + @override + def __repr__(self): + return f"Check that robot {self.robot_id} is at angular velocity {self.angular_velocity} (threshold={self.threshold})" + + +( + RobotEventuallyAtAngularVelocity, + RobotEventuallyNotAtAngularVelocity, + RobotAlwaysAtAngularVelocity, + RobotNeverAtAngularVelocity, +) = create_validation_types(RobotAtAngularVelocity) diff --git a/src/software/simulated_tests/pytest_validations/robot_at_orientation.py b/src/software/simulated_tests/pytest_validations/robot_at_orientation.py new file mode 100644 index 0000000000..d8e7c1e632 --- /dev/null +++ b/src/software/simulated_tests/pytest_validations/robot_at_orientation.py @@ -0,0 +1,74 @@ +import software.python_bindings as tbots_cpp +from proto.import_all_protos import * + +from software.simulated_tests.pytest_validations.validation import ( + Validation, + create_validation_geometry, + create_validation_types, +) +from typing import override + + +class RobotAtOrientation(Validation): + """Checks if a robot is at a specific orientation""" + + LINE_LENGTH = 0.3 + + def __init__(self, robot_id, orientation, threshold=0.1): + """Constructs the validation object + + :param robot_id: The ID of the robot to check + :param orientation: The expected orientation (tbots_cpp.Angle) + :param threshold: The tolerance in radians for the orientation check + """ + self.robot_id = robot_id + self.orientation = orientation + self.threshold = threshold + + @override + def get_validation_status(self, world) -> ValidationStatus: + """Checks if the robot is at the expected orientation + + :param world: The world msg to validate + :return: FAILING when the robot is not at the expected orientation + PASSING when the robot is at the expected orientation + """ + for robot in world.friendly_team.team_robots: + if robot.id == self.robot_id: + robot_angle = tbots_cpp.Angle.fromRadians( + robot.current_state.global_orientation.radians + ) + angle_diff = robot_angle.minDiff(self.orientation).toRadians() + if angle_diff < self.threshold: + return ValidationStatus.PASSING + return ValidationStatus.FAILING + + @override + def get_validation_geometry(self, world) -> ValidationGeometry: + """Returns the orientation of the robot for visualization + + :param world: The world msg to create validation geometry from + :return: ValidationGeometry containing the robot's orientation + """ + # TODO (#2558): create better validation geometry + for robot in world.friendly_team.team_robots: + if robot.id == self.robot_id: + robot_pos = tbots_cpp.createPoint(robot.current_state.global_position) + direction = tbots_cpp.Vector(1, 0).rotate(self.orientation) + end_point = robot_pos + direction * self.LINE_LENGTH + return create_validation_geometry( + [tbots_cpp.Segment(robot_pos, end_point)] + ) + return create_validation_geometry() + + @override + def __repr__(self): + return f"Check that robot {self.robot_id} is at orientation {self.orientation} (threshold={self.threshold})" + + +( + RobotEventuallyAtOrientation, + RobotEventuallyNotAtOrientation, + RobotAlwaysAtOrientation, + RobotNeverAtOrientation, +) = create_validation_types(RobotAtOrientation) diff --git a/src/software/simulated_tests/pytest_validations/robot_at_position.py b/src/software/simulated_tests/pytest_validations/robot_at_position.py new file mode 100644 index 0000000000..1a391c34b5 --- /dev/null +++ b/src/software/simulated_tests/pytest_validations/robot_at_position.py @@ -0,0 +1,63 @@ +import software.python_bindings as tbots_cpp +from proto.import_all_protos import * + +from software.simulated_tests.pytest_validations.validation import ( + Validation, + ValidationStatus, + create_validation_geometry, + create_validation_types, +) +from typing import override + + +class RobotAtPosition(Validation): + """Checks if a specific robot is at a given position within a threshold distance.""" + + def __init__( + self, robot_id: int, position: tbots_cpp.Point, threshold: float = 0.05 + ): + """Initializes the validation class with robot id, position, and threshold + + :param robot_id: the id of the robot to check + :param position: the target position the robot should be at + :param threshold: the distance threshold for the robot to be considered at position + """ + self.robot_id = robot_id + self.position = position + self.threshold = threshold + + @override + def get_validation_status(self, world) -> ValidationStatus: + """Checks if the robot is at the target position + + :param world: The world msg to validate + :returns: PASSING if the robot is at the target position within threshold + FAILING otherwise + """ + for robot in world.friendly_team.team_robots: + if robot.id == self.robot_id: + robot_pos = tbots_cpp.createPoint(robot.current_state.global_position) + distance = (robot_pos - self.position).length() + if distance <= self.threshold: + return ValidationStatus.PASSING + break + + return ValidationStatus.FAILING + + @override + def get_validation_geometry(self, world) -> ValidationGeometry: + """(override) shows the target position""" + circle = tbots_cpp.Circle(self.position, self.threshold) + return create_validation_geometry([circle]) + + @override + def __repr__(self): + return f"Robot {self.robot_id} at position {self.position} with threshold {self.threshold}" + + +( + RobotEventuallyAtPosition, + RobotEventuallyNotAtPosition, + RobotAlwaysAtPosition, + RobotAlwaysNotAtPosition, +) = create_validation_types(RobotAtPosition) diff --git a/src/software/simulated_tests/robot_enters_placement_region.py b/src/software/simulated_tests/pytest_validations/robot_enters_placement_region.py similarity index 97% rename from src/software/simulated_tests/robot_enters_placement_region.py rename to src/software/simulated_tests/pytest_validations/robot_enters_placement_region.py index bb23adff33..978402f149 100644 --- a/src/software/simulated_tests/robot_enters_placement_region.py +++ b/src/software/simulated_tests/pytest_validations/robot_enters_placement_region.py @@ -2,7 +2,7 @@ from software.py_constants import ENEMY_BALL_PLACEMENT_DISTANCE_METERS from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/robot_enters_region.py b/src/software/simulated_tests/pytest_validations/robot_enters_region.py similarity index 97% rename from src/software/simulated_tests/robot_enters_region.py rename to src/software/simulated_tests/pytest_validations/robot_enters_region.py index 2f7ac6d1c2..1ddba832b9 100644 --- a/src/software/simulated_tests/robot_enters_region.py +++ b/src/software/simulated_tests/pytest_validations/robot_enters_region.py @@ -1,7 +1,7 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/robot_enters_region_and_stops.py b/src/software/simulated_tests/pytest_validations/robot_enters_region_and_stops.py similarity index 95% rename from src/software/simulated_tests/robot_enters_region_and_stops.py rename to src/software/simulated_tests/pytest_validations/robot_enters_region_and_stops.py index 78210c6f43..2a9982f5be 100644 --- a/src/software/simulated_tests/robot_enters_region_and_stops.py +++ b/src/software/simulated_tests/pytest_validations/robot_enters_region_and_stops.py @@ -2,11 +2,13 @@ from software.py_constants import * from proto.import_all_protos import * -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( create_validation_types, create_validation_geometry, ) -from software.simulated_tests.robot_enters_region import RobotEntersRegion +from software.simulated_tests.pytest_validations.robot_enters_region import ( + RobotEntersRegion, +) from typing import override diff --git a/src/software/simulated_tests/pytest_validations/robot_received_ball.py b/src/software/simulated_tests/pytest_validations/robot_received_ball.py new file mode 100644 index 0000000000..eb0b43f5c8 --- /dev/null +++ b/src/software/simulated_tests/pytest_validations/robot_received_ball.py @@ -0,0 +1,60 @@ +import software.python_bindings as tbots_cpp +from proto.import_all_protos import * + +from software.simulated_tests.pytest_validations.validation import ( + Validation, + create_validation_geometry, + create_validation_types, +) +from typing import override + + +class RobotReceivedBall(Validation): + """Checks if a specific robot has received the ball (ball is near dribbler)""" + + def __init__(self, robot_id, tolerance=0.02): + """Constructs the validation object + + :param robot_id: The ID of the robot to check + :param tolerance: The tolerance for when we check if the robot has the ball + """ + self.robot_id = robot_id + self.tolerance = tolerance + + @override + def get_validation_status(self, world) -> ValidationStatus: + """Checks if the specific robot has received the ball + + :param world: The world msg to validate + :return: FAILING when the robot does not have the ball + PASSING when the robot has the ball + """ + ball_position = tbots_cpp.createPoint(world.ball.current_state.global_position) + for robot in world.friendly_team.team_robots: + if robot.id == self.robot_id: + if tbots_cpp.Robot(robot).isNearDribbler(ball_position, self.tolerance): + return ValidationStatus.PASSING + return ValidationStatus.FAILING + + @override + def get_validation_geometry(self, world) -> ValidationGeometry: + """Highlights the dribbler area of the robot""" + # TODO (#2558): create better validation geometry + for robot in world.friendly_team.team_robots: + if robot.id == self.robot_id: + return create_validation_geometry( + [tbots_cpp.Robot(robot).dribblerArea()] + ) + return create_validation_geometry() + + @override + def __repr__(self): + return f"Check that robot {self.robot_id} has received the ball" + + +( + RobotEventuallyReceivedBall, + RobotEventuallyDidNotReceiveBall, + RobotAlwaysHasBall, + RobotNeverHasBall, +) = create_validation_types(RobotReceivedBall) diff --git a/src/software/simulated_tests/robot_speed_threshold.py b/src/software/simulated_tests/pytest_validations/robot_speed_threshold.py similarity index 94% rename from src/software/simulated_tests/robot_speed_threshold.py rename to src/software/simulated_tests/pytest_validations/robot_speed_threshold.py index 52a9687ba7..5cf5474221 100644 --- a/src/software/simulated_tests/robot_speed_threshold.py +++ b/src/software/simulated_tests/pytest_validations/robot_speed_threshold.py @@ -1,11 +1,11 @@ import software.python_bindings as tbots_cpp from proto.import_all_protos import * from software.py_constants import * -from software.simulated_tests.speed_threshold_helpers import * +from software.simulated_tests.pytest_validations.speed_threshold_helpers import * from typing import override -from software.simulated_tests.validation import ( +from software.simulated_tests.pytest_validations.validation import ( Validation, create_validation_geometry, create_validation_types, diff --git a/src/software/simulated_tests/speed_threshold_helpers.py b/src/software/simulated_tests/pytest_validations/speed_threshold_helpers.py similarity index 100% rename from src/software/simulated_tests/speed_threshold_helpers.py rename to src/software/simulated_tests/pytest_validations/speed_threshold_helpers.py diff --git a/src/software/simulated_tests/validation.py b/src/software/simulated_tests/pytest_validations/validation.py similarity index 100% rename from src/software/simulated_tests/validation.py rename to src/software/simulated_tests/pytest_validations/validation.py index 3b5e70d538..1191bd1d99 100644 --- a/src/software/simulated_tests/validation.py +++ b/src/software/simulated_tests/pytest_validations/validation.py @@ -137,6 +137,44 @@ def flip_validation(self, world): return eventually_true, eventually_false, always_true, always_false +def create_validation_geometry(geometry=[]) -> ValidationGeometry: + """Creates a ValidationGeometry which is a visual representation of the + validation to be rendered as either green (PASSING) or red (FAILING) + + Given a list of (vectors, polygons, circles), creates a ValidationGeometry + proto containing the protobuf representations. + + :param geometry: A list of geom + :return: ValidationGeometry + """ + validation_geometry = ValidationGeometry() + + CREATE_PROTO_DISPATCH = { + tbots_cpp.Vector.__name__: tbots_cpp.createVectorProto, + tbots_cpp.Polygon.__name__: tbots_cpp.createPolygonProto, + tbots_cpp.Rectangle.__name__: tbots_cpp.createPolygonProto, + tbots_cpp.Circle.__name__: tbots_cpp.createCircleProto, + tbots_cpp.Segment.__name__: tbots_cpp.createSegmentProto, + tbots_cpp.Stadium.__name__: tbots_cpp.createStadiumProto, + } + + ADD_TO_VALIDATION_GEOMETRY_DISPATCH = { + tbots_cpp.Vector.__name__: validation_geometry.vectors.append, + tbots_cpp.Polygon.__name__: validation_geometry.polygons.append, + tbots_cpp.Rectangle.__name__: validation_geometry.polygons.append, + tbots_cpp.Circle.__name__: validation_geometry.circles.append, + tbots_cpp.Segment.__name__: validation_geometry.segments.append, + tbots_cpp.Stadium.__name__: validation_geometry.stadiums.append, + } + + for geom in geometry: + ADD_TO_VALIDATION_GEOMETRY_DISPATCH[type(geom).__name__]( + CREATE_PROTO_DISPATCH[type(geom).__name__](geom) + ) + + return validation_geometry + + def run_validation_sequence_sets( world, eventually_validation_sequence_set, always_validation_sequence_set ): @@ -219,41 +257,3 @@ def check_validation(validation_proto_set): for validation_proto in validation_proto_set.validations: if validation_proto.status == ValidationStatus.FAILING: raise AssertionError(validation_proto.failure_msg) - - -def create_validation_geometry(geometry=[]) -> ValidationGeometry: - """Creates a ValidationGeometry which is a visual representation of the - validation to be rendered as either green (PASSING) or red (FAILING) - - Given a list of (vectors, polygons, circles), creates a ValidationGeometry - proto containing the protobuf representations. - - :param geometry: A list of geom - :return: ValidationGeometry - """ - validation_geometry = ValidationGeometry() - - CREATE_PROTO_DISPATCH = { - tbots_cpp.Vector.__name__: tbots_cpp.createVectorProto, - tbots_cpp.Polygon.__name__: tbots_cpp.createPolygonProto, - tbots_cpp.Rectangle.__name__: tbots_cpp.createPolygonProto, - tbots_cpp.Circle.__name__: tbots_cpp.createCircleProto, - tbots_cpp.Segment.__name__: tbots_cpp.createSegmentProto, - tbots_cpp.Stadium.__name__: tbots_cpp.createStadiumProto, - } - - ADD_TO_VALIDATION_GEOMETRY_DISPATCH = { - tbots_cpp.Vector.__name__: validation_geometry.vectors.append, - tbots_cpp.Polygon.__name__: validation_geometry.polygons.append, - tbots_cpp.Rectangle.__name__: validation_geometry.polygons.append, - tbots_cpp.Circle.__name__: validation_geometry.circles.append, - tbots_cpp.Segment.__name__: validation_geometry.segments.append, - tbots_cpp.Stadium.__name__: validation_geometry.stadiums.append, - } - - for geom in geometry: - ADD_TO_VALIDATION_GEOMETRY_DISPATCH[type(geom).__name__]( - CREATE_PROTO_DISPATCH[type(geom).__name__](geom) - ) - - return validation_geometry diff --git a/src/software/simulated_tests/simulated_test_ball_model.py b/src/software/simulated_tests/simulated_test_ball_model.py index d1a845b2b7..55607bd139 100644 --- a/src/software/simulated_tests/simulated_test_ball_model.py +++ b/src/software/simulated_tests/simulated_test_ball_model.py @@ -1,14 +1,14 @@ import pytest import software.python_bindings as tbots_cpp -from software.simulated_tests.robot_enters_region import * -from software.simulated_tests.ball_enters_region import * -from software.simulated_tests.ball_moves_in_direction import * -from software.simulated_tests.friendly_has_ball_possession import * -from software.simulated_tests.ball_speed_threshold import * -from software.simulated_tests.robot_speed_threshold import * -from software.simulated_tests.ball_stops_in_region import * -from software.simulated_tests.excessive_dribbling import * +from software.simulated_tests.pytest_validations.robot_enters_region import * +from software.simulated_tests.pytest_validations.ball_enters_region import * +from software.simulated_tests.pytest_validations.ball_moves_in_direction import * +from software.simulated_tests.pytest_validations.friendly_has_ball_possession import * +from software.simulated_tests.pytest_validations.ball_speed_threshold import * +from software.simulated_tests.pytest_validations.robot_speed_threshold import * +from software.simulated_tests.pytest_validations.ball_stops_in_region import * +from software.simulated_tests.pytest_validations.excessive_dribbling import * from proto.message_translation.tbots_protobuf import create_world_state from software.simulated_tests.simulated_test_fixture import ( pytest_main, @@ -48,29 +48,17 @@ def test_simulator_move_ball( ball_initial_velocity, simulated_test_runner, ): - # Setup Ball - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - [], - blue_robot_locations=[], - ball_location=ball_initial_position, - ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - - 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 - ) + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=[], + blue_robot_locations=[], + ball_location=ball_initial_position, + ball_velocity=ball_initial_velocity, + ) + ) + + simulated_test_runner.set_tactics() # expected ball position initial_v = ball_initial_velocity.length() @@ -105,9 +93,10 @@ def test_simulator_move_ball( ] simulated_test_runner.run_test( - test_timeout_s=8, + setup=setup, inv_eventually_validation_sequence_set=eventually_validation_sequence_set, inv_always_validation_sequence_set=always_validation_sequence_set, + test_timeout_s=8, ) @@ -116,40 +105,17 @@ def test_ball_robot_collision(simulated_test_runner): ball_initial_velocity = tbots_cpp.Vector(2.5, 0) robot_position = tbots_cpp.Point(2.5, 0) - # Setup Robot - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - [], - blue_robot_locations=[robot_position], - ball_location=ball_initial_position, - ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Ball - simulated_test_runner.simulator_proto_unix_io.send_proto( - WorldState, - create_world_state( - [], - blue_robot_locations=[], - ball_location=ball_initial_position, - ball_velocity=ball_initial_velocity, - ), - ) - - # Setup Tactic - params = AssignedTacticPlayControlParams() - - simulated_test_runner.blue_full_system_proto_unix_io.send_proto( - AssignedTacticPlayControlParams, params - ) + def setup(*args): + simulated_test_runner.set_world_state( + create_world_state( + yellow_robot_locations=[], + blue_robot_locations=[robot_position], + ball_location=ball_initial_position, + ball_velocity=ball_initial_velocity, + ), + ) - # 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.set_tactics() # expected ball position initial_v = ball_initial_velocity.length() @@ -170,9 +136,6 @@ def test_ball_robot_collision(simulated_test_runner): distance_from_robot = (ball_expected_position - robot_position).length() - # Always Validation - always_validation_sequence_set = [] - # Eventually Validation eventually_validation_sequence_set = [ [ @@ -183,11 +146,11 @@ def test_ball_robot_collision(simulated_test_runner): ] simulated_test_runner.run_test( + setup=setup, 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, ) if __name__ == "__main__": - # Run the test, -s disables all capturing at -vv increases verbosity pytest_main(__file__) diff --git a/src/software/simulated_tests/simulated_test_fixture.py b/src/software/simulated_tests/simulated_test_fixture.py index ad2d319dbb..4e3bd1aeb9 100644 --- a/src/software/simulated_tests/simulated_test_fixture.py +++ b/src/software/simulated_tests/simulated_test_fixture.py @@ -8,7 +8,7 @@ import pytest from proto.import_all_protos import * -from software.simulated_tests import validation +from software.simulated_tests.pytest_validations import validation from software.simulated_tests.tbots_test_runner import TbotsTestRunner from software.thunderscope.thunderscope import Thunderscope from software.thunderscope.proto_unix_io import ProtoUnixIO @@ -17,6 +17,7 @@ from software.thunderscope.binary_context_managers.simulator import Simulator from software.thunderscope.binary_context_managers.game_controller import Gamecontroller from software.thunderscope.thunderscope_config import configure_simulated_test_view +from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer from software.logger.logger import create_logger from typing import override @@ -26,7 +27,6 @@ LAUNCH_DELAY_S = 0.1 WORLD_BUFFER_TIMEOUT = 0.5 PROCESS_BUFFER_DELAY_S = 0.01 -TEST_START_DELAY_S = 0.01 PAUSE_AFTER_FAIL_DELAY_S = 3 @@ -61,7 +61,7 @@ def __init__( self.simulator_proto_unix_io = simulator_proto_unix_io @override - def set_worldState(self, worldstate: WorldState): + def set_world_state(self, worldstate: WorldState): """Sets the simulation worldstate :param worldstate: proto containing the desired worldstate @@ -90,6 +90,31 @@ def __stopper(self, delay=PROCESS_BUFFER_DELAY_S): if self.thunderscope: self.thunderscope.close() + def sync_setup(self, setup, param): + """Run setup until simulator has received game state + + :param setup: Function that sets up the world state + :param param: Parameter passed into setup + """ + world_state_received_buffer = ThreadSafeBuffer(1, WorldStateReceivedTrigger) + self.simulator_proto_unix_io.register_observer( + WorldStateReceivedTrigger, world_state_received_buffer + ) + + while True: + setup(param) + + try: + world_state_received_buffer.get( + block=True, timeout=WORLD_BUFFER_TIMEOUT + ) + except queue.Empty: + # Did not receive a response within timeout period + continue + else: + # Received a response from the simulator + break + def runner( self, always_validation_sequence_set=[[]], @@ -97,7 +122,7 @@ def runner( test_timeout_s=3, tick_duration_s=0.0166, # Default to 60hz ci_cmd_with_delay=[], - run_till_end=True, + run_till_end=False, ): """Run a test @@ -259,14 +284,6 @@ def run_test( test_timeout_s[index] if type(test_timeout_s) == list else test_timeout_s ) - # Start the test with a delay to allow the simulator to receive - # the initial world state. Without this delay, the SimulatorTick - # message may be received before the initial world state, causing - # the world to be empty, failing some AlwaysValidations - # TODO (#2858): Replace delay with an actual feedback from the simulator - # for when it has received the initial world state - time.sleep(TEST_START_DELAY_S) - # If thunderscope is enabled, run the test in a thread and show # thunderscope on this thread. The excepthook is setup to catch # any test failures and propagate them to the main thread @@ -332,7 +349,7 @@ def run_test( """ threading.excepthook = self.excepthook - setup(params[0]) + super().sync_setup(setup, params[0]) super().run_test( inv_always_validation_sequence_set, @@ -377,7 +394,7 @@ def run_test( # Catches Assertion Error thrown by failing test and increments counter # Calculates overall results and prints them for x in range(len(params)): - setup(params[x]) + super().sync_setup(setup, params[x]) try: super().run_test( diff --git a/src/software/simulated_tests/tbots_test_runner.py b/src/software/simulated_tests/tbots_test_runner.py index 0f69c98730..64e50366f3 100644 --- a/src/software/simulated_tests/tbots_test_runner.py +++ b/src/software/simulated_tests/tbots_test_runner.py @@ -1,7 +1,6 @@ from proto.import_all_protos import * from software.logger.logger import create_logger from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer -from proto.ssl_gc_common_pb2 import Team from abc import abstractmethod logger = create_logger(__name__) @@ -72,7 +71,7 @@ def __init__( def send_gamecontroller_command( self, gc_command: proto.ssl_gc_state_pb2.Command, - is_friendly: bool, + team: proto.ssl_gc_common_pb2.Team, final_ball_placement_point=None, ): """Sends a gamecontroller command that is to be broadcasted to the given team @@ -81,13 +80,6 @@ def send_gamecontroller_command( :param is_friendly: whether the command should be sent to the friendly team :param final_ball_placement_point: where to place the ball in ball placement """ - friendly_team, enemy_team = ( - (Team.YELLOW, Team.BLUE) - if self.is_yellow_friendly - else (Team.BLUE, Team.YELLOW) - ) - team = friendly_team if is_friendly else enemy_team - self.gamecontroller.send_gc_command( gc_command=gc_command, team=team, @@ -96,22 +88,24 @@ def send_gamecontroller_command( def set_tactics( self, - tactics: AssignedTacticPlayControlParams, - is_friendly: bool, + blue_tactics={}, + yellow_tactics={}, ): - """Overrides current AI tactic for the given team + """Overrides current AI tactics for robots on each team - :param tactic: the tactic params proto to use - :param is_friendly: whether the play should be applied to the "friendly" team + :param blue_tactics: dict of robot_id -> tactic for blue team + :param yellow_tactics: dict of robot_id -> tactic for yellow team """ - fs_proto_unix_io = self.blue_full_system_proto_unix_io - # If (friendly & yellow_friendly) or (~friendly & ~yellow_friendly), set command team to yellow - if (is_friendly and self.is_yellow_friendly) or not ( - is_friendly or self.is_yellow_friendly - ): - fs_proto_unix_io = self.yellow_full_system_proto_unix_io + # TODO: add option to not set assigned tactics for play tests + blue_params = self._create_assigned_tactic_params(blue_tactics) + yellow_params = self._create_assigned_tactic_params(yellow_tactics) - fs_proto_unix_io.send_proto(AssignedTacticPlayControlParams, tactics) + self.blue_full_system_proto_unix_io.send_proto( + AssignedTacticPlayControlParams, blue_params + ) + self.yellow_full_system_proto_unix_io.send_proto( + AssignedTacticPlayControlParams, yellow_params + ) def set_play(self, play: Play, is_friendly: bool): """Overrides current AI play for the given team @@ -128,13 +122,30 @@ def set_play(self, play: Play, is_friendly: bool): fs_proto_unix_io.send_proto(Play, play) + def _create_assigned_tactic_params(self, tactics): + """Converts dict to AssignedTacticPlayControlParams + + :param tactics: dict of robot_id -> tactic + """ + params = AssignedTacticPlayControlParams() + + # Checks which oneof field in Tactic to assign the specified tactic to + for robot_id, specific_tactic in tactics.items(): + tactic = params.assigned_tactics[robot_id] + for field in tactic.DESCRIPTOR.oneofs_by_name["tactic"].fields: + if field.message_type == specific_tactic.DESCRIPTOR: + getattr(tactic, field.name).CopyFrom(specific_tactic) + break + + return params + @abstractmethod - def set_worldState(self, worldstate: WorldState): + def set_world_state(self, worldstate: WorldState): """Sets the worldstate for the given team :param worldstate: the worldstate proto to use """ - raise NotImplementedError("abstract class method called set_worldstate") + raise NotImplementedError("abstract class method called set_world_state") @abstractmethod def run_test( diff --git a/src/software/thunderscope/common/proto_configuration_widget.py b/src/software/thunderscope/common/proto_configuration_widget.py index 98eb26693c..aa4fc531de 100644 --- a/src/software/thunderscope/common/proto_configuration_widget.py +++ b/src/software/thunderscope/common/proto_configuration_widget.py @@ -16,6 +16,8 @@ class ProtoConfigurationWidget(QWidget): """Creates a searchable parameter widget that can take any protobuf, and convert it into a pyqtgraph ParameterTree. This will allow users to modify the values. + + NOTE: If this widget is initialized, it will send a config proto with params """ DELAYED_CONFIGURATION_TIMEOUT_S = 5 diff --git a/src/software/thunderscope/thunderscope_config.py b/src/software/thunderscope/thunderscope_config.py index f3c328aee6..4816e0268b 100644 --- a/src/software/thunderscope/thunderscope_config.py +++ b/src/software/thunderscope/thunderscope_config.py @@ -1,20 +1,37 @@ +import os +import signal +from dataclasses import dataclass +from typing import Sequence + +import pyqtgraph +import qdarktheme + from software.thunderscope.common.frametime_counter import FrameTimeCounter -from software.thunderscope.widget_setup_functions import * from software.thunderscope.constants import ProtoUnixIOTypes from software.thunderscope.proto_unix_io import ProtoUnixIO from software.thunderscope.robot_communication import RobotCommunication -from typing import Sequence -from dataclasses import dataclass from software.thunderscope.thunderscope_types import ( TScopeTab, TScopeWidget, WidgetPosition, WidgetStretchData, ) -import pyqtgraph -import qdarktheme -import signal -import os +from software.thunderscope.widget_setup_functions import ( + IndividualRobotMode, + RobotView, + setup_ball_speed_plot, + setup_diagnostics_widget, + setup_estop_view, + setup_fps_widget, + setup_gl_widget, + setup_log_widget, + setup_parameter_widget, + setup_performance_plot, + setup_play_info, + setup_referee_info, + setup_robot_error_log_view_widget, + setup_robot_view, +) @dataclass @@ -37,7 +54,7 @@ def initialize_application() -> None: signal.signal(signal.SIGINT, signal.SIG_DFL) # Setup MainApp and initialize DockArea - app = pyqtgraph.mkQApp("Thunderscope") + pyqtgraph.mkQApp("Thunderscope") # Setup stylesheet qdarktheme.setup_theme() @@ -71,9 +88,10 @@ def connect_robot_view_to_robot_communication( :param robot_communication: the RobotCommunication instance to connect to """ robot_view_widget.individual_robot_control_mode_signal.connect( - lambda robot_id, - robot_mode: robot_communication.toggle_individual_robot_control_mode( - robot_id, robot_mode + lambda robot_id, robot_mode: ( + robot_communication.toggle_individual_robot_control_mode( + robot_id, robot_mode + ) ) ) @@ -254,6 +272,51 @@ def configure_base_diagnostics( ] + extra_widgets +def configure_base_simulated_test( + full_system_proto_unix_io: ProtoUnixIO, + sim_proto_unix_io: ProtoUnixIO, + friendly_colour_yellow: bool, +) -> list: + """Returns a list of widget data for a FullSystem tab + along with any extra widgets passed in + + :param full_system_proto_unix_io: the proto unix io to configure widgets with + :param sim_proto_unix_io: the proto unix io for the simulator + :param friendly_colour_yellow: if this is Yellow FullSystem (True) or Blue (False) + :param visualization_buffer_size: The size of the visualization buffer. + Increasing this will increase smoothness but will be less realtime. + :param frame_swap_counter: a FrameTimeCounter for the GLWidget to track + the time between frame swaps + :param refresh_counter: a FrameTimeCounter for the refresh function + :return: list of widget data for FullSystem when running simulated tests + """ + return [ + TScopeWidget( + name="Field", + widget=setup_gl_widget( + sim_proto_unix_io=sim_proto_unix_io, + full_system_proto_unix_io=full_system_proto_unix_io, + friendly_colour_yellow=friendly_colour_yellow, + visualization_buffer_size=5, + ), + ), + TScopeWidget( + name="Logs", + widget=setup_log_widget(proto_unix_io=full_system_proto_unix_io), + anchor="Field", + position=WidgetPosition.LEFT, + stretch=WidgetStretchData(x=3), + ), + TScopeWidget( + name="Play Info", + widget=setup_play_info(proto_unix_io=full_system_proto_unix_io), + anchor="Field", + position=WidgetPosition.BOTTOM, + stretch=WidgetStretchData(y=3), + ), + ] + + def configure_two_ai_gamecontroller_view( visualization_buffer_size: int = 5, ) -> TScopeConfig: @@ -321,7 +384,6 @@ def configure_simulated_test_view( simulator_proto_unix_io: ProtoUnixIO, blue_full_system_proto_unix_io: ProtoUnixIO, yellow_full_system_proto_unix_io: ProtoUnixIO, - visualization_buffer_size: int = 5, ) -> TScopeConfig: """Constructs the Thunderscope Config for simulated tests A view with 2 FullSystem tabs (Blue and Yellow) @@ -348,24 +410,20 @@ def configure_simulated_test_view( tabs=[ TScopeTab( name="Blue FullSystem", - widgets=configure_base_fullsystem( + widgets=configure_base_simulated_test( full_system_proto_unix_io=proto_unix_io_map[ProtoUnixIOTypes.BLUE], sim_proto_unix_io=proto_unix_io_map[ProtoUnixIOTypes.SIM], friendly_colour_yellow=False, - visualization_buffer_size=visualization_buffer_size, - extra_widgets=[], ), ), TScopeTab( name="Yellow FullSystem", - widgets=configure_base_fullsystem( + widgets=configure_base_simulated_test( full_system_proto_unix_io=proto_unix_io_map[ ProtoUnixIOTypes.YELLOW ], sim_proto_unix_io=proto_unix_io_map[ProtoUnixIOTypes.SIM], friendly_colour_yellow=True, - visualization_buffer_size=visualization_buffer_size, - extra_widgets=[], ), ), ],