From ec055a6cdd07a2e4d9fbbab5563d16880c4691f2 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Fri, 24 Jan 2025 04:10:59 +0000 Subject: [PATCH] Untangle pathfinding dependencies 1. Makes `path.cpp` concerned solely with the pathfinding algorithm. 2. Turns `path_test` into a standalone test. --- Source/CMakeLists.txt | 3 +- Source/controls/plrctrls.cpp | 3 +- Source/engine/path.cpp | 122 ++++------------------------ Source/engine/path.h | 50 +++++------- Source/engine/render/scrollrt.cpp | 1 + Source/inv.cpp | 1 + Source/items.cpp | 1 + Source/levels/themes.cpp | 1 + Source/levels/tile_properties.cpp | 88 ++++++++++++++++++++ Source/levels/tile_properties.hpp | 32 ++++++++ Source/lua/modules/dev/monsters.cpp | 1 + Source/missiles.cpp | 1 + Source/monster.cpp | 3 +- Source/objects.cpp | 1 + Source/player.cpp | 5 +- test/CMakeLists.txt | 4 +- test/path_test.cpp | 96 +--------------------- test/tile_properties_test.cpp | 103 +++++++++++++++++++++++ 18 files changed, 285 insertions(+), 231 deletions(-) create mode 100644 Source/levels/tile_properties.cpp create mode 100644 Source/levels/tile_properties.hpp create mode 100644 test/tile_properties_test.cpp diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 6d833094b99d..906e3f6d3763 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -99,6 +99,7 @@ set(libdevilutionx_SRCS levels/reencode_dun_cels.cpp levels/setmaps.cpp levels/themes.cpp + levels/tile_properties.cpp levels/town.cpp levels/trigs.cpp @@ -450,8 +451,6 @@ target_link_dependencies(libdevilutionx_pathfinding PUBLIC tl libdevilutionx_crawl libdevilutionx_direction - libdevilutionx_gendung - libdevilutionx_level_objects ) if(SUPPORTS_MPQ OR NOT NONET) diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 8ce42dae390e..0e7614b04d5c 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -27,6 +27,7 @@ #include "hwcursor.hpp" #include "inv.h" #include "items.h" +#include "levels/tile_properties.hpp" #include "levels/town.h" #include "levels/trigs.h" #include "minitext.h" @@ -127,7 +128,7 @@ int GetDistance(Point destination, int maxDistance) int8_t walkpath[MaxPathLength]; Player &myPlayer = *MyPlayer; - int steps = FindPath([&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, myPlayer.position.future, destination, walkpath); + int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, myPlayer.position.future, destination, walkpath); if (steps > maxDistance) return 0; diff --git a/Source/engine/path.cpp b/Source/engine/path.cpp index 8930ba67bd8c..bfbf5258e4f4 100644 --- a/Source/engine/path.cpp +++ b/Source/engine/path.cpp @@ -5,16 +5,17 @@ */ #include "engine/path.h" +#include #include #include +#include #include #include "appfat.h" #include "crawl.hpp" #include "engine/direction.hpp" -#include "levels/gendung.h" -#include "objects.h" +#include "engine/point.hpp" namespace devilution { namespace { @@ -191,7 +192,7 @@ int GetHeuristicCost(Point startPosition, Point destinationPosition) /** * @brief update all path costs using depth-first search starting at pPath */ -void SetCoords(uint16_t pPath) +void SetCoords(tl::function_ref canStep, uint16_t pPath) { PushActiveStep(pPath); // while there are path nodes to check @@ -204,7 +205,7 @@ void SetCoords(uint16_t pPath) PathNode &pathAct = PathNodes[childIndex]; if (pathOld.g + GetDistance(pathOld.position(), pathAct.position()) < pathAct.g) { - if (CanStep(pathOld.position(), pathAct.position())) { + if (canStep(pathOld.position(), pathAct.position())) { pathAct.parentIndex = pathOldIndex; pathAct.g = pathOld.g + GetDistance(pathOld.position(), pathAct.position()); pathAct.f = pathAct.g + pathAct.h; @@ -215,32 +216,16 @@ void SetCoords(uint16_t pPath) } } -/** - * Returns a number representing the direction from a starting tile to a neighbouring tile. - * - * Used in the pathfinding code, each step direction is assigned a number like this: - * dx - * -1 0 1 - * +----- - * -1|5 1 6 - * dy 0|2 0 3 - * 1|8 4 7 - */ -int8_t GetPathDirection(Point startPosition, Point destinationPosition) -{ - constexpr int8_t PathDirections[9] = { 5, 1, 6, 2, 0, 3, 8, 4, 7 }; - return PathDirections[3 * (destinationPosition.y - startPosition.y) + 4 + destinationPosition.x - startPosition.x]; -} - /** * @brief add a step from pPath to destination, return 1 if successful, and update the frontier/visited nodes accordingly * + * @param canStep specifies whether a step between two adjacent points is allowed * @param pathIndex index of the current path node * @param candidatePosition expected to be a neighbour of the current path node position * @param destinationPosition where we hope to end up * @return true if step successfully added, false if we ran out of nodes to use */ -bool ExploreFrontier(uint16_t pathIndex, Point candidatePosition, Point destinationPosition) +bool ExploreFrontier(tl::function_ref canStep, uint16_t pathIndex, Point candidatePosition, Point destinationPosition) { PathNode &path = PathNodes[pathIndex]; int nextG = path.g + GetDistance(path.position(), candidatePosition); @@ -252,7 +237,7 @@ bool ExploreFrontier(uint16_t pathIndex, Point candidatePosition, Point destinat path.addChild(dxdyIndex); PathNode &dxdy = PathNodes[dxdyIndex]; if (nextG < dxdy.g) { - if (CanStep(path.position(), candidatePosition)) { + if (canStep(path.position(), candidatePosition)) { // we'll explore it later, just update dxdy.parentIndex = pathIndex; dxdy.g = nextG; @@ -265,13 +250,13 @@ bool ExploreFrontier(uint16_t pathIndex, Point candidatePosition, Point destinat if (dxdyIndex != PathNode::InvalidIndex) { path.addChild(dxdyIndex); PathNode &dxdy = PathNodes[dxdyIndex]; - if (nextG < dxdy.g && CanStep(path.position(), candidatePosition)) { + if (nextG < dxdy.g && canStep(path.position(), candidatePosition)) { // update the node dxdy.parentIndex = pathIndex; dxdy.g = nextG; dxdy.f = nextG + dxdy.h; // already explored, so re-update others starting from that node - SetCoords(dxdyIndex); + SetCoords(canStep, dxdyIndex); } } else { // case 3: (dx,dy) is totally new @@ -298,14 +283,14 @@ bool ExploreFrontier(uint16_t pathIndex, Point candidatePosition, Point destinat * * @return false if we ran out of preallocated nodes to use, else true */ -bool GetPath(tl::function_ref posOk, uint16_t pathIndex, Point destination) +bool GetPath(tl::function_ref canStep, tl::function_ref posOk, uint16_t pathIndex, Point destination) { for (Displacement dir : PathDirs) { const PathNode &path = PathNodes[pathIndex]; const Point tile = path.position() + dir; const bool ok = posOk(tile); - if ((ok && CanStep(path.position(), tile)) || (!ok && tile == destination)) { - if (!ExploreFrontier(pathIndex, tile, destination)) + if ((ok && canStep(path.position(), tile)) || (!ok && tile == destination)) { + if (!ExploreFrontier(canStep, pathIndex, tile, destination)) return false; } } @@ -315,62 +300,13 @@ bool GetPath(tl::function_ref posOk, uint16_t pathIndex, Point dest } // namespace -bool IsTileNotSolid(Point position) -{ - if (!InDungeonBounds(position)) { - return false; - } - - return !TileHasAny(position, TileProperties::Solid); -} - -bool IsTileSolid(Point position) -{ - if (!InDungeonBounds(position)) { - return false; - } - - return TileHasAny(position, TileProperties::Solid); -} - -bool IsTileWalkable(Point position, bool ignoreDoors) -{ - Object *object = FindObjectAtPosition(position); - if (object != nullptr) { - if (ignoreDoors && object->isDoor()) { - return true; - } - if (object->_oSolidFlag) { - return false; - } - } - - return IsTileNotSolid(position); -} - -bool IsTileOccupied(Point position) +int8_t GetPathDirection(Point startPosition, Point destinationPosition) { - if (!InDungeonBounds(position)) { - return true; // OOB positions are considered occupied. - } - - if (IsTileSolid(position)) { - return true; - } - if (dMonster[position.x][position.y] != 0) { - return true; - } - if (dPlayer[position.x][position.y] != 0) { - return true; - } - if (IsObjectAtPosition(position)) { - return true; - } - - return false; + constexpr int8_t PathDirections[9] = { 5, 1, 6, 2, 0, 3, 8, 4, 7 }; + return PathDirections[3 * (destinationPosition.y - startPosition.y) + 4 + destinationPosition.x - startPosition.x]; } -int FindPath(tl::function_ref posOk, Point startPosition, Point destinationPosition, int8_t path[MaxPathLength]) +int FindPath(tl::function_ref canStep, tl::function_ref posOk, Point startPosition, Point destinationPosition, int8_t path[MaxPathLength]) { /** * for reconstructing the path after the A* search is done. The longest @@ -413,35 +349,13 @@ int FindPath(tl::function_ref posOk, Point startPosition, Point des return 0; } // ran out of nodes, abort! - if (!GetPath(posOk, nextNodeIndex, destinationPosition)) + if (!GetPath(canStep, posOk, nextNodeIndex, destinationPosition)) return 0; } // frontier is empty, no path! return 0; } -bool CanStep(Point startPosition, Point destinationPosition) -{ - // These checks are written as if working backwards from the destination to the source, given - // both tiles are expected to be adjacent this doesn't matter beyond being a bit confusing - bool rv = true; - switch (GetPathDirection(startPosition, destinationPosition)) { - case 5: // Stepping north - rv = IsTileNotSolid(destinationPosition + Direction::SouthWest) && IsTileNotSolid(destinationPosition + Direction::SouthEast); - break; - case 6: // Stepping east - rv = IsTileNotSolid(destinationPosition + Direction::SouthWest) && IsTileNotSolid(destinationPosition + Direction::NorthWest); - break; - case 7: // Stepping south - rv = IsTileNotSolid(destinationPosition + Direction::NorthEast) && IsTileNotSolid(destinationPosition + Direction::NorthWest); - break; - case 8: // Stepping west - rv = IsTileNotSolid(destinationPosition + Direction::SouthEast) && IsTileNotSolid(destinationPosition + Direction::NorthEast); - break; - } - return rv; -} - std::optional FindClosestValidPosition(tl::function_ref posOk, Point startingPosition, unsigned int minimumRadius, unsigned int maximumRadius) { return Crawl(minimumRadius, maximumRadius, [&](Displacement displacement) -> std::optional { diff --git a/Source/engine/path.h b/Source/engine/path.h index 58fb73dfa6d5..27b9cc167833 100644 --- a/Source/engine/path.h +++ b/Source/engine/path.h @@ -10,43 +10,24 @@ #include +#include "engine/displacement.hpp" #include "engine/point.hpp" -#include "utils/attributes.h" namespace devilution { constexpr size_t MaxPathLength = 25; -bool IsTileNotSolid(Point position); -bool IsTileSolid(Point position); - -/** - * @brief Checks the position is solid or blocked by an object - */ -bool IsTileWalkable(Point position, bool ignoreDoors = false); - -/** - * @brief Checks if the position contains an object, player, monster, or solid dungeon piece - */ -bool IsTileOccupied(Point position); - -/** - * @brief Find the shortest path from startPosition to destinationPosition, using PosOk(Point) to check that each step is a valid position. - * Store the step directions (corresponds to an index in PathDirs) in path, which must have room for 24 steps - */ -int FindPath(tl::function_ref posOk, Point startPosition, Point destinationPosition, int8_t path[MaxPathLength]); - /** - * @brief check if stepping from a given position to a neighbouring tile cuts a corner. - * - * If you step from A to B, both Xs need to be clear: - * - * AX - * XB + * @brief Find the shortest path from `startPosition` to `destinationPosition`. * - * @return true if step is allowed + * @param canStep specifies whether a step between two adjacent points is allowed. + * @param posOk specifies whether a position can be stepped on. + * @param startPosition + * @param destinationPosition + * @param path Resulting path represented as the step directions, which are indices in `PathDirs`. Must have room for `MaxPathLength` steps. + * @return The length of the resulting path, or 0 if there is no valid path. */ -bool CanStep(Point startPosition, Point destinationPosition); +int FindPath(tl::function_ref canStep, tl::function_ref posOk, Point startPosition, Point destinationPosition, int8_t path[MaxPathLength]); /** For iterating over the 8 possible movement directions */ const Displacement PathDirs[8] = { @@ -62,6 +43,19 @@ const Displacement PathDirs[8] = { // clang-format on }; +/** + * Returns a number representing the direction from a starting tile to a neighbouring tile. + * + * Used in the pathfinding code, each step direction is assigned a number like this: + * dx + * -1 0 1 + * +----- + * -1|5 1 6 + * dy 0|2 0 3 + * 1|8 4 7 + */ +[[nodiscard]] int8_t GetPathDirection(Point startPosition, Point destinationPosition); + /** * @brief Searches for the closest position that passes the check in expanding "rings". * diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index d8f3bfcad410..f73b9d50db53 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/Source/engine/render/scrollrt.cpp @@ -36,6 +36,7 @@ #include "inv.h" #include "levels/dun_tile.hpp" #include "levels/gendung.h" +#include "levels/tile_properties.hpp" #include "lighting.h" #include "lua/lua.hpp" #include "minitext.h" diff --git a/Source/inv.cpp b/Source/inv.cpp index e64b8206650b..c7918393b6f4 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -24,6 +24,7 @@ #include "engine/size.hpp" #include "hwcursor.hpp" #include "inv_iterators.hpp" +#include "levels/tile_properties.hpp" #include "levels/town.h" #include "minitext.h" #include "options.h" diff --git a/Source/items.cpp b/Source/items.cpp index 46f955cde4c5..3415b455bd12 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -31,6 +31,7 @@ #include "game_mode.hpp" #include "headless_mode.hpp" #include "inv_iterators.hpp" +#include "levels/tile_properties.hpp" #include "levels/town.h" #include "lighting.h" #include "minitext.h" diff --git a/Source/levels/themes.cpp b/Source/levels/themes.cpp index 2c266dd16f3f..610526842de5 100644 --- a/Source/levels/themes.cpp +++ b/Source/levels/themes.cpp @@ -13,6 +13,7 @@ #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "items.h" +#include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "monster.h" #include "objects.h" diff --git a/Source/levels/tile_properties.cpp b/Source/levels/tile_properties.cpp new file mode 100644 index 000000000000..0e4d914ae918 --- /dev/null +++ b/Source/levels/tile_properties.cpp @@ -0,0 +1,88 @@ +#include "levels/tile_properties.hpp" + +#include "engine/direction.hpp" +#include "engine/path.h" +#include "engine/point.hpp" +#include "gendung.h" +#include "objects.h" + +namespace devilution { + +bool IsTileNotSolid(Point position) +{ + if (!InDungeonBounds(position)) { + return false; + } + + return !TileHasAny(position, TileProperties::Solid); +} + +bool IsTileSolid(Point position) +{ + if (!InDungeonBounds(position)) { + return false; + } + + return TileHasAny(position, TileProperties::Solid); +} + +bool IsTileWalkable(Point position, bool ignoreDoors) +{ + Object *object = FindObjectAtPosition(position); + if (object != nullptr) { + if (ignoreDoors && object->isDoor()) { + return true; + } + if (object->_oSolidFlag) { + return false; + } + } + + return IsTileNotSolid(position); +} + +bool IsTileOccupied(Point position) +{ + if (!InDungeonBounds(position)) { + return true; // OOB positions are considered occupied. + } + + if (IsTileSolid(position)) { + return true; + } + if (dMonster[position.x][position.y] != 0) { + return true; + } + if (dPlayer[position.x][position.y] != 0) { + return true; + } + if (IsObjectAtPosition(position)) { + return true; + } + + return false; +} + +bool CanStep(Point startPosition, Point destinationPosition) +{ + // These checks are written as if working backwards from the destination to the source, given + // both tiles are expected to be adjacent this doesn't matter beyond being a bit confusing + bool rv = true; + switch (GetPathDirection(startPosition, destinationPosition)) { + case 5: // Stepping north + rv = IsTileNotSolid(destinationPosition + Direction::SouthWest) && IsTileNotSolid(destinationPosition + Direction::SouthEast); + break; + case 6: // Stepping east + rv = IsTileNotSolid(destinationPosition + Direction::SouthWest) && IsTileNotSolid(destinationPosition + Direction::NorthWest); + break; + case 7: // Stepping south + rv = IsTileNotSolid(destinationPosition + Direction::NorthEast) && IsTileNotSolid(destinationPosition + Direction::NorthWest); + break; + case 8: // Stepping west + rv = IsTileNotSolid(destinationPosition + Direction::SouthEast) && IsTileNotSolid(destinationPosition + Direction::NorthEast); + break; + } + return rv; +} + +} // namespace devilution diff --git a/Source/levels/tile_properties.hpp b/Source/levels/tile_properties.hpp new file mode 100644 index 000000000000..43b23d3ff931 --- /dev/null +++ b/Source/levels/tile_properties.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "engine/point.hpp" + +namespace devilution { + +[[nodiscard]] bool IsTileNotSolid(Point position); +[[nodiscard]] bool IsTileSolid(Point position); + +/** + * @brief Checks the position is solid or blocked by an object + */ +[[nodiscard]] bool IsTileWalkable(Point position, bool ignoreDoors = false); + +/** + * @brief Checks if the position contains an object, player, monster, or solid dungeon piece + */ +[[nodiscard]] bool IsTileOccupied(Point position); + +/** + * @brief check if stepping from a given position to a neighbouring tile cuts a corner. + * + * If you step from A to B, both Xs need to be clear: + * + * AX + * XB + * + * @return true if step is allowed + */ +[[nodiscard]] bool CanStep(Point startPosition, Point destinationPosition); + +} // namespace devilution diff --git a/Source/lua/modules/dev/monsters.cpp b/Source/lua/modules/dev/monsters.cpp index 4d3e869da7c0..d9ee171fa1f8 100644 --- a/Source/lua/modules/dev/monsters.cpp +++ b/Source/lua/modules/dev/monsters.cpp @@ -8,6 +8,7 @@ #include "crawl.hpp" #include "levels/gendung.h" +#include "levels/tile_properties.hpp" #include "lighting.h" #include "lua/metadoc.hpp" #include "monstdat.h" diff --git a/Source/missiles.cpp b/Source/missiles.cpp index 9b17f9ddbcba..a3010519516f 100644 --- a/Source/missiles.cpp +++ b/Source/missiles.cpp @@ -27,6 +27,7 @@ #include "headless_mode.hpp" #include "inv.h" #include "levels/dun_tile.hpp" +#include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "lighting.h" #include "monster.h" diff --git a/Source/monster.cpp b/Source/monster.cpp index 78a6890eb1d1..fcf4c5600963 100644 --- a/Source/monster.cpp +++ b/Source/monster.cpp @@ -36,6 +36,7 @@ #include "levels/crypt.h" #include "levels/drlg_l4.h" #include "levels/themes.h" +#include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "lighting.h" #include "minitext.h" @@ -1694,7 +1695,7 @@ bool AiPlanWalk(Monster &monster) /** Maps from walking path step to facing direction. */ const Direction plr2monst[9] = { Direction::South, Direction::NorthEast, Direction::NorthWest, Direction::SouthEast, Direction::SouthWest, Direction::North, Direction::East, Direction::South, Direction::West }; - if (FindPath([&monster](Point position) { return IsTileAccessible(monster, position); }, monster.position.tile, monster.enemyPosition, path) == 0) { + if (FindPath(CanStep, [&monster](Point position) { return IsTileAccessible(monster, position); }, monster.position.tile, monster.enemyPosition, path) == 0) { return false; } diff --git a/Source/objects.cpp b/Source/objects.cpp index 4ccd53f582f5..8b5a35dcf289 100644 --- a/Source/objects.cpp +++ b/Source/objects.cpp @@ -33,6 +33,7 @@ #include "levels/drlg_l4.h" #include "levels/setmaps.h" #include "levels/themes.h" +#include "levels/tile_properties.hpp" #include "lighting.h" #include "minitext.h" #include "missiles.h" diff --git a/Source/player.cpp b/Source/player.cpp index d0b2edb93a7d..8325f90a07a8 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -30,6 +30,7 @@ #include "headless_mode.hpp" #include "help.h" #include "inv_iterators.hpp" +#include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "lighting.h" #include "loadsave.h" @@ -1923,7 +1924,7 @@ void Player::UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1 if (minimalWalkDistance >= 0 && position.future != point) { int8_t testWalkPath[MaxPathLength]; - int steps = FindPath([this](Point position) { return PosOkPlayer(*this, position); }, position.future, point, testWalkPath); + int steps = FindPath(CanStep, [this](Point position) { return PosOkPlayer(*this, position); }, position.future, point, testWalkPath); if (steps == 0) { // Can't walk to desired location => stand still return; @@ -3056,7 +3057,7 @@ void MakePlrPath(Player &player, Point targetPosition, bool endspace) return; } - int path = FindPath([&player](Point position) { return PosOkPlayer(player, position); }, player.position.future, targetPosition, player.walkpath); + int path = FindPath(CanStep, [&player](Point position) { return PosOkPlayer(player, position); }, player.position.future, targetPosition, player.walkpath); if (path == 0) { return; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a4a1e98de3f0..a3a458c6228c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,13 +27,13 @@ set(tests math_test missiles_test pack_test - path_test player_test quests_test random_test rectangle_test scrollrt_test stores_test + tile_properties_test timedemo_test writehero_test ) @@ -45,6 +45,7 @@ set(standalone_tests format_int_test ini_test parse_int_test + path_test str_cat_test utf8_test ) @@ -97,6 +98,7 @@ target_link_dependencies(file_util_test PRIVATE libdevilutionx_file_util app_fat target_link_dependencies(format_int_test PRIVATE libdevilutionx_format_int language_for_testing) target_link_dependencies(ini_test PRIVATE libdevilutionx_ini app_fatal_for_testing) target_link_dependencies(parse_int_test PRIVATE libdevilutionx_parse_int) +target_link_dependencies(path_test PRIVATE libdevilutionx_pathfinding libdevilutionx_direction app_fatal_for_testing) target_link_dependencies(str_cat_test PRIVATE libdevilutionx_strings) target_link_dependencies(utf8_test PRIVATE libdevilutionx_utf8) diff --git a/test/path_test.cpp b/test/path_test.cpp index a4821d6fe51e..3874a75f4ca9 100644 --- a/test/path_test.cpp +++ b/test/path_test.cpp @@ -7,9 +7,6 @@ #include #include "engine/direction.hpp" -#include "levels/dun_tile.hpp" -#include "levels/gendung.h" -#include "objects.h" #include "utils/algorithm/container.hpp" namespace devilution { @@ -52,65 +49,6 @@ TEST(PathTest, Heuristics) EXPECT_EQ(TestPathGetHeuristicCost(source, destination), 4) << "Wrong cost for travelling to a { 2, 0 } offset"; } -TEST(PathTest, Solid) -{ - dPiece[5][5] = 0; - SOLData[0] = TileProperties::Solid; - EXPECT_TRUE(IsTileSolid({ 5, 5 })) << "Solid in-bounds tiles are solid"; - EXPECT_FALSE(IsTileNotSolid({ 5, 5 })) << "IsTileNotSolid returns the inverse of IsTileSolid for in-bounds tiles"; - - dPiece[6][6] = 1; - SOLData[1] = TileProperties::None; - EXPECT_FALSE(IsTileSolid({ 6, 6 })) << "Non-solid in-bounds tiles are not solid"; - EXPECT_TRUE(IsTileNotSolid({ 6, 6 })) << "IsTileNotSolid returns the inverse of IsTileSolid for in-bounds tiles"; - - EXPECT_FALSE(IsTileSolid({ -1, 1 })) << "Out of bounds tiles are not solid"; // this reads out of bounds in the current code and may fail unexpectedly - EXPECT_FALSE(IsTileNotSolid({ -1, 1 })) << "Out of bounds tiles are also not not solid"; -} - -TEST(PathTest, CanStepTest) -{ - dPiece[0][0] = 0; - dPiece[0][1] = 0; - dPiece[1][0] = 0; - dPiece[1][1] = 0; - SOLData[0] = TileProperties::None; - EXPECT_TRUE(CanStep({ 0, 0 }, { 1, 1 })) << "A step in open space is free of solid pieces"; - EXPECT_TRUE(CanStep({ 1, 1 }, { 0, 0 })) << "A step in open space is free of solid pieces"; - EXPECT_TRUE(CanStep({ 1, 0 }, { 0, 1 })) << "A step in open space is free of solid pieces"; - EXPECT_TRUE(CanStep({ 0, 1 }, { 1, 0 })) << "A step in open space is free of solid pieces"; - - SOLData[1] = TileProperties::Solid; - dPiece[1][0] = 1; - EXPECT_TRUE(CanStep({ 0, 1 }, { 1, 0 })) << "Can path to a destination which is solid"; - EXPECT_TRUE(CanStep({ 1, 0 }, { 0, 1 })) << "Can path from a starting position which is solid"; - EXPECT_TRUE(CanStep({ 0, 1 }, { 1, 1 })) << "Stepping in a cardinal direction ignores solid pieces"; - EXPECT_TRUE(CanStep({ 1, 0 }, { 1, 1 })) << "Stepping in a cardinal direction ignores solid pieces"; - EXPECT_TRUE(CanStep({ 0, 0 }, { 1, 0 })) << "Stepping in a cardinal direction ignores solid pieces"; - EXPECT_TRUE(CanStep({ 1, 1 }, { 1, 0 })) << "Stepping in a cardinal direction ignores solid pieces"; - - EXPECT_FALSE(CanStep({ 0, 0 }, { 1, 1 })) << "Can't cut a solid corner"; - EXPECT_FALSE(CanStep({ 1, 1 }, { 0, 0 })) << "Can't cut a solid corner"; - dPiece[0][1] = 1; - EXPECT_FALSE(CanStep({ 0, 0 }, { 1, 1 })) << "Can't walk through the boundary between two corners"; - EXPECT_FALSE(CanStep({ 1, 1 }, { 0, 0 })) << "Can't walk through the boundary between two corners"; - dPiece[1][0] = 0; - EXPECT_FALSE(CanStep({ 0, 0 }, { 1, 1 })) << "Can't cut a solid corner"; - EXPECT_FALSE(CanStep({ 1, 1 }, { 0, 0 })) << "Can't cut a solid corner"; - dPiece[0][1] = 0; - - dPiece[0][0] = 1; - EXPECT_FALSE(CanStep({ 1, 0 }, { 0, 1 })) << "Can't cut a solid corner"; - EXPECT_FALSE(CanStep({ 0, 1 }, { 1, 0 })) << "Can't cut a solid corner"; - dPiece[1][1] = 1; - EXPECT_FALSE(CanStep({ 1, 0 }, { 0, 1 })) << "Can't walk through the boundary between two corners"; - EXPECT_FALSE(CanStep({ 0, 1 }, { 1, 0 })) << "Can't walk through the boundary between two corners"; - dPiece[0][0] = 0; - EXPECT_FALSE(CanStep({ 1, 0 }, { 0, 1 })) << "Can't cut a solid corner"; - EXPECT_FALSE(CanStep({ 0, 1 }, { 1, 0 })) << "Can't cut a solid corner"; - dPiece[1][1] = 0; -} - // These symbols are in terms of coordinates (not in terms of on-screen direction). // -1, -1 is top-left. enum class Dir { @@ -152,7 +90,10 @@ std::vector ToSyms(std::span indices) void CheckPath(Point startPosition, Point destinationPosition, std::vector expectedSteps) { int8_t pathSteps[MaxPathLength]; - auto pathLength = FindPath([](Point) { return true; }, startPosition, destinationPosition, pathSteps); + auto pathLength = FindPath( + /*canStep=*/[](Point, Point) { return true; }, + /*posOk=*/[](Point) { return true; }, + startPosition, destinationPosition, pathSteps); EXPECT_THAT(ToSyms(std::span(pathSteps, pathLength)), ElementsAreArray(ToSyms(expectedSteps))) << "Path steps differs from expectation for a path from " << startPosition << " to " << destinationPosition; @@ -217,35 +158,6 @@ TEST(PathTest, LongPaths) CheckPath(startingPosition, startingPosition + Displacement { 25, 25 }, {}); } -TEST(PathTest, Walkable) -{ - dPiece[5][5] = 0; - SOLData[0] = TileProperties::Solid; // Doing this manually to save running through the code in gendung.cpp - EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile which is marked as solid should be considered blocked"; - EXPECT_FALSE(IsTileWalkable({ 5, 5 }, true)) << "Solid non-door tiles remain unwalkable when ignoring doors"; - - SOLData[0] = TileProperties::None; - EXPECT_TRUE(IsTileWalkable({ 5, 5 })) << "Non-solid tiles are walkable"; - EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Non-solid tiles remain walkable when ignoring doors"; - - dObject[5][5] = 1; - Objects[0]._oSolidFlag = true; - EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile occupied by a solid object is unwalkable"; - EXPECT_FALSE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a solid non-door object are unwalkable when ignoring doors"; - - Objects[0]._otype = _object_id::OBJ_L1LDOOR; - EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile occupied by a door which is marked as solid should be considered blocked"; - EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a door is considered walkable when ignoring doors"; - - Objects[0]._oSolidFlag = false; - EXPECT_TRUE(IsTileWalkable({ 5, 5 })) << "Tile occupied by an open door is walkable"; - EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a door is considered walkable when ignoring doors"; - - SOLData[0] = TileProperties::Solid; - EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Solid tiles occupied by an open door remain unwalkable"; - EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Solid tiles occupied by an open door become walkable when ignoring doors"; -} - TEST(PathTest, FindClosest) { { diff --git a/test/tile_properties_test.cpp b/test/tile_properties_test.cpp new file mode 100644 index 000000000000..56d5024590ba --- /dev/null +++ b/test/tile_properties_test.cpp @@ -0,0 +1,103 @@ +#include "levels/tile_properties.hpp" + +#include +#include + +#include "levels/dun_tile.hpp" +#include "levels/gendung.h" +#include "objdat.h" +#include "objects.h" + +namespace devilution { +namespace { + +TEST(TilePropertiesTest, Solid) +{ + dPiece[5][5] = 0; + SOLData[0] = TileProperties::Solid; + EXPECT_TRUE(IsTileSolid({ 5, 5 })) << "Solid in-bounds tiles are solid"; + EXPECT_FALSE(IsTileNotSolid({ 5, 5 })) << "IsTileNotSolid returns the inverse of IsTileSolid for in-bounds tiles"; + + dPiece[6][6] = 1; + SOLData[1] = TileProperties::None; + EXPECT_FALSE(IsTileSolid({ 6, 6 })) << "Non-solid in-bounds tiles are not solid"; + EXPECT_TRUE(IsTileNotSolid({ 6, 6 })) << "IsTileNotSolid returns the inverse of IsTileSolid for in-bounds tiles"; + + EXPECT_FALSE(IsTileSolid({ -1, 1 })) << "Out of bounds tiles are not solid"; // this reads out of bounds in the current code and may fail unexpectedly + EXPECT_FALSE(IsTileNotSolid({ -1, 1 })) << "Out of bounds tiles are also not not solid"; +} + +TEST(TilePropertiesTest, Walkable) +{ + dPiece[5][5] = 0; + SOLData[0] = TileProperties::Solid; // Doing this manually to save running through the code in gendung.cpp + EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile which is marked as solid should be considered blocked"; + EXPECT_FALSE(IsTileWalkable({ 5, 5 }, true)) << "Solid non-door tiles remain unwalkable when ignoring doors"; + + SOLData[0] = TileProperties::None; + EXPECT_TRUE(IsTileWalkable({ 5, 5 })) << "Non-solid tiles are walkable"; + EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Non-solid tiles remain walkable when ignoring doors"; + + dObject[5][5] = 1; + Objects[0]._oSolidFlag = true; + EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile occupied by a solid object is unwalkable"; + EXPECT_FALSE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a solid non-door object are unwalkable when ignoring doors"; + + Objects[0]._otype = _object_id::OBJ_L1LDOOR; + EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Tile occupied by a door which is marked as solid should be considered blocked"; + EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a door is considered walkable when ignoring doors"; + + Objects[0]._oSolidFlag = false; + EXPECT_TRUE(IsTileWalkable({ 5, 5 })) << "Tile occupied by an open door is walkable"; + EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Tile occupied by a door is considered walkable when ignoring doors"; + + SOLData[0] = TileProperties::Solid; + EXPECT_FALSE(IsTileWalkable({ 5, 5 })) << "Solid tiles occupied by an open door remain unwalkable"; + EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Solid tiles occupied by an open door become walkable when ignoring doors"; +} + +TEST(TilePropertiesTest, CanStepTest) +{ + dPiece[0][0] = 0; + dPiece[0][1] = 0; + dPiece[1][0] = 0; + dPiece[1][1] = 0; + SOLData[0] = TileProperties::None; + EXPECT_TRUE(CanStep({ 0, 0 }, { 1, 1 })) << "A step in open space is free of solid pieces"; + EXPECT_TRUE(CanStep({ 1, 1 }, { 0, 0 })) << "A step in open space is free of solid pieces"; + EXPECT_TRUE(CanStep({ 1, 0 }, { 0, 1 })) << "A step in open space is free of solid pieces"; + EXPECT_TRUE(CanStep({ 0, 1 }, { 1, 0 })) << "A step in open space is free of solid pieces"; + + SOLData[1] = TileProperties::Solid; + dPiece[1][0] = 1; + EXPECT_TRUE(CanStep({ 0, 1 }, { 1, 0 })) << "Can path to a destination which is solid"; + EXPECT_TRUE(CanStep({ 1, 0 }, { 0, 1 })) << "Can path from a starting position which is solid"; + EXPECT_TRUE(CanStep({ 0, 1 }, { 1, 1 })) << "Stepping in a cardinal direction ignores solid pieces"; + EXPECT_TRUE(CanStep({ 1, 0 }, { 1, 1 })) << "Stepping in a cardinal direction ignores solid pieces"; + EXPECT_TRUE(CanStep({ 0, 0 }, { 1, 0 })) << "Stepping in a cardinal direction ignores solid pieces"; + EXPECT_TRUE(CanStep({ 1, 1 }, { 1, 0 })) << "Stepping in a cardinal direction ignores solid pieces"; + + EXPECT_FALSE(CanStep({ 0, 0 }, { 1, 1 })) << "Can't cut a solid corner"; + EXPECT_FALSE(CanStep({ 1, 1 }, { 0, 0 })) << "Can't cut a solid corner"; + dPiece[0][1] = 1; + EXPECT_FALSE(CanStep({ 0, 0 }, { 1, 1 })) << "Can't walk through the boundary between two corners"; + EXPECT_FALSE(CanStep({ 1, 1 }, { 0, 0 })) << "Can't walk through the boundary between two corners"; + dPiece[1][0] = 0; + EXPECT_FALSE(CanStep({ 0, 0 }, { 1, 1 })) << "Can't cut a solid corner"; + EXPECT_FALSE(CanStep({ 1, 1 }, { 0, 0 })) << "Can't cut a solid corner"; + dPiece[0][1] = 0; + + dPiece[0][0] = 1; + EXPECT_FALSE(CanStep({ 1, 0 }, { 0, 1 })) << "Can't cut a solid corner"; + EXPECT_FALSE(CanStep({ 0, 1 }, { 1, 0 })) << "Can't cut a solid corner"; + dPiece[1][1] = 1; + EXPECT_FALSE(CanStep({ 1, 0 }, { 0, 1 })) << "Can't walk through the boundary between two corners"; + EXPECT_FALSE(CanStep({ 0, 1 }, { 1, 0 })) << "Can't walk through the boundary between two corners"; + dPiece[0][0] = 0; + EXPECT_FALSE(CanStep({ 1, 0 }, { 0, 1 })) << "Can't cut a solid corner"; + EXPECT_FALSE(CanStep({ 0, 1 }, { 1, 0 })) << "Can't cut a solid corner"; + dPiece[1][1] = 0; +} + +} // namespace +} // namespace devilution