//
// Pathfinder.cs
//
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using NosSmooth.Core.Client;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Extensions.Pathfinding.Errors;
using NosSmooth.Packets.Enums;
using Remora.Results;
namespace NosSmooth.Extensions.Pathfinding;
///
/// Find path between two given points.
///
public class Pathfinder
{
private readonly PathfinderState _state;
///
/// Initializes a new instance of the class.
///
/// The state.
public Pathfinder(PathfinderState state)
{
_state = state;
}
///
/// Attempts to find a path between the current character position and the target.
///
/// The target x coordinate.
/// The target y coordinate.
/// A path or an error.
public Result FindPathFromCurrent
(
short targetX,
short targetY
)
=> FindPathFrom(_state.Character.X, _state.Character.Y, targetX, targetY);
///
/// Attempts to find a path between the current position of the entity and the target.
///
///
/// Works only for entities that are stored in state,
/// that means only the character and mates owned by the character.
///
/// The id of the entity to find path from.
/// The target x coordinate.
/// The target y coordinate.
/// A path or an error.
public Result FindPathFromEntity
(
long entityId,
short targetX,
short targetY
)
{
if (!_state.Entities.TryGetValue(entityId, out var entityState))
{
return new EntityStateNotFoundError(entityId);
}
return FindPathFrom(entityState.X, entityState.Y, targetX, targetY);
}
///
/// Attempts to find a path between the given positions on the current map.
///
/// The start x coordinate.
/// The start y coordinate.
/// The target x coordinate.
/// The target y coordinate.
/// A path or an error.
public Result FindPathFrom
(
short x,
short y,
short targetX,
short targetY
)
{
if (_state.MapInfo is null)
{
return new StateNotInitializedError();
}
return FindPathOnMap
(
_state.MapInfo,
x,
y,
targetX,
targetY
);
}
///
/// Attempts to find path on the given map with the given coordinates.
///
/// The map info.
/// The start x coordinate.
/// The start y coordinate.
/// The target x coordinate.
/// The target y coordinate.
/// A path or an error.
public Result FindPathOnMap
(
IMapInfo mapInfo,
short x,
short y,
short targetX,
short targetY
)
{
if (!mapInfo.IsWalkable(targetX, targetY))
{
return new PathNotFoundError(targetX, targetY);
}
if (x == targetX && y == targetY)
{
return new Path(mapInfo.Id, x, y, targetX, targetY, Array.Empty<(short X, short Y)>());
}
var target = (targetX, targetY);
var visited = new HashSet<(short X, short Y)>();
var offsets = new (short X, short Y)[] { (0, 1), (1, 0), (1, 1), (0, -1), (-1, 0), (-1, -1), (1, -1), (-1, 1) };
var distances = new[] { 1, 1, 1.41421356237, 1, 1, 1.41421356237, 1.41421356237, 1.41421356237 };
var queue = new PriorityQueue(); // estimated cost to path.
var start = new PathEntry(0, null, (x, y));
queue.Enqueue(start, 0);
visited.Add((x, y));
while (queue.TryDequeue(out var current, out _))
{
for (int i = 0; i < offsets.Length; i++)
{
var offset = offsets[i];
var distance = distances[i];
var currX = current.Position.X + offset.X;
var currY = current.Position.Y + offset.Y;
if (visited.Contains(((short)currX, (short)currY)))
{
// The estimated distance function should be consistent,
// the cost cannot be lower on this visit.
continue;
}
visited.Add(((short)currX, (short)currY));
if (currX == targetX && currY == targetY)
{
return ReconstructPath
(
mapInfo.Id,
x,
y,
targetX,
targetY,
current.CreateChild(distance, (short)currX, (short)currY)
);
}
if (currX < 0 || currY < 0 || currX >= mapInfo.Width || currY >= mapInfo.Height)
{
// Out of bounds
continue;
}
if (!mapInfo.IsWalkable((short)currX, (short)currY))
{
// Current tile not walkable
continue;
}
var path = current.CreateChild(distance, (short)currX, (short)currY);
var estimatedDistance = EstimateDistance(path.Position, target);
queue.Enqueue(path, path.Cost + estimatedDistance);
}
}
return new PathNotFoundError(targetX, targetY);
}
private Path ReconstructPath
(
int mapId,
short x,
short y,
short targetX,
short targetY,
PathEntry entry
)
{
var entries = new List<(short X, short Y)>();
var current = entry;
while (current is not null)
{
entries.Add(current.Position);
current = current.Previous;
}
entries.Reverse();
return new Path
(
mapId,
x,
y,
targetX,
targetY,
entries
);
}
private double EstimateDistance((short X, short Y) current, (short X, short Y) next)
{
return Math.Sqrt(((current.X - next.X) * (current.X - next.X)) + ((current.Y - next.Y) * (current.Y - next.Y)));
}
private class PathEntry
{
public PathEntry(double cost, PathEntry? previous, (short X, short Y) position)
{
Cost = cost;
Previous = previous;
Position = position;
}
public double Cost { get; }
public PathEntry? Previous { get; }
public (short X, short Y) Position { get; }
public PathEntry CreateChild(double walkCost, short x, short y)
{
return new PathEntry(Cost + walkCost, this, (x, y));
}
}
}