package games.strategy.triplea.ai.proAI; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Resource; import games.strategy.engine.data.Route; import games.strategy.engine.data.TechnologyFrontier; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.triplea.Constants; import games.strategy.triplea.Properties; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.MoveValidator; import games.strategy.triplea.delegate.TechAdvance; import games.strategy.triplea.delegate.TransportTracker; import games.strategy.triplea.delegate.remote.ITechDelegate; import games.strategy.util.CompositeMatch; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.CompositeMatchOr; import games.strategy.util.IntegerMap; import games.strategy.util.InverseMatch; import games.strategy.util.Match; /** * Pro tech AI. */ public final class ProTechAI { public static void tech(final ITechDelegate techDelegate, final GameData data, final PlayerID player) { if (!games.strategy.triplea.Properties.getWW2V3TechModel(data)) { return; } final Territory myCapitol = TerritoryAttachment.getFirstOwnedCapitalOrFirstUnownedCapital(player, data); final float eStrength = getStrengthOfPotentialAttackers(myCapitol, data, player, false, true, null); float myStrength = strength(myCapitol.getUnits().getUnits(), false, false, false); final List<Territory> areaStrength = getNeighboringLandTerritories(data, player, myCapitol); for (final Territory areaTerr : areaStrength) { myStrength += strength(areaTerr.getUnits().getUnits(), false, false, false) * 0.75F; } final boolean capDanger = myStrength < (eStrength * 1.25F + 3.0F); final Resource pus = data.getResourceList().getResource(Constants.PUS); final int PUs = player.getResources().getQuantity(pus); final Resource techtokens = data.getResourceList().getResource(Constants.TECH_TOKENS); final int TechTokens = player.getResources().getQuantity(techtokens); int TokensToBuy = 0; if (!capDanger && TechTokens < 3 && PUs > Math.random() * 160) { TokensToBuy = 1; } if (TechTokens > 0 || TokensToBuy > 0) { final List<TechnologyFrontier> cats = TechAdvance.getPlayerTechCategories(data, player); // retaining 65% chance of choosing land advances using basic ww2v3 model. if (data.getTechnologyFrontier().isEmpty()) { if (Math.random() > 0.35) { techDelegate.rollTech(TechTokens + TokensToBuy, cats.get(1), TokensToBuy, null); } else { techDelegate.rollTech(TechTokens + TokensToBuy, cats.get(0), TokensToBuy, null); } } else { final int rand = (int) (Math.random() * cats.size()); techDelegate.rollTech(TechTokens + TokensToBuy, cats.get(rand), TokensToBuy, null); } } } /** * Returns the strength of all attackers to a territory * differentiates between sea and land attack * determines all transports within range of territory * determines all air units within range of territory (using 2 for fighters and 3 for bombers) * does not check for extended range fighters or bombers * * @param tFirst * - can transports be killed before other sea units * @param ignoreOnlyPlanes * - if true, returns 0.0F if only planes can attack the territory */ private static float getStrengthOfPotentialAttackers(final Territory location, final GameData data, final PlayerID player, final boolean tFirst, final boolean ignoreOnlyPlanes, final List<Territory> ignoreTerr) { PlayerID ePlayer = null; final List<PlayerID> qID = getEnemyPlayers(data, player); final HashMap<PlayerID, Float> ePAttackMap = new HashMap<>(); final Iterator<PlayerID> playerIter = qID.iterator(); if (location == null) { return -1000.0F; } boolean nonTransportsInAttack = false; final boolean onWater = location.isWater(); if (!onWater) { nonTransportsInAttack = true; } final Set<Territory> waterTerr = data.getMap().getNeighbors(location, Matches.TerritoryIsWater); while (playerIter.hasNext()) { float seaStrength = 0.0F; float firstStrength = 0.0F; float secondStrength = 0.0F; float blitzStrength = 0.0F; float strength = 0.0F; float airStrength = 0.0F; ePlayer = playerIter.next(); final CompositeMatch<Unit> enemyPlane = new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.unitIsOwnedBy(ePlayer), Matches.UnitCanMove); final CompositeMatch<Unit> enemyTransport = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(ePlayer), Matches.UnitIsSea, Matches.UnitIsTransport, Matches.UnitCanMove); final CompositeMatch<Unit> enemyShip = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(ePlayer), Matches.UnitIsSea, Matches.UnitCanMove); final CompositeMatch<Unit> enemyTransportable = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(ePlayer), Matches.UnitCanBeTransported, Matches.UnitIsNotAA, Matches.UnitCanMove); final CompositeMatch<Unit> aTransport = new CompositeMatchAnd<>(Matches.UnitIsSea, Matches.UnitIsTransport, Matches.UnitCanMove); final List<Territory> eFTerrs = findUnitTerr(data, enemyPlane); int maxFighterDistance = 0; int maxBomberDistance = 0; // should change this to read production frontier and tech // reality is 99% of time units considered will have full move. // and likely player will have at least 1 max move plane. for (final Territory eFTerr : eFTerrs) { final List<Unit> eFUnits = eFTerr.getUnits().getMatches(enemyPlane); maxFighterDistance = Math.max(maxFighterDistance, MoveValidator.getMaxMovement(eFUnits)); } // must be able to land...we will miss fighters who have a Carrier that can reach same sea zone...C'est la vie maxFighterDistance--; if (maxFighterDistance < 0) { maxFighterDistance = 0; } // must be able to land...won't miss anything here...unless special bombers that can land on carrier per above maxBomberDistance--; if (maxBomberDistance < 0) { maxBomberDistance = 0; } final List<Territory> eTTerrs = findUnitTerr(data, aTransport); int maxTransportDistance = 0; for (final Territory eTTerr : eTTerrs) { final List<Unit> eTUnits = eTTerr.getUnits().getMatches(aTransport); maxTransportDistance = Math.max(maxTransportDistance, MoveValidator.getMaxMovement(eTUnits)); } final List<Unit> alreadyLoaded = new ArrayList<>(); final List<Route> blitzTerrRoutes = new ArrayList<>(); final List<Territory> checked = new ArrayList<>(); final List<Unit> enemyWaterUnits = new ArrayList<>(); for (final Territory t : data.getMap().getNeighbors(location, onWater ? Matches.TerritoryIsWater : Matches.TerritoryIsLand)) { if (ignoreTerr != null && ignoreTerr.contains(t)) { continue; } final List<Unit> enemies = t.getUnits().getMatches(Matches.unitIsOwnedBy(ePlayer)); enemyWaterUnits.addAll(enemies); firstStrength += strength(enemies, true, onWater, tFirst); checked.add(t); } if (Matches.TerritoryIsLand.match(location)) { blitzStrength = determineEnemyBlitzStrength(location, blitzTerrRoutes, null, data, ePlayer); } else { // get ships attack strength // old assumed fleets won't split up, new lets them. no biggie. // assumes max ship movement is 3. // note, both old and new implementations // allow units to be calculated that are in // territories we have already assaulted // this can be easily changed final HashSet<Integer> ignore = new HashSet<>(); ignore.add(1); final List<Route> r = new ArrayList<>(); final List<Unit> ships = findAttackers(location, 3, ignore, ePlayer, data, enemyShip, Matches.territoryIsBlockedSea(ePlayer, data), ignoreTerr, r, true); secondStrength = strength(ships, true, true, tFirst); enemyWaterUnits.addAll(ships); } final List<Unit> attackPlanes = findPlaneAttackersThatCanLand(location, maxFighterDistance, ePlayer, data, ignoreTerr, checked); airStrength += allairstrength(attackPlanes, true); if (Matches.territoryHasWaterNeighbor(data).match(location) && Matches.TerritoryIsLand.match(location)) { for (final Territory t4 : data.getMap().getNeighbors(location, maxTransportDistance)) { if (!t4.isWater()) { continue; } boolean transportsCounted = false; final Iterator<Territory> iterTerr = waterTerr.iterator(); while (!transportsCounted && iterTerr.hasNext()) { final Territory waterCheck = iterTerr.next(); if (ePlayer == null) { continue; } final List<Unit> transports = t4.getUnits().getMatches(enemyTransport); if (transports.isEmpty()) { continue; } if (!t4.equals(waterCheck)) { final Route seaRoute = getMaxSeaRoute(data, t4, waterCheck, ePlayer, true, maxTransportDistance); if (seaRoute == null || seaRoute.getEnd() == null || seaRoute.getEnd() != waterCheck) { continue; } } final List<Unit> loadedUnits = new ArrayList<>(); int availInf = 0; int availOther = 0; for (final Unit xTrans : transports) { final Collection<Unit> thisTransUnits = TransportTracker.transporting(xTrans); if (thisTransUnits == null) { availInf += 2; availOther += 1; continue; } else { int Inf = 2; int Other = 1; for (final Unit checkUnit : thisTransUnits) { if (Matches.UnitIsInfantry.match(checkUnit)) { Inf--; } if (Matches.UnitIsNotInfantry.match(checkUnit)) { Inf--; Other--; } loadedUnits.add(checkUnit); } availInf += Inf; availOther += Other; } } final Set<Territory> transNeighbors = data.getMap().getNeighbors(t4, Matches.isTerritoryAllied(ePlayer, data)); for (final Territory xN : transNeighbors) { final List<Unit> aTransUnits = xN.getUnits().getMatches(enemyTransportable); aTransUnits.removeAll(alreadyLoaded); final List<Unit> availTransUnits = sortTransportUnits(aTransUnits); for (final Unit aTUnit : availTransUnits) { if (availInf > 0 && Matches.UnitIsInfantry.match(aTUnit)) { availInf--; loadedUnits.add(aTUnit); alreadyLoaded.add(aTUnit); } if (availInf > 0 && availOther > 0 && Matches.UnitIsNotInfantry.match(aTUnit)) { availInf--; availOther--; loadedUnits.add(aTUnit); alreadyLoaded.add(aTUnit); } } } seaStrength += strength(loadedUnits, true, false, tFirst); transportsCounted = true; } } } strength = seaStrength + blitzStrength + firstStrength + secondStrength; if (!ignoreOnlyPlanes || strength > 0.0F) { strength += airStrength; } if (onWater) { final Iterator<Unit> eWaterIter = enemyWaterUnits.iterator(); while (eWaterIter.hasNext() && !nonTransportsInAttack) { if (Matches.UnitIsNotTransport.match(eWaterIter.next())) { nonTransportsInAttack = true; } } } if (!nonTransportsInAttack) { strength = 0.0F; } ePAttackMap.put(ePlayer, strength); } float maxStrength = 0.0F; for (final PlayerID xP : qID) { if (ePAttackMap.get(xP) > maxStrength) { ePlayer = xP; maxStrength = ePAttackMap.get(xP); } } for (final PlayerID xP : qID) { if (ePlayer != xP) { // give 40% of other players...this is will affect a lot of decisions by AI maxStrength += ePAttackMap.get(xP) * 0.40F; } } return maxStrength; } /** * Get a quick and dirty estimate of the strength of some units in a battle. * * @param units - the units to measure * @param attacking - are the units on attack or defense * @param sea - calculate the strength of the units in a sea or land battle? */ private static float strength(final Collection<Unit> units, final boolean attacking, final boolean sea, final boolean transportsFirst) { float strength = 0.0F; if (units.isEmpty()) { return strength; } if (attacking && Match.noneMatch(units, Matches.unitHasAttackValueOfAtLeast(1))) { return strength; } else if (!attacking && Match.noneMatch(units, Matches.unitHasDefendValueOfAtLeast(1))) { return strength; } for (final Unit u : units) { final UnitAttachment unitAttachment = UnitAttachment.get(u.getType()); if (unitAttachment.getIsInfrastructure()) { continue; } else if (unitAttachment.getIsSea() == sea) { final int unitAttack = unitAttachment.getAttack(u.getOwner()); // BB = 6.0; AC=2.0/4.0; SUB=3.0; DS=4.0; TR=0.50/2.0; F=4.0/5.0; B=5.0/2.0; // played with this value a good bit strength += 1.00F; if (attacking) { strength += unitAttack * unitAttachment.getHitPoints(); } else { strength += unitAttachment.getDefense(u.getOwner()) * unitAttachment.getHitPoints(); } if (attacking) { if (unitAttack == 0) { strength -= 0.50F; } } if (unitAttack == 0 && unitAttachment.getTransportCapacity() > 0 && !transportsFirst) { // only allow transport to have 0.35 on defense; none on attack strength -= 0.50F; } } else if (unitAttachment.getIsAir() == sea) { strength += 1.00F; if (attacking) { strength += unitAttachment.getAttack(u.getOwner()) * unitAttachment.getAttackRolls(u.getOwner()); } else { strength += unitAttachment.getDefense(u.getOwner()); } } } if (attacking && !sea) { final int art = Match.countMatches(units, Matches.UnitIsArtillery); final int artSupport = Match.countMatches(units, Matches.UnitIsArtillerySupportable); strength += Math.min(art, artSupport); } return strength; } /** * Returns a list of all enemy players. */ private static List<PlayerID> getEnemyPlayers(final GameData data, final PlayerID player) { final List<PlayerID> enemyPlayers = new ArrayList<>(); for (final PlayerID players : data.getPlayerList().getPlayers()) { if (!data.getRelationshipTracker().isAllied(player, players)) { enemyPlayers.add(players); } } return enemyPlayers; } /** * Determine the enemy potential for blitzing a territory - all enemies are combined. * * @param blitzHere * - Territory expecting to be blitzed * @param blitzTerr * - Territory which is being blitzed through (not guaranteed to be all possible route territories!) * @param ePlayer * - the enemy Player * @return actual strength of enemy units (armor) */ private static float determineEnemyBlitzStrength(final Territory blitzHere, final List<Route> blitzTerrRoutes, final List<Territory> blockTerr, final GameData data, final PlayerID ePlayer) { final HashSet<Integer> ignore = new HashSet<>(); ignore.add(1); final CompositeMatch<Unit> blitzUnit = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(ePlayer), Matches.UnitCanBlitz, Matches.UnitCanMove); final CompositeMatch<Territory> validBlitzRoute = new CompositeMatchAnd<>( Matches.territoryHasNoEnemyUnits(ePlayer, data), Matches.TerritoryIsNotImpassableToLandUnits(ePlayer, data)); final List<Route> routes = new ArrayList<>(); final List<Unit> blitzUnits = findAttackers(blitzHere, 2, ignore, ePlayer, data, blitzUnit, validBlitzRoute, blockTerr, routes, false); for (final Route r : routes) { if (r.numberOfSteps() == 2) { blitzTerrRoutes.add(r); } } return strength(blitzUnits, true, false, true); } private static List<Unit> findAttackers(final Territory start, final int maxDistance, final HashSet<Integer> ignoreDistance, final PlayerID player, final GameData data, final Match<Unit> unitCondition, final Match<Territory> routeCondition, final List<Territory> blocked, final List<Route> routes, final boolean sea) { final IntegerMap<Territory> distance = new IntegerMap<>(); final Map<Territory, Territory> visited = new HashMap<>(); final List<Unit> units = new ArrayList<>(); final Queue<Territory> q = new LinkedList<>(); q.add(start); Territory current = null; distance.put(start, 0); visited.put(start, null); while (!q.isEmpty()) { current = q.remove(); if (distance.getInt(current) == maxDistance) { break; } for (final Territory neighbor : data.getMap().getNeighbors(current)) { if (!distance.keySet().contains(neighbor)) { if (!neighbor.getUnits().someMatch(unitCondition)) { if (!routeCondition.match(neighbor)) { continue; } } if (sea) { final Route r = new Route(); r.setStart(neighbor); r.add(current); if (MoveValidator.validateCanal(r, null, player, data) != null) { continue; } } distance.put(neighbor, distance.getInt(current) + 1); visited.put(neighbor, current); if (blocked != null && blocked.contains(neighbor)) { continue; } q.add(neighbor); final int dist = distance.getInt(neighbor); if (ignoreDistance.contains(dist)) { continue; } for (final Unit u : neighbor.getUnits()) { if (unitCondition.match(u) && Matches.UnitHasEnoughMovementForRoutes(routes).match(u)) { units.add(u); } } } } } // pain in the ass, should just redesign stop blitz attack for (final Territory t : visited.keySet()) { final Route r = new Route(); Territory t2 = t; r.setStart(t); while (t2 != null) { t2 = visited.get(t2); if (t2 != null) { r.add(t2); } } routes.add(r); } return units; } /** * does not count planes already in the starting territory. */ private static List<Unit> findPlaneAttackersThatCanLand(final Territory start, final int maxDistance, final PlayerID player, final GameData data, final List<Territory> ignore, final List<Territory> checked) { if (checked.isEmpty()) { return new ArrayList<>(); } final IntegerMap<Territory> distance = new IntegerMap<>(); final IntegerMap<Unit> unitDistance = new IntegerMap<>(); final List<Unit> units = new ArrayList<>(); final Queue<Territory> q = new LinkedList<>(); Territory lz = null; Territory ac = null; final CompositeMatch<Unit> enemyPlane = new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.unitIsOwnedBy(player), Matches.UnitCanMove); final CompositeMatch<Unit> enemyCarrier = new CompositeMatchAnd<>(Matches.UnitIsCarrier, Matches.unitIsOwnedBy(player), Matches.UnitCanMove); q.add(start); Territory current = null; distance.put(start, 0); while (!q.isEmpty()) { current = q.remove(); if (distance.getInt(current) == maxDistance) { break; } for (final Territory neighbor : data.getMap().getNeighbors(current, TerritoryIsNotImpassableToAirUnits())) { if (!distance.keySet().contains(neighbor)) { q.add(neighbor); distance.put(neighbor, distance.getInt(current) + 1); if (lz == null && Matches.isTerritoryAllied(player, data).match(neighbor) && !neighbor.isWater()) { lz = neighbor; } if ((ignore != null && ignore.contains(neighbor)) || (checked != null && checked.contains(neighbor))) { for (final Unit u : neighbor.getUnits()) { if (ac == null && enemyCarrier.match(u)) { ac = neighbor; } } } else { for (final Unit u : neighbor.getUnits()) { if (ac == null && enemyCarrier.match(u)) { ac = neighbor; } if (enemyPlane.match(u)) { unitDistance.put(u, distance.getInt(neighbor)); } } } } } } for (final Unit u : unitDistance.keySet()) { if (lz != null && Matches.UnitHasEnoughMovementForRoute(checked).match(u)) { units.add(u); } else if (ac != null && Matches.UnitCanLandOnCarrier.match(u) && Matches.UnitHasEnoughMovementForRoute(checked).match(u)) { units.add(u); } } return units; } /** * Determine the strength of a collection of airUnits * Caller should guarantee units are all air. */ private static float allairstrength(final Collection<Unit> units, final boolean attacking) { float airstrength = 0.0F; for (final Unit u : units) { final UnitAttachment unitAttachment = UnitAttachment.get(u.getType()); airstrength += 1.00F; if (attacking) { airstrength += unitAttachment.getAttack(u.getOwner()); } else { airstrength += unitAttachment.getDefense(u.getOwner()); } } return airstrength; } private static Route getMaxSeaRoute(final GameData data, final Territory start, final Territory destination, final PlayerID player, final boolean attacking, final int maxDistance) { // note this does not care if subs are submerged or not // should it? does submerging affect movement of enemies? if (start == null || destination == null || !start.isWater() || !destination.isWater()) { return null; } final CompositeMatch<Unit> ignore = new CompositeMatchAnd<>(Matches.UnitIsInfrastructure.invert(), Matches.alliedUnit(player, data).invert()); final CompositeMatch<Unit> sub = new CompositeMatchAnd<>(Matches.UnitIsSub.invert()); final CompositeMatch<Unit> transport = new CompositeMatchAnd<>(Matches.UnitIsTransport.invert(), Matches.UnitIsLand.invert()); final CompositeMatch<Unit> unitCond = ignore; if (Properties.getIgnoreTransportInMovement(data)) { unitCond.add(transport); } if (Properties.getIgnoreSubInMovement(data)) { unitCond.add(sub); } final CompositeMatch<Territory> routeCond = new CompositeMatchAnd<>(Matches.territoryHasUnitsThatMatch(unitCond).invert(), Matches.TerritoryIsWater); CompositeMatch<Territory> routeCondition; if (attacking) { routeCondition = new CompositeMatchOr<>(Matches.territoryIs(destination), routeCond); } else { routeCondition = routeCond; } Route r = data.getMap().getRoute(start, destination, routeCondition); if (r == null || r.getEnd() == null) { return null; } // cheating because can't do stepwise calculation with canals // shouldn't be a huge problem // if we fail due to canal, then don't go near any enemy canals if (MoveValidator.validateCanal(r, null, player, data) != null) { r = data.getMap().getRoute(start, destination, new CompositeMatchAnd<>(routeCondition, Matches.territoryHasNonAllowedCanal(player, null, data).invert())); } if (r == null || r.getEnd() == null) { return null; } final int rDist = r.numberOfSteps(); Route route2 = new Route(); if (rDist <= maxDistance) { route2 = r; } else { route2.setStart(start); for (int i = 1; i <= maxDistance; i++) { route2.add(r.getAllTerritories().get(i)); } } return route2; } /** * All Allied Territories which neighbor a territory * This duplicates getNeighbors(check, Matches.isTerritoryAllied(player, data)) */ private static List<Territory> getNeighboringLandTerritories(final GameData data, final PlayerID player, final Territory check) { final ArrayList<Territory> rVal = new ArrayList<>(); final List<Territory> checkList = getExactNeighbors(check, 1, data, false); for (final Territory t : checkList) { if (Matches.isTerritoryAllied(player, data).match(t) && Matches.TerritoryIsNotImpassableToLandUnits(player, data).match(t)) { rVal.add(t); } } return rVal; } /** * Gets the neighbors which are exactly a certain # of territories away (distance) * Removes the inner circle neighbors * neutral - whether to include neutral countries. */ private static List<Territory> getExactNeighbors(final Territory territory, final int distance, final GameData data, final boolean neutral) { // old functionality retained, i.e. no route condition is imposed. // feel free to change, if you are confortable all calls to this function conform. final CompositeMatch<Territory> endCond = new CompositeMatchAnd<>(Matches.TerritoryIsImpassable.invert()); if (!neutral || Properties.getNeutralsImpassable(data)) { endCond.add(Matches.TerritoryIsNeutralButNotWater.invert()); } return findFontier(territory, endCond, Match.getAlwaysMatch(), distance, data); } /** * Finds list of territories at exactly distance from the start. * * @param endCondition * condition that all end points must satisfy * @param routeCondition * condition that all traversed internal territories must satisy */ private static List<Territory> findFontier(final Territory start, final Match<Territory> endCondition, final Match<Territory> routeCondition, final int distance, final GameData data) { final Match<Territory> canGo = new CompositeMatchOr<>(endCondition, routeCondition); final IntegerMap<Territory> visited = new IntegerMap<>(); final Queue<Territory> q = new LinkedList<>(); final List<Territory> frontier = new ArrayList<>(); q.addAll(data.getMap().getNeighbors(start, canGo)); Territory current = null; visited.put(start, 0); for (final Territory t : q) { visited.put(t, 1); if (1 == distance && endCondition.match(t)) { frontier.add(t); } } while (!q.isEmpty()) { current = q.remove(); if (visited.getInt(current) == distance) { break; } else { for (final Territory neighbor : data.getMap().getNeighbors(current, canGo)) { if (!visited.keySet().contains(neighbor)) { q.add(neighbor); final int dist = visited.getInt(current) + 1; visited.put(neighbor, dist); if (dist == distance && endCondition.match(neighbor)) { frontier.add(neighbor); } } } } } return frontier; } /** * Return Territories containing any unit depending on unitCondition * Differs from findCertainShips because it doesn't require the units be owned. */ private static List<Territory> findUnitTerr(final GameData data, final Match<Unit> unitCondition) { // Return territories containing a certain unit or set of Units final CompositeMatch<Unit> limitShips = new CompositeMatchAnd<>(unitCondition); final List<Territory> shipTerr = new ArrayList<>(); final Collection<Territory> tNeighbors = data.getMap().getTerritories(); for (final Territory t2 : tNeighbors) { if (t2.getUnits().someMatch(limitShips)) { shipTerr.add(t2); } } return shipTerr; } /** * Interleave infantry and artillery/armor for loading on transports. */ private static List<Unit> sortTransportUnits(final List<Unit> transUnits) { final List<Unit> sorted = new ArrayList<>(); final List<Unit> infantry = new ArrayList<>(); final List<Unit> artillery = new ArrayList<>(); final List<Unit> armor = new ArrayList<>(); final List<Unit> others = new ArrayList<>(); for (final Unit x : transUnits) { if (Matches.UnitIsArtillerySupportable.match(x)) { infantry.add(x); } else if (Matches.UnitIsArtillery.match(x)) { artillery.add(x); } else if (Matches.UnitCanBlitz.match(x)) { armor.add(x); } else { others.add(x); } } int artilleryCount = artillery.size(); int armorCount = armor.size(); int othersCount = others.size(); for (final Unit anInfantry : infantry) { sorted.add(anInfantry); // this should be based on combined attack and defense powers, not on attachments like blitz if (armorCount > 0) { sorted.add(armor.get(armorCount - 1)); armorCount--; } else if (artilleryCount > 0) { sorted.add(artillery.get(artilleryCount - 1)); artilleryCount--; } else if (othersCount > 0) { sorted.add(others.get(othersCount - 1)); othersCount--; } } if (artilleryCount > 0) { for (int j2 = 0; j2 < artilleryCount; j2++) { sorted.add(artillery.get(j2)); } } if (othersCount > 0) { for (int j4 = 0; j4 < othersCount; j4++) { sorted.add(others.get(j4)); } } if (armorCount > 0) { for (int j3 = 0; j3 < armorCount; j3++) { sorted.add(armor.get(j3)); } } return sorted; } private static Match<Territory> TerritoryIsNotImpassableToAirUnits() { return new InverseMatch<>(TerritoryIsImpassableToAirUnits()); } /** * Assumes that water is passable to air units always. */ private static Match<Territory> TerritoryIsImpassableToAirUnits() { return new Match<Territory>() { @Override public boolean match(final Territory t) { if (Matches.TerritoryIsLand.match(t) && Matches.TerritoryIsImpassable.match(t)) { return true; } return false; } }; } }