diff --git a/docs/fsm-diagrams.md b/docs/fsm-diagrams.md index af08fee6ac..4b7fa604da 100644 --- a/docs/fsm-diagrams.md +++ b/docs/fsm-diagrams.md @@ -106,6 +106,19 @@ Terminate:::terminate --> Terminate:::terminate ``` +## [ExamplePlayFSM](/src/software/ai/hl/stp/play/example/example_play_fsm.h) + +```mermaid + +stateDiagram-v2 +classDef terminate fill:white,color:black,font-weight:bold +direction LR +[*] --> MoveState +MoveState --> MoveState : moveToPosition +Terminate:::terminate --> Terminate:::terminate + +``` + ## [FreeKickPlayFSM](/src/software/ai/hl/stp/play/free_kick/free_kick_play_fsm.h) ```mermaid diff --git a/src/software/ai/hl/stp/play/BUILD b/src/software/ai/hl/stp/play/BUILD index 7f2c78772d..bd9c16119d 100644 --- a/src/software/ai/hl/stp/play/BUILD +++ b/src/software/ai/hl/stp/play/BUILD @@ -6,20 +6,6 @@ load("@simulated_tests_deps//:requirements.bzl", "requirement") # "factory" design pattern to work are linked in # https://www.bfilipek.com/2018/02/static-vars-static-lib.html -cc_library( - name = "example_play", - srcs = ["example_play.cpp"], - hdrs = ["example_play.h"], - deps = [ - ":play", - "//shared:constants", - "//software/ai/hl/stp/tactic/move:move_tactic", - "//software/logger", - "//software/util/generic_factory", - ], - alwayslink = True, -) - cc_library( name = "kickoff_enemy_play", srcs = ["kickoff_enemy_play.cpp"], @@ -119,7 +105,6 @@ cc_library( cc_library( name = "all_plays", deps = [ - ":example_play", ":kickoff_enemy_play", ":kickoff_friendly_play", ":shoot_or_chip_play", @@ -129,6 +114,7 @@ cc_library( "//software/ai/hl/stp/play/defense:defense_play", "//software/ai/hl/stp/play/enemy_ball_placement:enemy_ball_placement_play", "//software/ai/hl/stp/play/enemy_free_kick:enemy_free_kick_play", + "//software/ai/hl/stp/play/example:example_play", "//software/ai/hl/stp/play/free_kick:free_kick_play", "//software/ai/hl/stp/play/halt_play", "//software/ai/hl/stp/play/hardware_challenge_plays:dribbling_parcour_play", @@ -142,20 +128,6 @@ cc_library( ], ) -cc_test( - name = "example_play_test", - srcs = ["example_play_test.cpp"], - deps = [ - "//shared/test_util:tbots_gtest_main", - "//software/ai/hl/stp/play:example_play", - "//software/simulated_tests:simulated_er_force_sim_play_test_fixture", - "//software/simulated_tests/validation:validation_function", - "//software/test_util", - "//software/time:duration", - "//software/world", - ], -) - cc_test( name = "kickoff_friendly_play_cpp_test", srcs = ["kickoff_friendly_play_test.cpp"], diff --git a/src/software/ai/hl/stp/play/example/BUILD b/src/software/ai/hl/stp/play/example/BUILD new file mode 100644 index 0000000000..91ba8b5d23 --- /dev/null +++ b/src/software/ai/hl/stp/play/example/BUILD @@ -0,0 +1,63 @@ +package(default_visibility = ["//visibility:public"]) + +load("@simulated_tests_deps//:requirements.bzl", "requirement") + +cc_library( + name = "example_play", + srcs = [ + "example_play.cpp", + "example_play_fsm.cpp", + ], + hdrs = [ + "example_play.h", + "example_play_fsm.h", + ], + deps = [ + "//shared:constants", + "//software/ai/hl/stp/play", + "//software/ai/hl/stp/tactic/move:move_tactic", + "//software/logger", + "//software/util/generic_factory", + ], + alwayslink = True, +) + +cc_test( + name = "example_play_test", + srcs = ["example_play_test.cpp"], + deps = [ + "//shared/test_util:tbots_gtest_main", + "//software/ai/hl/stp/play/example:example_play", + "//software/simulated_tests:simulated_er_force_sim_play_test_fixture", + "//software/simulated_tests/validation:validation_function", + "//software/test_util", + "//software/time:duration", + "//software/world", + ], +) + +cc_test( + name = "example_play_fsm_test", + srcs = ["example_play_fsm_test.cpp"], + deps = [ + ":example_play", + "//shared/test_util:tbots_gtest_main", + "//software/test_util", + ], +) + +py_test( + name = "example_play_test_py", + srcs = ["example_play_test.py"], + main = "example_play_test.py", + # TODO (#2619) Remove tag to run in parallel + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests:speed_threshold_helpers", + "//software/simulated_tests:validation", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/play/example/example_play.cpp b/src/software/ai/hl/stp/play/example/example_play.cpp new file mode 100644 index 0000000000..ecece8d333 --- /dev/null +++ b/src/software/ai/hl/stp/play/example/example_play.cpp @@ -0,0 +1,31 @@ +#include "software/ai/hl/stp/play/example/example_play.h" + +#include "shared/constants.h" +#include "software/util/generic_factory/generic_factory.h" + +ExamplePlay::ExamplePlay(TbotsProto::AiConfig config) + : Play(config, false), fsm{ExamplePlayFSM{}}, control_params{} +{ +} + +void ExamplePlay::getNextTactics(TacticCoroutine::push_type &yield, + const WorldPtr &world_ptr) +{ + // This function doesn't get called and it will be removed once coroutines are phased + // out +} + +void ExamplePlay::updateTactics(const PlayUpdate &play_update) +{ + fsm.process_event(ExamplePlayFSM::Update(control_params, play_update)); +} + +std::vector ExamplePlay::getState() +{ + std::vector state; + state.emplace_back(objectTypeName(*this) + " - " + getCurrentFullStateName(fsm)); + return state; +} + +// Register this play in the genericFactory +static TGenericFactory factory; diff --git a/src/software/ai/hl/stp/play/example/example_play.h b/src/software/ai/hl/stp/play/example/example_play.h new file mode 100644 index 0000000000..debf856cdf --- /dev/null +++ b/src/software/ai/hl/stp/play/example/example_play.h @@ -0,0 +1,26 @@ +#pragma once + +#include "software/ai/hl/stp/play/example/example_play_fsm.h" + +/** + * An example play that moves the robots in a circle around the ball + */ +class ExamplePlay : public Play +{ + public: + /** + * Creates an example play + * + * @param ai_config the play config for this play + */ + ExamplePlay(TbotsProto::AiConfig config); + + void getNextTactics(TacticCoroutine::push_type &yield, + const WorldPtr &world_ptr) override; + void updateTactics(const PlayUpdate &play_update) override; + std::vector getState() override; + + private: + FSM fsm; + ExamplePlayFSM::ControlParams control_params; +}; diff --git a/src/software/ai/hl/stp/play/example/example_play_fsm.cpp b/src/software/ai/hl/stp/play/example/example_play_fsm.cpp new file mode 100644 index 0000000000..a3528a40e1 --- /dev/null +++ b/src/software/ai/hl/stp/play/example/example_play_fsm.cpp @@ -0,0 +1,29 @@ +#include "software/ai/hl/stp/play/example/example_play_fsm.h" + +ExamplePlayFSM::ExamplePlayFSM() : move_tactics(DIV_A_NUM_ROBOTS) +{ + std::generate(move_tactics.begin(), move_tactics.end(), + []() { return std::make_shared(); }); +} + +void ExamplePlayFSM::moveToPosition(const Update &event) +{ + // The angle between each robot spaced out in a circle around the ball + Angle angle_between_robots = Angle::full() / static_cast(move_tactics.size()); + + for (size_t k = 0; k < move_tactics.size(); k++) + { + move_tactics[k]->updateControlParams( + event.common.world_ptr->ball().position() + + Vector::createFromAngle(angle_between_robots * + static_cast(k + 1)), + (angle_between_robots * static_cast(k + 1)) + Angle::half()); + } + + // Set the Tactics this Play wants to run, in order of priority. + // If there are fewer robots in play, robots at the end of the list will not be + // assigned + TacticVector result = {}; + result.insert(result.end(), move_tactics.begin(), move_tactics.end()); + event.common.set_tactics({result}); +} diff --git a/src/software/ai/hl/stp/play/example/example_play_fsm.h b/src/software/ai/hl/stp/play/example/example_play_fsm.h new file mode 100644 index 0000000000..53b6947b74 --- /dev/null +++ b/src/software/ai/hl/stp/play/example/example_play_fsm.h @@ -0,0 +1,51 @@ +#pragma once + +#include "proto/parameters.pb.h" +#include "shared/constants.h" +#include "software/ai/hl/stp/play/play.h" +#include "software/ai/hl/stp/tactic/move/move_tactic.h" +#include "software/logger/logger.h" + +/** + * An example play that moves the robots in a circle around the ball + */ +struct ExamplePlayFSM +{ + class MoveState; + + struct ControlParams + { + }; + + DEFINE_PLAY_UPDATE_STRUCT_WITH_CONTROL_AND_COMMON_PARAMS + + /** + * Creates an example play FSM + */ + explicit ExamplePlayFSM(); + + /** + * Action that moves the robots to certain positions around the ball + * + * @param event the ExamplePlayFSM Update event + */ + void moveToPosition(const Update& event); + + auto operator()() + { + using namespace boost::sml; + + DEFINE_SML_STATE(MoveState) + + DEFINE_SML_EVENT(Update) + + DEFINE_SML_ACTION(moveToPosition) + + return make_transition_table( + // src_state + event [guard] / action = dest_state + *MoveState_S + Update_E / moveToPosition_A = MoveState_S, X + Update_E = X); + } + + private: + std::vector> move_tactics; +}; diff --git a/src/software/ai/hl/stp/play/example/example_play_fsm_test.cpp b/src/software/ai/hl/stp/play/example/example_play_fsm_test.cpp new file mode 100644 index 0000000000..8bf6bc5a83 --- /dev/null +++ b/src/software/ai/hl/stp/play/example/example_play_fsm_test.cpp @@ -0,0 +1,27 @@ +#include "software/ai/hl/stp/play/example/example_play_fsm.h" + +#include + +#include "proto/parameters.pb.h" +#include "software/test_util/equal_within_tolerance.h" +#include "software/test_util/test_util.h" + +TEST(ExamplePlayFSMTest, test_transitions) +{ + std::shared_ptr world = ::TestUtil::createBlankTestingWorld(); + TbotsProto::AiConfig ai_config; + + FSM fsm(ExamplePlayFSM{}); + + EXPECT_TRUE(fsm.is(boost::sml::state)); + + int num_tactics = 6; + + fsm.process_event(ExamplePlayFSM::Update( + ExamplePlayFSM::ControlParams{}, + PlayUpdate( + world, num_tactics, [](PriorityTacticVector new_tactics) {}, + InterPlayCommunication{}, [](InterPlayCommunication comm) {}))); + + EXPECT_TRUE(fsm.is(boost::sml::state)); +} diff --git a/src/software/ai/hl/stp/play/example_play_test.cpp b/src/software/ai/hl/stp/play/example/example_play_test.cpp similarity index 97% rename from src/software/ai/hl/stp/play/example_play_test.cpp rename to src/software/ai/hl/stp/play/example/example_play_test.cpp index 2d39958651..bee24a35ec 100644 --- a/src/software/ai/hl/stp/play/example_play_test.cpp +++ b/src/software/ai/hl/stp/play/example/example_play_test.cpp @@ -1,4 +1,4 @@ -#include "software/ai/hl/stp/play/example_play.h" +#include "software/ai/hl/stp/play/example/example_play.h" #include 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 new file mode 100644 index 0000000000..ef5560feb1 --- /dev/null +++ b/src/software/ai/hl/stp/play/example/example_play_test.py @@ -0,0 +1,100 @@ +import sys + +import pytest + +import software.python_bindings as tbots_cpp +from software.simulated_tests.robot_enters_region import ( + NumberOfRobotsEventuallyExitsRegion, + NumberOfRobotsEventuallyEntersRegion, +) +from software.simulated_tests.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 + + +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), + 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(), + ] + + # Force play override here + 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 + ) + + # 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.NORMAL_START, team=Team.BLUE + ) + simulated_test_runner.gamecontroller.send_gc_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, + params=[0], + inv_always_validation_sequence_set=[[]], + inv_eventually_validation_sequence_set=[ + [ + NumberOfRobotsEventuallyEntersRegion( + region=tbots_cpp.Circle(ball_initial_pos, 1.1), req_robot_cnt=6 + ), + NumberOfRobotsEventuallyExitsRegion( + region=tbots_cpp.Circle(ball_initial_pos, 0.9), req_robot_cnt=6 + ), + ] + ], + ag_always_validation_sequence_set=[[]], + ag_eventually_validation_sequence_set=[[]], + test_timeout_s=10, + ) + + +if __name__ == "__main__": + # Run the test, -s disables all capturing at -vv increases verbosity + sys.exit(pytest.main([__file__, "-svv"])) diff --git a/src/software/ai/hl/stp/play/example_play.cpp b/src/software/ai/hl/stp/play/example_play.cpp deleted file mode 100644 index e9480f767b..0000000000 --- a/src/software/ai/hl/stp/play/example_play.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "software/ai/hl/stp/play/example_play.h" - -#include "software/ai/hl/stp/tactic/move/move_tactic.h" -#include "software/util/generic_factory/generic_factory.h" - -ExamplePlay::ExamplePlay(TbotsProto::AiConfig config) : Play(config, false) {} - -void ExamplePlay::getNextTactics(TacticCoroutine::push_type &yield, - const WorldPtr &world_ptr) -{ - std::vector> move_tactics(DIV_A_NUM_ROBOTS); - std::generate(move_tactics.begin(), move_tactics.end(), - []() { return std::make_shared(); }); - - // Continue to loop to demonstrate the example play indefinitely - do - { - // The angle between each robot spaced out in a circle around the ball - Angle angle_between_robots = - Angle::full() / static_cast(move_tactics.size()); - - for (size_t k = 0; k < move_tactics.size(); k++) - { - move_tactics[k]->updateControlParams( - world_ptr->ball().position() + - Vector::createFromAngle(angle_between_robots * - static_cast(k + 1)), - (angle_between_robots * static_cast(k + 1)) + Angle::half()); - } - - // yield the Tactics this Play wants to run, in order of priority - // If there are fewer robots in play, robots at the end of the list will not be - // assigned - TacticVector result = {}; - result.insert(result.end(), move_tactics.begin(), move_tactics.end()); - yield({result}); - - } while (true); -} - -// Register this play in the genericFactory -static TGenericFactory factory; diff --git a/src/software/ai/hl/stp/play/example_play.h b/src/software/ai/hl/stp/play/example_play.h deleted file mode 100644 index e42c936767..0000000000 --- a/src/software/ai/hl/stp/play/example_play.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "proto/parameters.pb.h" -#include "software/ai/hl/stp/play/play.h" - -/** - * An example Play that moves the robots in a circle around the ball - */ -class ExamplePlay : public Play -{ - public: - explicit ExamplePlay(TbotsProto::AiConfig config); - - void getNextTactics(TacticCoroutine::push_type &yield, - const WorldPtr &world_ptr) override; -}; diff --git a/src/software/ai/hl/stp/tactic/keep_away/keep_away_fsm.h b/src/software/ai/hl/stp/tactic/keep_away/keep_away_fsm.h index 7afb334997..38283aa7d5 100644 --- a/src/software/ai/hl/stp/tactic/keep_away/keep_away_fsm.h +++ b/src/software/ai/hl/stp/tactic/keep_away/keep_away_fsm.h @@ -11,7 +11,7 @@ struct KeepAwayFSM * * @param ai_config The config to fetch parameters from */ - explicit KeepAwayFSM(const TbotsProto::AiConfig& ai_config) : ai_config(ai_config){}; + explicit KeepAwayFSM(const TbotsProto::AiConfig& ai_config) : ai_config(ai_config) {}; struct ControlParams {