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 55b892fbff..38e2059c62 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 @@ -1,29 +1,31 @@ import pytest -import software.python_bindings as tbots_cpp +import software.python_bindings as tbots_cpp from proto.import_all_protos import * - from proto.play_pb2 import PlayName -from proto.ssl_gc_common_pb2 import Team - from proto.message_translation.tbots_protobuf import create_world_state -from software.simulated_tests.simulated_test_fixture import ( - pytest_main, +from software.simulated_tests.simulated_test_fixture import pytest_main +from software.simulated_tests.validation.ball_enters_region import * +from software.simulated_tests.validation.friendly_team_scored import * +from software.simulated_tests.validation.ball_speed_threshold import * +from software.simulated_tests.validation.friendly_has_ball_possession import ( + FriendlyEventuallyHasBallPossession, ) +from proto.ssl_gc_common_pb2 import Team -def free_kick_play_setup( - blue_bots, yellow_bots, ball_initial_pos, play_name, simulated_test_runner +def setup_free_kick_play( + simulated_test_runner, blue_bots, yellow_bots, ball_initial_pos ): + """Set world state and issue a blue-team direct free kick.""" 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 ) @@ -34,182 +36,261 @@ def free_kick_play_setup( gc_command=Command.Type.DIRECT, team=Team.BLUE ) - simulated_test_runner.set_plays(blue_play=play_name, yellow_play=PlayName.HaltPlay) + simulated_test_runner.set_plays( + blue_play=PlayName.FreeKickPlay, yellow_play=PlayName.HaltPlay + ) -# We want to test friendly half, enemy half, and at the border of the field -@pytest.mark.parametrize( - "ball_initial_pos,must_score", - [ - (tbots_cpp.Point(1.5, -2.75), False), - (tbots_cpp.Point(-1.5, -2.75), False), - (tbots_cpp.Point(1.5, -3), False), - (tbots_cpp.Point(1.5, -0.5), True), - ], -) -def test_free_kick_play_friendly(simulated_test_runner, ball_initial_pos, must_score): - # TODO- #2753 Validation - # params just have to be a list of length 1 to ensure the test runs at least once +def test_free_kick_play_direct_shot(simulated_test_runner): + """Ball in enemy half with no enemies blocking the goal so kicker shoots directly. + FSM: SetupPositionState -> ShootState -> X + Triggers: setupDone=True, shotFound=True, shotDone=True + Validates: FriendlyTeamEventuallyScored. + """ + field = tbots_cpp.Field.createSSLDivisionBField() + ball_initial_pos = tbots_cpp.Point(1.5, 0.0) + + blue_bots = [ + tbots_cpp.Point(-4.5, 0), + tbots_cpp.Point(1.3, 0.3), + tbots_cpp.Point(-1.0, 1.5), + tbots_cpp.Point(-1.0, -1.5), + tbots_cpp.Point(0.5, 2.0), + tbots_cpp.Point(0.5, -2.0), + ] + + yellow_bots = [ + tbots_cpp.Point(0.0, 3.0), + tbots_cpp.Point(0.0, -3.0), + tbots_cpp.Point(-2.0, 2.0), + tbots_cpp.Point(-2.0, -2.0), + tbots_cpp.Point(-3.0, 1.0), + tbots_cpp.Point(-3.0, -1.0), + ] + + def setup(*args): + setup_free_kick_play( + simulated_test_runner, blue_bots, yellow_bots, ball_initial_pos + ) + simulated_test_runner.run_test( - setup=lambda test_setup_arg: free_kick_play_setup( - test_setup_arg["blue_bots"], - test_setup_arg["yellow_bots"], - test_setup_arg["ball_initial_pos"], - test_setup_arg["play_name"], - simulated_test_runner, - ), - params=[ - { - "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(4, -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(), - ], - "ball_initial_pos": ball_initial_pos, - "play_name": PlayName.FreeKickPlay, - } + setup=setup, + params=[0], + inv_always_validation_sequence_set=[ + [BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])] ], - inv_always_validation_sequence_set=[[]], - inv_eventually_validation_sequence_set=[[]], + inv_eventually_validation_sequence_set=[[FriendlyTeamEventuallyScored()]], ag_always_validation_sequence_set=[[]], - ag_eventually_validation_sequence_set=[[]], - test_timeout_s=10, + ag_eventually_validation_sequence_set=[[FriendlyTeamEventuallyScored()]], + test_timeout_s=15, ) -@pytest.mark.parametrize( - "ball_initial_pos,yellow_bots", - [ - # not close to our net - ( - tbots_cpp.Point(0.9, 2.85), - [ - tbots_cpp.Point(1, 3), - tbots_cpp.Point(-2, -1.25), - tbots_cpp.Point(-1, -0.25), - tbots_cpp.Field.createSSLDivisionBField().enemyGoalCenter(), - tbots_cpp.Field.createSSLDivisionBField() - .enemyDefenseArea() - .negXNegYCorner(), - tbots_cpp.Field.createSSLDivisionBField() - .enemyDefenseArea() - .negXPosYCorner(), - ], - ), - # close to our net - ( - tbots_cpp.Point(-2.4, 1), - [ - tbots_cpp.Point(-2.3, 1.05), - tbots_cpp.Point(-3.5, 2), - tbots_cpp.Point(-1.2, 0), - tbots_cpp.Point(-2.3, -1), - tbots_cpp.Point(-3.8, -2), - tbots_cpp.Field.createSSLDivisionBField().enemyGoalCenter(), - ], - ), - ], -) -def test_free_kick_play_enemy(simulated_test_runner, ball_initial_pos, yellow_bots): +def test_free_kick_play_pass_completes(simulated_test_runner): + """Goal is blocked by enemy robots and blue receivers are open in the enemy half. Pass will be made. + FSM path: SetupPositionState -> AttemptPassState -> PassState -> X + Triggers: setupDone=True, shotFound=False, passFound=True, passDone=True + Validates: Receiver eventually gets possession. + """ + field = tbots_cpp.Field.createSSLDivisionBField() + ball_initial_pos = tbots_cpp.Point(-0.5, 0.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(4, -2.5), + tbots_cpp.Point(-0.7, 0.2), # kicker near ball + tbots_cpp.Point(2.5, 1.5), + tbots_cpp.Point(2.5, -1.5), + tbots_cpp.Point(-2.0, 1.0), + tbots_cpp.Point(-2.0, -1.0), ] - # TODO- #2753 Validation + + yellow_bots = [ + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + tbots_cpp.Point(3.5, 0.5), + tbots_cpp.Point(3.5, -0.5), + tbots_cpp.Point(3.5, 0.0), + ] + + def setup(*args): + setup_free_kick_play( + simulated_test_runner, blue_bots, yellow_bots, ball_initial_pos + ) + simulated_test_runner.run_test( - setup=lambda test_setup_arg: free_kick_play_setup( - test_setup_arg["blue_bots"], - test_setup_arg["yellow_bots"], - test_setup_arg["ball_initial_pos"], - test_setup_arg["play_name"], - simulated_test_runner, - ), - params=[ - { - "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(4, -2.5), - ], - "yellow_bots": yellow_bots, - "ball_initial_pos": ball_initial_pos, - "play_name": PlayName.EnemyFreeKickPlay, - } + setup=setup, + params=[0], + inv_always_validation_sequence_set=[ + [BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])] + ], + inv_eventually_validation_sequence_set=[ + [FriendlyEventuallyHasBallPossession(tolerance=0.15)] ], + ag_always_validation_sequence_set=[[]], + ag_eventually_validation_sequence_set=[ + [FriendlyEventuallyHasBallPossession(tolerance=0.15)] + ], + test_timeout_s=20, + ) + + +def test_free_kick_play_abort_pass_and_retry(simulated_test_runner): + """Ball in friendly half so shotFound is reliably False. + A yellow robot sits directly on the pass lane from the ball to receiver B, + so ratePass() scores the found pass below abs_min_pass_score (0.05). + FSM path: SetupPositionState -> AttemptPassState -> PassState -> AttemptPassState -> ... -> X (chip or second pass) + Triggers: passFound=True, shouldAbortPass=True + Validates: ball eventually moves via chip or a cleared second-attempt pass. + """ + field = tbots_cpp.Field.createSSLDivisionBField() + # (-0.5, 0) shot angle to enemy goal about 4 < min_open_angle_for_shot_deg (6) + ball_initial_pos = tbots_cpp.Point(-0.5, 0.0) + + blue_bots = [ + tbots_cpp.Point(-4.5, 0), + tbots_cpp.Point(-0.7, 0.2), # kicker near ball + tbots_cpp.Point(2.0, 0.5), # receiver A in enemy half + tbots_cpp.Point(0.5, 2.0), # receiver B near midfield + tbots_cpp.Point(-2.0, 1.5), + tbots_cpp.Point(-2.0, -1.5), + ] + + yellow_bots = [ + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + tbots_cpp.Point(0.0, 1.0), # directly on pass lane to receiver B + tbots_cpp.Point(3.5, 0.5), + tbots_cpp.Point(3.5, -0.5), + ] + + def setup(*args): + setup_free_kick_play( + simulated_test_runner, blue_bots, yellow_bots, ball_initial_pos + ) + + simulated_test_runner.run_test( + setup=setup, + params=[0], + inv_always_validation_sequence_set=[ + [BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])] + ], + inv_eventually_validation_sequence_set=[ + [BallSpeedEventuallyAtOrAboveThreshold(0.05)] + ], + ag_always_validation_sequence_set=[[]], + ag_eventually_validation_sequence_set=[ + [BallSpeedEventuallyAtOrAboveThreshold(0.05)] + ], + test_timeout_s=25, + ) + + +def test_free_kick_play_chip_on_timeout(simulated_test_runner): + """Goal is blocked and all receiver zones are covered by enemies. passFound never becomes True, + so timeExpired fires and the FSM chips. + FSM path: SetupPositionState -> AttemptPassState -> ChipState -> X + Triggers: setupDone=True, shotFound=False, passFound=False (timeout), chipDone=True + Validates: Ball eventually moves above a threshold speed. + """ + field = tbots_cpp.Field.createSSLDivisionBField() + ball_initial_pos = tbots_cpp.Point(-1.0, 0.0) + + blue_bots = [ + tbots_cpp.Point(-4.5, 0), + tbots_cpp.Point(-1.2, 0.2), + tbots_cpp.Point(-3.0, 1.5), + tbots_cpp.Point(-3.0, -1.5), + tbots_cpp.Point(-3.5, 0.5), + tbots_cpp.Point(-3.5, -0.5), + ] + + yellow_bots = [ + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + tbots_cpp.Point(1.0, 0.0), + tbots_cpp.Point(1.0, 1.5), + tbots_cpp.Point(1.0, -1.5), + ] + + def setup(*args): + setup_free_kick_play( + simulated_test_runner, blue_bots, yellow_bots, ball_initial_pos + ) + + simulated_test_runner.run_test( + setup=setup, + params=[0], inv_always_validation_sequence_set=[[]], - inv_eventually_validation_sequence_set=[[]], + inv_eventually_validation_sequence_set=[ + [BallSpeedEventuallyAtOrAboveThreshold(0.05)] + ], ag_always_validation_sequence_set=[[]], - ag_eventually_validation_sequence_set=[[]], - test_timeout_s=10, + ag_eventually_validation_sequence_set=[ + [BallSpeedEventuallyAtOrAboveThreshold(0.05)] + ], + test_timeout_s=20, ) @pytest.mark.parametrize( - "ball_initial_pos", + "ball_initial_pos,must_score", [ - tbots_cpp.Point(1.5, -2.75), - tbots_cpp.Point(-1.5, -2.75), - tbots_cpp.Point(1.5, -3), + (tbots_cpp.Point(1.5, 0.0), True), # Enemy half, near sideline for direct shot + ( + tbots_cpp.Point(-1.5, -2.75), + False, + ), # Friendly half, near sideline for pass or chip + (tbots_cpp.Point(1.5, -3.0), False), # Near corner for pass or chip + (tbots_cpp.Point(1.5, -0.5), True), # Enemy half, center for direct shot likely ], ) -def test_free_kick_play_both(simulated_test_runner, ball_initial_pos): - # TODO- #2753 Validation +def test_free_kick_play_near_sideline( + simulated_test_runner, ball_initial_pos, must_score +): + """Parametric generalized free kick tests. Requires score if must_score is True, otherwise moving the ball will pass the test.""" + field = tbots_cpp.Field.createSSLDivisionBField() + + blue_bots = [ + tbots_cpp.Point(-4.5, 0), + tbots_cpp.Point(-3.0, 1.5), + tbots_cpp.Point(-3.0, 0.5), + tbots_cpp.Point(-3.0, -0.5), + tbots_cpp.Point(-3.0, -1.5), + tbots_cpp.Point(4.0, -2.5), + ] + yellow_bots = [ + tbots_cpp.Point(1.0, 0), + tbots_cpp.Point(1.0, 2.5), + tbots_cpp.Point(1.0, -2.5), + field.enemyGoalCenter(), + field.enemyDefenseArea().negXNegYCorner(), + field.enemyDefenseArea().negXPosYCorner(), + ] + + def setup(*args): + setup_free_kick_play( + simulated_test_runner, blue_bots, yellow_bots, ball_initial_pos + ) + + if must_score: + inv_eventually = [[FriendlyTeamEventuallyScored()]] + ag_eventually = [[FriendlyTeamEventuallyScored()]] + else: + inv_eventually = [[BallSpeedEventuallyAtOrAboveThreshold(0.05)]] + ag_eventually = [[BallSpeedEventuallyAtOrAboveThreshold(0.05)]] + simulated_test_runner.run_test( - setup=lambda test_setup_arg: free_kick_play_setup( - test_setup_arg["blue_bots"], - test_setup_arg["yellow_bots"], - test_setup_arg["ball_initial_pos"], - test_setup_arg["play_name"], - simulated_test_runner, - ), - params=[ - { - "blue_bots": [ - tbots_cpp.Point(-3, 0.25), - 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, -0.25), - ], - "yellow_bots": [ - tbots_cpp.Point(3, 0.25), - 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, -0.25), - ], - "ball_initial_pos": ball_initial_pos, - "play_name": PlayName.EnemyFreeKickPlay, - } + setup=setup, + params=[0], + inv_always_validation_sequence_set=[ + [BallAlwaysStaysInRegion(regions=[field.fieldBoundary()])] ], - inv_always_validation_sequence_set=[[]], - inv_eventually_validation_sequence_set=[[]], + inv_eventually_validation_sequence_set=inv_eventually, ag_always_validation_sequence_set=[[]], - ag_eventually_validation_sequence_set=[[]], + ag_eventually_validation_sequence_set=ag_eventually, test_timeout_s=15, )