From 596800807465da0508c981acf4d2ce0747354946 Mon Sep 17 00:00:00 2001 From: val antonini Date: Thu, 25 Apr 2024 14:47:24 +1000 Subject: [PATCH] implemented support for weighted paths --- AStar.Tests/LongerPathingTests.cs | 4 +- AStar.Tests/OptionsTest.cs | 2 - AStar.Tests/PathfinderTests.cs | 11 ---- AStar.Tests/PathingTests.cs | 6 -- AStar.Tests/PunishChangeDirectionIssueTest.cs | 4 -- AStar.Tests/PunishChangeDirectionTests.cs | 5 -- AStar.Tests/WeightedPathingTests.cs | 55 +++++++++++++++++++ AStar/Options/PathFinderOptions.cs | 11 ++++ AStar/PathFinder.cs | 18 +++++- README.md | 13 ++++- 10 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 AStar.Tests/WeightedPathingTests.cs diff --git a/AStar.Tests/LongerPathingTests.cs b/AStar.Tests/LongerPathingTests.cs index b52c8d5..76b79f9 100644 --- a/AStar.Tests/LongerPathingTests.cs +++ b/AStar.Tests/LongerPathingTests.cs @@ -55,7 +55,7 @@ public void TestPathingOptions() var pathfinder = new PathFinder(_world, pathfinderOptions); var path = pathfinder.FindPath(new Position(1, 1), new Position(30, 30)); - Helper.Print(_world, path); + } [Test] @@ -65,7 +65,7 @@ public void ShouldPathEnvironment() var path = pathfinder.FindPath(new Position(1, 1), new Position(30, 30)); - Helper.Print(_world, path); + path.ShouldBe(new[] { new Position(1, 1), diff --git a/AStar.Tests/OptionsTest.cs b/AStar.Tests/OptionsTest.cs index 2c5afe9..9bca621 100644 --- a/AStar.Tests/OptionsTest.cs +++ b/AStar.Tests/OptionsTest.cs @@ -27,8 +27,6 @@ public void ShouldEnforceSearchLimit() var path = pathfinder.FindPath(new Position(1, 1), new Position(1, 5)); - Helper.Print(_world, path); - path.ShouldBeEmpty(); } } diff --git a/AStar.Tests/PathfinderTests.cs b/AStar.Tests/PathfinderTests.cs index 626c2cf..2bed3c2 100644 --- a/AStar.Tests/PathfinderTests.cs +++ b/AStar.Tests/PathfinderTests.cs @@ -26,8 +26,6 @@ public void ShouldPathRectangleGrid() var path = pathfinder.FindPath(new Position(0, 0), new Position(2, 4)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(0, 0), new Position(1, 1), @@ -52,8 +50,6 @@ public void ShouldPathToAdjacent() { var path = _pathFinder.FindPath(new Position(1, 1), new Position(2, 1)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 1), new Position(2, 1), @@ -65,8 +61,6 @@ public void ShouldDoSimplePath() { var path = _pathFinder.FindPath(new Position(1, 1), new Position(4, 2)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 1), new Position(2, 2), @@ -83,8 +77,6 @@ public void ShouldDoSimplePathWithNoDiagonal() var path = _pathFinder.FindPath(new Position(1, 1), new Position(4, 2)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 1), new Position(2, 1), @@ -105,7 +97,6 @@ public void ShouldDoSimplePathWithNoDiagonalAroundObstacle() _world[2, 2] = 0; var path = _pathFinder.FindPath(new Position(1, 1), new Position(4, 2)); - Helper.Print(_world, path); path.ShouldBe(new[] { new Position(1, 1), @@ -127,8 +118,6 @@ public void ShouldPathAroundObstacle() var path = _pathFinder.FindPath(new Position(1, 1), new Position(4, 2)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 1), new Position(1, 2), diff --git a/AStar.Tests/PathingTests.cs b/AStar.Tests/PathingTests.cs index 1602d2c..2438f2e 100644 --- a/AStar.Tests/PathingTests.cs +++ b/AStar.Tests/PathingTests.cs @@ -27,8 +27,6 @@ public void ShouldPathPredictably() var path = pathfinder.FindPath(new Position(1, 1), new Position(2, 3)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 1), new Position(2, 2), @@ -43,8 +41,6 @@ public void ShouldPathPredictably2() var path = pathfinder.FindPath(new Position(1, 1), new Position(1, 5)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 1), new Position(1, 2), @@ -62,8 +58,6 @@ public void ShouldPathPredictably3() var path = pathfinder.FindPath(new Position(1, 1), new Position(1, 5)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 1), new Position(1, 2), diff --git a/AStar.Tests/PunishChangeDirectionIssueTest.cs b/AStar.Tests/PunishChangeDirectionIssueTest.cs index 9e38a59..1b389ee 100644 --- a/AStar.Tests/PunishChangeDirectionIssueTest.cs +++ b/AStar.Tests/PunishChangeDirectionIssueTest.cs @@ -34,8 +34,6 @@ public void ShouldPunishChangingDirectionsIssue() var path = pathfinder.FindPath(new Position(7, 2), new Position(1, 17)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(7, 2), new Position(7, 3), @@ -70,8 +68,6 @@ public void ShouldCorrectIssue() var path = pathfinder.FindPath(new Position(1, 2), new Position(8, 14)); - Helper.Print(_world, path); - path.ShouldBe(new[] { new Position(1, 2), new Position(2, 3), diff --git a/AStar.Tests/PunishChangeDirectionTests.cs b/AStar.Tests/PunishChangeDirectionTests.cs index add09e0..81d6b31 100644 --- a/AStar.Tests/PunishChangeDirectionTests.cs +++ b/AStar.Tests/PunishChangeDirectionTests.cs @@ -40,9 +40,6 @@ public void ShouldPunishChangingDirections() var path = pathfinder.FindPath(new Position(2, 9), new Position(15, 3)); - - Helper.Print(world, path); - path.ShouldBe(new[] { new Position(2, 9), @@ -92,9 +89,7 @@ public void ShouldCalculateAdjacentCorrectly() }; Console.WriteLine("actual"); - Helper.Print(world, path); Console.WriteLine("expected"); - Helper.Print(world, expected); path.ShouldBe(expected); } diff --git a/AStar.Tests/WeightedPathingTests.cs b/AStar.Tests/WeightedPathingTests.cs new file mode 100644 index 0000000..a907993 --- /dev/null +++ b/AStar.Tests/WeightedPathingTests.cs @@ -0,0 +1,55 @@ +using AStar.Options; +using NUnit.Framework; +using Shouldly; + +namespace AStar.Tests +{ + [TestFixture] + public class WeightedPathingTests + { + [Test] + public void ShouldPathWithWeight() + { + var level = @"1111115 + 1511151 + 1155511 + 1111111"; + var world = Helper.ConvertStringToPathfinderGrid(level); + var opts = new PathFinderOptions { Weighting = Weighting.Positive }; + var pathfinder = new PathFinder(world, opts); + + var path = pathfinder.FindPath(new Position(1, 1), new Position(1, 5)); + + path.ShouldBe(new[] { + new Position(1, 1), + new Position(2, 2), + new Position(2, 3), + new Position(2, 4), + new Position(1, 5), + }); + } + + [Test] + public void ShouldPathWithInvertedWeight() + { + var level = @"9999995 + 9599959 + 9955599 + 9999999"; + + var world = Helper.ConvertStringToPathfinderGrid(level); + var opts = new PathFinderOptions { Weighting = Weighting.Negative }; + var pathfinder = new PathFinder(world, opts); + + var path = pathfinder.FindPath(new Position(1, 1), new Position(1, 5)); + + path.ShouldBe(new[] { + new Position(1, 1), + new Position(2, 2), + new Position(2, 3), + new Position(2, 4), + new Position(1, 5), + }); + } + } +} diff --git a/AStar/Options/PathFinderOptions.cs b/AStar/Options/PathFinderOptions.cs index 39fabdd..a056dc1 100644 --- a/AStar/Options/PathFinderOptions.cs +++ b/AStar/Options/PathFinderOptions.cs @@ -12,6 +12,8 @@ public class PathFinderOptions public int SearchLimit { get; set; } + public Weighting Weighting {get;set;} + public PathFinderOptions() { HeuristicFormula = HeuristicFormula.Manhattan; @@ -19,4 +21,13 @@ public PathFinderOptions() SearchLimit = 2000; } } + + public enum Weighting { + // The number in the grid will not influence the path. + None, + // Higher open values will be favoured and applied to the new h value. + Positive, + // Lower open values will be favoured and applied to the new h value. + Negative + } } diff --git a/AStar/PathFinder.cs b/AStar/PathFinder.cs index 0af2cf9..11cd3db 100644 --- a/AStar/PathFinder.cs +++ b/AStar/PathFinder.cs @@ -68,10 +68,24 @@ public Position[] FindPath(Position start, Position end) newG += CalculateModifierToG(q, successor, end); } + var newH = _heuristic.Calculate(successor.Position, end); + switch (_options.Weighting) + { + case Weighting.Positive: + newH -= _world[successor.Position]; + break; + case Weighting.Negative: + newH += _world[successor.Position]; + break; + case Weighting.None: + default: + break; + } + var updatedSuccessor = new PathFinderNode( position: successor.Position, g: newG, - h:_heuristic.Calculate(successor.Position, end), + h: newH, parentNodePosition: q.Position); if (BetterPathToSuccessorFound(updatedSuccessor, successor)) @@ -163,4 +177,4 @@ private static Position[] OrderClosedNodesAsArray(IModelAGraph g return path.ToArray(); } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 8df83cd..1d9f2e2 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,18 @@ q. why doesn't this algorithm always find the shortest path? a. A* optimises speed over accuracy. Because the algorithm relies on a heuristic to determine the distances from start and finish, it won't necessarily produce the shortest path to the target. - +## Changes from 1.1.0 to 1.3.0 +- Introduced path weighting to favour or penalize cells. This is off by default and +can be opted into using the new options. See [this blog post for more info](https://valantonini.com/posts/20210401/) +```csharp +var level = @"1111115 + 1511151 + 1155511 + 1111111"; +var world = Helper.ConvertStringToPathfinderGrid(level); +var opts = new PathFinderOptions { Weighting = Weighting.Positive }; +var pathfinder = new PathFinder(world, opts); +``` ## Changes from 1.0.0 to 1.1.0 - Reimplemented the punish change direction to perform more consistently