/**
* Copyright (C) 2002-2012 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol 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 2 of the License, or
* (at your option) any later version.
*
* FreeCol 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 FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.common.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.HashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
import java.util.logging.Logger;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import net.sf.freecol.common.model.pathfinding.CostDecider;
import net.sf.freecol.common.model.pathfinding.CostDeciders;
import net.sf.freecol.common.model.pathfinding.GoalDecider;
import net.sf.freecol.common.model.pathfinding.GoalDeciders;
import net.sf.freecol.common.util.Utils;
/**
* A rectangular isometric map. The map is represented as a
* two-dimensional array of tiles. Off-map destinations, such as
* {@link Europe}, can be reached via the {@link HighSeas}.
*
* In theory, a {@link Game} might contain several Map instances
* connected by the HighSeas.
*/
public class Map extends FreeColGameObject implements Location {
private static final Logger logger = Logger.getLogger(Map.class.getName());
/**
* Possible actions by the unit travelling along a path in consideration
* of the next tile.
*/
private static enum MoveStep { FAIL, BYLAND, BYWATER, EMBARK, DISEMBARK };
/**
* The number of tiles from the upper edge that are considered
* polar by default.
*/
public final static int POLAR_HEIGHT = 2;
/**
* The layers included in the map. The RIVERS layer includes all
* natural tile improvements that are not resources. The NATIVES
* layer includes Lost City Rumours as well as settlements.
*/
public static enum Layer {
NONE, LAND, TERRAIN, RIVERS, RESOURCES, NATIVES, ALL;
};
/**
* The directions a Unit can move to. Includes deltas for moving
* to adjacent squares, which are required due to the isometric
* map. Starting north and going clockwise.
*/
public static enum Direction {
N ( 0, -2, 0, -2),
NE ( 1, -1, 0, -1),
E ( 1, 0, 1, 0),
SE ( 1, 1, 0, 1),
S ( 0, 2, 0, 2),
SW ( 0, 1, -1, 1),
W (-1, 0, -1, 0),
NW ( 0, -1, -1, -1);
public final static int NUMBER_OF_DIRECTIONS = values().length;
public final static Direction[] longSides = new Direction[] {
Direction.NE, Direction.SE, Direction.SW, Direction.NW
};
public static final Direction[] corners = new Direction[] {
Direction.N, Direction.E, Direction.S, Direction.W
};
private int oddDX, oddDY, evenDX, evenDY;
Direction(int oddDX, int oddDY, int evenDX, int evenDY) {
this.oddDX = oddDX;
this.oddDY = oddDY;
this.evenDX = evenDX;
this.evenDY = evenDY;
}
public int getOddDX() {
return oddDX;
}
public int getOddDY() {
return oddDY;
}
public int getEvenDX() {
return evenDX;
}
public int getEvenDY() {
return evenDY;
}
/**
* Gets this direction rotated by n places.
*
* @param n The number of places to rotate.
* @return The rotated direction.
*/
private Direction rotate(int n) {
return values()[(ordinal() + n + NUMBER_OF_DIRECTIONS)
% NUMBER_OF_DIRECTIONS];
}
/**
* Get the next direction after this one (clockwise).
*
* @return The next <code>Direction</code>.
*/
public Direction getNextDirection() {
return rotate(1);
}
/**
* Get the previous direction after this one (anticlockwise).
*
* @return The previous <code>Direction</code>.
*/
public Direction getPreviousDirection() {
return rotate(-1);
}
/**
* Returns the reverse direction of the given direction.
*
* @return The reverse <code>Direction</code>.
*/
public Direction getReverseDirection() {
return rotate(NUMBER_OF_DIRECTIONS/2);
}
public String getNameKey() {
return "direction." + this.toString();
}
/**
* Gets a random Direction.
*
* @param logMe A string to log with the random results.
* @param random A <code>Random</code> number source.
* @return A random <code>Direction</code> value.
*/
public static Direction getRandomDirection(String logMe,
Random random) {
return values()[Utils.randomInt(logger, logMe, random,
NUMBER_OF_DIRECTIONS)];
}
/**
* Creates an array of the eight directions in a random order.
*
* @param logMe A string to log with the random results.
* @param random A <code>Random</code> number source.
* @return An array of the <code>Direction</code>s in a random order.
*/
public static Direction[] getRandomDirections(String logMe,
Random random) {
int[] randoms = Utils.randomInts(logger, logMe, random,
NUMBER_OF_DIRECTIONS, NUMBER_OF_DIRECTIONS);
Direction[] directions = Direction.values();
for (int i = 0; i < directions.length; i++) {
if (randoms[i] != i) {
Direction temp = directions[randoms[i]];
directions[randoms[i]] = directions[i];
directions[i] = temp;
}
}
return directions;
}
/**
* Creates an array of the directions in an order that favours
* a supplied one. Entry 0 will be the supplied direction,
* entry 1+2 will be those immediately to the left and right
* of it (chosen randomly), and so on until the last entry
* will be the complete reverse of the supplied direction.
*
* Useful if we to step in a particular direction, but if this
* fails to be able to try the closest other directions to the
* original one in order.
*
* @param logMe A string to log with the random results.
* @param random A <code>Random</code> number source.
* @return An array of the <code>Direction</code>s favouring this one.
*/
public Direction[] getClosestDirections(String logMe, Random random) {
// Will need 3 bits of randomness --- 2 directions are known,
// need one bit to randomize each remaining pair.
final int nbits = (NUMBER_OF_DIRECTIONS - 2) / 2;
final int r = Utils.randomInt(logger, logMe, random, 1 << nbits);
Direction[] ret = new Direction[NUMBER_OF_DIRECTIONS];
ret[0] = this;
int step = 1, mask = 1;
for (int i = 1; i < NUMBER_OF_DIRECTIONS - 1; i += 2) {
Direction dr = this.rotate(step);
Direction dl = this.rotate(NUMBER_OF_DIRECTIONS - step);
ret[i] = ((r & mask) == 0) ? dr : dl;
ret[i+1] = ((r & mask) == 0) ? dl : dr;
step += 1;
mask *= 2;
}
ret[NUMBER_OF_DIRECTIONS-1] = this.getReverseDirection();
return ret;
}
}
/**
* The infinite cost used by
* {@link #findPath(Unit, Tile, Tile, Unit, CostDecider)}.
*/
public static final int COST_INFINITY = Integer.MIN_VALUE;
private Tile[][] tiles;
/**
* The highest map layer included.
*/
private Layer layer;
/**
* The latitude of the northern edge of the map. A negative value
* indicates northern latitude, a positive value southern
* latitude. Thus, -30 equals 30°N, and 40 equals 40°S.
*/
private int minimumLatitude = -90;
/**
* The latitude of the southern edge of the map. A negative value
* indicates northern latitude, a positive value southern
* latitude. Thus, -30 equals 30°N, and 40 equals 40°S.
*/
private int maximumLatitude = 90;
/**
* Variable used to convert rows to latitude.
*/
private float latitudePerRow;
/**
* The regions, indexed by id.
*/
private final java.util.Map<String, Region> regions
= new HashMap<String, Region>();
/**
* The search tracing status.
*/
private boolean traceSearch = false;
/**
* Create a new <code>Map</code> from a collection of tiles.
*
* @param game
* The <code>Game</code> this map belongs to.
* @param tiles
* The 2D array of tiles.
*/
public Map(Game game, Tile[][] tiles) {
super(game);
this.tiles = tiles;
setLayer(Layer.RESOURCES);
calculateLatitudePerRow();
}
/**
* Create a new <code>Map</code> from an <code>Element</code> in a
* DOM-parsed XML-tree.
*
* @param game
* The <code>Game</code> this map belongs to.
* @param in
* The input stream containing the XML.
* @throws XMLStreamException
* if a problem was encountered during parsing.
*/
public Map(Game game, XMLStreamReader in) throws XMLStreamException {
super(game, in);
readFromXML(in);
}
/**
* Initiates a new <code>Map</code> with the given ID. The object should
* later be initialized by calling either
* {@link #readFromXML(XMLStreamReader)} or
* {@link #readFromXMLElement(Element)}.
*
* @param game
* The <code>Game</code> in which this object belong.
* @param id
* The unique identifier for this object.
*/
public Map(Game game, String id) {
super(game, id);
}
/**
* Get the <code>Layer</code> value.
*
* @return a <code>Layer</code> value
*/
public final Layer getLayer() {
return layer;
}
/**
* Set the <code>Layer</code> value.
*
* @param newLayer The new Layer value.
*/
public final void setLayer(final Layer newLayer) {
this.layer = newLayer;
}
/**
* Get the <code>MinimumLatitude</code> value.
*
* @return an <code>int</code> value
*/
public final int getMinimumLatitude() {
return minimumLatitude;
}
/**
* Set the <code>MinimumLatitude</code> value.
*
* @param newMinimumLatitude The new MinimumLatitude value.
*/
public final void setMinimumLatitude(final int newMinimumLatitude) {
this.minimumLatitude = newMinimumLatitude;
calculateLatitudePerRow();
}
/**
* Get the <code>MaximumLatitude</code> value.
*
* @return an <code>int</code> value
*/
public final int getMaximumLatitude() {
return maximumLatitude;
}
/**
* Set the <code>MaximumLatitude</code> value.
*
* @param newMaximumLatitude The new MaximumLatitude value.
*/
public final void setMaximumLatitude(final int newMaximumLatitude) {
this.maximumLatitude = newMaximumLatitude;
calculateLatitudePerRow();
}
/**
* Get the <code>LatitudePerRow</code> value.
*
* @return a <code>float</code> value
*/
public final float getLatitudePerRow() {
return latitudePerRow;
}
/**
* Calculates the <code>LatitudePerRow</code> value.
*/
private final void calculateLatitudePerRow() {
this.latitudePerRow = 1f * (maximumLatitude - minimumLatitude) /
(getHeight() - 1);
}
/**
* Get the latitude of the given map row.
*
* @param row an <code>int</code> value
* @return an <code>int</code> value
*/
public int getLatitude(int row) {
return minimumLatitude + (int) (row * latitudePerRow);
}
/**
* Get the map row with the given latitude.
*
* @param latitude an <code>int</code> value
* @return an <code>int</code> value
*/
public int getRow(int latitude) {
return (int) ((latitude - minimumLatitude) / latitudePerRow);
}
/**
* Returns a Collection containing all map regions.
*
* @return a Collection containing all map regions
*/
public Collection<Region> getRegions() {
return regions.values();
}
/**
* Returns the <code>Region</code> with the given key (the nameKey).
*
* @param key The key to lookup the region with.
* @return The region with the given name key.
*/
public Region getRegion(final String id) {
return regions.get(id);
}
/**
* Returns the <code>Region</code> with the given name.
*
* @param id a <code>String</code> value
* @return a <code>Region</code> value
*/
public Region getRegionByName(final String id) {
for (Region region : regions.values()) {
if (id.equals(region.getName())) {
return region;
}
}
return null;
}
/**
* Describe <code>setRegion</code> method here.
*
* @param region a <code>Region</code> value
*/
public void setRegion(final Region region) {
regions.put(region.getNameKey(), region);
}
/**
* Is a tile in the map in a polar region?
*
* @param tile The <code>Tile</code> to examine.
* @return True if the tile is in a polar region.
*/
public boolean isPolar(Tile tile) {
return tile.getY() <= POLAR_HEIGHT
|| tile.getY() >= getHeight() - POLAR_HEIGHT - 1;
}
/**
* Returns the Tile at a requested position.
*
* @param p
* The position.
* @return The Tile at the given position.
*/
public Tile getTile(Position p) {
return getTile(p.getX(), p.getY());
}
/**
* Returns the Tile at position (x, y). 'x' specifies a column and 'y'
* specifies a row. (0, 0) is the Tile at the top-left corner of the Map.
*
* @param x
* The x-coordinate of the <code>Tile</code>.
* @param y
* The y-coordinate of the <code>Tile</code>.
* @return The Tile at position (x, y) or <code>null</code> if the
* position is invalid.
*/
public Tile getTile(int x, int y) {
if (isValid(x, y)) {
return tiles[x][y];
} else {
return null;
}
}
/**
* Sets the given tile the the given coordinates.
*
* @param x
* The x-coordinate of the <code>Tile</code>.
* @param y
* The y-coordinate of the <code>Tile</code>.
* @param tile
* The <code>Tile</code>.
*/
public void setTile(Tile tile, int x, int y) {
tiles[x][y] = tile;
}
/**
* Returns the width of this Map.
*
* @return The width of this Map.
*/
public int getWidth() {
if (tiles == null) {
return 0;
} else {
return tiles.length;
}
}
/**
* Returns the height of this Map.
*
* @return The height of this Map.
*/
public int getHeight() {
if (tiles == null) {
return 0;
} else {
return tiles[0].length;
}
}
/**
* Gets the direction a unit needs to move in
* order to get from <code>t1</code> to <code>t2</code>
*
* @param t1 The tile to move from.
* @param t2 The target tile if moving from <code>t1</code>
* in the direction returned by this method.
* @return The direction you need to move from <code>t1</code>
* in order to reach <code>t2</code>, or null if the two
* specified tiles are not neighbours.
*/
public Direction getDirection(Tile t1, Tile t2) {
for (Direction d : Direction.values()) {
if (t1.getNeighbourOrNull(d) == t2) return d;
}
return null;
}
/**
* Gets the adjacent Tile in a given direction.
*
* @param x The x coordinate to work from.
* @param y The y coordinate to work from.
* @param direction The <code>Direction</code> to check.
* @return The adjacent tile in the specified direction, or null
* if invalid.
*/
public Tile getAdjacentTile(int x, int y, Direction direction) {
x += ((y & 1) != 0) ? direction.getOddDX() : direction.getEvenDX();
y += ((y & 1) != 0) ? direction.getOddDY() : direction.getEvenDY();
return getTile(x, y);
}
/**
* Are two locations non-null and either the same or at the same tile.
* This routine is here because Location is an interface.
*
* @param l1 The first <code>Location</code>.
* @param l2 The second <code>Location</code>.
* @return True if the locations are the same or at the same tile.
*/
public static final boolean isSameLocation(Location l1, Location l2) {
return (l1 == null || l2 == null) ? false
: (l1 == l2) ? true
: (l1.getTile() == null) ? false
: l1.getTile() == l2.getTile();
}
/**
* Are two locations at least in the same contiguous land/sea-mass?
*
* @param l1 The first <code>Location</code>.
* @param l2 The second <code>Location</code>.
* @return True if the locations are the same or in the same land/sea-mass.
*/
public static final boolean isSameContiguity(Location l1, Location l2) {
return (l1 == null || l2 == null) ? false
: (l1 == l2) ? true
: (l1.getTile() == null || l2.getTile() == null) ? false
: l1.getTile().getContiguity() == l2.getTile().getContiguity();
}
/**
* Gets the list of tiles that might be claimable by a settlement.
* We can not do a simple iteration of the rings because this
* allows settlements to claim tiles across unclaimable gaps
* (e.g. Aztecs owning tiles on nearby islands). So we have to
* only allow tiles that are adjacent to a known connected tile.
*
* @param player The <code>Player</code> that intends to found a settlement.
* @param centerTile The intended settlement center <code>Tile</code>.
* @param radius The radius of the settlement.
* @return A list of potentially claimable tiles.
*/
public List<Tile> getClaimableTiles(Player player, Tile centerTile,
int radius) {
List<Tile> tiles = new ArrayList<Tile>();
List<Tile> layer = new ArrayList<Tile>();
if (player.canClaimToFoundSettlement(centerTile)) {
layer.add(centerTile);
for (int r = 1; r <= radius; r++) {
List<Tile> lastLayer = new ArrayList<Tile>(layer);
tiles.addAll(layer);
layer.clear();
for (Tile have : lastLayer) {
for (Tile next : have.getSurroundingTiles(1)) {
if (!tiles.contains(next)
&& player.canClaimForSettlement(next)) {
layer.add(next);
}
}
}
}
tiles.addAll(layer);
}
return tiles;
}
/**
* Simple interface to supply a heuristic to the A* routine.
*/
private interface SearchHeuristic {
public int getValue(Tile tile);
}
/**
* Gets a search heuristic using the Manhatten distance to an end tile.
*
* @param endTile The <code>Tile</code> to aim for.
* @return A new <code>SearchHeuristic</code> aiming for the end tile.
*/
private SearchHeuristic getManhattenHeuristic(final Tile endTile) {
return new SearchHeuristic() {
// Manhatten distance to the end tile.
public int getValue(Tile tile) {
return tile.getDistanceTo(endTile);
}
};
}
/**
* Unified argument tests for full path searches, which then finds
* the actual starting location for the path. Deals with special
* cases like starting on a carrier and/or high seas.
*
* @param unit The <code>Unit</code> to find the path for.
* @param start The <code>Location</code> in which the path starts from.
* @param carrier An optional naval carrier <code>Unit</code> to use.
* @return The actual starting location.
* @throws IllegalArgumentException If there are any argument problems.
*/
private Location findRealStart(final Unit unit, final Location start,
final Unit carrier) {
if (unit == null) {
throw new IllegalArgumentException("Null unit.");
} else if (carrier != null && !carrier.canCarryUnits()) {
throw new IllegalArgumentException("Non-carrier carrier: "
+ carrier);
} else if (carrier != null && !carrier.couldCarry(unit)) {
throw new IllegalArgumentException("Carrier could not carry unit: "
+ carrier + "/" + unit);
}
Location entry;
if (start == null) {
throw new IllegalArgumentException("Null start.");
} else if (start instanceof Unit) {
Location unitLoc = ((Unit)start).getLocation();
if (unitLoc == null) {
throw new IllegalArgumentException("Null on-carrier start.");
} else if (unitLoc instanceof HighSeas) {
if (carrier == null) {
throw new IllegalArgumentException("Null carrier when starting on high seas: " + unit);
} else if (carrier != (Unit)start) {
throw new IllegalArgumentException("Wrong carrier when starting on high seas: " + unit + "/" + carrier);
}
entry = carrier.resolveDestination();
} else {
entry = unitLoc;
}
} else if (start instanceof HighSeas) {
if (unit.isOnCarrier()) {
entry = unit.getCarrier().resolveDestination();
} else if (unit.isNaval()) {
entry = unit.resolveDestination();
} else {
throw new IllegalArgumentException("No carrier when starting on high seas: " + unit);
}
} else if (start instanceof Europe || start.getTile() != null) {
entry = start; // OK
} else {
throw new IllegalArgumentException("Invalid start: " + start);
}
return entry;
}
/**
* Gets the best (closest) path location for this unit to reach a
* given tile from off the map.
*
* @param unit The <code>Unit</code> to check.
* @param tile The target <code>Tile</code>.
* @param carrier An optional carrier <code>Unit</code>to use.
* @param costDecider An optional <code>CostDecider</code> to use.
* @return A path to the best entry location tile to arrive on the
* map at, or null if none found.
*/
private PathNode getBestEntryPath(Unit unit, Tile tile, Unit carrier,
CostDecider costDecider) {
return search(unit, tile, GoalDeciders.getHighSeasGoalDecider(),
costDecider, INFINITY, carrier);
}
/**
* Gets the best (closest) entry location for this unit to reach a
* given tile from off the map.
*
* @param unit The <code>Unit</code> to check.
* @param tile The target <code>Tile</code>.
* @param carrier An optional carrier <code>Unit</code>to use.
* @param costDecider An optional <code>CostDecider</code> to use.
* @return The best entry location tile to arrive on the map at, or null
* if none found.
*/
public Tile getBestEntryTile(Unit unit, Tile tile, Unit carrier,
CostDecider costDecider) {
PathNode path = getBestEntryPath(unit, tile, carrier, costDecider);
return (path == null) ? null : path.getLastNode().getTile();
}
/**
* Version of findPath that includes the start tile and generalized
* start and end locations.
*
* @param unit The <code>Unit</code> to find the path for.
* @param start The <code>Location</code> in which the path starts from.
* @param end The <code>Location</code> at the end of the path.
* @param carrier An optional naval carrier <code>Unit</code> to use.
* @param costDecider An optional <code>CostDecider</code> for
* determining the movement costs (uses default cost deciders
* for the unit/s if not provided).
* @return A <code>PathNode</code> starting at the start location and
* ending at the end location, or null if no path is found.
* @throws IllegalArgumentException If the unit is null, or the
* start and end locations do not make sense, or the
* carrier/unit combination is bogus.
*/
public PathNode findPath(final Unit unit,
final Location start, final Location end,
final Unit carrier, CostDecider costDecider) {
if (end == null) {
throw new IllegalArgumentException("Null end.");
} else if (!(end instanceof Europe || end.getTile() != null)) {
throw new IllegalArgumentException("Invalid end: " + end);
}
Location entry = findRealStart(unit, start, carrier);
int initialTurns = (!unit.isAtSea()) ? 0
: (unit.isOnCarrier()) ? unit.getCarrier().getWorkLeft()
: unit.getWorkLeft();
PathNode path;
Unit waterUnit = (carrier != null) ? carrier : unit;
if (entry instanceof Europe) {
if (end instanceof Europe) { // The trivial(Europe) path.
path = new PathNode(entry, unit.getMovesLeft(), 0,
false, null, null);
} else { // Start in Europe, end on a Tile
// Fail fast without capable water unit.
if (!waterUnit.getType().canMoveToHighSeas()) return null;
// Find the best place to enter the map from Europe.
PathNode p = getBestEntryPath(unit, end.getTile(), carrier,
costDecider);
if (p == null) return null;
Tile tile = p.getLastNode().getTile();
// Now search forward from there to get a path in the right
// order (the existing one might not be optimal if reversed!)
path = searchInternal(unit, tile,
GoalDeciders.getLocationGoalDecider(end.getTile()),
costDecider, INFINITY, carrier,
getManhattenHeuristic(end.getTile()));
if (path == null) {
throw new IllegalStateException("SEARCH-FAIL: " + unit
+ "/" + carrier + " from " + tile + " to " + end
+ "\n" + p.fullPathToString());
}
// At the front of the path insert a node for the
// starting location in Europe, correcting for the turns
// to sail to the entry location.
path.addTurns(waterUnit.getSailTurns());
path.previous = new PathNode(entry, unit.getMovesLeft(),
0, carrier != null, null, path);
path = path.previous;
}
} else { // entry has Tile
if (end instanceof Europe) {
// Fail fast if Europe is unattainable.
if (!waterUnit.getType().canMoveToHighSeas()) return null;
// Search forwards to the high seas.
path = searchInternal(unit, entry.getTile(),
GoalDeciders.getLocationGoalDecider(end),
costDecider, INFINITY, carrier, null);
} else { // entry and end are Tiles
final Tile startTile = entry.getTile();
final Tile endTile = end.getTile();
final GoalDecider gd
= GoalDeciders.getLocationGoalDecider(end);
final SearchHeuristic sh = getManhattenHeuristic(endTile);
if (startTile.getContiguity() == endTile.getContiguity()) {
// If the unit potentially could get to the
// destination without a carrier, compare both
// with-carrier and without-carrier paths. The
// latter will usually be faster, but not always,
// e.g. mounted units on a good road system.
PathNode carrierPath;
path = searchInternal(unit, startTile, gd,
costDecider, INFINITY, null, sh);
if (carrier != null
&& (carrierPath = searchInternal(unit, startTile, gd,
costDecider, INFINITY,
carrier, sh)) != null
&& (path == null
|| (path.getLastNode().getCost()
> carrierPath.getLastNode().getCost()))) {
path = carrierPath;
}
} else if (waterUnit != null) {
// If there is a water unit then complex paths which
// use settlements and inland lakes are possible, but
// hard to capture with the contiguity test, so just
// allow the search to proceed.
path = searchInternal(unit, startTile, gd,
costDecider, INFINITY,
carrier, sh);
} else { // Otherwise, there is a connectivity failure.
path = null;
}
}
}
if (path != null && initialTurns != 0) path.addTurns(initialTurns);
return path;
}
/**
* Searches for a goal.
* Assumes units in Europe return to their current entry location,
* which is not optimal most of the time.
* Returns the full path including the start and end locations.
*
* @param unit The <code>Unit</code> to find a path for.
* @param start The <code>Location</code> to start the search from.
* @param goalDecider The object responsible for determining whether a
* given <code>PathNode</code> is a goal or not.
* @param costDecider An optional <code>CostDecider</code>
* responsible for determining the path cost.
* @param maxTurns The maximum number of turns the given
* <code>Unit</code> is allowed to move. This is the
* maximum search range for a goal.
* @param carrier An optional naval carrier <code>Unit</code> to use.
* @return The path to a goal, or null if none can be found.
* @throws IllegalArgumentException If the unit is null, or the
* start location does not make sense, or the carrier/unit
* combination is bogus.
*/
public PathNode search(final Unit unit, Location start,
final GoalDecider goalDecider,
final CostDecider costDecider,
final int maxTurns, final Unit carrier) {
Location entry = findRealStart(unit, start, carrier);
int initialTurns = (!unit.isAtSea()) ? 0
: ((unit.isOnCarrier()) ? unit.getCarrier() : unit).getWorkLeft();
PathNode path;
if (entry instanceof Europe) {
Unit waterUnit = (carrier != null) ? carrier : unit;
// Fail fast if Europe is unattainable.
if (!waterUnit.getType().canMoveToHighSeas()) return null;
path = searchInternal(unit, (Tile)waterUnit.getEntryLocation(),
goalDecider, costDecider, maxTurns,
carrier, null);
if (path == null) return null;
path.addTurns(waterUnit.getSailTurns());
path.previous = new PathNode(entry, waterUnit.getMovesLeft(),
0, carrier != null, null, path);
path = path.previous;
} else {
path = searchInternal(unit, entry.getTile(),
goalDecider, costDecider, maxTurns,
carrier, null);
}
if (path != null && initialTurns != 0) path.addTurns(initialTurns);
return path;
}
/**
* Sets the search tracing status.
*
* @param trace The new search tracing status.
*/
public void setSearchTrace(boolean trace) {
traceSearch = trace;
}
/**
* Was a carrier used previously on a path?
*
* Beware! This is special case code for partially constructed
* paths that do not yet have valid .next links, so we can not use the
* generic PathNode routines.
*
* @param path The path the search.
* @return True if the path includes a previous on-carrier node.
*/
private boolean usedCarrier(PathNode path) {
while (path != null) {
if (path.isOnCarrier()) return true;
path = path.previous;
}
return false;
}
/**
* Internal class for evaluating a candidate move.
*/
private class MoveCandidate {
private Unit unit;
private PathNode current;
private Location dst;
private int movesLeft;
private int turns;
private boolean onCarrier;
private CostDecider decider;
private int cost;
private PathNode path;
/**
* Creates a new move candidate where a cost decider will be used
* to work out the new moves and turns left.
*
* @param unit The <code>Unit</code> to move.
* @param current The current position on the path.
* @param dst The <code>Location</code> to move to.
* @param movesLeft The initial number of moves left.
* @param turns The initial number of turns.
* @param onCarrier Will the new move be on a carrier.
* @param decider The <code>CostDecider</code> to use.
*/
public MoveCandidate(Unit unit, PathNode current, Location dst,
int movesLeft, int turns, boolean onCarrier,
CostDecider decider) {
this.unit = unit;
this.current = current;
this.dst = dst;
this.movesLeft = movesLeft;
this.turns = turns;
this.onCarrier = onCarrier;
this.decider = decider;
this.cost = decider.getCost(unit, current.getLocation(),
dst, movesLeft);
if (this.cost != CostDecider.ILLEGAL_MOVE) {
this.turns += decider.getNewTurns();
this.movesLeft = decider.getMovesLeft();
this.cost = PathNode.getCost(turns, movesLeft);
}
this.path = null;
}
/**
* Handles the change of unit as a result of an embark.
*/
public void embarkUnit(Unit unit) {
this.unit = unit;
this.movesLeft = unit.getInitialMovesLeft();
this.cost = PathNode.getCost(turns, movesLeft);
}
/**
* Resets the path. Required after the parameters change.
*/
public void resetPath() {
this.path = new PathNode(dst, movesLeft, turns, onCarrier,
current, null);
}
/**
* Do not let the CostDecider (which may be conservative)
* block a final destination if it is still a legal move
* or only illegal because it is occupied by an enemy unit, which
* may be a temporary condition.
*
* @param goalDecider A <code>GoalDecider</code> to check the
* goal with.
*/
public void recoverGoal(GoalDecider goalDecider) {
Unit.MoveType mt;
if (cost == CostDecider.ILLEGAL_MOVE
&& unit != null
&& current.getTile() != null
&& dst.getTile() != null
&& goalDecider != null
&& goalDecider.check(unit, path)
&& ((mt = unit.getSimpleMoveType(current.getTile(),
dst.getTile())).isLegal()
|| mt == Unit.MoveType.MOVE_NO_ATTACK_CIVILIAN)) {
// Pretend it finishes the move.
movesLeft = unit.getInitialMovesLeft();
turns++;
cost = PathNode.getCost(turns, movesLeft);
resetPath();
}
}
/**
* Does this move candidate improve on a specified move.
*
* @param best The <code>PathNode</code> to compare against.
*/
public boolean canImprove(PathNode best) {
return cost != CostDecider.ILLEGAL_MOVE
&& (best == null || cost < best.getCost());
}
/**
* Replace a given path with that of this candidate move.
*
* @param openList The list of available nodes.
* @param openListQueue The queue of available nodes.
* @param f The heuristic values for A*.
* @param sh An optional <code>SearchHeuristic</code> to apply.
*/
public void improve(HashMap<String, PathNode> openList,
PriorityQueue<PathNode> openListQueue,
HashMap<String, Integer> f,
SearchHeuristic sh) {
PathNode best = openList.get(dst.getId());
if (best != null) {
openList.remove(dst.getId());
openListQueue.remove(best);
}
int fcost = cost;
if (sh != null && dst.getTile() != null) {
fcost += sh.getValue(dst.getTile());
}
f.put(dst.getId(), new Integer(fcost));
openList.put(dst.getId(), path);
openListQueue.offer(path);
}
/**
* Debug helper.
*/
public String toString() {
return "[candidate unit=" + unit.toString()
+ " dst=" + ((FreeColGameObject)dst).toString()
+ " movesLeft=" + movesLeft
+ " turns=" + turns
+ " onCarrier=" + onCarrier
+ " decider=" + decider
+ " cost=" + cost
+ "]";
}
};
/**
* Searches for a path to a goal determined by the given
* <code>GoalDecider</code>.
*
* Using A* with a List (closedList) for marking the visited nodes
* and using a PriorityQueue (openListQueue) for getting the next
* edge with the least cost. This implementation could be
* improved by having the visited attribute stored on each Tile in
* order to avoid both of the HashMaps currently being used to
* serve this purpose.
*
* If the SearchHeuristic is not supplied, then the algorithm
* degrades gracefully to Dijkstra's algorithm.
*
* The data structure for the open list is a combined structure: using a
* HashMap for membership tests and a PriorityQueue for getting the node
* with the minimal f (cost+heuristics). This gives O(1) on membership
* test and O(log N) for remove-best and insertions.
*
* @param unit The <code>Unit</code> to find a path for, which may be null!
* @param start The <code>Tile</code> to start the search from.
* @param goalDecider The object responsible for determining whether a
* given <code>PathNode</code> is a goal or not.
* @param costDecider An optional <code>CostDecider</code>
* responsible for determining the path cost.
* @param maxTurns The maximum number of turns the given
* <code>Unit</code> is allowed to move. This is the
* maximum search range for a goal.
* @param carrier An optional naval carrier <code>Unit</code> to use.
* @param searchHeuristic An optional <code>SearchHeuristic</code>.
* @return The path to a goal determined by the given
* <code>GoalDecider</code>.
*/
private PathNode searchInternal(final Unit unit, final Tile start,
final GoalDecider goalDecider,
final CostDecider costDecider,
final int maxTurns, final Unit carrier,
final SearchHeuristic searchHeuristic) {
final HashMap<String, PathNode> openList
= new HashMap<String, PathNode>();
final HashMap<String, PathNode> closedList
= new HashMap<String, PathNode>();
final HashMap<String, Integer> f
= new HashMap<String, Integer>();
final PriorityQueue<PathNode> openListQueue
= new PriorityQueue<PathNode>(1024,
new Comparator<PathNode>() {
public int compare(PathNode p1, PathNode p2) {
return (f.get(p1.getLocation().getId()).intValue()
- f.get(p2.getLocation().getId()).intValue());
}
});
final Europe europe = (unit == null) ? null
: unit.getOwner().getEurope();
final List<Location> tracing = (traceSearch)
? new ArrayList<Location>()
: null;
Unit waterUnit = (carrier != null) ? carrier : unit;
Unit currentUnit = (start.isLand())
? ((start.getSettlement() != null
&& unit != null
&& unit.getLocation() == carrier) ? carrier : unit)
: waterUnit;
// Create the start node and put it on the open list.
final PathNode firstNode = new PathNode(start,
((currentUnit != null) ? currentUnit.getMovesLeft() : -1),
0, carrier != null && currentUnit == carrier, null, null);
f.put(start.getId(),
new Integer((searchHeuristic == null) ? 0
: searchHeuristic.getValue(start)));
openList.put(start.getId(), firstNode);
openListQueue.offer(firstNode);
while (!openList.isEmpty()) {
// Choose the node with the lowest f.
final PathNode currentNode = openListQueue.poll();
final Location currentLocation = currentNode.getLocation();
if (tracing != null) tracing.add(currentLocation);
openList.remove(currentLocation.getId());
closedList.put(currentLocation.getId(), currentNode);
// Reset current unit to that of this node.
currentUnit = (currentNode.isOnCarrier()) ? carrier : unit;
// Stop at simple success.
if (goalDecider.check(currentUnit, currentNode)
&& !goalDecider.hasSubGoals()) {
break;
}
// Ignore nodes over the turn limit.
if (currentNode.getTurns() > maxTurns) continue;
// Collect the parameters for the current node.
final int currentMovesLeft = currentNode.getMovesLeft();
final int currentTurns = currentNode.getTurns();
final boolean currentOnCarrier = currentNode.isOnCarrier();
final Tile currentTile = currentNode.getTile();
if (currentTile == null) { // Must be in Europe.
// TODO: Do not consider tiles `adjacent' to Europe, yet.
// There may indeed be cases where going to Europe and
// coming back on the other side of the map is faster.
continue;
}
// Try the tiles in each direction
for (Tile moveTile : currentTile.getSurroundingTiles(1)) {
// If the new tile is the tile we just visited, skip it.
if (currentNode.previous != null
&& currentNode.previous.getTile() == moveTile) {
continue;
}
// Skip tiles already visited.
if (closedList.containsKey(moveTile.getId())) continue;
// Is this move possible? Loop on failure.
//
// Check for a carrier change at the new tile,
// creating a MoveCandidate for each case.
//
// Do *not* allow units to re-embark on the carrier.
// Note that embarking can actually increase the moves
// left because the carrier might be not have spent
// any moves yet that turn.
//
// Note that we always favour using the carrier if
// both carrier and non-carrier moves are possible,
// which can only be true moving into a settlement.
// Usually when moving into a settlement it will
// be useful to dock the carrier so it can collect new
// cargo. OTOH if the carrier is just passing through
// the right thing is to keep the passenger on board.
// Still there is a suboptimality if the carrier ends
// its turn one short of a settlement where there is
// no useful cargo but we fail to consider an
// immediate move into the settlement by the
// passenger. The trouble with looking for such
// special cases is that we do not know yet whether
// that settlement is worth disembarking the unit to.
// There have been nasty bugs where passengers would
// disembark too early, so for now we live with the
// suboptimality.
//
boolean unitMove = unit == null
|| unit.isTileAccessible(moveTile);
boolean carrierMove = carrier != null
&& carrier.isTileAccessible(moveTile);
MoveStep step = (currentOnCarrier)
? ((carrierMove) ? MoveStep.BYWATER
: (unitMove) ? MoveStep.DISEMBARK
: MoveStep.FAIL)
: ((carrierMove && !usedCarrier(currentNode))
? MoveStep.EMBARK
: (unitMove) ? ((unit.isNaval())
? MoveStep.BYWATER
: MoveStep.BYLAND)
: MoveStep.FAIL);
MoveCandidate move;
switch (step) {
case BYLAND:
move = new MoveCandidate(unit, currentNode, moveTile,
currentMovesLeft, currentTurns, false,
((costDecider != null) ? costDecider
: CostDeciders.defaultCostDeciderFor(unit)));
break;
case BYWATER:
move = new MoveCandidate(waterUnit, currentNode, moveTile,
currentMovesLeft, currentTurns, currentOnCarrier,
((costDecider != null) ? costDecider
: CostDeciders.defaultCostDeciderFor(waterUnit)));
break;
case EMBARK:
move = new MoveCandidate(unit, currentNode, moveTile,
currentMovesLeft, currentTurns, true,
((costDecider != null) ? costDecider
: CostDeciders.defaultCostDeciderFor(unit)));
move.embarkUnit(carrier);
break;
case DISEMBARK:
// Check if already embarked this turn. If so, the
// disembarking unit should have zero moves left,
// if not, its full amount is available.
int movesLeft = unit.getInitialMovesLeft();
for (PathNode p = currentNode; p != null; p = p.previous) {
if (p.getTurns() < currentTurns) break;
if (!p.isOnCarrier()) {
movesLeft = 0;
break;
}
}
move = new MoveCandidate(unit, currentNode, moveTile,
movesLeft, currentTurns, false,
((costDecider != null) ? costDecider
: CostDeciders.defaultCostDeciderFor(unit)));
break;
case FAIL: default: // Loop on failure.
move = null;
break;
}
if (move != null) {
move.resetPath();
// Special case when on the map.
move.recoverGoal(goalDecider);
// Is this an improvement? If not, ignore.
if (move.canImprove(openList.get(moveTile.getId()))) {
move.improve(openList, openListQueue, f,
searchHeuristic);
}
}
}
// Also try moving to Europe if it exists and the move is ok.
if (europe != null
&& (currentNode.previous != null
&& currentNode.previous.getLocation() != europe)
&& !closedList.containsKey(europe.getId())
&& currentUnit != null
&& currentUnit.getType().canMoveToHighSeas()
&& currentTile.isDirectlyHighSeasConnected()) {
MoveCandidate move = new MoveCandidate(currentUnit,
currentNode, europe, currentMovesLeft, currentTurns,
currentOnCarrier,
((costDecider != null) ? costDecider
: CostDeciders.defaultCostDeciderFor(currentUnit)));
move.resetPath();
if (move.canImprove(openList.get(europe.getId()))) {
move.improve(openList, openListQueue, f, null);
}
}
}
// Relink the path. We omitted the .next link while constructing it.
PathNode best = goalDecider.getGoal();
if (best != null) {
while (best.previous != null) {
best.previous.next = best;
best = best.previous;
}
}
// Output the trace result.
if (tracing != null) {
String logMe = "Search trace(" + unit + ", " + start
+ ", " + ((carrier == null) ? "null" : carrier) + "):";
for (Location t : tracing) logMe += " " + t;
logMe += "\n";
if (best != null) logMe += best.fullPathToString() + "\n";
logger.info(logMe);
}
return best;
}
// Support for various kinds of iteration.
/**
* A position on the Map.
*/
public static final class Position {
/**
* The coordinates of the position.
*/
public final int x, y;
/**
* Creates a new <code>Position</code> object with the given
* coordinates.
*
* @param posX The x-coordinate for this position.
* @param posY The y-coordinate for this position.
*/
public Position(int posX, int posY) {
x = posX;
y = posY;
}
/**
* Gets the x-coordinate of this Position.
*
* @return The x-coordinate of this Position.
*/
public int getX() {
return x;
}
/**
* Gets the y-coordinate of this Position.
*
* @return The y-coordinate of this Position.
*/
public int getY() {
return y;
}
/**
* Compares the other Position based on the coordinates.
*
* @param other The other object to compare with.
* @return True iff the coordinates match.
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (other == null) {
return false;
} else if (!(other instanceof Position)) {
return false;
} else {
return x == ((Position)other).x && y == ((Position)other).y;
}
}
/**
* Gets a hash code value. The current implementation
* (which may change at any time) works well as long as the
* maximum coordinates fit in 16 bits.
*
* @return A hash code value for this object.
*/
@Override
public int hashCode() {
return x | (y << 16);
}
/**
* Gets the position adjacent to a given position, in a given
* direction.
*
* @param direction The <code>Direction</code> to check.
* @return The adjacent position.
*/
public Position getAdjacent(Direction direction) {
int x = this.x + (((this.y & 1) != 0) ? direction.getOddDX()
: direction.getEvenDX());
int y = this.y + (((this.y & 1) != 0) ? direction.getOddDY()
: direction.getEvenDY());
return new Position(x, y);
}
/**
* Gets the distance in tiles between two map positions.
* With an isometric map this is a non-trivial task.
* The formula below has been developed largely through trial and
* error. It should cover all cases, but I wouldn't bet my
* life on it.
*
* @param position The other <code>Position</code> to compare.
* @return The distance in tiles to the other position.
*/
public int getDistance(Position position) {
int ay = getY();
int by = position.getY();
int r = position.getX() - getX() - (ay - by) / 2;
if (by > ay && ay % 2 == 0 && by % 2 != 0) {
r++;
} else if (by < ay && ay % 2 != 0 && by % 2 == 0) {
r--;
}
return Math.max(Math.abs(ay - by + r), Math.abs(r));
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
/**
* Checks if an (x,y) coordinate tuple is within a map of
* specified width and height.
*
* @param x The x-coordinate of the position.
* @param y The y-coordinate of the position.
* @param width The width of the map.
* @param height The height of the map.
* @return True if the given position is within the bounds of the map.
*/
public static boolean isValid(int x, int y, int width, int height) {
return x >= 0 && x < width && y >= 0 && y < height;
}
/**
* Checks whether a position is valid within a given map size.
*
* @param position The <code>Position</code> to check.
* @param width The width of the map.
* @param height The height of the map.
* @return True if the given position is within the bounds of the map.
*/
public static boolean isValid(Position position, int width, int height) {
return isValid(position.x, position.y, width, height);
}
/**
* Checks whether a position is valid (within the map limits).
*
* @param position The <code>Position</code> to check.
* @return True if the position is valid.
*/
public boolean isValid(Position position) {
return isValid(position.x, position.y, getWidth(), getHeight());
}
/**
* Checks whether a position is valid (within the map limits).
*
* @param x The X coordinate to check.
* @param y The Y coordinate to check.
* @return True if the coordinates are valid.
*/
public boolean isValid(int x, int y) {
return isValid(x, y, getWidth(), getHeight());
}
/**
* Base class for internal iterators.
*/
private abstract class MapIterator implements Iterator<Position> {
/**
* Get the next position as a position rather as an object.
*
* @return The next <code>Position</code>.
* @throws NoSuchElementException if the iterator is exhausted.
*/
public abstract Position nextPosition() throws NoSuchElementException;
/**
* Returns the next element in the iteration.
*
* @return The next element in the iteration.
* @exception NoSuchElementException if the iterator is exhausted.
*/
public Position next() {
return nextPosition();
}
/**
* Removes from the underlying collection the last element returned by
* the iterator (optional operation).
*
* @exception UnsupportedOperationException no matter what.
*/
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* Makes an iterable version of a map iterator.
*
* @param m The <code>MapIterator</code>.
* @return A corresponding iterable.
*/
private Iterable<Tile> makeMapIteratorIterable(final MapIterator m) {
return new Iterable<Tile>() {
public Iterator<Tile> iterator() {
return new Iterator<Tile>() {
public boolean hasNext() { return m.hasNext(); }
public Tile next() { return getTile(m.next()); }
public void remove() { m.remove(); }
};
}
};
}
/**
* An iterator for the valid tiles immediately around a base tile.
*/
private final class AdjacentIterator extends MapIterator {
/** The starting tile position */
private Position basePosition;
/** The index into the list of adjacent tiles. */
private int index = 0;
/**
* Create a new AdjacentIterator.
*
* @param basePosition The <code>Position</code> around which
* to iterate.
*/
public AdjacentIterator(Position basePosition) {
this.basePosition = basePosition;
}
/**
* Checks if the iterator has another position in it.
*
* @return True of there is another position
*/
public boolean hasNext() {
for (int i = index; i < Direction.NUMBER_OF_DIRECTIONS; i++) {
Direction d = Direction.values()[i];
Position newPosition = basePosition.getAdjacent(d);
if (isValid(newPosition)) return true;
}
return false;
}
/**
* Gets the next position in the iteration.
*
* @return The next <code>Position</code>.
* @throws NoSuchElementException if the iterator is exhausted.
*/
@Override
public Position nextPosition() throws NoSuchElementException {
for (int i = index; i < Direction.NUMBER_OF_DIRECTIONS; i++) {
Direction d = Direction.values()[i];
Position newPosition = basePosition.getAdjacent(d);
if (isValid(newPosition)) {
index = i + 1;
return newPosition;
}
}
throw new NoSuchElementException("AdjacentIterator exhausted");
}
}
/**
* Get an iterator for the adjacent tiles to a center position.
*
* @param centerPosition The center <code>Position</code> to
* iterate around.
* @return An adjacent tile iterator.
*/
public MapIterator getAdjacentIterator(Position centerPosition) {
return new AdjacentIterator(centerPosition);
}
/**
* An iterator returning positions in a spiral starting at a given
* center tile. The center tile is never included in the
* positions returned, and all returned positions are valid.
*/
private final class CircleIterator extends MapIterator {
/** The maximum radius. */
private int radius;
/** The current radius of the iteration. */
private int currentRadius;
/** The current index in the circle with the current radius: */
private int n;
/** The current position in the circle. */
private Position nextPosition = null;
/**
* Create a new Circle Iterator.
*
* @param center The center <code>Position</code> of the circle.
* @param isFilled True to get all of the positions within the circle.
* @param radius The radius of the circle.
*/
public CircleIterator(Position center, boolean isFilled, int radius) {
if (center == null) {
throw new IllegalArgumentException("center must not be null.");
}
this.radius = radius;
n = 0;
if (isFilled || radius == 1) {
nextPosition = center.getAdjacent(Direction.NE);
currentRadius = 1;
} else {
this.currentRadius = radius;
nextPosition = center;
for (int i = 1; i < radius; i++) {
nextPosition = nextPosition.getAdjacent(Direction.N);
}
nextPosition = nextPosition.getAdjacent(Direction.NE);
}
if (!isValid(nextPosition)) {
determineNextPosition();
}
}
/**
* Gets the current radius of the circle.
*
* @return The distance from the center tile this
* <code>CircleIterator</code> was initialized with.
*/
public int getCurrentRadius() {
return currentRadius;
}
/**
* Finds the next position.
*/
private void determineNextPosition() {
boolean positionReturned = n != 0;
do {
n++;
final int width = currentRadius * 2;
if (n >= width * 4) {
currentRadius++;
if (currentRadius > radius) {
nextPosition = null;
} else if (!positionReturned) {
nextPosition = null;
} else {
n = 0;
positionReturned = false;
nextPosition = nextPosition.getAdjacent(Direction.NE);
}
} else {
int i = n / width;
Direction direction;
switch (i) {
case 0:
direction = Direction.SE;
break;
case 1:
direction = Direction.SW;
break;
case 2:
direction = Direction.NW;
break;
case 3:
direction = Direction.NE;
break;
default:
throw new IllegalStateException("i=" + i + ", n=" + n
+ ", width=" + width);
}
nextPosition = nextPosition.getAdjacent(direction);
}
} while (nextPosition != null && !isValid(nextPosition));
}
/**
* Check if the iterator has another position in it.
*
* @return True if there is another position.
*/
public boolean hasNext() {
return nextPosition != null;
}
/**
* Gets the next position.
*
* @return The next valid position.
*/
@Override
public Position nextPosition() {
if (nextPosition == null) return null;
final Position p = nextPosition;
determineNextPosition();
return p;
}
}
/**
* Gets a circle iterator.
*
* @param center The center <code>Position</code> to iterate around.
* @param isFilled True to get all of the positions in the circle.
* @param radius The radius of circle.
* @return The circle iterator.
*/
public CircleIterator getCircleIterator(Position center, boolean isFilled,
int radius) {
return new CircleIterator(center, isFilled, radius);
}
/**
* Gets an iterable for all the tiles in a circle using an
* underlying CircleIterator.
*
* @param center The center <code>Tile</code> to iterate around.
* @param isFilled True to get all of the positions in the circle.
* @param radius The radius of circle.
* @return An <code>Iterable</code> for a circle of tiles.
*/
public Iterable<Tile> getCircleTiles(Tile center, boolean isFilled,
int radius) {
return makeMapIteratorIterable(getCircleIterator(center.getPosition(),
isFilled, radius));
}
/**
* An iterator for the whole map.
*/
private final class WholeMapIterator extends MapIterator {
/**
* The current coordinate position in the iteration.
*/
private int x, y;
/**
* Default constructor.
*/
public WholeMapIterator() {
x = 0;
y = 0;
}
/**
* Checks if the iterator has another position in it.
*
* @return True of there is another position
*/
public boolean hasNext() {
return y < getHeight();
}
/**
* Gets the next position in the iteration.
*
* @return The next <code>Position</code>.
* @throws NoSuchElementException if the iterator is exhausted.
*/
@Override
public Position nextPosition() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException("WholeMapIterator exhausted");
}
Position newPosition = new Position(x, y);
x++;
if (x == getWidth()) {
x = 0;
y++;
}
return newPosition;
}
}
/**
* Gets an <code>Iterator</code> of every <code>Tile</code> on the map.
*
* @return The <code>Iterator</code>.
*/
public MapIterator getWholeMapIterator() {
return new WholeMapIterator();
}
/**
* Gets an iterable for all the tiles in the map on using an
* underlying WholeMapIterator.
*
* @return An <code>Iterable</code> for all tiles of the map.
*/
public Iterable<Tile> getAllTiles() {
return makeMapIteratorIterable(getWholeMapIterator());
}
/**
* Gets all the tiles surrounding a tile within the given range.
* The center tile is not included.
*
* @param center The center <code>Tile</code>.
* @param range How far away do we need to go starting from this.
* @return The tiles surrounding this <code>Tile</code>.
*/
public Iterable<Tile> getSurroundingTiles(final Tile center,
final int range) {
return new Iterable<Tile>() {
public Iterator<Tile> iterator() {
final Iterator<Position> m = (range == 1)
? getAdjacentIterator(center.getPosition())
: getCircleIterator(center.getPosition(), true, range);
return new Iterator<Tile>() {
public boolean hasNext() { return m.hasNext(); }
public Tile next() { return getTile(m.next()); }
public void remove() { m.remove(); }
};
}
};
}
/**
* Searches for land within the given radius.
*
* @param x X-component of the position to search from.
* @param y Y-component of the position to search from.
* @param distance The radius in tiles that should be searched for land.
* @return The first land tile found within the radius, or null if none
* found.
*/
public Tile getLandWithinDistance(int x, int y, int distance) {
for (Tile t : getCircleTiles(getTile(x, y), true, distance)) {
if (t.isLand()) return t;
}
return null;
}
/**
* Get a flood fill iterator.
* Simulated by making a filled circle iterator with an unlimited radius.
*
* @param centerPosition The center <code>Position</code> to
* iterate around.
* @return A simulated flood fill iterator.
*/
public MapIterator getFloodFillIterator(Position centerPosition) {
return new CircleIterator(centerPosition, true, INFINITY);
}
/**
* Flood fills from a given <code>Position</code> p, based on
* connectivity information encoded in boolmap
*
* @param boolmap The connectivity information for this floodfill.
* @param p The starting <code>Position</code>.
* @return A boolean[][] of the same size as boolmap, where "true"
* means the fill succeeded at that location.
*/
public static boolean[][] floodFill(boolean[][] boolmap, Position p) {
return floodFill(boolmap, p, Integer.MAX_VALUE);
}
/**
* Flood fills from a given <code>Position</code> p, based on
* connectivity information encoded in boolmap
*
* @param boolmap The connectivity information for this floodfill.
* @param p The starting <code>Position</code>.
* @param limit Limit to stop flood fill at.
* @return A boolean[][] of the same size as boolmap, where "true"
* means the fill succeeded at that location.
*/
public static boolean[][] floodFill(boolean[][] boolmap, Position p,
int limit) {
Queue<Position>q = new LinkedList<Position>();
boolean[][] visited = new boolean[boolmap.length][boolmap[0].length];
visited[p.getX()][p.getY()] = true;
limit--;
do {
for (Direction direction : Direction.values()) {
Position n = p.getAdjacent(direction);
if (Map.isValid(n, boolmap.length, boolmap[0].length)
&& boolmap[n.getX()][n.getY()]
&& !visited[n.getX()][n.getY()] && limit > 0) {
visited[n.getX()][n.getY()] = true;
limit--;
q.add(n);
}
}
p = q.poll();
} while (p != null && limit > 0);
return visited;
}
/**
* Sets the contiguity identifier for all tiles.
*/
public void resetContiguity() {
// Create the water map. It is an error for any tile not to
// have a region at this point.
boolean[][] waterMap = new boolean[getWidth()][getHeight()];
for (int y = 0; y < getHeight(); y++) {
for (int x = 0; x < getWidth(); x++) {
if (isValid(x, y)) {
waterMap[x][y] = !getTile(x,y).isLand();
}
}
}
// Flood fill each contiguous water region, setting the
// contiguity number.
int contig = 0;
for (int y = 0; y < getHeight(); y++) {
for (int x = 0; x < getWidth(); x++) {
Tile tile = getTile(x, y);
if (waterMap[x][y]) {
if (tile.getContiguity() >= 0) continue;
boolean[][] found = floodFill(waterMap,
new Position(x, y));
for (int yy = 0; yy < getHeight(); yy++) {
for (int xx = 0; xx < getWidth(); xx++) {
if (found[xx][yy]) {
Tile t = getTile(xx, yy);
if (t.getContiguity() < 0) {
t.setContiguity(contig);
}
}
}
}
contig++;
}
}
}
// Complement the waterMap, it is now the land map.
for (int y = 0; y < getHeight(); y++) {
for (int x = 0; x < getWidth(); x++) {
if (isValid(x, y)) waterMap[x][y] = !waterMap[x][y];
}
}
// Flood fill again for each contiguous land region.
for (int y = 0; y < getHeight(); y++) {
for (int x = 0; x < getWidth(); x++) {
if (waterMap[x][y]) {
Tile tile = getTile(x, y);
if (tile.getContiguity() >= 0) continue;
boolean[][] found = floodFill(waterMap,
new Position(x, y));
for (int yy = 0; yy < getHeight(); yy++) {
for (int xx = 0; xx < getWidth(); xx++) {
if (found[xx][yy]) {
Tile t = getTile(xx, yy);
if (t.getContiguity() < 0) {
t.setContiguity(contig);
}
}
}
}
contig++;
}
}
}
}
/**
* Sets the high seas count for all tiles connected to the high seas.
* Any ocean tiles on the map vertical edges that do not have an
* explicit false moveToEurope attribute are given a true one.
*
* Set all high seas counts negative, then start with a count of
* zero for tiles with the moveToEurope attribute or of a type
* with that ability. Iterate outward by neighbouring tile,
* incrementing the count on each pass, stopping at land. Thus,
* only the coastal land tiles will have a non-negative high seas
* count. This significantly speeds up the colony site evaluator,
* as it does not have to try to find a path to Europe for each
* tile.
*/
public void resetHighSeasCount() {
List<Tile> curr = new ArrayList<Tile>();
List<Tile> next = new ArrayList<Tile>();
int hsc = 0;
for (Tile t : getAllTiles()) {
t.setHighSeasCount(-1);
if (!t.isLand()) {
if ((t.getX() == 0 || t.getX() == getWidth()-1)
&& t.getType().isHighSeasConnected()
&& !t.getType().isDirectlyHighSeasConnected()
&& t.getMoveToEurope() == null) {
t.setMoveToEurope(true);
}
if (t.isDirectlyHighSeasConnected()) {
t.setHighSeasCount(hsc);
next.add(t);
}
}
}
while (!next.isEmpty()) {
hsc++;
curr.addAll(next);
next.clear();
while (!curr.isEmpty()) {
Tile tile = curr.remove(0);
// Deliberately using low level access to neighbours
// rather than Tile.getSurroundingTiles() because that
// relies on the map being attached to the game, which
// is not necessarily true in the test suite.
Position position = new Position(tile.getX(), tile.getY());
for (Direction d : Direction.values()) {
Position p = position.getAdjacent(d);
if (isValid(p)) {
Tile t = getTile(p);
if (t.getHighSeasCount() < 0) {
t.setHighSeasCount(hsc);
if (!t.isLand()) next.add(t);
}
}
}
}
}
}
// Location interface.
// getId() inherited.
/**
* Gets the location tile. Obviously not applicable to a Map.
*
* @return Null.
*/
public Tile getTile() {
return null;
}
/**
* {@inheritDoc}
*/
public StringTemplate getLocationName() {
return StringTemplate.key("NewWorld");
}
/**
* {@inheritDoc}
*/
public StringTemplate getLocationNameFor(Player player) {
String name = player.getNewLandName();
return (name == null) ? getLocationName() : StringTemplate.name(name);
}
/**
* {@inheritDoc}
*/
public boolean add(Locatable locatable) {
if (locatable instanceof Unit) {
Unit unit = (Unit)locatable;
unit.setLocation(unit.getEntryLocation());
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
public boolean remove(Locatable locatable) {
if (locatable instanceof Unit) {
Tile tile = ((Unit)locatable).getTile();
if (tile != null) return tile.remove(locatable);
}
return false;
}
/**
* {@inheritDoc}
*/
public boolean contains(Locatable locatable) {
return locatable instanceof Unit
&& locatable.getLocation() != null
&& locatable.getLocation().getTile() != null;
}
/**
* {@inheritDoc}
*/
public boolean canAdd(Locatable locatable) {
return locatable instanceof Unit;
}
/**
* {@inheritDoc}
*/
public int getUnitCount() {
return -1;
}
/**
* {@inheritDoc}
*/
public List<Unit> getUnitList() {
return Collections.emptyList();
}
/**
* {@inheritDoc}
*/
public Iterator<Unit> getUnitIterator() {
return getUnitList().iterator();
}
/**
* Gets the GoodsContainer for this Location. Obviously
* irrelevant for a Map.
*
* @return Null.
*/
public GoodsContainer getGoodsContainer() {
return null;
}
/**
* Gets the Settlement for this Location. Obviously irrelevant
* for a Map.
*
* @return Null.
*/
public Settlement getSettlement() {
return null;
}
/**
* Gets the Colony for this Location. Obviously irrelevant for a Map.
*
* @return Null.
*/
public Colony getColony() {
return null;
}
// Serialization
/**
* {@inheritDoc}
*/
@Override
protected void toXMLImpl(XMLStreamWriter out, Player player,
boolean showAll, boolean toSavedGame)
throws XMLStreamException {
out.writeStartElement(getXMLElementTagName());
writeAttributes(out, player, showAll, toSavedGame);
writeChildren(out, player, showAll, toSavedGame);
out.writeEndElement();
}
/**
* {@inheritDoc}
*/
protected void writeAttributes(XMLStreamWriter out, Player player,
boolean showAll, boolean toSavedGame)
throws XMLStreamException {
out.writeAttribute(ID_ATTRIBUTE, getId());
out.writeAttribute("width", Integer.toString(getWidth()));
out.writeAttribute("height", Integer.toString(getHeight()));
out.writeAttribute("layer", layer.toString());
out.writeAttribute("minimumLatitude",
Integer.toString(minimumLatitude));
out.writeAttribute("maximumLatitude",
Integer.toString(maximumLatitude));
}
/**
* {@inheritDoc}
*/
protected void writeChildren(XMLStreamWriter out, Player player,
boolean showAll, boolean toSavedGame)
throws XMLStreamException {
for (Region region : regions.values()) {
region.toXML(out);
}
for (Tile tile: getAllTiles()) {
if (showAll || toSavedGame || player.hasExplored(tile)) {
tile.toXML(out, player, showAll, toSavedGame);
} else {
tile.toXMLMinimal(out);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void readAttributes(XMLStreamReader in)
throws XMLStreamException {
setLayer(Layer.valueOf(getAttribute(in, "layer", "ALL")));
if (tiles == null) {
int width = Integer.parseInt(in.getAttributeValue(null, "width"));
int height = Integer.parseInt(in.getAttributeValue(null, "height"));
tiles = new Tile[width][height];
}
minimumLatitude = getAttribute(in, "minimumLatitude", -90);
maximumLatitude = getAttribute(in, "maximumLatitude", 90);
calculateLatitudePerRow();
}
/**
* {@inheritDoc}
*/
@Override
protected void readChildren(XMLStreamReader in) throws XMLStreamException {
boolean fixupHighSeas = false; // @compat 0.10.5
while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
String tag = in.getLocalName();
if (Tile.getXMLElementTagName().equals(tag)) {
Tile t = updateFreeColGameObject(in, Tile.class);
setTile(t, t.getX(), t.getY());
// @compat 0.10.5
if (t.getHighSeasCount() == Tile.FLAG_RECALCULATE) {
fixupHighSeas = true;
}
// @end compatibility code
} else if (Region.getXMLElementTagName().equals(tag)) {
setRegion(updateFreeColGameObject(in, Region.class));
} else {
logger.warning("Unknown tag: " + tag + " loading map");
in.nextTag();
}
}
if (fixupHighSeas) resetHighSeasCount(); // @compat 0.10.5
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "map".
*/
public static String getXMLElementTagName() {
return "map";
}
}