package games.strategy.triplea.delegate;
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.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.GameMap;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.Route;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.triplea.Constants;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.delegate.dataObjects.MoveValidationResult;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
/**
* Provides static methods for evaluating movement of air units.
*/
public class AirMovementValidator {
public static final String NOT_ALL_AIR_UNITS_CAN_LAND = "Not all air units can land";
// TODO: this class does a pretty good job already, but could be improved by having the carriers that are potentially
// moved also look for
// any owned air units that are in sea zones without carriers. these would be air units that have already been moved
// this turn, and
// therefore would need pickup.
public static MoveValidationResult validateAirCanLand(final GameData data, final Collection<Unit> units,
final Route route, final PlayerID player, final MoveValidationResult result) {
// First check if we even need to check
if (getEditMode(data) || // Edit Mode, no need to check
!Match.someMatch(units, Matches.UnitIsAir) || // No Airunits, nothing to check
route.hasNoSteps() || // if there are no steps, we didn't move, so it is always OK!
// we can land at the end, nothing left to check
Matches.airCanLandOnThisAlliedNonConqueredLandTerritory(player, data).match(route.getEnd())
|| isKamikazeAircraft(data) // we do not do any validation at all, cus they can all die and we don't care
) {
return result;
}
// Find which aircraft cannot find friendly land to land on
final Collection<Unit> ownedAirThatMustLandOnCarriers =
getAirThatMustLandOnCarriers(data, getAirUnitsToValidate(units, route, player), route, result);
if (ownedAirThatMustLandOnCarriers.isEmpty()) {
// we are done, everything can find a place to land
return result;
}
final Territory routeEnd = route.getEnd();
final Territory routeStart = route.getStart();
// we cannot forget to account for allied air at our location already
final Match<Unit> airAlliedNotOwned = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player).invert(),
Matches.isUnitAllied(player, data), Matches.UnitIsAir, Matches.UnitCanLandOnCarrier);
final HashSet<Unit> airThatMustLandOnCarriersHash = new HashSet<>();
airThatMustLandOnCarriersHash.addAll(Match.getMatches(routeEnd.getUnits().getUnits(), airAlliedNotOwned));
airThatMustLandOnCarriersHash.addAll(Match.getMatches(units, airAlliedNotOwned));
// now we must see if we also need to account for units (allied cargo) that are moving with our carriers, if we have
// selected any
// carriers
final Collection<Unit> movingCarriersAtStartLocationBeingMoved = Match.getMatches(units, Matches.UnitIsCarrier);
if (!movingCarriersAtStartLocationBeingMoved.isEmpty()) {
final Map<Unit, Collection<Unit>> carrierToAlliedCargo =
MoveValidator.carrierMustMoveWith(units, routeStart, data, player);
for (final Collection<Unit> alliedAirOnCarrier : carrierToAlliedCargo.values()) {
airThatMustLandOnCarriersHash.addAll(alliedAirOnCarrier);
}
}
// now we can add our owned air. we add our owned air last because it can be moved, while allied air cannot be. we
// want the lowest
// movement to be validated first.
airThatMustLandOnCarriersHash.addAll(ownedAirThatMustLandOnCarriers);
final List<Unit> airThatMustLandOnCarriers = new ArrayList<>(airThatMustLandOnCarriersHash);
// sort the list by shortest range first so those birds will get first pick of landingspots
Collections.sort(airThatMustLandOnCarriers, getLowestToHighestMovementComparatorIncludingUnitsNotYetMoved(route));
// now we should see if the carriers we are moving with, plus the carriers already there, can handle all our air
// units (we check ending
// territories first, separately, because it is special [it includes units in our selection])
final Collection<Unit> carriersAtEnd = Match.getMatches(getFriendly(routeEnd, player, data), Matches.UnitIsCarrier);
carriersAtEnd.addAll(movingCarriersAtStartLocationBeingMoved);
// to keep track of all carriers, and their fighters, that have moved, so that we do not move them again.
final Map<Unit, Collection<Unit>> movedCarriersAndTheirFighters = new HashMap<>();
for (final Unit carrier : carriersAtEnd) {
movedCarriersAndTheirFighters.put(carrier, new ArrayList<>());
}
final Collection<Unit> airNotToConsiderBecauseWeAreValidatingThem = new ArrayList<>(airThatMustLandOnCarriers);
airThatMustLandOnCarriers
.removeAll(whatAirCanLandOnTheseCarriers(carriersAtEnd, airThatMustLandOnCarriers, routeEnd));
if (airThatMustLandOnCarriers.isEmpty()) {
return result;
}
// we still have air left, so begin calling carriers to come here to pick up the air
// figure out what is the max distance of
// our remaining air units
final int maxMovementLeftForTheseAirUnitsBeingValidated =
maxMovementLeftForTheseAirUnitsBeingValidated(airThatMustLandOnCarriers, route, player);
// figure out what is the max distance
// of our remaining carrier units
final int maxMovementLeftForAllOwnedCarriers = maxMovementLeftForAllOwnedCarriers(player, data);
final List<Territory> landingSpots = new ArrayList<>(Collections.singleton(routeEnd));
landingSpots.addAll(data.getMap().getNeighbors(routeEnd, maxMovementLeftForTheseAirUnitsBeingValidated,
// where can we fly to?
Matches.airCanFlyOver(player, data, areNeutralsPassableByAir(data))));
// we only want to consider
landingSpots.removeAll(Match.getMatches(landingSpots, Matches.seaCanMoveOver(player, data).invert()));
// places we can move carriers to
Collections.sort(landingSpots, getLowestToHighestDistance(routeEnd, Matches.seaCanMoveOver(player, data)));
final Collection<Territory> potentialCarrierOrigins = new LinkedHashSet<>(landingSpots);
potentialCarrierOrigins.addAll(data.getMap().getNeighbors(new HashSet<>(landingSpots),
maxMovementLeftForAllOwnedCarriers, Matches.seaCanMoveOver(player, data)));
potentialCarrierOrigins.remove(routeEnd);
potentialCarrierOrigins
.removeAll(Match.getMatches(potentialCarrierOrigins, Matches.TerritoryHasOwnedCarrier(player).invert()));
// now see if we can move carriers there to pick up
validateAirCaughtByMovingCarriersAndOwnedAndAlliedAir(result, landingSpots, potentialCarrierOrigins,
movedCarriersAndTheirFighters, airThatMustLandOnCarriers, airNotToConsiderBecauseWeAreValidatingThem, player,
route, data);
return result;
}
private static LinkedHashMap<Unit, Integer> getMovementLeftForValidatingAir(final Collection<Unit> airBeingValidated,
final PlayerID player, final Route route) {
final LinkedHashMap<Unit, Integer> map = new LinkedHashMap<>();
for (final Unit unit : airBeingValidated) {
// unit must be in either start or end.
int movementLeft;
if (Matches.unitIsOwnedBy(player).match(unit)) {
movementLeft = getMovementLeftForAirUnitNotMovedYet(unit, route);
} else {
movementLeft = 0;
}
map.put(unit, movementLeft);
}
return map;
}
private static int getMovementLeftForAirUnitNotMovedYet(final Unit airBeingValidated, final Route route) {
if (route.getEnd().getUnits().getUnits().contains(airBeingValidated)) {
// they are not being moved, they are already at the end
return ((TripleAUnit) airBeingValidated).getMovementLeft();
} else {
// they are being moved (they are still at the start location)
return route.getMovementLeft(airBeingValidated);
}
}
private static IntegerMap<Territory> populateStaticAlliedAndBuildingCarrierCapacity(
final List<Territory> landingSpots, final Map<Unit, Collection<Unit>> movedCarriersAndTheirFighters,
final PlayerID player, final GameData data) {
final IntegerMap<Territory> startingSpace = new IntegerMap<>();
final Match<Unit> carrierAlliedNotOwned = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player).invert(),
Matches.isUnitAllied(player, data), Matches.UnitIsCarrier);
// final Match<Unit> airAlliedNotOwned = new CompositeMatchAnd<Unit>(Matches.unitIsOwnedBy(player).invert(),
// Matches.isUnitAllied(player, data), Matches.UnitIsAir, Matches.UnitCanLandOnCarrier);
final boolean landAirOnNewCarriers = AirThatCantLandUtil.isLHTRCarrierProduction(data)
|| AirThatCantLandUtil.isLandExistingFightersOnNewCarriers(data);
// final boolean areNeutralsPassableByAir = areNeutralsPassableByAir(data);
final List<Unit> carriersInProductionQueue = player.getUnits().getMatches(Matches.UnitIsCarrier);
for (final Territory t : landingSpots) {
if (landAirOnNewCarriers && !carriersInProductionQueue.isEmpty()) {
if (Matches.TerritoryIsWater.match(t)
&& Matches.territoryHasOwnedAtBeginningOfTurnIsFactoryOrCanProduceUnitsNeighbor(data, player).match(t)) {
// TODO: Here we are assuming that this factory can produce all of the carriers. Actually it might not be able
// to produce any
// carriers (because of complex requires units coding) or because of unit damage or maximum production.
// TODO: Here we are also assuming that the first territory we find that has an adjacent factory is the
// closest one in terms of
// unit movement. We have sorted the list of territories so this IS the closest in terms of steps, but each
// unit may have specific
// movement allowances for different terrain or some bullshit like that.
final int producedCarrierCapacity = carrierCapacity(carriersInProductionQueue, t);
startingSpace.add(t, producedCarrierCapacity);
carriersInProductionQueue.clear();
}
}
final Collection<Unit> alliedCarriers = t.getUnits().getMatches(carrierAlliedNotOwned);
alliedCarriers.removeAll(movedCarriersAndTheirFighters.keySet());
// Collection<Unit> alliedAir = t.getUnits().getMatches(airAlliedNotOwned);
final int alliedCarrierCapacity = carrierCapacity(alliedCarriers, t);
startingSpace.add(t, alliedCarrierCapacity);
}
return startingSpace;
}
private static void validateAirCaughtByMovingCarriersAndOwnedAndAlliedAir(final MoveValidationResult result,
final List<Territory> landingSpots, final Collection<Territory> potentialCarrierOrigins,
final Map<Unit, Collection<Unit>> movedCarriersAndTheirFighters, final Collection<Unit> airThatMustLandOnCarriers,
final Collection<Unit> airNotToConsider, final PlayerID player, final Route route, final GameData data) {
final Match<Unit> ownedCarrierMatch =
new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player), Matches.UnitIsCarrier);
final Match<Unit> ownedAirMatch =
new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player), Matches.UnitIsAir, Matches.UnitCanLandOnCarrier);
final Match<Unit> alliedNotOwnedAirMatch = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player).invert(),
Matches.isUnitAllied(player, data), Matches.UnitIsAir, Matches.UnitCanLandOnCarrier);
final Match<Unit> alliedNotOwnedCarrierMatch = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player).invert(),
Matches.isUnitAllied(player, data), Matches.UnitIsCarrier);
final Territory routeEnd = route.getEnd();
final boolean areNeutralsPassableByAir = areNeutralsPassableByAir(data);
final IntegerMap<Territory> landingSpotsWithCarrierCapacity =
// fill our landing spot capacity with capacity from allied carriers and potential building of new carriers
populateStaticAlliedAndBuildingCarrierCapacity(landingSpots, movedCarriersAndTheirFighters, player, data);
final LinkedHashMap<Unit, Integer> movementLeftForAirToValidate =
// calculate movement left only once
getMovementLeftForValidatingAir(airThatMustLandOnCarriers, player, route);
for (final Territory landingSpot : landingSpots) {
// since we are here, no point looking at this place twice
potentialCarrierOrigins.remove(landingSpot);
final List<Unit> airCanReach = new ArrayList<>();
for (final Unit air : airThatMustLandOnCarriers) {
if (canAirReachThisSpot(data, player, air, routeEnd, movementLeftForAirToValidate.get(air), landingSpot,
areNeutralsPassableByAir)) {
// get all air that can reach this spot
airCanReach.add(air);
}
}
if (airCanReach.isEmpty()) {
continue;
}
final Collection<Unit> unitsInLandingSpot = landingSpot.getUnits().getUnits();
unitsInLandingSpot.removeAll(movedCarriersAndTheirFighters.keySet());
// make sure to remove any units we have already moved, or units that are excluded
unitsInLandingSpot.removeAll(airNotToConsider);
// because they are in our mouse selection
for (final Collection<Unit> ftrs : movedCarriersAndTheirFighters.values()) {
// also remove any fighters that are being moved with carriers that we have already moved
unitsInLandingSpot.removeAll(ftrs);
}
final Collection<Unit> ownedCarriersInLandingSpot = Match.getMatches(unitsInLandingSpot, ownedCarrierMatch);
// get air we own here, but exclude any air that can fly to allied land
final Collection<Unit> airInLandingSpot = Match.getMatches(Match.getMatches(unitsInLandingSpot, ownedAirMatch),
UnitCanFindLand(data, landingSpot).invert());
// add allied air (it can't fly away)
airInLandingSpot.addAll(Match.getMatches(unitsInLandingSpot, alliedNotOwnedAirMatch));
// make sure we don't count this again
// airNotToConsider.addAll(airInLandingSpot);
// get the current capacity
int landingSpotCapacity = landingSpotsWithCarrierCapacity.getInt(landingSpot);
// add capacity of owned carriers
landingSpotCapacity += carrierCapacity(ownedCarriersInLandingSpot, landingSpot);
// minus capacity of air in the territory
landingSpotCapacity -= carrierCost(airInLandingSpot);
if (!airCanReach.isEmpty()) {
final Iterator<Unit> airIter = airCanReach.iterator();
while (airIter.hasNext()) {
final Unit air = airIter.next();
final int carrierCost = carrierCost(air);
if (landingSpotCapacity >= carrierCost) {
landingSpotCapacity -= carrierCost;
// we can land this one here, yay
airThatMustLandOnCarriers.remove(air);
airIter.remove();
}
}
}
if (airThatMustLandOnCarriers.isEmpty()) {
// all can land here, so return
return;
}
// final int lowestCarrierCost = getLowestCarrierCost(airCanReach);
// now bring carriers here...
final Iterator<Territory> iter = potentialCarrierOrigins.iterator();
while (iter.hasNext()) {
final Territory carrierSpot = iter.next();
final Collection<Unit> unitsInCarrierSpot = carrierSpot.getUnits().getUnits();
// remove carriers we have already moved
unitsInCarrierSpot.removeAll(movedCarriersAndTheirFighters.keySet());
// remove units we do not want to consider because they are in our mouse selection
unitsInCarrierSpot.removeAll(airNotToConsider);
for (final Collection<Unit> ftrs : movedCarriersAndTheirFighters.values()) {
// remove the fighters that are moving with the carriers we have already moved
unitsInCarrierSpot.removeAll(ftrs);
}
final Collection<Unit> ownedCarriersInCarrierSpot = Match.getMatches(unitsInCarrierSpot, ownedCarrierMatch);
if (ownedCarriersInCarrierSpot.isEmpty()) {
iter.remove();
continue;
}
final Collection<Unit> ownedAirInCarrierSpot = Match.getMatches(
// exclude any owned air that can fly to land
Match.getMatches(unitsInCarrierSpot, ownedAirMatch), UnitCanFindLand(data, carrierSpot).invert());
final Collection<Unit> alliedNotOwnedAirInCarrierSpot =
Match.getMatches(unitsInCarrierSpot, alliedNotOwnedAirMatch);
final Map<Unit, Collection<Unit>> mustMoveWithMap =
// this only returns the allied cargo
MoveValidator.carrierMustMoveWith(ownedCarriersInCarrierSpot, carrierSpot, data, player);
// planes that MUST travel with the carrier
// get the current capacity for the carrier spot
int carrierSpotCapacity = landingSpotsWithCarrierCapacity.getInt(carrierSpot);
// we don't have it because this spot is not in the landing zone area.
if (!landingSpotsWithCarrierCapacity.containsKey(carrierSpot)) {
// we still have a capacity for allied carriers, but only to carry other allied or local owned units, not to
// carry our selected
// units.
carrierSpotCapacity =
carrierCapacity(carrierSpot.getUnits().getMatches(alliedNotOwnedCarrierMatch), carrierSpot);
landingSpotsWithCarrierCapacity.put(carrierSpot, carrierSpotCapacity);
}
// we have allied air here, so we need to account for them before moving any carriers
if (!alliedNotOwnedAirInCarrierSpot.isEmpty() || !mustMoveWithMap.isEmpty()) {
// mustMoveWithMap is only filled if we have so many allied air that our owned carriers are carrying some of
// them
if (mustMoveWithMap.isEmpty()) {
// allied carriers can carry enough
carrierSpotCapacity -= carrierCost(alliedNotOwnedAirInCarrierSpot);
// we do not want to consider these units again
airNotToConsider.addAll(alliedNotOwnedAirInCarrierSpot);
if (carrierSpotCapacity > 0) {
// we can hold some of the owned air here too
final Iterator<Unit> ownedIter = ownedAirInCarrierSpot.iterator();
while (ownedIter.hasNext()) {
final Unit air = ownedIter.next();
final int carrierCost = carrierCost(air);
if (carrierSpotCapacity >= carrierCost) {
carrierSpotCapacity -= carrierCost;
// we do not want to consider this one again
airNotToConsider.add(air);
ownedIter.remove();
}
}
}
// put correct value for future reference now that we
// have considered the allied air
landingSpotsWithCarrierCapacity.put(carrierSpot, carrierSpotCapacity);
} else {
// carrierMustMoveWith does not account for any allied cargo already moved out.
for (final Collection<Unit> airMovingWith : mustMoveWithMap.values()) {
for (final Collection<Unit> ftrs : movedCarriersAndTheirFighters.values()) {
// remove the fighters that are moving with the carriers we have already moved
airMovingWith.removeAll(ftrs);
}
}
for (final Collection<Unit> airMovingWith : mustMoveWithMap.values()) {
// we will consider these as part of their moving carrier
alliedNotOwnedAirInCarrierSpot.removeAll(airMovingWith);
}
carrierSpotCapacity -= carrierCost(alliedNotOwnedAirInCarrierSpot);
// we do not want to consider these units again
airNotToConsider.addAll(alliedNotOwnedAirInCarrierSpot);
// put correct value for future reference now that we
// have considered the allied air
landingSpotsWithCarrierCapacity.put(carrierSpot, carrierSpotCapacity);
}
}
final Route toLandingSpot =
data.getMap().getRoute(carrierSpot, landingSpot, Matches.seaCanMoveOver(player, data));
if (toLandingSpot == null) {
continue;
}
final List<Unit> carrierCanReach =
Match.getMatches(ownedCarriersInCarrierSpot, Matches.UnitHasEnoughMovementForRoute(toLandingSpot));
if (carrierCanReach.isEmpty()) {
// none can reach
continue;
}
final List<Unit> carrierNotReach = new ArrayList<>(ownedCarriersInCarrierSpot);
// we want to see if the air units can be put on the carriers that cannot make it
// first, before taking up room on the carriers that can make it
carrierNotReach.removeAll(carrierCanReach);
final List<Unit> allCarriers = new ArrayList<>(carrierNotReach);
// so we remove them from the list then re-add them so that they will be at the end of the list
allCarriers.addAll(carrierCanReach);
// now we want to make a map of the carriers to the units they must carry with them (both allied and owned)
final Map<Unit, Collection<Unit>> carriersToMove = new HashMap<>();
final List<Unit> carrierFull = new ArrayList<>();
for (final Unit carrier : allCarriers) {
final Collection<Unit> airMovingWith = new ArrayList<>();
// first add allied cargo
final Collection<Unit> alliedMovingWith = mustMoveWithMap.get(carrier);
if (alliedMovingWith != null) {
airMovingWith.addAll(alliedMovingWith);
}
// now test if our carrier has any room for owned fighters
int carrierCapacity = carrierCapacity(carrier, carrierSpot);
carrierCapacity -= carrierCost(airMovingWith);
final Iterator<Unit> ownedIter = ownedAirInCarrierSpot.iterator();
while (ownedIter.hasNext()) {
final Unit air = ownedIter.next();
final int carrierCost = carrierCost(air);
if (carrierCapacity >= carrierCost) {
carrierCapacity -= carrierCost;
airMovingWith.add(air);
ownedIter.remove();
}
}
carriersToMove.put(carrier, airMovingWith);
if (carrierCapacity <= 0) {
carrierFull.add(carrier);
}
}
// if all carriers full, remove this carrier spot from consideration
if (carrierFull.containsAll(allCarriers)) {
iter.remove();
continue;
}
if (carrierFull.containsAll(carrierNotReach)) {
iter.remove();
}
// ok, now lets move them.
for (final Unit carrier : carrierCanReach) {
movedCarriersAndTheirFighters.put(carrier, carriersToMove.get(carrier));
landingSpotCapacity += carrierCapacity(carrier, carrierSpot);
landingSpotCapacity -= carrierCost(carriersToMove.get(carrier));
}
// optional for debugging
// landingSpotsWithCarrierCapacity.put(landingSpot, landingSpotCapacity);
final Iterator<Unit> reachIter = airCanReach.iterator();
while (reachIter.hasNext()) {
final Unit air = reachIter.next();
final int carrierCost = carrierCost(air);
if (landingSpotCapacity >= carrierCost) {
landingSpotCapacity -= carrierCost;
// we can land this one here, yay
airThatMustLandOnCarriers.remove(air);
reachIter.remove();
}
}
if (airThatMustLandOnCarriers.isEmpty()) {
// all can land here, so return
return;
}
}
}
// anyone left over cannot land
for (final Unit air : airThatMustLandOnCarriers) {
result.addDisallowedUnit(NOT_ALL_AIR_UNITS_CAN_LAND, air);
}
}
private static Comparator<Territory> getLowestToHighestDistance(final Territory territoryWeMeasureDistanceFrom,
final Match<Territory> condition) {
return (t1, t2) -> {
if (t1.equals(t2)) {
return 0;
}
final GameMap map = t1.getData().getMap();
final int distance1 = map.getDistance(territoryWeMeasureDistanceFrom, t1, condition);
final int distance2 = map.getDistance(territoryWeMeasureDistanceFrom, t2, condition);
if (distance1 == distance2) {
return 0;
}
if (distance1 < 0) {
return 1;
}
if (distance2 < 0) {
return -1;
}
if (distance1 < distance2) {
return -1;
}
return 1;
};
}
private static int maxMovementLeftForAllOwnedCarriers(final PlayerID player, final GameData data) {
int max = 0;
final Match<Unit> ownedCarrier = new CompositeMatchAnd<>(Matches.UnitIsCarrier, Matches.unitIsOwnedBy(player));
for (final Territory t : data.getMap().getTerritories()) {
for (final Unit carrier : t.getUnits().getMatches(ownedCarrier)) {
max = Math.max(max, ((TripleAUnit) carrier).getMovementLeft());
}
}
return max;
}
private static int maxMovementLeftForTheseAirUnitsBeingValidated(final Collection<Unit> airUnits, final Route route,
final PlayerID player) {
int max = 0;
for (final Unit u : airUnits) {
if (Matches.unitIsOwnedBy(player).match(u)) {
// unit must be in either start or end.
final int movementLeft = getMovementLeftForAirUnitNotMovedYet(u, route);
if (movementLeft > max) {
max = movementLeft;
}
}
// allied units can't move....
}
return max;
}
private static Collection<Unit> whatAirCanLandOnTheseCarriers(final Collection<Unit> carriers,
final Collection<Unit> airUnits, final Territory territoryUnitsAreIn) {
final Collection<Unit> airThatCanLandOnThem = new ArrayList<>();
for (final Unit carrier : carriers) {
int carrierCapacity = carrierCapacity(carrier, territoryUnitsAreIn);
for (final Unit air : airUnits) {
if (airThatCanLandOnThem.contains(air)) {
continue;
}
final int airCost = carrierCost(air);
if (carrierCapacity >= airCost) {
carrierCapacity -= airCost;
airThatCanLandOnThem.add(air);
}
}
}
return airThatCanLandOnThem;
}
/**
* @param units
* the units flying this route.
* @param route
* the route flown
* @param player
* the player owning the units
* @return the combination of units that fly here and the existing owned units
*/
private static List<Unit> getAirUnitsToValidate(final Collection<Unit> units, final Route route,
final PlayerID player) {
final Match<Unit> ownedAirMatch =
new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.unitOwnedBy(player), Matches.UnitIsKamikaze.invert());
final List<Unit> ownedAir = new ArrayList<>();
ownedAir.addAll(Match.getMatches(route.getEnd().getUnits().getUnits(), ownedAirMatch));
ownedAir.addAll(Match.getMatches(units, ownedAirMatch));
// sort the list by shortest range first so those birds will get first pick of landingspots
Collections.sort(ownedAir, getLowestToHighestMovementComparatorIncludingUnitsNotYetMoved(route));
return ownedAir;
}
private static Comparator<Unit> getLowestToHighestMovementComparatorIncludingUnitsNotYetMoved(final Route route) {
return (u1, u2) -> {
final int left1 = getMovementLeftForAirUnitNotMovedYet(u1, route);
final int left2 = getMovementLeftForAirUnitNotMovedYet(u2, route);
if (left1 == left2) {
return 0;
}
if (left1 > left2) {
return 1;
}
return -1;
};
}
private static boolean canAirReachThisSpot(final GameData data, final PlayerID player, final Unit unit,
final Territory currentSpot, final int movementLeft, final Territory landingSpot,
final boolean areNeutralsPassableByAir) {
if (areNeutralsPassableByAir) {
final Route neutralViolatingRoute = data.getMap().getRoute(currentSpot, landingSpot,
Matches.airCanFlyOver(player, data, areNeutralsPassableByAir));
return (neutralViolatingRoute != null && neutralViolatingRoute.getMovementCost(unit) <= movementLeft
&& getNeutralCharge(data, neutralViolatingRoute) <= player.getResources().getQuantity(Constants.PUS));
} else {
final Route noNeutralRoute = data.getMap().getRoute(currentSpot, landingSpot,
Matches.airCanFlyOver(player, data, areNeutralsPassableByAir));
return (noNeutralRoute != null && noNeutralRoute.getMovementCost(unit) <= movementLeft);
}
}
/**
* Can this airunit reach safe land at this point in the route?
*
* @param unit
* the airunit in question
* @param route
* the current spot from which he needs to reach safe land.
* @return whether the air-unit can find a stretch of friendly land to land on given her current spot and the
* remaining range.
*/
private static boolean canFindLand(final GameData data, final Unit unit, final Route route) {
final Territory routeEnd = route.getEnd();
// unit must be in either start or end.
final int movementLeft = getMovementLeftForAirUnitNotMovedYet(unit, route);
return canFindLand(data, unit, routeEnd, movementLeft);
}
private static boolean canFindLand(final GameData data, final Unit unit, final Territory current) {
final int movementLeft = ((TripleAUnit) unit).getMovementLeft();
return canFindLand(data, unit, current, movementLeft);
}
private static boolean canFindLand(final GameData data, final Unit unit, final Territory current,
final int movementLeft) {
if (movementLeft <= 0) {
return false;
}
final boolean areNeutralsPassableByAir = areNeutralsPassableByAir(data);
final PlayerID player = unit.getOwner();
final List<Territory> possibleSpots = Match.getMatches(data.getMap().getNeighbors(current, movementLeft),
Matches.airCanLandOnThisAlliedNonConqueredLandTerritory(player, data));
// TODO EW: Assuming movement cost of 1, this could get VERY slow when the movement cost is very high and air units
// have a lot of movement capacity.
for (final Territory landingSpot : possibleSpots) {
if (canAirReachThisSpot(data, player, unit, current, movementLeft, landingSpot, areNeutralsPassableByAir)) {
return true;
}
}
return false;
}
private static Match<Unit> UnitCanFindLand(final GameData data, final Territory current) {
return new Match<Unit>() {
@Override
public boolean match(final Unit u) {
return canFindLand(data, u, current);
}
};
}
/**
* Returns true if the given air units can land in the given territory.
* Does take into account whether a battle has been fought in the territory already.
* Note units must only be air units
*/
public static boolean canLand(final Collection<Unit> airUnits, final Territory territory, final PlayerID player,
final GameData data) {
if (!Match.allMatch(airUnits, Matches.UnitIsAir)) {
throw new IllegalArgumentException("can only test if air will land");
}
if (!territory.isWater() && AbstractMoveDelegate.getBattleTracker(data).wasConquered(territory)) {
return false;
}
if (territory.isWater()) {
// if they cant all land on carriers
if (!Match.allMatch(airUnits, Matches.UnitCanLandOnCarrier)) {
return false;
}
// when doing the calculation, make sure to include the units
// in the territory
final Set<Unit> friendly = new HashSet<>();
friendly.addAll(getFriendly(territory, player, data));
friendly.addAll(airUnits);
// make sure we have the carrier capacity
final int capacity = carrierCapacity(friendly, territory);
final int cost = carrierCost(friendly);
return capacity >= cost;
} else {
return data.getRelationshipTracker().canLandAirUnitsOnOwnedLand(player, territory.getOwner());
}
}
private static Collection<Unit> getAirThatMustLandOnCarriers(final GameData data, final Collection<Unit> ownedAir,
final Route route, final MoveValidationResult result) {
final Collection<Unit> airThatMustLandOnCarriers = new ArrayList<>();
final Match<Unit> canLandOnCarriers = Matches.UnitCanLandOnCarrier;
for (final Unit unit : ownedAir) {
if (!canFindLand(data, unit, route)) {
if (canLandOnCarriers.match(unit)) {
airThatMustLandOnCarriers.add(unit);
} else {
// not everything can land on a carrier (i.e. bombers)
result.addDisallowedUnit(NOT_ALL_AIR_UNITS_CAN_LAND, unit);
}
}
}
return airThatMustLandOnCarriers;
}
/**
* Does not, and is not supposed to, account for any units already on this carrier (like allied/cargo fighters).
* Instead this method only adds up the total capacity of each unit, and accounts for damaged carriers with special
* properties and
* restrictions.
*/
public static int carrierCapacity(final Collection<Unit> units, final Territory territoryUnitsAreCurrentlyIn) {
int sum = 0;
for (final Unit unit : units) {
sum += carrierCapacity(unit, territoryUnitsAreCurrentlyIn);
}
return sum;
}
/**
* Does not, and is not supposed to, account for any units already on this carrier (like allied/cargo fighters).
* Instead this method only adds up the total capacity of each unit, and accounts for damaged carriers with special
* properties and
* restrictions.
*/
public static int carrierCapacity(final Unit unit, final Territory territoryUnitsAreCurrentlyIn) {
if (Matches.UnitIsCarrier.match(unit)) {
// here we check to see if the unit can no longer carry units
if (Matches.UnitHasWhenCombatDamagedEffect(UnitAttachment.UNITSMAYNOTLANDONCARRIER).match(unit)) {
// and we must check to make sure we let any allied air that are cargo stay here
if (Matches.UnitHasWhenCombatDamagedEffect(UnitAttachment.UNITSMAYNOTLEAVEALLIEDCARRIER).match(unit)) {
int cargo = 0;
final Collection<Unit> airCargo = territoryUnitsAreCurrentlyIn.getUnits()
.getMatches(new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.UnitCanLandOnCarrier));
for (final Unit airUnit : airCargo) {
final TripleAUnit taUnit = (TripleAUnit) airUnit;
if (taUnit.getTransportedBy() != null && taUnit.getTransportedBy().equals(unit)) {
// capacity = are cargo only
cargo += UnitAttachment.get(taUnit.getType()).getCarrierCost();
}
}
return cargo;
} else {
// capacity = zero 0
return 0;
}
} else {
final UnitAttachment ua = UnitAttachment.get(unit.getType());
return ua.getCarrierCapacity();
}
}
return 0;
}
public static int carrierCost(final Collection<Unit> units) {
int sum = 0;
for (final Unit unit : units) {
sum += carrierCost(unit);
}
return sum;
}
public static int carrierCost(final Unit unit) {
if (Matches.UnitCanLandOnCarrier.match(unit)) {
return UnitAttachment.get(unit.getType()).getCarrierCost();
}
return 0;
}
private static boolean getEditMode(final GameData data) {
return BaseEditDelegate.getEditMode(data);
}
public static Collection<Unit> getFriendly(final Territory territory, final PlayerID player, final GameData data) {
return territory.getUnits().getMatches(Matches.alliedUnit(player, data));
}
private static boolean isKamikazeAircraft(final GameData data) {
return games.strategy.triplea.Properties.getKamikaze_Airplanes(data);
}
private static boolean areNeutralsPassableByAir(final GameData data) {
return (games.strategy.triplea.Properties.getNeutralFlyoverAllowed(data) && !isNeutralsImpassable(data));
}
private static boolean isNeutralsImpassable(final GameData data) {
return games.strategy.triplea.Properties.getNeutralsImpassable(data);
}
private static int getNeutralCharge(final GameData data, final Route route) {
return getNeutralCharge(data, MoveDelegate.getEmptyNeutral(route).size());
}
private static int getNeutralCharge(final GameData data, final int numberOfTerritories) {
return numberOfTerritories * games.strategy.triplea.Properties.getNeutralCharge(data);
}
}