package games.strategy.triplea.ai.proAI; 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.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.NamedAttachable; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.ProductionRule; import games.strategy.engine.data.Resource; 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.ai.proAI.logging.ProLogger; import games.strategy.triplea.ai.proAI.util.ProUtils; 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.TransportTracker; import games.strategy.triplea.delegate.dataObjects.PlaceableUnits; import games.strategy.triplea.delegate.remote.IAbstractPlaceDelegate; import games.strategy.triplea.delegate.remote.IPurchaseDelegate; 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 bid AI. */ public class ProBidAI { private static final int PURCHASE_LOOP_MAX_TIME_MILLIS = 150 * 1000; private GameData data; public void bid(int PUsToSpend, final IPurchaseDelegate purchaseDelegate, final GameData data, final PlayerID player) { ProLogger.info("Starting bid purchase phase"); // Current data at the start of combat move this.data = data; if (PUsToSpend == 0 && player.getResources().getQuantity(data.getResourceList().getResource(Constants.PUS)) == 0) { return; } // breakdown Rules by type and cost int highPrice = 0; final List<ProductionRule> rules = player.getProductionFrontier().getRules(); final IntegerMap<ProductionRule> purchase = new IntegerMap<>(); final List<ProductionRule> landProductionRules = new ArrayList<>(); final List<ProductionRule> airProductionRules = new ArrayList<>(); final List<ProductionRule> seaProductionRules = new ArrayList<>(); final List<ProductionRule> transportProductionRules = new ArrayList<>(); final List<ProductionRule> subProductionRules = new ArrayList<>(); final IntegerMap<ProductionRule> bestAttack = new IntegerMap<>(); final IntegerMap<ProductionRule> bestDefense = new IntegerMap<>(); final IntegerMap<ProductionRule> bestTransport = new IntegerMap<>(); final IntegerMap<ProductionRule> bestMaxUnits = new IntegerMap<>(); final IntegerMap<ProductionRule> bestMobileAttack = new IntegerMap<>(); ProductionRule carrierRule = null; ProductionRule fighterRule = null; int carrierFighterLimit = 0; int maxFighterAttack = 0; float averageSeaMove = 0; final Resource pus = data.getResourceList().getResource(Constants.PUS); final boolean isAmphib = isAmphibAttack(player, true); for (final ProductionRule ruleCheck : rules) { final int costCheck = ruleCheck.getCosts().getInt(pus); final NamedAttachable resourceOrUnit = ruleCheck.getResults().keySet().iterator().next(); if (!(resourceOrUnit instanceof UnitType)) { continue; } final UnitType x = (UnitType) resourceOrUnit; // Remove from consideration any unit with Zero Movement if (UnitAttachment.get(x).getMovement(player) < 1 && !(UnitAttachment.get(x).getCanProduceUnits())) { continue; } // Remove from consideration any unit with Zero defense, or 3 or more attack/defense than defense/attack, that is // not a // transport/factory/aa unit if (((UnitAttachment.get(x).getAttack(player) - UnitAttachment.get(x).getDefense(player) >= 3 || UnitAttachment.get(x).getDefense(player) - UnitAttachment.get(x).getAttack(player) >= 3) || UnitAttachment.get(x).getDefense(player) < 1) && !(UnitAttachment.get(x).getCanProduceUnits() || (UnitAttachment.get(x).getTransportCapacity() > 0 && Matches.UnitTypeIsSea.match(x)))) { // maybe the map only has weird units. make sure there is at least one of each type before we decide not to use // it (we are relying // on the fact that map makers generally put specialty units AFTER useful units in their production lists [ie: // bombers listed after // fighters, mortars after artillery, etc.]) if (Matches.UnitTypeIsAir.match(x) && !airProductionRules.isEmpty()) { continue; } if (Matches.UnitTypeIsSea.match(x) && !seaProductionRules.isEmpty()) { continue; } if (!Matches.UnitTypeCanProduceUnits.match(x) && !landProductionRules.isEmpty() && !Matches.UnitTypeIsAir.match(x) && !Matches.UnitTypeIsSea.match(x)) { continue; } } // Remove from consideration any unit which has maxBuiltPerPlayer if (Matches.UnitTypeHasMaxBuildRestrictions.match(x)) { continue; } // Remove from consideration any unit which has consumesUnits if (Matches.UnitTypeConsumesUnitsOnCreation.match(x)) { continue; } if (Matches.UnitTypeIsAir.match(x)) { airProductionRules.add(ruleCheck); } else if (Matches.UnitTypeIsSea.match(x)) { seaProductionRules.add(ruleCheck); averageSeaMove += UnitAttachment.get(x).getMovement(player); } else if (!Matches.UnitTypeCanProduceUnits.match(x)) { if (costCheck > highPrice) { highPrice = costCheck; } landProductionRules.add(ruleCheck); } if (Matches.UnitTypeCanTransport.match(x) && Matches.UnitTypeIsSea.match(x)) { // might be more than 1 transport rule... use ones that can hold at least "2" capacity (we should instead check // for median transport // cost, and then add all those at or above that capacity) if (UnitAttachment.get(x).getTransportCapacity() > 1) { transportProductionRules.add(ruleCheck); } } if (Matches.UnitTypeIsSub.match(x)) { subProductionRules.add(ruleCheck); } // might be more than 1 carrier rule...use the one which will hold the most fighters if (Matches.UnitTypeIsCarrier.match(x)) { final int thisFighterLimit = UnitAttachment.get(x).getCarrierCapacity(); if (thisFighterLimit >= carrierFighterLimit) { carrierRule = ruleCheck; carrierFighterLimit = thisFighterLimit; } } // might be more than 1 fighter...use the one with the best attack if (Matches.UnitTypeCanLandOnCarrier.match(x)) { final int thisFighterAttack = UnitAttachment.get(x).getAttack(player); if (thisFighterAttack > maxFighterAttack) { fighterRule = ruleCheck; maxFighterAttack = thisFighterAttack; } } } // most sea units move at least 2 movement, so remove any sea units with 1 movement (dumb t-boats) (some maps like // 270BC have mostly 1 movement sea units, so we must be sure not to remove those) if (averageSeaMove / seaProductionRules.size() >= 1.8) { final List<ProductionRule> seaProductionRulesCopy = new ArrayList<>(seaProductionRules); for (final ProductionRule seaRule : seaProductionRulesCopy) { final NamedAttachable resourceOrUnit = seaRule.getResults().keySet().iterator().next(); if (!(resourceOrUnit instanceof UnitType)) { continue; } final UnitType x = (UnitType) resourceOrUnit; if (UnitAttachment.get(x).getMovement(player) < 2) { seaProductionRules.remove(seaRule); } } } if (subProductionRules.size() > 0 && seaProductionRules.size() > 0) { // remove submarines from consideration, unless we are mostly subs if (subProductionRules.size() / seaProductionRules.size() < 0.3) { seaProductionRules.removeAll(subProductionRules); } } int buyLimit = PUsToSpend / 3; if (buyLimit == 0) { buyLimit = 1; } boolean landPurchase = true; boolean goTransports = false; // boolean alreadyBought = false; final List<Territory> enemyTerritoryBorderingOurTerrs = getNeighboringEnemyLandTerritories(data, player); if (enemyTerritoryBorderingOurTerrs.isEmpty()) { landPurchase = false; } if (Math.random() > 0.25) { seaProductionRules.removeAll(subProductionRules); } if (PUsToSpend < 25) { if ((!isAmphib || Math.random() < 0.15) && landPurchase) { findPurchaseMix(bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, landProductionRules, PUsToSpend, buyLimit, data, player, 2); } else { landPurchase = false; buyLimit = PUsToSpend / 5; // assume a larger threshhold if (Math.random() > 0.40) { findPurchaseMix(bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, seaProductionRules, PUsToSpend, buyLimit, data, player, 2); } else { goTransports = true; } } } else if ((!isAmphib || Math.random() < 0.15) && landPurchase) { if (Math.random() > 0.80) { findPurchaseMix(bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, landProductionRules, PUsToSpend, buyLimit, data, player, 2); } } else if (Math.random() < 0.35) { // force a carrier purchase if enough available $$ for it and at least 1 fighter if (Math.random() > 0.55 && carrierRule != null && fighterRule != null) { final int cost = carrierRule.getCosts().getInt(pus); final int fighterCost = fighterRule.getCosts().getInt(pus); if ((cost + fighterCost) <= PUsToSpend) { purchase.add(carrierRule, 1); purchase.add(fighterRule, 1); carrierFighterLimit--; PUsToSpend -= (cost + fighterCost); while ((PUsToSpend >= fighterCost) && carrierFighterLimit > 0) { // max out the carrier purchase.add(fighterRule, 1); carrierFighterLimit--; PUsToSpend -= fighterCost; } } } final int airPUs = PUsToSpend / 6; findPurchaseMix(bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, airProductionRules, airPUs, buyLimit, data, player, 2); final boolean buyAttack = Math.random() > 0.50; for (final ProductionRule rule1 : airProductionRules) { int buyThese = bestAttack.getInt(rule1); final int cost = rule1.getCosts().getInt(pus); if (!buyAttack) { buyThese = bestDefense.getInt(rule1); } PUsToSpend -= cost * buyThese; while (PUsToSpend < 0 && buyThese > 0) { buyThese--; PUsToSpend += cost; } if (buyThese > 0) { purchase.add(rule1, buyThese); } } final int landPUs = PUsToSpend; buyLimit = landPUs / 3; bestAttack.clear(); bestDefense.clear(); bestMaxUnits.clear(); bestMobileAttack.clear(); findPurchaseMix(bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, landProductionRules, landPUs, buyLimit, data, player, 2); } else { landPurchase = false; buyLimit = PUsToSpend / 8; // assume higher end purchase seaProductionRules.addAll(airProductionRules); if (Math.random() > 0.45) { findPurchaseMix(bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, seaProductionRules, PUsToSpend, buyLimit, data, player, 2); } else { goTransports = true; } } final List<ProductionRule> processRules = new ArrayList<>(); if (landPurchase) { processRules.addAll(landProductionRules); } else { if (goTransports) { processRules.addAll(transportProductionRules); } else { processRules.addAll(seaProductionRules); } } final boolean buyAttack = Math.random() > 0.25; int buyThese = 0; for (final ProductionRule rule1 : processRules) { final int cost = rule1.getCosts().getInt(pus); if (goTransports) { buyThese = PUsToSpend / cost; } else if (buyAttack) { buyThese = bestAttack.getInt(rule1); } else if (Math.random() <= 0.25) { buyThese = bestDefense.getInt(rule1); } else { buyThese = bestMaxUnits.getInt(rule1); } PUsToSpend -= cost * buyThese; while (buyThese > 0 && PUsToSpend < 0) { buyThese--; PUsToSpend += cost; } if (buyThese > 0) { purchase.add(rule1, buyThese); } } bestAttack.clear(); bestDefense.clear(); bestTransport.clear(); bestMaxUnits.clear(); bestMobileAttack.clear(); if (PUsToSpend > 0) { // verify a run through the land units buyLimit = PUsToSpend / 2; findPurchaseMix(bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, landProductionRules, PUsToSpend, buyLimit, data, player, 2); for (final ProductionRule rule2 : landProductionRules) { final int cost = rule2.getCosts().getInt(pus); buyThese = bestDefense.getInt(rule2); PUsToSpend -= cost * buyThese; while (buyThese > 0 && PUsToSpend < 0) { buyThese--; PUsToSpend += cost; } if (buyThese > 0) { purchase.add(rule2, buyThese); } } } purchaseDelegate.purchase(purchase); } private boolean isAmphibAttack(final PlayerID player, final boolean requireWaterFactory) { final Territory capitol = TerritoryAttachment.getFirstOwnedCapitalOrFirstUnownedCapital(player, data); if (capitol == null || !capitol.getOwner().equals(player)) { return false; } if (requireWaterFactory) { final List<Territory> factories = findTersWithUnitsMatching(data, player, Matches.UnitCanProduceUnits); final List<Territory> waterFactories = stripLandLockedTerr(data, factories); if (waterFactories.isEmpty()) { return false; } } // find a land route to an enemy territory from our capitol boolean amphibPlayer = !hasLandRouteToEnemyOwnedCapitol(capitol, player, data); int totProduction = 0; int allProduction = 0; if (amphibPlayer) { final List<Territory> allFactories = findTersWithUnitsMatching(data, player, Matches.UnitCanProduceUnits); // allFactories.remove(capitol); for (final Territory checkFactory : allFactories) { final boolean isLandRoute = hasLandRouteToEnemyOwnedCapitol(checkFactory, player, data); final int factProduction = TripleAUnit.getProductionPotentialOfTerritory(checkFactory.getUnits().getUnits(), checkFactory, player, data, false, true); allProduction += factProduction; if (isLandRoute) { totProduction += factProduction; } } } // if the land based production is greater than 2/5 (used to be 1/3) of all factory production, turn off amphib // works better on NWO where Brits start with factories in North Africa amphibPlayer = amphibPlayer ? (totProduction * 5 < allProduction * 2) : false; return amphibPlayer; } // TODO: Rewrite this as its from the old AI public void bidPlace(final IAbstractPlaceDelegate placeDelegate, final GameData data, final PlayerID player) { ProLogger.info("Starting bid place phase"); // if we have purchased a factory, it will be a priority for placing units // should place most expensive on it // need to be able to handle AA purchase if (player.getUnits().isEmpty()) { return; } final Collection<Territory> impassableTerrs = new ArrayList<>(); for (final Territory t : data.getMap().getTerritories()) { if (Matches.TerritoryIsPassableAndNotRestricted(player, data).invert().match(t) && Matches.TerritoryIsLand.match(t)) { impassableTerrs.add(t); } } final boolean tFirst = !games.strategy.triplea.Properties.getTransportCasualtiesRestricted(data); final CompositeMatch<Unit> ownedUnit = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player)); final CompositeMatch<Unit> attackUnit = new CompositeMatchAnd<>(Matches.UnitIsSea, Matches.UnitIsNotTransport); final CompositeMatch<Unit> enemyUnit = new CompositeMatchAnd<>(Matches.enemyUnit(player, data)); final CompositeMatch<Unit> enemyAttackUnit = new CompositeMatchAnd<>(attackUnit, enemyUnit); // CompositeMatch<Unit> enemyTransUnit = new CompositeMatchAnd<Unit>(transUnit, enemyUnit); final CompositeMatch<Unit> ourFactory = new CompositeMatchAnd<>(ownedUnit, Matches.UnitCanProduceUnits); // CompositeMatch<Territory> ourLandTerr = new CompositeMatchAnd<Territory>(Matches.isTerritoryOwnedBy(player), // Matches.TerritoryIsLand); final Territory capitol = TerritoryAttachment.getFirstOwnedCapitalOrFirstUnownedCapital(player, data); final List<Territory> factoryTerritories = Match.getMatches(findUnitTerr(data, ourFactory), Matches.isTerritoryOwnedBy(player)); factoryTerritories.removeAll(impassableTerrs); /* * Bid place with following criteria: * 1) Has an enemy Neighbor * 2) Has the largest combination value: * a) enemy Terr * b) our Terr * c) other Terr neighbors to our Terr * d) + 2 for each of these which are victory cities */ final List<Territory> ourFriendlyTerr = new ArrayList<>(); final List<Territory> ourEnemyTerr = new ArrayList<>(); final List<Territory> ourSemiRankedBidTerrs = new ArrayList<>(); final List<Territory> ourTerrs = allOurTerritories(data, player); ourTerrs.remove(capitol); // we'll check the cap last final HashMap<Territory, Float> rankMap = rankTerritories(data, ourFriendlyTerr, ourEnemyTerr, null, player, tFirst, false, true); final List<Territory> ourTerrWithEnemyNeighbors = getTerritoriesWithEnemyNeighbor(data, player, false, false); reorder(ourTerrWithEnemyNeighbors, rankMap, true); // ourFriendlyTerr.retainAll(ourTerrs); if (ourTerrWithEnemyNeighbors.contains(capitol)) { ourTerrWithEnemyNeighbors.remove(capitol); ourTerrWithEnemyNeighbors.add(capitol); // move capitol to the end of the list, if it is touching enemies } Territory bidLandTerr = null; if (ourTerrWithEnemyNeighbors.size() > 0) { bidLandTerr = ourTerrWithEnemyNeighbors.get(0); } if (bidLandTerr == null) { bidLandTerr = capitol; } if (player.getUnits().someMatch(Matches.UnitIsSea)) { Territory bidSeaTerr = null; Territory bidTransTerr = null; // CompositeMatch<Territory> enemyWaterTerr = new CompositeMatchAnd<Territory>(Matches.TerritoryIsWater, // Matches.territoryHasEnemyUnits(player, data)); final CompositeMatch<Territory> waterFactoryWaterTerr = new CompositeMatchAnd<>(Matches.TerritoryIsWater, Matches.territoryHasOwnedNeighborWithOwnedUnitMatching(data, player, Matches.UnitCanProduceUnits)); final List<Territory> enemySeaTerr = findUnitTerr(data, enemyAttackUnit); final List<Territory> isWaterTerr = onlyWaterTerr(enemySeaTerr); enemySeaTerr.retainAll(isWaterTerr); Territory maxEnemySeaTerr = null; int maxUnits = 0; for (final Territory seaTerr : enemySeaTerr) { final int unitCount = seaTerr.getUnits().countMatches(enemyAttackUnit); if (unitCount > maxUnits) { maxUnits = unitCount; maxEnemySeaTerr = seaTerr; } } final Route seaRoute = findNearest(maxEnemySeaTerr, waterFactoryWaterTerr, Matches.TerritoryIsWater, data); if (seaRoute != null) { final Territory checkSeaTerr = seaRoute.getEnd(); if (checkSeaTerr != null) { final float seaStrength = getStrengthOfPotentialAttackers(checkSeaTerr, data, player, tFirst, false, null); final float aStrength = strength(checkSeaTerr.getUnits().getUnits(), false, true, tFirst); final float bStrength = strength(player.getUnits().getMatches(attackUnit), false, true, tFirst); final float totStrength = aStrength + bStrength; if (totStrength > 0.9F * seaStrength) { bidSeaTerr = checkSeaTerr; } } } for (final Territory factCheck : factoryTerritories) { if (bidSeaTerr == null) { bidSeaTerr = findASeaTerritoryToPlaceOn(factCheck, data, player, tFirst); } if (bidTransTerr == null) { bidTransTerr = findASeaTerritoryToPlaceOn(factCheck, data, player, tFirst); } } placeSeaUnits(true, data, bidSeaTerr, bidSeaTerr, placeDelegate, player); } if (player.getUnits().someMatch(Matches.UnitIsNotSea)) { ourSemiRankedBidTerrs.addAll(ourTerrWithEnemyNeighbors); ourTerrs.removeAll(ourTerrWithEnemyNeighbors); Collections.shuffle(ourTerrs); ourSemiRankedBidTerrs.addAll(ourTerrs); // need to remove places like greenland, iceland and west indies that have no route to the enemy, but somehow keep // places like borneo, // gibralter, etc. for (final Territory noRouteTerr : ourTerrs) { // do not place bids on areas that have no direct land access to an enemy, unless the value is 3 or greater if (distanceToEnemy(noRouteTerr, data, player, false) < 1 && TerritoryAttachment.getProduction(noRouteTerr) < 3) { ourSemiRankedBidTerrs.remove(noRouteTerr); } } final List<Territory> isWaterTerr = onlyWaterTerr(ourSemiRankedBidTerrs); ourSemiRankedBidTerrs.removeAll(isWaterTerr); ourSemiRankedBidTerrs.removeAll(impassableTerrs); // This will bid a max of 5 units to ALL territories except for the capitol. The capitol gets units last, and gets // unlimited units // (veqryn) final int maxBidPerTerritory = 5; int bidCycle = 0; while (!(player.getUnits().isEmpty()) && bidCycle < maxBidPerTerritory) { for (int i = 0; i <= ourSemiRankedBidTerrs.size() - 1; i++) { bidLandTerr = ourSemiRankedBidTerrs.get(i); placeAllWeCanOn(true, data, null, bidLandTerr, placeDelegate, player); } bidCycle++; } if (!player.getUnits().isEmpty()) { placeAllWeCanOn(true, data, null, capitol, placeDelegate, player); } } } private void placeSeaUnits(final boolean bid, final GameData data, final Territory seaPlaceAttack, final Territory seaPlaceTrans, final IAbstractPlaceDelegate placeDelegate, final PlayerID player) { final CompositeMatch<Unit> attackUnit = new CompositeMatchAnd<>(Matches.UnitIsSea, Matches.UnitIsNotTransport); final List<Unit> seaUnits = player.getUnits().getMatches(attackUnit); final List<Unit> transUnits = player.getUnits().getMatches(Matches.UnitIsTransport); final List<Unit> airUnits = player.getUnits().getMatches(Matches.UnitCanLandOnCarrier); final List<Unit> carrierUnits = player.getUnits().getMatches(Matches.UnitIsCarrier); if (carrierUnits.size() > 0 && airUnits.size() > 0 && (Properties.getProduceFightersOnCarriers(data) || Properties.getLHTRCarrierProductionRules(data) || bid)) { int carrierSpace = 0; for (final Unit carrier1 : carrierUnits) { carrierSpace += UnitAttachment.get(carrier1.getType()).getCarrierCapacity(); } final Iterator<Unit> airIter = airUnits.iterator(); while (airIter.hasNext() && carrierSpace > 0) { final Unit airPlane = airIter.next(); seaUnits.add(airPlane); carrierSpace -= UnitAttachment.get(airPlane.getType()).getCarrierCost(); } } if (bid) { if (!seaUnits.isEmpty()) { doPlace(seaPlaceAttack, seaUnits, placeDelegate); } if (!transUnits.isEmpty()) { doPlace(seaPlaceTrans, transUnits, placeDelegate); } return; } if (seaUnits.isEmpty() && transUnits.isEmpty()) { return; } if (seaPlaceAttack == seaPlaceTrans) { seaUnits.addAll(transUnits); transUnits.clear(); } final PlaceableUnits pu = placeDelegate.getPlaceableUnits(seaUnits, seaPlaceAttack); int pLeft = 0; if (pu.getErrorMessage() != null) { return; } if (!seaUnits.isEmpty()) { pLeft = pu.getMaxUnits(); if (pLeft == -1) { pLeft = Integer.MAX_VALUE; } final int numPlace = Math.min(pLeft, seaUnits.size()); pLeft -= numPlace; final Collection<Unit> toPlace = seaUnits.subList(0, numPlace); doPlace(seaPlaceAttack, toPlace, placeDelegate); } if (!transUnits.isEmpty()) { final PlaceableUnits pu2 = placeDelegate.getPlaceableUnits(transUnits, seaPlaceTrans); if (pu2.getErrorMessage() != null) { return; } pLeft = pu2.getMaxUnits(); if (pLeft == -1) { pLeft = Integer.MAX_VALUE; } final int numPlace = Math.min(pLeft, transUnits.size()); final Collection<Unit> toPlace = transUnits.subList(0, numPlace); doPlace(seaPlaceTrans, toPlace, placeDelegate); } } private void placeAllWeCanOn(final boolean bid, final GameData data, final Territory factoryPlace, final Territory placeAt, final IAbstractPlaceDelegate placeDelegate, final PlayerID player) { final CompositeMatch<Unit> landOrAir = new CompositeMatchOr<>(Matches.UnitIsAir, Matches.UnitIsLand); if (factoryPlace != null) { // place a factory? final Collection<Unit> toPlace = new ArrayList<>(player.getUnits().getMatches(Matches.UnitCanProduceUnitsAndIsConstruction)); if (toPlace.size() == 1) { // only 1 may have been purchased...anything greater is wrong doPlace(factoryPlace, toPlace, placeDelegate); return; } else if (toPlace.size() > 1) { return; } } final List<Unit> landUnits = player.getUnits().getMatches(landOrAir); final Territory capitol = TerritoryAttachment.getFirstOwnedCapitalOrFirstUnownedCapital(player, data); final PlaceableUnits pu3 = placeDelegate.getPlaceableUnits(landUnits, placeAt); if (pu3.getErrorMessage() != null) { return; } int placementLeft3 = pu3.getMaxUnits(); if (placementLeft3 == -1) { placementLeft3 = Integer.MAX_VALUE; } // allow placing only 1 unit per territory if a bid, unless it is the capitol (water is handled in placeseaunits) if (bid) { placementLeft3 = 1; } if (bid && (placeAt == capitol)) { placementLeft3 = 1000; } if (!landUnits.isEmpty()) { final int landPlaceCount = Math.min(placementLeft3, landUnits.size()); placementLeft3 -= landPlaceCount; final Collection<Unit> toPlace = landUnits.subList(0, landPlaceCount); doPlace(placeAt, toPlace, placeDelegate); } } private void doPlace(final Territory t, final Collection<Unit> toPlace, final IAbstractPlaceDelegate del) { for (final Unit unit : toPlace) { final List<Unit> unitList = new ArrayList<>(); unitList.add(unit); final String message = del.placeUnits(unitList, t, IAbstractPlaceDelegate.BidMode.BID); if (message != null) { ProLogger.warn(message); ProLogger.warn("Attempt was at: " + t + " with: " + unit); } } ProUtils.pause(); } /** * All the territories that border one of our territories. */ private static List<Territory> getNeighboringEnemyLandTerritories(final GameData data, final PlayerID player) { final ArrayList<Territory> rVal = new ArrayList<>(); for (final Territory t : data.getMap()) { if (Matches.isTerritoryEnemy(player, data).match(t) && Matches.TerritoryIsLand.match(t) && Matches.TerritoryIsNotImpassable.match(t)) { if (!data.getMap().getNeighbors(t, Matches.isTerritoryOwnedBy(player)).isEmpty()) { rVal.add(t); } } } return rVal; } /** * Take the mix of Production Rules and determine the best purchase set for attack, defense or transport * So much more that can be done with this...track units and try to minimize or maximize the # purchased */ private static boolean findPurchaseMix(final IntegerMap<ProductionRule> bestAttack, final IntegerMap<ProductionRule> bestDefense, final IntegerMap<ProductionRule> bestTransport, final IntegerMap<ProductionRule> bestMaxUnits, final IntegerMap<ProductionRule> bestMobileAttack, final List<ProductionRule> rules, final int totPU, final int maxUnits, final GameData data, final PlayerID player, final int fighters) { // Resource key = data.getResourceList().getResource(Constants.PUS); final IntegerMap<String> parameters = new IntegerMap<>(); parameters.put("attack", 0); parameters.put("defense", 0); parameters.put("maxAttack", 0); parameters.put("maxDefense", 0); parameters.put("maxUnitAttack", 0); parameters.put("maxTransAttack", 0); parameters.put("maxMobileAttack", 0); parameters.put("maxTransCost", 100000); parameters.put("maxAttackCost", 100000); parameters.put("maxUnitCount", 0); parameters.put("maxDefenseCost", 100000); parameters.put("maxUnitCost", 100000); parameters.put("totcost", 0); parameters.put("totUnit", 0); parameters.put("totMovement", 0); parameters.put("maxMovement", 0); // never changed parameters.put("maxUnits", maxUnits); // never changed parameters.put("maxCost", totPU); parameters.put("infantry", 0); parameters.put("nonInfantry", 0); final HashMap<ProductionRule, Boolean> infMap = new HashMap<>(); final HashMap<ProductionRule, Boolean> nonInfMap = new HashMap<>(); final HashMap<ProductionRule, Boolean> supportableInfMap = new HashMap<>(); final Iterator<ProductionRule> prodIter = rules.iterator(); final HashMap<ProductionRule, Boolean> transportMap = new HashMap<>(); // int minCost = 10000; // ProductionRule minCostRule = null; while (prodIter.hasNext()) { final ProductionRule rule = prodIter.next(); // initialize with 0 bestAttack.put(rule, 0); bestDefense.put(rule, 0); bestMaxUnits.put(rule, 0); bestTransport.put(rule, 0); final NamedAttachable resourceOrUnit = rule.getResults().keySet().iterator().next(); if (!(resourceOrUnit instanceof UnitType)) { continue; } final UnitType x = (UnitType) resourceOrUnit; supportableInfMap.put(rule, UnitAttachment.get(x).getArtillerySupportable()); transportMap.put(rule, Matches.UnitTypeCanBeTransported.match(x)); infMap.put(rule, Matches.UnitTypeIsInfantry.match(x)); nonInfMap.put(rule, Matches.UnitTypeCanBeTransported.match(x) && Matches.UnitTypeIsInfantry.invert().match(x) && Matches.UnitTypeIsAAforAnything.invert().match(x)); } final int countNum = 1; final int goodLoop = purchaseLoop(parameters, countNum, bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, transportMap, infMap, nonInfMap, supportableInfMap, data, player, fighters); if (goodLoop > 0 && bestAttack.size() > 0 && bestDefense.size() > 0) { return true; } else { return false; } } /** * Recursive routine to determine the bestAttack and bestDefense set of purchase * Expects bestAttack to already be filled with the rules. * * @param parameters * - set of parameters to be used (8 of them) * @param ruleNum * - which rule should the routine use * @param bestAttack * - list of the rules and the number to be purchased (optimized for attack) * @param bestDefense * - list of the rules and the number to be purchased (optimized for defense) * @param bestTransport * - list of the rules and the number to be purchased (optimized for transporting) * @param bestMaxUnits * - list of the rules and the number to be purchased (optimized for attack and max units) * @param bestTransport * - list of the rules and the number to be purchased (optimized for transport) * @return - integer which is 1 if bestAttack has changed, 2 if bestDefense has changed, 3 if both have changed */ private static int purchaseLoop(final IntegerMap<String> parameters, final int ruleNum, final IntegerMap<ProductionRule> bestAttack, final IntegerMap<ProductionRule> bestDefense, final IntegerMap<ProductionRule> bestTransport, final IntegerMap<ProductionRule> bestMaxUnits, final IntegerMap<ProductionRule> bestMobileAttack, final HashMap<ProductionRule, Boolean> transportMap, final HashMap<ProductionRule, Boolean> infMap, final HashMap<ProductionRule, Boolean> nonInfMap, final HashMap<ProductionRule, Boolean> supportableInfMap, final GameData data, final PlayerID player, final int fighters) { final long start = System.currentTimeMillis(); /* * It is expected that this is called with a subset of possible units (i.e. just land Units or just Air Units) * Routine has the potential to be very costly if the number of rules is high * Computation cost is exponential with the number of rules: maxUnits^(number of rules(i.e. different Units)) * Germany on revised map has maxunits of 14 and ships size is 5 --> 14^5 potential iterations (537824) * Becomes 1.4 billion if there are 8 units * intended to be self-nesting for each rule in bestAttack * countMax tells us which rule we are on...it should increase each time it is passed * parametersChanged tells us if the next call changed the parameters (forcing a change at this level) * thisParametersChanged tells us if this routine changed parameters either way (by calculation or by return from a * nested call) * Assumptions: 1) artillery purchased with infantry has a bonus * 2) fighters have attack: 3 and defense: 4 TODO: Recode this to use fighter attack/defense and to handle tech * bonus */ final Resource key = data.getResourceList().getResource(Constants.PUS); final Set<ProductionRule> ruleCheck = bestAttack.keySet(); final Iterator<ProductionRule> ruleIter = ruleCheck.iterator(); int counter = 1; ProductionRule rule = null; while (counter <= ruleNum && ruleIter.hasNext()) { rule = ruleIter.next(); counter++; } if (rule == null) { return 0; } int totAttack = parameters.getInt("attack"); int totDefense = parameters.getInt("defense"); int totCost = parameters.getInt("totcost"); int totMovement = parameters.getInt("totMovement"); final int maxCost = parameters.getInt("maxCost"); final int maxUnits = parameters.getInt("maxUnits"); int totUnits = parameters.getInt("totUnits"); int maxAttack = parameters.getInt("maxAttack"); int maxDefense = parameters.getInt("maxDefense"); int maxTransAttack = parameters.getInt("maxTransAttack"); int maxTransCost = parameters.getInt("maxTransCost"); int maxAttackCost = parameters.getInt("maxAttackCost"); int maxDefenseCost = parameters.getInt("maxDefenseCost"); int maxUnitAttack = parameters.getInt("maxUnitAttack"); int maxUnitCost = parameters.getInt("maxUnitCost"); int maxUnitCount = parameters.getInt("maxUnitCount"); int maxMobileAttack = parameters.getInt("maxMobileAttack"); int maxMovement = parameters.getInt("maxMovement"); int supportableInfCount = parameters.getInt("supportableInfCount"); int infCount = parameters.getInt("infantry"); int nonInfCount = parameters.getInt("nonInfantry"); int parametersChanged = 0; int thisParametersChanged = 0; final NamedAttachable resourceOrUnit = rule.getResults().keySet().iterator().next(); if (!(resourceOrUnit instanceof UnitType)) { return 0; } final UnitType x = (UnitType) resourceOrUnit; final UnitAttachment u = UnitAttachment.get(x); final boolean thisIsSupportableInf = supportableInfMap.get(rule); final boolean thisIsInf = infMap.get(rule); final boolean thisIsNonInf = nonInfMap.get(rule); final boolean thisIsArt = u.getArtillery(); final int uMovement = u.getMovement(player); int uAttack = u.getAttack(player); int uDefense = u.getDefense(player); final int aRolls = u.getAttackRolls(player); final int cost = rule.getCosts().getInt(key); // Discourage buying submarines, since the AI has no clue how to use them (veqryn) final boolean thisIsSub = u.getIsSub(); if (thisIsSub && uAttack >= 1) { uAttack--; } else if (thisIsSub && uDefense >= 1) { uDefense--; } // Encourage buying balanced units. Added by veqryn, to decrease the rate at which the AI buys walls, fortresses, // and mortars, among // other specialty units that should not be bought often if at all. if (u.getMovement(player) == 0) { uAttack = 0; } if ((u.getAttack(player) == 0 || u.getDefense(player) - u.getAttack(player) >= 4) && u.getDefense(player) >= 1) { uDefense--; if (u.getDefense(player) - u.getAttack(player) >= 4) { uDefense--; } } if ((u.getDefense(player) == 0 || u.getAttack(player) - u.getDefense(player) >= 4) && u.getAttack(player) >= 1) { uAttack--; if (u.getAttack(player) - u.getDefense(player) >= 4) { uAttack--; } } // TODO: stop it from buying zero movement units under all circumstances. Also, lessen the number of artillery type // units bought // slightly. And lessen sub purchases, or eliminate entirely. (veqryn) // TODO: some transport ships have large capacity, others have a small capacity and are made for fighting. Make sure // if the AI is buying // transports, it chooses high capacity transports even if more expensive and less att/def than normal ships int fightersremaining = fighters; int usableMaxUnits = maxUnits; if (usableMaxUnits * ruleCheck.size() > 1000 && Math.random() <= 0.50) { usableMaxUnits = usableMaxUnits / 2; } for (int i = 0; i <= (usableMaxUnits - totUnits); i++) { // allow 0 so that this unit might be skipped...due to low value...consider special capabilities later if (i > 0) { totCost += cost; if (totCost > maxCost) { continue; } if (thisIsInf) { infCount++; } else if (thisIsNonInf) { nonInfCount++; } if (thisIsSupportableInf) { supportableInfCount++; } // give bonus of 1 hit per 2 units and if fighters are on the capital, a bonus for carrier equal to fighter // attack or defense int carrierLoad = Math.min(u.getCarrierCapacity(), fightersremaining); if (carrierLoad < 0) { carrierLoad = 0; } int bonusAttack = ((u.getHitPoints() - 1) * uAttack) + (uAttack > 0 && (i % 2) == 0 ? 1 : 0) + carrierLoad * 3; if (thisIsArt && i <= supportableInfCount) { // add one bonus for each artillery purchased with supportable infantry bonusAttack++; } final int bonusDefense = ((u.getHitPoints() - 1) * uDefense) + (uDefense > 0 && (i % 2) == 0 ? 1 : 0) + (carrierLoad * 4); fightersremaining -= carrierLoad; totUnits++; totAttack += uAttack * aRolls + bonusAttack; totDefense += uDefense * aRolls + bonusDefense; totMovement += uMovement; } if (totUnits <= maxUnits && ruleIter.hasNext()) { parameters.put("attack", totAttack); parameters.put("defense", totDefense); parameters.put("totcost", totCost); parameters.put("totUnits", totUnits); parameters.put("totMovement", totMovement); parameters.put("infantry", infCount); parameters.put("nonInfantry", nonInfCount); parameters.put("supportableInfCount", supportableInfCount); parametersChanged = purchaseLoop(parameters, counter, bestAttack, bestDefense, bestTransport, bestMaxUnits, bestMobileAttack, transportMap, infMap, nonInfMap, supportableInfMap, data, player, fighters); maxAttack = parameters.getInt("maxAttack"); maxTransAttack = parameters.getInt("maxTransAttack"); maxTransCost = parameters.getInt("maxTransCost"); maxDefense = parameters.getInt("maxDefense"); maxAttackCost = parameters.getInt("maxAttackCost"); maxDefenseCost = parameters.getInt("maxDefenseCost"); maxUnitCost = parameters.getInt("maxUnitCost"); maxUnitAttack = parameters.getInt("maxUnitAttack"); maxMobileAttack = parameters.getInt("maxMobileAttack"); maxMovement = parameters.getInt("maxMovement"); if (System.currentTimeMillis() - start > PURCHASE_LOOP_MAX_TIME_MILLIS) { break; } } if (totCost == 0) { continue; } // parameters changed: 001: attack, 010: defense, 100: maxUnits, 1000: transport, 10000: mobileAttack if (parametersChanged > 0) { // change forced by another rule if ((parametersChanged - 3) % 4 == 0) { bestAttack.put(rule, i); bestDefense.put(rule, i); thisParametersChanged = 3; parametersChanged -= 3; } else if ((parametersChanged - 1) % 4 == 0) { bestAttack.put(rule, i); if (thisParametersChanged % 2 == 0) { thisParametersChanged += 1; } parametersChanged -= 1; } else if ((parametersChanged - 2) % 4 == 0) { bestDefense.put(rule, i); if ((thisParametersChanged + 2) % 4 != 0 && (thisParametersChanged + 1) % 4 != 0) { thisParametersChanged += 2; } parametersChanged -= 2; } if ((parametersChanged > 0) && (parametersChanged - 4) % 8 == 0) { bestMaxUnits.put(rule, i); if (thisParametersChanged == 0 || (thisParametersChanged - 4) % 8 != 0) { thisParametersChanged += 4; } parametersChanged -= 4; } if ((parametersChanged - 8) % 16 == 0) { bestTransport.put(rule, i); if (thisParametersChanged == 0 || (thisParametersChanged - 8) % 16 != 0) { thisParametersChanged += 8; } } if (parametersChanged >= 16) { bestMobileAttack.put(rule, i); if (thisParametersChanged < 16) { thisParametersChanged += 16; } } parametersChanged = 0; continue; } if ((totAttack > maxAttack) || (totAttack == maxAttack && (Math.random() < 0.50))) { maxAttack = totAttack; maxAttackCost = totCost; parameters.put("maxAttack", maxAttack); parameters.put("maxAttackCost", maxAttackCost); bestAttack.put(rule, i); if (thisParametersChanged % 2 == 0) { thisParametersChanged += 1; } final Iterator<ProductionRule> changeIter = ruleCheck.iterator(); ProductionRule changeThis = null; int countThis = 1; while (changeIter.hasNext()) { // have to clear the rules below this rule changeThis = changeIter.next(); if (countThis >= counter) { bestAttack.put(changeThis, 0); } countThis++; } } if ((totDefense > maxDefense) || (totDefense == maxDefense && (Math.random() < 0.50))) { maxDefense = totDefense; maxDefenseCost = totCost; parameters.put("maxDefense", maxDefense); parameters.put("maxDefenseCost", maxDefenseCost); bestDefense.put(rule, i); if ((thisParametersChanged + 2) % 4 != 0 && (thisParametersChanged + 1) % 4 != 0) { thisParametersChanged += 2; } final Iterator<ProductionRule> changeIter = ruleCheck.iterator(); ProductionRule changeThis = null; int countThis = 1; while (changeIter.hasNext()) { // have to clear the rules below this rule changeThis = changeIter.next(); if (countThis >= counter) { bestDefense.put(changeThis, 0); } countThis++; } } if (totAttack > maxUnitAttack && totUnits >= maxUnitCount) { maxUnitAttack = totAttack; maxUnitCount = totUnits; maxUnitCost = totCost; parameters.put("maxUnitAttack", maxUnitAttack); parameters.put("maxUnitCount", maxUnitCount); parameters.put("maxUnitCost", maxUnitCost); bestMaxUnits.put(rule, i); if ((thisParametersChanged + 4) % 8 != 0) { thisParametersChanged += 4; } final Iterator<ProductionRule> changeIter = ruleCheck.iterator(); ProductionRule changeThis = null; int countThis = 1; while (changeIter.hasNext()) { // have to clear the rules below this rule changeThis = changeIter.next(); if (countThis >= counter) { bestMaxUnits.put(changeThis, 0); } countThis++; } } if (totAttack > maxTransAttack && (infCount <= nonInfCount + 1 && infCount >= nonInfCount - 1)) { maxTransAttack = totAttack; maxTransCost = totCost; parameters.put("maxTransAttack", totAttack); parameters.put("maxTransCost", maxTransCost); bestTransport.put(rule, i); if ((thisParametersChanged + 8) % 16 != 0) { thisParametersChanged += 8; } final Iterator<ProductionRule> changeIter = ruleCheck.iterator(); ProductionRule changeThis = null; int countThis = 1; while (changeIter.hasNext()) { changeThis = changeIter.next(); if (countThis >= counter) { bestTransport.put(changeThis, 0); } countThis++; } } if ((totAttack >= maxMobileAttack && (totMovement > maxMovement)) || (totAttack > maxMobileAttack && (totMovement >= maxMovement))) { maxMobileAttack = totAttack; maxMovement = totMovement; parameters.put("maxMobileAttack", maxMobileAttack); parameters.put("maxMovement", maxMovement); bestMobileAttack.put(rule, i); if (thisParametersChanged < 16) { thisParametersChanged += 16; } final Iterator<ProductionRule> changeIter = ruleCheck.iterator(); ProductionRule changeThis = null; int countThis = 1; while (changeIter.hasNext()) { changeThis = changeIter.next(); if (countThis >= counter) { bestMobileAttack.put(changeThis, 0); } countThis++; } } } return thisParametersChanged; } /** * Return all territories that have units matching unitCondition and owned by us. * * @return List of territories */ private static List<Territory> findTersWithUnitsMatching(final GameData data, final PlayerID player, final Match<Unit> unitCondition) { final CompositeMatch<Unit> unitMatch = new CompositeMatchAnd<>(unitCondition, Matches.unitIsOwnedBy(player)); final List<Territory> result = new ArrayList<>(); final Collection<Territory> allTers = data.getMap().getTerritories(); for (final Territory ter : allTers) { if (ter.getUnits().someMatch(unitMatch)) { result.add(ter); } } return result; } /** * Returns a List of all territories with a water neighbor. * * @param allTerr - List of Territories */ private static List<Territory> stripLandLockedTerr(final GameData data, final List<Territory> allTerr) { final List<Territory> waterTerrs = new ArrayList<>(allTerr); final Iterator<Territory> wFIter = waterTerrs.iterator(); while (wFIter.hasNext()) { final Territory waterFact = wFIter.next(); if (Matches.territoryHasWaterNeighbor(data).invert().match(waterFact)) { wFIter.remove(); } } return waterTerrs; } /** * true or false...does a land route exist from territory to any enemy owned capitol? */ private static boolean hasLandRouteToEnemyOwnedCapitol(final Territory t, final PlayerID player, final GameData data) { for (final PlayerID ePlayer : data.getPlayerList().getPlayers()) { for (final Territory capital : TerritoryAttachment.getAllCapitals(ePlayer, data)) { if (data.getRelationshipTracker().isAtWar(player, capital.getOwner()) && data.getMap().getDistance(t, capital, Matches.TerritoryIsNotImpassableToLandUnits(player, data)) != -1) { return true; } } } return false; } /** * Return Territories containing any unit depending on unitCondition. * * <p> * Differs from findCertainShips because it doesn't require the units be owned * </p> */ 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; } /** * Territories we actually own in a modifiable List. */ private static List<Territory> allOurTerritories(final GameData data, final PlayerID player) { final Collection<Territory> ours = data.getMap().getTerritoriesOwnedBy(player); final List<Territory> ours2 = new ArrayList<>(); ours2.addAll(ours); return ours2; } /** * Territory ranking system. * * @param waterBased - attack is Water Based - Remove all terr with no avail water * @param nonCombat - if nonCombat, emphasize threatened factories over their neighbors * @return HashMap ranking of Territories */ private static HashMap<Territory, Float> rankTerritories(final GameData data, final List<Territory> ourFriendlyTerr, final List<Territory> ourEnemyTerr, final List<Territory> ignoreTerr, final PlayerID player, final boolean tFirst, final boolean waterBased, final boolean nonCombat) { final HashMap<Territory, Float> landRankMap = new HashMap<>(); final HashMap<Territory, Float> landStrengthMap = new HashMap<>(); final CompositeMatch<Territory> noEnemyOrWater = new CompositeMatchAnd<>( Matches.TerritoryIsNotImpassableToLandUnits(player, data), Matches.isTerritoryAllied(player, data)); final CompositeMatch<Territory> enemyAndNoWater = new CompositeMatchAnd<>(Matches.TerritoryIsNotImpassableToLandUnits(player, data), Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(player, data)); final List<PlayerID> ePlayers = getEnemyPlayers(data, player); final PlayerID ePlayer = ePlayers.get(0); final List<Territory> enemyCapitals = getEnemyCapitals(data, player); int minDist = 1000; final int playerPUs = getLeftToSpend(data, player); final List<Territory> myCapitals = TerritoryAttachment.getAllCurrentlyOwnedCapitals(player, data); if (myCapitals.isEmpty()) { myCapitals.addAll(TerritoryAttachment.getAllCapitals(player, data)); } if (myCapitals.isEmpty()) { myCapitals.addAll(Match.getMatches(data.getMap().getTerritories(), new CompositeMatchAnd<>(Matches.TerritoryIsNotImpassableToLandUnits(player, data), Matches.territoryHasUnitsThatMatch(Matches.unitIsLandAndOwnedBy(player))))); } for (final Territory myCapital : myCapitals) { final Iterator<Territory> eCapsIter = enemyCapitals.iterator(); while (eCapsIter.hasNext()) { final Territory eCap = eCapsIter.next(); if (Matches.isTerritoryFriendly(player, data).match(eCap) && Matches.territoryHasAlliedUnits(player, data).match(eCap) && !Matches.territoryHasEnemyLandNeighbor(data, player).match(eCap)) { eCapsIter.remove(); continue; } final int dist = data.getMap().getDistance(myCapital, eCap); minDist = Math.min(minDist, dist); } } /* * Send units because: * 1) Production Value * 2) Victory City * 3) Has a Land Route to Enemy Capitol * 4) Has enemy factory * 5) Is close to enemy * 6) Is closer than half the distance from cap to Enemy cap */ final List<Territory> alliedFactories = getEnemyCapitals(data, ePlayer); final Iterator<Territory> aFIter = alliedFactories.iterator(); while (aFIter.hasNext()) { final Territory aFTerr = aFIter.next(); final float aFPotential = getStrengthOfPotentialAttackers(aFTerr, data, player, tFirst, true, null); final float alliedStrength = strengthOfTerritory(data, aFTerr, player, false, false, tFirst, true); if (aFPotential < alliedStrength * 0.75F || aFPotential < 1.0F || !Matches.TerritoryIsPassableAndNotRestricted(player, data).match(aFTerr) || (Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(player, data).match(aFTerr) && Matches.territoryHasEnemyLandNeighbor(data, player).match(aFTerr))) { aFIter.remove(); } } final List<Territory> aFNeighbors = new ArrayList<>(); for (final Territory aF : alliedFactories) { aFNeighbors.addAll(data.getMap().getNeighbors(aF, Matches.isTerritoryAllied(player, data))); } for (final Territory eTerr : data.getMap().getTerritories()) { if (eTerr.isWater() || Matches.TerritoryIsImpassable.match(eTerr) || !Matches.TerritoryIsPassableAndNotRestricted(player, data).match(eTerr)) { continue; } final float alliedPotential = getStrengthOfPotentialAttackers(eTerr, data, ePlayer, tFirst, true, null); final float rankStrength = getStrengthOfPotentialAttackers(eTerr, data, player, tFirst, true, ignoreTerr); final TerritoryAttachment ta = TerritoryAttachment.get(eTerr); if (ta == null) { continue; } final float productionValue = ta.getProduction(); float eTerrValue = 0.0F; final boolean island = !doesLandExistAt(eTerr, data, false); eTerrValue += ta.getVictoryCity() > 0 ? 2.0F : 0.0F; final boolean lRCap = hasLandRouteToEnemyOwnedCapitol(eTerr, player, data); // 16 might be too much, consider changing to 8 eTerrValue += lRCap ? 16.0F : 0.0F; if (lRCap && (!Matches .territoryIsEnemyNonNeutralAndHasEnemyUnitMatching(data, player, Matches.UnitCanProduceUnits).match(eTerr) && !Matches.territoryIsAlliedAndHasAlliedUnitMatching(data, player, Matches.UnitCanProduceUnits) .match(eTerr))) { final Route eCapRoute = findNearest(eTerr, Matches.territoryIsEnemyNonNeutralAndHasEnemyUnitMatching(data, player, Matches.UnitCanProduceUnits), Matches.TerritoryIsNotImpassableToLandUnits(player, data), data); if (eCapRoute != null) { // 8 might be too much, consider changing to 4 eTerrValue = Math.max(eTerrValue - 8, eTerrValue - (eCapRoute.numberOfSteps() - 1)); } } eTerrValue += Matches.territoryHasEnemyNonNeutralNeighborWithEnemyUnitMatching(data, player, Matches.UnitCanProduceUnits) .match(eTerr) ? 3.0F : 0.0F; int eMinDist = 1000; for (final Territory eTerrCap : enemyCapitals) { final int eDist = data.getMap().getDistance(eTerr, eTerrCap, Matches.TerritoryIsNotImpassable); eMinDist = Math.min(eMinDist, eDist); } eTerrValue -= eMinDist - 1; // bonus for general closeness to enemy Capital // eTerrValue += (eMinDist < minDist - 1) ? 4.0F : 0.0F; if (Matches.TerritoryIsLand.match(eTerr) && Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(player, data).match(eTerr)) { ourEnemyTerr.add(eTerr); eTerrValue += productionValue * 2; final float eTerrStrength = strength(eTerr.getUnits().getMatches(Matches.enemyUnit(player, data)), false, false, tFirst); eTerrValue += alliedPotential > (rankStrength + eTerrStrength) ? productionValue : 0.0F; if (island) { eTerrValue += 5.0F; } // bonus for killing air units eTerrValue += eTerr.getUnits().countMatches(Matches.UnitIsAir) * 2; eTerrValue += Matches.territoryIsEnemyNonNeutralAndHasEnemyUnitMatching(data, player, Matches.UnitCanProduceUnits) .match(eTerr) ? 4.0F : 0.0F; eTerrValue += Matches.territoryHasAlliedNeighborWithAlliedUnitMatching(data, player, Matches.UnitCanProduceUnits) .match(eTerr) ? 8.0F : 0.0F; eTerrValue += Matches.territoryHasEnemyLandNeighbor(data, player).invert().match(eTerr) ? productionValue + 1 : 0.0F; final float netStrength = (eTerrStrength - alliedPotential + 0.5F * rankStrength); landStrengthMap.put(eTerr, netStrength); landRankMap.put(eTerr, eTerrValue + netStrength * 0.25F); } else if (Matches.isTerritoryAllied(player, data).match(eTerr) && Matches.TerritoryIsNotNeutralButCouldBeWater.match(eTerr)) { final boolean hasENeighbors = Matches.territoryHasEnemyLandNeighbor(data, player).match(eTerr); final Route testERoute = findNearest(eTerr, enemyAndNoWater, noEnemyOrWater, data); if (island) { eTerrValue += -5.0F; } eTerrValue += (hasENeighbors ? 2.0F : -2.0F); eTerrValue += (aFNeighbors.contains(eTerr)) ? 8.0F : 0.0F; // -20 and -10 might be too much, // consider changing to -8 and -4 eTerrValue += (testERoute == null ? -20.0F : Math.max(-10.0F, -(testERoute.numberOfSteps() - 2))); eTerrValue += (testERoute != null ? productionValue : 0.0F); final float aTerrStrength = strength(eTerr.getUnits().getMatches(Matches.alliedUnit(player, data)), false, false, tFirst); // bonus for allied factory and allied factory with enemy neighbor final boolean hasAlliedFactory = Matches.territoryIsAlliedAndHasAlliedUnitMatching(data, player, Matches.UnitCanProduceUnits).match(eTerr); if (hasAlliedFactory) { eTerrValue += 4.0F + (hasENeighbors && rankStrength > 5.0F ? 3.0F : 0.0F); alliedFactories.add(eTerr); } final float netStrength = rankStrength - aTerrStrength - 0.5F * alliedPotential; landStrengthMap.put(eTerr, netStrength); landRankMap.put(eTerr, eTerrValue + netStrength * 0.50F); if ((netStrength > -15.0F && rankStrength > 2.0F) || hasENeighbors || testERoute != null) { ourFriendlyTerr.add(eTerr); } } else if (Matches.TerritoryIsNeutralButNotWater.match(eTerr)) { if (Matches.TerritoryIsNotImpassable.match(eTerr) && (Matches.isTerritoryFreeNeutral(data).match(eTerr) || Properties.getNeutralCharge(data) <= playerPUs)) { // Make sure most neutral territories have lower priorities than enemy territories. eTerrValue += -100.0F; final boolean hasENeighbors = Matches.territoryHasEnemyLandNeighbor(data, player).match(eTerr); final Route testERoute = findNearest(eTerr, enemyAndNoWater, noEnemyOrWater, data); eTerrValue += (hasENeighbors ? 1.0F : -1.0F); eTerrValue += (testERoute == null ? -1.0F : -(testERoute.numberOfSteps() - 1)); eTerrValue += productionValue > 0 ? productionValue : -5.0F; final float netStrength = rankStrength - 0.5F * alliedPotential; landStrengthMap.put(eTerr, netStrength); landRankMap.put(eTerr, eTerrValue + netStrength * 0.50F); } } // Currently there are a lot of territories that don't make it into the list, especially if the politics involves // neutral nations. we // should add them here. } if (nonCombat) { final CompositeMatch<Territory> alliedLandTerr = new CompositeMatchAnd<>(Matches.isTerritoryAllied(player, data), Matches.TerritoryIsLand, Matches.TerritoryIsNotImpassable); // Set<Territory> terrList = landRankMap.keySet(); for (final Territory terr1 : alliedFactories) { if (!landRankMap.containsKey(terr1)) { continue; } float landRank = landRankMap.get(terr1); if (Matches.territoryHasEnemyLandNeighbor(data, player).match(terr1)) { for (final Territory neighbor : data.getMap().getNeighbors(terr1, alliedLandTerr)) { if (!landRankMap.containsKey(neighbor)) { continue; } final float thisRank = landRankMap.get(neighbor); landRank = Math.max(landRank, thisRank); } landRank += 1.0F; landRankMap.put(terr1, landRank); } } } return landRankMap; } /** * 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; } /** * List containing the enemy Capitals. */ private static List<Territory> getEnemyCapitals(final GameData data, final PlayerID player) { // generate a list of all enemy capitals final List<Territory> enemyCapitals = new ArrayList<>(); final List<PlayerID> ePlayers = getEnemyPlayers(data, player); for (final PlayerID otherPlayer : ePlayers) { for (final Territory capital : TerritoryAttachment.getAllCapitals(otherPlayer, data)) { if (capital != null && Matches.TerritoryIsNotImpassableToLandUnits(player, data).match(capital)) { enemyCapitals.add(capital); } } } return enemyCapitals; } /** * Returns the players current pus available. */ private static int getLeftToSpend(final GameData data, final PlayerID player) { final Resource pus = data.getResourceList().getResource(Constants.PUS); return player.getResources().getQuantity(pus); } /** * 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; } /** * Find the Route to the nearest Territory. * * @param start - starting territory * @param endCondition - condition for the ending Territory * @param routeCondition - condition for each Territory in Route */ private static Route findNearest(final Territory start, final Match<Territory> endCondition, final Match<Territory> routeCondition, final GameData data) { final Match<Territory> canGo = new CompositeMatchOr<>(endCondition, routeCondition); final Map<Territory, Territory> visited = new HashMap<>(); final Queue<Territory> q = new LinkedList<>(); final List<Territory> route = new ArrayList<>(); // changing to exclude checking start q.addAll(data.getMap().getNeighbors(start, canGo)); Territory current = null; visited.put(start, null); for (final Territory t : q) { visited.put(t, start); } while (!q.isEmpty()) { current = q.remove(); if (endCondition.match(current)) { break; } else { for (final Territory neighbor : data.getMap().getNeighbors(current, canGo)) { if (!visited.containsKey(neighbor)) { q.add(neighbor); visited.put(neighbor, current); } } } } if (current == null || !endCondition.match(current)) { return null; } for (Territory t = current; t != null; t = visited.get(t)) { route.add(t); } Collections.reverse(route); return new Route(route); } /** * 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; } /** * 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()) { Route route1 = new Route(); for (final Route r : routes) { route1 = Route.join(route1, r); } if (unitCondition.match(u) && Matches.UnitHasEnoughMovementForRoute(route1).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(new Route(checked)).match(u)) { units.add(u); } else if (ac != null && Matches.UnitCanLandOnCarrier.match(u) && Matches.UnitHasEnoughMovementForRoute(new Route(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 have a Land Enemy Neighbor. * * @neutral - include neutral territories * @allied - include allied territories * return - List of territories */ private static List<Territory> getTerritoriesWithEnemyNeighbor(final GameData data, final PlayerID player, final boolean allied, final boolean neutral) { final List<Territory> ourTerr = new ArrayList<>(); final List<Territory> enemyLandTerr = allEnemyTerritories(data, player); if (!neutral) { final Iterator<Territory> eIter = enemyLandTerr.iterator(); while (eIter.hasNext()) { final Territory checkTerr = eIter.next(); if (Matches.TerritoryIsNeutralButNotWater.match(checkTerr)) { eIter.remove(); } } } final Iterator<Territory> eIter = enemyLandTerr.iterator(); while (eIter.hasNext()) { final Territory enemy = eIter.next(); if (doesLandExistAt(enemy, data, false)) { final List<Territory> newTerrs = new ArrayList<>(); if (allied) { newTerrs.addAll(getNeighboringLandTerritories(data, player, enemy)); } else { newTerrs.addAll(data.getMap().getNeighbors(enemy, Matches.isTerritoryOwnedBy(player))); } for (final Territory nT : newTerrs) { if (!ourTerr.contains(nT)) { ourTerr.add(nT); } } } } return ourTerr; } private static void reorder(final List<?> reorder, final Map<?, ? extends Number> map, final boolean greaterThan) { Collections.sort(reorder, (Comparator<Object>) (o1, o2) -> { double v1 = safeGet(map, o1); double v2 = safeGet(map, o2); if (greaterThan) { final double t = v1; v1 = v2; v2 = t; } if (v1 > v2) { return 1; } else if (v1 == v2) { return 0; } else { return -1; } }); } private static double safeGet(final Map<?, ? extends Number> map, final Object o1) { if (!map.containsKey(o1)) { return 0; } return map.get(o1).doubleValue(); } /** * All Enemy Territories in a modifiable List. */ private static List<Territory> allEnemyTerritories(final GameData data, final PlayerID player) { final List<Territory> badGuys = new ArrayList<>(); for (final Territory t : data.getMap().getTerritories()) { if (Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(player, data).match(t)) { badGuys.add(t); } } return badGuys; } /** * returns all territories that are water territories. used to remove convoy zones from places the ai will put a * factory */ private static List<Territory> onlyWaterTerr(final List<Territory> allTerr) { final List<Territory> water = new ArrayList<>(allTerr); final Iterator<Territory> wFIter = water.iterator(); while (wFIter.hasNext()) { final Territory waterFact = wFIter.next(); if (!Matches.TerritoryIsWater.match(waterFact)) { wFIter.remove(); } } return water; } /** * Look for an available sea Territory to place sea Units * if other owned sea units exist, place them with these units * Otherwise, look for the location which is least likely to get them killed. * * @param landTerr * - factory territory * @param tFirst * - can transports be killed during battle * Should be modified to include the list of units which will be dropped (for strength measurement) */ private static Territory findASeaTerritoryToPlaceOn(final Territory landTerr, final GameData data, final PlayerID player, final boolean tFirst) { final CompositeMatch<Territory> ourSeaTerr = new CompositeMatchAnd<>(Matches.TerritoryIsWater, Matches.territoryHasUnitsOwnedBy(player)); final CompositeMatch<Unit> seaUnit = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player), Matches.UnitIsSea); final CompositeMatch<Unit> airUnit = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(player), Matches.UnitIsAir); final CompositeMatch<Unit> seaAirUnit = new CompositeMatchOr<>(seaUnit, airUnit); Territory seaPlaceAt = null; Territory bestSeaPlaceAt = null; Territory xPlace = null; if (landTerr == null) { return seaPlaceAt; } final Set<Territory> seaNeighbors = data.getMap().getNeighbors(landTerr, ourSeaTerr); // float eStrength = 0.0F; float minStrength = 1000.0F; float maxStrength = -1000.0F; for (final Territory t : seaNeighbors) { // give preference to territory with units float enemyStrength = getStrengthOfPotentialAttackers(t, data, player, tFirst, true, null); final float extraEnemy = strength(t.getUnits().getMatches(Matches.enemyUnit(player, data)), true, true, tFirst); enemyStrength += extraEnemy; float ourStrength = strength(t.getUnits().getMatches(seaAirUnit), false, true, tFirst); final float existingStrength = strength(t.getUnits().getMatches(Matches.alliedUnit(player, data)), false, true, tFirst); ourStrength += existingStrength; final float strengthDiff = enemyStrength - ourStrength; if (strengthDiff < minStrength && ourStrength > 0.0F) { seaPlaceAt = t; minStrength = strengthDiff; } if (strengthDiff > maxStrength && strengthDiff < 3.0F && (ourStrength > 0.0F || existingStrength > 0.0F)) { bestSeaPlaceAt = t; maxStrength = strengthDiff; } } if (seaPlaceAt == null && bestSeaPlaceAt == null) { final Set<Territory> seaNeighbors2 = data.getMap().getNeighbors(landTerr, Matches.TerritoryIsWater); for (final Territory t : seaNeighbors2) { // find Terr away from enemy units final float enemyStrength = getStrengthOfPotentialAttackers(t, data, player, tFirst, true, null); final float ourStrength = strength(t.getUnits().getMatches(seaAirUnit), false, true, tFirst); if (t.getUnits().someMatch(Matches.enemyUnit(player, data))) { // try to avoid Territories with enemy Units xPlace = t; continue; } if ((enemyStrength - ourStrength) < minStrength) { seaPlaceAt = t; minStrength = enemyStrength - ourStrength; } } } if (seaPlaceAt == null && bestSeaPlaceAt == null && xPlace != null) { // this will be null if there are no water territories seaPlaceAt = xPlace; } if (bestSeaPlaceAt == null) { return seaPlaceAt; } else { return bestSeaPlaceAt; } } /** * distance to the closest enemy * just uses findNearest. */ private static int distanceToEnemy(final Territory t, final GameData data, final PlayerID player, final boolean sea) { // note: neutrals are enemies // also note: if sea, you are finding distance to enemy sea units, not to enemy land over sea if (Matches.TerritoryIsImpassable.match(t)) { return 0; } Match<Territory> endCondition; Match<Territory> routeCondition; if (sea) { endCondition = new CompositeMatchAnd<>(Matches.TerritoryIsWater, Matches.territoryHasEnemyUnits(player, data)); routeCondition = Matches.TerritoryIsWater; } else { endCondition = new CompositeMatchAnd<>(Matches.isTerritoryEnemy(player, data), Matches.TerritoryIsNotImpassable, Matches.TerritoryIsLand); routeCondition = new CompositeMatchAnd<>(Matches.isTerritoryAllied(player, data), Matches.TerritoryIsNotImpassable, Matches.TerritoryIsLand); } final Route r = findNearest(t, endCondition, routeCondition, data); if (r == null) { return 0; } else { return r.numberOfSteps(); } } /** * Determine the strength of a territory. * * @param attacking - attacking strength or defending * @param allied - allied = true - all allied units --> false - owned units only */ private static float strengthOfTerritory(final GameData data, final Territory thisTerr, final PlayerID player, final boolean attacking, final boolean sea, final boolean tFirst, final boolean allied) { final List<Unit> theUnits = new ArrayList<>(); if (allied) { theUnits.addAll(thisTerr.getUnits().getMatches(Matches.alliedUnit(player, data))); } else { theUnits.addAll(thisTerr.getUnits().getMatches(Matches.unitIsOwnedBy(player))); } final float theStrength = strength(theUnits, attacking, sea, tFirst); return theStrength; } /** * Does this territory have any land? i.e. it isn't an island * * @neutral - count an attackable neutral as a land neighbor * @return boolean (true if a land territory is a neighbor to t */ private static boolean doesLandExistAt(final Territory t, final GameData data, final boolean neutral) { // simply: is this territory surrounded by water boolean isLand = false; final Set<Territory> checkList = data.getMap().getNeighbors(t, Matches.TerritoryIsLand); if (!neutral) { final Iterator<Territory> nIter = checkList.iterator(); while (nIter.hasNext()) { final Territory nTerr = nIter.next(); if (Matches.TerritoryIsNeutralButNotWater.match(nTerr)) { nIter.remove(); } } } for (final Territory checkNeutral : checkList) { if (Matches.TerritoryIsNotImpassable.match(checkNeutral)) { isLand = true; } } return isLand; } /** * 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; } }; } /** * 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; } }