diff --git a/docs/fsm-diagrams.md b/docs/fsm-diagrams.md index 8059b8e10d..adfd5c8380 100644 --- a/docs/fsm-diagrams.md +++ b/docs/fsm-diagrams.md @@ -159,6 +159,34 @@ Terminate:::terminate --> Terminate:::terminate : updateStop ``` +## [KickoffEnemyPlayFSM](/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.h) + +```mermaid + +stateDiagram-v2 +classDef terminate fill:white,color:black,font-weight:bold +direction LR +[*] --> SetupState +SetupState --> SetupState : kickoff + +``` + +## [KickoffFriendlyPlayFSM](/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.h) + +```mermaid + +stateDiagram-v2 +classDef terminate fill:white,color:black,font-weight:bold +direction LR +[*] --> SetupState +SetupState --> SetupState : [!isSetupDone]\nsetupKickoff +SetupState --> ChipState : [isSetupDone] +ChipState --> ChipState : [!isPlaying]\nchipBall +ChipState --> Terminate:::terminate : [isPlaying] +Terminate:::terminate --> Terminate:::terminate + +``` + ## [OffensePlayFSM](/src/software/ai/hl/stp/play/offense/offense_play_fsm.h) ```mermaid diff --git a/src/software/ai/hl/stp/play/BUILD b/src/software/ai/hl/stp/play/BUILD index db672cbc01..46cf78cd4a 100644 --- a/src/software/ai/hl/stp/play/BUILD +++ b/src/software/ai/hl/stp/play/BUILD @@ -6,40 +6,6 @@ package(default_visibility = ["//visibility:public"]) # "factory" design pattern to work are linked in # https://www.bfilipek.com/2018/02/static-vars-static-lib.html -cc_library( - name = "kickoff_enemy_play", - srcs = ["kickoff_enemy_play.cpp"], - hdrs = ["kickoff_enemy_play.h"], - deps = [ - ":play", - "//shared:constants", - "//software/ai/evaluation:enemy_threat", - "//software/ai/evaluation:possession", - "//software/ai/hl/stp/tactic/goalie:goalie_tactic", - "//software/ai/hl/stp/tactic/move:move_tactic", - "//software/ai/hl/stp/tactic/shadow_enemy:shadow_enemy_tactic", - "//software/logger", - "//software/util/generic_factory", - ], - alwayslink = True, -) - -cc_library( - name = "kickoff_friendly_play", - srcs = ["kickoff_friendly_play.cpp"], - hdrs = ["kickoff_friendly_play.h"], - deps = [ - ":play", - "//shared:constants", - "//software/ai/evaluation:enemy_threat", - "//software/ai/hl/stp/tactic/chip:chip_tactic", - "//software/ai/hl/stp/tactic/move:move_tactic", - "//software/logger", - "//software/util/generic_factory", - ], - alwayslink = True, -) - cc_library( name = "shoot_or_chip_play", srcs = ["shoot_or_chip_play.cpp"], @@ -106,8 +72,6 @@ cc_library( cc_library( name = "all_plays", deps = [ - ":kickoff_enemy_play", - ":kickoff_friendly_play", ":shoot_or_chip_play", ":stop_play", "//software/ai/hl/stp/play/ball_placement:ball_placement_play", @@ -118,6 +82,8 @@ cc_library( "//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/kickoff_enemy:kickoff_enemy_play", + "//software/ai/hl/stp/play/kickoff_friendly:kickoff_friendly_play", "//software/ai/hl/stp/play/offense:offense_play", "//software/ai/hl/stp/play/penalty_kick:penalty_kick_play", "//software/ai/hl/stp/play/penalty_kick_enemy:penalty_kick_enemy_play", @@ -125,55 +91,6 @@ cc_library( ], ) -cc_test( - name = "kickoff_friendly_play_cpp_test", - srcs = ["kickoff_friendly_play_test.cpp"], - deps = [ - "//shared/test_util:tbots_gtest_main", - "//software/ai/hl/stp/play:kickoff_friendly_play", - "//software/simulated_tests:simulated_er_force_sim_play_test_fixture", - "//software/simulated_tests/non_terminating_validation_functions", - "//software/simulated_tests/terminating_validation_functions", - "//software/simulated_tests/validation:validation_function", - "//software/test_util", - "//software/time:duration", - "//software/world", - ], -) - -cc_test( - name = "kickoff_enemy_play_cpp_test", - srcs = ["kickoff_enemy_play_test.cpp"], - deps = [ - "//shared/test_util:tbots_gtest_main", - "//software/ai/hl/stp/play:kickoff_enemy_play", - "//software/geom/algorithms", - "//software/simulated_tests:simulated_er_force_sim_play_test_fixture", - "//software/simulated_tests/non_terminating_validation_functions", - "//software/simulated_tests/terminating_validation_functions", - "//software/simulated_tests/validation:validation_function", - "//software/test_util", - "//software/time:duration", - "//software/world", - ], -) - -py_test( - name = "kickoff_play_test", - srcs = [ - "kickoff_play_test.py", - ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], - deps = [ - "//software:conftest", - "//software/simulated_tests:validation", - requirement("pytest"), - ], -) - cc_test( name = "stop_play_test", srcs = ["stop_play_test.cpp"], @@ -274,3 +191,19 @@ py_test( requirement("pytest"), ], ) + +py_test( + name = "kickoff_play_test", + srcs = [ + "kickoff_play_test.py", + ], + # TODO (#2619) Remove tag to run in parallel + tags = [ + "exclusive", + ], + deps = [ + "//software:conftest", + "//software/simulated_tests:validation", + requirement("pytest"), + ], +) diff --git a/src/software/ai/hl/stp/play/kickoff_enemy/BUILD b/src/software/ai/hl/stp/play/kickoff_enemy/BUILD new file mode 100644 index 0000000000..a4c26ccb3f --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_enemy/BUILD @@ -0,0 +1,44 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "kickoff_enemy_play", + srcs = [ + "kickoff_enemy_play.cpp", + "kickoff_enemy_play_fsm.cpp", + ], + hdrs = [ + "kickoff_enemy_play.h", + "kickoff_enemy_play_fsm.h", + ], + deps = [ + "//shared:constants", + "//software/ai/evaluation:enemy_threat", + "//software/ai/evaluation:possession", + "//software/ai/hl/stp/play", + "//software/ai/hl/stp/tactic/goalie:goalie_tactic", + "//software/ai/hl/stp/tactic/move:move_tactic", + "//software/ai/hl/stp/tactic/shadow_enemy:shadow_enemy_tactic", + "//software/logger", + "//software/util/generic_factory", + ], + alwayslink = True, +) + +# cc_test( +# name = "kickoff_enemy_play_cpp_test", +# srcs = ["kickoff_enemy_play_test.cpp"], +# deps = [ +# "//shared/test_util:tbots_gtest_main", +# "//software/ai/hl/stp/play:kickoff_enemy_play", +# "//software/geom/algorithms", +# "//software/simulated_tests:simulated_er_force_sim_play_test_fixture", +# "//software/simulated_tests/non_terminating_validation_functions", +# "//software/simulated_tests/terminating_validation_functions", +# "//software/simulated_tests/validation:validation_function", +# "//software/test_util", +# "//software/time:duration", +# "//software/world", +# ], +#) diff --git a/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.cpp b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.cpp new file mode 100644 index 0000000000..c6502c3b14 --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.cpp @@ -0,0 +1,39 @@ +#include "software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.h" + +#include "proto/parameters.pb.h" +#include "shared/constants.h" +#include "software/util/generic_factory/generic_factory.h" + +KickoffEnemyPlay::KickoffEnemyPlay( + std::shared_ptr ai_config_ptr) + : PlayBase(ai_config_ptr, false) +{ +} + +void KickoffEnemyPlay::getNextTactics(TacticCoroutine::push_type &yield, + const WorldPtr &world_ptr) +{ + // Does not get called. + while (true) + { + yield({{}}); + } +} + +void KickoffEnemyPlay::updateTactics(const PlayUpdate &play_update) +{ + fsm.process_event(KickoffEnemyPlayFSM::Update(control_params, play_update)); +} + +std::vector KickoffEnemyPlay::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/kickoff_enemy/kickoff_enemy_play.h b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.h new file mode 100644 index 0000000000..275d41c790 --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.h @@ -0,0 +1,27 @@ +#pragma once + +#include "proto/parameters.pb.h" +#include "software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.h" +#include "software/ai/hl/stp/play/play.h" +#include "software/ai/hl/stp/play/play_base.hpp" +#include "software/ai/hl/stp/play/play_fsm.hpp" + +/** + * A play that runs when its currently the enemy kick off. + */ + +class KickoffEnemyPlay : public PlayBase +{ + public: + /** + * Creates an enemy kickoff play + * + * @param ai_config_ptr the play config for this play + */ + explicit KickoffEnemyPlay(std::shared_ptr ai_config_ptr); + + void getNextTactics(TacticCoroutine::push_type &yield, + const WorldPtr &world_ptr) override; + void updateTactics(const PlayUpdate &play_update) override; + std::vector getState() override; +}; diff --git a/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.cpp b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.cpp new file mode 100644 index 0000000000..c137d340bd --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.cpp @@ -0,0 +1,156 @@ +#include "software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.h" + +KickoffEnemyPlayFSM::KickoffEnemyPlayFSM( + const std::shared_ptr &ai_config_ptr) + : PlayFSM(ai_config_ptr), + shadow_enemy_tactics{std::make_shared(ai_config_ptr), + std::make_shared(ai_config_ptr)}, + move_tactics{std::make_shared(ai_config_ptr), + std::make_shared(ai_config_ptr), + std::make_shared(ai_config_ptr), + std::make_shared(ai_config_ptr), + std::make_shared(ai_config_ptr)} +{ +} + + +void KickoffEnemyPlayFSM::createKickoffSetupPositions(const WorldPtr &world_ptr) +{ + // these positions are picked according to the following: + // createKickoffSetupPositions(); slide + // https://images.slideplayer.com/32/9922349/slides/slide_2.jpg since we only have 6 + // robots at the maximum, 3 robots will shadow threats up front, 1 robot is dedicated + // as the goalie, and the other 2 robots will defend either post (as show in the + // image) + // + // Positions 1,2 are the most important, 3,4,5 are a fallback + // if there aren't as many threats to shadow. Robots will be assigned + // to those positions in order of priority. The 5 positions shown below + // are in the same order as in the defense_position vector. + // + // +--------------------+--------------------+ + // | | | + // | | | + // | | | + // +--+ 2 4 | +--+ + // | | | | | + // | | +-+-+ | | + // | | 3 | | | | + // | | +-+-+ | | + // | | | | | + // +--+ 1 5 | +--+ + // | | | + // | | | + // | | | + // +--------------------+--------------------+ + if (!kickoff_setup_positions.empty()) + { + return; + } + + kickoff_setup_positions = { + Point(world_ptr->field().friendlyGoalpostNeg().x() + + world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, + -world_ptr->field().defenseAreaYLength() / 2.0), + Point(world_ptr->field().friendlyGoalpostPos().x() + + world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, + world_ptr->field().defenseAreaYLength() / 2.0), + Point(world_ptr->field().friendlyGoalCenter().x() + + world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, + world_ptr->field().friendlyGoalCenter().y()), + Point(-(world_ptr->field().centerCircleRadius() + 2 * ROBOT_MAX_RADIUS_METERS), + world_ptr->field().defenseAreaYLength() / 2.0), + Point(-(world_ptr->field().centerCircleRadius() + 2 * ROBOT_MAX_RADIUS_METERS), + -world_ptr->field().defenseAreaYLength() / 2.0), + }; +} + +void KickoffEnemyPlayFSM::assignShadowing(const std::vector &enemy_threats, + PriorityTacticVector &tactics_to_run, + size_t &defense_position_index) +{ + const auto shadower_count = std::min(2, enemy_threats.size()); + + for (size_t i = 0; i < shadower_count; i++) + { + // Assign the first 2 robots to shadow enemies, if the enemies exist + auto enemy_threat = enemy_threats.at(i); + // Shadow with a distance slightly more than the distance from the enemy + // robot to the center line, so we are always just on our side of the + // center line + double shadow_dist = + std::fabs(enemy_threat.robot.position().x()) + 2 * ROBOT_MAX_RADIUS_METERS; + // We shadow assuming the robots do not pass so we do not try block passes + // while shadowing, since we can't go on the enemy side to block the pass + // anyway + shadow_enemy_tactics.at(i)->updateControlParams(enemy_threat, shadow_dist); + + tactics_to_run[0].emplace_back(shadow_enemy_tactics.at(i)); + } +} + +void KickoffEnemyPlayFSM::assignDefenders(PriorityTacticVector &tactics_to_run, + size_t &defense_position_index) +{ + while (defense_position_index < move_tactics.size() - 1 && + defense_position_index < kickoff_setup_positions.size()) + { + move_tactics.at(defense_position_index) + ->updateControlParams(kickoff_setup_positions.at(defense_position_index), + Angle::zero()); + tactics_to_run[0].emplace_back(move_tactics.at(defense_position_index)); + defense_position_index++; + } +} + +void KickoffEnemyPlayFSM::assignGoalBlocker(const WorldPtr &world_ptr, + PriorityTacticVector &tactics_to_run, + size_t &defense_position_index) +{ + move_tactics.back()->updateControlParams( + calculateBlockCone(world_ptr->field().friendlyGoalpostPos(), + world_ptr->field().friendlyGoalpostNeg(), + world_ptr->field().centerPoint(), ROBOT_MAX_RADIUS_METERS), + Angle::zero(), TbotsProto::MaxAllowedSpeedMode::PHYSICAL_LIMIT, + TbotsProto::ObstacleAvoidanceMode::AGGRESSIVE); + tactics_to_run[0].emplace_back(move_tactics.at(defense_position_index)); + defense_position_index++; +} + +void KickoffEnemyPlayFSM::kickoff(const Update &event) +{ + createKickoffSetupPositions(event.common.world_ptr); + WorldPtr world_ptr = event.common.world_ptr; + Team enemy_team = world_ptr->enemyTeam(); + PriorityTacticVector tactics_to_run = {{}}; + + // TODO: (Mathew): Minor instability with defenders and goalie when the ball and + // attacker are in the middle of the net + + // We find the nearest enemy robot closest to (0,0) then ignore it from the enemy + // team. Since the center circle is a motion constraint during enemy kickoff, the + // shadowing robot will navigate to the closest point that it can to shadow, which + // might not be ideal. (i.e robot won't block a straight shot on net) + auto robot = Team::getNearestRobot(world_ptr->enemyTeam().getAllRobots(), + world_ptr->field().centerPoint()); + if (robot.has_value()) + { + int robot_id = robot.value().id(); + enemy_team.removeRobotWithId(robot_id); + } + else + { + LOG(WARNING) << "No Robot on the Field!"; + } + + auto enemy_threats = + getAllEnemyThreats(world_ptr->field(), world_ptr->friendlyTeam(), + world_ptr->enemyTeam(), world_ptr->ball(), false); + + size_t defense_position_index = 0; + assignShadowing(enemy_threats, tactics_to_run, defense_position_index); + assignDefenders(tactics_to_run, defense_position_index); + assignGoalBlocker(world_ptr, tactics_to_run, defense_position_index); + + event.common.set_tactics(tactics_to_run); +} diff --git a/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.h b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.h new file mode 100644 index 0000000000..bcb9a6447e --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_fsm.h @@ -0,0 +1,109 @@ +#pragma once + +#include "proto/parameters.pb.h" +#include "shared/constants.h" +#include "software/ai/evaluation/enemy_threat.h" +#include "software/ai/evaluation/possession.h" +#include "software/ai/hl/stp/play/play.h" +#include "software/ai/hl/stp/play/play_fsm.hpp" +#include "software/ai/hl/stp/tactic/move/move_tactic.h" +#include "software/ai/hl/stp/tactic/shadow_enemy/shadow_enemy_tactic.h" +#include "software/geom/algorithms/calculate_block_cone.h" +#include "software/logger/logger.h" + + +/** + * This FSM implements the Kickoff Enemy Play. It manages kickoff when the enemy side is + * kicking. + * - Bots will shadow the enemy robots but stay on the correct side of the field. + */ +struct KickoffEnemyPlayFSM : PlayFSM +{ + class SetupState; + + /** + * Control Parameters for a Kickoff Enemy Play + */ + struct ControlParams + { + }; + + + /** + * Creates a kickoff enemy play FSM + * + * @param ai_config the play config for this play FSM + */ + explicit KickoffEnemyPlayFSM( + const std::shared_ptr &ai_config_ptr); + + + /** + * create a vector of setup positions if not already existing. + * + * @param world_ptr the world pointer + */ + void createKickoffSetupPositions(const WorldPtr &world_ptr); + + /** + * add shadowing robots to tactics to run. + * + * @param enemy_threats the enemies that must be shadowed. + * @param tactics_to_run vector of tactics to run. + * @param defense_position_index index of robot for priority. + */ + void assignShadowing(const std::vector &enemy_threats, + PriorityTacticVector &tactics_to_run, + size_t &defense_position_index); + + /** + * add defenders to tactics to run. + * + * @param tactics_to_run vector of tactics to run. + * @param defense_position_index index of robot for priority. + */ + void assignDefenders(PriorityTacticVector &tactics_to_run, + size_t &defense_position_index); + + /** + * add a goal blocker to tactics to run. + * + * @param world_ptr the world pointer + * @param tactics_to_run vector of tactics to run. + * @param defense_position_index index of robot for priority. + */ + void assignGoalBlocker(const WorldPtr &world_ptr, + PriorityTacticVector &tactics_to_run, + size_t &defense_position_index); + + /** + * Action to organize the bots to be ready for enemy kickoff. + * + * @param event the FreeKickPlayFSM Update event + */ + void kickoff(const Update &event); + + + auto operator()() + { + using namespace boost::sml; + + DEFINE_SML_STATE(SetupState) + + DEFINE_SML_EVENT(Update) + + DEFINE_SML_ACTION(kickoff) + + + return make_transition_table( + // src_state + event [guard] / action = dest_state + // PlaySelectionFSM will transition to OffensePlay after the kick. + *SetupState_S + Update_E / kickoff_A = SetupState_S); + } + + private: + TbotsProto::AiConfig ai_config; + std::vector> shadow_enemy_tactics; + std::vector> move_tactics; + std::vector kickoff_setup_positions; +}; diff --git a/src/software/ai/hl/stp/play/kickoff_enemy_play_test.cpp b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_test.cpp similarity index 97% rename from src/software/ai/hl/stp/play/kickoff_enemy_play_test.cpp rename to src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_test.cpp index e9ca71ca24..6a363cceaf 100644 --- a/src/software/ai/hl/stp/play/kickoff_enemy_play_test.cpp +++ b/src/software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play_test.cpp @@ -1,4 +1,4 @@ -#include "software/ai/hl/stp/play/kickoff_enemy_play.h" +#include "software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.h" #include diff --git a/src/software/ai/hl/stp/play/kickoff_enemy_play.cpp b/src/software/ai/hl/stp/play/kickoff_enemy_play.cpp deleted file mode 100644 index 2f9ad2027c..0000000000 --- a/src/software/ai/hl/stp/play/kickoff_enemy_play.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "software/ai/hl/stp/play/kickoff_enemy_play.h" - -#include "proto/parameters.pb.h" -#include "shared/constants.h" -#include "software/ai/evaluation/enemy_threat.h" -#include "software/ai/evaluation/possession.h" -#include "software/ai/hl/stp/tactic/move/move_tactic.h" -#include "software/ai/hl/stp/tactic/shadow_enemy/shadow_enemy_tactic.h" -#include "software/geom/algorithms/calculate_block_cone.h" -#include "software/util/generic_factory/generic_factory.h" - -KickoffEnemyPlay::KickoffEnemyPlay( - std::shared_ptr ai_config_ptr) - : Play(ai_config_ptr, true) -{ -} - -void KickoffEnemyPlay::getNextTactics(TacticCoroutine::push_type &yield, - const WorldPtr &world_ptr) -{ - // 3 robots assigned to shadow enemies. Other robots will be assigned positions - // on the field to be evenly spread out - std::vector> shadow_enemy_tactics( - 2, std::make_shared(ai_config_ptr)); - - // these positions are picked according to the following slide - // https://images.slideplayer.com/32/9922349/slides/slide_2.jpg - // since we only have 6 robots at the maximum, 3 robots will shadow threats - // up front, 1 robot is dedicated as the goalie, and the other 2 robots will defend - // either post (as show in the image) - // - // Positions 1,2 are the most important, 3,4,5 are a fallback - // if there aren't as many threats to shadow. Robots will be assigned - // to those positions in order of priority. The 5 positions shown below - // are in the same order as in the defense_position vector. - // - // +--------------------+--------------------+ - // | | | - // | | | - // | | | - // +--+ 2 4 | +--+ - // | | | | | - // | | +-+-+ | | - // | | 3 | | | | - // | | +-+-+ | | - // | | | | | - // +--+ 1 5 | +--+ - // | | | - // | | | - // | | | - // +--------------------+--------------------+ - - std::vector defense_positions = { - Point(world_ptr->field().friendlyGoalpostNeg().x() + - world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, - -world_ptr->field().defenseAreaYLength() / 2.0), - Point(world_ptr->field().friendlyGoalpostPos().x() + - world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, - world_ptr->field().defenseAreaYLength() / 2.0), - Point(world_ptr->field().friendlyGoalCenter().x() + - world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, - world_ptr->field().friendlyGoalCenter().y()), - Point(-(world_ptr->field().centerCircleRadius() + 2 * ROBOT_MAX_RADIUS_METERS), - world_ptr->field().defenseAreaYLength() / 2.0), - Point(-(world_ptr->field().centerCircleRadius() + 2 * ROBOT_MAX_RADIUS_METERS), - -world_ptr->field().defenseAreaYLength() / 2.0), - }; - // these move tactics will be used to go to those positions - std::vector> move_tactics( - 5, std::make_shared(ai_config_ptr)); - - // created an enemy_team for mutation - Team enemy_team = world_ptr->enemyTeam(); - - do - { - // TODO: (Mathew): Minor instability with defenders and goalie when the ball and - // attacker are in the middle of the net - - // We find the nearest enemy robot closest to (0,0) then ignore it from the enemy - // team. Since the center circle is a motion constraint during enemy kickoff, the - // shadowing robot will navigate to the closest point that it can to shadow, which - // might not be ideal. (i.e robot won't block a straight shot on net) - auto robot = Team::getNearestRobot(world_ptr->enemyTeam().getAllRobots(), - world_ptr->field().centerPoint()); - if (robot.has_value()) - { - int robot_id = robot.value().id(); - enemy_team.removeRobotWithId(robot_id); - } - else - { - LOG(WARNING) << "No Robot on the Field!"; - } - - auto enemy_threats = - getAllEnemyThreats(world_ptr->field(), world_ptr->friendlyTeam(), - world_ptr->enemyTeam(), world_ptr->ball(), false); - - PriorityTacticVector result = {{}}; - - // keeps track of the next defense position to assign - int defense_position_index = 0; - for (unsigned i = 0; i < defense_positions.size() - 1; ++i) - { - if (i < 2 && i < enemy_threats.size()) - { - // Assign the first 2 robots to shadow enemies, if the enemies exist - auto enemy_threat = enemy_threats.at(i); - // Shadow with a distance slightly more than the distance from the enemy - // robot to the center line, so we are always just on our side of the - // center line - double shadow_dist = std::fabs(enemy_threat.robot.position().x()) + - 2 * ROBOT_MAX_RADIUS_METERS; - // We shadow assuming the robots do not pass so we do not try block passes - // while shadowing, since we can't go on the enemy side to block the pass - // anyway - shadow_enemy_tactics.at(i)->updateControlParams(enemy_threat, - shadow_dist); - - result[0].emplace_back(shadow_enemy_tactics.at(i)); - } - else - { - // Once we are out of enemies to shadow, or are already shadowing 2 - // enemies, we move the rest of the robots to the defense positions - // listed above - move_tactics.at(defense_position_index) - ->updateControlParams(defense_positions.at(defense_position_index), - Angle::zero()); - result[0].emplace_back(move_tactics.at(defense_position_index)); - defense_position_index++; - } - } - - // update robot 3 to be directly between the ball and the friendly net - move_tactics.at(defense_position_index) - ->updateControlParams( - calculateBlockCone(world_ptr->field().friendlyGoalpostPos(), - world_ptr->field().friendlyGoalpostNeg(), - world_ptr->field().centerPoint(), - ROBOT_MAX_RADIUS_METERS), - Angle::zero(), TbotsProto::MaxAllowedSpeedMode::PHYSICAL_LIMIT, - TbotsProto::ObstacleAvoidanceMode::AGGRESSIVE); - result[0].emplace_back(move_tactics.at(defense_position_index)); - - // yield the Tactics this Play wants to run, in order of priority - yield(result); - } while (true); -} - -// Register this play in the genericFactory -static TGenericFactory> - factory; diff --git a/src/software/ai/hl/stp/play/kickoff_enemy_play.h b/src/software/ai/hl/stp/play/kickoff_enemy_play.h deleted file mode 100644 index f85425ca03..0000000000 --- a/src/software/ai/hl/stp/play/kickoff_enemy_play.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "proto/parameters.pb.h" -#include "software/ai/hl/stp/play/play.h" - -/** - * A play that runs when its currently the enemies kick off, - * prioritizes defending the net and shadowing the robot - * that is nearest to the ball. Any remaining bots will block - * some odd angles to the net. - */ -class KickoffEnemyPlay : public Play -{ - public: - KickoffEnemyPlay(std::shared_ptr ai_config_ptr); - - void getNextTactics(TacticCoroutine::push_type &yield, - const WorldPtr &world_ptr) override; -}; diff --git a/src/software/ai/hl/stp/play/kickoff_friendly/BUILD b/src/software/ai/hl/stp/play/kickoff_friendly/BUILD new file mode 100644 index 0000000000..17a0943cd4 --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_friendly/BUILD @@ -0,0 +1,42 @@ +load("@simulated_tests_deps//:requirements.bzl", "requirement") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "kickoff_friendly_play", + srcs = [ + "kickoff_friendly_play.cpp", + "kickoff_friendly_play_fsm.cpp", + ], + hdrs = [ + "kickoff_friendly_play.h", + "kickoff_friendly_play_fsm.h", + ], + deps = [ + "//shared:constants", + "//software/ai/evaluation:enemy_threat", + "//software/ai/evaluation:find_open_areas", + "//software/ai/hl/stp/play", + "//software/ai/hl/stp/tactic/chip:chip_tactic", + "//software/ai/hl/stp/tactic/move:move_tactic", + "//software/logger", + "//software/util/generic_factory", + ], + alwayslink = True, +) + +#cc_test( +# name = "kickoff_friendly_play_cpp_test", +# srcs = ["kickoff_friendly_play_test.cpp"], +# deps = [ +# "//shared/test_util:tbots_gtest_main", +# "//software/ai/hl/stp/play/kickoff_friendly:kickoff_friendly_play", +# "//software/simulated_tests:simulated_er_force_sim_play_test_fixture", +# "//software/simulated_tests/non_terminating_validation_functions", +# "//software/simulated_tests/terminating_validation_functions", +# "//software/simulated_tests/validation:validation_function", +# "//software/test_util", +# "//software/time:duration", +# "//software/world", +# ], +#) diff --git a/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.cpp b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.cpp new file mode 100644 index 0000000000..c8651e8e84 --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.cpp @@ -0,0 +1,39 @@ +#include "software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.h" + +#include "proto/parameters.pb.h" +#include "shared/constants.h" +#include "software/util/generic_factory/generic_factory.h" + + +KickoffFriendlyPlay::KickoffFriendlyPlay( + std::shared_ptr ai_config_ptr) + : PlayBase(ai_config_ptr, true) +{ +} + +void KickoffFriendlyPlay::getNextTactics(TacticCoroutine::push_type &yield, + const WorldPtr &world_ptr) +{ + // Does not get called. + while (true) + { + yield({{}}); + } +} + +void KickoffFriendlyPlay::updateTactics(const PlayUpdate &play_update) +{ + fsm.process_event(KickoffFriendlyPlayFSM::Update(control_params, play_update)); +} + +std::vector KickoffFriendlyPlay::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/kickoff_friendly/kickoff_friendly_play.h b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.h new file mode 100644 index 0000000000..7fa06804fe --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.h @@ -0,0 +1,29 @@ +#pragma once + +#include "proto/parameters.pb.h" +#include "software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.h" +#include "software/ai/hl/stp/play/play.h" +#include "software/ai/hl/stp/play/play_base.hpp" +#include "software/ai/hl/stp/play/play_fsm.hpp" + +/** + * A play that runs when its currently the friendly kick off, + * only one robot grabs the ball and passes to another robot. + */ + +class KickoffFriendlyPlay : public PlayBase +{ + public: + /** + * Creates a friendly kickoff play + * + * @param ai_config_ptr the play config for this play + */ + explicit KickoffFriendlyPlay( + std::shared_ptr ai_config_ptr); + + void getNextTactics(TacticCoroutine::push_type &yield, + const WorldPtr &world_ptr) override; + void updateTactics(const PlayUpdate &play_update) override; + std::vector getState() override; +}; diff --git a/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.cpp b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.cpp new file mode 100644 index 0000000000..c43190e511 --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.cpp @@ -0,0 +1,150 @@ +#include "software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.h" + +KickoffFriendlyPlayFSM::KickoffFriendlyPlayFSM( + const std::shared_ptr& ai_config_ptr) + : PlayFSM(ai_config_ptr), + kickoff_chip_tactic(std::make_shared(ai_config_ptr)), + move_tactics{std::make_shared(ai_config_ptr), // robot 1 + std::make_shared(ai_config_ptr), // robot 2-5 + std::make_shared(ai_config_ptr), + std::make_shared(ai_config_ptr), + std::make_shared(ai_config_ptr)} +{ +} + +void KickoffFriendlyPlayFSM::createKickoffSetupPositions(const WorldPtr& world_ptr) +{ + // Since we only have 6 robots at the maximum, the number one priority + // is the robot doing the kickoff up front. The goalie is the second most + // important, followed by 3 and 4 setup for offense. 5 and 6 will stay + // back near the goalie just in case the ball quickly returns to the friendly + // side of the field. + // + // +--------------------+--------------------+ + // | | | + // | 3 | | + // | | | + // +--+ 5 | +--+ + // | | | | | + // | | +-+-+ | | + // |2 | |1 | | | + // | | +-+-+ | | + // | | | | | + // +--+ 6 | +--+ + // | | | + // | 4 | | + // | | | + // +--------------------+--------------------+ + // + + if (kickoff_setup_positions.empty()) + { + kickoff_setup_positions = { + // Robot 1 + Point(world_ptr->field().centerPoint() + + Vector(-world_ptr->field().centerCircleRadius(), 0)), + // Robot 2 + // Goalie positions will be handled by the goalie tactic + // Robot 3 + Point(world_ptr->field().centerPoint() + + Vector(-world_ptr->field().centerCircleRadius() - + 4 * ROBOT_MAX_RADIUS_METERS, + -1.0 / 3.0 * world_ptr->field().yLength())), + // Robot 4 + Point(world_ptr->field().centerPoint() + + Vector(-world_ptr->field().centerCircleRadius() - + 4 * ROBOT_MAX_RADIUS_METERS, + 1.0 / 3.0 * world_ptr->field().yLength())), + // Robot 5 + Point(world_ptr->field().friendlyGoalpostPos().x() + + world_ptr->field().defenseAreaXLength() + + 2 * ROBOT_MAX_RADIUS_METERS, + world_ptr->field().friendlyGoalpostPos().y()), + // Robot 6 + Point(world_ptr->field().friendlyGoalpostNeg().x() + + world_ptr->field().defenseAreaXLength() + + 2 * ROBOT_MAX_RADIUS_METERS, + world_ptr->field().friendlyGoalpostNeg().y()), + }; + } +} + + +void KickoffFriendlyPlayFSM::setupKickoff(const Update& event) +{ + createKickoffSetupPositions(event.common.world_ptr); + + PriorityTacticVector tactics_to_run = {{}}; + + // first priority requires the ability to kick and chip. + move_tactics.at(0)->mutableRobotCapabilityRequirements() = {RobotCapability::Kick, + RobotCapability::Chip}; + + // set each tactic to its movement location. + for (unsigned i = 0; i < kickoff_setup_positions.size(); i++) + { + move_tactics.at(i)->updateControlParams(kickoff_setup_positions.at(i), + Angle::zero()); + tactics_to_run[0].emplace_back(move_tactics.at(i)); + } + + event.common.set_tactics(tactics_to_run); +} + + +void KickoffFriendlyPlayFSM::chipBall(const Update& event) +{ + const WorldPtr& world_ptr = event.common.world_ptr; + const auto& field = world_ptr->field(); + const Point ball_position = world_ptr->ball().position(); + + PriorityTacticVector tactics_to_run = {{}}; + + constexpr double enemy_x_padding_m = 2.0; + constexpr double sideline_padding_m = 0.3; + constexpr double fallback_target_x_fraction = 1.0 / 6.0; + + const double min_chip_x = ball_position.x(); + const double max_chip_x = field.enemyGoalCenter().x() - enemy_x_padding_m; + const double min_chip_y = field.enemyCornerNeg().y() + sideline_padding_m; + const double max_chip_y = field.enemyCornerPos().y() - sideline_padding_m; + + const Rectangle chip_target_region(Point(min_chip_x, min_chip_y), + Point(max_chip_x, max_chip_y)); + + const Point fallback_target = + field.centerPoint() + Vector(field.xLength() * fallback_target_x_fraction, 0.0); + const std::vector chip_targets = + findGoodChipTargets(*world_ptr, chip_target_region); + + Point selected_target = fallback_target; + + // Chooses a viable open circle that is closest to the enemy goal. + if (!chip_targets.empty()) + { + const auto best_target_it = + std::min_element(chip_targets.begin(), chip_targets.end(), + [&field](const Circle& a, const Circle& b) + { + return distance(field.enemyGoalCenter(), a.origin()) < + distance(field.enemyGoalCenter(), b.origin()); + }); + + selected_target = best_target_it->origin(); + } + + kickoff_chip_tactic->updateControlParams(ball_position, selected_target); + tactics_to_run[0].emplace_back(kickoff_chip_tactic); + event.common.set_tactics(tactics_to_run); +} + + +bool KickoffFriendlyPlayFSM::isSetupDone(const Update& event) +{ + return !event.common.world_ptr->gameState().isSetupState(); +} + +bool KickoffFriendlyPlayFSM::isPlaying(const Update& event) +{ + return event.common.world_ptr->gameState().isPlaying(); +} diff --git a/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.h b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.h new file mode 100644 index 0000000000..6dd163523e --- /dev/null +++ b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_fsm.h @@ -0,0 +1,111 @@ +#pragma once + +#include "proto/parameters.pb.h" +#include "shared/constants.h" +#include "software/ai/evaluation/enemy_threat.h" +#include "software/ai/evaluation/find_open_areas.h" +#include "software/ai/hl/stp/play/play.h" +#include "software/ai/hl/stp/play/play_fsm.hpp" +#include "software/ai/hl/stp/tactic/chip/chip_tactic.h" +#include "software/ai/hl/stp/tactic/move/move_tactic.h" +#include "software/logger/logger.h" + + +/** + * This FSM implements the Kickoff Friendly Play. It manages kickoff when the friendly + * side is kicking. + * - It positions robots to starting points. + * - It stays ready to start the game. + * - It chips the ball into the largest open circle that is sufficiently close to the + * enemy net, but also reasonably far from the edges of the field. + * - Terminates after the ball is touched, passing control to OffensePlay. + */ +struct KickoffFriendlyPlayFSM : PlayFSM +{ + class SetupState; + class ChipState; + + /** + * Control Parameters for a Kickoff Friendly Play + */ + struct ControlParams + { + }; + + + /** + * Creates a kickoff friendly play FSM + * + * @param ai_config_ptr the play config for this play FSM + */ + explicit KickoffFriendlyPlayFSM( + const std::shared_ptr& ai_config_ptr); + + + /** + * create a vector of setup positions if not already existing. + * + * @param world_ptr the world pointer + */ + void createKickoffSetupPositions(const WorldPtr& world_ptr); + + /** + * Action to move robots to starting positions + * + * @param event the FreeKickPlayFSM Update event + */ + void setupKickoff(const Update& event); + + /** + * Action to chip the ball forward over the defenders. + * - Creates a rectangle within the enemy half of the field with padding. + * - Finds the largest open circles between enemy bots. + * - Chooses the largest viable open circle that is closest to the enemy net. + * - Defaults to a short chip if no open circle returned. + * @param event the FreeKickPlayFSM Update event + */ + void chipBall(const Update& event); + + /** + * Guard that checks if positions are set up. + * + * @param event the FreeKickPlayFSM Update event + */ + static bool isSetupDone(const Update& event); + + /** + * Guard that checks if game has started (ball kicked). + * + * @param event the FreeKickPlayFSM Update event + */ + static bool isPlaying(const Update& event); + + auto operator()() + { + using namespace boost::sml; + + DEFINE_SML_STATE(SetupState) + DEFINE_SML_STATE(ChipState) + + DEFINE_SML_EVENT(Update) + + DEFINE_SML_ACTION(setupKickoff) + DEFINE_SML_ACTION(chipBall) + + DEFINE_SML_GUARD(isSetupDone) + DEFINE_SML_GUARD(isPlaying) + return make_transition_table( + *SetupState_S + Update_E[!isSetupDone_G] / setupKickoff_A = SetupState_S, + SetupState_S + Update_E[isSetupDone_G] = ChipState_S, + ChipState_S + Update_E[!isPlaying_G] / chipBall_A = ChipState_S, + ChipState_S + Update_E[isPlaying_G] = X, + + X + Update_E = X); + } + + private: + TbotsProto::AiConfig ai_config; + std::shared_ptr kickoff_chip_tactic; + std::vector> move_tactics; + std::vector kickoff_setup_positions; +}; diff --git a/src/software/ai/hl/stp/play/kickoff_friendly_play_test.cpp b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_test.cpp similarity index 97% rename from src/software/ai/hl/stp/play/kickoff_friendly_play_test.cpp rename to src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_test.cpp index 26b164fcdf..73a5dab569 100644 --- a/src/software/ai/hl/stp/play/kickoff_friendly_play_test.cpp +++ b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_test.cpp @@ -1,4 +1,4 @@ -#include "software/ai/hl/stp/play/kickoff_friendly_play.h" +#include "software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.h" #include diff --git a/src/software/ai/hl/stp/play/kickoff_friendly_play.cpp b/src/software/ai/hl/stp/play/kickoff_friendly_play.cpp deleted file mode 100644 index 1898be7716..0000000000 --- a/src/software/ai/hl/stp/play/kickoff_friendly_play.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include "software/ai/hl/stp/play/kickoff_friendly_play.h" - -#include "shared/constants.h" -#include "software/ai/evaluation/enemy_threat.h" -#include "software/ai/hl/stp/tactic/chip/chip_tactic.h" -#include "software/ai/hl/stp/tactic/move/move_tactic.h" -#include "software/util/generic_factory/generic_factory.h" - -KickoffFriendlyPlay::KickoffFriendlyPlay( - std::shared_ptr ai_config_ptr) - : Play(ai_config_ptr, true) -{ -} - -void KickoffFriendlyPlay::getNextTactics(TacticCoroutine::push_type &yield, - const WorldPtr &world_ptr) -{ - // Since we only have 6 robots at the maximum, the number one priority - // is the robot doing the kickoff up front. The goalie is the second most - // important, followed by 3 and 4 setup for offense. 5 and 6 will stay - // back near the goalie just in case the ball quickly returns to the friendly - // side of the field. - // - // +--------------------+--------------------+ - // | | | - // | 3 | | - // | | | - // +--+ 5 | +--+ - // | | | | | - // | | +-+-+ | | - // |2 | |1 | | | - // | | +-+-+ | | - // | | | | | - // +--+ 6 | +--+ - // | | | - // | 4 | | - // | | | - // +--------------------+--------------------+ - // - // This is a two part play: - // Part 1: Get into position, but don't touch the ball (ref kickoff) - // Part 2: Chip the ball over the defender (ref normal start) - - // the following positions are in the same order as the positions shown above, - // excluding the goalie for part 1 of this play - std::vector kickoff_setup_positions = { - // Robot 1 - Point(world_ptr->field().centerPoint() + - Vector(-world_ptr->field().centerCircleRadius(), 0)), - // Robot 2 - // Goalie positions will be handled by the goalie tactic - // Robot 3 - Point( - world_ptr->field().centerPoint() + - Vector(-world_ptr->field().centerCircleRadius() - 4 * ROBOT_MAX_RADIUS_METERS, - -1.0 / 3.0 * world_ptr->field().yLength())), - // Robot 4 - Point( - world_ptr->field().centerPoint() + - Vector(-world_ptr->field().centerCircleRadius() - 4 * ROBOT_MAX_RADIUS_METERS, - 1.0 / 3.0 * world_ptr->field().yLength())), - // Robot 5 - Point(world_ptr->field().friendlyGoalpostPos().x() + - world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, - world_ptr->field().friendlyGoalpostPos().y()), - // Robot 6 - Point(world_ptr->field().friendlyGoalpostNeg().x() + - world_ptr->field().defenseAreaXLength() + 2 * ROBOT_MAX_RADIUS_METERS, - world_ptr->field().friendlyGoalpostNeg().y()), - }; - - // move tactics to use to move to positions defined above - std::vector> move_tactics = { - std::make_shared(ai_config_ptr), - std::make_shared(ai_config_ptr), - std::make_shared(ai_config_ptr), - std::make_shared(ai_config_ptr), - std::make_shared(ai_config_ptr)}; - - // specific tactics - auto kickoff_chip_tactic = std::make_shared(ai_config_ptr); - - // Part 1: setup state (move to key positions) - while (world_ptr->gameState().isSetupState()) - { - auto enemy_threats = - getAllEnemyThreats(world_ptr->field(), world_ptr->friendlyTeam(), - world_ptr->enemyTeam(), world_ptr->ball(), false); - - PriorityTacticVector result = {{}}; - - // set the requirement that Robot 1 must be able to kick and chip - move_tactics.at(0)->mutableRobotCapabilityRequirements() = { - RobotCapability::Kick, RobotCapability::Chip}; - - // setup 5 kickoff positions in order of priority - for (unsigned i = 0; i < kickoff_setup_positions.size(); i++) - { - move_tactics.at(i)->updateControlParams(kickoff_setup_positions.at(i), - Angle::zero()); - result[0].emplace_back(move_tactics.at(i)); - } - - // yield the Tactics this Play wants to run, in order of priority - yield(result); - } - - // Part 2: not normal play, currently ready state (chip the ball) - while (!world_ptr->gameState().isPlaying()) - { - auto enemy_threats = - getAllEnemyThreats(world_ptr->field(), world_ptr->friendlyTeam(), - world_ptr->enemyTeam(), world_ptr->ball(), false); - - PriorityTacticVector result = {{}}; - - // TODO (#2612): This needs to be adjusted post field testing, ball needs to land - // exactly in the middle of the enemy field - kickoff_chip_tactic->updateControlParams( - world_ptr->ball().position(), - world_ptr->field().centerPoint() + - Vector(world_ptr->field().xLength() / 6, 0)); - result[0].emplace_back(kickoff_chip_tactic); - - // the robot at position 0 will be closest to the ball, so positions starting from - // 1 will be assigned to the rest of the robots - for (unsigned i = 1; i < kickoff_setup_positions.size(); i++) - { - move_tactics.at(i)->updateControlParams(kickoff_setup_positions.at(i), - Angle::zero()); - result[0].emplace_back(move_tactics.at(i)); - } - - // yield the Tactics this Play wants to run, in order of priority - yield(result); - } -} - - -// Register this play in the genericFactory -static TGenericFactory> - factory; diff --git a/src/software/ai/hl/stp/play/kickoff_friendly_play.h b/src/software/ai/hl/stp/play/kickoff_friendly_play.h deleted file mode 100644 index 8719e688a8..0000000000 --- a/src/software/ai/hl/stp/play/kickoff_friendly_play.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "proto/parameters.pb.h" -#include "software/ai/hl/stp/play/play.h" - -/** - * A play that runs when its currently the friendly kick off, - * only one robot grabs the ball and passes to another robot. - */ -class KickoffFriendlyPlay : public Play -{ - public: - KickoffFriendlyPlay(std::shared_ptr ai_config_ptr); - - void getNextTactics(TacticCoroutine::push_type &yield, - const WorldPtr &world_ptr) override; -}; 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..b0fde37ccf 100644 --- a/src/software/ai/hl/stp/play/kickoff_play_test.py +++ b/src/software/ai/hl/stp/play/kickoff_play_test.py @@ -1,3 +1,5 @@ +import sys + import pytest import software.python_bindings as tbots_cpp @@ -8,18 +10,11 @@ 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 -@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 = [ +def setup_kickoff_attackers(): + return [ tbots_cpp.Point(-3, 2.5), tbots_cpp.Point(-3, 1.5), tbots_cpp.Point(-3, 0.5), @@ -28,121 +23,105 @@ def test_kickoff_play(simulated_test_runner, is_friendly_test): tbots_cpp.Point(-3, -2.5), ] - yellow_bots = [ - tbots_cpp.Point(1, 0), + +def setup_kickoff_defenders(): + return [ + tbots_cpp.Point(1, 0.5), tbots_cpp.Point(1, 2.5), tbots_cpp.Point(1, -2.5), - tbots_cpp.Field.createSSLDivisionBField().enemyGoalCenter(), + tbots_cpp.Point(2, -1.5), tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXNegYCorner(), tbots_cpp.Field.createSSLDivisionBField().enemyDefenseArea().negXPosYCorner(), ] - blue_play = Play() - yellow_play = Play() - - # 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.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 - ) - 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( +def init_world_state(runner, blue_bots, yellow_bots): + 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_location=tbots_cpp.Point(0, 0), ball_velocity=tbots_cpp.Vector(0, 0), ), ) - # Always Validation - always_validation_sequence_set = [[]] - ball_moves_at_rest_validation = BallAlwaysMovesFromRest( - position=tbots_cpp.Point(0, 0), threshold=0.05 +def test_blue_kickoff_chip(simulated_test_runner): + ball_initial_pos = tbots_cpp.Point(0, 0) + field = tbots_cpp.Field.createSSLDivisionBField() + + init_world_state( + simulated_test_runner, + setup_kickoff_attackers(), + setup_kickoff_defenders(), ) - expected_center_circle_or_validation_set = [ - ball_moves_at_rest_validation, - NumberOfRobotsAlwaysStaysInRegion( - regions=[tbots_cpp.Field.createSSLDivisionBField().centerCircle()], - req_robot_cnt=0, - ), - ] + blue_play = Play() + blue_play.name = PlayName.KickoffFriendlyPlay - friendly_half = tbots_cpp.Field.createSSLDivisionBField().friendlyHalf() - friendly_goal = tbots_cpp.Field.createSSLDivisionBField().friendlyGoal() - center_circle = tbots_cpp.Field.createSSLDivisionBField().centerCircle() - - friendly_regions = [friendly_half, friendly_goal, center_circle] - - if is_friendly_test: - # this expected_center_circle_or_validation_set version checks - # that either 0 or 1 robots are in centerCircle OR ball moves from center point - expected_center_circle_or_validation_set.append( - NumberOfRobotsAlwaysStaysInRegion( - regions=[tbots_cpp.Field.createSSLDivisionBField().centerCircle()], - req_robot_cnt=1, - ) - ) - else: - # Checks that 0 robots are in centerCircle OR ball moves from center point - friendly_regions.remove(center_circle) - - # Checks that there are 6 friendly robots in friendly_regions - # friendly_regions definition depends on if/else case above - expected_robot_regions_or_validations_set = [ - ball_moves_at_rest_validation, - NumberOfRobotsAlwaysStaysInRegion( - regions=friendly_regions, - req_robot_cnt=6, - ), - ] + yellow_play = Play() + yellow_play.name = PlayName.KickoffEnemyPlay + + simulated_test_runner.gamecontroller.send_gc_command( + gc_command=Command.Type.KICKOFF, team=Team.BLUE + ) + simulated_test_runner.gamecontroller.send_gc_command( + gc_command=Command.Type.KICKOFF, team=Team.YELLOW + ) - always_validation_sequence_set[0] = [ - OrValidation(expected_center_circle_or_validation_set), - OrValidation(expected_robot_regions_or_validations_set), + blue_regions = [ + tbots_cpp.Field.createSSLDivisionBField().friendlyHalf(), + tbots_cpp.Field.createSSLDivisionBField().friendlyGoal(), + tbots_cpp.Field.createSSLDivisionBField().centerCircle(), ] - eventually_validation_sequence_set = [[]] + ball_moves_at_rest_validation = BallAlwaysMovesFromRest( + position=ball_initial_pos, threshold=0.05 + ) + + always_validations = [ + [ + OrValidation( + [ + ball_moves_at_rest_validation, + NumberOfRobotsAlwaysStaysInRegion( + regions=[ + tbots_cpp.Field.createSSLDivisionBField().centerCircle() + ], + req_robot_cnt=1, + ), + NumberOfRobotsAlwaysStaysInRegion( + regions=[ + tbots_cpp.Field.createSSLDivisionBField().centerCircle() + ], + req_robot_cnt=0, + ), + ] + ), + OrValidation( + [ + ball_moves_at_rest_validation, + NumberOfRobotsAlwaysStaysInRegion( + regions=blue_regions, req_robot_cnt=6 + ), + ] + ), + ] + ] - # Eventually Validation - if is_friendly_test: - # Checks that ball leaves center point by 0.05 meters within 10 seconds of kickoff - eventually_validation_sequence_set[0].append( - BallEventuallyExitsRegion( - regions=[tbots_cpp.Circle(ball_initial_pos, 0.05)] - ) - ) + eventually_validations = [ + [BallEventuallyExitsRegion(regions=[tbots_cpp.Circle(ball_initial_pos, 0.05)])] + ] simulated_test_runner.run_test( - inv_eventually_validation_sequence_set=eventually_validation_sequence_set, - inv_always_validation_sequence_set=always_validation_sequence_set, + inv_eventually_validation_sequence_set=eventually_validations, + inv_always_validation_sequence_set=always_validations, + ci_cmd_with_delay=[(4, Command.Type.NORMAL_START, Team.BLUE)], test_timeout_s=10, ) if __name__ == "__main__": - pytest_main(__file__) + # Run the test, -s disables all capturing at -vv increases verbosity + sys.exit(pytest.main([__file__, "-svv"])) diff --git a/src/software/ai/play_selection_fsm.cpp b/src/software/ai/play_selection_fsm.cpp index 25d97b4c05..a72d79cf7f 100644 --- a/src/software/ai/play_selection_fsm.cpp +++ b/src/software/ai/play_selection_fsm.cpp @@ -5,8 +5,8 @@ #include "software/ai/hl/stp/play/enemy_free_kick/enemy_free_kick_play.h" #include "software/ai/hl/stp/play/free_kick/free_kick_play.h" #include "software/ai/hl/stp/play/halt_play/halt_play.h" -#include "software/ai/hl/stp/play/kickoff_enemy_play.h" -#include "software/ai/hl/stp/play/kickoff_friendly_play.h" +#include "software/ai/hl/stp/play/kickoff_enemy/kickoff_enemy_play.h" +#include "software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play.h" #include "software/ai/hl/stp/play/offense/offense_play.h" #include "software/ai/hl/stp/play/penalty_kick/penalty_kick_play.h" #include "software/ai/hl/stp/play/penalty_kick_enemy/penalty_kick_enemy_play.h"