diff --git a/gdx-ai/src/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinder.java b/gdx-ai/src/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinder.java index feb862b3..54777d39 100644 --- a/gdx-ai/src/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinder.java +++ b/gdx-ai/src/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinder.java @@ -52,6 +52,7 @@ public class IndexedAStarPathFinder implements PathFinder { NodeRecord[] nodeRecords; BinaryHeap> openList; NodeRecord current; + public StopCondition stopCondition; public Metrics metrics; /** The unique ID for each search run. Used to mark nodes. */ @@ -69,7 +70,8 @@ public IndexedAStarPathFinder (IndexedGraph graph) { public IndexedAStarPathFinder (IndexedGraph graph, boolean calculateMetrics) { this.graph = graph; this.nodeRecords = (NodeRecord[])new NodeRecord[graph.getNodeCount()]; - this.openList = new BinaryHeap>(); + this.openList = new BinaryHeap<>(); + this.stopCondition = new EqualsByReferenceStopCondition<>(); if (calculateMetrics) this.metrics = new Metrics(); } @@ -111,8 +113,8 @@ protected boolean search (N startNode, N endNode, Heuristic heuristic) { current = openList.pop(); current.category = CLOSED; - // Terminate if we reached the goal node - if (current.node == endNode) return true; + // Terminate if we reached the stop condition + if (stopCondition.shouldStopSearch(current.node, endNode)) return true; visitChildren(endNode, heuristic); @@ -145,8 +147,8 @@ public boolean search (PathFinderRequest request, long timeToRun) { current = openList.pop(); current.category = CLOSED; - // Terminate if we reached the goal node; we've found a path. - if (current.node == request.endNode) { + // Terminate if we reached the stop condition; we've found a path. + if (stopCondition.shouldStopSearch(current.node, request.endNode)) { request.pathFound = true; generateNodePath(request.startNode, request.resultPath); @@ -339,4 +341,25 @@ public void reset () { openListPeak = 0; } } + + /** This interface is used to define criteria to interrupt the search. + * + * @param Type of node + * + * @author niemandkun */ + public interface StopCondition { + boolean shouldStopSearch(N currentNode, N endNode); + } + + /** Default implementation of {@link StopCondition}, which compares two given nodes by reference. + * + * @param Type of node + * + * @author niemandkun */ + private static class EqualsByReferenceStopCondition implements StopCondition { + @Override + public boolean shouldStopSearch(N currentNode, N endNode) { + return currentNode == endNode; + } + } } diff --git a/gdx-ai/tests/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinderTest.java b/gdx-ai/tests/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinderTest.java index 5711d48d..eb03ac26 100644 --- a/gdx-ai/tests/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinderTest.java +++ b/gdx-ai/tests/com/badlogic/gdx/ai/pfa/indexed/IndexedAStarPathFinderTest.java @@ -29,7 +29,7 @@ public class IndexedAStarPathFinderTest { @Test - public void searchNodePath_WhenSearchingAdjacentTile_ExpectedOuputPathLengthEquals2 () { + public void searchNodePath_WhenSearchingAdjacentTile_ExpectedOutputPathLengthEquals2 () { // @off - disable libgdx formatter final String graphDrawing = "..........\n" + @@ -96,7 +96,7 @@ public void searchNodePath_WhenSearchingAdjacentTile_ExpectedOuputPathLengthEqua } @Test - public void searchNodePath_WhenSearchCanHitDeadEnds_ExpectedOuputPathFound () { + public void searchNodePath_WhenSearchCanHitDeadEnds_ExpectedOutputPathFound () { // @off - disable libgdx formatter final String graphDrawing = ".#.#.......#..#...............\n" + @@ -138,7 +138,7 @@ public void searchNodePath_WhenSearchCanHitDeadEnds_ExpectedOuputPathFound () { } @Test - public void searchNodePath_WhenDestinationUnreachable_ExpectedNoOuputPathFound () { + public void searchNodePath_WhenDestinationUnreachable_ExpectedNoOutputPathFound () { // @off - disable libgdx formatter final String graphDrawing = ".....#....\n" + @@ -164,6 +164,87 @@ public void searchNodePath_WhenDestinationUnreachable_ExpectedNoOuputPathFound ( Assert.assertFalse("Unexpected search result", searchResult); } + @Test + public void searchNodePath_WhenGraphIsUpdatingOnTheFly_ExpectedFailToFindEndByReference() { + // @off - disable libgdx formatter + final String graphDrawing = + "..."; + // @on - enable libgdx formatter + + final MyDynamicGraph dynamicGraph = new MyDynamicGraph( + createGraphFromTextRepresentation(graphDrawing), + new MyNodesFactory() { + @Override + public MyNode getNewInstance(MyNode old) { + MyNode newNode = new MyNode(old.index, old.x, old.y, old.connections.size); + newNode.connections.addAll(old.connections); + return newNode; + } + } + ); + + final IndexedAStarPathFinder pathfinder = new IndexedAStarPathFinder<>(dynamicGraph); + + final GraphPath outPath = new DefaultGraphPath<>(); + + // @off - disable libgdx formatter + // 012 + // S.E 0 + // @on - enable libgdx formatter + final boolean searchResult = pathfinder.searchNodePath( + dynamicGraph.graph.nodes.get(0), + dynamicGraph.graph.nodes.get(2), + new ManhattanDistance(), + outPath + ); + + Assert.assertFalse("Unexpected search result", searchResult); + } + + @Test + public void searchNodePath_WhenGraphIsUpdatedOnTheFly_ExpectedSucceedToFindEndByEquals() { + // @off - disable libgdx formatter + final String graphDrawing = + "..."; + // @on - enable libgdx formatter + + final MyDynamicGraph dynamicGraph = new MyDynamicGraph( + createGraphFromTextRepresentation(graphDrawing), + new MyNodesFactory() { + @Override + public MyNode getNewInstance(MyNode old) { + MyNode newNode = new MyNodeWithEquals(old.index, old.x, old.y, old.connections.size); + newNode.connections.addAll(old.connections); + return newNode; + } + } + ); + + final IndexedAStarPathFinder pathfinder = new IndexedAStarPathFinder<>(dynamicGraph); + pathfinder.stopCondition = new IndexedAStarPathFinder.StopCondition() { + @Override + public boolean shouldStopSearch(MyNode currentNode, MyNode endNode) { + return currentNode.equals(endNode); + } + }; + + final GraphPath outPath = new DefaultGraphPath<>(); + + // @off - disable libgdx formatter + // 012 + // S.E 0 + // @on - enable libgdx formatter + final boolean searchResult = pathfinder.searchNodePath( + dynamicGraph.graph.nodes.get(0), + dynamicGraph.graph.nodes.get(2), + new ManhattanDistance(), + outPath + ); + + Assert.assertTrue("Unexpected search result", searchResult); + Assert.assertEquals("Unexpected number of nodes in path", 3, outPath.getCount()); + } + private static MyGraph createGraphFromTextRepresentation (final String graphTextRepresentation) { final String[][] tiles = createStringTilesFromGraphTextRepresentation(graphTextRepresentation); @@ -229,7 +310,7 @@ private static String[][] createStringTilesFromGraphTextRepresentation (final St private static class MyNode { - private final int index; + final int index; private final int x; private final int y; private final Array> connections; @@ -256,6 +337,22 @@ public String toString () { } + private static class MyNodeWithEquals extends MyNode { + + MyNodeWithEquals(int index, int x, int y, int capacity) { + super(index, x, y, capacity); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MyNode)) { + return false; + } + MyNode otherNode = (MyNode) other; + return this.index == otherNode.index; + } + } + private static class MyGraph implements IndexedGraph { protected Array nodes; @@ -280,6 +377,49 @@ public int getNodeCount () { } } + private interface MyNodesFactory { + + MyNode getNewInstance(MyNode old); + } + + /** + * Works like {@link MyGraph}, but each time when {@link MyDynamicGraph#getConnections} is called, + * it creates new instances of {@link MyNode} using given {@link MyNodesFactory}. + */ + private static class MyDynamicGraph implements IndexedGraph { + + private MyGraph graph; + private MyNodesFactory factory; + + MyDynamicGraph(MyGraph graph, MyNodesFactory factory) { + this.graph = graph; + this.factory = factory; + } + + @Override + public Array> getConnections(MyNode fromNode) { + Array> connections = graph.getConnections(fromNode); + Array> newInstanceConnections = new Array<>(connections.size); + for (Connection connection : connections) { + newInstanceConnections.add(new DefaultConnection<>( + factory.getNewInstance(connection.getFromNode()), + factory.getNewInstance(connection.getToNode()) + )); + } + return newInstanceConnections; + } + + @Override + public int getIndex(MyNode node) { + return graph.getIndex(node); + } + + @Override + public int getNodeCount() { + return graph.getNodeCount(); + } + } + private static class ManhattanDistance implements Heuristic { @Override public float estimate (final MyNode node, final MyNode endNode) {