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"