package org.drooms.impl.logic;
import edu.uci.ics.jung.algorithms.shortestpath.ShortestPath;
import edu.uci.ics.jung.algorithms.shortestpath.ShortestPathUtils;
import edu.uci.ics.jung.algorithms.shortestpath.UnweightedShortestPath;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.UndirectedGraph;
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
import edu.uci.ics.jung.graph.util.Graphs;
import org.drooms.api.Edge;
import org.drooms.api.Node;
import org.drooms.api.Player;
import org.drooms.api.Playground;
import java.util.*;
import java.util.stream.Collectors;
/**
* A helper class for the strategies to be able to quickly and easily find paths
* from one {@link Node} to another.
*/
public class PathTracker {
protected static <V, E> List<E> getPath(final Graph<V, E> graph, final V start, final Set<V> otherNodeSet) {
return PathTracker.getPath(graph, start, otherNodeSet, new UnweightedShortestPath<>(graph));
}
// synchronize access to the shortest path algorithm, otherwise it breaks down horribly in parallel streams
private static synchronized <V, E> List<E> getPath(final Graph<V, E> graph, final ShortestPath<V, E>
shortestPathAlgorithm, final V start, final V end) {
return ShortestPathUtils.getPath(graph, shortestPathAlgorithm, start, end);
}
/**
* Finds the shortest path through given nodes.
*
* @param graph Graph to look inside of.
* @param start The starting node for the path.
* @param otherNodeSet All the nodes to visit.
* @param shortestPathAlgorithm The algorithm for constructing shortest path. Must be thread-safe.
* @param <V> Type of node in the graph.
* @param <E> Type of edge in the graph.
* @return List of edges on the path, or empty if no path.
*/
private static <V, E> List<E> getPath(final Graph<V, E> graph, final V start, final Set<V> otherNodeSet,
final ShortestPath<V, E> shortestPathAlgorithm) {
if (start == null || otherNodeSet == null || otherNodeSet.size() == 0) {
throw new IllegalArgumentException("Please provide both a start node and a set of other nodes.");
}
// some nodes make no sense; start node is included implicitly, null nodes are nonsense
final Set<V> filteredNodeSet = otherNodeSet.stream().filter(node -> !(node == null || node.equals(start)))
.collect(Collectors.toSet());
/*
* a brute-force algorithm to recursively find the shortest path. this algorithm picks all the other nodes,
* one after another, and will call this algorithm again with the chosen node as the starting node and the
* remaining nodes as the other nodes. it will try to be smart and not include among the other nodes the
* nodes that it already went through earlier in the recursion.
*/
final List<E> path = filteredNodeSet.parallelStream().map(newStart -> {
// path to this node
final List<E> totalPath = PathTracker.getPath(graph, shortestPathAlgorithm, start, newStart);
if (totalPath.size() == 0) { // no route exists between two nodes; path is impossible
return null;
}
// if any of the other nodes are in the above path already, don't go through them again
final Set<V> nodesToTraverse = new LinkedHashSet<>(filteredNodeSet);
totalPath.forEach(edge -> nodesToTraverse.removeAll(graph.getIncidentVertices(edge)));
if (nodesToTraverse.size() > 0) {
// find the new path and merge it
final List<E> remainingPath = PathTracker.getPath(graph, newStart, Collections.unmodifiableSet
(nodesToTraverse), shortestPathAlgorithm);
if (remainingPath.size() == 0) { // no route exists between remaining nodes; path is impossible
return null;
}
totalPath.addAll(remainingPath);
}
return totalPath;
}).filter(edges -> !(edges == null || edges.isEmpty())).min((edges, edges2) -> {
final int size1 = edges.size();
final int size2 = edges2.size();
if (size1 > size2) {
return 1;
} else if (size1 == size2) {
return 0;
} else {
return -1;
}
}).orElseGet(Collections::emptyList);
return Collections.unmodifiableList(path);
}
/**
* Create a clone of the original graph with certain nodes removed.
*
* @param src Original graph.
* @param removeNodes Nodes to remove from the original graph.
* @return Original graph, except all the edges with specified incident nodes are removed.
*/
private static UndirectedGraph<Node, Edge> cloneGraph(final Graph<Node, Edge> src,
final Collection<Node> removeNodes) {
final UndirectedGraph<Node, Edge> clone = new UndirectedSparseGraph<>();
src.getEdges().forEach(e -> {
final boolean isEdgeAdded = clone.addEdge(e, src.getIncidentVertices(e));
if (!isEdgeAdded) {
throw new IllegalStateException("Failed cloning graph. This surely is a bug in Drooms.");
}
});
removeNodes.forEach(node -> clone.removeVertex(node));
return clone;
}
private Graph<Node, Edge> currentGraph;
private ShortestPath<Node, Edge> currentPath;
private final Player player;
private final Playground playground;
/**
* Initialize the class.
*
* @param playground The playground to base the path-finding algos on.
* @param p The player that will be using this particular instance.
*/
public PathTracker(final Playground playground, final Player p) {
this.playground = playground;
this.player = p;
}
public List<Edge> getPath(final Node start, final Set<Node> otherNodeSet) {
return PathTracker.getPath(this.currentGraph, start, otherNodeSet, this.currentPath);
}
/**
* Find the shortest path from the start node that leads through the other nodes regardless of their order.
* <p>
* This is effectively TSP, so try to keep the amount of nodes very, very small. :-)
*
* @param start Beginning of the path.
* @param otherNodes All the other nodes to go through. If this includes the start node, it will be ignored. Null
* nodes will be ignored as well.
* @return Unmodifiable list of nodes on the path, ordered from start to end. Empty if path cannot be found.
*/
public List<Edge> getPath(final Node start, final Node... otherNodes) {
return this.getPath(start, Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(otherNodes))));
}
public Player getPlayer() {
return this.player;
}
public Playground getPlayground() {
return this.playground;
}
/**
* Update the internal state of this class so that future paths can avoid
* places where the worms currently reside.
*
* @param newPositions New current positions of all the worms.
*/
protected void updatePlayerPositions(final Map<Player, Collection<Node>> newPositions, final Node currentHead) {
if (currentHead == null || !newPositions.get(this.player).contains(currentHead)) {
throw new IllegalStateException("Invalid worm head node: " + currentHead);
} else if (newPositions.isEmpty()) {
this.currentGraph = Graphs.unmodifiableGraph(this.playground.getGraph());
} else {
// enumerate all the nodes occupied by worms at this point
final Set<Node> unavailable = new LinkedHashSet<>();
newPositions.forEach((player, nodes) -> unavailable.addAll(nodes));
// make sure we keep the head node, since otherwise there is no path from the current position to any other
unavailable.remove(currentHead);
// update internal structures
this.currentGraph = Graphs.unmodifiableUndirectedGraph(PathTracker.cloneGraph(this.playground.getGraph(),
unavailable));
}
this.currentPath = new UnweightedShortestPath<>(this.currentGraph);
}
}