Skip to content

Commit

Permalink
Untangle pathfinding dependencies
Browse files Browse the repository at this point in the history
1. Makes `path.cpp` concerned solely with the pathfinding algorithm.
2. Turns `path_test` into a standalone test.
  • Loading branch information
glebm committed Jan 24, 2025
1 parent 4779f27 commit ec055a6
Show file tree
Hide file tree
Showing 18 changed files with 285 additions and 231 deletions.
3 changes: 1 addition & 2 deletions Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion Source/controls/plrctrls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;

Expand Down
122 changes: 18 additions & 104 deletions Source/engine/path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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> {
Expand Down
50 changes: 22 additions & 28 deletions Source/engine/path.h
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand All @@ -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".
*
Expand Down
1 change: 1 addition & 0 deletions Source/engine/render/scrollrt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Source/inv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Source/items.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Source/levels/themes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

0 comments on commit ec055a6

Please sign in to comment.