package games.strategy.engine.data; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.Matches; import games.strategy.util.Match; import games.strategy.util.Util; /** * A route between two territories. * * <p> * A route consists of a start territory, and a sequence of steps. To create a route do, * <code> * Route aRoute = new Route(); * route.setStart(someTerritory); * route.add(anotherTerritory); * route.add(yetAnotherTerritory); * </code> * </p> */ public class Route implements Serializable, Iterable<Territory> { private static final long serialVersionUID = 8743882455488948557L; static final List<Territory> emptyTerritoryList = Collections.unmodifiableList(new ArrayList<>()); private final List<Territory> m_steps = new ArrayList<>(); private Territory m_start; public Route() {} public Route(final List<Territory> route) { setStart(route.get(0)); if (route.size() == 1) { return; } for (final Territory t : route.subList(1, route.size())) { add(t); } } public Route(final Territory start, final Territory... route) { setStart(start); for (final Territory t : route) { add(t); } } /** * Join the two routes. It must be the case that r1.end() equals r2.start() * or r1.end() == null and r1.start() equals r2 * * @param r1 * route 1 * @param r2 * route 2 * @return a new Route starting at r1.start() going to r2.end() along r1, * r2, or null if the routes can't be joined it the joining would * form a loop */ public static Route join(final Route r1, final Route r2) { if (r1 == null || r2 == null) { // throw new IllegalArgumentException("route cant be null r1:" + r1 + " r2:" + r2); return null; } if (r1.numberOfSteps() == 0) { if (!r1.getStart().equals(r2.getStart())) { throw new IllegalArgumentException("Cannot join, r1 doesnt end where r2 starts. r1:" + r1 + " r2:" + r2); } } else { if (!r1.getEnd().equals(r2.getStart())) { throw new IllegalArgumentException("Cannot join, r1 doesnt end where r2 starts. r1:" + r1 + " r2:" + r2); } } final Collection<Territory> c1 = new ArrayList<>(r1.m_steps); c1.add(r1.getStart()); final Collection<Territory> c2 = new ArrayList<>(r2.m_steps); if (!Util.intersection(c1, c2).isEmpty()) { return null; } final Route joined = new Route(); joined.setStart(r1.getStart()); for (final Territory t : r1.getSteps()) { joined.add(t); } for (final Territory t : r2.getSteps()) { joined.add(t); } return joined; } @Override public boolean equals(final Object o) { if (o == null) { return false; } final Route other = (Route) o; if (!(other.numberOfSteps() == this.numberOfSteps())) { return false; } if (!other.getStart().equals(this.getStart())) { return false; } return other.getAllTerritories().equals(this.getAllTerritories()); } @Override public int hashCode() { return toString().hashCode(); } /** * Set the start of this route. * * @param t * new start territory */ public void setStart(final Territory t) { if (t == null) { throw new IllegalStateException("Null territory"); } m_start = t; } /** * @return start territory for this route. */ public Territory getStart() { return m_start; } /** * Determines if the route crosses water by checking if any of the * territories except the start and end are sea territories. * * @return whether the route encounters water other than at the start of the * route. */ public boolean crossesWater() { final boolean startLand = !m_start.isWater(); boolean overWater = false; final Iterator<Territory> routeIter = m_steps.iterator(); Territory terr = null; while (routeIter.hasNext()) { terr = routeIter.next(); if (terr.isWater()) { overWater = true; } } if (terr == null) { return false; } // If we started on land, went over water, and ended on land, we cross // water. return (startLand && overWater && !terr.isWater()); } /** * Add the given territory to the end of the route. * * @param t * referring territory */ public void add(final Territory t) { if (t == null) { throw new IllegalStateException("Null territory"); } if (t.equals(m_start) || m_steps.contains(t)) { throw new IllegalArgumentException("Loops not allowed in m_routes, route:" + this + " new territory:" + t); } m_steps.add(t); } /** * @param u * unit that is moving on this route * @return the total cost of the route including modifications due to territoryEffects and territoryConnections. */ public int getMovementCost(final Unit u) { // TODO implement me return m_steps.size(); } /** * @return The number of steps in this route. Does not include start. */ public int numberOfSteps() { return m_steps.size(); } /** * @return The number of steps in this route. DOES include start. */ public int numberOfStepsIncludingStart() { return this.getAllTerritories().size(); } /** * @param i * step number * @return territory we will be in after the i'th step for this route has * been made. */ public Territory getTerritoryAtStep(final int i) { return m_steps.get(i); } /** * @param aMatch * referring match * @return whether all territories in this route match the given match (start territory is not tested). */ public boolean allMatch(final Match<Territory> aMatch) { for (final Territory t : m_steps) { if (!aMatch.match(t)) { return false; } } return true; } /** * @param aMatch * referring match * @return whether some territories in this route match the given match (start territory is not tested). */ public boolean someMatch(final Match<Territory> aMatch) { for (final Territory t : m_steps) { if (aMatch.match(t)) { return true; } } return false; } /** * @param aMatch * referring match * @return whether all territories in this route match the given match (start and end territories are not tested). */ public boolean allMatchMiddleSteps(final Match<Territory> aMatch, final boolean defaultWhenNoMiddleSteps) { final List<Territory> middle = getMiddleSteps(); if (middle.isEmpty()) { return defaultWhenNoMiddleSteps; } for (final Territory t : middle) { if (!aMatch.match(t)) { return false; } } return true; } /** * @param aMatch * referring match * @return all territories in this route match the given match (start territory is not tested). */ public Collection<Territory> getMatches(final Match<Territory> aMatch) { return Match.getMatches(m_steps, aMatch); } @Override public String toString() { final StringBuilder buf = new StringBuilder("Route:").append(m_start); for (final Territory t : getSteps()) { buf.append(" -> "); buf.append(t.getName()); } return buf.toString(); } public List<Territory> getAllTerritories() { final ArrayList<Territory> list = new ArrayList<>(m_steps); list.add(0, m_start); return list; } /** * @return collection of all territories in this route, without the start. */ public List<Territory> getSteps() { if (numberOfSteps() > 0) { return new ArrayList<>(m_steps); } return emptyTerritoryList; } /** * @return collection of all territories in this route without the start or * the end. */ public List<Territory> getMiddleSteps() { if (numberOfSteps() > 1) { return new ArrayList<>(m_steps).subList(0, numberOfSteps() - 1); } return emptyTerritoryList; } /** * @return last territory in the route. */ public Territory getEnd() { if (m_steps.isEmpty()) { return m_start; } return m_steps.get(m_steps.size() - 1); } @Override public Iterator<Territory> iterator() { return Collections.unmodifiableList(getAllTerritories()).iterator(); } /** * @return whether this route has any steps. */ public boolean hasSteps() { return !m_steps.isEmpty(); } /** * @return whether this route has no steps. */ public boolean hasNoSteps() { return !hasSteps(); } /** * This means that there are 2 territories in the route: the start and the end (this is only 1 step). * * @return whether the route has 1 step */ public boolean hasExactlyOneStep() { return this.m_steps.size() == 1; } /** * the territory before the end territory (this could be the start territory * in the case of 1 step). * * @return the territory before the end territory */ public Territory getTerritoryBeforeEnd() { if (m_steps.size() <= 1) { return getStart(); } else { return getTerritoryAtStep(m_steps.size() - 2); } } /** * This only checks if start is water and end is not water. * * @return whether this route is an unloading route (unloading from transport to land) */ public boolean isUnload() { if (hasNoSteps()) { return false; } // we should not check if there is only 1 step, because otherwise movement validation will let users move their // tanks over water, so // long as they end on land return getStart().isWater() && !getEnd().isWater(); } /** * This only checks if start is not water, and end is water. * * @return whether this route is a loading route (loading from land into a transport @ sea) */ public boolean isLoad() { if (hasNoSteps()) { return false; } return !getStart().isWater() && getEnd().isWater(); } /** * @return whether this route has more then one step. */ public boolean hasMoreThenOneStep() { return m_steps.size() > 1; } /** * @return whether there are territories before the end where the territory is owned by null and is not sea. */ public boolean hasNeutralBeforeEnd() { for (final Territory current : getMiddleSteps()) { // neutral is owned by null and is not sea if (!current.isWater() && current.getOwner().equals(PlayerID.NULL_PLAYERID)) { return true; } } return false; } /** * @return whether there is some water in the route including start and end. */ public boolean hasWater() { if (getStart().isWater()) { return true; } return Match.someMatch(getSteps(), Matches.TerritoryIsWater); } /** * @return whether there is some land in the route including start and end. */ public boolean hasLand() { if (!getStart().isWater()) { return true; } return !Match.allMatch(getAllTerritories(), Matches.TerritoryIsWater); } public int getLargestMovementCost(final Collection<Unit> units) { int largestCost = 0; for (final Unit unit : units) { largestCost = Math.max(largestCost, getMovementCost(unit)); } return largestCost; } public int getMovementLeft(final Unit unit) { final int movementLeft = ((TripleAUnit) unit).getMovementLeft() - getMovementCost(unit); return movementLeft; } public ResourceCollection getMovementFuelCostCharge(final Unit unit, final GameData data) { final ResourceCollection col = new ResourceCollection(data); if (Matches.unitIsBeingTransported().match(unit)) { return col; } final UnitAttachment ua = UnitAttachment.get(unit.getType()); col.add(ua.getFuelCost()); col.multiply(getMovementCost(unit)); return col; } public static ResourceCollection getMovementFuelCostCharge(final Collection<Unit> unitsAll, final Route route, final PlayerID currentPlayer, final GameData data /* , final boolean mustFight */) { final Set<Unit> units = new HashSet<>(unitsAll); units.removeAll(Match.getMatches(unitsAll, Matches.unitIsBeingTransportedByOrIsDependentOfSomeUnitInThisList(unitsAll, route, currentPlayer, data, true))); final ResourceCollection movementCharge = new ResourceCollection(data); for (final Unit unit : units) { movementCharge.add(route.getMovementFuelCostCharge(unit, data)); } return movementCharge; } public static Route create(final List<Route> routes) { Route route = new Route(); for (final Route r : routes) { route = Route.join(route, r); } return route; } }