diff --git a/Graph/DirectedGraph.cs b/Graph/DirectedGraph.cs index 70c2cde..2ebcd5c 100644 --- a/Graph/DirectedGraph.cs +++ b/Graph/DirectedGraph.cs @@ -10,7 +10,7 @@ namespace MoonTools.Core.Graph finish } - public class DirectedGraph : IGraph + public class DirectedGraph : IGraph where TNode : IEquatable { protected HashSet nodes = new HashSet(); protected HashSet<(TNode, TNode)> edges = new HashSet<(TNode, TNode)>(); diff --git a/Graph/DirectedWeightedMultiGraph.cs b/Graph/DirectedWeightedMultiGraph.cs new file mode 100644 index 0000000..839ccee --- /dev/null +++ b/Graph/DirectedWeightedMultiGraph.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MoreLinq; + +namespace MoonTools.Core.Graph +{ + public class DirectedWeightedMultiGraph : IGraph where TNode : IEquatable + { + protected HashSet nodes = new HashSet(); + protected Dictionary> neighbors = new Dictionary>(); + protected Dictionary<(TNode, TNode), HashSet> edges = new Dictionary<(TNode, TNode), HashSet>(); + protected Dictionary IDToEdge = new Dictionary(); + protected Dictionary weights = new Dictionary(); + protected Dictionary edgeToEdgeData = new Dictionary(); + + // store search sets to prevent GC + protected HashSet openSet = new HashSet(); + protected HashSet closedSet = new HashSet(); + protected Dictionary gScore = new Dictionary(); + protected Dictionary fScore = new Dictionary(); + protected Dictionary cameFrom = new Dictionary(); + + public IEnumerable Nodes => nodes; + + public void AddNode(TNode node) + { + nodes.Add(node); + } + + public void AddNodes(params TNode[] nodes) + { + foreach (var node in nodes) + { + AddNode(node); + } + } + + public void AddEdge(TNode v, TNode u, int weight, TEdgeData data) + { + if (Exists(v) && Exists(u)) + { + var id = Guid.NewGuid(); + if (!neighbors.ContainsKey(v)) + { + neighbors[v] = new HashSet(); + } + neighbors[v].Add(u); + weights.Add(id, weight); + if (!edges.ContainsKey((v, u))) + { + edges[(v, u)] = new HashSet(); + } + edges[(v, u)].Add(id); + edgeToEdgeData.Add(id, data); + IDToEdge.Add(id, (v, u)); + } + else if (!Exists(v)) + { + throw new InvalidVertexException("Vertex {0} does not exist in the graph", v); + } + else + { + throw new InvalidVertexException("Vertex {0} does not exist in the graph", u); + } + } + + public void AddEdges(params (TNode, TNode, int, TEdgeData)[] edges) + { + foreach (var edge in edges) + { + AddEdge(edge.Item1, edge.Item2, edge.Item3, edge.Item4); + } + } + + public void Clear() + { + nodes.Clear(); + neighbors.Clear(); + weights.Clear(); + edgeToEdgeData.Clear(); + } + + private void CheckNodes(params TNode[] givenNodes) + { + foreach (var node in givenNodes) + { + if (!Exists(node)) + { + throw new ArgumentException($"Vertex {node} does not exist in the graph"); + } + } + } + + public IEnumerable EdgeIDs(TNode v, TNode u) + { + CheckNodes(v, u); + return edges.ContainsKey((v, u)) ? edges[(v, u)] : Enumerable.Empty(); + } + + public bool Exists(TNode node) + { + return nodes.Contains(node); + } + + public bool Exists(TNode v, TNode u) + { + CheckNodes(v, u); + return edges.ContainsKey((v, u)); + } + + public IEnumerable Neighbors(TNode node) + { + CheckNodes(node); + return neighbors.ContainsKey(node) ? neighbors[node] : Enumerable.Empty(); + } + + public IEnumerable Weights(TNode v, TNode u) + { + CheckNodes(v, u); + return edges[(v, u)].Select(id => weights[id]); + } + + public TEdgeData EdgeData(Guid id) + { + if (!edgeToEdgeData.ContainsKey(id)) + { + throw new ArgumentException($"Edge {id} does not exist in the graph."); + } + + return edgeToEdgeData[id]; + } + + private IEnumerable ReconstructPath(Dictionary cameFrom, TNode currentNode) + { + while (cameFrom.ContainsKey(currentNode)) + { + var edgeID = cameFrom[currentNode]; + var edge = IDToEdge[edgeID]; + currentNode = edge.Item1; + yield return edgeID; + } + } + + public IEnumerable AStarShortestPath(TNode start, TNode end, Func heuristic) + { + CheckNodes(start, end); + + openSet.Clear(); + closedSet.Clear(); + gScore.Clear(); + fScore.Clear(); + cameFrom.Clear(); + + openSet.Add(start); + + gScore[start] = 0; + fScore[start] = heuristic(start, end); + + while (openSet.Any()) + { + var currentNode = openSet.MinBy(node => fScore[node]).First(); + + if (currentNode.Equals(end)) + { + return ReconstructPath(cameFrom, currentNode).Reverse(); + } + + openSet.Remove(currentNode); + closedSet.Add(currentNode); + + foreach (var neighbor in Neighbors(currentNode)) + { + if (!closedSet.Contains(neighbor)) + { + var lowestEdgeID = EdgeIDs(currentNode, neighbor).MinBy(id => weights[id]).First(); + var weight = weights[lowestEdgeID]; + + var tentativeGScore = gScore.ContainsKey(currentNode) ? gScore[currentNode] + weight : int.MaxValue; + + if (!openSet.Contains(neighbor) || tentativeGScore < gScore[neighbor]) + { + cameFrom[neighbor] = lowestEdgeID; + gScore[neighbor] = tentativeGScore; + fScore[neighbor] = tentativeGScore + heuristic(neighbor, end); + openSet.Add(neighbor); + } + } + } + } + + return Enumerable.Empty(); + } + } +} \ No newline at end of file diff --git a/Graph/IGraph.cs b/Graph/IGraph.cs index 1fe0be6..a3939f1 100644 --- a/Graph/IGraph.cs +++ b/Graph/IGraph.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace MoonTools.Core.Graph { - public interface IGraph + public interface IGraph where TNode : System.IEquatable { IEnumerable Nodes { get; } diff --git a/Graph/MoonTools.Core.Graph.csproj b/Graph/MoonTools.Core.Graph.csproj index 72764a6..e69a172 100644 --- a/Graph/MoonTools.Core.Graph.csproj +++ b/Graph/MoonTools.Core.Graph.csproj @@ -1,7 +1,8 @@ - - - - netstandard2.0 - - - + + + netstandard2.0 + + + + + \ No newline at end of file diff --git a/Graph/UndirectedGraph.cs b/Graph/UndirectedGraph.cs deleted file mode 100644 index d64e306..0000000 --- a/Graph/UndirectedGraph.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MoonTools.Core.Graph -{ - public class UndirectedGraph - { - - } -} \ No newline at end of file diff --git a/test/DirectedGraphTest.cs b/test/DirectedGraphTest.cs index 4481c1e..106ae08 100644 --- a/test/DirectedGraphTest.cs +++ b/test/DirectedGraphTest.cs @@ -8,8 +8,6 @@ using MoonTools.Core.Graph; namespace Tests { - struct EdgeData { } - public class DirectedGraphTest { EdgeData dummyEdgeData; @@ -399,17 +397,12 @@ namespace Tests myGraph.Exists((1, 2)).Should().BeTrue(); } - struct TestEdgeData - { - public int testNum; - } - [Test] public void EdgeData() { - var myGraph = new DirectedGraph(); + var myGraph = new DirectedGraph(); myGraph.AddNodes(1, 2); - myGraph.AddEdge(1, 2, new TestEdgeData { testNum = 4 }); + myGraph.AddEdge(1, 2, new NumEdgeData { testNum = 4 }); myGraph.EdgeData((1, 2)).testNum.Should().Be(4); } diff --git a/test/DirectedWeightedMultiGraph.cs b/test/DirectedWeightedMultiGraph.cs new file mode 100644 index 0000000..abdf254 --- /dev/null +++ b/test/DirectedWeightedMultiGraph.cs @@ -0,0 +1,252 @@ +using NUnit.Framework; +using FluentAssertions; + +using MoonTools.Core.Graph; +using System.Linq; + +namespace Tests +{ + public class DirectedWeightedMultiGraphTests + { + EdgeData dummyEdgeData; + + [Test] + public void AddNode() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNode(4); + + Assert.That(myGraph.Nodes, Does.Contain(4)); + } + + [Test] + public void AddNodes() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(4, 20, 69); + + myGraph.Exists(4).Should().BeTrue(); + myGraph.Exists(20).Should().BeTrue(); + myGraph.Exists(69).Should().BeTrue(); + } + + [Test] + public void AddEdge() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(5, 6); + myGraph.AddEdge(5, 6, 10, dummyEdgeData); + + myGraph.Neighbors(5).Should().Contain(6); + } + + [Test] + public void AddEdges() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3, 4); + myGraph.AddEdges( + (1, 2, 5, dummyEdgeData), + (2, 3, 6, dummyEdgeData), + (2, 4, 7, dummyEdgeData), + (3, 4, 8, dummyEdgeData) + ); + + myGraph.Neighbors(1).Should().Contain(2); + myGraph.Neighbors(2).Should().Contain(3); + myGraph.Neighbors(2).Should().Contain(4); + myGraph.Neighbors(3).Should().Contain(4); + myGraph.Neighbors(1).Should().NotContain(4); + + myGraph.EdgeIDs(1, 2).Should().HaveCount(1); + myGraph.Weights(1, 2).Should().Contain(5); + } + + [Test] + public void AddMultiEdges() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3, 4); + myGraph.AddEdges( + (1, 2, 5, dummyEdgeData), + (2, 3, 6, dummyEdgeData), + (2, 4, 7, dummyEdgeData), + (2, 4, 8, dummyEdgeData) + ); + + myGraph.Neighbors(1).Should().Contain(2); + myGraph.Neighbors(2).Should().Contain(3); + myGraph.Neighbors(2).Should().Contain(4); + myGraph.Neighbors(1).Should().NotContain(4); + + myGraph.EdgeIDs(2, 4).Should().HaveCount(2); + myGraph.Weights(2, 4).Should().HaveCount(2); + myGraph.Weights(2, 4).Should().Contain(7); + myGraph.Weights(2, 4).Should().Contain(8); + } + + [Test] + public void Clear() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3, 4); + myGraph.AddEdges( + (1, 2, 5, dummyEdgeData), + (2, 3, 6, dummyEdgeData), + (2, 4, 7, dummyEdgeData), + (2, 4, 8, dummyEdgeData) + ); + + myGraph.Clear(); + + myGraph.Invoking(x => x.Neighbors(1)).Should().Throw(); + myGraph.Invoking(x => x.Weights(1, 2)).Should().Throw(); + myGraph.Invoking(x => x.EdgeIDs(1, 2)).Should().Throw(); + } + + [Test] + public void Edges() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (1, 2, 3, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.EdgeIDs(1, 2).Should().HaveCount(2); + myGraph.EdgeIDs(2, 3).Should().HaveCount(1); + } + + [Test] + public void NodeExists() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (1, 2, 3, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Exists(1).Should().BeTrue(); + myGraph.Exists(2).Should().BeTrue(); + myGraph.Exists(3).Should().BeTrue(); + myGraph.Exists(4).Should().BeFalse(); + } + + [Test] + public void EdgeExists() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (1, 2, 3, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Exists(1, 2).Should().BeTrue(); + myGraph.Exists(2, 3).Should().BeTrue(); + myGraph.Exists(1, 3).Should().BeFalse(); + myGraph.Invoking(x => x.Exists(3, 4)).Should().Throw(); + } + + [Test] + public void Neighbors() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (1, 2, 3, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Neighbors(1).Should().Contain(2); + myGraph.Neighbors(2).Should().Contain(3); + myGraph.Neighbors(1).Should().NotContain(3); + myGraph.Invoking(x => x.Neighbors(4)).Should().Throw(); + } + + [Test] + public void Weights() + { + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (1, 2, 3, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Weights(1, 2).Should().Contain(3); + myGraph.Weights(1, 2).Should().Contain(4); + myGraph.Weights(2, 3).Should().Contain(5); + myGraph.Invoking(x => x.Weights(3, 4)).Should().Throw(); + } + + [Test] + public void EdgeData() + { + var a = new NumEdgeData { testNum = 3 }; + var b = new NumEdgeData { testNum = 4 }; + var c = new NumEdgeData { testNum = 5 }; + + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, a), + (1, 2, 3, b), + (2, 3, 5, c) + ); + + myGraph.EdgeIDs(1, 2).Select(id => myGraph.EdgeData(id)).Should().Contain(a); + myGraph.EdgeIDs(1, 2).Select(id => myGraph.EdgeData(id)).Should().Contain(b); + myGraph.EdgeIDs(2, 3).Select(id => myGraph.EdgeData(id)).Should().Contain(c); + myGraph.Invoking(x => x.EdgeData(new System.Guid())).Should().Throw(); + } + + [Test] + public void AStarShortestPath() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'c', 1, jump), + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 2, jump), + ('b', 'd', 5, run), + ('b', 'e', 1, run), + ('c', 'g', 2, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('d', 'f', 2, run), + ('d', 'h', 3, wallJump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump) + ); + + myGraph + .AStarShortestPath('a', 'h', (x, y) => 15) + .Select(id => myGraph.EdgeData(id)) + .Should() + .ContainInOrder( + run, jump, wallJump + ) + .And + .HaveCount(3); + } + } +} \ No newline at end of file diff --git a/test/Structs.cs b/test/Structs.cs new file mode 100644 index 0000000..9afa939 --- /dev/null +++ b/test/Structs.cs @@ -0,0 +1,18 @@ +namespace Tests +{ + public enum MoveType + { + Run, + Jump, + WallJump + } + public struct EdgeData { } + public struct NumEdgeData + { + public int testNum; + } + public struct MoveTypeEdgeData + { + public MoveType moveType; + } +} \ No newline at end of file