package games.strategy.triplea.ai; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Resource; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.net.GUID; import games.strategy.triplea.Constants; import games.strategy.triplea.attachments.PlayerAttachment; import games.strategy.triplea.attachments.PoliticalActionAttachment; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.DelegateFinder; import games.strategy.triplea.delegate.DiceRoll; import games.strategy.triplea.delegate.IBattle.BattleType; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.PoliticsDelegate; import games.strategy.triplea.delegate.dataObjects.BattleListing; import games.strategy.triplea.delegate.dataObjects.CasualtyDetails; import games.strategy.triplea.delegate.dataObjects.CasualtyList; import games.strategy.triplea.delegate.remote.IAbstractForumPosterDelegate; import games.strategy.triplea.delegate.remote.IAbstractPlaceDelegate; import games.strategy.triplea.delegate.remote.IBattleDelegate; import games.strategy.triplea.delegate.remote.IMoveDelegate; import games.strategy.triplea.delegate.remote.IPoliticsDelegate; import games.strategy.triplea.delegate.remote.IPurchaseDelegate; import games.strategy.triplea.delegate.remote.ITechDelegate; import games.strategy.triplea.player.AbstractBasePlayer; import games.strategy.triplea.player.ITripleAPlayer; import games.strategy.triplea.ui.AbstractUIContext; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.IntegerMap; import games.strategy.util.Match; import games.strategy.util.ThreadUtil; import games.strategy.util.Tuple; /** * Base class for AIs. * * <p> * Control pausing with the AI pause menu option. * AIs should note that any data that is stored in the AI instance, will be lost when the game is restarted. * We cannot save data with an AI, since the player may choose to restart the game with a different AI, * or with a human player. * </p> * * <p> * If an AI finds itself starting in the middle of a move phase, or the middle of a purchase phase, * (as would happen if a player saved the game during the middle of an AI's move phase) it is acceptable * for the AI to play badly for a turn, but the AI should recover, and play correctly when the next phase * of the game starts. * </p> * * <p> * As a rule, nothing that changes GameData should be in here (it should be in a delegate, and done * through an IDelegate using a change). * </p> */ public abstract class AbstractAI extends AbstractBasePlayer implements ITripleAPlayer { private static final Logger s_logger = Logger.getLogger(AbstractAI.class.getName()); public AbstractAI(final String name, final String type) { super(name, type); } @Override public Territory selectBombardingTerritory(final Unit unit, final Territory unitTerritory, final Collection<Territory> territories, final boolean noneAvailable) { return territories.iterator().next(); } @Override public boolean selectAttackSubs(final Territory unitTerritory) { return true; } @Override public boolean selectAttackTransports(final Territory unitTerritory) { return true; } @Override public boolean selectAttackUnits(final Territory unitTerritory) { return true; } @Override public boolean selectShoreBombard(final Territory unitTerritory) { return true; } @Override public boolean confirmMoveKamikaze() { return false; } @Override public boolean confirmMoveHariKari() { return false; } @Override public Territory whereShouldRocketsAttack(final Collection<Territory> candidates, final Territory from) { return candidates.iterator().next(); } @Override public CasualtyDetails selectCasualties(final Collection<Unit> selectFrom, final Map<Unit, Collection<Unit>> dependents, final int count, final String message, final DiceRoll dice, final PlayerID hit, final Collection<Unit> friendlyUnits, final PlayerID enemyPlayer, final Collection<Unit> enemyUnits, final boolean amphibious, final Collection<Unit> amphibiousLandAttackers, final CasualtyList defaultCasualties, final GUID battleID, final Territory battlesite, final boolean allowMultipleHitsPerUnit) { if (defaultCasualties.size() != count) { throw new IllegalStateException("Select Casualties showing different numbers for number of hits to take vs total " + "size of default casualty selections"); } if (defaultCasualties.getKilled().size() <= 0) { return new CasualtyDetails(defaultCasualties, false); } final int numberOfPlanesThatDoNotNeedToLandOnCarriers = 0; final CasualtyDetails myCasualties = new CasualtyDetails(false); myCasualties.addToDamaged(defaultCasualties.getDamaged()); final List<Unit> selectFromSorted = new ArrayList<>(selectFrom); final List<Unit> interleavedTargetList = new ArrayList<>( AIUtils.interleaveCarriersAndPlanes(selectFromSorted, numberOfPlanesThatDoNotNeedToLandOnCarriers)); for (int i = 0; i < defaultCasualties.getKilled().size(); ++i) { myCasualties.addToKilled(interleavedTargetList.get(i)); } if (count != myCasualties.size()) { throw new IllegalStateException("AI chose wrong number of casualties"); } return myCasualties; } @Override public Unit whatShouldBomberBomb(final Territory territory, final Collection<Unit> potentialTargets, final Collection<Unit> bombers) { final Collection<Unit> factories = Match.getMatches(potentialTargets, Matches.UnitCanProduceUnitsAndCanBeDamaged); if (factories.isEmpty()) { return potentialTargets.iterator().next(); } return factories.iterator().next(); } @Override public Collection<Unit> getNumberOfFightersToMoveToNewCarrier(final Collection<Unit> fightersThatCanBeMoved, final Territory from) { final List<Unit> rVal = new ArrayList<>(); for (final Unit fighter : fightersThatCanBeMoved) { if (Math.random() < 0.8) { rVal.add(fighter); } } return rVal; } @Override public Territory selectTerritoryForAirToLand(final Collection<Territory> candidates, final Territory currentTerritory, final String unitMessage) { return candidates.iterator().next(); } @Override public boolean confirmMoveInFaceOfAA(final Collection<Territory> aaFiringTerritories) { return true; } @Override public Territory retreatQuery(final GUID battleID, final boolean submerge, final Territory battleTerritory, final Collection<Territory> possibleTerritories, final String message) { return null; } @Override public HashMap<Territory, Collection<Unit>> scrambleUnitsQuery(final Territory scrambleTo, final Map<Territory, Tuple<Collection<Unit>, Collection<Unit>>> possibleScramblers) { return null; } @Override public Collection<Unit> selectUnitsQuery(final Territory current, final Collection<Unit> possible, final String message) { return null; } @Override public boolean shouldBomberBomb(final Territory territory) { return false; } // TODO: This really needs to be rewritten with some basic logic @Override public boolean acceptAction(final PlayerID playerSendingProposal, final String acceptanceQuestion, final boolean politics) { // we are dead, just accept if (!getPlayerID().amNotDeadYet(getGameData())) { return true; } // not related to politics? just accept i guess if (!politics) { return true; } // politics from ally? accept if (Matches.isAllied(getPlayerID(), getGameData()).match(playerSendingProposal)) { return true; } // would we normally be allies? final List<String> allies = Arrays.asList(new String[] {Constants.PLAYER_NAME_AMERICANS, Constants.PLAYER_NAME_AUSTRALIANS, Constants.PLAYER_NAME_BRITISH, Constants.PLAYER_NAME_CANADIANS, Constants.PLAYER_NAME_CHINESE, Constants.PLAYER_NAME_FRENCH, Constants.PLAYER_NAME_RUSSIANS}); if (allies.contains(getPlayerID().getName()) && allies.contains(playerSendingProposal.getName())) { return true; } final List<String> axis = Arrays.asList(new String[] {Constants.PLAYER_NAME_GERMANS, Constants.PLAYER_NAME_ITALIANS, Constants.PLAYER_NAME_JAPANESE, Constants.PLAYER_NAME_PUPPET_STATES}); if (axis.contains(getPlayerID().getName()) && axis.contains(playerSendingProposal.getName())) { return true; } final Collection<String> myAlliances = new HashSet<>(getGameData().getAllianceTracker().getAlliancesPlayerIsIn(getPlayerID())); myAlliances.retainAll(getGameData().getAllianceTracker().getAlliancesPlayerIsIn(playerSendingProposal)); if (!myAlliances.isEmpty()) { return true; } return Math.random() < .5; } @Override public HashMap<Territory, HashMap<Unit, IntegerMap<Resource>>> selectKamikazeSuicideAttacks( final HashMap<Territory, Collection<Unit>> possibleUnitsToAttack) { final PlayerID id = getPlayerID(); // we are going to just assign random attacks to each unit randomly, til we run out of tokens to attack with. final PlayerAttachment pa = PlayerAttachment.get(id); if (pa == null) { return null; } final IntegerMap<Resource> resourcesAndAttackValues = pa.getSuicideAttackResources(); if (resourcesAndAttackValues.size() <= 0) { return null; } final IntegerMap<Resource> playerResourceCollection = id.getResources().getResourcesCopy(); final IntegerMap<Resource> attackTokens = new IntegerMap<>(); for (final Resource possible : resourcesAndAttackValues.keySet()) { final int amount = playerResourceCollection.getInt(possible); if (amount > 0) { attackTokens.put(possible, amount); } } if (attackTokens.size() <= 0) { return null; } final HashMap<Territory, HashMap<Unit, IntegerMap<Resource>>> rVal = new HashMap<>(); for (final Entry<Territory, Collection<Unit>> entry : possibleUnitsToAttack.entrySet()) { if (attackTokens.size() <= 0) { continue; } final Territory t = entry.getKey(); final List<Unit> targets = new ArrayList<>(entry.getValue()); Collections.shuffle(targets); for (final Unit u : targets) { if (attackTokens.size() <= 0) { continue; } final IntegerMap<Resource> rMap = new IntegerMap<>(); final Resource r = attackTokens.keySet().iterator().next(); final int num = Math.min(attackTokens.getInt(r), (UnitAttachment.get(u.getType()).getHitPoints() * (Math.random() < .3 ? 1 : (Math.random() < .5 ? 2 : 3)))); rMap.put(r, num); HashMap<Unit, IntegerMap<Resource>> attMap = rVal.get(t); if (attMap == null) { attMap = new HashMap<>(); } attMap.put(u, rMap); rVal.put(t, attMap); attackTokens.add(r, -num); if (attackTokens.getInt(r) <= 0) { attackTokens.removeKey(r); } } } return rVal; } @Override public Tuple<Territory, Set<Unit>> pickTerritoryAndUnits(final List<Territory> territoryChoices, final List<Unit> unitChoices, final int unitsPerPick) { pause(); final GameData data = getGameData(); final PlayerID me = getPlayerID(); final Territory picked; if (territoryChoices == null || territoryChoices.isEmpty()) { picked = null; } else if (territoryChoices.size() == 1) { picked = territoryChoices.get(0); } else { Collections.shuffle(territoryChoices); final List<Territory> notOwned = Match.getMatches(territoryChoices, Matches.isTerritoryOwnedBy(me).invert()); if (notOwned.isEmpty()) { // only owned territories left final boolean nonFactoryUnitsLeft = Match.someMatch(unitChoices, Matches.UnitCanProduceUnits.invert()); final Match<Unit> ownedFactories = new CompositeMatchAnd<>(Matches.UnitCanProduceUnits, Matches.unitIsOwnedBy(me)); final List<Territory> capitals = TerritoryAttachment.getAllCapitals(me, data); final List<Territory> test = new ArrayList<>(capitals); test.retainAll(territoryChoices); final List<Territory> territoriesWithFactories = Match.getMatches(territoryChoices, Matches.territoryHasUnitsThatMatch(ownedFactories)); if (!nonFactoryUnitsLeft) { test.retainAll(Match.getMatches(test, Matches.territoryHasUnitsThatMatch(ownedFactories).invert())); if (!test.isEmpty()) { picked = test.get(0); } else { if (capitals.isEmpty()) { capitals.addAll(Match.getMatches(data.getMap().getTerritories(), new CompositeMatchAnd<>( Matches.isTerritoryOwnedBy(me), Matches.territoryHasUnitsOwnedBy(me), Matches.TerritoryIsLand))); } final List<Territory> doesNotHaveFactoryYet = Match.getMatches(territoryChoices, Matches.territoryHasUnitsThatMatch(ownedFactories).invert()); if (capitals.isEmpty() || doesNotHaveFactoryYet.isEmpty()) { picked = territoryChoices.get(0); } else { final IntegerMap<Territory> distanceMap = data.getMap().getDistance(capitals.get(0), doesNotHaveFactoryYet, Match.getAlwaysMatch()); picked = distanceMap.lowestKey(); } } } else { final int maxTerritoriesToPopulate = Math.min(territoryChoices.size(), Math.max(4, Match.countMatches(unitChoices, Matches.UnitCanProduceUnits))); test.addAll(territoriesWithFactories); if (!test.isEmpty()) { if (test.size() < maxTerritoriesToPopulate) { final IntegerMap<Territory> distanceMap = data.getMap().getDistance(test.get(0), territoryChoices, Match.getAlwaysMatch()); for (int i = 0; i < maxTerritoriesToPopulate; i++) { final Territory choice = distanceMap.lowestKey(); distanceMap.removeKey(choice); test.add(choice); } } Collections.shuffle(test); picked = test.get(0); } else { if (capitals.isEmpty()) { capitals.addAll(Match.getMatches(data.getMap().getTerritories(), new CompositeMatchAnd<>( Matches.isTerritoryOwnedBy(me), Matches.territoryHasUnitsOwnedBy(me), Matches.TerritoryIsLand))); } if (capitals.isEmpty()) { picked = territoryChoices.get(0); } else { final IntegerMap<Territory> distanceMap = data.getMap().getDistance(capitals.get(0), territoryChoices, Match.getAlwaysMatch()); if (territoryChoices.contains(capitals.get(0))) { distanceMap.put(capitals.get(0), 0); } final List<Territory> choices = new ArrayList<>(); for (int i = 0; i < maxTerritoriesToPopulate; i++) { final Territory choice = distanceMap.lowestKey(); distanceMap.removeKey(choice); choices.add(choice); } Collections.shuffle(choices); picked = choices.get(0); } } } } else { // pick a not owned territory if possible final List<Territory> capitals = TerritoryAttachment.getAllCapitals(me, data); final List<Territory> test = new ArrayList<>(capitals); test.retainAll(notOwned); if (!test.isEmpty()) { picked = test.get(0); } else { if (capitals.isEmpty()) { capitals.addAll(Match.getMatches(data.getMap().getTerritories(), new CompositeMatchAnd<>( Matches.isTerritoryOwnedBy(me), Matches.territoryHasUnitsOwnedBy(me), Matches.TerritoryIsLand))); } if (capitals.isEmpty()) { picked = territoryChoices.get(0); } else { final IntegerMap<Territory> distanceMap = data.getMap().getDistance(capitals.get(0), notOwned, Match.getAlwaysMatch()); picked = distanceMap.lowestKey(); } } } } final Set<Unit> unitsToPlace = new HashSet<>(); if (unitChoices != null && !unitChoices.isEmpty() && unitsPerPick > 0) { Collections.shuffle(unitChoices); final List<Unit> nonFactory = Match.getMatches(unitChoices, Matches.UnitCanProduceUnits.invert()); if (nonFactory.isEmpty()) { for (int i = 0; i < unitsPerPick && !unitChoices.isEmpty(); i++) { unitsToPlace.add(unitChoices.get(0)); } } else { for (int i = 0; i < unitsPerPick && !nonFactory.isEmpty(); i++) { unitsToPlace.add(nonFactory.get(0)); } } } return Tuple.of(picked, unitsToPlace); } @Override public void confirmEnemyCasualties(final GUID battleId, final String message, final PlayerID hitPlayer) {} @Override public void reportError(final String error) {} @Override public void reportMessage(final String message, final String title) {} @Override public void confirmOwnCasualties(final GUID battleId, final String message) { pause(); } @Override public int[] selectFixedDice(final int numRolls, final int hitAt, final boolean hitOnlyIfEquals, final String message, final int diceSides) { final int[] dice = new int[numRolls]; for (int i = 0; i < numRolls; i++) { dice[i] = (int) Math.ceil(Math.random() * diceSides); } return dice; } /** * The given phase has started. We parse the phase name and call the appropriate method. */ @Override public final void start(final String name) { super.start(name); final PlayerID id = getPlayerID(); if (name.endsWith("Bid")) { final IPurchaseDelegate purchaseDelegate = (IPurchaseDelegate) getPlayerBridge().getRemoteDelegate(); final String propertyName = id.getName() + " bid"; final int bidAmount = getGameData().getProperties().get(propertyName, 0); purchase(true, bidAmount, purchaseDelegate, getGameData(), id); } else if (name.endsWith("Purchase")) { final IPurchaseDelegate purchaseDelegate = (IPurchaseDelegate) getPlayerBridge().getRemoteDelegate(); final Resource PUs = getGameData().getResourceList().getResource(Constants.PUS); final int leftToSpend = id.getResources().getQuantity(PUs); purchase(false, leftToSpend, purchaseDelegate, getGameData(), id); } else if (name.endsWith("Tech")) { final ITechDelegate techDelegate = (ITechDelegate) getPlayerBridge().getRemoteDelegate(); tech(techDelegate, getGameData(), id); } else if (name.endsWith("Move")) { final IMoveDelegate moveDel = (IMoveDelegate) getPlayerBridge().getRemoteDelegate(); if (name.endsWith("AirborneCombatMove")) { // do nothing } else { move(name.endsWith("NonCombatMove"), moveDel, getGameData(), id); } } else if (name.endsWith("Battle")) { battle((IBattleDelegate) getPlayerBridge().getRemoteDelegate(), getGameData(), id); } else if (name.endsWith("Politics")) { politicalActions(); } else if (name.endsWith("Place")) { final IAbstractPlaceDelegate placeDel = (IAbstractPlaceDelegate) getPlayerBridge().getRemoteDelegate(); place(name.indexOf("Bid") != -1, placeDel, getGameData(), id); } else if (name.endsWith("EndTurn")) { endTurn((IAbstractForumPosterDelegate) getPlayerBridge().getRemoteDelegate(), getGameData(), id); } } /************************ * The following methods are called when the AI starts a phase. *************************/ /** * It is the AI's turn to purchase units. * * @param purcahseForBid * - is this a bid purchase, or a normal purchase * @param PUsToSpend * - how many PUs we have to spend * @param purchaseDelegate * - the purchase delgate to buy things with * @param data * - the GameData * @param player * - the player to buy for */ protected abstract void purchase(boolean purchaseForBid, int PUsToSpend, IPurchaseDelegate purchaseDelegate, GameData data, PlayerID player); /** * It is the AI's turn to roll for technology. * * @param techDelegate * - the tech delegate to roll for * @param data * - the game data * @param player * - the player to roll tech for */ protected abstract void tech(ITechDelegate techDelegate, GameData data, PlayerID player); /** * It is the AI's turn to move. Make all moves before returning from this method. * * @param nonCombat * - are we moving in combat, or non combat * @param moveDel * - the move delegate to make moves with * @param data * - the current game data * @param player * - the player to move with */ protected abstract void move(boolean nonCombat, IMoveDelegate moveDel, GameData data, PlayerID player); /** * It is the AI's turn to place units. get the units available to place with player.getUnits() * * @param placeForBid * - is this a placement for bid * @param placeDelegate * - the place delegate to place with * @param data * - the current Game Data * @param player * - the player to place for */ protected abstract void place(boolean placeForBid, IAbstractPlaceDelegate placeDelegate, GameData data, PlayerID player); /** * No need to override this. */ protected void endTurn(final IAbstractForumPosterDelegate endTurnForumPosterDelegate, final GameData data, final PlayerID player) { // we should not override this... } /** * It is the AI's turn to fight. Subclasses may override this if they want, but * generally the AI does not need to worry about the order of fighting battles. * * @param battleDelegate * the battle delegate to query for battles not fought and the * @param data * - the current GameData * @param player * - the player to fight for */ protected void battle(final IBattleDelegate battleDelegate, final GameData data, final PlayerID player) { // generally all AI's will follow the same logic. // loop until all battles are fought. // rather than try to analyze battles to figure out which must be fought before others // as in the case of a naval battle preceding an amphibious attack, // keep trying to fight every battle while (true) { final BattleListing listing = battleDelegate.getBattles(); if (listing.isEmpty()) { return; } for (final Entry<BattleType, Collection<Territory>> entry : listing.getBattles().entrySet()) { for (final Territory current : entry.getValue()) { final String error = battleDelegate.fightBattle(current, entry.getKey().isBombingRun(), entry.getKey()); if (error != null) { s_logger.fine(error); } } } } } protected void politicalActions() { final IPoliticsDelegate iPoliticsDelegate = (IPoliticsDelegate) getPlayerBridge().getRemoteDelegate(); final GameData data = getGameData(); final PlayerID id = getPlayerID(); final float numPlayers = data.getPlayerList().getPlayers().size(); final PoliticsDelegate politicsDelegate = DelegateFinder.politicsDelegate(data); // We want to test the conditions each time to make sure they are still valid if (Math.random() < .5) { final List<PoliticalActionAttachment> actionChoicesTowardsWar = AIPoliticalUtils.getPoliticalActionsTowardsWar(id, politicsDelegate.getTestedConditions(), data); if (actionChoicesTowardsWar != null && !actionChoicesTowardsWar.isEmpty()) { Collections.shuffle(actionChoicesTowardsWar); int i = 0; // should we use bridge's random source here? final double random = Math.random(); int MAX_WAR_ACTIONS_PER_TURN = (random < .5 ? 0 : (random < .9 ? 1 : (random < .99 ? 2 : (int) numPlayers / 2))); if ((MAX_WAR_ACTIONS_PER_TURN > 0) && (Match.countMatches(data.getRelationshipTracker().getRelationships(id), Matches.RelationshipIsAtWar)) / numPlayers < 0.4) { if (Math.random() < .9) { MAX_WAR_ACTIONS_PER_TURN = 0; } else { MAX_WAR_ACTIONS_PER_TURN = 1; } } final Iterator<PoliticalActionAttachment> actionWarIter = actionChoicesTowardsWar.iterator(); while (actionWarIter.hasNext() && MAX_WAR_ACTIONS_PER_TURN > 0) { final PoliticalActionAttachment action = actionWarIter.next(); if (!Matches.AbstractUserActionAttachmentCanBeAttempted(politicsDelegate.getTestedConditions()) .match(action)) { continue; } i++; if (i > MAX_WAR_ACTIONS_PER_TURN) { break; } iPoliticsDelegate.attemptAction(action); } } } else { final List<PoliticalActionAttachment> actionChoicesOther = AIPoliticalUtils.getPoliticalActionsOther(id, politicsDelegate.getTestedConditions(), data); if (actionChoicesOther != null && !actionChoicesOther.isEmpty()) { Collections.shuffle(actionChoicesOther); int i = 0; // should we use bridge's random source here? final double random = Math.random(); final int MAX_OTHER_ACTIONS_PER_TURN = (random < .3 ? 0 : (random < .6 ? 1 : (random < .9 ? 2 : (random < .99 ? 3 : (int) numPlayers)))); final Iterator<PoliticalActionAttachment> actionOtherIter = actionChoicesOther.iterator(); while (actionOtherIter.hasNext() && MAX_OTHER_ACTIONS_PER_TURN > 0) { final PoliticalActionAttachment action = actionOtherIter.next(); if (!Matches.AbstractUserActionAttachmentCanBeAttempted(politicsDelegate.getTestedConditions()) .match(action)) { continue; } if (action.getCostPU() > 0 && action.getCostPU() > id.getResources().getQuantity(Constants.PUS)) { continue; } i++; if (i > MAX_OTHER_ACTIONS_PER_TURN) { break; } iPoliticsDelegate.attemptAction(action); } } } } /** * Pause the game to allow the human player to see what is going on. */ protected void pause() { ThreadUtil.sleep(AbstractUIContext.getAIPauseDuration()); } }