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.
 
 
 
 
 
 

302 lines
10 KiB

/**
* @file path.cpp
*
* Implementation of the path finding algorithms.
*/
#include "engine/path.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <utility>
#include <function_ref.hpp>
#include "appfat.h"
#include "crawl.hpp"
#include "engine/displacement.hpp"
#include "engine/point.hpp"
#include "utils/algorithm/container.hpp"
#include "utils/static_vector.hpp"
namespace devilution {
// The frame times for axis-aligned and diagonal steps are actually the same.
//
// However, we set the diagonal step cost a bit higher to avoid
// excessive diagonal movement. For example, the frame times for these
// two paths are the same: ↑↑ and ↗↖. However, ↑↑ looks more natural.
const int PathAxisAlignedStepCost = 100;
const int PathDiagonalStepCost = 101;
namespace {
constexpr size_t MaxPathNodes = 1024;
using NodeIndexType = uint16_t;
using CoordType = uint8_t;
using CostType = uint16_t;
using PointT = PointOf<CoordType>;
struct FrontierNode {
PointT position;
// Current best guess of the cost of the path to destination
// if it goes through this node.
CostType f;
};
struct ExploredNode {
// Preceding node (needed to reconstruct the path at the end).
PointT prev;
// The current lowest cost from start to this node (0 for the start node).
CostType g;
};
// A simple map with a fixed number of buckets and static storage.
class ExploredNodes {
static const size_t NumBuckets = 64;
static const size_t BucketCapacity = 3 * MaxPathNodes / NumBuckets;
using Entry = std::pair<uint16_t, ExploredNode>;
using Bucket = StaticVector<Entry, BucketCapacity>;
public:
using value_type = Entry;
using iterator = value_type *;
using const_iterator = const value_type *;
[[nodiscard]] const_iterator find(const PointT &point) const
{
const Bucket &b = bucket(point);
const auto it = c_find_if(b, [r = repr(point)](const Entry &e) { return e.first == r; });
if (it == b.end()) return nullptr;
return it;
}
[[nodiscard]] iterator find(const PointT &point)
{
Bucket &b = bucket(point);
auto it = c_find_if(b, [r = repr(point)](const Entry &e) { return e.first == r; });
if (it == b.end()) return nullptr;
return it;
}
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
[[nodiscard]] const_iterator end() const { return nullptr; }
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
[[nodiscard]] iterator end() { return nullptr; }
void emplace(const PointT &point, const ExploredNode &exploredNode)
{
bucket(point).emplace_back(repr(point), exploredNode);
}
[[nodiscard]] bool canInsert(const PointT &point) const
{
return bucket(point).size() < BucketCapacity;
}
private:
[[nodiscard]] const Bucket &bucket(const PointT &point) const { return buckets_[bucketIndex(point)]; }
[[nodiscard]] Bucket &bucket(const PointT &point) { return buckets_[bucketIndex(point)]; }
[[nodiscard]] static size_t bucketIndex(const PointT &point)
{
return ((point.x & 0b111) << 3) | (point.y & 0b111);
}
[[nodiscard]] static uint16_t repr(const PointT &point)
{
return (point.x << 8) | point.y;
}
std::array<Bucket, NumBuckets> buckets_;
};
bool IsDiagonalStep(const Point &a, const Point &b)
{
return a.x != b.x && a.y != b.y;
}
/**
* @brief Returns the distance between 2 adjacent nodes.
*/
CostType GetDistance(PointT startPosition, PointT destinationPosition)
{
return IsDiagonalStep(startPosition, destinationPosition)
? PathDiagonalStepCost
: PathAxisAlignedStepCost;
}
/**
* @brief heuristic, estimated cost from startPosition to destinationPosition.
*/
CostType GetHeuristicCost(PointT startPosition, PointT destinationPosition)
{
// This function needs to be admissible, i.e. it should never over-estimate
// the distance.
//
// This calculation assumes we can take diagonal steps until we reach
// the same row or column and then take the remaining axis-aligned steps.
const int dx = std::abs(static_cast<int>(startPosition.x) - static_cast<int>(destinationPosition.x));
const int dy = std::abs(static_cast<int>(startPosition.y) - static_cast<int>(destinationPosition.y));
const int diagSteps = std::min(dx, dy);
// After we've taken `diagSteps`, the remaining steps in one coordinate
// will be zero, and in the other coordinate it will be reduced by `diagSteps`.
// We then still need to take the remaining steps:
// max(dx, dy) - diagSteps = max(dx, dy) - min(dx, dy) = abs(dx - dy)
const int axisAlignedSteps = std::abs(dx - dy);
return diagSteps * PathDiagonalStepCost + axisAlignedSteps * PathAxisAlignedStepCost;
}
int ReconstructPath(const ExploredNodes &explored, PointT dest, int8_t *path, size_t maxPathLength)
{
size_t len = 0;
PointT cur = dest;
while (true) {
const auto it = explored.find(cur);
if (it == explored.end()) app_fatal("Failed to reconstruct path");
if (it->second.g == 0) break; // reached start
if (len == maxPathLength) {
// Path too long.
len = 0;
break;
}
path[len++] = GetPathDirection(it->second.prev, cur);
cur = it->second.prev;
}
std::reverse(path, path + len);
std::fill(path + len, path + maxPathLength, -1);
return static_cast<int>(len);
}
} // namespace
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];
}
int FindPath(tl::function_ref<bool(Point, Point)> canStep, tl::function_ref<bool(Point)> posOk, Point startPosition, Point destinationPosition, int8_t *path, size_t maxPathLength)
{
const PointT start { startPosition };
const PointT dest { destinationPosition };
const CostType initialHeuristicCost = GetHeuristicCost(start, dest);
if (initialHeuristicCost > PathDiagonalStepCost * maxPathLength) {
// Heuristic cost never underestimates the true cost, so we can give up early.
return 0;
}
StaticVector<FrontierNode, MaxPathNodes> frontier;
ExploredNodes explored;
{
frontier.emplace_back(FrontierNode { .position = start, .f = initialHeuristicCost });
explored.emplace(start, ExploredNode { .prev = {}, .g = 0 });
}
const auto frontierComparator = [&explored, &dest](const FrontierNode &a, const FrontierNode &b) {
// We use heap functions from <algorithm> which form a max-heap.
// We reverse the comparison sign here to get a min-heap.
if (a.f != b.f) return a.f > b.f;
// For nodes with the same f-score, prefer the ones with lower
// heuristic cost (likely to be closer to the goal).
const CostType hA = GetHeuristicCost(a.position, dest);
const CostType hB = GetHeuristicCost(b.position, dest);
if (hA != hB) return hA > hB;
// Prefer diagonal steps first.
const ExploredNode &aInfo = explored.find(a.position)->second;
const ExploredNode &bInfo = explored.find(b.position)->second;
const bool isDiagonalA = IsDiagonalStep(aInfo.prev, a.position);
const bool isDiagonalB = IsDiagonalStep(bInfo.prev, b.position);
if (isDiagonalA != isDiagonalB) return isDiagonalB;
// Finally, disambiguate by coordinate:
if (a.position.x != b.position.x) return a.position.x > b.position.x;
return a.position.y > b.position.y;
};
while (!frontier.empty()) {
const FrontierNode cur = frontier.front(); // argmin(node.f) for node in openSet
if (cur.position == destinationPosition) {
return ReconstructPath(explored, cur.position, path, maxPathLength);
}
std::pop_heap(frontier.begin(), frontier.end(), frontierComparator);
frontier.pop_back();
const CostType curG = explored.find(cur.position)->second.g;
// Discard invalid nodes.
// If this node is already at the maximum number of steps, we can skip processing it.
// We don't keep track of the maximum number of steps, so we approximate it.
if (curG >= PathDiagonalStepCost * maxPathLength) continue;
// When we discover a better path to a node, we push the node to the heap
// with the new `f` value even if the node is already in the heap.
if (curG + GetHeuristicCost(cur.position, dest) > cur.f) continue;
for (const DisplacementOf<int8_t> d : PathDirs) {
// We're using `uint8_t` for coordinates. Avoid underflow:
if ((cur.position.x == 0 && d.deltaX < 0) || (cur.position.y == 0 && d.deltaY < 0)) continue;
const PointT neighborPos = cur.position + d;
const bool ok = posOk(neighborPos);
if (ok) {
if (!canStep(cur.position, neighborPos)) continue;
} else {
// We allow targeting a non-walkable node if it is the destination.
if (neighborPos != dest) continue;
}
const CostType g = curG + GetDistance(cur.position, neighborPos);
if (curG >= PathDiagonalStepCost * maxPathLength) continue;
bool improved = false;
if (auto it = explored.find(neighborPos); it == explored.end()) {
if (explored.canInsert(neighborPos)) {
explored.emplace(neighborPos, ExploredNode { .prev = cur.position, .g = g });
improved = true;
}
} else if (it->second.g > g) {
it->second.prev = cur.position;
it->second.g = g;
improved = true;
}
if (improved) {
const CostType f = g + GetHeuristicCost(neighborPos, dest);
if (frontier.size() < MaxPathNodes) {
// We always push the node to the heap, even if the same position already exists in it.
// When popping from the heap, we discard invalid nodes by checking that `g + h <= f`.
frontier.emplace_back(FrontierNode { .position = neighborPos, .f = f });
std::push_heap(frontier.begin(), frontier.end(), frontierComparator);
}
}
}
}
return 0; // no path
}
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> {
Point candidatePosition = startingPosition + displacement;
if (posOk(candidatePosition))
return candidatePosition;
return {};
});
}
#ifdef BUILD_TESTING
int TestPathGetHeuristicCost(Point startPosition, Point destinationPosition)
{
return GetHeuristicCost(startPosition, destinationPosition);
}
#endif
} // namespace devilution