Browse Source

Untangle pathfinding dependencies

1. Makes `path.cpp` concerned solely with the pathfinding algorithm.
2. Turns `path_test` into a standalone test.
pull/7673/head
Gleb Mazovetskiy 1 year ago
parent
commit
ee16071761
  1. 3
      Source/CMakeLists.txt
  2. 3
      Source/controls/plrctrls.cpp
  3. 122
      Source/engine/path.cpp
  4. 50
      Source/engine/path.h
  5. 1
      Source/engine/render/scrollrt.cpp
  6. 1
      Source/inv.cpp
  7. 1
      Source/items.cpp
  8. 1
      Source/levels/themes.cpp
  9. 88
      Source/levels/tile_properties.cpp
  10. 32
      Source/levels/tile_properties.hpp
  11. 1
      Source/lua/modules/dev/monsters.cpp
  12. 1
      Source/missiles.cpp
  13. 3
      Source/monster.cpp
  14. 1
      Source/objects.cpp
  15. 5
      Source/player.cpp
  16. 4
      test/CMakeLists.txt
  17. 97
      test/path_test.cpp
  18. 103
      test/tile_properties_test.cpp

3
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)

3
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;

122
Source/engine/path.cpp

@ -5,16 +5,17 @@
*/
#include "engine/path.h"
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <function_ref.hpp>
#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<bool(Point, Point)> 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<bool(Point, Point)> 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<bool(Point)> posOk, uint16_t pathIndex, Point destination)
bool GetPath(tl::function_ref<bool(Point, Point)> canStep, tl::function_ref<bool(Point)> 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<bool(Point)> 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<bool(Point)> posOk, Point startPosition, Point destinationPosition, int8_t path[MaxPathLength])
int FindPath(tl::function_ref<bool(Point, Point)> canStep, tl::function_ref<bool(Point)> 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<bool(Point)> 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<Point> FindClosestValidPosition(tl::function_ref<bool(Point)> posOk, Point startingPosition, unsigned int minimumRadius, unsigned int maximumRadius)
{
return Crawl(minimumRadius, maximumRadius, [&](Displacement displacement) -> std::optional<Point> {

50
Source/engine/path.h

@ -10,43 +10,24 @@
#include <function_ref.hpp>
#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<bool(Point)> 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<bool(Point, Point)> canStep, tl::function_ref<bool(Point)> 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".
*

1
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"

1
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"

1
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"

1
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"

88
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

32
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

1
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"

1
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"

3
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;
}

1
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"

5
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;
}

4
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)

97
test/path_test.cpp

@ -1,15 +1,13 @@
#include "engine/path.h"
#include <algorithm>
#include <array>
#include <span>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#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 +50,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 +91,10 @@ std::vector<Dir> ToSyms(std::span<const int8_t> indices)
void CheckPath(Point startPosition, Point destinationPosition, std::vector<std::string> 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<const int8_t>(pathSteps, pathLength)), ElementsAreArray(ToSyms(expectedSteps)))
<< "Path steps differs from expectation for a path from "
<< startPosition << " to " << destinationPosition;
@ -217,35 +159,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)
{
{

103
test/tile_properties_test.cpp

@ -0,0 +1,103 @@
#include "levels/tile_properties.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#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
Loading…
Cancel
Save