From 683119e36ea4292969e11745d5d0c8f910187285 Mon Sep 17 00:00:00 2001 From: muk Date: Sat, 28 Mar 2026 17:39:00 -0700 Subject: [PATCH 1/3] free kick play test passes --- .../stp/play/free_kick/free_kick_play_test.py | 409 +++++++++++------- 1 file changed, 254 insertions(+), 155 deletions(-) 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 c3636d1418..c3eb6a7baf 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 @@ -4,6 +4,10 @@ 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.ball_moves_from_rest import * +from software.simulated_tests.friendly_has_ball_possession import ( + FriendlyEventuallyHasBallPossession, +) 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,18 +15,18 @@ ) -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,7 +38,7 @@ def free_kick_play_setup( ) blue_play = Play() - blue_play.name = play_name + blue_play.name = PlayName.FreeKickPlay yellow_play = Play() yellow_play.name = PlayName.HaltPlay @@ -43,179 +47,274 @@ def free_kick_play_setup( 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 -@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 +# Test 1: Direct Shot +# FSM path: SetupPositionState -> ShootState -> X +# Triggers: setupDone=True, shotFound=True, shotDone=True +def test_free_kick_play_direct_shot(simulated_test_runner): + """ + Ball in enemy half with no enemies blocking the goal so kicker shoots directly. + 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): +# Test 2: Pass Completes +# FSM path: SetupPositionState -> AttemptPassState -> PassState -> X +# Triggers: setupDone=True, shotFound=False, passFound=True, passDone=True +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. + Validates that a 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(-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), + ] + + 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=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, + ) + + +# Test 3: Abort Pass and Retry +# FSM path: SetupPositionState -> AttemptPassState -> PassState +# -> AttemptPassState -> ... -> X (chip or second pass) +# Triggers: passFound=True, shouldAbortPass=True +# shouldAbortPass fires when ratePass() drops below abs_min_pass_score (0.05) + + +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). + 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(-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.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), ] - # TODO- #2753 Validation + + 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=[ + [BallEventuallyMovesFromRest(position=ball_initial_pos)] + ], + ag_always_validation_sequence_set=[[]], + ag_eventually_validation_sequence_set=[ + [BallEventuallyMovesFromRest(position=ball_initial_pos)] + ], + test_timeout_s=25, + ) + + +# Test 4: Timeout -> Chip +# FSM path: SetupPositionState -> AttemptPassState -> ChipState -> X +# Triggers: setupDone=True, shotFound=False, passFound=False (timeout), chipDone=True +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. + Validates that the ball eventually moves from rest via the chip. + """ + 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=[ + [BallEventuallyMovesFromRest(position=ball_initial_pos)] + ], ag_always_validation_sequence_set=[[]], - ag_eventually_validation_sequence_set=[[]], - test_timeout_s=10, + ag_eventually_validation_sequence_set=[ + [BallEventuallyMovesFromRest(position=ball_initial_pos)] + ], + test_timeout_s=20, ) +# Test 5: Near-Sideline +# Exercises all FSM paths with ball at field edges @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), + # Enemy half, near sideline for direct shot + (tbots_cpp.Point(1.5, 0.0), True), + # Friendly half, near sideline for pass or chip + (tbots_cpp.Point(-1.5, -2.75), False), + # Near corner for pass or chip + (tbots_cpp.Point(1.5, -3.0), False), + # Enemy half, center for direct shot likely + (tbots_cpp.Point(1.5, -0.5), True), ], ) -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 +): + """ + Parametrized test covering ball positions at and near field edges. 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 = [[BallEventuallyMovesFromRest(position=ball_initial_pos)]] + ag_eventually = [[BallEventuallyMovesFromRest(position=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(-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, ) From a6baa03d894da221d088e3feb1c2ab92ad119bcd Mon Sep 17 00:00:00 2001 From: muk Date: Sat, 28 Mar 2026 17:42:07 -0700 Subject: [PATCH 2/3] lint --- .../hl/stp/play/free_kick/free_kick_play_test.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) 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 c3eb6a7baf..e8aef85336 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 @@ -51,8 +51,7 @@ def setup_free_kick_play( # FSM path: SetupPositionState -> ShootState -> X # Triggers: setupDone=True, shotFound=True, shotDone=True def test_free_kick_play_direct_shot(simulated_test_runner): - """ - Ball in enemy half with no enemies blocking the goal so kicker shoots directly. + """Ball in enemy half with no enemies blocking the goal so kicker shoots directly. Validates FriendlyTeamEventuallyScored. """ field = tbots_cpp.Field.createSSLDivisionBField() @@ -98,8 +97,7 @@ def setup(*args): # FSM path: SetupPositionState -> AttemptPassState -> PassState -> X # Triggers: setupDone=True, shotFound=False, passFound=True, passDone=True 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. + """Goal is blocked by enemy robots and blue receivers are open in the enemy half. Pass will be made. Validates that a receiver eventually gets possession. """ field = tbots_cpp.Field.createSSLDivisionBField() @@ -153,8 +151,7 @@ def setup(*args): def test_free_kick_play_abort_pass_and_retry(simulated_test_runner): - """ - Ball in friendly half so shotFound is reliably False. + """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). Validates ball eventually moves via chip or a cleared second-attempt pass. @@ -207,8 +204,7 @@ def setup(*args): # FSM path: SetupPositionState -> AttemptPassState -> ChipState -> X # Triggers: setupDone=True, shotFound=False, passFound=False (timeout), chipDone=True 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, + """Goal is blocked and all receiver zones are covered by enemies. passFound never becomes True, so timeExpired fires and the FSM chips. Validates that the ball eventually moves from rest via the chip. """ @@ -271,8 +267,7 @@ def setup(*args): def test_free_kick_play_near_sideline( simulated_test_runner, ball_initial_pos, must_score ): - """ - Parametrized test covering ball positions at and near field edges. Requires score if must_score is True, otherwise + """Parametrized test covering ball positions at and near field edges. Requires score if must_score is True, otherwise moving the ball will pass the test. """ field = tbots_cpp.Field.createSSLDivisionBField() From 327181890a8a74e28145d72b4a1843b9cf26ec88 Mon Sep 17 00:00:00 2001 From: muk Date: Sat, 4 Apr 2026 19:02:04 -0700 Subject: [PATCH 3/3] docstring fix --- .../stp/play/free_kick/free_kick_play_test.py | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) 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 559f626877..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 @@ -41,12 +41,11 @@ def setup_free_kick_play( ) -# Test 1: Direct Shot -# FSM path: SetupPositionState -> ShootState -> X -# Triggers: setupDone=True, shotFound=True, shotDone=True def test_free_kick_play_direct_shot(simulated_test_runner): """Ball in enemy half with no enemies blocking the goal so kicker shoots directly. - Validates FriendlyTeamEventuallyScored. + 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) @@ -87,12 +86,11 @@ def setup(*args): ) -# Test 2: Pass Completes -# FSM path: SetupPositionState -> AttemptPassState -> PassState -> X -# Triggers: setupDone=True, shotFound=False, passFound=True, passDone=True 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. - Validates that a receiver eventually gets possession. + 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) @@ -137,18 +135,13 @@ def setup(*args): ) -# Test 3: Abort Pass and Retry -# FSM path: SetupPositionState -> AttemptPassState -> PassState -# -> AttemptPassState -> ... -> X (chip or second pass) -# Triggers: passFound=True, shouldAbortPass=True -# shouldAbortPass fires when ratePass() drops below abs_min_pass_score (0.05) - - 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). - Validates ball eventually moves via chip or a cleared second-attempt pass. + 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) @@ -194,13 +187,12 @@ def setup(*args): ) -# Test 4: Timeout -> Chip -# FSM path: SetupPositionState -> AttemptPassState -> ChipState -> X -# Triggers: setupDone=True, shotFound=False, passFound=False (timeout), chipDone=True 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. - Validates that the ball eventually moves from rest via the chip. + 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) @@ -243,27 +235,22 @@ def setup(*args): ) -# Test 5: Near-Sideline -# Exercises all FSM paths with ball at field edges @pytest.mark.parametrize( "ball_initial_pos,must_score", [ - # Enemy half, near sideline for direct shot - (tbots_cpp.Point(1.5, 0.0), True), - # Friendly half, near sideline for pass or chip - (tbots_cpp.Point(-1.5, -2.75), False), - # Near corner for pass or chip - (tbots_cpp.Point(1.5, -3.0), False), - # Enemy half, center for direct shot likely - (tbots_cpp.Point(1.5, -0.5), True), + (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_near_sideline( simulated_test_runner, ball_initial_pos, must_score ): - """Parametrized test covering ball positions at and near field edges. Requires score if must_score is True, otherwise - moving the ball will pass the test. - """ + """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 = [