Skip to content

Commit

Permalink
Merge pull request #105 from niemandkun/astar-equals-fix
Browse files Browse the repository at this point in the history
Add stop condition to IndexedAStarPathFinder
  • Loading branch information
tommyettinger authored Nov 25, 2021
2 parents 122a9e9 + 7c372d1 commit e8ecb9f
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class IndexedAStarPathFinder<N> implements PathFinder<N> {
NodeRecord<N>[] nodeRecords;
BinaryHeap<NodeRecord<N>> openList;
NodeRecord<N> current;
public StopCondition<N> stopCondition;
public Metrics metrics;

/** The unique ID for each search run. Used to mark nodes. */
Expand All @@ -69,7 +70,8 @@ public IndexedAStarPathFinder (IndexedGraph<N> graph) {
public IndexedAStarPathFinder (IndexedGraph<N> graph, boolean calculateMetrics) {
this.graph = graph;
this.nodeRecords = (NodeRecord<N>[])new NodeRecord[graph.getNodeCount()];
this.openList = new BinaryHeap<NodeRecord<N>>();
this.openList = new BinaryHeap<>();
this.stopCondition = new EqualsByReferenceStopCondition<>();
if (calculateMetrics) this.metrics = new Metrics();
}

Expand Down Expand Up @@ -111,8 +113,8 @@ protected boolean search (N startNode, N endNode, Heuristic<N> 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);

Expand Down Expand Up @@ -145,8 +147,8 @@ public boolean search (PathFinderRequest<N> 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);
Expand Down Expand Up @@ -339,4 +341,25 @@ public void reset () {
openListPeak = 0;
}
}

/** This interface is used to define criteria to interrupt the search.
*
* @param <N> Type of node
*
* @author niemandkun */
public interface StopCondition<N> {
boolean shouldStopSearch(N currentNode, N endNode);
}

/** Default implementation of {@link StopCondition}, which compares two given nodes by reference.
*
* @param <N> Type of node
*
* @author niemandkun */
private static class EqualsByReferenceStopCondition<N> implements StopCondition<N> {
@Override
public boolean shouldStopSearch(N currentNode, N endNode) {
return currentNode == endNode;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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" +
Expand Down Expand Up @@ -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" +
Expand Down Expand Up @@ -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" +
Expand All @@ -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<MyNode> pathfinder = new IndexedAStarPathFinder<>(dynamicGraph);

final GraphPath<MyNode> 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<MyNode> pathfinder = new IndexedAStarPathFinder<>(dynamicGraph);
pathfinder.stopCondition = new IndexedAStarPathFinder.StopCondition<MyNode>() {
@Override
public boolean shouldStopSearch(MyNode currentNode, MyNode endNode) {
return currentNode.equals(endNode);
}
};

final GraphPath<MyNode> 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);

Expand Down Expand Up @@ -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<Connection<MyNode>> connections;
Expand All @@ -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<MyNode> {

protected Array<MyNode> nodes;
Expand All @@ -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<MyNode> {

private MyGraph graph;
private MyNodesFactory factory;

MyDynamicGraph(MyGraph graph, MyNodesFactory factory) {
this.graph = graph;
this.factory = factory;
}

@Override
public Array<Connection<MyNode>> getConnections(MyNode fromNode) {
Array<Connection<MyNode>> connections = graph.getConnections(fromNode);
Array<Connection<MyNode>> newInstanceConnections = new Array<>(connections.size);
for (Connection<MyNode> 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<MyNode> {
@Override
public float estimate (final MyNode node, final MyNode endNode) {
Expand Down

0 comments on commit e8ecb9f

Please sign in to comment.