From 0cdc8fa1b51d0991fc2aa0de75aa82012e975356 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Wed, 24 Jul 2024 14:24:46 +0200 Subject: [PATCH 01/21] Generalize mazeAdapter to be reused in clustering algorithm --- demo/include/utils/astar.hpp | 45 ++++++++++-------------------------- demo/include/utils/maze.hpp | 37 +++++++++++++++++++++++++++++ demo/src/astar.cpp | 8 +++---- demo/test/astar.cpp | 4 ++-- 4 files changed, 55 insertions(+), 39 deletions(-) diff --git a/demo/include/utils/astar.hpp b/demo/include/utils/astar.hpp index ac69d3ad..ca483044 100644 --- a/demo/include/utils/astar.hpp +++ b/demo/include/utils/astar.hpp @@ -1,40 +1,36 @@ #pragma once #include -#include #include #include #include #include -#include #include -#include #include "demo/types.hpp" #include "utils/maze.hpp" namespace utils { -using Move = demo::Move; using Path = demo::Path; using Position = demo::Position; using TileType = demo::TileType; -struct Cell { +struct AStarCell : public BaseCell { + using Move = demo::Move; + struct CompareCells { - bool operator()(const Cell& left, const Cell& right) { + bool operator()(const AStarCell& left, const AStarCell& right) { return (left.distanceFromStart + left.heuristic) > (right.distanceFromStart + right.heuristic); } }; - Cell(const Position& position, const TileType& type) : position(position), type(type) {}; + AStarCell(const Position& position, const TileType& type) : BaseCell(position, type) {}; - Position position; + bool visited{false}; int distanceFromStart{std::numeric_limits::max()}; double heuristic{std::numeric_limits::max()}; - bool visited{false}; - TileType type; std::optional moveFromPredecessor; double manhattanDistance(const Position& other) const { @@ -42,29 +38,12 @@ struct Cell { } }; -class MazeAdapter { -public: - using MazeStateConstPtr = std::shared_ptr; - - explicit MazeAdapter(Maze::ConstPtr maze) : maze_(std::move(maze)), cells_({maze_->width(), maze_->height()}) {}; - - Cell& cell(const Position& position) const { - if (!cells_[{position.x, position.y}]) { - cells_[{position.x, position.y}] = Cell(position, maze_->at(position)); - } - - return cells_[{position.x, position.y}].value(); - } - -private: - Maze::ConstPtr maze_; - mutable Grid> cells_; -}; - class AStar { public: - using HeuristicFunction = std::function; - using Set = std::priority_queue, Cell::CompareCells>; + using AStarMazeAdapter = MazeAdapter; + using Cell = AStarCell; + using HeuristicFunction = std::function; + using Set = std::priority_queue, AStarCell::CompareCells>; constexpr static int NoPathFound = std::numeric_limits::max(); @@ -95,7 +74,7 @@ class AStar { } private: - void expandCell(Set& openSet, MazeAdapter& mazeAdapter, const HeuristicFunction& heuristic) const; + void expandCell(Set& openSet, AStarMazeAdapter& mazeAdapter, const HeuristicFunction& heuristic) const; /** * @brief Create a path by traversing predecessor relationships up to a goal position. @@ -103,7 +82,7 @@ class AStar { * Will expand the path backwards until no more predecessor relationship is available. If the cell at the goal * position does not have a predecessor, the path will be empty. */ - Path pathTo(const MazeAdapter& maze, const Position& goal) const; + Path pathTo(const AStarMazeAdapter& maze, const Position& goal) const; /** * @brief Approximates the distance of a given cell to a goal while considering a shortcut via the tunnel. diff --git a/demo/include/utils/maze.hpp b/demo/include/utils/maze.hpp index 1d33e91e..82c64990 100644 --- a/demo/include/utils/maze.hpp +++ b/demo/include/utils/maze.hpp @@ -1,6 +1,8 @@ #pragma once +#include #include + #include #include "demo/types.hpp" @@ -55,4 +57,39 @@ class Maze { }; }; +struct BaseCell { + using TileType = demo::TileType; + using Position = demo::Position; + + BaseCell(const Position& position, const TileType& type) : position(position), type(type) {}; + + double manhattanDistance(const Position& other) const { + return std::abs(position.x - other.x) + std::abs(position.y - other.y); + } + + Position position; + TileType type; +}; + +template +class MazeAdapter { +public: + using MazeStateConstPtr = std::shared_ptr; + using Position = demo::Position; + + explicit MazeAdapter(Maze::ConstPtr maze) : maze_(std::move(maze)), cells_({maze_->width(), maze_->height()}) {}; + + CellType& cell(const Position& position) const { + if (!cells_[{position.x, position.y}]) { + cells_[{position.x, position.y}] = CellType(position, maze_->operator[](position)); + } + + return cells_[{position.x, position.y}].value(); + } + +private: + Maze::ConstPtr maze_; + mutable Grid> cells_; +}; + } // namespace utils diff --git a/demo/src/astar.cpp b/demo/src/astar.cpp index a4e1d733..f27e4dae 100644 --- a/demo/src/astar.cpp +++ b/demo/src/astar.cpp @@ -36,7 +36,7 @@ Path AStar::shortestPath(const Position& start, const Position& goal) const { Position wrappedStart = positionConsideringTunnel(start); Position wrappedGoal = positionConsideringTunnel(goal); - MazeAdapter mazeAdapter(maze_); + AStarMazeAdapter mazeAdapter(maze_); Set openSet; Cell& startCell = mazeAdapter.cell(wrappedStart); @@ -68,7 +68,7 @@ std::optional AStar::pathToClosestDot(const Position& start) const { // tunnel. Position wrappedStart = positionConsideringTunnel(start); - MazeAdapter mazeAdapter(maze_); + AStarMazeAdapter mazeAdapter(maze_); Set openSet; Cell& startCell = mazeAdapter.cell(wrappedStart); @@ -93,7 +93,7 @@ std::optional AStar::pathToClosestDot(const Position& start) const { return {}; } -void AStar::expandCell(Set& openSet, MazeAdapter& mazeAdapter, const HeuristicFunction& heuristic) const { +void AStar::expandCell(Set& openSet, AStarMazeAdapter& mazeAdapter, const HeuristicFunction& heuristic) const { Cell current = openSet.top(); openSet.pop(); @@ -123,7 +123,7 @@ void AStar::expandCell(Set& openSet, MazeAdapter& mazeAdapter, const HeuristicFu } } -Path AStar::pathTo(const MazeAdapter& maze, const Position& goal) const { +Path AStar::pathTo(const AStarMazeAdapter& maze, const Position& goal) const { Path path; Cell current = maze.cell(goal); while (current.moveFromPredecessor) { diff --git a/demo/test/astar.cpp b/demo/test/astar.cpp index f7d3ecca..b209feaa 100644 --- a/demo/test/astar.cpp +++ b/demo/test/astar.cpp @@ -6,7 +6,7 @@ #include "mock_environment_model.hpp" -namespace utils { +namespace utils::a_star { using namespace demo; @@ -145,4 +145,4 @@ TEST_F(AStarTest, pathToClosestDot) { ASSERT_EQ(path->size(), 3); } -} // namespace utils +} // namespace utils::a_star From 1fb734862a426179dfa84845c28e327cda654755 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 08:52:25 +0200 Subject: [PATCH 02/21] Move positionConsideringTunnel into maze class --- demo/include/utils/astar.hpp | 6 ------ demo/include/utils/maze.hpp | 23 +++++++++++++++++++++++ demo/src/astar.cpp | 34 +++++----------------------------- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/demo/include/utils/astar.hpp b/demo/include/utils/astar.hpp index ca483044..cd73ae26 100644 --- a/demo/include/utils/astar.hpp +++ b/demo/include/utils/astar.hpp @@ -95,12 +95,6 @@ class AStar { return std::min(cell.manhattanDistance(goal), maze_->width() - cell.manhattanDistance(goal)); } - /** - * @brief If we are about to step of the maze and the opposite end is passable as well, - * we assume they are connected by a tunnel and adjust the position accordingly. - */ - Position positionConsideringTunnel(const Position& position) const; - Maze::ConstPtr maze_; mutable util_caching::Cache, int> distanceCache_; }; diff --git a/demo/include/utils/maze.hpp b/demo/include/utils/maze.hpp index 82c64990..d3c8f05d 100644 --- a/demo/include/utils/maze.hpp +++ b/demo/include/utils/maze.hpp @@ -9,6 +9,21 @@ namespace utils { +/** + * @brief Computes the modulus of a given numerator and denominator, ensuring a non-negative result when the denominator + * is positive. + * + * This function calculates the result of the modulus operation. If the denominator is positive, the result is + * non-negative and in the range [0, denominator - 1]. + * + * @param numerator The value to be divided (dividend). + * @param denominator The value by which the numerator is divided (divisor). + * @return An integer representing the modulus of the division. + */ +inline int nonNegativeModulus(const int& numerator, const int& denominator) { + return (denominator + (numerator % denominator)) % denominator; +} + class Maze { public: using MazeState = demo::entt::MazeState; @@ -36,6 +51,14 @@ class Maze { return mazeState_.height(); } + Position positionConsideringTunnel(const Position& position) const { + Position wrappedPosition{nonNegativeModulus(position.x, width()), nonNegativeModulus(position.y, height())}; + if (isPassableCell(wrappedPosition)) { + return wrappedPosition; + } + return position; + } + bool isWall(const Position& position) const { return at(position) == TileType::WALL; } diff --git a/demo/src/astar.cpp b/demo/src/astar.cpp index f27e4dae..af19eb45 100644 --- a/demo/src/astar.cpp +++ b/demo/src/astar.cpp @@ -2,21 +2,6 @@ namespace utils { -/** - * @brief Computes the modulus of a given numerator and denominator, ensuring a non-negative result when the denominator - * is positive. - * - * This function calculates the result of the modulus operation. If the denominator is positive, the result is - * non-negative and in the range [0, denominator - 1]. - * - * @param numerator The value to be divided (dividend). - * @param denominator The value by which the numerator is divided (divisor). - * @return An integer representing the modulus of the division. - */ -int nonNegativeModulus(const int& numerator, const int& denominator) { - return (denominator + (numerator % denominator)) % denominator; -} - int AStar::mazeDistance(const Position& start, const Position& goal) const { if (distanceCache_.cached({start, goal})) { return distanceCache_.cached({start, goal}).value(); @@ -33,8 +18,8 @@ Path AStar::shortestPath(const Position& start, const Position& goal) const { // There is a "virtual" position outside of the maze that entities are on when entering the tunnel. We accept a // small error in the distance computation by neglecting this and wrapping the position to be on either end of the // tunnel. - Position wrappedStart = positionConsideringTunnel(start); - Position wrappedGoal = positionConsideringTunnel(goal); + Position wrappedStart = maze_->positionConsideringTunnel(start); + Position wrappedGoal = maze_->positionConsideringTunnel(goal); AStarMazeAdapter mazeAdapter(maze_); Set openSet; @@ -66,7 +51,7 @@ std::optional AStar::pathToClosestDot(const Position& start) const { // There is a "virtual" position outside of the maze that entities are on when entering the tunnel. We accept a // small error in the distance computation by neglecting this and wrapping the position to be on either end of the // tunnel. - Position wrappedStart = positionConsideringTunnel(start); + Position wrappedStart = maze_->positionConsideringTunnel(start); AStarMazeAdapter mazeAdapter(maze_); Set openSet; @@ -102,7 +87,7 @@ void AStar::expandCell(Set& openSet, AStarMazeAdapter& mazeAdapter, const Heuris for (const auto& move : demo::Move::possibleMoves()) { Position nextPosition = current.position + move.deltaPosition; - nextPosition = positionConsideringTunnel(nextPosition); + nextPosition = maze_->positionConsideringTunnel(nextPosition); if (!maze_->isPassableCell(nextPosition)) { continue; @@ -129,7 +114,7 @@ Path AStar::pathTo(const AStarMazeAdapter& maze, const Position& goal) const { while (current.moveFromPredecessor) { path.push_back(current.moveFromPredecessor->direction); Position predecessorPosition = current.position - current.moveFromPredecessor->deltaPosition; - predecessorPosition = positionConsideringTunnel(predecessorPosition); + predecessorPosition = maze_->positionConsideringTunnel(predecessorPosition); current = maze.cell(predecessorPosition); } @@ -137,13 +122,4 @@ Path AStar::pathTo(const AStarMazeAdapter& maze, const Position& goal) const { return path; } -Position AStar::positionConsideringTunnel(const Position& position) const { - Position wrappedPosition{nonNegativeModulus(position.x, maze_->width()), - nonNegativeModulus(position.y, maze_->height())}; - if (maze_->isPassableCell(wrappedPosition)) { - return wrappedPosition; - } - return position; -} - } // namespace utils From 64e7719f2f74496381e7bb11fd074fe2c1653e73 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 08:52:36 +0200 Subject: [PATCH 03/21] Add ClusterFinder class --- demo/CMakeLists.txt | 1 + demo/include/utils/cluster.hpp | 41 ++++++++++++++++++++++ demo/src/cluster.cpp | 62 ++++++++++++++++++++++++++++++++++ demo/test/cluster.cpp | 35 +++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 demo/include/utils/cluster.hpp create mode 100644 demo/src/cluster.cpp create mode 100644 demo/test/cluster.cpp diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 6bf12b41..2ed6c530 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(${PROJECT_NAME} STATIC src/astar.cpp src/avoid_ghost_behavior.cpp src/chase_ghost_behavior.cpp + src/cluster.cpp src/entities.cpp src/environment_model.cpp src/random_walk_behavior.cpp diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp new file mode 100644 index 00000000..a25fea6c --- /dev/null +++ b/demo/include/utils/cluster.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "demo/types.hpp" +#include "utils/maze.hpp" + +namespace utils { + +using Path = demo::Path; +using Position = demo::Position; +using TileType = demo::TileType; + +struct ClusterCell : public BaseCell { + ClusterCell(const Position& position, const TileType& type) : BaseCell(position, type) {}; + + bool visited{false}; +}; + +struct Cluster { + explicit Cluster(int clusterId) : id(clusterId) { + } + + int id; + std::vector dots; +}; + +class ClusterFinder { +public: + using ClusterMazeAdapter = MazeAdapter; + using Cell = ClusterCell; + explicit ClusterFinder(Maze::ConstPtr maze) : maze_(std::move(maze)) { + } + + std::vector findDotClusters() const; + +private: + Cluster expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter, const int& clusterID) const; + + Maze::ConstPtr maze_; +}; + +} // namespace utils diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp new file mode 100644 index 00000000..60f35b35 --- /dev/null +++ b/demo/src/cluster.cpp @@ -0,0 +1,62 @@ +#include "utils/cluster.hpp" + +#include + +namespace utils { + +std::vector ClusterFinder::findDotClusters() const { + ClusterMazeAdapter mazeAdapter(maze_); + std::vector clusters; + int clusterId = 0; + + for (int row = 0; row < maze_->height(); row++) { + for (int column = 0; column < maze_->width(); column++) { + Position start{column, row}; + ClusterCell& startCell = mazeAdapter.cell(start); + + if (startCell.type != TileType::DOT || startCell.visited) { + continue; + } + Cluster cluster = expandDot(startCell, mazeAdapter, clusterId++); + clusters.push_back(cluster); + } + } + return clusters; +} + +Cluster ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter, const int& clusterID) const { + Cluster cluster(clusterID); + + std::queue bfsQueue; + bfsQueue.push(start.position); + + while (!bfsQueue.empty()) { + Position currentPosition = bfsQueue.front(); + bfsQueue.pop(); + ClusterCell& current = mazeAdapter.cell(currentPosition); + + if (current.visited) { + continue; + } + current.visited = true; + cluster.dots.push_back(currentPosition); + + for (const auto& move : demo::Move::possibleMoves()) { + Position nextPosition = currentPosition + move.deltaPosition; + nextPosition = maze_->positionConsideringTunnel(nextPosition); + + if (!maze_->isPassableCell(nextPosition)) { + continue; + } + ClusterCell& neighbor = mazeAdapter.cell(nextPosition); + + if (neighbor.type == TileType::DOT && !neighbor.visited) { + bfsQueue.push(nextPosition); + } + } + } + + return cluster; +} + +} // namespace utils diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp new file mode 100644 index 00000000..a5eae323 --- /dev/null +++ b/demo/test/cluster.cpp @@ -0,0 +1,35 @@ +#include "utils/cluster.hpp" + +#include + +#include "mock_environment_model.hpp" + +namespace utils::a_star { + +using namespace demo; + +class ClusterTest : public ::testing::Test { +protected: + ClusterTest() : environmentModel_(std::make_shared()) { + } + + MockEnvironmentModel::Ptr environmentModel_; +}; + +TEST_F(ClusterTest, findDotClusters) { + const char str[] = {"#####" + "#...#" + " " + "#.. #" + "#####"}; + environmentModel_->setMaze({5, 5}, str); + + ClusterFinder clusterFinder(environmentModel_->maze()); + std::vector cluster = clusterFinder.findDotClusters(); + + EXPECT_EQ(cluster.size(), 2); + EXPECT_EQ(cluster.front().dots.size(), 3); + EXPECT_EQ(cluster.back().dots.size(), 2); +} + +} // namespace utils::a_star From ea750336aad4abf6148511e3688bfaae932e1f1b Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 10:08:33 +0200 Subject: [PATCH 04/21] Add and compute center for each cluster --- demo/include/utils/cluster.hpp | 13 +++++++++-- demo/src/cluster.cpp | 41 +++++++++++++++++++++++++++++----- demo/test/cluster.cpp | 17 +++++++++----- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index a25fea6c..57ce7301 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "demo/types.hpp" #include "utils/maze.hpp" @@ -16,11 +18,18 @@ struct ClusterCell : public BaseCell { }; struct Cluster { - explicit Cluster(int clusterId) : id(clusterId) { + explicit Cluster(const int& clusterId, const std::vector& dots) + : id(clusterId), dots(dots), center{findClusterCenter()} { } int id; std::vector dots; + + /// @brief The dot closest to the average position of all the dots of this cluster + Position center; + +private: + Position findClusterCenter() const; }; class ClusterFinder { @@ -33,7 +42,7 @@ class ClusterFinder { std::vector findDotClusters() const; private: - Cluster expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter, const int& clusterID) const; + std::vector expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const; Maze::ConstPtr maze_; }; diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index 60f35b35..5aaa049a 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -1,9 +1,38 @@ #include "utils/cluster.hpp" +#include #include namespace utils { +Position Cluster::findClusterCenter() const { + int sumX = 0; + int sumY = 0; + + for (const auto& dot : dots) { + sumX += dot.x; + sumY += dot.y; + } + + int avgX = std::floor(sumX / dots.size()); + int avgY = std::floor(sumY / dots.size()); + + Position avgPosition{avgX, avgY}; + Position closestDot = dots.front(); + double minDistance = std::numeric_limits::max(); + + for (const auto& dot : dots) { + double distance = avgPosition.distance(dot); + if (distance < minDistance) { + minDistance = distance; + closestDot = dot; + } + } + + return closestDot; +} + + std::vector ClusterFinder::findDotClusters() const { ClusterMazeAdapter mazeAdapter(maze_); std::vector clusters; @@ -17,15 +46,15 @@ std::vector ClusterFinder::findDotClusters() const { if (startCell.type != TileType::DOT || startCell.visited) { continue; } - Cluster cluster = expandDot(startCell, mazeAdapter, clusterId++); - clusters.push_back(cluster); + std::vector dots = expandDot(startCell, mazeAdapter); + clusters.emplace_back(clusterId++, dots); } } return clusters; } -Cluster ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter, const int& clusterID) const { - Cluster cluster(clusterID); +std::vector ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const { + std::vector dots; std::queue bfsQueue; bfsQueue.push(start.position); @@ -39,7 +68,7 @@ Cluster ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& ma continue; } current.visited = true; - cluster.dots.push_back(currentPosition); + dots.push_back(currentPosition); for (const auto& move : demo::Move::possibleMoves()) { Position nextPosition = currentPosition + move.deltaPosition; @@ -56,7 +85,7 @@ Cluster ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& ma } } - return cluster; + return dots; } } // namespace utils diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index a5eae323..66b8161b 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -25,11 +25,18 @@ TEST_F(ClusterTest, findDotClusters) { environmentModel_->setMaze({5, 5}, str); ClusterFinder clusterFinder(environmentModel_->maze()); - std::vector cluster = clusterFinder.findDotClusters(); - - EXPECT_EQ(cluster.size(), 2); - EXPECT_EQ(cluster.front().dots.size(), 3); - EXPECT_EQ(cluster.back().dots.size(), 2); + std::vector clusters = clusterFinder.findDotClusters(); + + EXPECT_EQ(clusters.size(), 2); + EXPECT_EQ(clusters.front().dots.size(), 3); + EXPECT_EQ(clusters.front().center.x, 2); + EXPECT_EQ(clusters.front().center.y, 1); + + EXPECT_EQ(clusters.back().dots.size(), 2); + // We are using std::floor when computing the cluster center so in this + // case the center should be the left of two dots + EXPECT_EQ(clusters.back().center.x, 1); + EXPECT_EQ(clusters.back().center.y, 3); } } // namespace utils::a_star From c0fd77d543278153d9b72477433fc02e3661bd41 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 10:30:06 +0200 Subject: [PATCH 05/21] Find clusters during init and use getter function --- demo/include/utils/cluster.hpp | 11 +++++++++-- demo/src/cluster.cpp | 11 +++++++++-- demo/test/cluster.cpp | 19 +++++++++++-------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index 57ce7301..10847dbc 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -34,17 +34,24 @@ struct Cluster { class ClusterFinder { public: + using Clusters = std::vector; using ClusterMazeAdapter = MazeAdapter; using Cell = ClusterCell; - explicit ClusterFinder(Maze::ConstPtr maze) : maze_(std::move(maze)) { + + explicit ClusterFinder(Maze::ConstPtr maze) : maze_(std::move(maze)), clusters_{findDotClusters()} { + } + Clusters clusters() const { + return clusters_; } + std::vector clusterCenters() const; - std::vector findDotClusters() const; private: std::vector expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const; + Clusters findDotClusters() const; Maze::ConstPtr maze_; + Clusters clusters_; }; } // namespace utils diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index 5aaa049a..eedca8d8 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -32,10 +32,17 @@ Position Cluster::findClusterCenter() const { return closestDot; } +std::vector ClusterFinder::clusterCenters() const { + std::vector centerDots; + for (const auto& cluster : clusters_) { + centerDots.push_back(cluster.center); + } + return centerDots; +} -std::vector ClusterFinder::findDotClusters() const { +ClusterFinder::Clusters ClusterFinder::findDotClusters() const { ClusterMazeAdapter mazeAdapter(maze_); - std::vector clusters; + Clusters clusters; int clusterId = 0; for (int row = 0; row < maze_->height(); row++) { diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index 66b8161b..88f49b33 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -16,7 +16,7 @@ class ClusterTest : public ::testing::Test { MockEnvironmentModel::Ptr environmentModel_; }; -TEST_F(ClusterTest, findDotClusters) { +TEST_F(ClusterTest, dotClusters) { const char str[] = {"#####" "#...#" " " @@ -25,18 +25,21 @@ TEST_F(ClusterTest, findDotClusters) { environmentModel_->setMaze({5, 5}, str); ClusterFinder clusterFinder(environmentModel_->maze()); - std::vector clusters = clusterFinder.findDotClusters(); - EXPECT_EQ(clusters.size(), 2); + std::vector clusters = clusterFinder.clusters(); + ASSERT_EQ(clusters.size(), 2); EXPECT_EQ(clusters.front().dots.size(), 3); - EXPECT_EQ(clusters.front().center.x, 2); - EXPECT_EQ(clusters.front().center.y, 1); - EXPECT_EQ(clusters.back().dots.size(), 2); + + std::vector clusterCenter = clusterFinder.clusterCenters(); + ASSERT_EQ(clusterCenter.size(), 2); + EXPECT_EQ(clusterCenter.front().x, 2); + EXPECT_EQ(clusterCenter.front().y, 1); + // We are using std::floor when computing the cluster center so in this // case the center should be the left of two dots - EXPECT_EQ(clusters.back().center.x, 1); - EXPECT_EQ(clusters.back().center.y, 3); + EXPECT_EQ(clusterCenter.back().x, 1); + EXPECT_EQ(clusterCenter.back().y, 3); } } // namespace utils::a_star From bf3ced5766695ab35546d61e6225938f408c4da3 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 10:41:56 +0200 Subject: [PATCH 06/21] Use Positions alias --- demo/include/utils/cluster.hpp | 5 +++-- demo/src/cluster.cpp | 10 +++++----- demo/test/cluster.cpp | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index 10847dbc..a2485617 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -9,6 +9,7 @@ namespace utils { using Path = demo::Path; using Position = demo::Position; +using Positions = demo::Positions; using TileType = demo::TileType; struct ClusterCell : public BaseCell { @@ -43,11 +44,11 @@ class ClusterFinder { Clusters clusters() const { return clusters_; } - std::vector clusterCenters() const; + Positions clusterCenters() const; private: - std::vector expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const; + Positions expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const; Clusters findDotClusters() const; Maze::ConstPtr maze_; diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index eedca8d8..7967fe1c 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -32,8 +32,8 @@ Position Cluster::findClusterCenter() const { return closestDot; } -std::vector ClusterFinder::clusterCenters() const { - std::vector centerDots; +Positions ClusterFinder::clusterCenters() const { + Positions centerDots; for (const auto& cluster : clusters_) { centerDots.push_back(cluster.center); } @@ -53,15 +53,15 @@ ClusterFinder::Clusters ClusterFinder::findDotClusters() const { if (startCell.type != TileType::DOT || startCell.visited) { continue; } - std::vector dots = expandDot(startCell, mazeAdapter); + Positions dots = expandDot(startCell, mazeAdapter); clusters.emplace_back(clusterId++, dots); } } return clusters; } -std::vector ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const { - std::vector dots; +Positions ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const { + Positions dots; std::queue bfsQueue; bfsQueue.push(start.position); diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index 88f49b33..3efd8c15 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -31,7 +31,7 @@ TEST_F(ClusterTest, dotClusters) { EXPECT_EQ(clusters.front().dots.size(), 3); EXPECT_EQ(clusters.back().dots.size(), 2); - std::vector clusterCenter = clusterFinder.clusterCenters(); + Positions clusterCenter = clusterFinder.clusterCenters(); ASSERT_EQ(clusterCenter.size(), 2); EXPECT_EQ(clusterCenter.front().x, 2); EXPECT_EQ(clusterCenter.front().y, 1); From 529685b742e18c69b4b780a733f70fec5105395c Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 10:54:25 +0200 Subject: [PATCH 07/21] rename getter functions to clarify that we are talking about dot clusters --- demo/include/utils/cluster.hpp | 4 ++-- demo/src/cluster.cpp | 2 +- demo/test/cluster.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index a2485617..50347e95 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -41,10 +41,10 @@ class ClusterFinder { explicit ClusterFinder(Maze::ConstPtr maze) : maze_(std::move(maze)), clusters_{findDotClusters()} { } - Clusters clusters() const { + Clusters dotClusters() const { return clusters_; } - Positions clusterCenters() const; + Positions dotClusterCenters() const; private: diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index 7967fe1c..4199a2ac 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -32,7 +32,7 @@ Position Cluster::findClusterCenter() const { return closestDot; } -Positions ClusterFinder::clusterCenters() const { +Positions ClusterFinder::dotClusterCenters() const { Positions centerDots; for (const auto& cluster : clusters_) { centerDots.push_back(cluster.center); diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index 3efd8c15..19b6c94a 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -26,12 +26,12 @@ TEST_F(ClusterTest, dotClusters) { ClusterFinder clusterFinder(environmentModel_->maze()); - std::vector clusters = clusterFinder.clusters(); + std::vector clusters = clusterFinder.dotClusters(); ASSERT_EQ(clusters.size(), 2); EXPECT_EQ(clusters.front().dots.size(), 3); EXPECT_EQ(clusters.back().dots.size(), 2); - Positions clusterCenter = clusterFinder.clusterCenters(); + Positions clusterCenter = clusterFinder.dotClusterCenters(); ASSERT_EQ(clusterCenter.size(), 2); EXPECT_EQ(clusterCenter.front().x, 2); EXPECT_EQ(clusterCenter.front().y, 1); From 3e89d74854f019c2e8483bec10e246fe87a492b2 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 10:54:38 +0200 Subject: [PATCH 08/21] Add clusterFinder to environment model --- demo/include/demo/environment_model.hpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/demo/include/demo/environment_model.hpp b/demo/include/demo/environment_model.hpp index 4bc1a1f1..50f35f46 100644 --- a/demo/include/demo/environment_model.hpp +++ b/demo/include/demo/environment_model.hpp @@ -6,6 +6,7 @@ #include "types.hpp" #include "utils/astar.hpp" +#include "utils/cluster.hpp" #include "utils/entities.hpp" #include "utils/maze.hpp" @@ -32,7 +33,8 @@ class EnvironmentModel { int distance; }; - explicit EnvironmentModel(const Game& game) : maze_(std::make_shared(game.maze)), astar_(maze_) { + explicit EnvironmentModel(const Game& game) + : maze_{std::make_shared(game.maze)}, astar_{maze_}, clusterFinder_{maze_} { updateEntities(game.reg); }; @@ -42,6 +44,7 @@ class EnvironmentModel { void update(const Game& game) { maze_ = std::make_shared(game.maze); astar_.updateMaze(maze_); + clusterFinder_ = utils::ClusterFinder{maze_}; updateEntities(game.reg); } @@ -68,6 +71,16 @@ class EnvironmentModel { */ std::optional closestScaredGhost(const Time& time) const; + /** + * @brief Returns a vector of dots representing the centers of all dot clusters. + * + * A dot cluster is a set of dots that can be connected by a path passing through neither walls nor empty space. + * The center of a cluster is the dot closest to the average position of all dots of the cluster. + */ + Positions dotClusterCenters() const { + return clusterFinder_.dotClusterCenters(); + } + /** * @brief Calculates the Manhattan distance between two positions using A* considering the maze geometry. * @@ -94,6 +107,7 @@ class EnvironmentModel { Maze::ConstPtr maze_; utils::AStar astar_; + utils::ClusterFinder clusterFinder_; mutable util_caching::Cache closestGhostCache_; mutable util_caching::Cache closestScaredGhostCache_; }; From 529558d5534adb2ac0edb6e7c6c6a2ec2461881b Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 11:00:43 +0200 Subject: [PATCH 09/21] Power pellets can be part of a cluster --- demo/include/demo/environment_model.hpp | 5 +++-- demo/include/utils/maze.hpp | 3 +++ demo/src/cluster.cpp | 4 ++-- demo/test/cluster.cpp | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/demo/include/demo/environment_model.hpp b/demo/include/demo/environment_model.hpp index 50f35f46..c4d2e7ae 100644 --- a/demo/include/demo/environment_model.hpp +++ b/demo/include/demo/environment_model.hpp @@ -74,8 +74,9 @@ class EnvironmentModel { /** * @brief Returns a vector of dots representing the centers of all dot clusters. * - * A dot cluster is a set of dots that can be connected by a path passing through neither walls nor empty space. - * The center of a cluster is the dot closest to the average position of all dots of the cluster. + * A dot cluster is a set of dots (including power pellets) that can be connected by a path passing through neither + * walls nor empty space. The center of a cluster is the dot closest to the average position of all dots of the + * cluster. */ Positions dotClusterCenters() const { return clusterFinder_.dotClusterCenters(); diff --git a/demo/include/utils/maze.hpp b/demo/include/utils/maze.hpp index d3c8f05d..f194cfa0 100644 --- a/demo/include/utils/maze.hpp +++ b/demo/include/utils/maze.hpp @@ -89,6 +89,9 @@ struct BaseCell { double manhattanDistance(const Position& other) const { return std::abs(position.x - other.x) + std::abs(position.y - other.y); } + bool isConsumable() const { + return type == TileType::DOT || type == TileType::ENGERIZER; + } Position position; TileType type; diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index 4199a2ac..ba0db6ac 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -50,7 +50,7 @@ ClusterFinder::Clusters ClusterFinder::findDotClusters() const { Position start{column, row}; ClusterCell& startCell = mazeAdapter.cell(start); - if (startCell.type != TileType::DOT || startCell.visited) { + if (!startCell.isConsumable() || startCell.visited) { continue; } Positions dots = expandDot(startCell, mazeAdapter); @@ -86,7 +86,7 @@ Positions ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& } ClusterCell& neighbor = mazeAdapter.cell(nextPosition); - if (neighbor.type == TileType::DOT && !neighbor.visited) { + if (neighbor.isConsumable() && !neighbor.visited) { bfsQueue.push(nextPosition); } } diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index 19b6c94a..e5b72a2d 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -18,7 +18,7 @@ class ClusterTest : public ::testing::Test { TEST_F(ClusterTest, dotClusters) { const char str[] = {"#####" - "#...#" + "#o..#" " " "#.. #" "#####"}; From 4876ea55bb791934e96fc0757562443aca86f40a Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 12:03:12 +0200 Subject: [PATCH 10/21] Return the full clusters vector instead of just the cluster centers --- demo/include/demo/environment_model.hpp | 11 ++++++----- demo/include/utils/cluster.hpp | 2 -- demo/src/cluster.cpp | 8 -------- demo/test/cluster.cpp | 16 ++++++---------- 4 files changed, 12 insertions(+), 25 deletions(-) diff --git a/demo/include/demo/environment_model.hpp b/demo/include/demo/environment_model.hpp index c4d2e7ae..030f29bc 100644 --- a/demo/include/demo/environment_model.hpp +++ b/demo/include/demo/environment_model.hpp @@ -20,6 +20,8 @@ namespace demo { * the world. */ class EnvironmentModel { public: + using Cluster = utils::Cluster; + using Clusters = utils::ClusterFinder::Clusters; using Entities = utils::Entities; using Maze = utils::Maze; using Ghost = utils::Ghost; @@ -72,14 +74,13 @@ class EnvironmentModel { std::optional closestScaredGhost(const Time& time) const; /** - * @brief Returns a vector of dots representing the centers of all dot clusters. + * @brief Returns a vector of all dot clusters. * * A dot cluster is a set of dots (including power pellets) that can be connected by a path passing through neither - * walls nor empty space. The center of a cluster is the dot closest to the average position of all dots of the - * cluster. + * walls nor empty space. */ - Positions dotClusterCenters() const { - return clusterFinder_.dotClusterCenters(); + Clusters dotCluster() const { + return clusterFinder_.dotClusters(); } /** diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index 50347e95..ff855d4a 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -44,8 +44,6 @@ class ClusterFinder { Clusters dotClusters() const { return clusters_; } - Positions dotClusterCenters() const; - private: Positions expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const; diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index ba0db6ac..c4433a3c 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -32,14 +32,6 @@ Position Cluster::findClusterCenter() const { return closestDot; } -Positions ClusterFinder::dotClusterCenters() const { - Positions centerDots; - for (const auto& cluster : clusters_) { - centerDots.push_back(cluster.center); - } - return centerDots; -} - ClusterFinder::Clusters ClusterFinder::findDotClusters() const { ClusterMazeAdapter mazeAdapter(maze_); Clusters clusters; diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index e5b72a2d..c1886875 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -27,19 +27,15 @@ TEST_F(ClusterTest, dotClusters) { ClusterFinder clusterFinder(environmentModel_->maze()); std::vector clusters = clusterFinder.dotClusters(); + ASSERT_EQ(clusters.size(), 2); EXPECT_EQ(clusters.front().dots.size(), 3); - EXPECT_EQ(clusters.back().dots.size(), 2); + EXPECT_EQ(clusters.front().center.x, 2); + EXPECT_EQ(clusters.front().center.y, 1); - Positions clusterCenter = clusterFinder.dotClusterCenters(); - ASSERT_EQ(clusterCenter.size(), 2); - EXPECT_EQ(clusterCenter.front().x, 2); - EXPECT_EQ(clusterCenter.front().y, 1); - - // We are using std::floor when computing the cluster center so in this - // case the center should be the left of two dots - EXPECT_EQ(clusterCenter.back().x, 1); - EXPECT_EQ(clusterCenter.back().y, 3); + EXPECT_EQ(clusters.back().dots.size(), 2); + EXPECT_EQ(clusters.back().center.x, 1); + EXPECT_EQ(clusters.back().center.y, 3); } } // namespace utils::a_star From 46536c298ce2621780544a07c7d5807566f6fe07 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 12:03:40 +0200 Subject: [PATCH 11/21] Add convenience function to determine whether given position is within a cluster --- demo/include/utils/cluster.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index ff855d4a..b4ca87f2 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -22,6 +22,9 @@ struct Cluster { explicit Cluster(const int& clusterId, const std::vector& dots) : id(clusterId), dots(dots), center{findClusterCenter()} { } + bool isInCluster(const Position& target) const { + return std::any_of(dots.begin(), dots.end(), [target](Position dot) { return dot == target; }); + } int id; std::vector dots; From ce90279cd4d7094f655f89e62bfc9b60a68947e4 Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Fri, 4 Oct 2024 11:28:05 +0200 Subject: [PATCH 12/21] Add another astar interface to the env model --- demo/include/demo/environment_model.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/demo/include/demo/environment_model.hpp b/demo/include/demo/environment_model.hpp index 030f29bc..52ab3961 100644 --- a/demo/include/demo/environment_model.hpp +++ b/demo/include/demo/environment_model.hpp @@ -92,6 +92,10 @@ class EnvironmentModel { return astar_.mazeDistance(start, goal); } + Path pathTo(const Position& goal) { + return astar_.shortestPath(pacmanPosition(), goal); + } + std::optional pathToClosestDot(const Position& position) const { return astar_.pathToClosestDot(position); } From 52966c262ef32088944f233cda3e2705c5aa9531 Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Fri, 4 Oct 2024 11:32:52 +0200 Subject: [PATCH 13/21] Add changeDotClusterBehavior --- demo/CMakeLists.txt | 1 + .../demo/change_dot_cluster_behavior.hpp | 53 +++++++ demo/src/change_dot_cluster_behavior.cpp | 35 +++++ demo/test/change_dot_cluster_behavior.cpp | 132 ++++++++++++++++++ demo/test/mock_environment_model.hpp | 1 + 5 files changed, 222 insertions(+) create mode 100644 demo/include/demo/change_dot_cluster_behavior.hpp create mode 100644 demo/src/change_dot_cluster_behavior.cpp create mode 100644 demo/test/change_dot_cluster_behavior.cpp diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 2ed6c530..721a92b7 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -26,6 +26,7 @@ find_package(Yaml-cpp REQUIRED) add_library(${PROJECT_NAME} STATIC src/astar.cpp src/avoid_ghost_behavior.cpp + src/change_dot_cluster_behavior.cpp src/chase_ghost_behavior.cpp src/cluster.cpp src/entities.cpp diff --git a/demo/include/demo/change_dot_cluster_behavior.hpp b/demo/include/demo/change_dot_cluster_behavior.hpp new file mode 100644 index 00000000..b44e1428 --- /dev/null +++ b/demo/include/demo/change_dot_cluster_behavior.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "environment_model.hpp" +#include "types.hpp" +#include "utils/cluster.hpp" + +namespace demo { + +/** + * @brief The ChangeDotClusterBehavior makes pacman move towards another cluster of duts. + */ +class ChangeDotClusterBehavior : public arbitration_graphs::Behavior { +public: + using Ptr = std::shared_ptr; + using ConstPtr = std::shared_ptr; + + using Cluster = utils::Cluster; + using Clusters = utils::ClusterFinder::Clusters; + + explicit ChangeDotClusterBehavior(EnvironmentModel::Ptr environmentModel, + const std::string& name = "ChangeDotClusterBehavior") + : Behavior(name), environmentModel_{std::move(environmentModel)} { + } + + Command getCommand(const Time& /*time*/) override { + Path pathToTargetClusterCenter = environmentModel_->pathTo(targetCluster_->center); + return Command{pathToTargetClusterCenter}; + } + + bool checkInvocationCondition(const Time& /*time*/) const override; + + bool checkCommitmentCondition(const Time& /*time*/) const override { + Position pacmanPosition = environmentModel_->pacmanPosition(); + return !targetCluster_->isInCluster(pacmanPosition); + } + + void gainControl(const Time& /*time*/) override { + setTargetCluster(); + } + void loseControl(const Time& /*time*/) override { + targetCluster_.reset(); + } + +private: + void setTargetCluster(); + + std::optional targetCluster_; + EnvironmentModel::Ptr environmentModel_; +}; + +} // namespace demo diff --git a/demo/src/change_dot_cluster_behavior.cpp b/demo/src/change_dot_cluster_behavior.cpp new file mode 100644 index 00000000..11698032 --- /dev/null +++ b/demo/src/change_dot_cluster_behavior.cpp @@ -0,0 +1,35 @@ +#include "demo/change_dot_cluster_behavior.hpp" + +namespace demo { + +bool ChangeDotClusterBehavior::checkInvocationCondition(const Time& /*time*/) const { + auto pacmanPosition = environmentModel_->pacmanPosition(); + Clusters clusters = environmentModel_->dotCluster(); + + if (clusters.empty()) { + // We cannot navigate to a cluster if there aren't any + return false; + } + if (clusters.size() == 1 && clusters.front().isInCluster(pacmanPosition)) { + // The only cluster left is the one we are already in + return false; + } + + return true; +} + +void ChangeDotClusterBehavior::setTargetCluster() { + auto pacmanPosition = environmentModel_->pacmanPosition(); + Clusters clusters = environmentModel_->dotCluster(); + + int minDistance = std::numeric_limits::max(); + for (const auto& cluster : clusters) { + int distance = environmentModel_->mazeDistance(pacmanPosition, cluster.center); + if (distance < minDistance && !cluster.isInCluster(pacmanPosition)) { + minDistance = distance; + targetCluster_ = cluster; + } + } +} + +} // namespace demo diff --git a/demo/test/change_dot_cluster_behavior.cpp b/demo/test/change_dot_cluster_behavior.cpp new file mode 100644 index 00000000..8f1308f1 --- /dev/null +++ b/demo/test/change_dot_cluster_behavior.cpp @@ -0,0 +1,132 @@ +#include "demo/change_dot_cluster_behavior.hpp" + +#include + +#include + +#include "mock_environment_model.hpp" + +namespace demo { + +class ChangeDotClusterBehaviorTest : public ::testing::Test { +protected: + ChangeDotClusterBehaviorTest() + : environmentModel_(std::make_shared()), + changeDotClusterBehavior_{environmentModel_} { + setMazeWithTwoClusters(); + environmentModel_->setGhostPositions({1, 1}); + } + + void setMazeWithoutClusters() { + const char str[] = {"#####" + "# #" + "# #" + "# #" + "# #" + "#####"}; + environmentModel_->setMaze({5, 6}, str); + } + void setMazeWithOneCluster() { + const char str[] = {"#####" + "#o..#" + "# #" + "# #" + "# #" + "#####"}; + environmentModel_->setMaze({5, 6}, str); + } + void setMazeWithTwoClusters() { + const char str[] = {"#####" + "#o..#" + "# #" + "# #" + "#.. #" + "#####"}; + environmentModel_->setMaze({5, 6}, str); + } + + MockEnvironmentModel::Ptr environmentModel_; + + ChangeDotClusterBehavior changeDotClusterBehavior_; +}; + +TEST_F(ChangeDotClusterBehaviorTest, checkInvocationConditionTrue) { + Time time = Clock::now(); + + // The invocation condition should be true if... + + // ...we are outside the only cluster that's left + setMazeWithOneCluster(); + environmentModel_->setPacmanPosition({1, 3}); + + ASSERT_TRUE(changeDotClusterBehavior_.checkInvocationCondition(time)); + + // ...or there are multiple clusters left. Doesn't matter if we are outside... + setMazeWithTwoClusters(); + environmentModel_->setPacmanPosition({1, 3}); + ASSERT_TRUE(changeDotClusterBehavior_.checkInvocationCondition(time)); + + // .. or inside a cluster in that case + environmentModel_->setPacmanPosition({1, 1}); + ASSERT_TRUE(changeDotClusterBehavior_.checkInvocationCondition(time)); +} + +TEST_F(ChangeDotClusterBehaviorTest, checkInvocationConditionFalse) { + Time time = Clock::now(); + + // The invocation condition should be false if... + + // ..there are no clusters left + setMazeWithoutClusters(); + environmentModel_->setPacmanPosition({1, 3}); + + ASSERT_FALSE(changeDotClusterBehavior_.checkInvocationCondition(time)); + + // ...we are inside the only cluster that's left + setMazeWithOneCluster(); + environmentModel_->setPacmanPosition({1, 1}); + + ASSERT_FALSE(changeDotClusterBehavior_.checkInvocationCondition(time)); +} + +TEST_F(ChangeDotClusterBehaviorTest, checkCommitmentConditionTrue) { + Time time = Clock::now(); + setMazeWithTwoClusters(); + environmentModel_->setPacmanPosition({1, 2}); + ASSERT_TRUE(changeDotClusterBehavior_.checkInvocationCondition(time)); + + // Once we gained control, we commit to reaching the target dot cluster + changeDotClusterBehavior_.gainControl(time); + ASSERT_TRUE(changeDotClusterBehavior_.checkCommitmentCondition(time)); +} + +TEST_F(ChangeDotClusterBehaviorTest, checkCommitmentConditionFalse) { + Time time = Clock::now(); + setMazeWithTwoClusters(); + environmentModel_->setPacmanPosition({1, 2}); + ASSERT_TRUE(changeDotClusterBehavior_.checkInvocationCondition(time)); + changeDotClusterBehavior_.gainControl(time); + + // Once we reached the target cluster, we finished our intended behavior and give up the commitment + environmentModel_->setPacmanPosition({2, 1}); + ASSERT_FALSE(changeDotClusterBehavior_.checkCommitmentCondition(time)); + + // We reached our goal no matter if we reached the cluster center or just any of the cluster dots + environmentModel_->setPacmanPosition({1, 1}); + ASSERT_FALSE(changeDotClusterBehavior_.checkCommitmentCondition(time)); +} + +TEST_F(ChangeDotClusterBehaviorTest, getCommand) { + Time time = Clock::now(); + setMazeWithTwoClusters(); + environmentModel_->setPacmanPosition({2, 2}); + ASSERT_TRUE(changeDotClusterBehavior_.checkInvocationCondition(time)); + + changeDotClusterBehavior_.gainControl(time); + + // The resulting command should navigate us towards the closest cluster center + Command command = changeDotClusterBehavior_.getCommand(time); + ASSERT_EQ(command.nextDirection(), Direction::UP); +} + +} // namespace demo diff --git a/demo/test/mock_environment_model.hpp b/demo/test/mock_environment_model.hpp index 95a91c68..87f76c69 100644 --- a/demo/test/mock_environment_model.hpp +++ b/demo/test/mock_environment_model.hpp @@ -67,6 +67,7 @@ class MockEnvironmentModel : public EnvironmentModel { void setMaze(const Position& size, const char (&str)[Size]) { maze_ = std::make_shared(makeCustomMazeState({size.x, size.y}, str)); astar_ = utils::AStar(maze_); + clusterFinder_ = utils::ClusterFinder(maze_); } void setEmptyMaze() { const char str[] = {"##########" From f199a48e6de7404da9c26aafe6e80070df002d6c Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 13:32:29 +0200 Subject: [PATCH 14/21] Bug fix: Fix out of range error in run away and chase ghost behaviors --- demo/include/demo/environment_model.hpp | 3 +++ demo/src/avoid_ghost_behavior.cpp | 2 +- demo/src/chase_ghost_behavior.cpp | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/demo/include/demo/environment_model.hpp b/demo/include/demo/environment_model.hpp index 52ab3961..d59ccada 100644 --- a/demo/include/demo/environment_model.hpp +++ b/demo/include/demo/environment_model.hpp @@ -103,6 +103,9 @@ class EnvironmentModel { bool isWall(const Position& position) const { return maze_->isWall(position); } + Position positionConsideringTunnel(const Position& position) const { + return maze_->positionConsideringTunnel(position); + } protected: void updateEntities(const entt::Registry& registry); diff --git a/demo/src/avoid_ghost_behavior.cpp b/demo/src/avoid_ghost_behavior.cpp index 067add06..bbf80017 100644 --- a/demo/src/avoid_ghost_behavior.cpp +++ b/demo/src/avoid_ghost_behavior.cpp @@ -10,7 +10,7 @@ Command AvoidGhostBehavior::getCommand(const Time& time) { double maxDistance = -1; for (const auto& move : Move::possibleMoves()) { - auto nextPosition = pacmanPosition + move.deltaPosition; + auto nextPosition = environmentModel_->positionConsideringTunnel(pacmanPosition + move.deltaPosition); if (environmentModel_->isWall(nextPosition)) { continue; diff --git a/demo/src/chase_ghost_behavior.cpp b/demo/src/chase_ghost_behavior.cpp index 2a15e19b..907162ad 100644 --- a/demo/src/chase_ghost_behavior.cpp +++ b/demo/src/chase_ghost_behavior.cpp @@ -15,7 +15,7 @@ Command ChaseGhostBehavior::getCommand(const Time& time) { double minDistance = std::numeric_limits::max(); for (const auto& move : Move::possibleMoves()) { - auto nextPosition = pacmanPosition + move.deltaPosition; + auto nextPosition = environmentModel_->positionConsideringTunnel(pacmanPosition + move.deltaPosition); if (environmentModel_->isWall(nextPosition)) { continue; From b91d969bf5a86a4c435f96cc6d6b27dd02009774 Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Fri, 4 Oct 2024 11:35:15 +0200 Subject: [PATCH 15/21] Add changeDotClusterBehavior to pacmanAgent --- demo/include/demo/pacman_agent.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/demo/include/demo/pacman_agent.hpp b/demo/include/demo/pacman_agent.hpp index 3265be95..ebd3774c 100644 --- a/demo/include/demo/pacman_agent.hpp +++ b/demo/include/demo/pacman_agent.hpp @@ -3,6 +3,7 @@ #include #include "avoid_ghost_behavior.hpp" +#include "change_dot_cluster_behavior.hpp" #include "chase_ghost_behavior.hpp" #include "eat_closest_dot_behavior.hpp" #include "environment_model.hpp" @@ -34,6 +35,7 @@ class PacmanAgent { environmentModel_ = std::make_shared(game); avoidGhostBehavior_ = std::make_shared(environmentModel_, parameters_.avoidGhostBehavior); + changeDotClusterBehavior_ = std::make_shared(environmentModel_); chaseGhostBehavior_ = std::make_shared(environmentModel_, parameters_.chaseGhostBehavior); eatClosestDotBehavior_ = std::make_shared(environmentModel_); randomWalkBehavior_ = std::make_shared(parameters_.randomWalkBehavior); @@ -42,6 +44,7 @@ class PacmanAgent { rootArbitrator_ = std::make_shared(); rootArbitrator_->addOption(chaseGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); rootArbitrator_->addOption(avoidGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(changeDotClusterBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); rootArbitrator_->addOption(eatClosestDotBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); rootArbitrator_->addOption(randomWalkBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); rootArbitrator_->addOption(stayInPlaceBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); @@ -69,6 +72,7 @@ class PacmanAgent { Parameters parameters_; AvoidGhostBehavior::Ptr avoidGhostBehavior_; + ChangeDotClusterBehavior::Ptr changeDotClusterBehavior_; ChaseGhostBehavior::Ptr chaseGhostBehavior_; EatClosestDotBehavior::Ptr eatClosestDotBehavior_; RandomWalkBehavior::Ptr randomWalkBehavior_; From 704d91bdec2900061bcec1f334f9f1dc0054ca23 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 25 Jul 2024 15:16:01 +0200 Subject: [PATCH 16/21] Add documentation and tidy up Rename clusterFinder to dotClusterFinder --- .../demo/change_dot_cluster_behavior.hpp | 5 ++-- demo/include/demo/environment_model.hpp | 8 +++---- demo/include/utils/cluster.hpp | 24 ++++++++++++------- demo/include/utils/maze.hpp | 8 ++++++- demo/src/cluster.cpp | 10 ++++---- demo/test/cluster.cpp | 4 ++-- demo/test/mock_environment_model.hpp | 2 +- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/demo/include/demo/change_dot_cluster_behavior.hpp b/demo/include/demo/change_dot_cluster_behavior.hpp index b44e1428..858a1a93 100644 --- a/demo/include/demo/change_dot_cluster_behavior.hpp +++ b/demo/include/demo/change_dot_cluster_behavior.hpp @@ -9,7 +9,8 @@ namespace demo { /** - * @brief The ChangeDotClusterBehavior makes pacman move towards another cluster of duts. + * @brief The ChangeDotClusterBehavior makes pacman move towards the closest dot cluster + * that he is not currently inside of. */ class ChangeDotClusterBehavior : public arbitration_graphs::Behavior { public: @@ -17,7 +18,7 @@ class ChangeDotClusterBehavior : public arbitration_graphs::Behavior { using ConstPtr = std::shared_ptr; using Cluster = utils::Cluster; - using Clusters = utils::ClusterFinder::Clusters; + using Clusters = utils::DotClusterFinder::Clusters; explicit ChangeDotClusterBehavior(EnvironmentModel::Ptr environmentModel, const std::string& name = "ChangeDotClusterBehavior") diff --git a/demo/include/demo/environment_model.hpp b/demo/include/demo/environment_model.hpp index d59ccada..e0357435 100644 --- a/demo/include/demo/environment_model.hpp +++ b/demo/include/demo/environment_model.hpp @@ -21,7 +21,7 @@ namespace demo { class EnvironmentModel { public: using Cluster = utils::Cluster; - using Clusters = utils::ClusterFinder::Clusters; + using Clusters = utils::DotClusterFinder::Clusters; using Entities = utils::Entities; using Maze = utils::Maze; using Ghost = utils::Ghost; @@ -46,7 +46,7 @@ class EnvironmentModel { void update(const Game& game) { maze_ = std::make_shared(game.maze); astar_.updateMaze(maze_); - clusterFinder_ = utils::ClusterFinder{maze_}; + clusterFinder_ = utils::DotClusterFinder{maze_}; updateEntities(game.reg); } @@ -80,7 +80,7 @@ class EnvironmentModel { * walls nor empty space. */ Clusters dotCluster() const { - return clusterFinder_.dotClusters(); + return clusterFinder_.clusters(); } /** @@ -116,7 +116,7 @@ class EnvironmentModel { Maze::ConstPtr maze_; utils::AStar astar_; - utils::ClusterFinder clusterFinder_; + utils::DotClusterFinder clusterFinder_; mutable util_caching::Cache closestGhostCache_; mutable util_caching::Cache closestScaredGhostCache_; }; diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index b4ca87f2..9383f334 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -18,33 +18,39 @@ struct ClusterCell : public BaseCell { bool visited{false}; }; +/** + * @brief A cluster is defined by a set of points that can be connected by a path which passes through neither walls nor + * empty space. + */ struct Cluster { - explicit Cluster(const int& clusterId, const std::vector& dots) - : id(clusterId), dots(dots), center{findClusterCenter()} { + explicit Cluster(const int& clusterId, const std::vector& points) + : id(clusterId), dots(points), center{findClusterCenter()} { } bool isInCluster(const Position& target) const { return std::any_of(dots.begin(), dots.end(), [target](Position dot) { return dot == target; }); } int id; - std::vector dots; + Positions dots; - /// @brief The dot closest to the average position of all the dots of this cluster - Position center; + Position center; ///< The dot closest to the average position of all the dots of this cluster private: Position findClusterCenter() const; }; -class ClusterFinder { +/** + * @brief Search and store all clusters of dots (including power pellets) given the maze state. + */ +class DotClusterFinder { public: + using Cell = ClusterCell; using Clusters = std::vector; using ClusterMazeAdapter = MazeAdapter; - using Cell = ClusterCell; - explicit ClusterFinder(Maze::ConstPtr maze) : maze_(std::move(maze)), clusters_{findDotClusters()} { + explicit DotClusterFinder(Maze::ConstPtr maze) : maze_(std::move(maze)), clusters_{findDotClusters()} { } - Clusters dotClusters() const { + Clusters clusters() const { return clusters_; } diff --git a/demo/include/utils/maze.hpp b/demo/include/utils/maze.hpp index f194cfa0..5deb39da 100644 --- a/demo/include/utils/maze.hpp +++ b/demo/include/utils/maze.hpp @@ -97,7 +97,13 @@ struct BaseCell { TileType type; }; -template +/** + * @brief Adapter class for storing properties per cell of the underlying maze. + * + * The MazeAdapter is a helpful little class to put between an algorithm and the actual maze class. It can be used to + * store properties per cell of the maze grid using the templated CellType class. Cells are lazily initialized. + */ +template class MazeAdapter { public: using MazeStateConstPtr = std::shared_ptr; diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index c4433a3c..dbe33b7a 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -9,9 +9,9 @@ Position Cluster::findClusterCenter() const { int sumX = 0; int sumY = 0; - for (const auto& dot : dots) { - sumX += dot.x; - sumY += dot.y; + for (const auto& point : dots) { + sumX += point.x; + sumY += point.y; } int avgX = std::floor(sumX / dots.size()); @@ -32,7 +32,7 @@ Position Cluster::findClusterCenter() const { return closestDot; } -ClusterFinder::Clusters ClusterFinder::findDotClusters() const { +DotClusterFinder::Clusters DotClusterFinder::findDotClusters() const { ClusterMazeAdapter mazeAdapter(maze_); Clusters clusters; int clusterId = 0; @@ -52,7 +52,7 @@ ClusterFinder::Clusters ClusterFinder::findDotClusters() const { return clusters; } -Positions ClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const { +Positions DotClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const { Positions dots; std::queue bfsQueue; diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index c1886875..6ced0d05 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -24,9 +24,9 @@ TEST_F(ClusterTest, dotClusters) { "#####"}; environmentModel_->setMaze({5, 5}, str); - ClusterFinder clusterFinder(environmentModel_->maze()); + DotClusterFinder dotClusterFinder(environmentModel_->maze()); - std::vector clusters = clusterFinder.dotClusters(); + std::vector clusters = dotClusterFinder.clusters(); ASSERT_EQ(clusters.size(), 2); EXPECT_EQ(clusters.front().dots.size(), 3); diff --git a/demo/test/mock_environment_model.hpp b/demo/test/mock_environment_model.hpp index 87f76c69..032c3631 100644 --- a/demo/test/mock_environment_model.hpp +++ b/demo/test/mock_environment_model.hpp @@ -67,7 +67,7 @@ class MockEnvironmentModel : public EnvironmentModel { void setMaze(const Position& size, const char (&str)[Size]) { maze_ = std::make_shared(makeCustomMazeState({size.x, size.y}, str)); astar_ = utils::AStar(maze_); - clusterFinder_ = utils::ClusterFinder(maze_); + clusterFinder_ = utils::DotClusterFinder(maze_); } void setEmptyMaze() { const char str[] = {"##########" From 063ae1e3e5f33f325107aed1c89d13285ebd0948 Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Fri, 4 Oct 2024 12:28:47 +0200 Subject: [PATCH 17/21] Use std::optional for A* path return types --- .../demo/change_dot_cluster_behavior.hpp | 9 +++++++-- demo/include/demo/environment_model.hpp | 2 +- demo/include/utils/astar.hpp | 4 ++-- demo/src/astar.cpp | 16 ++++++++-------- demo/test/astar.cpp | 19 +++++++++++-------- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/demo/include/demo/change_dot_cluster_behavior.hpp b/demo/include/demo/change_dot_cluster_behavior.hpp index 858a1a93..e19ee3ac 100644 --- a/demo/include/demo/change_dot_cluster_behavior.hpp +++ b/demo/include/demo/change_dot_cluster_behavior.hpp @@ -26,8 +26,13 @@ class ChangeDotClusterBehavior : public arbitration_graphs::Behavior { } Command getCommand(const Time& /*time*/) override { - Path pathToTargetClusterCenter = environmentModel_->pathTo(targetCluster_->center); - return Command{pathToTargetClusterCenter}; + std::optional pathToTargetClusterCenter = environmentModel_->pathTo(targetCluster_->center); + + if (!pathToTargetClusterCenter) { + throw std::runtime_error("Failed to compute path to target cluster. Can not provide a sensible command."); + } + + return Command{pathToTargetClusterCenter.value()}; } bool checkInvocationCondition(const Time& /*time*/) const override; diff --git a/demo/include/demo/environment_model.hpp b/demo/include/demo/environment_model.hpp index e0357435..b6e375cb 100644 --- a/demo/include/demo/environment_model.hpp +++ b/demo/include/demo/environment_model.hpp @@ -92,7 +92,7 @@ class EnvironmentModel { return astar_.mazeDistance(start, goal); } - Path pathTo(const Position& goal) { + std::optional pathTo(const Position& goal) { return astar_.shortestPath(pacmanPosition(), goal); } diff --git a/demo/include/utils/astar.hpp b/demo/include/utils/astar.hpp index cd73ae26..a3aa3a79 100644 --- a/demo/include/utils/astar.hpp +++ b/demo/include/utils/astar.hpp @@ -60,7 +60,7 @@ class AStar { /** * @brief Returns the shortest path from the start to the goal position considering the maze geometry. */ - Path shortestPath(const Position& start, const Position& goal) const; + std::optional shortestPath(const Position& start, const Position& goal) const; /** * @brief Returns the path from a given start position to the closest dot. @@ -82,7 +82,7 @@ class AStar { * Will expand the path backwards until no more predecessor relationship is available. If the cell at the goal * position does not have a predecessor, the path will be empty. */ - Path pathTo(const AStarMazeAdapter& maze, const Position& goal) const; + Path extractPathTo(const AStarMazeAdapter& maze, const Position& goal) const; /** * @brief Approximates the distance of a given cell to a goal while considering a shortcut via the tunnel. diff --git a/demo/src/astar.cpp b/demo/src/astar.cpp index af19eb45..46afa063 100644 --- a/demo/src/astar.cpp +++ b/demo/src/astar.cpp @@ -7,14 +7,14 @@ int AStar::mazeDistance(const Position& start, const Position& goal) const { return distanceCache_.cached({start, goal}).value(); } - Path path = shortestPath(start, goal); - int pathLength = static_cast(path.size()); + std::optional path = shortestPath(start, goal); + int pathLength = path ? static_cast(path->size()) : NoPathFound; distanceCache_.cache({start, goal}, pathLength); return pathLength; } -Path AStar::shortestPath(const Position& start, const Position& goal) const { +std::optional AStar::shortestPath(const Position& start, const Position& goal) const { // There is a "virtual" position outside of the maze that entities are on when entering the tunnel. We accept a // small error in the distance computation by neglecting this and wrapping the position to be on either end of the // tunnel. @@ -39,12 +39,12 @@ Path AStar::shortestPath(const Position& start, const Position& goal) const { while (!openSet.empty()) { if (openSet.top().position == wrappedGoal) { - return pathTo(mazeAdapter, openSet.top().position); + return extractPathTo(mazeAdapter, openSet.top().position); } expandCell(openSet, mazeAdapter, heuristic); } - return {}; + return std::nullopt; } std::optional AStar::pathToClosestDot(const Position& start) const { @@ -70,12 +70,12 @@ std::optional AStar::pathToClosestDot(const Position& start) const { // Unfortunately, the pacman simulation will handle the dot consumption after the move, therefore we need to // explicitly exclude the start position from the search. if (openSet.top().type == TileType::DOT && openSet.top().position != start) { - return pathTo(mazeAdapter, openSet.top().position); + return extractPathTo(mazeAdapter, openSet.top().position); } expandCell(openSet, mazeAdapter, heuristic); } - return {}; + return std::nullopt; } void AStar::expandCell(Set& openSet, AStarMazeAdapter& mazeAdapter, const HeuristicFunction& heuristic) const { @@ -108,7 +108,7 @@ void AStar::expandCell(Set& openSet, AStarMazeAdapter& mazeAdapter, const Heuris } } -Path AStar::pathTo(const AStarMazeAdapter& maze, const Position& goal) const { +Path AStar::extractPathTo(const AStarMazeAdapter& maze, const Position& goal) const { Path path; Cell current = maze.cell(goal); while (current.moveFromPredecessor) { diff --git a/demo/test/astar.cpp b/demo/test/astar.cpp index b209feaa..ecfb6e11 100644 --- a/demo/test/astar.cpp +++ b/demo/test/astar.cpp @@ -95,11 +95,12 @@ TEST_F(AStarTest, path) { environmentModel_->setMaze({5, 5}, str); AStar astar(environmentModel_->maze()); - Path path = astar.shortestPath({2, 1}, {3, 3}); + std::optional path = astar.shortestPath({2, 1}, {3, 3}); Path targetPath = {Direction::LEFT, Direction::DOWN, Direction::DOWN, Direction::RIGHT, Direction::RIGHT}; - ASSERT_EQ(path.size(), targetPath.size()); + ASSERT_TRUE(path.has_value()); + ASSERT_EQ(path->size(), targetPath.size()); for (int i = 0; i < targetPath.size(); i++) { - EXPECT_EQ(path.at(i), targetPath.at(i)); + EXPECT_EQ(path->at(i), targetPath.at(i)); } } @@ -112,13 +113,15 @@ TEST_F(AStarTest, pathWithTunnel) { environmentModel_->setMaze({5, 5}, str); AStar astar(environmentModel_->maze()); - Path path = astar.shortestPath({0, 2}, {4, 2}); - ASSERT_EQ(path.size(), 1); - EXPECT_EQ(path.front(), demo::Direction::LEFT); + std::optional path = astar.shortestPath({0, 2}, {4, 2}); + ASSERT_TRUE(path.has_value()); + ASSERT_EQ(path->size(), 1); + EXPECT_EQ(path->front(), demo::Direction::LEFT); path = astar.shortestPath({4, 2}, {0, 2}); - ASSERT_EQ(path.size(), 1); - EXPECT_EQ(path.front(), demo::Direction::RIGHT); + ASSERT_TRUE(path.has_value()); + ASSERT_EQ(path->size(), 1); + EXPECT_EQ(path->front(), demo::Direction::RIGHT); } TEST_F(AStarTest, pathToClosestDot) { From 88d2e45958b0422a5a8dd33b7c4cb4e10a0c1810 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:20:29 +0200 Subject: [PATCH 18/21] Apply suggestions from code review Co-authored-by: Piotr Spieker --- demo/include/utils/cluster.hpp | 2 +- demo/include/utils/maze.hpp | 10 +++++----- demo/src/cluster.cpp | 23 ++++++++++------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/demo/include/utils/cluster.hpp b/demo/include/utils/cluster.hpp index 9383f334..c5e5ffbd 100644 --- a/demo/include/utils/cluster.hpp +++ b/demo/include/utils/cluster.hpp @@ -23,7 +23,7 @@ struct ClusterCell : public BaseCell { * empty space. */ struct Cluster { - explicit Cluster(const int& clusterId, const std::vector& points) + explicit Cluster(const int& clusterId, const Positions& points) : id(clusterId), dots(points), center{findClusterCenter()} { } bool isInCluster(const Position& target) const { diff --git a/demo/include/utils/maze.hpp b/demo/include/utils/maze.hpp index 5deb39da..29535822 100644 --- a/demo/include/utils/maze.hpp +++ b/demo/include/utils/maze.hpp @@ -101,9 +101,9 @@ struct BaseCell { * @brief Adapter class for storing properties per cell of the underlying maze. * * The MazeAdapter is a helpful little class to put between an algorithm and the actual maze class. It can be used to - * store properties per cell of the maze grid using the templated CellType class. Cells are lazily initialized. + * store properties per cell of the maze grid using the templated CellT class. Cells are lazily initialized. */ -template +template class MazeAdapter { public: using MazeStateConstPtr = std::shared_ptr; @@ -111,9 +111,9 @@ class MazeAdapter { explicit MazeAdapter(Maze::ConstPtr maze) : maze_(std::move(maze)), cells_({maze_->width(), maze_->height()}) {}; - CellType& cell(const Position& position) const { + CellT& cell(const Position& position) const { if (!cells_[{position.x, position.y}]) { - cells_[{position.x, position.y}] = CellType(position, maze_->operator[](position)); + cells_[{position.x, position.y}] = CellT(position, maze_->operator[](position)); } return cells_[{position.x, position.y}].value(); @@ -121,7 +121,7 @@ class MazeAdapter { private: Maze::ConstPtr maze_; - mutable Grid> cells_; + mutable Grid> cells_; }; } // namespace utils diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index dbe33b7a..062416e0 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -6,6 +6,9 @@ namespace utils { Position Cluster::findClusterCenter() const { + if (dots.empty()) { + throw std::runtime_error("Cannot find center of an empty cluster"); + } int sumX = 0; int sumY = 0; @@ -14,20 +17,14 @@ Position Cluster::findClusterCenter() const { sumY += point.y; } - int avgX = std::floor(sumX / dots.size()); - int avgY = std::floor(sumY / dots.size()); - - Position avgPosition{avgX, avgY}; - Position closestDot = dots.front(); - double minDistance = std::numeric_limits::max(); + const int avgX = std::floor(sumX / dots.size()); + const int avgY = std::floor(sumY / dots.size()); - for (const auto& dot : dots) { - double distance = avgPosition.distance(dot); - if (distance < minDistance) { - minDistance = distance; - closestDot = dot; - } - } + const Position avgPosition{avgX, avgY}; + auto distanceComparator = [&avgPosition](const Position& lhs, const Position& rhs) { + return avgPosition.distance(lhs) < avgPosition.distance(rhs); + }; + Position closestDot = *std::min_element(dots.begin(), dots.end(), distanceComparator); return closestDot; } From 5a6118b9db9c2b68bd78a4d64a5265f2ee75e242 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 7 Oct 2024 08:28:08 +0200 Subject: [PATCH 19/21] Move behavior implementation out of header --- .../demo/change_dot_cluster_behavior.hpp | 18 +++--------------- demo/src/change_dot_cluster_behavior.cpp | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/demo/include/demo/change_dot_cluster_behavior.hpp b/demo/include/demo/change_dot_cluster_behavior.hpp index e19ee3ac..c040514c 100644 --- a/demo/include/demo/change_dot_cluster_behavior.hpp +++ b/demo/include/demo/change_dot_cluster_behavior.hpp @@ -9,7 +9,7 @@ namespace demo { /** - * @brief The ChangeDotClusterBehavior makes pacman move towards the closest dot cluster + * @brief The ChangeDotClusterBehavior makes Pacman move towards the closest dot cluster * that he is not currently inside of. */ class ChangeDotClusterBehavior : public arbitration_graphs::Behavior { @@ -25,22 +25,10 @@ class ChangeDotClusterBehavior : public arbitration_graphs::Behavior { : Behavior(name), environmentModel_{std::move(environmentModel)} { } - Command getCommand(const Time& /*time*/) override { - std::optional pathToTargetClusterCenter = environmentModel_->pathTo(targetCluster_->center); - - if (!pathToTargetClusterCenter) { - throw std::runtime_error("Failed to compute path to target cluster. Can not provide a sensible command."); - } - - return Command{pathToTargetClusterCenter.value()}; - } + Command getCommand(const Time& /*time*/) override; bool checkInvocationCondition(const Time& /*time*/) const override; - - bool checkCommitmentCondition(const Time& /*time*/) const override { - Position pacmanPosition = environmentModel_->pacmanPosition(); - return !targetCluster_->isInCluster(pacmanPosition); - } + bool checkCommitmentCondition(const Time& /*time*/) const override; void gainControl(const Time& /*time*/) override { setTargetCluster(); diff --git a/demo/src/change_dot_cluster_behavior.cpp b/demo/src/change_dot_cluster_behavior.cpp index 11698032..7e30a05c 100644 --- a/demo/src/change_dot_cluster_behavior.cpp +++ b/demo/src/change_dot_cluster_behavior.cpp @@ -2,6 +2,16 @@ namespace demo { +Command ChangeDotClusterBehavior::getCommand(const Time& /*time*/) { + std::optional pathToTargetClusterCenter = environmentModel_->pathTo(targetCluster_->center); + + if (!pathToTargetClusterCenter) { + throw std::runtime_error("Failed to compute path to target cluster. Can not provide a sensible command."); + } + + return Command{pathToTargetClusterCenter.value()}; +} + bool ChangeDotClusterBehavior::checkInvocationCondition(const Time& /*time*/) const { auto pacmanPosition = environmentModel_->pacmanPosition(); Clusters clusters = environmentModel_->dotCluster(); @@ -18,6 +28,11 @@ bool ChangeDotClusterBehavior::checkInvocationCondition(const Time& /*time*/) co return true; } +bool ChangeDotClusterBehavior::checkCommitmentCondition(const Time& /*time*/) const { + Position pacmanPosition = environmentModel_->pacmanPosition(); + return !targetCluster_->isInCluster(pacmanPosition); +} + void ChangeDotClusterBehavior::setTargetCluster() { auto pacmanPosition = environmentModel_->pacmanPosition(); Clusters clusters = environmentModel_->dotCluster(); From 0da94db9e7e863ed58d8517febd7deabe7b69652 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 7 Oct 2024 08:32:59 +0200 Subject: [PATCH 20/21] Remove non-obvious abbreviation from variable name --- demo/src/cluster.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/src/cluster.cpp b/demo/src/cluster.cpp index 062416e0..b9fbff01 100644 --- a/demo/src/cluster.cpp +++ b/demo/src/cluster.cpp @@ -52,12 +52,12 @@ DotClusterFinder::Clusters DotClusterFinder::findDotClusters() const { Positions DotClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapter& mazeAdapter) const { Positions dots; - std::queue bfsQueue; - bfsQueue.push(start.position); + std::queue explorationQueue; + explorationQueue.push(start.position); - while (!bfsQueue.empty()) { - Position currentPosition = bfsQueue.front(); - bfsQueue.pop(); + while (!explorationQueue.empty()) { + Position currentPosition = explorationQueue.front(); + explorationQueue.pop(); ClusterCell& current = mazeAdapter.cell(currentPosition); if (current.visited) { @@ -76,7 +76,7 @@ Positions DotClusterFinder::expandDot(const Cell& start, const ClusterMazeAdapte ClusterCell& neighbor = mazeAdapter.cell(nextPosition); if (neighbor.isConsumable() && !neighbor.visited) { - bfsQueue.push(nextPosition); + explorationQueue.push(nextPosition); } } } From c783b60c0cdef875560320f7a03f31b2cac9912c Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 7 Oct 2024 08:46:31 +0200 Subject: [PATCH 21/21] Generalize unit test to not assume a given order of clusters --- demo/test/cluster.cpp | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/demo/test/cluster.cpp b/demo/test/cluster.cpp index 6ced0d05..72c57c33 100644 --- a/demo/test/cluster.cpp +++ b/demo/test/cluster.cpp @@ -8,6 +8,20 @@ namespace utils::a_star { using namespace demo; +struct ExpectedCluster { + int expectedSize; + Position center; + + bool matches(const Cluster& cluster) const { + return cluster.dots.size() == expectedSize && cluster.center == center; + } +}; + +bool clusterExists(const std::vector& clusters, const ExpectedCluster& expectedCluster) { + return std::any_of( + clusters.begin(), clusters.end(), [&](const Cluster& cluster) { return expectedCluster.matches(cluster); }); +} + class ClusterTest : public ::testing::Test { protected: ClusterTest() : environmentModel_(std::make_shared()) { @@ -25,17 +39,14 @@ TEST_F(ClusterTest, dotClusters) { environmentModel_->setMaze({5, 5}, str); DotClusterFinder dotClusterFinder(environmentModel_->maze()); - std::vector clusters = dotClusterFinder.clusters(); - ASSERT_EQ(clusters.size(), 2); - EXPECT_EQ(clusters.front().dots.size(), 3); - EXPECT_EQ(clusters.front().center.x, 2); - EXPECT_EQ(clusters.front().center.y, 1); - EXPECT_EQ(clusters.back().dots.size(), 2); - EXPECT_EQ(clusters.back().center.x, 1); - EXPECT_EQ(clusters.back().center.y, 3); + ExpectedCluster firstExpectedCluster{3, {2, 1}}; + ExpectedCluster secondExpectedCluster{2, {1, 3}}; + + EXPECT_TRUE(clusterExists(clusters, firstExpectedCluster)); + EXPECT_TRUE(clusterExists(clusters, secondExpectedCluster)); } } // namespace utils::a_star