diff --git a/Demos/PathfindingDemo.cs b/Demos/PathfindingDemo.cs index 24343e3..530e4c9 100644 --- a/Demos/PathfindingDemo.cs +++ b/Demos/PathfindingDemo.cs @@ -36,14 +36,14 @@ namespace Demos { // Create a cost function, which determines how expensive (or difficult) it should be to move from a given position // to the next, adjacent position. In our case, the only restriction should be walls and out-of-bounds positions, which - // both have a cost of float.MaxValue, meaning they are completely unwalkable. + // both have a cost of AStar2.InfiniteCost, meaning they are completely unwalkable. // If your game contains harder-to-move-on areas like, say, a muddy pit, you can return a higher cost value for those // locations. If you want to scale your cost function differently, you can specify a different default cost in your // pathfinder's constructor float Cost(Point pos, Point nextPos) { if (nextPos.X < 0 || nextPos.Y < 0 || nextPos.X >= 50 || nextPos.Y >= 50) - return float.MaxValue; - return this.world[nextPos.X, nextPos.Y] ? 1 : float.MaxValue; + return AStar2.InfiniteCost; + return this.world[nextPos.X, nextPos.Y] ? 1 : AStar2.InfiniteCost; } // Actually initialize the pathfinder with the cost function, as well as specify if moving diagonally between tiles should be diff --git a/MLEM/Pathfinding/AStar.cs b/MLEM/Pathfinding/AStar.cs index d435672..eed1717 100644 --- a/MLEM/Pathfinding/AStar.cs +++ b/MLEM/Pathfinding/AStar.cs @@ -82,7 +82,7 @@ namespace MLEM.Pathfinding { /// The default cost for each path point /// The maximum amount of tries before path finding is aborted /// If diagonals should be looked at for path finding - /// A stack of path points, where the top item is the first point to go to. + /// A stack of path points, where the top item is the first point to go to, or null if no path was found. public Stack FindPath(T start, T goal, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, bool? allowDiagonals = null) { var stopwatch = Stopwatch.StartNew(); @@ -118,7 +118,7 @@ namespace MLEM.Pathfinding { foreach (var dir in dirsUsed) { var neighborPos = this.AddPositions(current.Pos, dir); var cost = getCost(current.Pos, neighborPos); - if (!float.IsInfinity(cost) && cost < float.MaxValue && !closed.ContainsKey(neighborPos)) { + if (!float.IsPositiveInfinity(cost) && cost < float.MaxValue && !closed.ContainsKey(neighborPos)) { var neighbor = new PathPoint(neighborPos, this.GetManhattanDistance(neighborPos, goal), current, cost, defCost); // check if we already have a waypoint at this location with a worse path if (open.TryGetValue(neighborPos, out var alreadyNeighbor)) { diff --git a/Tests/NumberTests.cs b/Tests/NumberTests.cs index 0e6a629..184a645 100644 --- a/Tests/NumberTests.cs +++ b/Tests/NumberTests.cs @@ -43,7 +43,7 @@ namespace Tests { } [Test] - public void TestMatrixOps([Range(0.5F, 2, 1)] float scale, [Range(-0.5F, 0.5F, 1)] float rotationX, [Range(-0.5F, 0.5F, 1)] float rotationY, [Range(-0.5F, 0.5F, 0.5F)] float rotationZ) { + public void TestMatrixOps([Range(0.5F, 2, 0.5F)] float scale, [Range(-0.5F, 0.5F, 1)] float rotationX, [Range(-0.5F, 0.5F, 1)] float rotationY, [Range(-0.5F, 0.5F, 1)] float rotationZ) { var rotation = Matrix.CreateRotationX(rotationX) * Matrix.CreateRotationY(rotationY) * Matrix.CreateRotationZ(rotationZ); var matrix = rotation * Matrix.CreateScale(scale, scale, scale); Assert.IsTrue(matrix.Scale().Equals(new Vector3(scale), 0.001F), $"{matrix.Scale()} does not equal {new Vector2(scale)}"); diff --git a/Tests/PathfindingTests.cs b/Tests/PathfindingTests.cs new file mode 100644 index 0000000..343cde1 --- /dev/null +++ b/Tests/PathfindingTests.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using MLEM.Pathfinding; +using NUnit.Framework; + +namespace Tests { + public class PathfindingTests { + + [Test] + public void TestConsistency() { + var area = new[] { + "XXXX", + "X X", + "X X", + "XXXX" + }; + + var noDiagonals = FindPathInArea(new Point(1, 1), new Point(2, 2), area, false).ToArray(); + Assert.AreEqual(noDiagonals.Length, 3); + Assert.AreEqual(noDiagonals[0], new Point(1, 1)); + Assert.AreEqual(noDiagonals[2], new Point(2, 2)); + + var diagonals = FindPathInArea(new Point(1, 1), new Point(2, 2), area, true).ToArray(); + Assert.AreEqual(diagonals.Length, 2); + Assert.AreEqual(diagonals[0], new Point(1, 1)); + Assert.AreEqual(diagonals[1], new Point(2, 2)); + } + + [Test] + public void TestPathCost() { + var area = new[] { + "XXXXXXXX", + "X 5 X", + "X 5 X", + "XXXXXXXX" + }; + + var firstPath = FindPathInArea(new Point(1, 1), new Point(3, 1), area, false).ToArray(); + var firstExpected = new[] {new Point(1, 1), new Point(1, 2), new Point(2, 2), new Point(3, 2), new Point(3, 1)}; + Assert.AreEqual(firstPath, firstExpected); + + var secondPath = FindPathInArea(new Point(1, 1), new Point(5, 2), area, false).ToArray(); + var secondExpected = firstExpected.Concat(new[] {new Point(4, 1), new Point(5, 1), new Point(5, 2)}).ToArray(); + Assert.AreEqual(secondPath, secondExpected); + } + + [Test] + public void TestBlocked() { + var area = new[] { + "XXXX", + "X XX", + "XX X", + "X X", + "XXXX" + }; + // non-diagonal pathfinding should get stuck in the corner + Assert.IsNull(FindPathInArea(new Point(1, 1), new Point(2, 3), area, false)); + // diagonal pathfinding should be able to cross the diagonal gap + Assert.IsNotNull(FindPathInArea(new Point(1, 1), new Point(2, 3), area, true)); + } + + private static Stack FindPathInArea(Point start, Point end, IEnumerable area, bool allowDiagonals) { + var costs = area.Select(s => s.Select(c => c switch { + ' ' => 1, + 'X' => AStar2.InfiniteCost, + _ => (float) char.GetNumericValue(c) + }).ToArray()).ToArray(); + var pathFinder = new AStar2((p1, p2) => costs[p2.Y][p2.X], allowDiagonals); + return pathFinder.FindPath(start, end); + } + + } +} \ No newline at end of file