diff --git a/src/hades_extensions/CMakeLists.txt b/src/hades_extensions/CMakeLists.txt index 579033f4..f4aa6e3a 100644 --- a/src/hades_extensions/CMakeLists.txt +++ b/src/hades_extensions/CMakeLists.txt @@ -1,5 +1,5 @@ # Define some project-wide options -cmake_minimum_required(VERSION 3.25.2) +cmake_minimum_required(VERSION 3.22.1) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/src/hades_extensions/CMakePresets.json b/src/hades_extensions/CMakePresets.json index 2ad5157b..83434c71 100644 --- a/src/hades_extensions/CMakePresets.json +++ b/src/hades_extensions/CMakePresets.json @@ -1,8 +1,8 @@ { "cmakeMinimumRequired": { "major": 3, - "minor": 25, - "patch": 2 + "minor": 22, + "patch": 1 }, "configurePresets": [ { diff --git a/src/hades_extensions/include/generation/map.hpp b/src/hades_extensions/include/generation/map.hpp index 7dd033da..1d6ada7d 100644 --- a/src/hades_extensions/include/generation/map.hpp +++ b/src/hades_extensions/include/generation/map.hpp @@ -7,7 +7,7 @@ // Local headers #include "bsp.hpp" -#include "dijkstra.hpp" +#include "searching.hpp" // ----- STRUCTURES ------------------------------ /// Represents an undirected weighted edge in a graph. @@ -42,26 +42,26 @@ struct std::hash { }; // ----- FUNCTIONS ------------------------------ -/// Places a tile in the 2D grid using a Dijkstra map. +/// Place a random tile in the 2D grid. /// /// @param grid - The 2D grid which represents the dungeon. /// @param random_generator - The random generator used to pick the position. -/// @param item_positions - The positions of all the items in the 2D grid. +/// @param replaceable_tile - The tile to replace in the 2D grid. /// @param target_tile - The tile to place in the 2D grid. /// @param count - The number of tiles to place. -/// @throws std::length_error - If the grid is empty. -void place_tiles(const Grid &grid, std::mt19937 &random_generator, std::unordered_set &item_positions, - TileType target_tile, int count); +[[maybe_unused]] auto place_random_tiles(const Grid &grid, std::mt19937 &random_generator, TileType replaceable_tile, + TileType target_tile, int count = 1) -> std::unordered_set; -/// Place a random tile in the 2D grid. +/// Places a tile in the 2D grid using the Dijkstra map algorithm. /// /// @param grid - The 2D grid which represents the dungeon. /// @param random_generator - The random generator used to pick the position. -/// @param replaceable_tile - The tile to replace in the 2D grid. +/// @param item_positions - The positions of all the items in the 2D grid. /// @param target_tile - The tile to place in the 2D grid. /// @param count - The number of tiles to place. -[[maybe_unused]] auto place_tiles(const Grid &grid, std::mt19937 &random_generator, TileType replaceable_tile, - TileType target_tile, int count = 1) -> std::unordered_set; +/// @throws std::length_error - If the grid is empty. +void place_dijkstra_tiles(const Grid &grid, std::mt19937 &random_generator, + std::unordered_set &item_positions, TileType target_tile, int count); /// Create a complete graph from a given list of rooms. /// @@ -81,7 +81,7 @@ auto create_connections(const std::unordered_map> &compl /// /// @param grid - The 2D grid which represents the dungeon. /// @param connections - The connections to pathfind using the A* algorithm. -void create_hallways(Grid &grid, const std::unordered_set &connections); +void create_hallways(const Grid &grid, const std::unordered_set &connections); /// Generate the game map for a given game level. /// @@ -91,4 +91,5 @@ void create_hallways(Grid &grid, const std::unordered_set &connections); auto create_map(int level, std::optional seed = std::nullopt) -> std::pair, std::tuple>; -// TODO: Go over documentation for whole of generation/ to check for more @throws +// TODO: Go over generation/ documentation for whole of generation/ to check for more @throws +// TODO: Check if exceptions should have full stops diff --git a/src/hades_extensions/include/generation/dijkstra.hpp b/src/hades_extensions/include/generation/searching.hpp similarity index 84% rename from src/hades_extensions/include/generation/dijkstra.hpp rename to src/hades_extensions/include/generation/searching.hpp index c8848908..aa8f63fa 100644 --- a/src/hades_extensions/include/generation/dijkstra.hpp +++ b/src/hades_extensions/include/generation/searching.hpp @@ -19,11 +19,12 @@ /// @return A vector of positions mapping out the shortest path from start to end. auto calculate_astar_path(const Grid &grid, const Position &start, const Position &end) -> std::vector; -/// Get a random Dijkstra map position for the given grid and minimum distance. +/// Generate a random position in a grid using the Dijkstra map algorithm. /// /// @param grid - The grid to generate the Dijkstra map position for. /// @param item_positions - The positions of the items to generate the Dijkstra map position for. /// @param within - Whether to get a position within the minimum distance or not. /// @throws std::length_error - If the grid size is less than 0. /// @return A random Dijkstra map position. -auto generate_dijkstra_map_position(const Grid &grid, const std::unordered_set &item_positions, bool within) -> Position; +auto generate_item_position(const Grid &grid, const std::unordered_set &item_positions, bool within) + -> Position; diff --git a/src/hades_extensions/src/CMakeLists.txt b/src/hades_extensions/src/CMakeLists.txt index df2a8f7b..48a4ebed 100644 --- a/src/hades_extensions/src/CMakeLists.txt +++ b/src/hades_extensions/src/CMakeLists.txt @@ -23,9 +23,9 @@ add_library(${CPP_LIB} STATIC ${CMAKE_SOURCE_DIR}/src/game_objects/systems/movements.cpp ${CMAKE_SOURCE_DIR}/src/game_objects/systems/upgrade.cpp ${CMAKE_SOURCE_DIR}/src/generation/bsp.cpp - ${CMAKE_SOURCE_DIR}/src/generation/dijkstra.cpp ${CMAKE_SOURCE_DIR}/src/generation/map.cpp ${CMAKE_SOURCE_DIR}/src/generation/primitives.cpp + ${CMAKE_SOURCE_DIR}/src/generation/searching.cpp ) target_include_directories(${CPP_LIB} PUBLIC ${CMAKE_SOURCE_DIR}/include) diff --git a/src/hades_extensions/src/generation/map.cpp b/src/hades_extensions/src/generation/map.cpp index f967dc8c..fccdc94f 100644 --- a/src/hades_extensions/src/generation/map.cpp +++ b/src/hades_extensions/src/generation/map.cpp @@ -43,23 +43,8 @@ constexpr MapGenerationConstant ITEM_COUNT{5, 1.1, 30}; map_generation_constant.max_value)); } -void place_tiles(const Grid &grid, std::mt19937 &random_generator, std::unordered_set &item_positions, - const TileType target_tile, const int count) { - // Place each tile using the Dijkstra map - for (int _ = 0; _ < count; _++) { - // Determine if we should select a position within or outside the minimum distance - const bool within_min_distance{std::uniform_real_distribution<>{0, 1}(random_generator) < - WITHIN_MIN_DISTANCE_CHANCE}; - - // Generate the Dijkstra map for the grid and place the tile in a random position - const Position possible_tile{generate_dijkstra_map_position(grid, item_positions, within_min_distance)}; - grid.set_value(possible_tile, target_tile); - item_positions.emplace(possible_tile); - } -} - -auto place_tiles(const Grid &grid, std::mt19937 &random_generator, const TileType replaceable_tile, - const TileType target_tile, const int count) -> std::unordered_set { +auto place_random_tiles(const Grid &grid, std::mt19937 &random_generator, const TileType replaceable_tile, + const TileType target_tile, const int count) -> std::unordered_set { // Get all the positions that match the replaceable tile std::vector replaceable_tiles; for (int y = 0; y < grid.height; y++) { @@ -70,6 +55,11 @@ auto place_tiles(const Grid &grid, std::mt19937 &random_generator, const TileTyp } } + // Check if there are enough replaceable tiles to place the target tile + if (static_cast(replaceable_tiles.size()) < count) { + throw std::length_error("Not enough replaceable tiles to place the target tiles."); + } + // Create a collection to store the item positions then place each tile std::unordered_set item_positions; for (int _ = 0; _ < count; _++) { @@ -84,6 +74,21 @@ auto place_tiles(const Grid &grid, std::mt19937 &random_generator, const TileTyp return item_positions; } +void place_dijkstra_tiles(const Grid &grid, std::mt19937 &random_generator, + std::unordered_set &item_positions, const TileType target_tile, const int count) { + // Place each tile using the Dijkstra map + for (int _ = 0; _ < count; _++) { + // Determine if we should select a position within or outside the minimum distance + const bool within_min_distance{std::uniform_real_distribution<>{0, 1}(random_generator) < + WITHIN_MIN_DISTANCE_CHANCE}; + + // Generate the Dijkstra map for the grid and place the tile in a random position + const Position possible_tile{generate_item_position(grid, item_positions, within_min_distance)}; + grid.set_value(possible_tile, target_tile); + item_positions.emplace(possible_tile); + } +} + auto create_complete_graph(const std::vector &rooms) -> std::unordered_map> { // Check if the rooms vector is empty if (rooms.empty()) { @@ -140,7 +145,7 @@ auto create_connections(const std::unordered_map> &compl return mst; } -void create_hallways(Grid &grid, const std::unordered_set &connections) { +void create_hallways(const Grid &grid, const std::unordered_set &connections) { // Use the A* algorithm to connect each pair of rooms avoiding the obstacles std::vector> path_positions(connections.size()); std::transform(std::execution::par, connections.begin(), connections.end(), path_positions.begin(), @@ -157,8 +162,6 @@ void create_hallways(Grid &grid, const std::unordered_set &connections) { } } -#include - auto create_map(const int level, std::optional seed) -> std::pair, std::tuple> { // Check that the level number is valid @@ -180,21 +183,20 @@ auto create_map(const int level, std::optional seed) Grid grid{grid_width, grid_height}; Leaf bsp{{{0, 0}, {grid_width - 1, grid_height - 1}}}; - // Split the bsp, create the rooms, and create the hallways between the rooms + // Split the bsp and create the rooms std::vector rooms; split(bsp, random_generator); create_room(bsp, grid, random_generator, rooms); // Place random obstacles in the grid then create hallways between the rooms - place_tiles(grid, random_generator, TileType::Empty, TileType::Obstacle, generate_value(OBSTACLE_COUNT, level)); + place_random_tiles(grid, random_generator, TileType::Empty, TileType::Obstacle, + generate_value(OBSTACLE_COUNT, level)); create_hallways(grid, create_connections(create_complete_graph(rooms))); // Place the player as well as the item tiles in the grid - auto item_positions{place_tiles(grid, random_generator, TileType::Floor, TileType::Player)}; - place_tiles(grid, random_generator, item_positions, TileType::Potion, generate_value(ITEM_COUNT, level)); + auto item_positions{place_random_tiles(grid, random_generator, TileType::Floor, TileType::Player)}; + place_dijkstra_tiles(grid, random_generator, item_positions, TileType::Potion, generate_value(ITEM_COUNT, level)); // Return the grid and a LevelConstants object return std::make_pair(*grid.grid, std::make_tuple(level, grid_width, grid_height)); } - -// TODO: Optimise/simplify this whole file and maybe combine place_tiles with a boolean diff --git a/src/hades_extensions/src/generation/dijkstra.cpp b/src/hades_extensions/src/generation/searching.cpp similarity index 94% rename from src/hades_extensions/src/generation/dijkstra.cpp rename to src/hades_extensions/src/generation/searching.cpp index ccec9d71..8625f578 100644 --- a/src/hades_extensions/src/generation/dijkstra.cpp +++ b/src/hades_extensions/src/generation/searching.cpp @@ -1,5 +1,5 @@ // Related header -#include "generation/dijkstra.hpp" +#include "generation/searching.hpp" // Std headers #include @@ -90,10 +90,8 @@ auto calculate_astar_path(const Grid &grid, const Position &start, const Positio return result; } -// TODO: Optimise this and try to combine/simplify them - -auto generate_dijkstra_map_position(const Grid &grid, const std::unordered_set &item_positions, - const bool within) -> Position { +auto generate_item_position(const Grid &grid, const std::unordered_set &item_positions, const bool within) + -> Position { // Check if the grid size is not zero if (grid.width == 0 || grid.height == 0) { throw std::length_error("Grid size must be bigger than 0."); @@ -143,7 +141,7 @@ auto generate_dijkstra_map_position(const Grid &grid, const std::unordered_set

