package com.pointcliki.dizgruntled.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.TreeSet;
import com.pointcliki.dizgruntled.LevelScene;
import com.pointcliki.dizgruntled.grunt.GruntState;
import com.pointcliki.dizgruntled.logic.Grunt;
import com.pointcliki.dizgruntled.map.Map;
import com.pointcliki.grid.GridCoordinate;
public class PathFinder {
protected LevelScene fScene;
protected Map fMap;
public PathFinder(LevelScene scene) {
fScene = scene;
fMap = fScene.mapManager().map();
}
public List<GridCoordinate> calculate(GridCoordinate xy, GridCoordinate finish, int maxIterations) {
PriorityQueue<PossibleTile> tiles = new PriorityQueue<PossibleTile>();
HashMap<String, PossibleTile> searched = new HashMap<String, PossibleTile>();
PossibleTile start = new PossibleTile(xy, null, 0, xy.distance(finish));
PossibleTile end = null;
tiles.add(start);
searched.put(xy.toString(), start);
while (maxIterations > 0 && !tiles.isEmpty()) {
maxIterations--;
PossibleTile poll = tiles.poll();
// Check for end
if (poll.xy.equals(finish)) {
end = poll;
break;
}
// Check for passable
if (!passable(poll.xy)) continue;
// Add neighbours
ArrayList<PossibleTile> poss = new ArrayList<PossibleTile>(8);
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.NORTH), poll, poll.pathWeight + 1, poll.xy.add(GridCoordinate.NORTH).distance(finish)));
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.EAST), poll, poll.pathWeight + 1, poll.xy.add(GridCoordinate.EAST).distance(finish)));
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.SOUTH), poll, poll.pathWeight + 1, poll.xy.add(GridCoordinate.SOUTH).distance(finish)));
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.WEST), poll, poll.pathWeight + 1, poll.xy.add(GridCoordinate.WEST).distance(finish)));
// Get traits for locals
TreeSet<String> traitsNorth = fMap.traits(poll.xy.add(GridCoordinate.NORTH));
TreeSet<String> traitsEast = fMap.traits(poll.xy.add(GridCoordinate.EAST));
TreeSet<String> traitsSouth = fMap.traits(poll.xy.add(GridCoordinate.SOUTH));
TreeSet<String> traitsWest = fMap.traits(poll.xy.add(GridCoordinate.WEST));
if (!traitsNorth.contains("solid") && !traitsEast.contains("solid"))
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.NORTH_EAST), poll, poll.pathWeight + 1.41421f, poll.xy.add(GridCoordinate.NORTH_EAST).distance(finish)));
if (!traitsEast.contains("solid") && !traitsSouth.contains("solid"))
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.SOUTH_EAST), poll, poll.pathWeight + 1.41421f, poll.xy.add(GridCoordinate.SOUTH_EAST).distance(finish)));
if (!traitsSouth.contains("solid") && !traitsWest.contains("solid"))
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.SOUTH_WEST), poll, poll.pathWeight + 1.41421f, poll.xy.add(GridCoordinate.SOUTH_WEST).distance(finish)));
if (!traitsWest.contains("solid") && !traitsNorth.contains("solid"))
poss.add(new PossibleTile(poll.xy.add(GridCoordinate.NORTH_WEST), poll, poll.pathWeight + 1.41421f, poll.xy.add(GridCoordinate.NORTH_WEST).distance(finish)));
for (PossibleTile other: poss) {
if (!fMap.isValid(other.xy)) continue;
if (!searched.containsKey(other.xy.toString())) {
searched.put(other.xy.toString(), other);
tiles.add(other);
} else {
PossibleTile a = searched.get(other.xy.toString());
if (a.pathWeight + a.approxWeight > other.pathWeight + other.approxWeight) {
searched.put(other.xy.toString(), other);
tiles.remove(a);
tiles.add(other);
}
}
}
}
if (end == null) return null;
LinkedList<GridCoordinate> path = new LinkedList<GridCoordinate>();
PossibleTile next = end;
while (next != null) {
path.add(next.xy);
next = next.parent;
}
Collections.reverse(path);
// Perform diagonal optimization
for (int i = 0; i < path.size() - 1; i++) {
// Can only optimize straight paths so ignore diagonal ones
GridCoordinate lineDir = path.get(i + 1).subtract(path.get(i));
if (lineDir.isDiagonal()) continue;
// Look ahead for the last diagonal in the next run of diagonals
boolean inRun = false;
boolean found = false;
int j = i + 1;
GridCoordinate diagDir = null;
while (j < path.size() - 1) {
GridCoordinate jDir = path.get(j + 1).subtract(path.get(j));
// Start following a diagonal run
if (jDir.isDiagonal() && !inRun) {
inRun = true;
found = true;
diagDir = jDir;
// Ensure we can go diagonally from i
if (!diagMove(path.get(i), diagDir)) {
found = false;
break;
}
// Stop following a diagonal run
} else if (inRun && !jDir.equals(diagDir)) {
break;
// Only go in straight lines
} else if ((jDir.isDiagonal() && !jDir.equals(diagDir)) || (!jDir.isDiagonal() && !jDir.equals(lineDir))) {
found = false;
break;
}
j++;
}
// If a possible optimization has been found
if (found) {
// Get distance
GridCoordinate dist = path.get(j).subtract(path.get(i));
// Get perpendicular and parallel distance
int u; // Perpendicular
int v; // Parallel
if (Math.abs(lineDir.x()) == 1) {
u = Math.abs(dist.y());
v = Math.abs(dist.x());
} else {
u = Math.abs(dist.x());
v = Math.abs(dist.y());
}
ArrayList<GridCoordinate> segment = new ArrayList<GridCoordinate>();
// Found
boolean segFound = true;
// Iterate over columns to find an optimal path segment
for (int m = u; m > 0; m--) {
GridCoordinate tile = path.get(i).add(diagDir);
// Try to draw a better path
segment.clear();
segFound = true;
// Iterate over rows
for (int n = 1; n < v + m - u; n++) {
if (!fMap.isValid(tile) || !passable(tile)) {
segFound = false;
break;
}
segment.add(tile);
if (n < m) {
// Ensure that we can move diagonally
if (!diagMove(tile, diagDir)) {
segFound = false;
break;
}
tile = tile.add(diagDir);
}
else tile = tile.add(lineDir);
}
if (segFound) break;
// Reduce j, the possible end point
j--;
}
// If a path segment is found, replace the old path
if (segFound) {
// Remove old path
List<GridCoordinate> sub = path.subList(i + 1, j);
sub.clear();
for (GridCoordinate tile: segment) sub.add(tile);
}
}
}
return path;
}
protected boolean passable(GridCoordinate tile) {
TreeSet<String> traits = fScene.mapManager().map().traits(tile);
if (traits.contains("nogo") || traits.contains("solid") || traits.contains("water")) return false;
Grunt g = fScene.gridManager().getFirstEntityOfTypeAt(tile, Grunt.class);
if (g != null && g.state() != GruntState.MOVING) return false;
return true;
}
protected boolean diagMove(GridCoordinate tile, GridCoordinate diagDir) {
GridCoordinate a = tile.add(new GridCoordinate(diagDir.x(), 0));
GridCoordinate b = tile.add(new GridCoordinate(0, diagDir.y()));
return !fMap.traits(a).contains("solid") && !fMap.traits(b).contains("solid");
}
protected class PossibleTile implements Comparable<PossibleTile> {
public GridCoordinate xy;
public PossibleTile parent;
public float pathWeight; // Weight of the path taken so far
public float approxWeight; // Approximate length of the remaining path
public PossibleTile(GridCoordinate xy, PossibleTile parent, float pathWeight, float approxWeight) {
this.xy = xy;
this.parent = parent;
this.pathWeight = pathWeight;
this.approxWeight = approxWeight;
}
@Override
public int compareTo(PossibleTile tile) {
if (tile.xy.equals(xy)) return 0;
if (pathWeight + approxWeight > tile.pathWeight + tile.approxWeight) return 1;
return -1;
}
@Override
public boolean equals(Object tile) {
return (tile instanceof PossibleTile && ((PossibleTile) tile).xy.equals(xy));
}
@Override
public int hashCode() {
return xy.hashCode();
}
public String toString() {
return xy.toString() + ": " + (pathWeight + approxWeight) + " <- " + ((parent != null) ? parent.xy.toString() : "");
}
}
}