package games.strategy.triplea.delegate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.ResourceCollection;
import games.strategy.engine.data.Route;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.UnitType;
import games.strategy.triplea.Constants;
import games.strategy.triplea.Properties;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.CanalAttachment;
import games.strategy.triplea.attachments.PlayerAttachment;
import games.strategy.triplea.attachments.RulesAttachment;
import games.strategy.triplea.attachments.TechAttachment;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.delegate.dataObjects.MoveValidationResult;
import games.strategy.triplea.delegate.dataObjects.MustMoveWithDetails;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.util.TransportUtils;
import games.strategy.triplea.util.UnitCategory;
import games.strategy.triplea.util.UnitSeperator;
import games.strategy.util.CompositeMatch;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.CompositeMatchOr;
import games.strategy.util.InverseMatch;
import games.strategy.util.Match;
/**
* Provides some static methods for validating movement.
*/
public class MoveValidator {
public static final String TRANSPORT_HAS_ALREADY_UNLOADED_UNITS_IN_A_PREVIOUS_PHASE =
"Transport has already unloaded units in a previous phase";
public static final String TRANSPORT_MAY_NOT_UNLOAD_TO_FRIENDLY_TERRITORIES_UNTIL_AFTER_COMBAT_IS_RESOLVED =
"Transport may not unload to friendly territories until after combat is resolved";
public static final String ENEMY_SUBMARINE_PREVENTING_UNESCORTED_AMPHIBIOUS_ASSAULT_LANDING =
"Enemy Submarine Preventing Unescorted Amphibious Assault Landing";
public static final String TRANSPORT_HAS_ALREADY_UNLOADED_UNITS_TO = "Transport has already unloaded units to ";
public static final String CANNOT_LOAD_AND_UNLOAD_AN_ALLIED_TRANSPORT_IN_THE_SAME_ROUND =
"Cannot load and unload an allied transport in the same round";
public static final String CANT_MOVE_THROUGH_IMPASSABLE = "Can't move through impassable territories";
public static final String CANT_MOVE_THROUGH_RESTRICTED = "Can't move through restricted territories";
public static final String TOO_POOR_TO_VIOLATE_NEUTRALITY = "Not enough money to pay for violating neutrality";
public static final String CANNOT_VIOLATE_NEUTRALITY = "Cannot violate neutrality";
public static final String NOT_ALL_AIR_UNITS_CAN_LAND = "Not all air units can land";
public static final String TRANSPORT_CANNOT_LOAD_AND_UNLOAD_AFTER_COMBAT =
"Transport cannot both load AND unload after being in combat";
public static final String LOST_BLITZ_ABILITY = "Unit lost blitz ability";
public static final String NOT_ALL_UNITS_CAN_BLITZ = "Not all units can blitz";
public static MoveValidationResult validateMove(final Collection<Unit> units, final Route route,
final PlayerID player, final Collection<Unit> transportsToLoad, final Map<Unit, Collection<Unit>> newDependents,
final boolean isNonCombat, final List<UndoableMove> undoableMoves, final GameData data) {
final MoveValidationResult result = new MoveValidationResult();
if (route.hasNoSteps()) {
return result;
}
if (validateFirst(data, units, route, player, result).getError() != null) {
return result;
}
if (isNonCombat) {
if (validateNonCombat(data, units, route, player, result).getError() != null) {
return result;
}
} else {
if (validateCombat(data, units, route, player, result).getError() != null) {
return result;
}
}
if (validateNonEnemyUnitsOnPath(data, units, route, player, result).getError() != null) {
return result;
}
if (validateBasic(data, units, route, player, transportsToLoad, newDependents, result)
.getError() != null) {
return result;
}
if (AirMovementValidator.validateAirCanLand(data, units, route, player, result).getError() != null) {
return result;
}
if (validateTransport(isNonCombat, data, undoableMoves, units, route, player, transportsToLoad,
result).getError() != null) {
return result;
}
if (validateParatroops(isNonCombat, data, units, route, player, result).getError() != null) {
return result;
}
if (validateCanal(data, units, route, player, result).getError() != null) {
return result;
}
if (validateFuel(data, units, route, player, result).getError() != null) {
return result;
}
// dont let the user move out of a battle zone
// the exception is air units and unloading units into a battle zone
if (AbstractMoveDelegate.getBattleTracker(data).hasPendingBattle(route.getStart(), false)
&& Match.someMatch(units, Matches.UnitIsNotAir)) {
// if the units did not move into the territory, then they can move out
// this will happen if there is a submerged sub in the area, and
// a different unit moved into the sea zone setting up a battle
// but the original unit can still remain
boolean unitsStartedInTerritory = true;
for (final Unit unit : units) {
if (AbstractMoveDelegate.getRouteUsedToMoveInto(undoableMoves, unit, route.getEnd()) != null) {
unitsStartedInTerritory = false;
break;
}
}
if (!unitsStartedInTerritory) {
final boolean unload = route.isUnload();
final PlayerID endOwner = route.getEnd().getOwner();
final boolean attack =
!data.getRelationshipTracker().isAllied(endOwner, player)
|| AbstractMoveDelegate.getBattleTracker(data).wasConquered(route.getEnd());
// unless they are unloading into another battle
if (!(unload && attack)) {
return result.setErrorReturnResult("Cannot move units out of battle zone");
}
}
}
return result;
}
static MoveValidationResult validateFirst(final GameData data, final Collection<Unit> units, final Route route,
final PlayerID player, final MoveValidationResult result) {
if (!units.isEmpty()
&& !getEditMode(data)
&& !Match.allMatch(Match.getMatches(units,
Matches.unitIsBeingTransportedByOrIsDependentOfSomeUnitInThisList(units, route, player, data, true)
.invert()),
Matches.unitIsOwnedBy(player))) {
result.setError("Player, " + player.getName() + ", is not owner of all the units: "
+ MyFormatter.unitsToTextNoOwner(units));
return result;
}
// this should never happen
if (new HashSet<>(units).size() != units.size()) {
result.setError("Not all units unique, units:" + units + " unique:" + new HashSet<>(units));
return result;
}
if (!data.getMap().isValidRoute(route)) {
result.setError("Invalid route:" + route);
return result;
}
if (validateMovementRestrictedByTerritory(data, route, player, result).getError() != null) {
return result;
}
// cannot enter territories owned by a player to which we are neutral towards
final Collection<Territory> landOnRoute = route.getMatches(Matches.TerritoryIsLand);
if (!landOnRoute.isEmpty()) {
// TODO: if this ever changes, we need to also update getBestRoute(), because getBestRoute is also checking to
// make sure we avoid land
// territories owned by nations with these 2 relationship type attachment options
for (final Territory t : landOnRoute) {
if (Match.someMatch(units, Matches.UnitIsLand)) {
if (!data.getRelationshipTracker().canMoveLandUnitsOverOwnedLand(player, t.getOwner())) {
result.setError(player.getName() + " may not move land units over land owned by " + t.getOwner().getName());
return result;
}
}
if (Match.someMatch(units, Matches.UnitIsAir)) {
if (!data.getRelationshipTracker().canMoveAirUnitsOverOwnedLand(player, t.getOwner())) {
result.setError(player.getName() + " may not move air units over land owned by " + t.getOwner().getName());
return result;
}
}
}
}
if (units.size() == 0) {
return result.setErrorReturnResult("No units");
}
for (final Unit unit : units) {
if (TripleAUnit.get(unit).getSubmerged()) {
result.addDisallowedUnit("Cannot move submerged units", unit);
}
}
// make sure all units are actually in the start territory
if (!route.getStart().getUnits().containsAll(units)) {
return result.setErrorReturnResult("Not enough units in starting territory");
}
return result;
}
static MoveValidationResult validateFuel(final GameData data, final Collection<Unit> units, final Route route,
final PlayerID player, final MoveValidationResult result) {
if (getEditMode(data)) {
return result;
}
if (!games.strategy.triplea.Properties.getUseFuelCost(data)) {
return result;
}
final ResourceCollection fuelCost = Route.getMovementFuelCostCharge(units, route, player, data);
if (player.getResources().has(fuelCost.getResourcesCopy())) {
return result;
}
return result.setErrorReturnResult("Not enough resources to perform this move, you need: " + fuelCost
+ " for this move");
}
private static MoveValidationResult validateCanal(final GameData data, final Collection<Unit> units,
final Route route, final PlayerID player, final MoveValidationResult result) {
if (getEditMode(data)) {
return result;
}
// TODO: merge validateCanal here and provide granular unit warnings
return result.setErrorReturnResult(validateCanal(route, units, player, data));
}
private static MoveValidationResult validateCombat(final GameData data, final Collection<Unit> units,
final Route route, final PlayerID player, final MoveValidationResult result) {
if (getEditMode(data)) {
return result;
}
for (final Territory t : route.getSteps()) {
if (!Matches.territoryOwnerRelationshipTypeCanMoveIntoDuringCombatMove(player).match(t)) {
return result.setErrorReturnResult("Cannot move into territories owned by " + t.getOwner().getName()
+ " during Combat Movement Phase");
}
}
// we are in a contested territory owned by the enemy, and we want to move to another enemy owned territory. do not
// allow unless each
// unit can blitz the current territory.
if (!route.getStart().isWater()
&& Matches.isAtWar(route.getStart().getOwner(), data).match(player)
&& (route.someMatch(Matches.isTerritoryEnemy(player, data)) && !route.allMatchMiddleSteps(Matches
.isTerritoryEnemy(player, data).invert(), false))) {
if (!Matches.TerritoryIsBlitzable(player, data).match(route.getStart())
&& !Match.allMatch(units, Matches.UnitIsAir)) {
return result.setErrorReturnResult("Cannot blitz out of a battle further into enemy territory");
}
for (final Unit u : Match.getMatches(units, new CompositeMatchAnd<>(Matches.UnitCanBlitz.invert(),
Matches.UnitIsNotAir))) {
result.addDisallowedUnit("Not all units can blitz out of empty enemy territory", u);
}
}
// we are in a contested territory owned by us, and we want to move to an enemy owned territory. do not allow unless
// the territory is
// blitzable.
if (!route.getStart().isWater()
&& !Matches.isAtWar(route.getStart().getOwner(), data).match(player)
&& (route.someMatch(Matches.isTerritoryEnemy(player, data)) && !route.allMatchMiddleSteps(Matches
.isTerritoryEnemy(player, data).invert(), false))) {
if (!Matches.TerritoryIsBlitzable(player, data).match(route.getStart())
&& !Match.allMatch(units, Matches.UnitIsAir)) {
return result.setErrorReturnResult("Cannot blitz out of a battle into enemy territory");
}
}
// Don't allow aa guns (and other disallowed units) to move in combat unless they are in a transport
if (Match.someMatch(units, Matches.UnitCanNotMoveDuringCombatMove)
&& (!route.getStart().isWater() || !route.getEnd().isWater())) {
for (final Unit unit : Match.getMatches(units, Matches.UnitCanNotMoveDuringCombatMove)) {
result.addDisallowedUnit("Cannot move AA guns in combat movement phase", unit);
}
}
// if there is a neutral in the middle must stop unless all are air or getNeutralsBlitzable
if (route.hasNeutralBeforeEnd()) {
if (!Match.allMatch(units, Matches.UnitIsAir) && !isNeutralsBlitzable(data)) {
return result.setErrorReturnResult("Must stop land units when passing through neutral territories");
}
}
if (Match.someMatch(units, Matches.UnitIsLand) && route.hasSteps()) {
// check all the territories but the end, if there are enemy territories, make sure they are blitzable
// if they are not blitzable, or we aren't all blitz units fail
int enemyCount = 0;
boolean allEnemyBlitzable = true;
for (final Territory current : route.getMiddleSteps()) {
if (current.isWater()) {
continue;
}
if (data.getRelationshipTracker().isAtWar(current.getOwner(), player)
|| AbstractMoveDelegate.getBattleTracker(data).wasConquered(current)) {
enemyCount++;
allEnemyBlitzable &= Matches.TerritoryIsBlitzable(player, data).match(current);
}
}
if (enemyCount > 0 && !allEnemyBlitzable) {
if (nonParatroopersPresent(player, units)) {
return result.setErrorReturnResult("Cannot blitz on that route");
}
} else if (allEnemyBlitzable && !(route.getStart().isWater() || route.getEnd().isWater())) {
final Match<Unit> blitzingUnit = new CompositeMatchOr<>(Matches.UnitCanBlitz, Matches.UnitIsAir);
final Match<Unit> nonBlitzing = new InverseMatch<>(blitzingUnit);
final Collection<Unit> nonBlitzingUnits = Match.getMatches(units, nonBlitzing);
// remove any units that gain blitz due to certain abilities
nonBlitzingUnits.removeAll(UnitAttachment.getUnitsWhichReceivesAbilityWhenWith(units, "canBlitz", data));
final Match<Territory> territoryIsNotEnd = new InverseMatch<>(Matches.territoryIs(route.getEnd()));
final Match<Territory> nonFriendlyTerritories =
new InverseMatch<>(Matches.isTerritoryFriendly(player, data));
final Match<Territory> notEndOrFriendlyTerrs =
new CompositeMatchAnd<>(nonFriendlyTerritories, territoryIsNotEnd);
final Match<Territory> foughtOver = Matches.territoryWasFoughOver(AbstractMoveDelegate.getBattleTracker(data));
final Match<Territory> notEndWasFought = new CompositeMatchAnd<>(territoryIsNotEnd, foughtOver);
final boolean wasStartFoughtOver =
AbstractMoveDelegate.getBattleTracker(data).wasConquered(route.getStart())
|| AbstractMoveDelegate.getBattleTracker(data).wasBlitzed(route.getStart());
nonBlitzingUnits.addAll(Match.getMatches(units, Matches.unitIsOfTypes(TerritoryEffectHelper
.getUnitTypesThatLostBlitz((wasStartFoughtOver ? route.getAllTerritories() : route.getSteps())))));
for (final Unit unit : nonBlitzingUnits) {
// TODO: we need to actually test if the units is being air transported, or mech-land-transported.
if (Matches.UnitIsAirTransportable.match(unit)) {
continue;
}
if (Matches.UnitIsInfantry.match(unit)) {
continue;
}
final TripleAUnit tAUnit = (TripleAUnit) unit;
if (wasStartFoughtOver || tAUnit.getWasInCombat() || route.someMatch(notEndOrFriendlyTerrs)
|| route.someMatch(notEndWasFought)) {
result.addDisallowedUnit(NOT_ALL_UNITS_CAN_BLITZ, unit);
}
}
}
}
if (Match.someMatch(units, Matches.UnitIsAir)) { // check aircraft
if (route.hasSteps()
&& (!games.strategy.triplea.Properties.getNeutralFlyoverAllowed(data) || isNeutralsImpassable(data))) {
if (Match.someMatch(route.getMiddleSteps(), Matches.TerritoryIsNeutralButNotWater)) {
return result.setErrorReturnResult("Air units cannot fly over neutral territories");
}
}
}
// make sure no conquered territories on route
if (MoveValidator.hasConqueredNonBlitzedNonWaterOnRoute(route, data)) {
// unless we are all air or we are in non combat OR the route is water (was a bug in convoy zone movement)
if (!Match.allMatch(units, Matches.UnitIsAir)) {
// what if we are paratroopers?
return result.setErrorReturnResult("Cannot move through newly captured territories");
}
}
// See if they've already been in combat
if (Match.someMatch(units, Matches.UnitWasInCombat) && Match.someMatch(units, Matches.UnitWasUnloadedThisTurn)) {
final Collection<Territory> end = Collections.singleton(route.getEnd());
if (Match.allMatch(end, Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(player, data))
&& !route.getEnd().getUnits().isEmpty()) {
return result.setErrorReturnResult("Units cannot participate in multiple battles");
}
}
// See if we are doing invasions in combat phase, with units or transports that can't do invasion.
if (route.isUnload() && Matches.isTerritoryEnemy(player, data).match(route.getEnd())) {
for (final Unit unit : Match.getMatches(units, Matches.UnitCanInvade.invert())) {
result.addDisallowedUnit(unit.getUnitType().getName() + " can't invade from "
+ TripleAUnit.get(unit).getTransportedBy().getUnitType().getName(), unit);
}
}
return result;
}
private static MoveValidationResult validateNonCombat(final GameData data, final Collection<Unit> units,
final Route route, final PlayerID player, final MoveValidationResult result) {
if (getEditMode(data)) {
return result;
}
if (route.someMatch(Matches.TerritoryIsImpassable)) {
return result.setErrorReturnResult(CANT_MOVE_THROUGH_IMPASSABLE);
}
if (!route.someMatch(Matches.TerritoryIsPassableAndNotRestricted(player, data))) {
return result.setErrorReturnResult(CANT_MOVE_THROUGH_RESTRICTED);
}
final CompositeMatch<Territory> neutralOrEnemy =
new CompositeMatchOr<>(Matches.TerritoryIsNeutralButNotWater,
Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(player, data));
// final CompositeMatch<Unit> transportsCanNotControl = new
// CompositeMatchAnd<Unit>(Matches.UnitIsTransportAndNotDestroyer,
// Matches.UnitIsTransportButNotCombatTransport);
final boolean navalMayNotNonComIntoControlled =
isWW2V2(data) || games.strategy.triplea.Properties.getNavalUnitsMayNotNonCombatMoveIntoControlledSeaZones(data);
// TODO need to account for subs AND transports that are ignored, not just OR
final Territory end = route.getEnd();
if (neutralOrEnemy.match(end)) {
// a convoy zone is controlled, so we must make sure we can still move there if there are actual battle there
if (!end.isWater() || navalMayNotNonComIntoControlled) {
return result.setErrorReturnResult("Cannot advance units to battle in non combat");
}
}
// Subs can't travel under DDs
if (isSubmersibleSubsAllowed(data) && Match.allMatch(units, Matches.UnitIsSub)) {
// this is ok unless there are destroyer on the path
if (MoveValidator.enemyDestroyerOnPath(route, player, data)) {
return result.setErrorReturnResult("Cannot move submarines under destroyers");
}
}
if (end.getUnits().someMatch(Matches.enemyUnit(player, data))) {
if (!onlyIgnoredUnitsOnPath(route, player, data, false)) {
final CompositeMatch<Unit> friendlyOrSubmerged = new CompositeMatchOr<>();
friendlyOrSubmerged.add(Matches.enemyUnit(player, data).invert());
friendlyOrSubmerged.add(Matches.UnitIsSubmerged);
if (!end.getUnits().allMatch(friendlyOrSubmerged)
&& !(Match.allMatch(units, Matches.UnitIsAir) && end.isWater())) {
if (!Match.allMatch(units, Matches.UnitIsSub)
|| !games.strategy.triplea.Properties.getSubsCanEndNonCombatMoveWithEnemies(data)) {
return result.setErrorReturnResult("Cannot advance to battle in non combat");
}
}
}
}
// if there are enemy units on the path blocking us, that is validated elsewhere (validateNonEnemyUnitsOnPath)
// now check if we can move over neutral or enemies territories in noncombat
if (Match.allMatch(units, Matches.UnitIsAir)
|| (Match.noneMatch(units, Matches.UnitIsSea) && !nonParatroopersPresent(player, units))) {
// if there are non-paratroopers present, then we cannot fly over stuff
// if there are neutral territories in the middle, we cannot fly over (unless allowed to)
// otherwise we can generally fly over anything in noncombat
if (route.someMatch(new CompositeMatchAnd<>(Matches.TerritoryIsNeutralButNotWater,
Matches.TerritoryIsWater.invert()))
&& (!games.strategy.triplea.Properties.getNeutralFlyoverAllowed(data) || isNeutralsImpassable(data))) {
return result.setErrorReturnResult("Air units cannot fly over neutral territories in non combat");
}
// if sea units, or land units moving over/onto sea (ex: loading onto a transport), then only check if old rules
// stop us
} else if (Match.someMatch(units, Matches.UnitIsSea) || route.someMatch(Matches.TerritoryIsWater)) {
// if there are neutral or owned territories, we cannot move through them (only under old rules. under new rules
// we can move through
// owned sea zones.)
if (navalMayNotNonComIntoControlled && route.someMatch(neutralOrEnemy)) {
return result.setErrorReturnResult("Cannot move units through neutral or enemy territories in non combat");
}
} else {
if (route.someMatch(neutralOrEnemy)) {
return result.setErrorReturnResult("Cannot move units through neutral or enemy territories in non combat");
}
}
return result;
}
// Added to handle restriction of movement to listed territories
static MoveValidationResult validateMovementRestrictedByTerritory(final GameData data,
final Route route, final PlayerID player, final MoveValidationResult result) {
if (getEditMode(data)) {
return result;
}
if (!isMovementByTerritoryRestricted(data)) {
return result;
}
final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME);
if (ra == null || ra.getMovementRestrictionTerritories() == null) {
return result;
}
final String movementRestrictionType = ra.getMovementRestrictionType();
final Collection<Territory> listedTerritories =
ra.getListedTerritories(ra.getMovementRestrictionTerritories(), true, true);
if (movementRestrictionType.equals("allowed")) {
for (final Territory current : route.getAllTerritories()) {
if (!listedTerritories.contains(current)) {
return result.setErrorReturnResult("Cannot move outside restricted territories");
}
}
} else if (movementRestrictionType.equals("disallowed")) {
for (final Territory current : route.getAllTerritories()) {
if (listedTerritories.contains(current)) {
return result.setErrorReturnResult("Cannot move to restricted territories");
}
}
}
return result;
}
private static MoveValidationResult validateNonEnemyUnitsOnPath(final GameData data, final Collection<Unit> units,
final Route route, final PlayerID player, final MoveValidationResult result) {
if (getEditMode(data)) {
return result;
}
// check to see no enemy units on path
if (MoveValidator.noEnemyUnitsOnPathMiddleSteps(route, player, data)) {
return result;
}
// if we are all air, then its ok
if (Match.allMatch(units, Matches.UnitIsAir)) {
return result;
}
// subs may possibly carry units...
if (isSubmersibleSubsAllowed(data)
&& Match.allMatch(Match.getMatches(units, Matches.unitIsBeingTransported().invert()), Matches.UnitIsSub)) {
// this is ok unless there are destroyer on the path
if (MoveValidator.enemyDestroyerOnPath(route, player, data)) {
return result.setErrorReturnResult("Cannot move submarines under destroyers");
} else {
return result;
}
}
if (onlyIgnoredUnitsOnPath(route, player, data, true)) {
return result;
}
// omit paratroops
if (nonParatroopersPresent(player, units)) {
return result.setErrorReturnResult("Enemy units on path");
}
return result;
}
private static MoveValidationResult validateBasic(final GameData data,
final Collection<Unit> units, final Route route, final PlayerID player, final Collection<Unit> transportsToLoad,
final Map<Unit, Collection<Unit>> newDependents, final MoveValidationResult result) {
final boolean isEditMode = getEditMode(data);
// make sure transports in the destination
if (route.getEnd() != null && !route.getEnd().getUnits().containsAll(transportsToLoad)
&& !units.containsAll(transportsToLoad)) {
return result.setErrorReturnResult("Transports not found in route end");
}
if (!isEditMode) {
// make sure all units are at least friendly
for (final Unit unit : Match.getMatches(units, Matches.enemyUnit(player, data))) {
result.addDisallowedUnit("Can only move friendly units", unit);
}
// check we have enough movement
// exclude transported units
Collection<Unit> moveTest;
if (route.getStart().isWater()) {
moveTest = MoveValidator.getNonLand(units);
} else {
moveTest = units;
}
for (final Unit unit : Match.getMatches(moveTest, Matches.unitIsOwnedBy(player).invert())) {
// allow allied fighters to move with carriers
if (!(UnitAttachment.get(unit.getType()).getCarrierCost() > 0 && data.getRelationshipTracker().isAllied(player,
unit.getOwner()))) {
result.addDisallowedUnit("Can only move own troops", unit);
}
}
// Initialize available Mechanized Inf support
int mechanizedSupportAvailable = getMechanizedSupportAvail(units, player);
final Map<Unit, Collection<Unit>> dependencies =
getDependents(Match.getMatches(units, Matches.UnitCanTransport));
// add those just added
// TODO: do not EVER user something from the UI in a validation method. Only the local computer (ie: client) has a
// copy of this UI
// data. The server has a different copy!!!!
// TODO: re-write the entire fucking Paratroopers code. It is garbage! We need a single all encompassing UI and
// engine for all the
// different types of transportation that exist.
if (!newDependents.isEmpty()) {
for (final Unit transport : dependencies.keySet()) {
if (dependencies.get(transport).isEmpty()) {
dependencies.put(transport, newDependents.get(transport));
}
}
}
// check units individually
final Match<Unit> hasEnoughMovementForRoute;
try {
data.acquireReadLock();
hasEnoughMovementForRoute = Matches.UnitHasEnoughMovementForRoute(route);
} finally {
data.releaseReadLock();
}
for (final Unit unit : moveTest) {
if (!hasEnoughMovementForRoute.match(unit)) {
boolean unitOK = false;
if ((Matches.UnitIsAirTransportable.match(unit) && Matches.unitHasNotMoved.match(unit))
&& (mechanizedSupportAvailable > 0 && Matches.unitHasNotMoved.match(unit) && Matches.UnitIsInfantry
.match(unit))) {
// we have paratroopers and mechanized infantry, so we must check for both
// simple: if it movement group contains an air-transport, then assume we are doing paratroopers. else,
// assume we are doing
// mechanized
if (Match.someMatch(units, Matches.UnitIsAirTransport)) {
for (final Unit airTransport : dependencies.keySet()) {
if (dependencies.get(airTransport) == null || dependencies.get(airTransport).contains(unit)) {
unitOK = true;
break;
}
}
if (!unitOK) {
result.addDisallowedUnit("Not all units have enough movement", unit);
}
} else {
mechanizedSupportAvailable--;
}
} else if (Matches.UnitIsAirTransportable.match(unit) && Matches.unitHasNotMoved.match(unit)) {
for (final Unit airTransport : dependencies.keySet()) {
if (dependencies.get(airTransport) == null || dependencies.get(airTransport).contains(unit)) {
unitOK = true;
break;
}
}
if (!unitOK) {
result.addDisallowedUnit("Not all units have enough movement", unit);
}
} else if (mechanizedSupportAvailable > 0 && Matches.unitHasNotMoved.match(unit)
&& Matches.UnitIsInfantry.match(unit)) {
mechanizedSupportAvailable--;
} else if (Matches.unitIsOwnedBy(player).invert().match(unit) && Matches.alliedUnit(player, data).match(unit)
&& Matches.UnitTypeCanLandOnCarrier.match(unit.getType())
&& Match.someMatch(moveTest, Matches.UnitIsAlliedCarrier(unit.getOwner(), data))) {
// this is so that if the unit is owned by any ally and it is cargo, then it will not count.
// (shouldn't it be a dependant in this case??)
} else {
result.addDisallowedUnit("Not all units have enough movement", unit);
}
}
}
// if there is a neutral in the middle must stop unless all are air or getNeutralsBlitzable
if (route.hasNeutralBeforeEnd()) {
if (!Match.allMatch(units, Matches.UnitIsAir) && !isNeutralsBlitzable(data)) {
return result.setErrorReturnResult("Must stop land units when passing through neutral territories");
}
}
// a territory effect can disallow unit types in
if (Match.someMatch(units,
Matches.unitIsOfTypes(TerritoryEffectHelper.getUnitTypesForUnitsNotAllowedIntoTerritory(route.getSteps())))) {
return result.setErrorReturnResult("Territory Effects disallow some units into "
+ (route.numberOfSteps() > 1 ? "these territories" : "this territory"));
}
} // !isEditMode
// make sure that no non sea non transportable no carriable units end at sea
if (route.getEnd() != null && route.getEnd().isWater()) {
for (final Unit unit : MoveValidator.getUnitsThatCantGoOnWater(units)) {
result.addDisallowedUnit("Not all units can end at water", unit);
}
}
// if we are water make sure no land
if (Match.someMatch(units, Matches.UnitIsSea)) {
if (route.hasLand()) {
for (final Unit unit : Match.getMatches(units, Matches.UnitIsSea)) {
result.addDisallowedUnit("Sea units cannot go on land", unit);
}
}
}
// test for stack limits per unit
if (route.getEnd() != null) {
final Collection<Unit> unitsWithStackingLimits =
Match.getMatches(units, new CompositeMatchOr<>(Matches.UnitHasMovementLimit,
Matches.UnitHasAttackingLimit));
for (final Territory t : route.getSteps()) {
final Collection<Unit> unitsAllowedSoFar = new ArrayList<>();
if (Matches.isTerritoryEnemyAndNotUnownedWater(player, data).match(t)
|| t.getUnits().someMatch(Matches.unitIsEnemyOf(data, player))) {
for (final Unit unit : unitsWithStackingLimits) {
final UnitType ut = unit.getType();
int maxAllowed =
UnitAttachment
.getMaximumNumberOfThisUnitTypeToReachStackingLimit("attackingLimit", ut, t, player, data);
maxAllowed -= Match.countMatches(unitsAllowedSoFar, Matches.unitIsOfType(ut));
if (maxAllowed > 0) {
unitsAllowedSoFar.add(unit);
} else {
result.addDisallowedUnit("UnitType " + ut.getName() + " has reached stacking limit", unit);
}
}
if (!PlayerAttachment.getCanTheseUnitsMoveWithoutViolatingStackingLimit("attackingLimit", units, t, player,
data)) {
return result.setErrorReturnResult("Units Cannot Go Over Stacking Limit");
}
} else {
for (final Unit unit : unitsWithStackingLimits) {
final UnitType ut = unit.getType();
int maxAllowed =
UnitAttachment.getMaximumNumberOfThisUnitTypeToReachStackingLimit("movementLimit", ut, t, player, data);
maxAllowed -= Match.countMatches(unitsAllowedSoFar, Matches.unitIsOfType(ut));
if (maxAllowed > 0) {
unitsAllowedSoFar.add(unit);
} else {
result.addDisallowedUnit("UnitType " + ut.getName() + " has reached stacking limit", unit);
}
}
if (!PlayerAttachment.getCanTheseUnitsMoveWithoutViolatingStackingLimit("movementLimit", units, t, player,
data)) {
return result.setErrorReturnResult("Units Cannot Go Over Stacking Limit");
}
}
}
}
// don't allow move through impassable territories
if (!isEditMode && route.someMatch(Matches.TerritoryIsImpassable)) {
return result.setErrorReturnResult(CANT_MOVE_THROUGH_IMPASSABLE);
}
if (canCrossNeutralTerritory(data, route, player, result).getError() != null) {
return result;
}
if (isNeutralsImpassable(data) && !isNeutralsBlitzable(data)
&& !route.getMatches(Matches.TerritoryIsNeutralButNotWater).isEmpty()) {
return result.setErrorReturnResult(CANNOT_VIOLATE_NEUTRALITY);
}
return result;
}
private static int getMechanizedSupportAvail(final Collection<Unit> units, final PlayerID player) {
int mechanizedSupportAvailable = 0;
if (TechAttachment.isMechanizedInfantry(player)) {
final CompositeMatch<Unit> transportLand =
new CompositeMatchAnd<>(Matches.UnitIsLandTransport, Matches.unitIsOwnedBy(player));
mechanizedSupportAvailable = Match.countMatches(units, transportLand);
}
return mechanizedSupportAvailable;
}
static Map<Unit, Collection<Unit>> getDependents(final Collection<Unit> units) {
// just worry about transports
final Map<Unit, Collection<Unit>> dependents = new HashMap<>();
for (Unit unit : units) {
dependents.put(unit, TransportTracker.transporting(unit));
}
return dependents;
}
/**
* Checks that there are no enemy units on the route except possibly at the end.
* Submerged enemy units are not considered as they don't affect
* movement.
* AA and factory dont count as enemy.
*/
static boolean noEnemyUnitsOnPathMiddleSteps(final Route route, final PlayerID player, final GameData data) {
final CompositeMatch<Unit> alliedOrNonCombat =
new CompositeMatchOr<>(Matches.UnitIsInfrastructure, Matches.enemyUnit(player, data).invert(),
Matches.UnitIsSubmerged);
// Submerged units do not interfere with movement
for (final Territory current : route.getMiddleSteps()) {
if (!current.getUnits().allMatch(alliedOrNonCombat)) {
return false;
}
}
return true;
}
/**
* Checks that there only transports, subs and/or allies on the route except at the end.
* AA and factory dont count as enemy.
*/
static boolean onlyIgnoredUnitsOnPath(final Route route, final PlayerID player, final GameData data,
final boolean ignoreRouteEnd) {
final CompositeMatch<Unit> subOnly =
new CompositeMatchOr<>(Matches.UnitIsInfrastructure, Matches.UnitIsSub, Matches.enemyUnit(player, data)
.invert());
final CompositeMatch<Unit> transportOnly =
new CompositeMatchOr<>(Matches.UnitIsInfrastructure, Matches.UnitIsTransportButNotCombatTransport,
Matches.UnitIsLand, Matches.enemyUnit(player, data).invert());
final CompositeMatch<Unit> transportOrSubOnly =
new CompositeMatchOr<>(Matches.UnitIsInfrastructure, Matches.UnitIsTransportButNotCombatTransport,
Matches.UnitIsLand, Matches.UnitIsSub, Matches.enemyUnit(player, data).invert());
final boolean getIgnoreTransportInMovement = isIgnoreTransportInMovement(data);
final boolean getIgnoreSubInMovement = isIgnoreSubInMovement(data);
boolean validMove = false;
List<Territory> steps;
if (ignoreRouteEnd) {
steps = route.getMiddleSteps();
} else {
steps = route.getSteps();
}
// if there are no steps, then we began in this sea zone, so see if there are ignored units in this sea zone (not
// sure if we need
// !ignoreRouteEnd here).
if (steps.isEmpty() && route.numberOfStepsIncludingStart() == 1 && !ignoreRouteEnd) {
steps.add(route.getStart());
}
for (final Territory current : steps) {
if (current.isWater()) {
if (getIgnoreTransportInMovement && getIgnoreSubInMovement && current.getUnits().allMatch(transportOrSubOnly)) {
validMove = true;
continue;
}
if (getIgnoreTransportInMovement && !getIgnoreSubInMovement && current.getUnits().allMatch(transportOnly)) {
validMove = true;
continue;
}
if (!getIgnoreTransportInMovement && getIgnoreSubInMovement && current.getUnits().allMatch(subOnly)) {
validMove = true;
continue;
}
return false;
}
}
return validMove;
}
private static boolean enemyDestroyerOnPath(final Route route, final PlayerID player, final GameData data) {
final Match<Unit> enemyDestroyer =
new CompositeMatchAnd<>(Matches.UnitIsDestroyer, Matches.enemyUnit(player, data));
for (final Territory current : route.getMiddleSteps()) {
if (current.getUnits().someMatch(enemyDestroyer)) {
return true;
}
}
return false;
}
private static boolean getEditMode(final GameData data) {
return BaseEditDelegate.getEditMode(data);
}
private static boolean hasConqueredNonBlitzedNonWaterOnRoute(final Route route, final GameData data) {
for (final Territory current : route.getMiddleSteps()) {
if (!Matches.TerritoryIsWater.match(current) && AbstractMoveDelegate.getBattleTracker(data).wasConquered(current)
&& !AbstractMoveDelegate.getBattleTracker(data).wasBlitzed(current)) {
return true;
}
}
return false;
}
// TODO KEV revise these to include paratroop load/unload
public static boolean isLoad(final Collection<Unit> units, final Map<Unit, Collection<Unit>> newDependents,
final Route route, final GameData data, final PlayerID player) {
final Map<Unit, Collection<Unit>> alreadyLoaded =
mustMoveWith(units, newDependents, route.getStart(), data, player);
if (route.hasNoSteps() && alreadyLoaded.isEmpty()) {
return false;
}
// See if we even need to go to the trouble of checking for AirTransported units
final boolean checkForAlreadyTransported = !route.getStart().isWater() && route.hasWater();
if (checkForAlreadyTransported) {
// TODO Leaving UnitIsTransport for potential use with amphib transports (hovercraft, ducks, etc...)
final List<Unit> transports =
Match.getMatches(units, new CompositeMatchOr<>(Matches.UnitIsTransport, Matches.UnitIsAirTransport));
final List<Unit> transportable =
Match.getMatches(units, new CompositeMatchOr<>(Matches.UnitCanBeTransported,
Matches.UnitIsAirTransportable));
// Check if there are transports in the group to be checked
if (alreadyLoaded.keySet().containsAll(transports)) {
// Check each transportable unit -vs those already loaded.
for (final Unit unit : transportable) {
boolean found = false;
for (final Unit transport : transports) {
if (alreadyLoaded.get(transport) == null || alreadyLoaded.get(transport).contains(unit)) {
found = true;
break;
}
}
if (!found) {
return true;
}
}
} else {
// TODO I think this is right
return true;
}
}
return false;
}
private static Collection<Unit> getUnitsThatCantGoOnWater(final Collection<Unit> units) {
final Collection<Unit> retUnits = new ArrayList<>();
for (final Unit unit : units) {
final UnitAttachment ua = UnitAttachment.get(unit.getType());
if (!ua.getIsSea() && !ua.getIsAir() && ua.getTransportCost() == -1) {
retUnits.add(unit);
}
}
return retUnits;
}
static boolean hasUnitsThatCantGoOnWater(final Collection<Unit> units) {
return !getUnitsThatCantGoOnWater(units).isEmpty();
}
private static Collection<Unit> getNonLand(final Collection<Unit> units) {
final CompositeMatch<Unit> match = new CompositeMatchOr<>();
match.add(Matches.UnitIsAir);
match.add(Matches.UnitIsSea);
return Match.getMatches(units, match);
}
public static int getMaxMovement(final Collection<Unit> units) {
if (units.size() == 0) {
throw new IllegalArgumentException("no units");
}
int max = 0;
for (Unit unit : units) {
final int left = TripleAUnit.get(unit).getMovementLeft();
max = Math.max(left, max);
}
return max;
}
static int getLeastMovement(final Collection<Unit> units) {
if (units.size() == 0) {
throw new IllegalArgumentException("no units");
}
int least = Integer.MAX_VALUE;
for (Unit unit : units) {
final int left = TripleAUnit.get(unit).getMovementLeft();
least = Math.min(left, least);
}
return least;
}
// Determines whether we can pay the neutral territory charge for a
// given route for air units. We can't cross neutral territories
// in WW2V2.
private static MoveValidationResult canCrossNeutralTerritory(final GameData data, final Route route,
final PlayerID player, final MoveValidationResult result) {
// neutrals we will overfly in the first place
final Collection<Territory> neutrals = MoveDelegate.getEmptyNeutral(route);
final int PUs = (player == null || player.isNull()) ? 0 : player.getResources().getQuantity(Constants.PUS);
if (PUs < getNeutralCharge(data, neutrals.size())) {
return result.setErrorReturnResult(TOO_POOR_TO_VIOLATE_NEUTRALITY);
}
return result;
}
private static Territory getTerritoryTransportHasUnloadedTo(final List<UndoableMove> undoableMoves,
final Unit transport) {
for (final UndoableMove undoableMove : undoableMoves) {
if (undoableMove.wasTransportUnloaded(transport)) {
return undoableMove.getRoute().getEnd();
}
}
return null;
}
private static MoveValidationResult validateTransport(final boolean isNonCombat, final GameData data,
final List<UndoableMove> undoableMoves, final Collection<Unit> units, final Route route, final PlayerID player,
final Collection<Unit> transportsToLoad, final MoveValidationResult result) {
final boolean isEditMode = getEditMode(data);
if (Match.allMatch(units, Matches.UnitIsAir)) {
return result;
}
if (!route.hasWater()) {
return result;
}
// If there are non-sea transports return
final boolean seaOrNoTransportsPresent =
transportsToLoad.isEmpty()
|| Match.someMatch(transportsToLoad, new CompositeMatchAnd<>(Matches.UnitIsSea,
Matches.UnitCanTransport));
if (!seaOrNoTransportsPresent) {
return result;
}
/*
* if(!MoveValidator.isLoad(units, route, data, player) && !MoveValidator.isUnload(route))
* return result;
*/
final Territory routeEnd = route.getEnd();
final Territory routeStart = route.getStart();
// if unloading make sure length of route is only 1
if (!isEditMode && route.isUnload()) {
if (route.hasMoreThenOneStep()) {
return result.setErrorReturnResult("Unloading units must stop where they are unloaded");
}
for (final Unit unit : TransportTracker.getUnitsLoadedOnAlliedTransportsThisTurn(units)) {
result.addDisallowedUnit(CANNOT_LOAD_AND_UNLOAD_AN_ALLIED_TRANSPORT_IN_THE_SAME_ROUND, unit);
}
final Collection<Unit> transports = TransportUtils.mapTransports(route, units, null).values();
final boolean isScramblingOrKamikazeAttacksEnabled =
games.strategy.triplea.Properties.getScramble_Rules_In_Effect(data)
|| games.strategy.triplea.Properties.getUseKamikazeSuicideAttacks(data);
final boolean submarinesPreventUnescortedAmphibAssaults =
games.strategy.triplea.Properties.getSubmarinesPreventUnescortedAmphibiousAssaults(data);
final Match<Unit> enemySubmarineMatch =
new CompositeMatchAnd<>(Matches.unitIsEnemyOf(data, player), Matches.UnitIsSub);
final Match<Unit> ownedSeaNonTransportMatch =
new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player), Matches.UnitIsSea,
Matches.UnitIsNotTransportButCouldBeCombatTransport);
for (final Unit transport : transports) {
if (!isNonCombat && route.numberOfStepsIncludingStart() == 2) {
if (Matches.territoryHasEnemyUnits(player, data).match(routeEnd)
|| Matches.isTerritoryEnemyAndNotUnownedWater(player, data).match(routeEnd)) {
// this is an amphibious assault
if (submarinesPreventUnescortedAmphibAssaults
&& !Matches.territoryHasUnitsThatMatch(ownedSeaNonTransportMatch).match(routeStart)
&& Matches.territoryHasUnitsThatMatch(enemySubmarineMatch).match(routeStart)) {
// we must have at least one warship (non-transport) unit, otherwise the enemy sub stops our unloading for
// amphibious assault
for (final Unit unit : TransportTracker.transporting(transport)) {
result.addDisallowedUnit(ENEMY_SUBMARINE_PREVENTING_UNESCORTED_AMPHIBIOUS_ASSAULT_LANDING, unit);
}
}
} else if (!AbstractMoveDelegate.getBattleTracker(data).wasConquered(routeEnd)) {
// this is an unload to a friendly territory
if (isScramblingOrKamikazeAttacksEnabled
|| !Matches.territoryIsEmptyOfCombatUnits(data, player).match(routeStart)) {
// Unloading a transport from a sea zone with a battle, to a friendly land territory, during combat move
// phase, is illegal
// and in addition to being illegal, it is also causing problems if the sea transports get killed (the
// land units are not
// dying)
// TODO: should we use the battle tracker for this instead?
for (final Unit unit : TransportTracker.transporting(transport)) {
result.addDisallowedUnit(
TRANSPORT_MAY_NOT_UNLOAD_TO_FRIENDLY_TERRITORIES_UNTIL_AFTER_COMBAT_IS_RESOLVED, unit);
}
}
}
}
// TODO This is very sensitive to the order of the transport collection. The users may
// need to modify the order in which they perform their actions.
// check whether transport has already unloaded
if (TransportTracker.hasTransportUnloadedInPreviousPhase(transport)) {
for (final Unit unit : TransportTracker.transporting(transport)) {
result.addDisallowedUnit(TRANSPORT_HAS_ALREADY_UNLOADED_UNITS_IN_A_PREVIOUS_PHASE, unit);
}
// check whether transport is restricted to another territory
} else if (TransportTracker.isTransportUnloadRestrictedToAnotherTerritory(transport, route.getEnd())) {
final Territory alreadyUnloadedTo = getTerritoryTransportHasUnloadedTo(undoableMoves, transport);
for (final Unit unit : TransportTracker.transporting(transport)) {
result.addDisallowedUnit(TRANSPORT_HAS_ALREADY_UNLOADED_UNITS_TO + alreadyUnloadedTo.getName(), unit);
}
// Check if the transport has already loaded after being in combat
} else if (TransportTracker.isTransportUnloadRestrictedInNonCombat(transport)) {
for (final Unit unit : TransportTracker.transporting(transport)) {
result.addDisallowedUnit(TRANSPORT_CANNOT_LOAD_AND_UNLOAD_AFTER_COMBAT, unit);
}
}
}
}
// if we are land make sure no water in route except for transport
// situations
final Collection<Unit> land = Match.getMatches(units, Matches.UnitIsLand);
final Collection<Unit> landAndAir =
Match.getMatches(units, new CompositeMatchOr<>(Matches.UnitIsLand, Matches.UnitIsAir));
// make sure we can be transported
final Match<Unit> cantBeTransported = new InverseMatch<>(Matches.UnitCanBeTransported);
for (final Unit unit : Match.getMatches(land, cantBeTransported)) {
result.addDisallowedUnit("Not all units can be transported", unit);
}
// make sure that the only the first or last territory is land
// dont want situation where they go sea land sea
if (!isEditMode && route.hasLand() && !(route.getStart().isWater() || route.getEnd().isWater())) {
// needs to include all land and air to work, since it makes sure the land units can be carried by the air and
// that the air has enough
// capacity
if (nonParatroopersPresent(player, landAndAir)) {
return result.setErrorReturnResult("Invalid move, only start or end can be land when route has water.");
}
}
// simply because I dont want to handle it yet
// checks are done at the start and end, dont want to worry about just
// using a transport as a bridge yet
// TODO handle this
if (!isEditMode && !route.getEnd().isWater() && !route.getStart().isWater()
&& nonParatroopersPresent(player, landAndAir)) {
return result.setErrorReturnResult("Must stop units at a transport on route");
}
if (route.getEnd().isWater() && route.getStart().isWater()) {
// make sure units and transports stick together
for (Unit unit : units) {
final UnitAttachment ua = UnitAttachment.get(unit.getType());
// make sure transports dont leave their units behind
if (ua.getTransportCapacity() != -1) {
final Collection<Unit> holding = TransportTracker.transporting(unit);
if (!units.containsAll(holding)) {
result.addDisallowedUnit("Transports cannot leave their units", unit);
}
}
// make sure units dont leave their transports behind
if (ua.getTransportCost() != -1) {
final Unit transport = TransportTracker.transportedBy(unit);
if (transport != null && !units.contains(transport)) {
result.addDisallowedUnit("Unit must stay with its transport while moving", unit);
}
}
}
} // end if end is water
if (route.isLoad()) {
if (!isEditMode && !route.hasExactlyOneStep() && nonParatroopersPresent(player, landAndAir)) {
return result.setErrorReturnResult("Units cannot move before loading onto transports");
}
final CompositeMatch<Unit> enemyNonSubmerged =
new CompositeMatchAnd<>(Matches.enemyUnit(player, data), Matches.UnitIsSubmerged.invert());
if (route.getEnd().getUnits().someMatch(enemyNonSubmerged) && nonParatroopersPresent(player, landAndAir)) {
if (!onlyIgnoredUnitsOnPath(route, player, data, false)) {
if (!AbstractMoveDelegate.getBattleTracker(data).didAllThesePlayersJustGoToWarThisTurn(player,
route.getEnd().getUnits().getUnits(), data)) {
return result.setErrorReturnResult("Cannot load when enemy sea units are present");
}
}
}
final Map<Unit, Unit> unitsToTransports = TransportUtils.mapTransports(route, land, transportsToLoad);
final Iterator<Unit> iter = land.iterator();
// CompositeMatch<Unit> landUnitsAtSea = new CompositeMatchOr<Unit>(Matches.unitIsLandAndOwnedBy(player),
// Matches.UnitCanBeTransported);
while (!isEditMode && iter.hasNext()) {
final TripleAUnit unit = (TripleAUnit) iter.next();
if (Matches.unitHasMoved.match(unit)) {
result.addDisallowedUnit("Units cannot move before loading onto transports", unit);
}
final Unit transport = unitsToTransports.get(unit);
if (transport == null) {
continue;
}
if (TransportTracker.hasTransportUnloadedInPreviousPhase(transport)) {
result.addDisallowedUnit(TRANSPORT_HAS_ALREADY_UNLOADED_UNITS_IN_A_PREVIOUS_PHASE, unit);
} else if (TransportTracker.isTransportUnloadRestrictedToAnotherTerritory(transport, route.getEnd())) {
Territory alreadyUnloadedTo = getTerritoryTransportHasUnloadedTo(undoableMoves, transport);
for (Unit aTransportsToLoad : transportsToLoad) {
final TripleAUnit trn = (TripleAUnit) aTransportsToLoad;
if (!TransportTracker.isTransportUnloadRestrictedToAnotherTerritory(trn, route.getEnd())) {
final UnitAttachment ua = UnitAttachment.get(unit.getType());
// UnitAttachment trna = UnitAttachment.get(trn.getType());
if (TransportTracker.getAvailableCapacity(trn) >= ua.getTransportCost()) {
alreadyUnloadedTo = null;
break;
}
}
}
if (alreadyUnloadedTo != null) {
result.addDisallowedUnit(TRANSPORT_HAS_ALREADY_UNLOADED_UNITS_TO + alreadyUnloadedTo.getName(), unit);
}
}
}
if (!unitsToTransports.keySet().containsAll(land)) {
// some units didn't get mapped to a transport
final Collection<UnitCategory> unitsToLoadCategories = UnitSeperator.categorize(land);
if (unitsToTransports.size() == 0 || unitsToLoadCategories.size() == 1) {
// set all unmapped units as disallowed if there are no transports
// or only one unit category
for (final Unit unit : land) {
if (unitsToTransports.containsKey(unit)) {
continue;
}
final UnitAttachment ua = UnitAttachment.get(unit.getType());
if (ua.getTransportCost() != -1) {
result.addDisallowedUnit("Not enough transports", unit);
// System.out.println("adding disallowed unit (Not enough transports): "+unit);
}
}
} else {
// set all units as unresolved if there is at least one transport
// and mixed unit categories
for (final Unit unit : land) {
final UnitAttachment ua = UnitAttachment.get(unit.getType());
if (ua.getTransportCost() != -1) {
result.addUnresolvedUnit("Not enough transports", unit);
// System.out.println("adding unresolved unit (Not enough transports): "+unit);
}
}
}
}
}
return result;
}
static boolean allLandUnitsAreBeingParatroopered(final Collection<Unit> units) {
// some units that can't be paratrooped
if (!Match.allMatch(units, new CompositeMatchOr<>(Matches.UnitIsAirTransportable, Matches.UnitIsAirTransport,
Matches.UnitIsAir))) {
return false;
}
// final List<Unit> paratroopsRequiringTransport = getParatroopsRequiringTransport(units, route);
// due to various problems with units like tanks, we will assume that if we are in this method, then all the land
// units need transports
final List<Unit> paratroopsRequiringTransport = Match.getMatches(units, Matches.UnitIsAirTransportable);
if (paratroopsRequiringTransport.isEmpty()) {
return false;
}
final List<Unit> airTransports = Match.getMatches(units, Matches.UnitIsAirTransport);
final List<Unit> allParatroops =
TransportUtils.findUnitsToLoadOnAirTransports(paratroopsRequiringTransport, airTransports);
if (!allParatroops.containsAll(paratroopsRequiringTransport)) {
return false;
}
final Map<Unit, Unit> transportLoadMap = TransportUtils.mapTransportsToLoad(units, airTransports);
return transportLoadMap.keySet().containsAll(paratroopsRequiringTransport);
}
// checks if there are non-paratroopers present that cause move validations to fail
private static boolean nonParatroopersPresent(final PlayerID player, final Collection<Unit> units) {
if (!TechAttachment.isAirTransportable(player)) {
return true;
}
if (!Match.allMatch(units, new CompositeMatchOr<>(Matches.UnitIsAir, Matches.UnitIsLand))) {
return true;
}
for (final Unit unit : Match.getMatches(units, Matches.UnitIsNotAirTransportable)) {
if (Matches.UnitIsLand.match(unit)) {
return true;
}
}
return !allLandUnitsAreBeingParatroopered(units);
}
private static List<Unit> getParatroopsRequiringTransport(final Collection<Unit> units, final Route route) {
return Match.getMatches(units, new CompositeMatchAnd<>(Matches.UnitIsAirTransportable, new Match<Unit>() {
@Override
public boolean match(final Unit u) {
return TripleAUnit.get(u).getMovementLeft() < route.getMovementCost(u) || route.crossesWater()
|| route.getEnd().isWater();
}
}));
}
private static MoveValidationResult validateParatroops(final boolean nonCombat, final GameData data,
final Collection<Unit> units, final Route route, final PlayerID player, final MoveValidationResult result) {
if (!TechAttachment.isAirTransportable(player)) {
return result;
}
if (Match.noneMatch(units, Matches.UnitIsAirTransportable) || Match.noneMatch(units, Matches.UnitIsAirTransport)) {
return result;
}
if (nonCombat && !isAirTransportableCanMoveDuringNonCombat(data)) {
return result.setErrorReturnResult("Paratroops may not move during NonCombat");
}
if (!getEditMode(data)) {
// if we can move without using paratroop tech, do so
// this allows moving a bomber/infantry from one friendly
// territory to another
final List<Unit> paratroopsRequiringTransport = getParatroopsRequiringTransport(units, route);
if (paratroopsRequiringTransport.isEmpty()) {
return result;
}
final List<Unit> airTransports = Match.getMatches(units, Matches.UnitIsAirTransport);
// TODO kev change below to mapAirTransports (or modify mapTransports to handle air cargo)
// Map<Unit, Unit> airTransportsAndParatroops = MoveDelegate.mapTransports(route, paratroopsRequiringTransport,
// airTransports);
final Map<Unit, Unit> airTransportsAndParatroops =
TransportUtils.mapTransportsToLoad(paratroopsRequiringTransport, airTransports);
for (final Unit paratroop : airTransportsAndParatroops.keySet()) {
if (Matches.unitHasMoved.match(paratroop)) {
result.addDisallowedUnit("Cannot paratroop units that have already moved", paratroop);
}
final Unit transport = airTransportsAndParatroops.get(paratroop);
if (Matches.unitHasMoved.match(transport)) {
result.addDisallowedUnit("Cannot move then transport paratroops", transport);
}
}
final Territory routeEnd = route.getEnd();
for (final Unit paratroop : paratroopsRequiringTransport) {
if (Matches.unitHasMoved.match(paratroop)) {
result.addDisallowedUnit("Cannot paratroop units that have already moved", paratroop);
}
if (Matches.isTerritoryFriendly(player, data).match(routeEnd)
&& !isAirTransportableCanMoveDuringNonCombat(data)) {
result.addDisallowedUnit("Paratroops must advance to battle", paratroop);
}
if (!nonCombat && Matches.isTerritoryFriendly(player, data).match(routeEnd)
&& isAirTransportableCanMoveDuringNonCombat(data)) {
result.addDisallowedUnit("Paratroops may only airlift during Non-Combat Movement Phase", paratroop);
}
}
if (!games.strategy.triplea.Properties.getParatroopersCanAttackDeepIntoEnemyTerritory(data)) {
for (final Territory current : Match.getMatches(route.getMiddleSteps(), Matches.TerritoryIsLand)) {
if (Matches.isTerritoryEnemy(player, data).match(current)) {
return result.setErrorReturnResult("Must stop paratroops in first enemy territory");
}
}
}
}
return result;
}
/**
* Test a route's canals to see if you can move through it.
*
* @param units
* (Can be null. If null we will assume all units would be stopped by the canal.)
*/
public static String validateCanal(final Route route, final Collection<Unit> units, final PlayerID player,
final GameData data) {
for (final Territory routeTerritory : route.getAllTerritories()) {
final Optional<String> result = validateCanal(routeTerritory, route, units, player, data);
if (result.isPresent()) {
return result.get();
}
}
return null;
}
/**
* Used for testing a single territory, either as part of a route, or just by itself. Returns Optional.empty if it
* can be passed through otherwise returns a failure message indicating why the canal can't be passed through.
*
* @param route
* (Can be null. If not null, we will check to see if the route includes both sea zones, and if it doesn't we
* will not test the
* canal)
* @param units
* (Can be null. If null we will assume all units would be stopped by the canal.)
*/
public static Optional<String> validateCanal(final Territory territory, final Route route,
final Collection<Unit> units, final PlayerID player, final GameData data) {
Optional<String> failureMessage = Optional.empty();
final Set<CanalAttachment> canalAttachments = CanalAttachment.get(territory);
for (final CanalAttachment canalAttachment : canalAttachments) {
if (!isCanalOnRoute(canalAttachment, route, data)) {
continue; // Only check canals that are on the route
}
failureMessage = canPassThroughCanal(canalAttachment, units, player, data);
final boolean canPass = !failureMessage.isPresent();
if ((!Properties.getControlAllCanalsBetweenTerritoriesToPass(data) && canPass)
|| (Properties.getControlAllCanalsBetweenTerritoriesToPass(data) && !canPass)) {
break; // If need to control any canal and can pass OR need to control all canals and can't pass
}
}
return failureMessage;
}
/*
* Checks if route is either null or includes both canal territories so needs to be checked.
*/
private static boolean isCanalOnRoute(final CanalAttachment canalAttachment, final Route route, final GameData data) {
if (route == null) {
return true;
}
Territory last = null;
final Set<Territory> connectionToCheck = CanalAttachment.getAllCanalSeaZones(canalAttachment.getCanalName(), data);
for (final Territory current : route.getAllTerritories()) {
if (last != null) {
final Collection<Territory> lastTwo = new ArrayList<>();
lastTwo.add(last);
lastTwo.add(current);
if (lastTwo.containsAll(connectionToCheck)) {
return true;
}
}
last = current;
}
return false;
}
/*
* Checks if units can pass through canal and returns Optional.empty() if true or a failure message if false.
*/
private static Optional<String> canPassThroughCanal(final CanalAttachment canalAttachment,
final Collection<Unit> units, final PlayerID player, final GameData data) {
if (units != null && Match.allMatch(units, Matches.unitIsOfTypes(canalAttachment.getExcludedUnits(data)))) {
return Optional.empty();
}
for (final Territory borderTerritory : canalAttachment.getLandTerritories()) {
if (!data.getRelationshipTracker().canMoveThroughCanals(player, borderTerritory.getOwner())) {
return Optional.of("Must control " + canalAttachment.getCanalName() + " to move through");
}
if (AbstractMoveDelegate.getBattleTracker(data).wasConquered(borderTerritory)) {
return Optional.of("Must control " + canalAttachment.getCanalName()
+ " for an entire turn to move through");
}
}
return Optional.empty();
}
public static MustMoveWithDetails getMustMoveWith(final Territory start, final Collection<Unit> units,
final Map<Unit, Collection<Unit>> newDependents, final GameData data, final PlayerID player) {
return new MustMoveWithDetails(mustMoveWith(units, newDependents, start, data, player));
}
private static Map<Unit, Collection<Unit>> mustMoveWith(final Collection<Unit> units,
final Map<Unit, Collection<Unit>> newDependents, final Territory start, final GameData data,
final PlayerID player) {
final List<Unit> sortedUnits = new ArrayList<>(units);
Collections.sort(sortedUnits, UnitComparator.getHighestToLowestMovementComparator());
final Map<Unit, Collection<Unit>> mapping = new HashMap<>();
mapping.putAll(transportsMustMoveWith(sortedUnits));
// Check if there are combined transports (carriers that are transports) and load them.
if (mapping.isEmpty()) {
mapping.putAll(carrierMustMoveWith(sortedUnits, start, data, player));
} else {
final Map<Unit, Collection<Unit>> newMapping = new HashMap<>();
newMapping.putAll(carrierMustMoveWith(sortedUnits, start, data, player));
if (!newMapping.isEmpty()) {
addToMapping(mapping, newMapping);
}
}
if (mapping.isEmpty()) {
mapping.putAll(airTransportsMustMoveWith(sortedUnits, newDependents));
} else {
final Map<Unit, Collection<Unit>> newMapping = new HashMap<>();
newMapping.putAll(airTransportsMustMoveWith(sortedUnits, newDependents));
if (!newMapping.isEmpty()) {
addToMapping(mapping, newMapping);
}
}
return mapping;
}
private static void addToMapping(final Map<Unit, Collection<Unit>> mapping,
final Map<Unit, Collection<Unit>> newMapping) {
for (final Unit key : newMapping.keySet()) {
if (mapping.containsKey(key)) {
final Collection<Unit> heldUnits = mapping.get(key);
heldUnits.addAll(newMapping.get(key));
mapping.put(key, heldUnits);
} else {
mapping.put(key, newMapping.get(key));
}
}
}
private static Map<Unit, Collection<Unit>> transportsMustMoveWith(final Collection<Unit> units) {
final Map<Unit, Collection<Unit>> mustMoveWith = new HashMap<>();
// map transports
final Collection<Unit> transports = Match.getMatches(units, Matches.UnitIsTransport);
final Iterator<Unit> iter = transports.iterator();
while (iter.hasNext()) {
final Unit transport = iter.next();
final Collection<Unit> transporting = TransportTracker.transporting(transport);
mustMoveWith.put(transport, transporting);
}
return mustMoveWith;
}
private static Map<Unit, Collection<Unit>> airTransportsMustMoveWith(final Collection<Unit> units,
final Map<Unit, Collection<Unit>> newDependents) {
final Map<Unit, Collection<Unit>> mustMoveWith = new HashMap<>();
final Collection<Unit> airTransports = Match.getMatches(units, Matches.UnitIsAirTransport);
// Then check those that have already had their transportedBy set
for (final Unit airTransport : airTransports) {
if (!mustMoveWith.containsKey(airTransport)) {
Collection<Unit> transporting = TransportTracker.transporting(airTransport);
if (transporting == null || transporting.isEmpty()) {
if (!newDependents.isEmpty()) {
transporting = newDependents.get(airTransport);
}
}
mustMoveWith.put(airTransport, transporting);
}
}
return mustMoveWith;
}
public static Map<Unit, Collection<Unit>> carrierMustMoveWith(final Collection<Unit> units, final Territory start,
final GameData data, final PlayerID player) {
return carrierMustMoveWith(units, start.getUnits().getUnits(), data, player);
}
public static Map<Unit, Collection<Unit>> carrierMustMoveWith(final Collection<Unit> units,
final Collection<Unit> startUnits, final GameData data, final PlayerID player) {
// we want to get all air units that are owned by our allies
// but not us that can land on a carrier
final CompositeMatch<Unit> friendlyNotOwnedAir = new CompositeMatchAnd<>();
friendlyNotOwnedAir.add(Matches.alliedUnit(player, data));
friendlyNotOwnedAir.addInverse(Matches.unitIsOwnedBy(player));
friendlyNotOwnedAir.add(Matches.UnitCanLandOnCarrier);
final Collection<Unit> alliedAir = Match.getMatches(startUnits, friendlyNotOwnedAir);
if (alliedAir.isEmpty()) {
return Collections.emptyMap();
}
// remove air that can be carried by allied
final CompositeMatch<Unit> friendlyNotOwnedCarrier = new CompositeMatchAnd<>();
friendlyNotOwnedCarrier.add(Matches.UnitIsCarrier);
friendlyNotOwnedCarrier.add(Matches.alliedUnit(player, data));
friendlyNotOwnedCarrier.addInverse(Matches.unitIsOwnedBy(player));
final Collection<Unit> alliedCarrier = Match.getMatches(startUnits, friendlyNotOwnedCarrier);
final Iterator<Unit> alliedCarrierIter = alliedCarrier.iterator();
while (alliedCarrierIter.hasNext()) {
final Unit carrier = alliedCarrierIter.next();
final Collection<Unit> carrying = getCanCarry(carrier, alliedAir, player, data);
alliedAir.removeAll(carrying);
}
if (alliedAir.isEmpty()) {
return Collections.emptyMap();
}
final Map<Unit, Collection<Unit>> mapping = new HashMap<>();
// get air that must be carried by our carriers
final Collection<Unit> ownedCarrier =
Match.getMatches(units, new CompositeMatchAnd<>(Matches.UnitIsCarrier, Matches.unitIsOwnedBy(player)));
final Iterator<Unit> ownedCarrierIter = ownedCarrier.iterator();
while (ownedCarrierIter.hasNext()) {
final Unit carrier = ownedCarrierIter.next();
final Collection<Unit> carrying = getCanCarry(carrier, alliedAir, player, data);
alliedAir.removeAll(carrying);
mapping.put(carrier, carrying);
}
return mapping;
}
public static Collection<Unit> getCanCarry(final Unit carrier, final Collection<Unit> selectFrom,
final PlayerID playerWhoIsDoingTheMovement, final GameData data) {
final UnitAttachment ua = UnitAttachment.get(carrier.getUnitType());
final Collection<Unit> canCarry = new ArrayList<>();
int available = ua.getCarrierCapacity();
final Iterator<Unit> iter = selectFrom.iterator();
final TripleAUnit tACarrier = (TripleAUnit) carrier;
while (iter.hasNext()) {
final Unit plane = iter.next();
final TripleAUnit tAPlane = (TripleAUnit) plane;
final UnitAttachment planeAttachment = UnitAttachment.get(plane.getUnitType());
final int cost = planeAttachment.getCarrierCost();
if (available >= cost) {
// this is to test if they started in the same sea zone or not, and its not a very good way of testing it.
if ((tACarrier.getAlreadyMoved() == tAPlane.getAlreadyMoved())
|| (Matches.unitHasNotMoved.match(plane) && Matches.unitHasNotMoved.match(carrier))
|| (Matches.unitIsOwnedBy(playerWhoIsDoingTheMovement).invert().match(plane) && Matches.alliedUnit(
playerWhoIsDoingTheMovement, data).match(plane))) {
available -= cost;
canCarry.add(plane);
}
}
if (available == 0) {
break;
}
}
return canCarry;
}
/**
* Get the route ignoring forced territories.
*/
public static Route getBestRoute(final Territory start, final Territory end, final GameData data,
final PlayerID player, final Collection<Unit> units, final boolean forceLandOrSeaRoute) {
final boolean hasLand = Match.someMatch(units, Matches.UnitIsLand);
final boolean hasAir = Match.someMatch(units, Matches.UnitIsAir);
// final boolean hasSea = Match.someMatch(units, Matches.UnitIsSea);
final boolean isNeutralsImpassable =
isNeutralsImpassable(data) || (hasAir && !games.strategy.triplea.Properties.getNeutralFlyoverAllowed(data));
// Ignore the end territory in our tests. it must be in the route, so it shouldn't affect the route choice
// final Match<Territory> territoryIsEnd = Matches.territoryIs(end);
// No neutral countries on route predicate
final Match<Territory> noNeutral = Matches.TerritoryIsNeutralButNotWater.invert();
// No aa guns on route predicate
final Match<Territory> noAA = Matches.territoryHasEnemyAAforAnything(player, data).invert();
// no enemy units on the route predicate
final Match<Territory> noEnemy = Matches.territoryHasEnemyUnits(player, data).invert();
// no impassable or restricted territories
final CompositeMatchAnd<Territory> noImpassable =
new CompositeMatchAnd<>(Matches.TerritoryIsPassableAndNotRestricted(player, data));
// if we have air or land, we don't want to move over territories owned by players who's relationships will not let
// us move into them
if (hasAir) {
noImpassable.add(Matches.TerritoryAllowsCanMoveAirUnitsOverOwnedLand(player, data));
}
if (hasLand) {
noImpassable.add(Matches.TerritoryAllowsCanMoveLandUnitsOverOwnedLand(player, data));
}
// now find the default route
Route defaultRoute;
if (isNeutralsImpassable) {
defaultRoute =
data.getMap().getRoute_IgnoreEnd(start, end, new CompositeMatchAnd<>(noNeutral, noImpassable));
} else {
defaultRoute = data.getMap().getRoute_IgnoreEnd(start, end, noImpassable);
}
// since all routes require at least noImpassable, then if we cannot find a route without impassables, just return
// any route
if (defaultRoute == null) {
// at least try for a route without impassable territories, but allowing restricted territories, since there is a
// chance politics may change in the future.
defaultRoute = data.getMap().getRoute_IgnoreEnd(start, end,
(isNeutralsImpassable ? new CompositeMatchAnd<>(noNeutral, Matches.TerritoryIsImpassable)
: Matches.TerritoryIsImpassable));
// ok, so there really is nothing, so just return any route, without conditions
if (defaultRoute == null) {
return data.getMap().getRoute(start, end);
}
return defaultRoute;
}
// we don't want to look at the dependents
final Collection<Unit> unitsWhichAreNotBeingTransportedOrDependent =
new ArrayList<>(Match.getMatches(units,
Matches.unitIsBeingTransportedByOrIsDependentOfSomeUnitInThisList(units, defaultRoute, player, data, true)
.invert()));
boolean mustGoLand = false;
boolean mustGoSea = false;
// If start and end are land, try a land route.
// don't force a land route, since planes may be moving
if (!start.isWater() && !end.isWater()) {
Route landRoute;
if (isNeutralsImpassable) {
landRoute = data.getMap().getRoute_IgnoreEnd(start, end,
new CompositeMatchAnd<>(Matches.TerritoryIsLand, noNeutral, noImpassable));
} else {
landRoute = data.getMap().getRoute_IgnoreEnd(start, end,
new CompositeMatchAnd<>(Matches.TerritoryIsLand, noImpassable));
}
if (landRoute != null
&& ((landRoute.getLargestMovementCost(unitsWhichAreNotBeingTransportedOrDependent) <= defaultRoute
.getLargestMovementCost(unitsWhichAreNotBeingTransportedOrDependent))
|| (forceLandOrSeaRoute
&& Match.someMatch(unitsWhichAreNotBeingTransportedOrDependent, Matches.UnitIsLand)))) {
defaultRoute = landRoute;
mustGoLand = true;
}
}
// if the start and end are in water, try and get a water route
// dont force a water route, since planes may be moving
if (start.isWater() && end.isWater()) {
final Route waterRoute =
data.getMap().getRoute_IgnoreEnd(start, end,
new CompositeMatchAnd<>(Matches.TerritoryIsWater, noImpassable));
if (waterRoute != null
&& ((waterRoute.getLargestMovementCost(unitsWhichAreNotBeingTransportedOrDependent) <= defaultRoute
.getLargestMovementCost(unitsWhichAreNotBeingTransportedOrDependent)) || (forceLandOrSeaRoute && Match
.someMatch(unitsWhichAreNotBeingTransportedOrDependent, Matches.UnitIsSea)))) {
defaultRoute = waterRoute;
mustGoSea = true;
}
}
// these are the conditions we would like the route to satisfy, starting
// with the most important
List<Match<Territory>> tests;
if (isNeutralsImpassable) {
tests = new ArrayList<>(Arrays.asList(
// best if no enemy and no neutral
new CompositeMatchAnd<>(noEnemy, noNeutral),
// we will be satisfied if no aa and no neutral
new CompositeMatchAnd<>(noAA, noNeutral)));
} else {
tests = new ArrayList<>(Arrays.asList(
// best if no enemy and no neutral
new CompositeMatchAnd<>(noEnemy, noNeutral),
// we will be satisfied if no aa and no neutral
new CompositeMatchAnd<>(noAA, noNeutral),
// single matches
noEnemy, noAA, noNeutral));
}
for (final Match<Territory> t : tests) {
Match<Territory> testMatch = null;
if (mustGoLand) {
testMatch = new CompositeMatchAnd<>(t, Matches.TerritoryIsLand, noImpassable);
} else if (mustGoSea) {
testMatch = new CompositeMatchAnd<>(t, Matches.TerritoryIsWater, noImpassable);
} else {
testMatch = new CompositeMatchAnd<>(t, noImpassable);
}
final Route testRoute = data.getMap().getRoute_IgnoreEnd(start, end, testMatch);
if (testRoute != null
&& testRoute.getLargestMovementCost(unitsWhichAreNotBeingTransportedOrDependent) <= defaultRoute
.getLargestMovementCost(unitsWhichAreNotBeingTransportedOrDependent)) {
return testRoute;
}
}
return defaultRoute;
}
private static boolean isWW2V2(final GameData data) {
return games.strategy.triplea.Properties.getWW2V2(data);
}
private static boolean isNeutralsImpassable(final GameData data) {
return games.strategy.triplea.Properties.getNeutralsImpassable(data);
}
private static boolean isNeutralsBlitzable(final GameData data) {
return games.strategy.triplea.Properties.getNeutralsBlitzable(data) && !isNeutralsImpassable(data);
}
private static boolean isMovementByTerritoryRestricted(final GameData data) {
return games.strategy.triplea.Properties.getMovementByTerritoryRestricted(data);
}
private static boolean isAirTransportableCanMoveDuringNonCombat(final GameData data) {
return games.strategy.triplea.Properties.getParatroopersCanMoveDuringNonCombat(data);
}
private static int getNeutralCharge(final GameData data, final int numberOfTerritories) {
return numberOfTerritories * games.strategy.triplea.Properties.getNeutralCharge(data);
}
private static boolean isSubmersibleSubsAllowed(final GameData data) {
return games.strategy.triplea.Properties.getSubmersible_Subs(data);
}
private static boolean isIgnoreTransportInMovement(final GameData data) {
return games.strategy.triplea.Properties.getIgnoreTransportInMovement(data);
}
private static boolean isIgnoreSubInMovement(final GameData data) {
return games.strategy.triplea.Properties.getIgnoreSubInMovement(data);
}
/** Creates new MoveValidator. */
private MoveValidator() {}
}