container, left_container); - ASSERT_EQ(*leaf.right->container, right_container); + ASSERT_EQ(*leaf.left->container, Rect({0, 0}, {7, 10})); + ASSERT_EQ(*leaf.right->container, Rect({9, 0}, {15, 10})); } /// Test that the split function correctly splits a leaf horizontally. @@ -39,10 +37,8 @@ TEST_F(BspFixture, TestBspSplitHorizontal) { split(leaf, random_generator); // Make sure the children are correct - const Rect left_container{{0, 0}, {10, 7}}; - const Rect right_container{{0, 9}, {10, 15}}; - ASSERT_EQ(*leaf.left->container, left_container); - ASSERT_EQ(*leaf.right->container, right_container); + ASSERT_EQ(*leaf.left->container, Rect({0, 0}, {10, 7})); + ASSERT_EQ(*leaf.right->container, Rect({0, 9}, {10, 15})); } /// Test that the split function correctly splits a leaf in a random direction. @@ -52,10 +48,8 @@ TEST_F(BspFixture, TestBspSplitRandom) { split(leaf, random_generator); // Make sure the children are correct - const Rect left_container{{0, 0}, {7, 15}}; - const Rect right_container{{9, 0}, {15, 15}}; - ASSERT_EQ(*leaf.left->container, left_container); - ASSERT_EQ(*leaf.right->container, right_container); + ASSERT_EQ(*leaf.left->container, Rect({0, 0}, {7, 15})); + ASSERT_EQ(*leaf.right->container, Rect({9, 0}, {15, 15})); } /// Test that the split function correctly splits a leaf multiple times. @@ -65,18 +59,12 @@ TEST_F(BspFixture, TestBspSplitMultiple) { split(leaf, random_generator); // Make sure the children are correct - const Rect left_container{{0, 0}, {10, 20}}; - const Rect left_left_container{{0, 0}, {10, 11}}; - const Rect left_right_container{{0, 13}, {10, 20}}; - const Rect right_container{{12, 0}, {20, 20}}; - const Rect right_left_container{{12, 0}, {20, 10}}; - const Rect right_right_container{{12, 12}, {20, 20}}; - ASSERT_EQ(*leaf.left->container, left_container); - ASSERT_EQ(*leaf.left->left->container, left_left_container); - ASSERT_EQ(*leaf.left->right->container, left_right_container); - ASSERT_EQ(*leaf.right->container, right_container); - ASSERT_EQ(*leaf.right->left->container, right_left_container); - ASSERT_EQ(*leaf.right->right->container, right_right_container); + ASSERT_EQ(*leaf.left->container, Rect({0, 0}, {10, 20})); + ASSERT_EQ(*leaf.left->left->container, Rect({0, 0}, {10, 11})); + ASSERT_EQ(*leaf.left->right->container, Rect({0, 13}, {10, 20})); + ASSERT_EQ(*leaf.right->container, Rect({12, 0}, {20, 20})); + ASSERT_EQ(*leaf.right->left->container, Rect({12, 0}, {20, 10})); + ASSERT_EQ(*leaf.right->right->container, Rect({12, 12}, {20, 20})); } /// Test that the split function returns if the leaf is already split. @@ -88,10 +76,8 @@ TEST_F(BspFixture, TestBspSplitExistingChildren) { split(leaf, random_generator); // Make sure the children haven't changed - const Rect left_container{{0, 0}, {0, 0}}; - const Rect right_container{{0, 0}, {0, 0}}; - ASSERT_EQ(*leaf.left->container, left_container); - ASSERT_EQ(*leaf.right->container, right_container); + ASSERT_EQ(*leaf.left->container, Rect({0, 0}, {0, 0})); + ASSERT_EQ(*leaf.right->container, Rect({0, 0}, {0, 0})); } /// Test that the split function overwrites the children if only one child exists. @@ -102,10 +88,8 @@ TEST_F(BspFixture, TestBspSplitSingleChild) { split(leaf, random_generator); // Make sure the children are correct - const Rect left_container{{0, 0}, {7, 15}}; - const Rect right_container{{9, 0}, {15, 15}}; - ASSERT_EQ(*leaf.left->container, left_container); - ASSERT_EQ(*leaf.right->container, right_container); + ASSERT_EQ(*leaf.left->container, Rect({0, 0}, {7, 15})); + ASSERT_EQ(*leaf.right->container, Rect({9, 0}, {15, 15})); } /// Test that the split function returns if the leaf is too small to split. @@ -124,8 +108,7 @@ TEST_F(BspFixture, TestBspCreateRoomSingleLeaf) { create_room(leaf, grid, random_generator, rooms); // Make sure the room is correct - const Rect room{{4, 4}, {13, 14}}; - ASSERT_EQ(*leaf.room, room); + ASSERT_EQ(*leaf.room, Rect({4, 4}, {13, 14})); ASSERT_EQ(rooms.size(), 1); } @@ -139,10 +122,8 @@ TEST_F(BspFixture, TestBspCreateRoomChildLeafs) { create_room(leaf, grid, random_generator, rooms); // Make sure the room is correct - const Rect left_room{{2, 0}, {6, 6}}; - const Rect right_room{{9, 4}, {14, 10}}; - ASSERT_EQ(*leaf.left->room, left_room); - ASSERT_EQ(*leaf.right->room, right_room); + ASSERT_EQ(*leaf.left->room, Rect({2, 0}, {6, 6})); + ASSERT_EQ(*leaf.right->room, Rect({9, 4}, {14, 10})); ASSERT_EQ(rooms.size(), 2); } @@ -155,8 +136,7 @@ TEST_F(BspFixture, TestBspCreateRoomExistingRoom) { create_room(leaf, grid, random_generator, rooms); // Make sure the room is correct - const Rect room{{4, 4}, {13, 14}}; - ASSERT_EQ(*leaf.room, room); + ASSERT_EQ(*leaf.room, Rect({4, 4}, {13, 14})); ASSERT_EQ(rooms.size(), 1); } diff --git a/src/hades_extensions/tests/generation/test_dijkstra.cpp b/src/hades_extensions/tests/generation/test_dijkstra.cpp deleted file mode 100644 index 5dd3f290..00000000 --- a/src/hades_extensions/tests/generation/test_dijkstra.cpp +++ /dev/null @@ -1,65 +0,0 @@ -// Local headers -#include "generation/dijkstra.hpp" -#include "macros.hpp" - -// ----- FIXTURES ------------------------------ -/// Implements the fixture for the generation/dijkstra.hpp tests. -class AstarFixture : public testing::Test { - protected: - /// A 2D grid for use in testing. - Grid grid{6, 9}; - - /// A position in the middle of the grid for use in testing. - const Position position_one{3, 7}; - - /// An extra position in the middle of the grid for use in testing. - const Position position_two{4, 1}; - - /// A position on the edge of the grid for use in testing. - const Position position_three{4, 0}; - - /// Add obstacles to the grid for use in testing. - void add_obstacles() const { - grid.set_value({1, 3}, TileType::Obstacle); - grid.set_value({2, 7}, TileType::Obstacle); - grid.set_value({3, 2}, TileType::Obstacle); - grid.set_value({3, 3}, TileType::Obstacle); - grid.set_value({3, 6}, TileType::Obstacle); - grid.set_value({4, 3}, TileType::Obstacle); - grid.set_value({4, 6}, TileType::Obstacle); - } -}; - -// ----- TESTS ------------------------------ -/// Test that A* works in a grid with no obstacles when started in the middle. -TEST_F(AstarFixture, TestCalculateAstarPathNoObstaclesMiddleStart) { - const std::vector no_obstacles_result{{4, 1}, {3, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 6}, {3, 7}}; - ASSERT_EQ(calculate_astar_path(grid, position_one, position_two), no_obstacles_result); -} - -/// Test that A* fails in a grid with no obstacles when ended on the edge. -TEST_F(AstarFixture, TestCalculateAstarPathNoObstaclesBoundaryEnd) { - const std::vector no_obstacles_result{{4, 0}, {3, 1}, {3, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 6}, {3, 7}}; - ASSERT_EQ(calculate_astar_path(grid, position_one, position_three), no_obstacles_result); -} - -/// Test that A* works in a grid with obstacles when started in the middle. -TEST_F(AstarFixture, TestCalculateAstarPathObstaclesMiddleStart) { - add_obstacles(); - const std::vector obstacles_result{{4, 1}, {4, 2}, {5, 3}, {4, 4}, {3, 5}, {2, 6}, {3, 7}}; - ASSERT_EQ(calculate_astar_path(grid, position_one, position_two), obstacles_result); -} - -/// Test that A* fails in a grid with obstacles when ended on the edge. -TEST_F(AstarFixture, TestCalculateAstarPathObstaclesBoundaryEnd) { - add_obstacles(); - const std::vector obstacles_result{{4, 0}, {3, 1}, {2, 2}, {2, 3}, {3, 4}, {2, 5}, {2, 6}, {3, 7}}; - ASSERT_EQ(calculate_astar_path(grid, position_one, position_three), obstacles_result); -} - -/// Test that A* fails in an empty grid. -TEST_F(AstarFixture, TestCalculateAstarPathEmptyGrid) { - const Grid empty_grid{0, 0}; - ASSERT_THROW_MESSAGE(calculate_astar_path(empty_grid, position_one, position_two), std::length_error, - "Grid size must be bigger than 0.") -} diff --git a/src/hades_extensions/tests/generation/test_map.cpp b/src/hades_extensions/tests/generation/test_map.cpp index 04f952e3..b6b83f7b 100644 --- a/src/hades_extensions/tests/generation/test_map.cpp +++ b/src/hades_extensions/tests/generation/test_map.cpp @@ -14,10 +14,13 @@ class MapFixture : public testing::Test { // NOLINT std::mt19937 random_generator; /// An 2D grid for use in testing. - Grid grid{5, 5}; + const Grid grid{5, 5}; /// A large 2D grid for use in testing. - Grid large_grid{8, 8}; + const Grid large_grid{8, 8}; + + /// A very large 2D grid for use in testing. + const Grid very_large_grid{50, 50}; /// A rect that fits inside the grid for use in testing. const Rect rect_one{{0, 1}, {3, 4}}; @@ -30,72 +33,122 @@ class MapFixture : public testing::Test { // NOLINT /// Set up the fixture for the tests. void SetUp() override { random_generator.seed(0); } + + /// Add item and floor tiles to the grid for use in testing. + /// + /// @param items The positions of the items to add. + void add_items_and_floors(const std::unordered_set &items) const { + for (int y = 0; y < very_large_grid.height; y++) { + for (int x = 0; x < very_large_grid.width; x++) { + very_large_grid.set_value({x, y}, !items.contains({x, y}) ? TileType::Floor : TileType::Obstacle); + } + } + } }; // ----- TESTS ------------------------------ -/// Test that finding a tile that exists in the grid returns a vector of positions. -TEST_F(MapFixture, TestMapCollectPositionsExist) { - const std::vector tile_exists_result{{3, 0}, {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 4}}; - for (const Position &position : tile_exists_result) { - grid.set_value(position, TileType::Floor); - } - ASSERT_EQ(collect_positions(grid, TileType::Floor), tile_exists_result); +/// Test that placing a tile randomly in the grid with a count of 0 doesn't do anything. +TEST_F(MapFixture, TestMapPlaceRandomTilesZeroCount) { + ASSERT_EQ(place_random_tiles(grid, random_generator, TileType::Empty, TileType::Obstacle, 0), + std::unordered_set{}); + ASSERT_EQ(std::ranges::count(grid.grid->begin(), grid.grid->end(), TileType::Obstacle), 0); } -/// Test that finding a tile that doesn't exist in the grid returns an empty vector. -TEST_F(MapFixture, TestMapCollectPositionsNoExist) { ASSERT_TRUE(collect_positions(grid, TileType::Player).empty()); } +/// Test that placing a tile randomly in the grid with a count of 1 works correctly. +TEST_F(MapFixture, TestMapPlaceRandomTilesSingleCount) { + const std::unordered_set single_count_result{{3, 2}}; + ASSERT_EQ(place_random_tiles(grid, random_generator, TileType::Empty, TileType::Obstacle, 1), single_count_result); + ASSERT_EQ(std::ranges::count(grid.grid->begin(), grid.grid->end(), TileType::Obstacle), 1); +} + +/// Test that placing a tile randomly in the grid with a count of 3 works correctly. +TEST_F(MapFixture, TestMapPlaceRandomTilesMultipleCount) { + const std::unordered_set multiple_count_result{{3, 2}, {4, 2}, {1, 3}}; + ASSERT_EQ(place_random_tiles(grid, random_generator, TileType::Empty, TileType::Obstacle, 3), multiple_count_result); + ASSERT_EQ(std::ranges::count(grid.grid->begin(), grid.grid->end(), TileType::Obstacle), 3); +} + +/// Test that placing a tile randomly in the grid with no available positions throws an exception. +TEST_F(MapFixture, TestMapPlaceRandomTilesNoAvailablePositions){ + ASSERT_THROW_MESSAGE(place_random_tiles(grid, random_generator, TileType::Wall, TileType::Obstacle, 1), + std::length_error, "Not enough replaceable tiles to place the target tiles.")} + +/// Test that placing a tile randomly in an empty grid throws an exception. +TEST_F(MapFixture, TestMapPlaceRandomTilesEmptyGrid){ + ASSERT_THROW_MESSAGE(place_random_tiles({0, 0}, random_generator, TileType::Empty, TileType::Obstacle, 1), + std::length_error, "Not enough replaceable tiles to place the target tiles.")} + +/// Test that placing a tile using the Dijkstra map with a count of 0 doesn't do anything. +TEST_F(MapFixture, TestMapPlaceDijkstraTilesZeroCount) { + std::unordered_set item_positions{}; + add_items_and_floors(item_positions); + place_dijkstra_tiles(very_large_grid, random_generator, item_positions, TileType::Obstacle, 0); + ASSERT_EQ(item_positions, std::unordered_set{}); + ASSERT_EQ(std::ranges::count(grid.grid->begin(), grid.grid->end(), TileType::Obstacle), 0); +} -/// Test that finding a tile in an empty grid returns an empty vector. -TEST_F(MapFixture, TestMapCollectPositionsEmptyGrid) { - const Grid empty_grid{0, 0}; - ASSERT_TRUE(collect_positions(empty_grid, TileType::Floor).empty()); +/// Test that placing a tile using the Dijkstra map with a count of 1 works correctly. +TEST_F(MapFixture, TestMapPlaceDijkstraTilesSingleCount) { + std::unordered_set item_positions{{2, 2}}; + add_items_and_floors(item_positions); + place_dijkstra_tiles(very_large_grid, random_generator, item_positions, TileType::Obstacle, 1); + ASSERT_EQ(std::ranges::count(very_large_grid.grid->begin(), very_large_grid.grid->end(), TileType::Obstacle), 2); } -/// Test that placing a tile in the grid with available positions works correctly. -TEST_F(MapFixture, TestMapPlaceTileGivenPositions) { - std::vector possible_tiles{{5, 6}, {4, 2}}; - place_tile(grid, random_generator, TileType::Player, possible_tiles); - ASSERT_EQ(std::ranges::count(grid.grid->begin(), grid.grid->end(), TileType::Player), 1); +/// Test that placing a tile using the Dijkstra map with a count of 3 works correctly. +TEST_F(MapFixture, TestMapPlaceDijkstraTilesMultipleCount) { + std::unordered_set item_positions{{2, 2}, {10, 20}}; + add_items_and_floors(item_positions); + place_dijkstra_tiles(very_large_grid, random_generator, item_positions, TileType::Obstacle, 3); + ASSERT_EQ(std::ranges::count(very_large_grid.grid->begin(), very_large_grid.grid->end(), TileType::Obstacle), 5); } -/// Test that placing a tile in the grid with no available positions throws an exception. -TEST_F(MapFixture, TestMapPlaceTileEmpty) { - std::vector possible_tiles; - ASSERT_THROW_MESSAGE(place_tile(grid, random_generator, TileType::Player, possible_tiles), std::length_error, - "Possible tiles size must be bigger than 0.") +/// Test that placing a tile using the Dijkstra map with no floors throws an exception. +TEST_F(MapFixture, TestMapPlaceDijkstraTilesNoFloors) { + std::unordered_set item_positions{}; + ASSERT_THROW_MESSAGE(place_dijkstra_tiles(very_large_grid, random_generator, item_positions, TileType::Obstacle, 1), + std::out_of_range, "Position must be within range") +} + +/// Test that placing a tile using the Dijkstra map with no items doesn't do anything. +TEST_F(MapFixture, TestMapPlaceDijkstraTilesNoItems) { + std::unordered_set item_positions{}; + add_items_and_floors(item_positions); + ASSERT_THROW_MESSAGE(place_dijkstra_tiles(very_large_grid, random_generator, item_positions, TileType::Obstacle, 1), + std::out_of_range, "Position must be within range") +} + +/// Test that placing a tile using the Dijkstra map in an empty grid throws an exception. +TEST_F(MapFixture, TestMapPlaceDijkstraTilesEmptyGrid) { + std::unordered_set item_positions{}; + ASSERT_THROW_MESSAGE(place_dijkstra_tiles({0, 0}, random_generator, item_positions, TileType::Obstacle, 1), + std::length_error, "Grid size must be bigger than 0.") } /// Test that creating a complete graph with a single room works correctly. TEST_F(MapFixture, TestMapCreateCompleteGraphSingleRoom) { - const std::vector rooms{rect_one}; const std::unordered_map> single_room_result{{rect_one, std::vector{}}}; - ASSERT_EQ(create_complete_graph(rooms), single_room_result); + ASSERT_EQ(create_complete_graph({rect_one}), single_room_result); } /// Test that creating a complete graph with multiple rooms works correctly. TEST_F(MapFixture, TestMapCreateCompleteGraphMultipleRooms) { - const std::vector rooms{rect_one, rect_two, rect_three}; const std::unordered_map> multiple_rooms_result{ {rect_one, std::vector{rect_two, rect_three}}, {rect_two, std::vector{rect_one, rect_three}}, {rect_three, std::vector{rect_one, rect_two}}}; - ASSERT_EQ(create_complete_graph(rooms), multiple_rooms_result); + ASSERT_EQ(create_complete_graph({rect_one, rect_two, rect_three}), multiple_rooms_result); } /// Test that creating a complete graph with no rooms throws an exception. -TEST_F(MapFixture, TestMapCreateCompleteGraphNoRooms) { - const std::vector rooms; - ASSERT_THROW_MESSAGE(create_complete_graph(rooms), std::length_error, "Rooms size must be bigger than 0.") -} +TEST_F(MapFixture, TestMapCreateCompleteGraphNoRooms){ + ASSERT_THROW_MESSAGE(create_complete_graph({}), std::length_error, "Rooms size must be bigger than 0.")} /// Test that creating a minimum spanning tree with a valid complete graph works correctly. TEST_F(MapFixture, TestMapCreateConnectionsValidCompleteGraph) { // Create the minimum-spanning tree and check its size - const std::unordered_map> complete_graph{{rect_one, std::vector{rect_two, rect_three}}, - {rect_two, std::vector{rect_one, rect_three}}, - {rect_three, std::vector{rect_one, rect_two}}}; - auto connections{create_connections(complete_graph)}; - const std::unordered_set all_rects{rect_one, rect_two, rect_three}; + auto connections{create_connections( + {{rect_one, {rect_two, rect_three}}, {rect_two, {rect_one, rect_three}}, {rect_three, {rect_one, rect_two}}})}; ASSERT_EQ(connections.size(), 2); // Check that the minimum spanning tree has the correct total cost @@ -104,7 +157,7 @@ TEST_F(MapFixture, TestMapCreateConnectionsValidCompleteGraph) { 4); // Check that every rect can be reached in the minimum spanning tree - for (const auto &rect : all_rects) { + for (const auto &rect : {rect_one, rect_two, rect_three}) { ASSERT_TRUE(std::ranges::any_of(connections.begin(), connections.end(), [&rect](const Edge &edge) { return edge.source == rect || edge.destination == rect; })); @@ -112,17 +165,13 @@ TEST_F(MapFixture, TestMapCreateConnectionsValidCompleteGraph) { } /// Test that creating a minimum spanning tree with an empty complete graph throws an exception. -TEST_F(MapFixture, TestMapCreateConnectionsEmptyCompleteGraph) { - const std::unordered_map> empty_complete_graph; - ASSERT_THROW_MESSAGE(create_connections(empty_complete_graph), std::length_error, - "Complete graph size must be bigger than 0.") -} +TEST_F(MapFixture, TestMapCreateConnectionsEmptyCompleteGraph){ + ASSERT_THROW_MESSAGE(create_connections({}), std::length_error, "Complete graph size must be bigger than 0.")} -/// Test that creating hallways with no obstacles works correctly. -TEST_F(MapFixture, TestMapCreateHallwaysNoObstacles) { - const std::unordered_set connections{{0, rect_one, rect_three}}; - create_hallways(large_grid, random_generator, connections, 0); - const std::vector no_obstacles_result{ +/// Test that creating hallways with a single connection works correctly. +TEST_F(MapFixture, TestMapCreateHallwaysSingleConnection) { + create_hallways(large_grid, {{0, rect_one, rect_three}}); + const std::vector single_connection_result{ TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Floor, @@ -135,24 +184,23 @@ TEST_F(MapFixture, TestMapCreateHallwaysNoObstacles) { TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, }; - ASSERT_EQ(*large_grid.grid, no_obstacles_result); + ASSERT_EQ(*large_grid.grid, single_connection_result); } -/// Test that creating hallways with obstacles works correctly. -TEST_F(MapFixture, TestMapCreateHallwaysObstacles) { - const std::unordered_set connections{{0, rect_one, rect_three}}; - create_hallways(large_grid, random_generator, connections, 5); +/// Test that creating hallways with multiple connections works correctly. +TEST_F(MapFixture, TestMapCreateHallwaysMultipleConnections) { + create_hallways(large_grid, {{0, rect_one, rect_two}, {0, rect_one, rect_three}}); const std::vector obstacles_result{ - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, - TileType::Wall, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Floor, - TileType::Floor, TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, + TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, + TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Floor, + TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, TileType::Empty, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, - TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, TileType::Floor, - TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, - TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, - TileType::Floor, TileType::Wall, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, + TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, + TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, }; ASSERT_EQ(*large_grid.grid, obstacles_result); @@ -160,24 +208,21 @@ TEST_F(MapFixture, TestMapCreateHallwaysObstacles) { /// Test that creating hallways with no connections doesn't do anything. TEST_F(MapFixture, TestMapCreateHallwaysNoConnections) { - const std::unordered_set connections; - create_hallways(large_grid, random_generator, connections, 5); - const std::vector no_obstacles_result{ - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Obstacle, TileType::Obstacle, TileType::Obstacle, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Obstacle, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + create_hallways(large_grid, {}); + const std::vector no_connections_result{ + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, }; - ASSERT_EQ(*large_grid.grid, no_obstacles_result); + ASSERT_EQ(*large_grid.grid, no_connections_result); } /// Test that creating a map with a valid level and seed works correctly. diff --git a/src/hades_extensions/tests/generation/test_primitives.cpp b/src/hades_extensions/tests/generation/test_primitives.cpp index dc94b66b..558977d0 100644 --- a/src/hades_extensions/tests/generation/test_primitives.cpp +++ b/src/hades_extensions/tests/generation/test_primitives.cpp @@ -23,6 +23,42 @@ TEST_F(PrimitivesFixture, TestRectGetDistanceToIdentical) { ASSERT_EQ(rect_one.g /// Test that finding the distance between two different rects works correctly. TEST_F(PrimitivesFixture, TestRectGetDistanceToDifferent) { ASSERT_EQ(rect_one.get_distance_to(rect_two), 2); } +/// Test that a position with two small values is not within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinSmallerXY) { ASSERT_FALSE(grid.is_position_within({-1, -1})); } + +/// Test that a position with a small X value is not within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinSmallerX) { ASSERT_FALSE(grid.is_position_within({-1, 3})); } + +/// Test that a position with a small Y value is not within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinSmallerY) { ASSERT_FALSE(grid.is_position_within({3, -1})); } + +/// Test that a position with two lower boundary values is within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinLowerBoundaryXY) { ASSERT_TRUE(grid.is_position_within({0, 0})); } + +/// Test that a position with a lower boundary X value is within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinLowerBoundaryX) { ASSERT_TRUE(grid.is_position_within({0, 3})); } + +/// Test that a position with a lower boundary Y value is within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinLowerBoundaryY) { ASSERT_TRUE(grid.is_position_within({3, 0})); } + +/// Test that a position with two upper boundary values is within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinUpperBoundaryXY) { ASSERT_TRUE(grid.is_position_within({4, 4})); } + +/// Test that a position with an upper boundary X value is within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinUpperBoundaryX) { ASSERT_TRUE(grid.is_position_within({4, 3})); } + +/// Test that a position with an upper boundary Y value is within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinUpperBoundaryY) { ASSERT_TRUE(grid.is_position_within({3, 4})); } + +/// Test that a position with two large values is not within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinLargerXY) { ASSERT_FALSE(grid.is_position_within({5, 5})); } + +/// Test that a position with a large X value is not within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinLargerX) { ASSERT_FALSE(grid.is_position_within({5, 3})); } + +/// Test that a position with a large Y value is not within the grid. +TEST_F(PrimitivesFixture, TestGridIsPositionWithinLargerY) { ASSERT_FALSE(grid.is_position_within({3, 5})); } + /// Test that a position in the middle of the grid can be converted correctly. TEST_F(PrimitivesFixture, TestGridConvertPositionMiddle) { ASSERT_EQ(grid.convert_position({1, 2}), 11); } @@ -103,8 +139,7 @@ TEST_F(PrimitivesFixture, TestGridPlaceRectValidGrid) { /// Test that placing a rect that doesn't fit in the grid works correctly. TEST_F(PrimitivesFixture, TestGridPlaceRectOutsideGrid) { - const Rect invalid_rect{{0, 0}, {10, 10}}; - grid.place_rect(invalid_rect); + grid.place_rect({{0, 0}, {10, 10}}); const std::vector target_result{ TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, diff --git a/src/hades_extensions/tests/generation/test_searching.cpp b/src/hades_extensions/tests/generation/test_searching.cpp new file mode 100644 index 00000000..89915222 --- /dev/null +++ b/src/hades_extensions/tests/generation/test_searching.cpp @@ -0,0 +1,136 @@ +// Local headers +#include "generation/searching.hpp" +#include "macros.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the generation/searching.hpp tests. +class SearchingFixture : public testing::Test { + protected: + /// A 2D grid for use in testing. + Grid grid{6, 9}; + + /// A position in the middle of the grid for use in testing. + const Position position_one{3, 7}; + + /// An extra position in the middle of the grid for use in testing. + const Position position_two{4, 1}; + + /// A position on the edge of the grid for use in testing. + const Position position_three{4, 0}; + + /// Add obstacles to the grid for use in testing. + void add_obstacles() const { + grid.set_value({1, 3}, TileType::Obstacle); + grid.set_value({2, 7}, TileType::Obstacle); + grid.set_value({3, 2}, TileType::Obstacle); + grid.set_value({3, 3}, TileType::Obstacle); + grid.set_value({3, 6}, TileType::Obstacle); + grid.set_value({4, 3}, TileType::Obstacle); + grid.set_value({4, 6}, TileType::Obstacle); + } + + /// Add item and floor tiles to the grid for use in testing. + /// + /// @param items The positions of the items to add. + /// @param all Whether to add item tiles to all positions. + void add_items_and_floors(const std::unordered_set &items, const bool all = false) const { + for (int y = 0; y < grid.height; y++) { + for (int x = 0; x < grid.width; x++) { + if (all) { + grid.set_value({x, y}, TileType::Obstacle); + } else { + grid.set_value({x, y}, !items.contains({x, y}) ? TileType::Floor : TileType::Obstacle); + } + } + } + } +}; + +// ----- TESTS ------------------------------ +/// Test that A* works in a grid with no obstacles when started in the middle. +TEST_F(SearchingFixture, TestCalculateAstarPathNoObstaclesMiddleStart) { + const std::vector no_obstacles_result{{4, 1}, {3, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_two), no_obstacles_result); +} + +/// Test that A* works in a grid with no obstacles when ended on the edge. +TEST_F(SearchingFixture, TestCalculateAstarPathNoObstaclesBoundaryEnd) { + const std::vector no_obstacles_result{{4, 0}, {3, 1}, {3, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_three), no_obstacles_result); +} + +/// Test that A* works in a grid with obstacles when started in the middle. +TEST_F(SearchingFixture, TestCalculateAstarPathObstaclesMiddleStart) { + add_obstacles(); + const std::vector obstacles_result{{4, 1}, {4, 2}, {5, 3}, {4, 4}, {3, 5}, {2, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_two), obstacles_result); +} + +/// Test that A* works in a grid with obstacles when ended on the edge. +TEST_F(SearchingFixture, TestCalculateAstarPathObstaclesBoundaryEnd) { + add_obstacles(); + const std::vector obstacles_result{{4, 0}, {3, 1}, {2, 2}, {2, 3}, {3, 4}, {2, 5}, {2, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_three), obstacles_result); +} + +/// Test that A* throws an exception in an empty grid. +TEST_F(SearchingFixture, + TestCalculateAstarPathEmptyGrid){ASSERT_THROW_MESSAGE(calculate_astar_path({0, 0}, position_one, position_two), + std::length_error, "Grid size must be bigger than 0.")} + +/// Test that generate_item_position returns a valid position within the minimum distance. +TEST_F(SearchingFixture, TestGenerateItemPositionWithinPosition) { + add_items_and_floors({position_one}); + const auto position_result = generate_item_position(grid, {position_one}, true); + ASSERT_TRUE(position_result != Position(-1, -1)); + const auto [diff_x, diff_y] = position_result - position_one; + ASSERT_TRUE(diff_x <= 5 && diff_y <= 5); +} + +/// Test that generate_item_position returns an invalid position if no valid positions are within the minimum distance. +TEST_F(SearchingFixture, TestGenerateItemPositionWithinNoValidPositions) { + add_items_and_floors({}, true); + ASSERT_EQ(generate_item_position(grid, {position_one}, true), Position(-1, -1)); +} + +/// Test that generate_item_position returns a valid position within the minimum distance if multiple valid item +/// positions are given. +TEST_F(SearchingFixture, TestGenerateItemPositionWithinMultipleItems) { + add_items_and_floors({position_one, position_two, position_three}); + const auto position_result = generate_item_position(grid, {position_one, position_two, position_three}, true); + ASSERT_TRUE(position_result != Position(-1, -1)); + const auto [diff_x, diff_y] = position_result - position_one; + ASSERT_TRUE(diff_x <= 5 && diff_y <= 5); +} + +/// Test that generate_item_position returns a valid position outside the minimum distance. +TEST_F(SearchingFixture, TestGenerateItemPositionOutsidePosition) { + add_items_and_floors({position_one}); + const auto position_result = generate_item_position(grid, {position_one}, false); + ASSERT_TRUE(position_result != Position(-1, -1)); + const auto [diff_x, diff_y] = position_result - position_one; + ASSERT_TRUE(diff_x > 5 || diff_y > 5); +} + +/// Test that generate_item_position returns an invalid position if no valid positions are outside the minimum distance. +TEST_F(SearchingFixture, TestGenerateItemPositionOutsideNoValidPositions) { + add_items_and_floors({}, true); + ASSERT_EQ(generate_item_position(grid, {position_one}, false), Position(-1, -1)); +} + +/// Test that generate_item_position returns a valid position outside the minimum distance if multiple valid item +/// positions are given. +TEST_F(SearchingFixture, TestGenerateItemPositionOutsideMultipleItems) { + add_items_and_floors({position_one, position_two, position_three}); + ASSERT_EQ(generate_item_position(grid, {position_one, position_two, position_three}, false), Position(-1, -1)); +} + +/// Test that generate_item_position returns an invalid position if no item positions are given. +TEST_F(SearchingFixture, TestGenerateItemPositionEmptyItemPositions) { + ASSERT_EQ(generate_item_position(grid, {}, false), Position(-1, -1)); +} + +/// Test that generate_item_position throws an exception in an empty grid. +TEST_F(SearchingFixture, TestGenerateItemPositionEmptyGrid) { + ASSERT_THROW_MESSAGE(generate_item_position({0, 0}, {}, false), std::length_error, "Grid size must be bigger than 0.") +}