package games.strategy.engine.data;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import games.strategy.triplea.delegate.Matches;
import games.strategy.util.CompositeMatch;
import games.strategy.util.CompositeMatchOr;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
/**
* Holds a collection of territories, and the links between them.
* Utility methods for finding routes and distances between different territories.
*/
public class GameMap extends GameDataComponent implements Iterable<Territory> {
private static final long serialVersionUID = -4606700588396439283L;
private final List<Territory> m_territories = new ArrayList<>();
// note that all entries are unmodifiable
private final Map<Territory, Set<Territory>> m_connections = new HashMap<>();
// for fast lookup based on the string name of the territory
private final Map<String, Territory> m_territoryLookup = new HashMap<>();
// nil if the map is not grid-based
// otherwise, m_gridDimensions.length is the number of dimensions,
// and each element is the size of a dimension
private int[] m_gridDimensions = null;
GameMap(final GameData data) {
super(data);
}
public void setGridDimensions(final int... gridDimensions) {
m_gridDimensions = gridDimensions;
}
public int getXDimension() {
if (m_gridDimensions == null || m_gridDimensions.length < 1) {
return 0;
}
return m_gridDimensions[0];
}
public int getYDimension() {
if (m_gridDimensions == null || m_gridDimensions.length < 2) {
return 0;
}
return m_gridDimensions[1];
}
public Territory getTerritoryFromCoordinates(final int... coordinate) {
return getTerritoryFromCoordinates(true, coordinate);
}
// public Territory getTerritoryFromCoordinates(int xCoordinate, int yCoordinate)
public Territory getTerritoryFromCoordinates(final boolean allowNull, final int... coordinate) {
if (m_gridDimensions == null) {
if (allowNull) {
return null;
}
throw new IllegalStateException("No Grid Dimensions");
}
if (!isCoordinateValid(coordinate)) {
if (allowNull) {
return null;
}
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < coordinate.length; i++) {
sb.append(coordinate[i]);
if (i + 1 < coordinate.length) {
sb.append(", ");
}
}
throw new IllegalStateException("No Territory at coordinates: " + sb.toString());
}
int listIndex = coordinate[0];
int multiplier = 1;
for (int i = 1; i < m_gridDimensions.length; i++) {
multiplier *= m_gridDimensions[i - 1];
// m_gridDimensions[i];
listIndex += coordinate[i] * multiplier;
}
return m_territories.get(listIndex);
}
protected void reorderTerritoryList() {
Collections.sort(m_territories, TERRITORY_GRID_ORDERING);
}
private static Comparator<Territory> TERRITORY_GRID_ORDERING = (t1, t2) -> {
if ((t1 == null && t2 == null) || t1 == t2) {
return 0;
}
if (t1 == null && t2 != null) {
return 1;
}
if (t1 != null && t2 == null) {
return -1;
}
if (t1.equals(t2)) {
return 0;
}
final int t1index = t1.getName().indexOf("_");
final int t2index = t2.getName().indexOf("_");
if (t1index == -1 && t2index == -1) {
return 0;
}
if (t1index == -1 && t2index != -1) {
return 1;
}
if (t1index != -1 && t2index == -1) {
return -1;
}
final String name1 = t1.getName().substring(0, t1index);
final String name2 = t1.getName().substring(0, t2index);
if (!name1.equals(name2)) {
return name1.compareTo(name2);
}
String tname1y = t1.getName().replaceFirst(name1 + "_", "");
tname1y = tname1y.substring(tname1y.indexOf("_") + 1, tname1y.length());
final int ty1 = Integer.parseInt(tname1y);
String tname2y = t2.getName().replaceFirst(name2 + "_", "");
tname2y = tname2y.substring(tname2y.indexOf("_") + 1, tname2y.length());
final int ty2 = Integer.parseInt(tname2y);
if (ty1 < ty2) {
return -1;
} else if (ty1 > ty2) {
return 1;
}
String tname1x = t1.getName().replaceFirst(name1 + "_", "");
tname1x = tname1x.substring(0, tname1x.indexOf("_"));
final int tx1 = Integer.parseInt(tname1x);
String tname2x = t2.getName().replaceFirst(name2 + "_", "");
tname2x = tname2x.substring(0, tname2x.indexOf("_"));
final int tx2 = Integer.parseInt(tname2x);
if (tx1 < tx2) {
return -1;
} else if (tx1 > tx2) {
return 1;
}
return 0;
};
public boolean isCoordinateValid(final int... coordinate) {
if (coordinate.length != m_gridDimensions.length) {
return false;
}
for (int i = 0; i < m_gridDimensions.length; i++) {
if (coordinate[i] >= m_gridDimensions[i] || coordinate[i] < 0) {
return false;
}
}
return true;
}
protected void addTerritory(final Territory t1) {
if (m_territories.contains(t1)) {
throw new IllegalArgumentException("Map already contains " + t1.getName());
}
m_territories.add(t1);
m_connections.put(t1, Collections.emptySet());
m_territoryLookup.put(t1.getName(), t1);
}
public void removeTerritory(final Territory t1) {
if (!m_territories.contains(t1)) {
throw new IllegalArgumentException("Map does not contain " + t1.getName());
}
m_territories.remove(t1);
m_connections.remove(t1);
m_territoryLookup.remove(t1.getName());
// remove territory from other connections
final Map<Territory, Set<Territory>> tempConnections = new HashMap<>();
for (final Entry<Territory, Set<Territory>> entry : m_connections.entrySet()) {
if (entry.getValue().contains(t1)) {
final Set<Territory> current = entry.getValue();
final Set<Territory> modified = new HashSet<>(current);
modified.remove(t1);
tempConnections.put(entry.getKey(), modified);
}
}
// preserve unmodifiable nature
for (final Entry<Territory, Set<Territory>> entry : tempConnections.entrySet()) {
m_connections.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue()));
}
}
/**
* Bi-directional. T1 connects to T2, and T2 connects to T1.
*/
protected void addConnection(final Territory t1, final Territory t2) {
if (t1.equals(t2)) {
throw new IllegalArgumentException("Cannot connect a territory to itself");
}
if (!m_territories.contains(t1) || !m_territories.contains(t2)) {
throw new IllegalArgumentException("Map doesnt know about one of " + t1 + " " + t2);
}
// connect t1 to t2
setConnection(t1, t2);
setConnection(t2, t1);
}
/**
* Uni-directional. T1 connects to T2, while T2 does NOT connect to T1.
*/
protected void addOneWayConnection(final Territory t1, final Territory t2) {
if (t1.equals(t2)) {
throw new IllegalArgumentException("Cannot connect a territory to itself");
}
if (!m_territories.contains(t1) || !m_territories.contains(t2)) {
throw new IllegalArgumentException("Map doesnt know about one of " + t1 + " " + t2);
}
// connect t1 to t2
setConnection(t1, t2);
}
private void setConnection(final Territory from, final Territory to) {
// preserves the unmodifiable nature of the entries
final Set<Territory> current = m_connections.get(from);
final Set<Territory> modified = new HashSet<>(current);
modified.add(to);
m_connections.put(from, Collections.unmodifiableSet(modified));
}
/**
* @param s
* name of the searched territory (case sensitive)
* @return the territory with the given name, or null if no territory can be found (case sensitive).
*/
public Territory getTerritory(final String s) {
return m_territoryLookup.get(s);
}
/**
* @param t
* referring territory
* @return All adjacent neighbors of the starting territory.
* Does NOT include the original/starting territory in the returned Set.
*/
public Set<Territory> getNeighbors(final Territory t) {
// ok since all entries in connections are already unmodifiable
final Set<Territory> neighbors = m_connections.get(t);
if (neighbors == null) {
throw new IllegalArgumentException("No neighbors for:" + t);
}
return neighbors;
}
/**
* @param t
* referring territory
* @param cond
* condition the neighboring territories have to match
* @return All adjacent neighbors of the starting territory that match the condition.
* Does NOT include the original/starting territory in the returned Set.
*/
public Set<Territory> getNeighbors(final Territory t, final Match<Territory> cond) {
if (cond == null) {
return getNeighbors(t);
}
final Set<Territory> possible = m_connections.get(t);
final Set<Territory> passed = new HashSet<>();
if (possible == null) {
return passed;
}
for (final Territory current : possible) {
if (cond.match(current)) {
passed.add(current);
}
}
return passed;
}
/**
* @param territory
* referring territory
* @param distance
* maximal distance of the neighboring territories
* @return All neighbors within a certain distance of the starting territory that match the condition.
* Does NOT include the original/starting territory in the returned Set.
*/
@SuppressWarnings("unchecked")
public Set<Territory> getNeighbors(final Territory territory, int distance) {
if (distance < 0) {
throw new IllegalArgumentException("Distance must be positive not:" + distance);
}
if (distance == 0) {
return Collections.EMPTY_SET;
}
final Set<Territory> start = getNeighbors(territory);
if (distance == 1) {
return start;
}
final Set<Territory> neighbors = getNeighbors(start, new HashSet<>(start), --distance);
neighbors.remove(territory);
return neighbors;
}
/**
* @return All neighbors within a certain distance of the starting territory that match the condition.
* Does NOT include the original/starting territory in the returned Set.
*/
@SuppressWarnings("unchecked")
public Set<Territory> getNeighbors(final Territory territory, int distance, final Match<Territory> cond) {
if (distance < 0) {
throw new IllegalArgumentException("Distance must be positive not:" + distance);
}
if (distance == 0) {
return Collections.EMPTY_SET;
}
final Set<Territory> start = getNeighbors(territory, cond);
if (distance == 1) {
return start;
}
final Set<Territory> neighbors = getNeighbors(start, new HashSet<>(start), --distance, cond);
neighbors.remove(territory);
return neighbors;
}
/**
* @return All neighbors within a certain distance of the starting territory set that match the condition.
* Does NOT include the original/starting territories in the returned Set, even if they are neighbors of each
* other.
*/
public Set<Territory> getNeighbors(final Set<Territory> frontier, final int distance, final Match<Territory> cond) {
final Set<Territory> rVal = getNeighbors(frontier, new HashSet<>(frontier), distance, cond);
rVal.removeAll(frontier);
return rVal;
}
/**
* @return All neighbors within a certain distance of the starting territory set.
* Does NOT include the original/starting territories in the returned Set, even if they are neighbors of each
* other.
*/
public Set<Territory> getNeighbors(final Set<Territory> frontier, final int distance) {
final Set<Territory> rVal = getNeighbors(frontier, new HashSet<>(frontier), distance);
rVal.removeAll(frontier);
return rVal;
}
private Set<Territory> getNeighbors(final Set<Territory> frontier, final Set<Territory> searched, int distance,
final Match<Territory> cond) {
if (distance == 0) {
return searched;
}
final Iterator<Territory> iter = frontier.iterator();
final Set<Territory> newFrontier = new HashSet<>();
while (iter.hasNext()) {
final Territory t = iter.next();
newFrontier.addAll(getNeighbors(t, cond));
}
newFrontier.removeAll(searched);
searched.addAll(newFrontier);
return getNeighbors(newFrontier, searched, --distance, cond);
}
private Set<Territory> getNeighbors(final Set<Territory> frontier, final Set<Territory> searched, int distance) {
if (distance == 0) {
return searched;
}
final Iterator<Territory> iter = frontier.iterator();
final Set<Territory> newFrontier = new HashSet<>();
while (iter.hasNext()) {
final Territory t = iter.next();
newFrontier.addAll(getNeighbors(t));
}
newFrontier.removeAll(searched);
searched.addAll(newFrontier);
return getNeighbors(newFrontier, searched, --distance);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @return the shortest route between two territories or null if no route exists.
*/
public Route getRoute(final Territory t1, final Territory t2) {
return getRoute(t1, t2, Matches.TerritoryIsLandOrWater);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @return the shortest land route between two territories or null if no route exists.
*/
public Route getLandRoute(final Territory t1, final Territory t2) {
return getRoute(t1, t2, Matches.TerritoryIsLand);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @return the shortest water route between two territories or null if no route exists.
*/
public Route getWaterRoute(final Territory t1, final Territory t2) {
return getRoute(t1, t2, Matches.TerritoryIsWater);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @param cond
* condition that covered territories of the route must match
* @return the shortest route between two territories so that covered territories match the condition
* or null if no route exists.
*/
public Route getRoute(final Territory t1, final Territory t2, final Match<Territory> cond) {
if (t1 == t2) {
return new Route(t1);
}
if (getNeighbors(t1, cond).contains(t2)) {
return new Route(t1, t2);
}
final RouteFinder engine = new RouteFinder(this, cond);
return engine.findRoute(t1, t2);
}
public Route getRoute_IgnoreEnd(final Territory t1, final Territory t2, final Match<Territory> match) {
return getRoute(t1, t2, new CompositeMatchOr<>(Matches.territoryIs(t2), match));
}
/**
* A composite route between two territories
* Example set of matches: [Friendly Land, score: 1] [Enemy Land, score: 2] [Neutral Land, score = 4]
* With this example set, an 8 length friendly route is considered equal in score to a 4 length enemy route and a 2
* length neutral route.
* This is because the friendly route score is 1/2 of the enemy route score and 1/4 of the neutral route score.
* Note that you can choose whatever scores you want, and that the matches can mix and match with each other in any
* way.
* (Recommended that you use 2,3,4 as scores, unless you will allow routes to be much longer under certain conditions)
* Returns null if there is no route that exists that matches any of the matches.
*
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @param matches
* HashMap of territory matches for covered territories
* @return a composite route between two territories
*/
public Route getCompositeRoute(final Territory t1, final Territory t2,
final HashMap<Match<Territory>, Integer> matches) {
if (t1 == t2) {
return new Route(t1);
}
final CompositeMatch<Territory> allCond = new CompositeMatchOr<>(matches.keySet());
if (getNeighbors(t1, allCond).contains(t2)) {
return new Route(t1, t2);
}
final CompositeRouteFinder engine = new CompositeRouteFinder(this, matches);
return engine.findRoute(t1, t2);
}
public Route getCompositeRoute_IgnoreEnd(final Territory t1, final Territory t2,
final HashMap<Match<Territory>, Integer> matches) {
matches.put(Matches.territoryIs(t2), 0);
return getCompositeRoute(t1, t2, matches);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @return the distance between two territories or -1 if they are not connected.
*/
public int getDistance(final Territory t1, final Territory t2) {
return getDistance(t1, t2, Matches.TerritoryIsLandOrWater);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @return the land distance between two territories or -1 if they are not connected.
*/
public int getLandDistance(final Territory t1, final Territory t2) {
return getDistance(t1, t2, Matches.TerritoryIsLand);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @return the water distance between two territories or -1 if they are not connected.
*/
public int getWaterDistance(final Territory t1, final Territory t2) {
return getDistance(t1, t2, Matches.TerritoryIsWater);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @param cond
* condition that covered territories of the route must match
* @return the distance between two territories where the covered territories of the route satisfy the condition
* or -1 if they are not connected.
*/
public int getDistance(final Territory t1, final Territory t2, final Match<Territory> cond) {
if (t1.equals(t2)) {
return 0;
}
final Set<Territory> frontier = new HashSet<>();
frontier.add(t1);
return getDistance(0, new HashSet<>(), frontier, t2, cond);
}
/**
* @param t1
* start territory of the route
* @param t2
* end territory of the route
* @param cond
* condition that covered territories of the route must match EXCEPT FOR THE END
* @return the distance between two territories where the covered territories of the route (except the end) satisfy
* the condition
* or -1 if they are not connected. (Distance includes to the end)
*/
public int getDistance_IgnoreEndForCondition(final Territory t1, final Territory t2, final Match<Territory> cond) {
return getDistance(t1, t2, new CompositeMatchOr<>(Matches.territoryIs(t2), cond));
}
/**
* Guaranteed that frontier doesn't contain target.
* Territories on the frontier are not target. They represent the extent of paths already searched.
* Territories in searched have already been on the frontier.
*/
private int getDistance(final int distance, final Set<Territory> searched, final Set<Territory> frontier,
final Territory target, final Match<Territory> cond) {
// add the frontier to the searched
searched.addAll(frontier);
// find the new frontier
final Set<Territory> newFrontier = new HashSet<>();
final Iterator<Territory> frontierIterator = frontier.iterator();
while (frontierIterator.hasNext()) {
final Territory onFrontier = frontierIterator.next();
final Set<Territory> connections = m_connections.get(onFrontier);
final Iterator<Territory> connectionIterator = connections.iterator();
while (connectionIterator.hasNext()) {
final Territory nextFrontier = connectionIterator.next();
if (cond == null || cond.match(nextFrontier)) {
newFrontier.add(nextFrontier);
}
}
}
if (newFrontier.contains(target)) {
return distance + 1;
}
newFrontier.removeAll(searched);
if (newFrontier.isEmpty()) {
return -1;
}
return getDistance(distance + 1, searched, newFrontier, target, cond);
}
public IntegerMap<Territory> getDistance(final Territory target, final Collection<Territory> territories,
final Match<Territory> condition) {
final IntegerMap<Territory> rVal = new IntegerMap<>();
if (target == null || territories == null || territories.isEmpty()) {
return rVal;
}
for (final Territory t : territories) {
rVal.put(t, getDistance(target, t, condition));
}
return rVal;
}
public List<Territory> getTerritories() {
return Collections.unmodifiableList(m_territories);
}
@Override
public Iterator<Territory> iterator() {
return m_territories.iterator();
}
public List<Territory> getTerritoriesOwnedBy(final PlayerID player) {
final Iterator<Territory> iter = m_territories.iterator();
final List<Territory> owner = new ArrayList<>();
while (iter.hasNext()) {
final Territory territory = iter.next();
if (territory.getOwner().equals(player)) {
owner.add(territory);
}
}
return owner;
}
/**
* @param route
* route containing the territories in question
* @return whether each territory is connected to the preceding territory.
*/
public boolean isValidRoute(final Route route) {
Territory previous = null;
for (final Territory t : route) {
if (previous != null) {
if (!getNeighbors(previous).contains(t)) {
return false;
}
}
previous = t;
}
return true;
}
/**
* If the actual territories in the map are deleted, or new ones added, call this.
*/
public void notifyChanged() {
getData().notifyMapDataChanged();
}
}