You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
272 lines
9.7 KiB
272 lines
9.7 KiB
#include "engine/path.h" |
|
|
|
#include <algorithm> |
|
#include <array> |
|
#include <cstddef> |
|
#include <span> |
|
|
|
#include <gmock/gmock.h> |
|
#include <gtest/gtest.h> |
|
|
|
#include "engine/direction.hpp" |
|
#include "utils/algorithm/container.hpp" |
|
|
|
namespace devilution { |
|
|
|
extern int TestPathGetHeuristicCost(Point startPosition, Point destinationPosition); |
|
|
|
namespace { |
|
|
|
using ::testing::ElementsAreArray; |
|
|
|
TEST(PathTest, Heuristics) |
|
{ |
|
constexpr Point source { 25, 32 }; |
|
Point destination = source; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), 0) << "Wrong cost for travelling to the same tile"; |
|
|
|
destination = source + Direction::NorthEast; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathAxisAlignedStepCost) << "Wrong cost for travelling to horizontal/vertical adjacent tile"; |
|
destination = source + Direction::SouthEast; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathAxisAlignedStepCost) << "Wrong cost for travelling to horizontal/vertical adjacent tile"; |
|
destination = source + Direction::SouthWest; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathAxisAlignedStepCost) << "Wrong cost for travelling to horizontal/vertical adjacent tile"; |
|
destination = source + Direction::NorthWest; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathAxisAlignedStepCost) << "Wrong cost for travelling to horizontal/vertical adjacent tile"; |
|
|
|
destination = source + Direction::North; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathDiagonalStepCost) << "Wrong cost for travelling to diagonally adjacent tile"; |
|
destination = source + Direction::East; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathDiagonalStepCost) << "Wrong cost for travelling to diagonally adjacent tile"; |
|
destination = source + Direction::South; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathDiagonalStepCost) << "Wrong cost for travelling to diagonally adjacent tile"; |
|
destination = source + Direction::West; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathDiagonalStepCost) << "Wrong cost for travelling to diagonally adjacent tile"; |
|
destination = source + Direction::SouthWest + Direction::SouthEast; // Effectively the same as Direction::South |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathDiagonalStepCost) << "Wrong cost for travelling to diagonally adjacent tile"; |
|
|
|
destination = source + Direction::NorthEast + Direction::North; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), PathAxisAlignedStepCost + PathDiagonalStepCost) << "Wrong cost for travelling to a { 2, 1 } offset"; |
|
destination = source + Direction::SouthEast + Direction::SouthEast; |
|
EXPECT_EQ(TestPathGetHeuristicCost(source, destination), 2 * PathAxisAlignedStepCost) << "Wrong cost for travelling to a { 2, 0 } offset"; |
|
} |
|
|
|
// These symbols are in terms of coordinates (not in terms of on-screen direction). |
|
// -1, -1 is top-left. |
|
enum class Dir { |
|
None, |
|
Up, |
|
Left, |
|
Right, |
|
Down, |
|
UpLeft, |
|
UpRight, |
|
DownRight, |
|
DownLeft |
|
}; |
|
std::array<std::string_view, 9> DirSymbols = { "∅", "↑", "←", "→", "↓", "↖", "↗", "↘", "↙" }; |
|
|
|
std::ostream &operator<<(std::ostream &os, Dir dir) |
|
{ |
|
return os << DirSymbols[static_cast<size_t>(dir)]; |
|
} |
|
|
|
std::vector<Dir> ToSyms(std::span<const std::string> strings) |
|
{ |
|
std::vector<Dir> result; |
|
result.reserve(strings.size()); |
|
for (const std::string &str : strings) |
|
result.emplace_back(static_cast<Dir>(std::distance(DirSymbols.begin(), c_find(DirSymbols, str)))); |
|
return result; |
|
} |
|
|
|
std::vector<Dir> ToSyms(std::span<const int8_t> indices) |
|
{ |
|
std::vector<Dir> result; |
|
result.reserve(indices.size()); |
|
for (const int8_t idx : indices) |
|
result.emplace_back(static_cast<Dir>(idx)); |
|
return result; |
|
} |
|
|
|
void CheckPath(Point startPosition, Point destinationPosition, std::vector<std::string> expectedSteps) |
|
{ |
|
constexpr size_t MaxPathLength = 25; |
|
int8_t pathSteps[MaxPathLength]; |
|
auto pathLength = FindPath( |
|
/*canStep=*/[](Point, Point) { return true; }, |
|
/*posOk=*/[](Point) { return true; }, |
|
startPosition, destinationPosition, pathSteps, MaxPathLength); |
|
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; |
|
} |
|
|
|
TEST(PathTest, FindPathToSelf) |
|
{ |
|
CheckPath({ 8, 8 }, { 8, 8 }, {}); |
|
} |
|
|
|
TEST(PathTest, FindPathTwoStepsUp) |
|
{ |
|
CheckPath({ 8, 8 }, { 8, 6 }, { "↑", "↑" }); |
|
} |
|
|
|
TEST(PathTest, FindPathTwoStepsLeft) |
|
{ |
|
CheckPath({ 8, 8 }, { 6, 8 }, { "←", "←" }); |
|
} |
|
|
|
TEST(PathTest, FindPathTwoStepsRight) |
|
{ |
|
CheckPath({ 8, 8 }, { 10, 8 }, { "→", "→" }); |
|
} |
|
|
|
TEST(PathTest, FindPathTwoStepsDown) |
|
{ |
|
CheckPath({ 8, 8 }, { 8, 10 }, { "↓", "↓" }); |
|
} |
|
|
|
TEST(PathTest, FindPathDiagonalsFirst3Left2Up) |
|
{ |
|
// Pathing biases along diagonals and the diagonal steps will always be first |
|
CheckPath({ 8, 8 }, { 5, 6 }, { "↖", "↖", "←" }); |
|
} |
|
|
|
TEST(PathTest, FindPathDiagonalsFirst4Left4Up) |
|
{ |
|
CheckPath({ 8, 8 }, { 4, 4 }, { "↖", "↖", "↖", "↖" }); |
|
} |
|
|
|
TEST(PathTest, FindPathDiagonalsFirst2Right4Down) |
|
{ |
|
CheckPath({ 8, 8 }, { 10, 12 }, { "↘", "↘", "↓", "↓" }); |
|
} |
|
|
|
TEST(PathTest, FindPathDiagonalsFirst4Right12Down) |
|
{ |
|
CheckPath({ 8, 8 }, { 12, 20 }, { "↘", "↘", "↘", "↘", "↓", "↓", "↓", "↓", "↓", "↓", "↓", "↓" }); |
|
} |
|
|
|
TEST(PathTest, LongPaths) |
|
{ |
|
// Starting from the middle of the world and trying to path to a border exceeds the maximum path size |
|
CheckPath({ 56, 56 }, { 0, 0 }, {}); |
|
|
|
// Longest possible path is currently 24 steps meaning tiles 24 units away are reachable |
|
Point startingPosition { 56, 56 }; |
|
CheckPath(startingPosition, startingPosition + Displacement { 24, 24 }, std::vector<std::string>(24, "↘")); |
|
|
|
// But trying to navigate 25 units fails |
|
CheckPath(startingPosition, startingPosition + Displacement { 25, 25 }, {}); |
|
} |
|
|
|
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 (size_t x = 0; x < searchedTiles.size(); x++) { |
|
for (size_t y = 0; y < searchedTiles[x].size(); y++) { |
|
if ((x == 0 || x == 100) && (y == 0 || y == 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 " << 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 (size_t x = 0; x < searchedTiles.size(); x++) { |
|
for (size_t y = 0; y < searchedTiles[x].size(); y++) { |
|
if (x == 2 && y == 2) { |
|
EXPECT_EQ(searchedTiles[x][y], 0) << "The starting tile should be skipped with a min radius of 1"; |
|
} else if ((x == 0 || x == 4) && (y == 0 || y == 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 (size_t x = 0; x < searchedTiles.size(); x++) { |
|
for (size_t y = 0; y < searchedTiles[x].size(); y++) { |
|
if (x == 1 && y == 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 " << 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 (size_t x = 0; x < searchedTiles.size(); x++) { |
|
for (size_t y = 0; y < searchedTiles[x].size(); y++) { |
|
if (((x == 1 || x == 5) && (y == 1 || y == 5)) // inset corners |
|
|| ((x == 0 || x == 6) && y != 0 && y != 6) // left/right sides |
|
|| (x != 0 && x != 6 && (y == 0 || y == 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 " << 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 |
|
} // namespace devilution
|
|
|