Browse Source

Introduce FindClosestValidPosition function

The pre-calculated crawl table is replaced with partially unrolled loops to handle the special cases covered by the table.

Arbitrary limit of 50 placed to allow using this function for searches where vanilla logic would check up to (±49, ±49).
pull/3431/head
ephphatha 4 years ago committed by Anders Jenbo
parent
commit
730f0e65d5
  1. 139
      Source/path.cpp
  2. 22
      Source/path.h
  3. 17
      Source/player.cpp
  4. 107
      test/path_test.cpp

139
Source/path.cpp

@ -5,6 +5,8 @@
*/
#include "path.h"
#include <array>
#include "gendung.h"
#include "objects.h"
@ -384,6 +386,143 @@ bool path_solid_pieces(Point startPosition, Point destinationPosition)
return rv;
}
std::optional<Point> FindClosestValidPosition(const std::function<bool(Point)> &posOk, Point startingPosition, unsigned int minimumRadius, unsigned int maximumRadius)
{
if (minimumRadius > maximumRadius) {
return {}; // No valid search space with the given params.
}
if (minimumRadius == 0U) {
if (posOk(startingPosition)) {
return startingPosition;
}
}
if (minimumRadius <= 1U && maximumRadius >= 1U) {
// unrolling the case for radius 1 to save having to guard the corner check in the loop below.
Point candidatePosition = startingPosition + Direction::SouthWest;
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Direction::NorthEast;
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Direction::NorthWest;
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Direction::SouthEast;
if (posOk(candidatePosition)) {
return candidatePosition;
}
}
if (maximumRadius >= 2U) {
for (int i = static_cast<int>(std::max(minimumRadius, 2U)); i <= static_cast<int>(std::min(maximumRadius, 50U)); i++) {
int x = 0;
int y = i;
// special case the checks when x == 0 to save checking the same tiles twice
Point candidatePosition = startingPosition + Displacement { x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, -y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
while (x < i - 1) {
x++;
candidatePosition = startingPosition + Displacement { -x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { -x, -y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, -y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
}
// special case for inset corners
y--;
candidatePosition = startingPosition + Displacement { -x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { -x, -y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, -y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
x++;
while (y > 0) {
candidatePosition = startingPosition + Displacement { -x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { -x, -y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, -y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
y--;
}
// as above, special case for y == 0
candidatePosition = startingPosition + Displacement { -x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
candidatePosition = startingPosition + Displacement { x, y };
if (posOk(candidatePosition)) {
return candidatePosition;
}
}
}
return {};
}
#ifdef RUN_TESTS
int TestPathGetHeuristicCost(Point startPosition, Point destinationPosition)
{

22
Source/path.h

@ -11,6 +11,7 @@
#include "engine/direction.hpp"
#include "engine/point.hpp"
#include "utils/stdcompat/optional.hpp"
namespace devilution {
@ -66,4 +67,25 @@ const Displacement PathDirs[8] = {
// clang-format on
};
/**
* @brief Searches for the closest position that passes the check in expanding "rings".
*
* The search space is roughly equivalent to a square of tiles where the walking distance is equal to the radius except
* the corners are "rounded" (inset) by one tile. For example the following is a search space of radius 4:
* _XXXXXXX_
* XX_____XX
* X_______X
* < snip >
* X_______X
* XX_____XX
* _XXXXXXX_
*
* @param posOk Used to check if a position is valid
* @param startingPosition dungeon tile location to start the search from
* @param minimumRadius A value from 0 to 50, allows skipping nearby tiles (e.g. specify radius 1 to skip checking the starting tile)
* @param maximumRadius The maximum distance to check, defaults to 18 for vanilla compatibility but supports values up to 50
* @return either the closest valid point or an empty optional
*/
std::optional<Point> FindClosestValidPosition(const std::function<bool(Point)> &posOk, Point startingPosition, unsigned int minimumRadius = 0, unsigned int maximumRadius = 18);
} // namespace devilution

17
Source/player.cpp

@ -3591,16 +3591,15 @@ void SyncInitPlrPos(int pnum)
return position;
}
for (int k : CrawlNum) {
int ck = k + 2;
for (auto j = static_cast<uint8_t>(CrawlTable[k]); j > 0; j--, ck += 2) {
Point position = player.position.tile + Displacement { CrawlTable[ck - 1], CrawlTable[ck] };
if (PosOkPlayer(player, position) && !PosOkPortal(currlevel, position.x, position.y))
return position;
}
}
std::optional<Point> nearPosition = FindClosestValidPosition(
[&player](Point testPosition) {
return PosOkPlayer(player, testPosition) && !PosOkPortal(currlevel, testPosition.x, testPosition.y);
},
player.position.tile,
1, // skip the starting tile since that was checked in the previous loop
50);
return Point { 0, 0 };
return nearPosition.value_or(Point { 0, 0 });
}();
player.position.tile = position;

107
test/path_test.cpp

@ -165,4 +165,111 @@ TEST(PathTest, Walkable)
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)
{
{
std::array<std::array<int, 101>, 101> searchedTiles {};
std::optional<Point> nearPosition = FindClosestValidPosition(
[&searchedTiles](Point testPosition) {
searchedTiles[testPosition.x][testPosition.y]++;
return false;
},
{ 50, 50 }, 0, 50);
EXPECT_FALSE(nearPosition) << "Searching with no valid tiles should return an empty optional";
for (int x = 0; x < searchedTiles.size(); x++) {
for (int y = 0; y < searchedTiles[x].size(); y++) {
if (IsAnyOf(x, 0, 100) && IsAnyOf(y, 0, 100)) {
EXPECT_EQ(searchedTiles[x][y], 0) << "Extreme corners should be skipped due to the inset/rounded search space";
} else {
EXPECT_EQ(searchedTiles[x][y], 1) << "Position " << Point { x, y } << " should have been searched exactly once";
}
}
}
}
{
std::array<std::array<int, 5>, 5> searchedTiles {};
std::optional<Point> nearPosition = FindClosestValidPosition(
[&searchedTiles](Point testPosition) {
searchedTiles[testPosition.x][testPosition.y]++;
return false;
},
{ 2, 2 }, 1, 2);
EXPECT_FALSE(nearPosition) << "Still shouldn't find a valid position with no valid tiles";
for (int x = 0; x < searchedTiles.size(); x++) {
for (int y = 0; y < searchedTiles[x].size(); y++) {
if (Point { x, y } == Point { 2, 2 }) {
EXPECT_EQ(searchedTiles[x][y], 0) << "The starting tile should be skipped with a min radius of 1";
} else if (IsAnyOf(x, 0, 4) && IsAnyOf(y, 0, 4)) {
EXPECT_EQ(searchedTiles[x][y], 0) << "Corners should be skipped";
} else {
EXPECT_EQ(searchedTiles[x][y], 1) << "All tiles in range should be searched exactly once";
}
}
}
}
{
std::array<std::array<int, 3>, 3> searchedTiles {};
std::optional<Point> nearPosition = FindClosestValidPosition(
[&searchedTiles](Point testPosition) {
searchedTiles[testPosition.x][testPosition.y]++;
return false;
},
{ 1, 1 }, 0, 0);
EXPECT_FALSE(nearPosition) << "Searching with no valid tiles should return an empty optional";
for (int x = 0; x < searchedTiles.size(); x++) {
for (int y = 0; y < searchedTiles[x].size(); y++) {
if (Point { x, y } == Point { 1, 1 }) {
EXPECT_EQ(searchedTiles[x][y], 1) << "Only the starting tile should be searched with max radius 0";
} else {
EXPECT_EQ(searchedTiles[x][y], 0) << "Position " << Point { x, y } << " should not have been searched";
}
}
}
}
{
std::array<std::array<int, 7>, 7> searchedTiles {};
std::optional<Point> nearPosition = FindClosestValidPosition(
[&searchedTiles](Point testPosition) {
searchedTiles[testPosition.x][testPosition.y]++;
return false;
},
{ 3, 3 }, 3, 3);
EXPECT_FALSE(nearPosition) << "Searching with no valid tiles should return an empty optional";
for (int x = 0; x < searchedTiles.size(); x++) {
for (int y = 0; y < searchedTiles[x].size(); y++) {
if ((IsAnyOf(x, 1, 5) && IsAnyOf(y, 1, 5)) // inset corners
|| (IsAnyOf(x, 0, 6) && IsNoneOf(y, 0, 6)) // left/right sides
|| (IsNoneOf(x, 0, 6) && IsAnyOf(y, 0, 6)) // top/bottom sides
) {
EXPECT_EQ(searchedTiles[x][y], 1) << "Searching with a fixed radius should make a square with inset corners";
} else {
EXPECT_EQ(searchedTiles[x][y], 0) << "Position " << Point { x, y } << " should not have been searched";
}
}
}
}
{
std::optional<Point> nearPosition = FindClosestValidPosition(
[](Point testPosition) {
return true;
},
{ 50, 50 }, 21, 50);
EXPECT_EQ(*nearPosition, (Point { 50, 50 } + Displacement { 0, 21 })) << "First candidate position with a minimum radius should be at {0, +y}";
}
}
} // namespace devilution

Loading…
Cancel
Save