package onlinefrontlines.utils;
import java.util.*;
import java.awt.Point;
/**
* Tile layout for a game
*
* @author jorrit
*
* Copyright (C) 2009-2013 Jorrit Rouwe
*
* This file is part of Online Frontlines.
*
* Online Frontlines is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Online Frontlines is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Online Frontlines. If not, see <http://www.gnu.org/licenses/>.
*/
public class HexagonGrid
{
/**
* Number of tiles in the horizontal direction
*/
public final int sizeX;
/**
* Number of tiles in the vertical direction
*/
public final int sizeY;
/**
* Neighbour table
*/
private static Point neighbourTable[][] = {
{ // Odd rows
new Point(0, -1),
new Point(1, -1),
new Point(1, 0),
new Point(1, 1),
new Point(0, 1),
new Point(-1, 0)
},
{ // Even rows
new Point(-1, -1),
new Point(0, -1),
new Point(1, 0),
new Point(0, 1),
new Point(-1, 1),
new Point(-1, 0)
}
};
/**
* Constructor
*
* @param sizeX Horizontal amount of tiles
* @param sizeY Vertical amount of tiles
*/
public HexagonGrid(int sizeX, int sizeY)
{
this.sizeX = sizeX;
this.sizeY = sizeY;
}
/**
* Copy constructor
*/
public HexagonGrid(HexagonGrid other)
{
sizeX = other.sizeX;
sizeY = other.sizeY;
}
/**
* Get shortest distance from one tile to the next
*/
public static int getDistance(int x1, int y1, int x2, int y2)
{
// Trivial calculation if on same row
if (y1 == y2)
return Math.abs(x2 - x1);
// Otherwise start looking for neighbours
int bestX = x1;
int bestY = y1;
Point n[] = neighbourTable[y1 & 1];
for (int i = 0; i < n.length; ++i)
{
// Try out this neighbour
int xr = x1 + n[i].x;
int yr = y1 + n[i].y;
// Check if it is the closest result so far
if (Math.abs(x2 - xr) <= Math.abs(x2 - bestX) // Find the best match in X direction
&& Math.abs(y2 - yr) < Math.abs(y2 - y1)) // Must be in the row closer to our destination
{
// Store it
bestX = xr;
bestY = yr;
}
}
// Recurse to best candidate
return getDistance(bestX, bestY, x2, y2) + 1;
}
/**
* Get shortest distance from one tile to the next
*/
public static int getDistance(Point from, Point to)
{
return getDistance(from.x, from.y, to.x, to.y);
}
/**
* Check if tile is in range
*/
public boolean isTileInRange(int x, int y)
{
return x >= 0 && x < sizeX && y >= 0 && y < sizeY;
}
/**
* Check if tile is in playable area
*/
public boolean isTileInPlayableArea(int x, int y)
{
if (y <= 0 || y >= sizeY - 1)
return false;
if ((y & 1) == 0)
return x >= 0 && x < sizeX - 1;
else
return x >= 1 && x < sizeX;
}
/**
* Check if tile is in playable area
*/
public boolean isTileInPlayableArea(Point p)
{
return isTileInPlayableArea(p.x, p.y);
}
/**
* Get neighbouring tiles
*/
public ArrayList<Point> getNeighbours(int x, int y)
{
ArrayList<Point> r = new ArrayList<Point>();
Point n[] = neighbourTable[y & 1];
for (int i = 0; i < n.length; ++i)
{
int xr = x + n[i].x;
int yr = y + n[i].y;
if (isTileInRange(xr, yr))
r.add(new Point(xr, yr));
}
return r;
}
/**
* Get neighbouring tiles
*/
public ArrayList<Point> getNeighbours(Point p)
{
return getNeighbours(p.x, p.y);
}
/**
* Helper class representing a node in the A* algorithm
*/
public class AStarNode implements Comparable<AStarNode>
{
public Point point;
public float costFromStart;
public float estimatedCostToGoal;
public AStarNode cameFrom;
public AStarNode(Point point, float costFromStart, float estimatedCostToGoal, AStarNode pathParent)
{
this.point = point;
this.costFromStart = costFromStart;
this.estimatedCostToGoal = estimatedCostToGoal;
this.cameFrom = pathParent;
}
public float getCost()
{
return costFromStart + estimatedCostToGoal;
}
@Override
public int compareTo(AStarNode other)
{
float diff = getCost() - other.getCost();
return diff > 0? 1 : (diff < 0? -1 : 0);
}
}
/**
* Get cost going from 'from' to 'to'
*/
public interface CostFunction
{
public float getCost(AStarNode from, Point to);
}
/**
* Get (under)estimated cost to goal
*/
public interface CostEstimator
{
public float getEstimatedCost(Point from, Point goal);
}
/**
* The result of the plan action
*/
public static class PlanResult
{
public ArrayList<Point> path;
public float cost;
public PlanResult(ArrayList<Point> path, float cost)
{
this.path = path;
this.cost = cost;
}
}
/**
* Find path using cost function
*/
public PlanResult planPath(Point from, Point to, CostFunction costFunction, CostEstimator costEstimator)
{
Queue<AStarNode> openSet = new PriorityQueue<AStarNode>();
Set<Point> closedSet = new HashSet<Point>();
Map<Point, AStarNode> pointToNode = new HashMap<Point, AStarNode>();
// Add first node
AStarNode startNode = new AStarNode(from, 0, costEstimator.getEstimatedCost(from, to), null);
openSet.add(startNode);
pointToNode.put(startNode.point, startNode);
while (!openSet.isEmpty())
{
// Get best from open set
AStarNode node = openSet.poll();
// Add to closed list
closedSet.add(node.point);
// Check if path found
if (node.point.equals(to))
{
// Remember cost
float cost = node.costFromStart;
// Construct the path from start to goal
ArrayList<Point> path = new ArrayList<Point>();
while (node != null)
{
path.add(node.point);
node = node.cameFrom;
}
Collections.reverse(path);
// Return path
return new PlanResult(path, cost);
}
for (Point n : getNeighbours(node.point))
{
// Don't consider points outside playable area
if (!isTileInPlayableArea(n))
continue;
// Don't consider if already in closed list
if (closedSet.contains(n))
continue;
// Calculate cost using this path
float tentativeCost = node.costFromStart + costFunction.getCost(node, n);
// Check if we already have a node
AStarNode current = pointToNode.get(n);
if (current == null)
{
// Create new node
AStarNode newNode = new AStarNode(n, tentativeCost, costEstimator.getEstimatedCost(n, to), node);
openSet.add(newNode);
pointToNode.put(newNode.point, newNode);
}
else if (tentativeCost < current.costFromStart)
{
// Update new cost
current.cameFrom = node;
current.costFromStart = tentativeCost;
}
}
}
// No path found
return null;
}
/**
* Take a list of points and get the set of points less than or equal to distance away
*/
public List<Point> extendBy(List<Point> points, int distance)
{
HashSet<Point> set = new HashSet<Point>(points);
for (int i = 0; i < distance; ++i)
for (Point p : new ArrayList<Point>(set))
for (Point n : getNeighbours(p))
if (isTileInPlayableArea(n))
set.add(n);
return new ArrayList<Point>(set);
}
/**
* Take a single point and get the set of points less than or equal to distance away
*/
public List<Point> extendBy(Point point, int distance)
{
ArrayList<Point> list = new ArrayList<Point>();
list.add(point);
return extendBy(list, distance);
}
/**
* Find path using cost function
*/
public ArrayList<Point> getReachableArea(Point from, CostFunction costFunction, float maxCost)
{
Queue<AStarNode> openSet = new PriorityQueue<AStarNode>();
Set<Point> closedSet = new HashSet<Point>();
Map<Point, AStarNode> pointToNode = new HashMap<Point, AStarNode>();
// Add first node
AStarNode startNode = new AStarNode(from, 0, 0, null);
openSet.add(startNode);
pointToNode.put(startNode.point, startNode);
while (!openSet.isEmpty())
{
// Get best from open set
AStarNode node = openSet.poll();
// Add to closed list
closedSet.add(node.point);
for (Point n : getNeighbours(node.point))
{
// Don't consider points outside playable area
if (!isTileInPlayableArea(n))
continue;
// Don't consider if already in closed list
if (closedSet.contains(n))
continue;
// Calculate cost using this path
float tentativeCost = node.costFromStart + costFunction.getCost(node, n);
if (tentativeCost > maxCost)
continue;
// Check if we already have a node
AStarNode current = pointToNode.get(n);
if (current == null)
{
// Create new node
AStarNode newNode = new AStarNode(n, tentativeCost, 0, node);
openSet.add(newNode);
pointToNode.put(newNode.point, newNode);
}
else if (tentativeCost < current.costFromStart)
{
// Update new cost
current.cameFrom = node;
current.costFromStart = tentativeCost;
}
}
}
// Return list of visited nodes
return new ArrayList<Point>(closedSet);
}
}