From 050b48bb00f33283636c95a34891df670893c8a1 Mon Sep 17 00:00:00 2001 From: Eric Xiao Date: Fri, 31 Oct 2025 19:29:54 -0700 Subject: [PATCH 1/5] Add failing test cases for receiver move away from shot --- src/proto/tactic.proto | 2 +- src/software/ai/hl/stp/tactic/receiver/BUILD | 18 +- .../tactic/receiver/receiver_tactic_test.py | 155 ++++++++++++++++++ .../ai/hl/stp/tactic/tactic_factory.cpp | 4 +- 4 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py diff --git a/src/proto/tactic.proto b/src/proto/tactic.proto index afc537c1b4..45b9d8c35b 100644 --- a/src/proto/tactic.proto +++ b/src/proto/tactic.proto @@ -160,7 +160,7 @@ message PivotKickTactic message ReceiverTactic { // The pass to receive - optional Pass pass = 1; + optional Pass pass_ = 1; // needs the _ because pass is a keyword in python // If set to true, we will only receive and dribble required bool disable_one_touch_shot = 2; } diff --git a/src/software/ai/hl/stp/tactic/receiver/BUILD b/src/software/ai/hl/stp/tactic/receiver/BUILD index 8548bfe80e..9ff8ee7728 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,17 @@ cc_test( "//software/world", ], ) + +py_test( + name = "receiver_tactic_test", + srcs = ["receiver_tactic_test.py"], + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests:speed_threshold_helpers", + "//software/simulated_tests:validation", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py b/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py new file mode 100644 index 0000000000..c8e8a275d9 --- /dev/null +++ b/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py @@ -0,0 +1,155 @@ +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 software.simulated_tests.simulated_test_fixture import ( + pytest_main, +) +from software.simulated_tests.ball_is_off_ground import * +from proto.message_translation.tbots_protobuf import create_world_state + +corner = tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXNegYCorner() +centre = tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().centre() + + +@pytest.mark.parametrize( + "blue_bots, yellow_bots, ball_initial_pos, ball_initial_velocity", + [ + # Test bottom left medium distance from attacker + ( + [ + corner + tbots_cpp.Vector(-1.5, -1.5), + corner + tbots_cpp.Vector(-0.5, -0.5), + ], + [], + corner + tbots_cpp.Vector(-1.3, -1.3), + tbots_cpp.Vector(0, 0), + ), + # Test bottom left close distance from attacker + ( + [ + corner + tbots_cpp.Vector(-1.5, -1.5), + corner + tbots_cpp.Vector(-1.0, -1.0), + ], + [], + corner + tbots_cpp.Vector(-1.3, -1.3), + tbots_cpp.Vector(0, 0), + ), + # Test centre medium distance from attacker + ( + [ + centre + tbots_cpp.Vector(-2.5, 0.0), + centre + tbots_cpp.Vector(-1.0, 0.0), + ], + [], + centre + tbots_cpp.Vector(-2.2, 0.0), + tbots_cpp.Vector(0, 0), + ), + # Test centre close distance from attacker + ( + [ + centre + tbots_cpp.Vector(-1.5, 0.0), + centre + tbots_cpp.Vector(-1.0, 0.0), + ], + [], + centre + tbots_cpp.Vector(-1.3, 0.0), + tbots_cpp.Vector(0, 0), + ), + ], +) +def test_receiver_move_away_from_shot_in_progress( + blue_bots, + yellow_bots, + ball_initial_pos, + ball_initial_velocity, + simulated_test_runner, +): + # Setup Robot + def setup(*args): + simulated_test_runner.simulator_proto_unix_io.send_proto( + WorldState, + 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() + + passer_point = blue_bots[0] + receiver_point = blue_bots[1] + receive_speed_m_per_s = 2.0 + min_pass_speed_m_per_s = 1.0 + max_pass_speed_m_per_s = 4.0 + pass_to_test = tbots_cpp.Pass.fromDestReceiveSpeed( + passer_point, + receiver_point, + receive_speed_m_per_s, + min_pass_speed_m_per_s, + max_pass_speed_m_per_s, + ) + + possible_pass = Pass( + passer_point=Point( + x_meters=pass_to_test.passerPoint().x(), + y_meters=pass_to_test.passerPoint().y(), + ), + receiver_point=Point( + x_meters=pass_to_test.receiverPoint().x(), + y_meters=pass_to_test.receiverPoint().y(), + ), + pass_speed_m_per_s=pass_to_test.speed(), + ) + + params.assigned_tactics[0].attacker.CopyFrom( + AttackerTactic( + best_pass_so_far=possible_pass, # optional + pass_committed=False, + # chip_target={"x_meters": 0.0, "y_meters": 0.0}, # optional + ) + ) + + params.assigned_tactics[1].receiver.CopyFrom( + ReceiverTactic( + pass_=possible_pass, # optional + disable_one_touch_shot=True, + ) + ) + + simulated_test_runner.blue_full_system_proto_unix_io.send_proto( + AssignedTacticPlayControlParams, params + ) + + # Always Validation + always_validation_sequence_set = [[]] + + # Eventually Validation + eventually_validation_sequence_set = [ + [ + BallEventuallyEntersRegion( + regions=[tbots_cpp.Field.createSSLDivisionBField().enemyGoal()] + ) + ] + ] + + 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, + ag_always_validation_sequence_set=always_validation_sequence_set, + test_timeout_s=4, + ) + + +if __name__ == "__main__": + pytest_main(__file__) diff --git a/src/software/ai/hl/stp/tactic/tactic_factory.cpp b/src/software/ai/hl/stp/tactic/tactic_factory.cpp index 73f51d2a66..11124b37cc 100644 --- a/src/software/ai/hl/stp/tactic/tactic_factory.cpp +++ b/src/software/ai/hl/stp/tactic/tactic_factory.cpp @@ -179,9 +179,9 @@ std::shared_ptr createTactic(const TbotsProto::ReceiverTactic &tactic_pr { auto tactic = std::make_shared(ai_config.receiver_tactic_config()); std::optional pass = std::nullopt; - if (tactic_proto.has_pass()) + if (tactic_proto.has_pass_()) { - pass = createPass(tactic_proto.pass()); + pass = createPass(tactic_proto.pass_()); } tactic->updateControlParams(pass, tactic_proto.disable_one_touch_shot()); From ab8a6832a1f8211ae30077763609ece63fbd3313 Mon Sep 17 00:00:00 2001 From: Eric Xiao Date: Sat, 15 Nov 2025 16:00:39 -0800 Subject: [PATCH 2/5] Add guard and action to receiver to move from shot --- docs/fsm-diagrams.md | 1 + .../hl/stp/tactic/receiver/receiver_fsm.cpp | 26 +++++++++++++++++++ .../ai/hl/stp/tactic/receiver/receiver_fsm.h | 22 +++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/fsm-diagrams.md b/docs/fsm-diagrams.md index 8059b8e10d..742df21c2a 100644 --- a/docs/fsm-diagrams.md +++ b/docs/fsm-diagrams.md @@ -469,6 +469,7 @@ stateDiagram-v2 classDef terminate fill:white,color:black,font-weight:bold direction LR [*] --> WaitingForPassState +WaitingForPassState --> WaitingForPassState : [shouldMoveAwayFromShot]\nmoveAwayFromShot WaitingForPassState --> WaitingForPassState : [!passStarted]\nupdateReceive WaitingForPassState --> OneTouchShotState : [passStarted && onetouchPossible]\nupdateOnetouch WaitingForPassState --> ReceiveAndDribbleState : [passStarted && !onetouchPossible]\nupdateReceive diff --git a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp index c1982d5599..71b8afeb5d 100644 --- a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp +++ b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp @@ -174,6 +174,26 @@ void ReceiverFSM::adjustReceive(const Update& event) } } +void ReceiverFSM::moveAwayFromShot(const Update& event) +{ + Point ball = event.common.world_ptr->ball().position(); + Point robot = event.common.robot.position(); + + Vector between_robot_and_ball = ball - robot; + Vector side_step_direction = between_robot_and_ball.normalize().perpendicular(); + + Point side_step_position = robot + side_step_direction; + + Angle orientation = (side_step_position - ball).orientation(); + + event.common.set_primitive(std::make_unique( + event.common.robot, side_step_position, orientation, + TbotsProto::MaxAllowedSpeedMode::PHYSICAL_LIMIT, + TbotsProto::ObstacleAvoidanceMode::AGGRESSIVE, + TbotsProto::DribblerMode::MAX_FORCE, TbotsProto::BallCollisionType::ALLOW, + AutoChipOrKick{AutoChipOrKickMode::OFF, {0}})); +} + bool ReceiverFSM::passStarted(const Update& event) { return event.common.world_ptr->ball().hasBallBeenKicked( @@ -207,3 +227,9 @@ bool ReceiverFSM::strayPass(const Update& event) return stray_pass; } + +bool ReceiverFSM::shouldMoveAwayFromShot(const Update& event) +{ + // TODO: implement this avah + return true; +} diff --git a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.h b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.h index 051ea8f7a7..cf2a2513f4 100644 --- a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.h +++ b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.h @@ -114,6 +114,14 @@ struct ReceiverFSM */ void adjustReceive(const Update& event); + /** + * Move the robot away from the planned shot path by moving + * perpendicular to the path by a metre. + * + * @param event ReceiverFSM::Update event + */ + void moveAwayFromShot(const Update& event); + /** * Guard that checks if the ball has been kicked * @@ -141,6 +149,14 @@ struct ReceiverFSM */ bool strayPass(const Update& event); + /** + * Checks if attacker wants to shoot and receiver is in the way. + * + * @param event ReceiverFSM::Update event + * @return true if should move away from shot attempt + */ + bool shouldMoveAwayFromShot(const Update& event); + auto operator()() { using namespace boost::sml; @@ -154,14 +170,18 @@ struct ReceiverFSM DEFINE_SML_GUARD(passStarted) DEFINE_SML_GUARD(passFinished) DEFINE_SML_GUARD(strayPass) + DEFINE_SML_GUARD(shouldMoveAwayFromShot) DEFINE_SML_ACTION(updateOnetouch) DEFINE_SML_ACTION(updateReceive) DEFINE_SML_ACTION(adjustReceive) + DEFINE_SML_ACTION(moveAwayFromShot) return make_transition_table( // src_state + event [guard] / action = dest_state - *WaitingForPassState_S + Update_E[!passStarted_G] / updateReceive_A, + *WaitingForPassState_S + + Update_E[shouldMoveAwayFromShot_G] / moveAwayFromShot_A, + WaitingForPassState_S + Update_E[!passStarted_G] / updateReceive_A, WaitingForPassState_S + Update_E[passStarted_G && onetouchPossible_G] / updateOnetouch_A = OneTouchShotState_S, WaitingForPassState_S + Update_E[passStarted_G && !onetouchPossible_G] / From 8245f9dca3686796779c989058814a3b4f9a60a9 Mon Sep 17 00:00:00 2001 From: Eric Xiao Date: Sat, 15 Nov 2025 17:35:22 -0800 Subject: [PATCH 3/5] Implement simple check if receiver is blocking ball --- src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp index 71b8afeb5d..236f303392 100644 --- a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp +++ b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp @@ -230,6 +230,11 @@ bool ReceiverFSM::strayPass(const Update& event) bool ReceiverFSM::shouldMoveAwayFromShot(const Update& event) { - // TODO: implement this avah - return true; + Point ball = event.common.world_ptr->ball().position(); + Point goal_left = event.common.world_ptr->field().enemyGoalpostNeg(); + Point goal_right = event.common.world_ptr->field().enemyGoalpostPos(); + + Point robot = event.common.robot.position(); + + return contains(Polygon{ball, goal_left, goal_right}, robot); } From 83c7559e28ba51e3960c98b27796a0b81591bafc Mon Sep 17 00:00:00 2001 From: Eric Xiao Date: Sat, 15 Nov 2025 19:56:52 -0800 Subject: [PATCH 4/5] Add margin around zone to avoid for receiver --- .../ai/hl/stp/tactic/receiver/receiver_fsm.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp index 236f303392..7a3660cc0d 100644 --- a/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp +++ b/src/software/ai/hl/stp/tactic/receiver/receiver_fsm.cpp @@ -230,11 +230,15 @@ bool ReceiverFSM::strayPass(const Update& event) bool ReceiverFSM::shouldMoveAwayFromShot(const Update& event) { - Point ball = event.common.world_ptr->ball().position(); - Point goal_left = event.common.world_ptr->field().enemyGoalpostNeg(); - Point goal_right = event.common.world_ptr->field().enemyGoalpostPos(); + Point ball = event.common.world_ptr->ball().position(); + Point goal = event.common.world_ptr->field().enemyGoalpostPos(); + + Vector ball_expand = (ball - goal).normalize(0.5).perpendicular(); + Vector goal_expand = Vector(0.5, 0.0); Point robot = event.common.robot.position(); - return contains(Polygon{ball, goal_left, goal_right}, robot); + return contains(Polygon{ball - ball_expand, ball + ball_expand, goal + goal_expand, + goal - goal_expand}, + robot); } From 79f52d92c45d482af16e6b7c88f7c6f2dc8f173a Mon Sep 17 00:00:00 2001 From: Eric Xiao Date: Sun, 4 Jan 2026 15:01:24 -0800 Subject: [PATCH 5/5] Remove unused imports --- .../ai/hl/stp/tactic/receiver/receiver_tactic_test.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 index c8e8a275d9..4a7fee2a68 100644 --- a/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py +++ b/src/software/ai/hl/stp/tactic/receiver/receiver_tactic_test.py @@ -1,17 +1,11 @@ import pytest +from proto.import_all_protos import * 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 software.simulated_tests.ball_enters_region import BallEventuallyEntersRegion from software.simulated_tests.simulated_test_fixture import ( pytest_main, ) -from software.simulated_tests.ball_is_off_ground import * from proto.message_translation.tbots_protobuf import create_world_state corner = tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXNegYCorner()