From c4d286af7a0ac61d14490a8822b2ad0416ae434d Mon Sep 17 00:00:00 2001 From: davidrackerby Date: Tue, 17 Oct 2023 20:55:59 -0400 Subject: [PATCH 1/7] Slightly modify Path Agent interface to take no supplied path but require one later --- source/Agents/PathAgent.cpp | 14 ++++++++++++++ source/Agents/PathAgent.hpp | 1 + 2 files changed, 15 insertions(+) diff --git a/source/Agents/PathAgent.cpp b/source/Agents/PathAgent.cpp index 245aaf8a..bc71314e 100644 --- a/source/Agents/PathAgent.cpp +++ b/source/Agents/PathAgent.cpp @@ -17,6 +17,14 @@ namespace walle { +/** + * Constructor (agent default) + * @param id unique agent id + * @param name name of path agent + * @note When this constructor is called, the agent must still be assigned a path before a call to Initialize + */ +PathAgent::PathAgent(size_t id, std::string const& name) : cse491::AgentBase(id, name) {} + /** * Constructor (vector) * @param id unique agent id @@ -51,6 +59,12 @@ PathAgent::PathAgent(size_t id, std::string const& name, * @return true if so; false otherwise */ bool PathAgent::Initialize() { + if (property_map.contains("path")) { + offsets_ = StrToOffsets(GetProperty>("path")); + } + else { + return false; + } return HasAction("move_arbitrary") && index_ >= 0 && static_cast(index_) < offsets_.size(); } diff --git a/source/Agents/PathAgent.hpp b/source/Agents/PathAgent.hpp index 5b6da82d..8787960a 100644 --- a/source/Agents/PathAgent.hpp +++ b/source/Agents/PathAgent.hpp @@ -32,6 +32,7 @@ class PathAgent : public cse491::AgentBase { public: PathAgent() = delete; + PathAgent(size_t id, std::string const& name); PathAgent(size_t id, std::string const& name, std::vector && offsets); PathAgent(size_t id, std::string const& name, std::string_view commands); ~PathAgent() override = default; From 77621cd25f93a782410b009fc3480b2ec5eef84f Mon Sep 17 00:00:00 2001 From: davidrackerby Date: Thu, 16 Nov 2023 16:35:42 -0500 Subject: [PATCH 2/7] Intermediate commit --- source/Agents/TrackingAgent.hpp | 53 +++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/source/Agents/TrackingAgent.hpp b/source/Agents/TrackingAgent.hpp index 05bb1a8a..e869520c 100644 --- a/source/Agents/TrackingAgent.hpp +++ b/source/Agents/TrackingAgent.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -57,6 +58,37 @@ class TrackingAgent : public cse491::AgentBase { /// How close the target needs to be to begin tracking double tracking_distance_ = 50; + class Alerter { + cse491::WorldBase* world_ptr_; + std::unordered_set agent_network_set_; + + Alerter(cse491::WorldBase* world_ptr, size_t id) : world_ptr_(world_ptr) { + assert(world_ptr != nullptr); + agent_network_set_.insert(id); + } + + void AddAgent(size_t id) { + agent_network_set_.insert(id); + } + + void RemoveAgent(size_t id) { + agent_network_set_.erase(id); + } + + void AlertAllTrackingAgents() { + for (auto id : agent_network_set_) { + auto & agent = world_ptr_->GetAgent(id); + assert(reinterpret_cast(&agent) != nullptr); + auto & tracking_agent = static_cast(agent); + // TODO + // + // + // + // TODO + } + }; + }; + public: /** * Delete default constructor @@ -68,8 +100,9 @@ class TrackingAgent : public cse491::AgentBase { * @param id unique agent id * @param name name of path agent */ - TrackingAgent(size_t id, std::string const &name) : cse491::AgentBase(id, name), - inner_(std::in_place_type, id, name) {} + TrackingAgent(size_t id, std::string const &name) : + cse491::AgentBase(id, name), + inner_(std::in_place_type, id, name) {} /** * Constructor (vector) @@ -78,14 +111,10 @@ class TrackingAgent : public cse491::AgentBase { * @param offsets collection of offsets to move the agent * @attention The sequence of offsets must not be empty */ - TrackingAgent(size_t id, std::string const &name, std::vector &&offsets) : cse491::AgentBase(id, - name), - inner_(std::in_place_type< - PathAgent>, - id, - name, - offsets), - offsets_(offsets) {} + TrackingAgent(size_t id, std::string const &name, std::vector &&offsets) : + cse491::AgentBase(id, name), + inner_(std::in_place_type, id, name, offsets), offsets_(offsets) {} + /** * Constructor (string view) * @param id unique agent id @@ -94,8 +123,8 @@ class TrackingAgent : public cse491::AgentBase { * @attention The sequence of offsets must not be empty */ TrackingAgent(size_t id, std::string const &name, std::string_view commands) : - cse491::AgentBase(id, name), - inner_(std::in_place_type, id, name, commands), offsets_(StrToOffsets(commands)) {} + cse491::AgentBase(id, name), + inner_(std::in_place_type, id, name, commands), offsets_(StrToOffsets(commands)) {} /** * Destructor From ca13684428e719188def271c7baeb7d90cbf14c3 Mon Sep 17 00:00:00 2001 From: davidrackerby Date: Wed, 22 Nov 2023 22:45:33 -0500 Subject: [PATCH 3/7] TrackingAgent alert system with basic test added --- source/Agents/TrackingAgent.hpp | 145 +++++++++++++++++++++----- tests/unit/Agents/TrackingAgent.cmake | 5 + tests/unit/Agents/TrackingAgent.cpp | 60 +++++++++++ 3 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 tests/unit/Agents/TrackingAgent.cmake create mode 100644 tests/unit/Agents/TrackingAgent.cpp diff --git a/source/Agents/TrackingAgent.hpp b/source/Agents/TrackingAgent.hpp index e869520c..fd1ae2da 100644 --- a/source/Agents/TrackingAgent.hpp +++ b/source/Agents/TrackingAgent.hpp @@ -13,6 +13,8 @@ #include "AStarAgent.hpp" #include "PathAgent.hpp" +#include +#include #include #include #include @@ -58,11 +60,21 @@ class TrackingAgent : public cse491::AgentBase { /// How close the target needs to be to begin tracking double tracking_distance_ = 50; + /** + * A single Alerter is responsible for forcefully changing the state of all trackers in its network + * to TrackingState::TRACKING when a single TrackingAgent in the set of trackers + * comes into range of its target + */ class Alerter { + private: + /// World the agents in the agent network are a part of cse491::WorldBase* world_ptr_; + + /// Set of all agents in a single network std::unordered_set agent_network_set_; - Alerter(cse491::WorldBase* world_ptr, size_t id) : world_ptr_(world_ptr) { + public: + Alerter(size_t id, cse491::WorldBase* world_ptr) : world_ptr_(world_ptr) { assert(world_ptr != nullptr); agent_network_set_.insert(id); } @@ -75,19 +87,33 @@ class TrackingAgent : public cse491::AgentBase { agent_network_set_.erase(id); } - void AlertAllTrackingAgents() { + /** + * Uses UpdateState to focus all trackers onto their target regardless of the distance away + * @param caller_id the original id of the agent that came into range of its target; does not need to be updated + */ + void AlertAllTrackingAgents(size_t caller_id) { for (auto id : agent_network_set_) { + // Does not need to update the caller who gave the alert + if (caller_id == id) { + continue; + } auto & agent = world_ptr_->GetAgent(id); - assert(reinterpret_cast(&agent) != nullptr); + assert(dynamic_cast(&agent) != nullptr); auto & tracking_agent = static_cast(agent); - // TODO - // - // - // - // TODO + + // UpdateState sets an agent's TrackingState to TRACKING if it is within some distance of the target + // if this distance is positive infinity, then the state will always be reset (given that there IS a target) + // Important: UpdateState must be called with alerting == false in order to + // avoid infinite recursion from recursively calling AlertAllTrackingAgents + double old_tracking_dist = tracking_agent.GetTrackingDistance(); + tracking_agent.SetTrackingDistance(std::numeric_limits::infinity()); + tracking_agent.UpdateState(false); + tracking_agent.SetTrackingDistance(old_tracking_dist); } - }; - }; + } + }; // Alerter + + std::shared_ptr alerter_ = nullptr; public: /** @@ -109,11 +135,18 @@ class TrackingAgent : public cse491::AgentBase { * @param id unique agent id * @param name name of path agent * @param offsets collection of offsets to move the agent + * @param alerter alerter network to add agent to * @attention The sequence of offsets must not be empty + * @attention alerter should be a nullptr if this tracker is not part of any group of tracking agents */ - TrackingAgent(size_t id, std::string const &name, std::vector &&offsets) : + TrackingAgent(size_t id, + std::string const &name, + std::vector &&offsets, + std::shared_ptr alerter = nullptr) : cse491::AgentBase(id, name), - inner_(std::in_place_type, id, name, offsets), offsets_(offsets) {} + inner_(std::in_place_type, id, name, offsets), + offsets_(offsets), + alerter_(alerter){} /** * Constructor (string view) @@ -122,9 +155,14 @@ class TrackingAgent : public cse491::AgentBase { * @param commands sequence of commands to be interpreted as offsets * @attention The sequence of offsets must not be empty */ - TrackingAgent(size_t id, std::string const &name, std::string_view commands) : + TrackingAgent(size_t id, + std::string const &name, + std::string_view commands, + std::shared_ptr alerter = nullptr) : cse491::AgentBase(id, name), - inner_(std::in_place_type, id, name, commands), offsets_(StrToOffsets(commands)) {} + inner_(std::in_place_type, id, name, commands), + offsets_(StrToOffsets(commands)), + alerter_(alerter){} /** * Destructor @@ -144,18 +182,43 @@ class TrackingAgent : public cse491::AgentBase { std::get(inner_).SetProperty("path", view); std::get(inner_).SetWorld(GetWorld()); std::get(inner_).SetPosition(GetPosition()); + + if (property_map.contains("alerter")) { + auto alerter = GetProperty>("alerter"); + AddToAlerter(alerter_); + } return std::get(inner_).Initialize(); } return false; } + void MakeAlerter() { + alerter_ = std::make_shared(id, &GetWorld()); + } + + void AddToAlerter(std::shared_ptr alerter) { + alerter_ = alerter; + alerter->AddAgent(id); + } + + void RemoveFromAlerter() { + alerter_->RemoveAgent(id); + alerter_ = nullptr; + } + + std::shared_ptr GetAlerter() { + return alerter_; + } + /** * Handles focusing the agent onto a target, returning it to its original location, and patrolling + * @param alerting determines whether this agent should alert all other TrackingAgents in its network when its + * target comes into range * @note the inner variant type will be AStarAgent when tracking OR returning to a location, but PathAgent when patrolling */ - void UpdateState() { + void UpdateState(bool alerting = true) { SetPosition(std::visit([](Agent const &agent) { return agent.GetPosition(); }, inner_)); - bool changed_internal_agent = false; +// bool changed_internal_agent = false; switch (state_) { // Tracking can transition only to Returning case TrackingState::TRACKING: { @@ -164,6 +227,11 @@ class TrackingAgent : public cse491::AgentBase { // Target is still in range of goal position if (target_ != nullptr && GetPosition().Distance(target_->GetPosition()) < tracking_distance_) { std::get(inner_).SetGoalPosition(target_->GetPosition()); + + // Alert all trackers + if (alerting && alerter_ != nullptr) { + alerter_->AlertAllTrackingAgents(id); + } } // Target moved out of range of goal position, return to start else { @@ -184,6 +252,11 @@ class TrackingAgent : public cse491::AgentBase { std::get(inner_).SetGoalPosition(target_->GetPosition()); std::get(inner_).RecalculatePath(); std::get(inner_).SetActionResult(1); + + // Alert other trackers + if (alerting && alerter_ != nullptr) { + alerter_->AlertAllTrackingAgents(id); + } } // Returned to the beginning, start patrolling again @@ -192,7 +265,9 @@ class TrackingAgent : public cse491::AgentBase { inner_.emplace(id, name); std::get(inner_).SetPosition(GetPosition()); std::get(inner_).SetPath(std::vector(offsets_)); - changed_internal_agent = true; + + // Inner world_ptr needs to be reset + SetWorld(GetWorld()); } break; } @@ -206,22 +281,21 @@ class TrackingAgent : public cse491::AgentBase { // Set internal AStarAgent's position to the outer position std::get(inner_).SetPosition(GetPosition()); std::get(inner_).SetGoalPosition(target_->GetPosition()); + + // Inner world_ptr needs to be set + SetWorld(GetWorld()); + std::get(inner_).RecalculatePath(); std::get(inner_).SetActionResult(1); - changed_internal_agent = true; + + // Alert all other trackers + if (alerting && alerter_ != nullptr) { + alerter_->AlertAllTrackingAgents(id); + } } break; } } - - if (changed_internal_agent) { - // Reset world configuration - std::visit([&in_world = GetWorld()](Agent &agent) { - std::as_const(in_world).ConfigAgent(agent); - agent.SetWorld(in_world); - }, - inner_); - } } /** @@ -295,6 +369,12 @@ class TrackingAgent : public cse491::AgentBase { return *this; } + /** + * Get the distance around this tracker that it surveys + * @return tracking distance + */ + [[nodiscard]] double GetTrackingDistance() { return tracking_distance_; } + /** * Set how close target has to be to start tracking * @param dist to start tracking at @@ -306,16 +386,25 @@ class TrackingAgent : public cse491::AgentBase { } /** - * Set both the world for our currernt agent and the agents it is a part of + * Set both the world for the current agent and the agents it is a part of * @param in_world * @return */ TrackingAgent &SetWorld(cse491::WorldBase &in_world) override { Entity::SetWorld(in_world); std::visit([&in_world](Agent &agent) { + agent.SetWorld(in_world); std::as_const(in_world).ConfigAgent(agent); }, inner_); return *this; } + + /** + * Retrieves the current internal state of the Tracking Agent + * @return + */ + TrackingState GetState() { + return state_; + } }; } // namespace walle diff --git a/tests/unit/Agents/TrackingAgent.cmake b/tests/unit/Agents/TrackingAgent.cmake new file mode 100644 index 00000000..0c780c6b --- /dev/null +++ b/tests/unit/Agents/TrackingAgent.cmake @@ -0,0 +1,5 @@ +# Filename should match the application's, just swapping .cpp with .cmake +# Example: The CMake file for my_main.cpp would be my_main.cmake in the same directory + +# Here, add one .cpp per line. Only the strings should +add_source_to_target(${EXE_NAME} "source/Agents/PathAgent.cpp") diff --git a/tests/unit/Agents/TrackingAgent.cpp b/tests/unit/Agents/TrackingAgent.cpp new file mode 100644 index 00000000..33c0910b --- /dev/null +++ b/tests/unit/Agents/TrackingAgent.cpp @@ -0,0 +1,60 @@ +/** + * @file TrackingAgent.cpp + * @author David Rackerby + */ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "Agents/TrackingAgent.hpp" +#include "Worlds/MazeWorld.hpp" + +#include + +using namespace std; +using namespace walle; +using namespace cse491; + +TEST_CASE("Tracking Agent Alert", "[Agents]") { + + // Avoid all the hurdles of encapsulation and just add the agents directly + struct MockWorld : MazeWorld { + int DoAction(cse491::AgentBase &agent, size_t action_id) override { return 0; } + agent_map_t & GetMap() { return agent_map; } + }; + + SECTION("Alerter network") { + MockWorld world; + Entity entity {0, "mock"}; + auto & map = world.GetMap(); + auto & first = map[0] = make_unique(0, "first", "x"); + auto & second = map[1] = make_unique(1, "second", "x"); + first->SetWorld(world); + second->SetWorld(world); + + auto & first_tracking = dynamic_cast(*first); + auto & second_tracking = dynamic_cast(*second); + + first_tracking.SetTarget(&entity); + second_tracking.SetTarget(&entity); + + REQUIRE(first_tracking.GetAlerter() == nullptr); + + first_tracking.MakeAlerter(); + + REQUIRE(first_tracking.GetAlerter() != nullptr); + + second_tracking.AddToAlerter(first_tracking.GetAlerter()); + + REQUIRE(first_tracking.GetState() == TrackingState::PATROLLING); + REQUIRE(second_tracking.GetState() == TrackingState::PATROLLING); + + // Call with an ID that is neither of the two above + first_tracking.GetAlerter()->AlertAllTrackingAgents(42); + + REQUIRE(first_tracking.GetState() == TrackingState::TRACKING); + REQUIRE(second_tracking.GetState() == TrackingState::TRACKING); + } +} + From 868193d47e9b32efd0fdb0f0b6164cf7e244420e Mon Sep 17 00:00:00 2001 From: davidrackerby Date: Sat, 2 Dec 2023 21:17:02 -0500 Subject: [PATCH 4/7] Finish first addition of the Alerter feature for TrackingAgent --- source/Agents/TrackingAgent.hpp | 265 +++++++++++++++++----------- source/Worlds/MazeWorld.hpp | 8 + source/group_1_main.cpp | 24 ++- tests/unit/Agents/TrackingAgent.cpp | 2 +- 4 files changed, 193 insertions(+), 106 deletions(-) diff --git a/source/Agents/TrackingAgent.hpp b/source/Agents/TrackingAgent.hpp index fd1ae2da..c562e1a7 100644 --- a/source/Agents/TrackingAgent.hpp +++ b/source/Agents/TrackingAgent.hpp @@ -24,6 +24,29 @@ namespace walle { +/** + * A single Alerter is responsible for forcefully changing the state of all trackers in its network + * to TrackingState::TRACKING when a single TrackingAgent in the set of trackers + * comes into range of its target + */ +class Alerter { + private: + /// World the agents in the agent network are a part of + cse491::WorldBase* world_ptr_; + + /// Set of all agents in a single network + std::unordered_set agent_network_set_; + + public: + // Must be constructed with an associated world + Alerter() = delete; + Alerter(cse491::WorldBase* world_ptr); + Alerter(cse491::WorldBase* world_ptr, size_t id); + void AddAgent(size_t id); + void RemoveAgent(size_t id); + void AlertAllTrackingAgents(size_t caller_id); +}; // Alerter + /** * Used to keep track of what action we are currently taking */ @@ -37,7 +60,7 @@ template concept TrackingAgentInner = std::is_same_v || std::is_same_v; /** - * Agent that switches between user-defined custom movement pattern and + * Agent that switches between user-defined custom movement pattern and * tracking a given agent */ class TrackingAgent : public cse491::AgentBase { @@ -60,59 +83,7 @@ class TrackingAgent : public cse491::AgentBase { /// How close the target needs to be to begin tracking double tracking_distance_ = 50; - /** - * A single Alerter is responsible for forcefully changing the state of all trackers in its network - * to TrackingState::TRACKING when a single TrackingAgent in the set of trackers - * comes into range of its target - */ - class Alerter { - private: - /// World the agents in the agent network are a part of - cse491::WorldBase* world_ptr_; - - /// Set of all agents in a single network - std::unordered_set agent_network_set_; - - public: - Alerter(size_t id, cse491::WorldBase* world_ptr) : world_ptr_(world_ptr) { - assert(world_ptr != nullptr); - agent_network_set_.insert(id); - } - - void AddAgent(size_t id) { - agent_network_set_.insert(id); - } - - void RemoveAgent(size_t id) { - agent_network_set_.erase(id); - } - - /** - * Uses UpdateState to focus all trackers onto their target regardless of the distance away - * @param caller_id the original id of the agent that came into range of its target; does not need to be updated - */ - void AlertAllTrackingAgents(size_t caller_id) { - for (auto id : agent_network_set_) { - // Does not need to update the caller who gave the alert - if (caller_id == id) { - continue; - } - auto & agent = world_ptr_->GetAgent(id); - assert(dynamic_cast(&agent) != nullptr); - auto & tracking_agent = static_cast(agent); - - // UpdateState sets an agent's TrackingState to TRACKING if it is within some distance of the target - // if this distance is positive infinity, then the state will always be reset (given that there IS a target) - // Important: UpdateState must be called with alerting == false in order to - // avoid infinite recursion from recursively calling AlertAllTrackingAgents - double old_tracking_dist = tracking_agent.GetTrackingDistance(); - tracking_agent.SetTrackingDistance(std::numeric_limits::infinity()); - tracking_agent.UpdateState(false); - tracking_agent.SetTrackingDistance(old_tracking_dist); - } - } - }; // Alerter - + /// Alerter that this tracking agent belongs to; Null implies that it's not associated with an alerter std::shared_ptr alerter_ = nullptr; public: @@ -126,9 +97,9 @@ class TrackingAgent : public cse491::AgentBase { * @param id unique agent id * @param name name of path agent */ - TrackingAgent(size_t id, std::string const &name) : - cse491::AgentBase(id, name), - inner_(std::in_place_type, id, name) {} + TrackingAgent(size_t id, std::string const &name) + : cse491::AgentBase(id, name), + inner_(std::in_place_type, id, name) {} /** * Constructor (vector) @@ -142,32 +113,36 @@ class TrackingAgent : public cse491::AgentBase { TrackingAgent(size_t id, std::string const &name, std::vector &&offsets, - std::shared_ptr alerter = nullptr) : - cse491::AgentBase(id, name), - inner_(std::in_place_type, id, name, offsets), - offsets_(offsets), - alerter_(alerter){} + std::shared_ptr alerter = nullptr) + : cse491::AgentBase(id, name), + inner_(std::in_place_type, id, name, offsets), + offsets_(offsets), + alerter_(alerter) {} /** * Constructor (string view) * @param id unique agent id * @param name name of path agent * @param commands sequence of commands to be interpreted as offsets + * @param alerter alerter network to add agent to * @attention The sequence of offsets must not be empty + * @attention alerter should be a nullptr if this tracker is not part of any group of tracking agents */ TrackingAgent(size_t id, std::string const &name, std::string_view commands, - std::shared_ptr alerter = nullptr) : - cse491::AgentBase(id, name), - inner_(std::in_place_type, id, name, commands), - offsets_(StrToOffsets(commands)), - alerter_(alerter){} + std::shared_ptr alerter = nullptr) + : cse491::AgentBase(id, name), + inner_(std::in_place_type, id, name, commands), + offsets_(StrToOffsets(commands)), + alerter_(alerter) {} /** * Destructor */ - ~TrackingAgent() override = default; + virtual ~TrackingAgent() override { + RemoveFromAlerter(); + } /** * Ensure that the TrackingAgent's internal PathAgent is correctly initialized @@ -184,32 +159,58 @@ class TrackingAgent : public cse491::AgentBase { std::get(inner_).SetPosition(GetPosition()); if (property_map.contains("alerter")) { - auto alerter = GetProperty>("alerter"); - AddToAlerter(alerter_); + auto alerter_property = GetProperty>("alerter"); + AddToAlerter(alerter_property); } return std::get(inner_).Initialize(); } return false; } + /// Creates an alerter network and adds this tracking agent to it void MakeAlerter() { - alerter_ = std::make_shared(id, &GetWorld()); + alerter_ = std::make_shared(&GetWorld(), id); } + /** + * Adds this tracking agent to an already-existing alerter network + * @param alerter alerter that this agent should be associated with + * @note alerter must not be null + */ void AddToAlerter(std::shared_ptr alerter) { - alerter_ = alerter; + assert(alerter != nullptr); alerter->AddAgent(id); + alerter_ = alerter; } + /** + * Removes this tracking agent from it's own tracking network + * @note called from the TrackingAgent destructor + */ void RemoveFromAlerter() { - alerter_->RemoveAgent(id); - alerter_ = nullptr; + if (alerter_ != nullptr) { + alerter_->RemoveAgent(id); + alerter_ = nullptr; + } } - std::shared_ptr GetAlerter() { + /** + * Used to expand the alerter network by adding other tracking agents to it + * @return a copy of this tracking agent's alerter + * @note it's expected that this function is used when calling AddToAlerter on a different tracking agent + * @note may be null + */ + [[nodiscard]] std::shared_ptr GetAlerter() const { return alerter_; } + /// Tells the alerter to notify all other tracking agents in network + void CallAlerter(size_t agent_id) { + if (alerter_ != nullptr) { + alerter_->AlertAllTrackingAgents(agent_id); + } + } + /** * Handles focusing the agent onto a target, returning it to its original location, and patrolling * @param alerting determines whether this agent should alert all other TrackingAgents in its network when its @@ -218,19 +219,18 @@ class TrackingAgent : public cse491::AgentBase { */ void UpdateState(bool alerting = true) { SetPosition(std::visit([](Agent const &agent) { return agent.GetPosition(); }, inner_)); -// bool changed_internal_agent = false; switch (state_) { // Tracking can transition only to Returning case TrackingState::TRACKING: { // Reached goal position if (GetPosition() == std::get(inner_).GetGoalPosition()) { - // Target is still in range of goal position if (target_ != nullptr && GetPosition().Distance(target_->GetPosition()) < tracking_distance_) { + // Target is still in range of goal position so std::get(inner_).SetGoalPosition(target_->GetPosition()); // Alert all trackers - if (alerting && alerter_ != nullptr) { - alerter_->AlertAllTrackingAgents(id); + if (alerting) { + CallAlerter(id); } } // Target moved out of range of goal position, return to start @@ -244,7 +244,7 @@ class TrackingAgent : public cse491::AgentBase { break; } - // Returning can transition to either Patrolling or Tracking + // Returning can transition to either Patrolling or Tracking case TrackingState::RETURNING_TO_START: { // Within tracking range, start tracking again if (target_ != nullptr && GetPosition().Distance(target_->GetPosition()) < tracking_distance_) { @@ -254,12 +254,12 @@ class TrackingAgent : public cse491::AgentBase { std::get(inner_).SetActionResult(1); // Alert other trackers - if (alerting && alerter_ != nullptr) { - alerter_->AlertAllTrackingAgents(id); + if (alerting) { + CallAlerter(id); } } - // Returned to the beginning, start patrolling again + // Returned to the beginning, start patrolling again else if (GetPosition() == start_pos_) { state_ = TrackingState::PATROLLING; inner_.emplace(id, name); @@ -272,7 +272,7 @@ class TrackingAgent : public cse491::AgentBase { break; } - // Patrolling can transition only to Tracking + // Patrolling can transition only to Tracking case TrackingState::PATROLLING: { // Within tracking range, needs internal object replacement if (target_ != nullptr && GetPosition().Distance(target_->GetPosition()) < tracking_distance_) { @@ -289,8 +289,8 @@ class TrackingAgent : public cse491::AgentBase { std::get(inner_).SetActionResult(1); // Alert all other trackers - if (alerting && alerter_ != nullptr) { - alerter_->AlertAllTrackingAgents(id); + if (alerting) { + CallAlerter(id); } } break; @@ -308,31 +308,34 @@ class TrackingAgent : public cse491::AgentBase { return pos; } - template - size_t SelectInnerAction(Agent &agent, cse491::WorldGrid const &grid, + /// Select action for PathAgent inner type + size_t SelectInnerAction(PathAgent &agent, + cse491::WorldGrid const &grid, cse491::type_options_t const &type, cse491::item_map_t const &item_set, cse491::agent_map_t const &agent_set) { return agent.SelectAction(grid, type, item_set, agent_set); } - template<> - size_t SelectInnerAction(AStarAgent &agent, cse491::WorldGrid const &grid, - cse491::type_options_t const &type, - cse491::item_map_t const &item_set, - cse491::agent_map_t const &agent_set) { + /// Select action for AStarAgent inner type + size_t SelectInnerAction(AStarAgent &agent, + cse491::WorldGrid const &grid, + cse491::type_options_t const &type, + cse491::item_map_t const &item_set, + cse491::agent_map_t const &agent_set) { auto next_pos = agent.GetNextPosition(); auto res = agent.SelectAction(grid, type, item_set, agent_set); agent.SetPosition(next_pos); return res; } + /// Updates the internal state of the TrackingAgent and calls the internal agent's select action method size_t SelectAction(cse491::WorldGrid const &grid, cse491::type_options_t const &type, cse491::item_map_t const &item_set, cse491::agent_map_t const &agent_set) override { UpdateState(); - return std::visit([&](Agent &agent) { + return std::visit([&](AgentInner &agent) { return SelectInnerAction(agent, grid, type, item_set, agent_set); }, inner_); @@ -343,15 +346,15 @@ class TrackingAgent : public cse491::AgentBase { * @param gp grid position of position * @return self */ - TrackingAgent &SetStartPosition(cse491::GridPosition gp) { - start_pos_ = gp; + TrackingAgent &SetStartPosition(cse491::GridPosition pos) { + start_pos_ = pos; return *this; } /** * Set where this agent "patrol area" starts - * @param x x-coor of start pos - * @param y y-coor of start pos + * @param x x-coordinate of start pos + * @param y y-coordinate of start pos * @return self */ TrackingAgent &SetStartPosition(double x, double y) { @@ -373,7 +376,7 @@ class TrackingAgent : public cse491::AgentBase { * Get the distance around this tracker that it surveys * @return tracking distance */ - [[nodiscard]] double GetTrackingDistance() { return tracking_distance_; } + [[nodiscard]] double GetTrackingDistance() const { return tracking_distance_; } /** * Set how close target has to be to start tracking @@ -406,5 +409,67 @@ class TrackingAgent : public cse491::AgentBase { TrackingState GetState() { return state_; } -}; + +}; // TrackingAgent + +/** + * Alerter constructor (only knows its world and has no agents in its network) + * @param world_ptr the world this alerter is associated with + */ +Alerter::Alerter(cse491::WorldBase *world_ptr) : world_ptr_(world_ptr) { assert(world_ptr != nullptr); } + +/** + * Alerter constructor (adds the TrackingAgent with that id to the alerter network) + * @param id id of agent to be added + * @param world_ptr the world this alerter is associated with + */ +Alerter::Alerter(cse491::WorldBase* world_ptr, size_t id) : world_ptr_(world_ptr) { + assert(world_ptr != nullptr); + AddAgent(id); +} + +/** + * Adds a TrackingAgent to the network + * @param id id of agent to be added + */ +void Alerter::AddAgent(size_t id) { + // Note: GetAgent already handles checking that the agent exists, but we must type-check + assert(dynamic_cast(&(world_ptr_->GetAgent(id))) != nullptr); + agent_network_set_.insert(id); +} + +/** + * Removes a TrackingAgent to the network + * @param id id of agent to be removed + * @note no assertions here since we may want to allow the TrackingAgent to join another network later if it's not being destructed + */ +void Alerter::RemoveAgent(size_t id) { + agent_network_set_.erase(id); +} + +/** + * Uses UpdateState to focus all trackers onto their target regardless of the distance away + * @param caller_id the original id of the agent that came into range of its target; does not need to be updated + */ +void Alerter::AlertAllTrackingAgents(size_t caller_id) { + for (auto id : agent_network_set_) { + // Do not update the caller who gave the alert + if (caller_id == id) { + continue; + } + auto & agent = world_ptr_->GetAgent(id); + assert(dynamic_cast(&agent) != nullptr); + auto & tracking_agent = static_cast(agent); + + // UpdateState sets an agent's TrackingState to TRACKING if it is within some distance of the target + // if this distance is positive infinity, then the state will always be reset (given that there IS a target) + // Important: UpdateState must be called with alerting == false in order to + // avoid infinite recursion from recursively calling AlertAllTrackingAgents + double old_tracking_dist = tracking_agent.GetTrackingDistance(); + tracking_agent.SetTrackingDistance(std::numeric_limits::infinity()); + tracking_agent.UpdateState(false); + tracking_agent.SetTrackingDistance(old_tracking_dist); + } +} + } // namespace walle diff --git a/source/Worlds/MazeWorld.hpp b/source/Worlds/MazeWorld.hpp index 6080860c..e16cd707 100644 --- a/source/Worlds/MazeWorld.hpp +++ b/source/Worlds/MazeWorld.hpp @@ -46,6 +46,14 @@ class MazeWorld : public WorldBase { agent.AddAction("move_arbitrary", MOVE_ARBITRARY); } + void ConfigAgent(AgentBase &agent) override { + agent.AddAction("up", MOVE_UP); + agent.AddAction("down", MOVE_DOWN); + agent.AddAction("left", MOVE_LEFT); + agent.AddAction("right", MOVE_RIGHT); + agent.AddAction("move_arbitrary", MOVE_ARBITRARY); + } + /// Allow the agents to move around the maze. int DoAction(AgentBase &agent, size_t action_id) override { // Determine where the agent is trying to move. diff --git a/source/group_1_main.cpp b/source/group_1_main.cpp index e208e3d8..7df8d188 100644 --- a/source/group_1_main.cpp +++ b/source/group_1_main.cpp @@ -12,13 +12,27 @@ int main() { cse491::MazeWorld world; - auto &entity = world.AddAgent("Looper").SetPosition(9, 2).SetProperty("symbol", '$'); - assert(dynamic_cast(&entity)); - auto& looper = static_cast(entity); - looper.SetProperty>("path", "e s w n"); - looper.Initialize(); auto &player = world.AddAgent("Interface").SetProperty("symbol", '@'); + + auto alerter = std::make_shared(&world); + + auto &looper_base = world.AddAgent("Looper").SetPosition(9, 2).SetProperty("symbol", '$'); + assert(dynamic_cast(&looper_base)); + auto& looper = static_cast(looper_base); + looper.SetProperty>("path", "e s w n"); + looper.SetProperty("alerter", alerter); looper.SetTarget(&player); looper.SetTrackingDistance(4); + looper.Initialize(); + + auto &corner_base = world.AddAgent("Corner-sitter").SetPosition(22, 8).SetProperty("symbol", '$'); + assert(dynamic_cast(&corner_base)); + auto& corner = static_cast(corner_base); + corner.SetProperty>("path", "x"); + corner.SetProperty("alerter", alerter); + corner.SetTarget(&player); + corner.SetTrackingDistance(4); + corner.Initialize(); + world.Run(); } diff --git a/tests/unit/Agents/TrackingAgent.cpp b/tests/unit/Agents/TrackingAgent.cpp index 33c0910b..270a6a23 100644 --- a/tests/unit/Agents/TrackingAgent.cpp +++ b/tests/unit/Agents/TrackingAgent.cpp @@ -20,7 +20,7 @@ TEST_CASE("Tracking Agent Alert", "[Agents]") { // Avoid all the hurdles of encapsulation and just add the agents directly struct MockWorld : MazeWorld { - int DoAction(cse491::AgentBase &agent, size_t action_id) override { return 0; } + int DoAction(cse491::AgentBase &, size_t) override { return 0; } agent_map_t & GetMap() { return agent_map; } }; From 4a9087d69e2423204dacebdf709a6395da2699a2 Mon Sep 17 00:00:00 2001 From: davidrackerby Date: Sun, 3 Dec 2023 13:24:14 -0500 Subject: [PATCH 5/7] Add SetPath to TrackingAgent --- source/Agents/TrackingAgent.hpp | 60 ++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/source/Agents/TrackingAgent.hpp b/source/Agents/TrackingAgent.hpp index 5357b288..d057de55 100644 --- a/source/Agents/TrackingAgent.hpp +++ b/source/Agents/TrackingAgent.hpp @@ -233,27 +233,15 @@ class TrackingAgent : public cse491::AgentBase { CallAlerter(id); } } - - // Returning can transition to either Patrolling or Tracking - case TrackingState::RETURNING_TO_START: { - // Within tracking range, start tracking again - if (target_ != nullptr && GetPosition().Distance(target_->GetPosition()) < tracking_distance_) { - state_ = TrackingState::TRACKING; - std::get(inner_).SetGoalPosition(target_->GetPosition()); - std::get(inner_).RecalculatePath(); - std::get(inner_).SetActionResult(1); - } - - // Returned to the beginning, start patrolling again - else if (GetPosition() == start_pos_) { - state_ = TrackingState::PATROLLING; - inner_.emplace(id, name); - std::get(inner_).SetPosition(GetPosition()); - std::get(inner_).SetPath(std::vector(offsets_)); - changed_internal_agent = true; - } - break; + else { + state_ = TrackingState::RETURNING_TO_START; + std::get(inner_).SetGoalPosition(start_pos_); } + std::get(inner_).RecalculatePath(); + std::get(inner_).SetActionResult(1); + } + break; + } // Returning can transition to either Patrolling or Tracking case TrackingState::RETURNING_TO_START: { @@ -392,7 +380,7 @@ class TrackingAgent : public cse491::AgentBase { /** * Set how close target has to be to start tracking * @param dist to start tracking at - * @return self + * @return calling object */ TrackingAgent &SetTrackingDistance(double dist) { tracking_distance_ = dist; @@ -401,8 +389,8 @@ class TrackingAgent : public cse491::AgentBase { /** * Set both the world for the current agent and the agents it is a part of - * @param in_world - * @return + * @param in_world new world to associate the agent with + * @return calling agent */ TrackingAgent &SetWorld(cse491::WorldBase &in_world) override { Entity::SetWorld(in_world); @@ -415,12 +403,36 @@ class TrackingAgent : public cse491::AgentBase { /** * Retrieves the current internal state of the Tracking Agent - * @return + * @return current state */ TrackingState GetState() { return state_; } + /** + * Sets the patrolling path of the TrackingAgent + * @param offsets grid position offsets creating the path + * @return + */ + TrackingAgent &SetPath(std::vector offsets) { + offsets_ = std::move(offsets); + if (offsets_.size() == 0) { + std::ostringstream what; + what << "TrackingAgent cannot have empty path. If you meant to make the agent stay still, use \"x\""; + throw std::invalid_argument(what.str()); + } + return *this; + } + + /** + * Sets the patrolling path of the TrackingAgent + * @param offsets grid position offsets creating the path + * @return + */ + TrackingAgent &SetPath(std::string_view offsets) { + return SetPath(StrToOffsets(offsets)); + } + }; // TrackingAgent /** From aeffbf600cc369318ba953d6d7983710d5a23c03 Mon Sep 17 00:00:00 2001 From: davidrackerby Date: Sun, 3 Dec 2023 19:48:48 -0500 Subject: [PATCH 6/7] Finish integrating Alerter into AgentFactory and create a demo in group_1_main --- source/Agents/AgentFactory.hpp | 297 +++++++++++++++++++---------- source/Agents/AgentLibary.hpp | 9 + source/Agents/TrackingAgent.hpp | 100 +++++----- source/Worlds/MazeWorld.hpp | 6 +- source/group_1_main.cpp | 37 ++-- tests/unit/Agents/AgentFactory.cpp | 295 +++++++++++++++++----------- 6 files changed, 451 insertions(+), 293 deletions(-) diff --git a/source/Agents/AgentFactory.hpp b/source/Agents/AgentFactory.hpp index 3f52f142..aa84ca02 100644 --- a/source/Agents/AgentFactory.hpp +++ b/source/Agents/AgentFactory.hpp @@ -1,132 +1,217 @@ -// -// Created by Matthew Kight on 9/24/23. -// +/** + * @file AgentFactory.hpp + * @author Matt Kight, David Rackerby + * + * A factory class that abstracts away the initialization of adding an agent to a world + */ #pragma once +#include "../core/AgentBase.hpp" #include "AStarAgent.hpp" #include "PacingAgent.hpp" #include "PathAgent.hpp" #include "TrackingAgent.hpp" +#include "AgentLibary.hpp" #include "../core/Entity.hpp" #include "../core/WorldBase.hpp" + namespace walle { +// Forward-declare since it's easier to understand how AddXAgent works when the structs are defined nearby +struct PacingAgentData; +struct PathAgentData; +struct AStarAgentData; +struct TrackingAgentData; + +class AgentFactory { + private: + cse491::WorldBase &world; + + public: + AgentFactory() = delete; + + /** + * Constructor for AgentFactory + * @param world we are adding agents too + */ + explicit AgentFactory(cse491::WorldBase &world) : world(world) {} + + AStarAgent &AddAStarAgent(const AStarAgentData &agent_data); + cse491::PacingAgent &AddPacingAgent(const PacingAgentData &agent_data); + TrackingAgent &AddTrackingAgent(const TrackingAgentData &agent_data); + PathAgent &AddPathAgent(const PathAgentData &agent_data); + +}; // class AgentFactory + +/** + * Stores data for AgentBase + */ struct BaseAgentData { - std::string name; - cse491::GridPosition position; - char symbol = '*'; -}; + /// Name of the agent + std::string name; + + /// Agent's position + cse491::GridPosition position; + + /// Agent's representation + char symbol = '*'; -struct AStarAgentData : public BaseAgentData { - int recalculate_after_x_turns = 5; - cse491::GridPosition target; }; +/** + * Stores data for a PacingAgent + */ struct PacingAgentData : public BaseAgentData { - bool vertical = false; + /// Whether the PacingAgent is moving up and down (vertical) or left and right(!vertical) + bool vertical = false; }; -struct TrackingAgentData : public BaseAgentData { - std::string path; - std::vector vector_path; - cse491::Entity *target; - int tracking_distance = 5; - cse491::GridPosition start_pos; -}; +/** + * Add a PacingAgent to the world + * @param agent_data data for agent we want to create + * @return self + */ +cse491::PacingAgent &AgentFactory::AddPacingAgent(const PacingAgentData &agent_data) { + auto &entity = world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( + "symbol", + agent_data.symbol); + auto &agent = DownCastAgent(entity); + agent.SetVertical(agent_data.vertical); + return agent; +} +/** + * Stores data for a PathAgent + */ struct PathAgentData : public BaseAgentData { - std::string path; - std::vector vector_path; + /// Starting index into the vector of GridPositions + int index; + + /// String representation of the path traveled (e.g. "n s e w" for north south east west) + std::string string_path; + + /// Set of grid positions that are applied to the agent's position during one step (constructed from string_path) + std::vector vector_path; }; -class AgentFactory { - private: - cse491::WorldBase &world; - public: - AgentFactory() = delete; - /** - * Constructor for AgentFactory - * @param world we are adding agents too - */ - AgentFactory(cse491::WorldBase &world) : world(world) {} - - /** - * Add Agent to the world - * @param agent_data data for agent we want to create - * @return self - */ - AStarAgent &AddAStarAgent(const AStarAgentData &agent_data) { - auto &entity = world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( - "symbol", - agent_data.symbol); - assert(dynamic_cast(&entity)); - auto &agent = static_cast(entity); - agent.SetGoalPosition(agent_data.target); - agent.SetRecalculate(agent_data.recalculate_after_x_turns); - - return agent; - } - /** - * Add Agent to the world - * @param agent_data data for agent we want to create - * @return self - */ - cse491::PacingAgent &AddPacingAgent(const PacingAgentData &agent_data) { - auto - &entity = world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( - "symbol", - agent_data.symbol); - assert(dynamic_cast(&entity)); - auto &agent = static_cast(entity); - agent.SetVertical(agent_data.vertical); - return agent; - } - /** - * Add Agent to the world - * @param agent_data data for agent we want to create - * @return self - */ - TrackingAgent &AddTrackingAgent(const TrackingAgentData &agent_data) { - auto &entity = - world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( - "symbol", - agent_data.symbol); - assert(dynamic_cast(&entity)); - auto &agent = static_cast(entity); - if (!agent_data.path.empty()) - agent.SetProperty>("path", - agent_data.path); - else - agent.SetPath(agent_data.vector_path); - agent.SetTarget(agent_data.target); - agent.SetTrackingDistance(agent_data.tracking_distance); - agent.SetStartPosition(agent_data.start_pos); - agent.Initialize(); - return agent; - } - - /** - * Add Agent to the world - * @param agent_data data for agent we want to create - * @return self - */ - PathAgent &AddPathAgent(const PathAgentData &agent_data) { - auto &entity = - world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( - "symbol", - agent_data.symbol); - assert(dynamic_cast(&entity)); - auto &agent = static_cast(entity); - if (!agent_data.path.empty()) - agent.SetProperty>("path", - agent_data.path); // TODO add another option to provide grid point - else - agent.SetPath(agent_data.vector_path); - agent.Initialize(); - return agent; - } +/** +* Add a PathAgent to the world +* @param agent_data data for agent we want to create +* @return self +*/ +PathAgent &AgentFactory::AddPathAgent(const PathAgentData &agent_data) { + auto &entity = world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( + "symbol", + agent_data.symbol); + auto &agent = DownCastAgent(entity); + if (!agent_data.string_path.empty()) { + agent.SetProperty>("path", + agent_data.string_path); // TODO add another option to provide grid point + } else { + agent.SetPath(agent_data.vector_path); + } + agent.Initialize(); + return agent; +} + +/** + * Stores data for an AStarAgent + */ +struct AStarAgentData : public BaseAgentData { + /// Number of steps after which the shortest path is recalculated + int recalculate_after_x_turns = 5; + + /// The final position in the world that the AStarAgent is travelling to + cse491::GridPosition goal_pos; }; + +/** + * Add an AStarAgent to the world + * @param agent_data data for agent we want to create + * @return self + */ +AStarAgent &AgentFactory::AddAStarAgent(const AStarAgentData &agent_data) { + auto &entity = world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( + "symbol", + agent_data.symbol); + auto &agent = DownCastAgent(entity); + agent.SetGoalPosition(agent_data.goal_pos); + agent.SetRecalculate(agent_data.recalculate_after_x_turns); + return agent; +} + +/** + * Stores data for a TrackingAgent + */ +struct TrackingAgentData : public BaseAgentData { + /// Set of grid positions that are applied to the agent's position during one step (constructed from string_path) like in PathAgent + std::vector vector_path; + + /// String representation of the path traveled (e.g. "n s e w" for north south east west) like in PathAgent + std::string string_path; + + /// Goal Entity being tracked (must not be null or else the agent simply behaves like a PathAgent) + cse491::Entity *target; + + /// Distance that the TrackingAgent can "see" such that when the target enters that range, it begins tracking + int tracking_distance = 5; + + /// Where the TrackingAgent begins from patrolling from and returns two after the target moves out of range + cse491::GridPosition start_pos; + + /// Shared reference to an Alerter, which is non-null if the agent should be able to tell other agents to immediately + /// focus on their targets + /// @remark You should be using the **same** shared pointer across multiple instances of TrackingAgentData in order + /// to make the TrackingAgents part of the same network. This means you need to copy around this shared pointer + /// when using the factory + std::shared_ptr alerter = nullptr; + + /// Use initial values + TrackingAgentData() = default; + + /// Set all values + TrackingAgentData(std::string name, + cse491::GridPosition curr_pos, + char symbol, + std::string path, + cse491::Entity * target, + int tracking_dist, + cse491::GridPosition start_pos, + std::shared_ptr alerter) + : BaseAgentData({std::move(name), curr_pos, symbol}), + vector_path(StrToOffsets(path)), + string_path(std::move(path)), + target(target), + tracking_distance(tracking_dist), + start_pos(start_pos), + alerter(alerter) {} +}; + +/** +* Add a TrackingAgent to the world +* @param agent_data data for agent we want to create +* @return self +*/ +TrackingAgent &AgentFactory::AddTrackingAgent(const TrackingAgentData &agent_data) { + auto &entity = world.AddAgent(agent_data.name).SetPosition(agent_data.position).SetProperty( + "symbol", + agent_data.symbol); + auto &agent = DownCastAgent(entity); + if (!agent_data.string_path.empty()) { + agent.SetProperty>("path", agent_data.string_path); + } else { + agent.SetPath(agent_data.vector_path); + } + agent.SetTarget(agent_data.target); + agent.SetTrackingDistance(agent_data.tracking_distance); + agent.SetStartPosition(agent_data.start_pos); + if (agent_data.alerter != nullptr) { + agent.SetProperty("alerter", agent_data.alerter); + } + agent.Initialize(); + return agent; } +} // namespace walle diff --git a/source/Agents/AgentLibary.hpp b/source/Agents/AgentLibary.hpp index 1b36689c..062ed195 100644 --- a/source/Agents/AgentLibary.hpp +++ b/source/Agents/AgentLibary.hpp @@ -252,4 +252,13 @@ StrToOffsets(std::string_view commands) { return positions; } +template +concept Agent_Type = std::is_base_of_v; + +template +T &DownCastAgent(cse491::Entity &entity) requires(Agent_Type) { + assert(dynamic_cast(&entity)!=nullptr); + return static_cast(entity); +} + } // namespace walle diff --git a/source/Agents/TrackingAgent.hpp b/source/Agents/TrackingAgent.hpp index 9c5db312..45a799ed 100644 --- a/source/Agents/TrackingAgent.hpp +++ b/source/Agents/TrackingAgent.hpp @@ -2,7 +2,7 @@ * @file TrackingAgent.hpp * @author Matt Kight, David Rackerby * - * Agent that switches between user-defined custom movement pattern and + * Agent that switches between user-defined custom movement pattern and * tracking a given agent */ @@ -12,6 +12,7 @@ #include "../core/GridPosition.hpp" #include "AStarAgent.hpp" #include "PathAgent.hpp" +#include "AgentLibary.hpp" #include #include @@ -27,12 +28,12 @@ namespace walle { /** * A single Alerter is responsible for forcefully changing the state of all trackers in its network * to TrackingState::TRACKING when a single TrackingAgent in the set of trackers - * comes into range of its target + * comes into range of its goal_pos */ class Alerter { private: /// World the agents in the agent network are a part of - cse491::WorldBase* world_ptr_; + cse491::WorldBase *world_ptr_; /// Set of all agents in a single network std::unordered_set agent_network_set_; @@ -40,11 +41,14 @@ class Alerter { public: // Must be constructed with an associated world Alerter() = delete; - Alerter(cse491::WorldBase* world_ptr); - Alerter(cse491::WorldBase* world_ptr, size_t id); + explicit Alerter(cse491::WorldBase *world_ptr); + Alerter(cse491::WorldBase *world_ptr, size_t id); void AddAgent(size_t id); void RemoveAgent(size_t id); void AlertAllTrackingAgents(size_t caller_id); + + /// Returns an immutable reference to the Alerter's set of agents it knows about + std::unordered_set const& GetNetworkSet() const { return agent_network_set_; } }; // Alerter /** @@ -57,7 +61,7 @@ enum class TrackingState { RETURNING_TO_START, TRACKING, PATROLLING }; * @tparam T either PathAgent or AStarAgent */ template -concept TrackingAgentInner = std::is_same_v || std::is_same_v; +concept TrackingAgentInner_Type = std::is_same_v || std::is_same_v; /** * Agent that switches between user-defined custom movement pattern and @@ -80,7 +84,7 @@ class TrackingAgent : public cse491::AgentBase { /// Entity that the agent tracks and moves towards Entity *target_ = nullptr; - /// How close the target needs to be to begin tracking + /// How close the goal_pos needs to be to begin tracking double tracking_distance_ = 50; /// Alerter that this tracking agent belongs to; Null implies that it's not associated with an alerter @@ -113,7 +117,7 @@ class TrackingAgent : public cse491::AgentBase { TrackingAgent(size_t id, std::string const &name, std::vector &&offsets, - std::shared_ptr alerter = nullptr) + std::shared_ptr &&alerter = nullptr) : cse491::AgentBase(id, name), inner_(std::in_place_type, id, name, offsets), offsets_(offsets), @@ -131,7 +135,7 @@ class TrackingAgent : public cse491::AgentBase { TrackingAgent(size_t id, std::string const &name, std::string_view commands, - std::shared_ptr alerter = nullptr) + std::shared_ptr &&alerter = nullptr) : cse491::AgentBase(id, name), inner_(std::in_place_type, id, name, commands), offsets_(StrToOffsets(commands)), @@ -140,7 +144,7 @@ class TrackingAgent : public cse491::AgentBase { /** * Destructor */ - virtual ~TrackingAgent() override { + ~TrackingAgent() override { RemoveFromAlerter(); } @@ -212,13 +216,13 @@ class TrackingAgent : public cse491::AgentBase { } /** - * Handles focusing the agent onto a target, returning it to its original location, and patrolling + * Handles focusing the agent onto a goal_pos, returning it to its original location, and patrolling * @param alerting determines whether this agent should alert all other TrackingAgents in its network when its - * target comes into range + * goal_pos comes into range * @note the inner variant type will be AStarAgent when tracking OR returning to a location, but PathAgent when patrolling */ void UpdateState(bool alerting = true) { - SetPosition(std::visit([](Agent const &agent) { return agent.GetPosition(); }, inner_)); + SetPosition(std::visit([](TrackingAgentInner_Type auto const &agent) { return agent.GetPosition(); }, inner_)); switch (state_) { // Tracking can transition only to Returning case TrackingState::TRACKING: { @@ -232,8 +236,7 @@ class TrackingAgent : public cse491::AgentBase { if (alerting) { CallAlerter(id); } - } - else { + } else { state_ = TrackingState::RETURNING_TO_START; std::get(inner_).SetGoalPosition(start_pos_); } @@ -243,7 +246,7 @@ class TrackingAgent : public cse491::AgentBase { break; } - // Returning can transition to either Patrolling or Tracking + // Returning can transition to either Patrolling or Tracking case TrackingState::RETURNING_TO_START: { // Within tracking range, start tracking again if (target_ != nullptr && GetPosition().Distance(target_->GetPosition()) < tracking_distance_) { @@ -258,7 +261,7 @@ class TrackingAgent : public cse491::AgentBase { } } - // Returned to the beginning, start patrolling again + // Returned to the beginning, start patrolling again else if (GetPosition() == start_pos_) { state_ = TrackingState::PATROLLING; inner_.emplace(id, name); @@ -271,7 +274,7 @@ class TrackingAgent : public cse491::AgentBase { break; } - // Patrolling can transition only to Tracking + // Patrolling can transition only to Tracking case TrackingState::PATROLLING: { // Within tracking range, needs internal object replacement if (target_ != nullptr && GetPosition().Distance(target_->GetPosition()) < tracking_distance_) { @@ -302,9 +305,9 @@ class TrackingAgent : public cse491::AgentBase { * @return inner PathAgent's next position */ [[nodiscard]] cse491::GridPosition GetNextPosition() override { - auto pos = std::get(inner_).GetNextPosition(); - std::get(inner_).SetPosition(pos); - return pos; + auto pos = std::get(inner_).GetNextPosition(); + std::get(inner_).SetPosition(pos); + return pos; } /// Select action for PathAgent inner type @@ -334,10 +337,10 @@ class TrackingAgent : public cse491::AgentBase { cse491::item_map_t const &item_set, cse491::agent_map_t const &agent_set) override { UpdateState(); - return std::visit([&](AgentInner &agent) { - return SelectInnerAction(agent, grid, type, item_set, agent_set); - }, - inner_); + return std::visit([&](TrackingAgentInner_Type auto &agent) { + return SelectInnerAction(agent, grid, type, item_set, agent_set); + }, + inner_); } /** @@ -357,8 +360,8 @@ class TrackingAgent : public cse491::AgentBase { * @return self */ TrackingAgent &SetStartPosition(double x, double y) { - start_pos_ = cse491::GridPosition(x, y); - return *this; + start_pos_ = cse491::GridPosition(x, y); + return *this; } /** @@ -367,8 +370,8 @@ class TrackingAgent : public cse491::AgentBase { * @return self */ TrackingAgent &SetTarget(Entity *agent) { - target_ = agent; - return *this; + target_ = agent; + return *this; } /** @@ -378,13 +381,13 @@ class TrackingAgent : public cse491::AgentBase { [[nodiscard]] double GetTrackingDistance() const { return tracking_distance_; } /** - * Set how close target has to be to start tracking + * Set how close goal_pos has to be to start tracking * @param dist to start tracking at * @return calling object */ TrackingAgent &SetTrackingDistance(double dist) { - tracking_distance_ = dist; - return *this; + tracking_distance_ = dist; + return *this; } /** @@ -394,7 +397,7 @@ class TrackingAgent : public cse491::AgentBase { */ TrackingAgent &SetWorld(cse491::WorldBase &in_world) override { Entity::SetWorld(in_world); - std::visit([&in_world](Agent &agent) { + std::visit([&in_world](TrackingAgentInner_Type auto &agent) { agent.SetWorld(in_world); std::as_const(in_world).ConfigAgent(agent); }, inner_); @@ -416,7 +419,7 @@ class TrackingAgent : public cse491::AgentBase { */ TrackingAgent &SetPath(std::vector offsets) { offsets_ = std::move(offsets); - if (offsets_.size() == 0) { + if (offsets_.empty()) { std::ostringstream what; what << "TrackingAgent cannot have empty path. If you meant to make the agent stay still, use \"x\""; throw std::invalid_argument(what.str()); @@ -433,6 +436,18 @@ class TrackingAgent : public cse491::AgentBase { return SetPath(StrToOffsets(offsets)); } + /** + * Returns an immutable reference to this agent's current path + * @return sequence of offsets + */ + std::vector const &GetPath() const { return offsets_; } + + /** + * Returns an immutable pointer to this agent's target + * @return ptr to entity + */ + cse491::Entity const* GetTarget() const { return target_; } + }; // TrackingAgent /** @@ -446,7 +461,7 @@ Alerter::Alerter(cse491::WorldBase *world_ptr) : world_ptr_(world_ptr) { assert( * @param id id of agent to be added * @param world_ptr the world this alerter is associated with */ -Alerter::Alerter(cse491::WorldBase* world_ptr, size_t id) : world_ptr_(world_ptr) { +Alerter::Alerter(cse491::WorldBase *world_ptr, size_t id) : world_ptr_(world_ptr) { assert(world_ptr != nullptr); AddAgent(id); } @@ -457,7 +472,7 @@ Alerter::Alerter(cse491::WorldBase* world_ptr, size_t id) : world_ptr_(world_ptr */ void Alerter::AddAgent(size_t id) { // Note: GetAgent already handles checking that the agent exists, but we must type-check - assert(dynamic_cast(&(world_ptr_->GetAgent(id))) != nullptr); + assert(dynamic_cast(&(world_ptr_->GetAgent(id))) != nullptr); agent_network_set_.insert(id); } @@ -471,8 +486,8 @@ void Alerter::RemoveAgent(size_t id) { } /** - * Uses UpdateState to focus all trackers onto their target regardless of the distance away - * @param caller_id the original id of the agent that came into range of its target; does not need to be updated + * Uses UpdateState to focus all trackers onto their goal_pos regardless of the distance away + * @param caller_id the original id of the agent that came into range of its goal_pos; does not need to be updated */ void Alerter::AlertAllTrackingAgents(size_t caller_id) { for (auto id : agent_network_set_) { @@ -480,12 +495,9 @@ void Alerter::AlertAllTrackingAgents(size_t caller_id) { if (caller_id == id) { continue; } - auto & agent = world_ptr_->GetAgent(id); - assert(dynamic_cast(&agent) != nullptr); - auto & tracking_agent = static_cast(agent); - - // UpdateState sets an agent's TrackingState to TRACKING if it is within some distance of the target - // if this distance is positive infinity, then the state will always be reset (given that there IS a target) + auto &tracking_agent = DownCastAgent(world_ptr_->GetAgent(id)); + // UpdateState sets an agent's TrackingState to TRACKING if it is within some distance of the goal_pos + // if this distance is positive infinity, then the state will always be reset (given that there IS a goal_pos) // Important: UpdateState must be called with alerting == false in order to // avoid infinite recursion from recursively calling AlertAllTrackingAgents double old_tracking_dist = tracking_agent.GetTrackingDistance(); diff --git a/source/Worlds/MazeWorld.hpp b/source/Worlds/MazeWorld.hpp index 3d0783af..87e1bd9a 100644 --- a/source/Worlds/MazeWorld.hpp +++ b/source/Worlds/MazeWorld.hpp @@ -77,14 +77,16 @@ class MazeWorld : public WorldBase { if (!main_grid.IsValid(new_position)) { return false; } if (!IsTraversable(agent, new_position)) { return false; } - // Set the agent to its new postion. + // Set the agent to its new position. agent.SetPosition(new_position); return true; } [[nodiscard]] bool IsTraversable(const AgentBase & /*agent*/, cse491::GridPosition pos) const override { - return !GetCellTypes().at(main_grid.At(pos)).HasProperty(CellType::CELL_WALL); + //return !GetCellTypes().at(main_grid.At(pos)).HasProperty(CellType::CELL_WALL); + // ^ This doesn't work because we're not assigning any properties to the cell types, so a band-aid solution is to use name + return !(GetCellTypes().at(main_grid.At(pos)).name == CellType::CELL_WALL); } }; diff --git a/source/group_1_main.cpp b/source/group_1_main.cpp index c57bb846..eb51f063 100644 --- a/source/group_1_main.cpp +++ b/source/group_1_main.cpp @@ -5,39 +5,24 @@ **/ // Include the modules that we will be using. -#include "Agents/AStarAgent.hpp" -#include "Agents/RandomAgent.hpp" -#include "Agents/TrackingAgent.hpp" - #include "Agents/AgentFactory.hpp" -#include "Agents/PacingAgent.hpp" - #include "Interfaces/TrashInterface.hpp" #include "Worlds/MazeWorld.hpp" -int main() { - cse491::MazeWorld world; - auto &player = world.AddAgent("Interface").SetProperty("symbol", '@'); +void InitializeWorld(cse491::MazeWorld & world, cse491::Entity & player) { + walle::AgentFactory factory(world); auto alerter = std::make_shared(&world); + walle::TrackingAgentData data_first("Looper", {9, 2}, '$', "e s w n", &player, 4, {9, 2}, alerter); + factory.AddTrackingAgent(data_first); - auto &looper_base = world.AddAgent("Looper").SetPosition(9, 2).SetProperty("symbol", '$'); - assert(dynamic_cast(&looper_base)); - auto& looper = static_cast(looper_base); - looper.SetProperty>("path", "e s w n"); - looper.SetProperty("alerter", alerter); - looper.SetTarget(&player); - looper.SetTrackingDistance(4); - looper.Initialize(); - - auto &corner_base = world.AddAgent("Corner-sitter").SetPosition(22, 8).SetProperty("symbol", '$'); - assert(dynamic_cast(&corner_base)); - auto& corner = static_cast(corner_base); - corner.SetProperty>("path", "x"); - corner.SetProperty("alerter", alerter); - corner.SetTarget(&player); - corner.SetTrackingDistance(4); - corner.Initialize(); + walle::TrackingAgentData data_second("Corner-sitter", {22, 8}, '$', "x", &player, 4, {22, 8}, alerter); + factory.AddTrackingAgent(data_second); +} +int main() { + cse491::MazeWorld world; + auto &player = world.AddAgent("Interface").SetProperty("symbol", '@'); + InitializeWorld(world, player); world.Run(); } diff --git a/tests/unit/Agents/AgentFactory.cpp b/tests/unit/Agents/AgentFactory.cpp index 4d945038..7c2e0a89 100644 --- a/tests/unit/Agents/AgentFactory.cpp +++ b/tests/unit/Agents/AgentFactory.cpp @@ -19,131 +19,196 @@ using namespace walle; /// Mock world to create factory class MockWorld : public cse491::WorldBase { - int DoAction(cse491::AgentBase &, size_t) override { return 0; } + int DoAction(cse491::AgentBase &, size_t) override { return 0; } }; -TEST_CASE("Adding a AStarAgent", "[Agents]"){ +TEST_CASE("Adding a AStarAgent", "[Agents]") { + + // create a fake world and factory + MockWorld world; + AgentFactory factory(world); + + // add properties/data to the factory + AStarAgentData agentData; + agentData.name = "AStarAgent"; + agentData.position = {0, 0}; + agentData.symbol = '*'; + agentData.goal_pos = {5, 5}; + agentData.recalculate_after_x_turns = 3; + + // factory creates correct agent + auto &agent = factory.AddAStarAgent(agentData); + + SECTION("Properties") { + // test properties of created agent + REQUIRE(agent.GetName() == agentData.name); + REQUIRE(agent.GetPosition() == agentData.position); + REQUIRE(agent.GetProperty("symbol") == agentData.symbol); + REQUIRE(agent.GetGoalPosition() == agentData.goal_pos); + REQUIRE(agent.GetRecalculateValue() == agentData.recalculate_after_x_turns); + } + + SECTION("Type") { + // test type of created agent + REQUIRE(dynamic_cast(&agent) != nullptr); + } +} + +TEST_CASE("Adding a PacingAgent", "[Agents]") { - // create a fake world and factory - MockWorld world; - AgentFactory factory(world); + // create a fake world and factory + MockWorld world; + AgentFactory factory(world); - // add properties/data to the factory - AStarAgentData agentData; - agentData.name = "AStarAgent"; - agentData.position = {0, 0}; - agentData.symbol = '*'; - agentData.target = {5, 5}; - agentData.recalculate_after_x_turns = 3; + // add properties/data to the factory + PacingAgentData agentData; + agentData.name = "PacingAgent"; + agentData.position = {0, 0}; + agentData.symbol = '*'; + agentData.vertical = true; + auto &agent = factory.AddPacingAgent(agentData); + + SECTION("Properties") { // factory creates correct agent - auto& agent = factory.AddAStarAgent(agentData); - - SECTION("Properties") { - // test properties of created agent - REQUIRE(agent.GetName() == agentData.name); - REQUIRE(agent.GetPosition() == agentData.position); - REQUIRE(agent.GetProperty("symbol") == agentData.symbol); - REQUIRE(agent.GetGoalPosition() == agentData.target); - REQUIRE(agent.GetRecalculateValue() == agentData.recalculate_after_x_turns); - } - - SECTION("Type") { - // test type of created agent - REQUIRE(dynamic_cast(&agent) != nullptr); - } + REQUIRE(agent.GetName() == agentData.name); + REQUIRE(agent.GetPosition() == agentData.position); + REQUIRE(agent.GetProperty("symbol") == agentData.symbol); + REQUIRE(agent.GetVertical() == agentData.vertical); + } + + SECTION("Type") { + // test type of created agent + REQUIRE(dynamic_cast(&agent) != nullptr); + } } -TEST_CASE("Adding a PacingAgent", "[Agents]"){ - - // create a fake world and factory - MockWorld world; - AgentFactory factory(world); - - // add properties/data to the factory - PacingAgentData agentData; - agentData.name = "PacingAgent"; - agentData.position = {0, 0}; - agentData.symbol = '*'; - agentData.vertical = true; - - auto& agent = factory.AddPacingAgent(agentData); - - SECTION("Properties") { - // factory creates correct agent - REQUIRE(agent.GetName() == agentData.name); - REQUIRE(agent.GetPosition() == agentData.position); - REQUIRE(agent.GetProperty("symbol") == agentData.symbol); - REQUIRE(agent.GetVertical() == agentData.vertical); - } - - SECTION("Type") { - // test type of created agent - REQUIRE(dynamic_cast(&agent) != nullptr); - } +TEST_CASE("Adding a TrackingAgent (no alerter)", "[Agents]") { + + // create a fake world and factory + MockWorld world; + cse491::Entity *targ = nullptr; + AgentFactory factory(world); + + // add properties/data to the factory + TrackingAgentData agentData; + agentData.name = "TrackingAgent"; + agentData.position = {0, 0}; + agentData.symbol = '*'; + agentData.string_path = "x"; + agentData.vector_path = StrToOffsets(agentData.string_path); + agentData.target = targ; + agentData.tracking_distance = 10; + agentData.start_pos = {1, 1}; + + auto &agent = factory.AddTrackingAgent(agentData); + + SECTION("Properties") { + // factory creates correct agent + REQUIRE(agent.GetName() == agentData.name); + REQUIRE(agent.GetPosition() == agentData.position); + REQUIRE(agent.GetProperty("symbol") == agentData.symbol); + REQUIRE(agent.GetTarget() == nullptr); + REQUIRE(agent.GetAlerter() == nullptr); + } + + SECTION("Type") { + // test type of created agent + REQUIRE(dynamic_cast(&agent) != nullptr); + } } -TEST_CASE("Adding a TrackingAgent", "[Agents]"){ - - // create a fake world and factory - MockWorld world; - cse491::Entity* targ = nullptr; - AgentFactory factory(world); - - // add properties/data to the factory - TrackingAgentData agentData; - agentData.name = "TrackingAgent"; - agentData.position = {0, 0}; - agentData.symbol = '*'; - agentData.path = ""; - agentData.vector_path = std::vector(); - agentData.target = targ; - agentData.tracking_distance = 10; - agentData.start_pos = {1,1}; - - auto& agent = factory.AddTrackingAgent(agentData); - - SECTION("Properties") { - // factory creates correct agent - REQUIRE(agent.GetName() == agentData.name); - REQUIRE(agent.GetPosition() == agentData.position); - REQUIRE(agent.GetProperty("symbol") == agentData.symbol); - } - - SECTION("Type") { - // test type of created agent - REQUIRE(dynamic_cast(&agent) != nullptr); - } +TEST_CASE("Adding a TrackingAgent (alerter)", "[Agents]") { + + // create a fake world and factory + MockWorld world; + cse491::AgentBase a{0, "dummy"}; + cse491::Entity *targ = &a; + AgentFactory factory(world); + + // add properties/data to the factory + TrackingAgentData agentData; + agentData.name = "First"; + agentData.position = {0, 0}; + agentData.symbol = '*'; + agentData.string_path = "x"; + agentData.vector_path = StrToOffsets(agentData.string_path); + agentData.target = targ; + agentData.tracking_distance = 10; + agentData.start_pos = {1, 1}; + agentData.alerter = std::make_shared(&world); + + auto &agent_first = factory.AddTrackingAgent(agentData); + + SECTION("Properties") { + // factory creates correct agent + REQUIRE(agent_first.GetName() == agentData.name); + REQUIRE(agent_first.GetPosition() == agentData.position); + REQUIRE(agent_first.GetProperty("symbol") == agentData.symbol); + REQUIRE(agent_first.GetPath() == std::vector(1, {0, 0})); + REQUIRE(agent_first.GetTarget() == &a); + REQUIRE(agent_first.GetTarget() != nullptr); + } + + agentData.name = "Second"; + auto &agent_second = factory.AddTrackingAgent(agentData); + + SECTION("Properties [other]") { + REQUIRE(agent_second.GetTarget() == &a); + REQUIRE(agent_second.GetTarget() == agent_first.GetTarget()); + REQUIRE(agent_second.GetAlerter() == agent_first.GetAlerter()); + } + + auto alerter = agent_first.GetAlerter(); + SECTION("Alerter properties") { + auto const& set = alerter->GetNetworkSet(); + REQUIRE(set.contains(agent_first.GetID())); + REQUIRE(set.contains(agent_second.GetID())); + } + + SECTION("Alerter properties [removal]") { + auto const& set = alerter->GetNetworkSet(); + + agent_first.RemoveFromAlerter(); + REQUIRE(!set.contains(agent_first.GetID())); + REQUIRE(agent_first.GetAlerter() == nullptr); + + agent_second.RemoveFromAlerter(); + REQUIRE(!set.contains(agent_second.GetID())); + REQUIRE(agent_second.GetAlerter() == nullptr); + + REQUIRE(set.empty()); + } } -TEST_CASE("Adding a PathAgent", "[Agents]"){ - - // create a fake world and factory - MockWorld world; - AgentFactory factory(world); - - // add properties/data to the factory - PathAgentData agentData; - agentData.name = "PathAgent"; - agentData.position = {0, 0}; - agentData.symbol = '*'; - // Path must not be empty - agentData.path = "x"; - // Convert to vector - agentData.vector_path = StrToOffsets(agentData.path); - - auto& agent = factory.AddPathAgent(agentData); - - SECTION("Properties") { - // factory creates correct agent - REQUIRE(agent.GetName() == agentData.name); - REQUIRE(agent.GetPosition() == agentData.position); - REQUIRE(agent.GetProperty("symbol") == agentData.symbol); - REQUIRE(agent.GetPath() == agentData.vector_path); - } - - SECTION("Type") { - // test type of created agent - REQUIRE(dynamic_cast(&agent) != nullptr); - } +TEST_CASE("Adding a PathAgent", "[Agents]") { + + // create a fake world and factory + MockWorld world; + AgentFactory factory(world); + + // add properties/data to the factory + PathAgentData agentData; + agentData.name = "PathAgent"; + agentData.position = {0, 0}; + agentData.symbol = '*'; + // Path must not be empty + agentData.string_path = "x"; + // Convert to vector + agentData.vector_path = StrToOffsets(agentData.string_path); + + auto &agent = factory.AddPathAgent(agentData); + + SECTION("Properties") { + // factory creates correct agent + REQUIRE(agent.GetName() == agentData.name); + REQUIRE(agent.GetPosition() == agentData.position); + REQUIRE(agent.GetProperty("symbol") == agentData.symbol); + REQUIRE(agent.GetPath() == agentData.vector_path); + } + + SECTION("Type") { + // test type of created agent + REQUIRE(dynamic_cast(&agent) != nullptr); + } } \ No newline at end of file From 38a5eb8d60ee75b56c5afe6300e1ed97b11402c2 Mon Sep 17 00:00:00 2001 From: davidrackerby Date: Sun, 3 Dec 2023 20:02:15 -0500 Subject: [PATCH 7/7] Name attribution for AgentFactory test --- tests/unit/Agents/AgentFactory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Agents/AgentFactory.cpp b/tests/unit/Agents/AgentFactory.cpp index 7c2e0a89..ef10902e 100644 --- a/tests/unit/Agents/AgentFactory.cpp +++ b/tests/unit/Agents/AgentFactory.cpp @@ -1,6 +1,6 @@ /** * @file AgentFactory.cpp - * @author Yousif Murrani + * @author Yousif Murrani, David Rackerby */ // Catch2