package games.strategy.triplea.ai.proAI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.GameStep;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.engine.framework.GameDataUtils;
import games.strategy.net.GUID;
import games.strategy.triplea.ai.AbstractAI;
import games.strategy.triplea.ai.proAI.data.ProBattleResult;
import games.strategy.triplea.ai.proAI.data.ProPurchaseTerritory;
import games.strategy.triplea.ai.proAI.data.ProTerritory;
import games.strategy.triplea.ai.proAI.logging.ProLogUI;
import games.strategy.triplea.ai.proAI.logging.ProLogger;
import games.strategy.triplea.ai.proAI.simulate.ProDummyDelegateBridge;
import games.strategy.triplea.ai.proAI.simulate.ProSimulateTurnUtils;
import games.strategy.triplea.ai.proAI.util.ProBattleUtils;
import games.strategy.triplea.ai.proAI.util.ProMatches;
import games.strategy.triplea.ai.proAI.util.ProOddsCalculator;
import games.strategy.triplea.ai.proAI.util.ProPurchaseUtils;
import games.strategy.triplea.ai.proAI.util.ProTransportUtils;
import games.strategy.triplea.attachments.PoliticalActionAttachment;
import games.strategy.triplea.delegate.BattleCalculator;
import games.strategy.triplea.delegate.BattleDelegate;
import games.strategy.triplea.delegate.DelegateFinder;
import games.strategy.triplea.delegate.DiceRoll;
import games.strategy.triplea.delegate.IBattle;
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.CasualtyDetails;
import games.strategy.triplea.delegate.dataObjects.CasualtyList;
import games.strategy.triplea.delegate.remote.IAbstractPlaceDelegate;
import games.strategy.triplea.delegate.remote.IMoveDelegate;
import games.strategy.triplea.delegate.remote.IPurchaseDelegate;
import games.strategy.triplea.delegate.remote.ITechDelegate;
import games.strategy.triplea.oddsCalculator.ta.ConcurrentOddsCalculator;
import games.strategy.triplea.oddsCalculator.ta.IOddsCalculator;
import games.strategy.triplea.ui.TripleAFrame;
import games.strategy.util.Match;
import games.strategy.util.Tuple;
/**
* Pro AI.
*/
public class ProAI extends AbstractAI {
private static final Logger s_logger = Logger.getLogger(ProAI.class.getName());
// Odds calculator
private static final IOddsCalculator concurrentCalc = new ConcurrentOddsCalculator("ProAI");
protected ProOddsCalculator calc;
// Phases
private final ProCombatMoveAI combatMoveAI;
private final ProNonCombatMoveAI nonCombatMoveAI;
private final ProPurchaseAI purchaseAI;
private final ProRetreatAI retreatAI;
private final ProScrambleAI scrambleAI;
private final ProPoliticsAI politicsAI;
private final ProBidAI bidAI;
// Data shared across phases
private Map<Territory, ProTerritory> storedCombatMoveMap;
private Map<Territory, ProTerritory> storedFactoryMoveMap;
private Map<Territory, ProPurchaseTerritory> storedPurchaseTerritories;
private List<PoliticalActionAttachment> storedPoliticalActions;
private List<Territory> storedStrafingTerritories;
public ProAI(final String name, final String type) {
super(name, type);
initializeCalc();
combatMoveAI = new ProCombatMoveAI(this);
nonCombatMoveAI = new ProNonCombatMoveAI(this);
purchaseAI = new ProPurchaseAI(this);
retreatAI = new ProRetreatAI(this);
scrambleAI = new ProScrambleAI(this);
politicsAI = new ProPoliticsAI(this);
bidAI = new ProBidAI();
storedCombatMoveMap = null;
storedFactoryMoveMap = null;
storedPurchaseTerritories = null;
storedPoliticalActions = null;
storedStrafingTerritories = new ArrayList<>();
}
protected void initializeCalc() {
calc = new ProOddsCalculator(concurrentCalc);
}
public ProOddsCalculator getCalc() {
return calc;
}
public static void initialize(final TripleAFrame frame) {
ProLogUI.initialize(frame);
ProLogger.info("Initialized Hard AI");
}
public static void showSettingsWindow() {
ProLogger.info("Showing Hard AI settings window");
ProLogUI.showSettingsWindow();
}
public static Logger getLogger() {
return s_logger;
}
public static void gameOverClearCache() {
// Are static, clear so that we don't keep the data around after a game is exited
concurrentCalc.setGameData(null);
ProLogUI.clearCachedInstances();
}
@Override
public void stopGame() {
super.stopGame(); // absolutely MUST call super.stopGame() first
calc.cancelCalcs();
}
private void initializeData() {
ProData.initialize(this);
}
public void setStoredStrafingTerritories(final List<Territory> strafingTerritories) {
storedStrafingTerritories = strafingTerritories;
}
@Override
protected void move(final boolean nonCombat, final IMoveDelegate moveDel, final GameData data,
final PlayerID player) {
final long start = System.currentTimeMillis();
BattleCalculator.clearOOLCache();
ProLogUI.notifyStartOfRound(data.getSequence().getRound(), player.getName());
initializeData();
calc.setData(data);
if (nonCombat) {
nonCombatMoveAI.doNonCombatMove(storedFactoryMoveMap, storedPurchaseTerritories, moveDel);
storedFactoryMoveMap = null;
} else {
if (storedCombatMoveMap == null) {
combatMoveAI.doCombatMove(moveDel);
} else {
combatMoveAI.doMove(storedCombatMoveMap, moveDel, data, player);
storedCombatMoveMap = null;
}
}
ProLogger
.info(player.getName() + " time for nonCombat=" + nonCombat + " time=" + (System.currentTimeMillis() - start));
}
@Override
protected void purchase(final boolean purchaseForBid, int PUsToSpend, final IPurchaseDelegate purchaseDelegate,
final GameData data, final PlayerID player) {
final long start = System.currentTimeMillis();
BattleCalculator.clearOOLCache();
ProLogUI.notifyStartOfRound(data.getSequence().getRound(), player.getName());
initializeData();
if (PUsToSpend <= 0) {
return;
}
if (purchaseForBid) {
bidAI.bid(PUsToSpend, purchaseDelegate, data, player);
} else {
// Repair factories
PUsToSpend = purchaseAI.repair(PUsToSpend, purchaseDelegate, data, player);
// Check if any place territories exist
final Map<Territory, ProPurchaseTerritory> purchaseTerritories = ProPurchaseUtils.findPurchaseTerritories(player);
final List<Territory> possibleFactoryTerritories = Match.getMatches(data.getMap().getTerritories(),
ProMatches.territoryHasNoInfraFactoryAndIsNotConqueredOwnedLand(player, data));
if (purchaseTerritories.isEmpty() && possibleFactoryTerritories.isEmpty()) {
ProLogger.info("No possible place or factory territories owned so exiting purchase logic");
return;
}
ProLogger.info("Starting simulation for purchase phase");
// Setup data copy and delegates
GameData dataCopy;
try {
data.acquireReadLock();
dataCopy = GameDataUtils.cloneGameData(data, true);
} catch (final Throwable t) {
ProLogger.log(Level.WARNING, "Error trying to clone game data for simulating phases", t);
return;
} finally {
data.releaseReadLock();
}
calc.setData(dataCopy);
final PlayerID playerCopy = dataCopy.getPlayerList().getPlayerID(player.getName());
final IMoveDelegate moveDel = DelegateFinder.moveDelegate(dataCopy);
final IDelegateBridge bridge = new ProDummyDelegateBridge(this, playerCopy, dataCopy);
moveDel.setDelegateBridgeAndPlayer(bridge);
// Determine turn sequence
final List<GameStep> gameSteps = new ArrayList<>();
for (final GameStep gameStep : dataCopy.getSequence()) {
gameSteps.add(gameStep);
}
// Simulate the next phases until place/end of turn is reached then use simulated data for purchase
final int nextStepIndex = dataCopy.getSequence().getStepIndex() + 1;
for (int i = nextStepIndex; i < gameSteps.size(); i++) {
final GameStep step = gameSteps.get(i);
if (!playerCopy.equals(step.getPlayerID())) {
continue;
}
dataCopy.getSequence().setRoundAndStep(dataCopy.getSequence().getRound(), step.getDisplayName(),
step.getPlayerID());
final String stepName = step.getName();
ProLogger.info("Simulating phase: " + stepName);
if (stepName.endsWith("NonCombatMove")) {
ProData.initializeSimulation(this, dataCopy, playerCopy);
final Map<Territory, ProTerritory> factoryMoveMap = nonCombatMoveAI.simulateNonCombatMove(moveDel);
if (storedFactoryMoveMap == null) {
storedFactoryMoveMap = ProSimulateTurnUtils.transferMoveMap(factoryMoveMap, data, player);
}
} else if (stepName.endsWith("CombatMove") && !stepName.endsWith("AirborneCombatMove")) {
ProData.initializeSimulation(this, dataCopy, playerCopy);
final Map<Territory, ProTerritory> moveMap = combatMoveAI.doCombatMove(moveDel);
if (storedCombatMoveMap == null) {
storedCombatMoveMap = ProSimulateTurnUtils.transferMoveMap(moveMap, data, player);
}
} else if (stepName.endsWith("Battle")) {
ProData.initializeSimulation(this, dataCopy, playerCopy);
ProSimulateTurnUtils.simulateBattles(dataCopy, playerCopy, bridge, calc);
} else if (stepName.endsWith("Place") || stepName.endsWith("EndTurn")) {
ProData.initializeSimulation(this, dataCopy, player);
storedPurchaseTerritories = purchaseAI.purchase(purchaseDelegate, data);
break;
} else if (stepName.endsWith("Politics")) {
ProData.initializeSimulation(this, dataCopy, player);
final PoliticsDelegate politicsDelegate = DelegateFinder.politicsDelegate(dataCopy);
politicsDelegate.setDelegateBridgeAndPlayer(bridge);
final List<PoliticalActionAttachment> actions = politicsAI.politicalActions();
if (storedPoliticalActions == null) {
storedPoliticalActions = actions;
}
}
}
}
ProLogger.info(player.getName() + " time for purchase=" + (System.currentTimeMillis() - start));
}
@Override
protected void place(final boolean bid, final IAbstractPlaceDelegate placeDelegate, final GameData data,
final PlayerID player) {
final long start = System.currentTimeMillis();
BattleCalculator.clearOOLCache();
ProLogUI.notifyStartOfRound(data.getSequence().getRound(), player.getName());
initializeData();
if (bid) {
bidAI.bidPlace(placeDelegate, data, player);
} else {
purchaseAI.place(storedPurchaseTerritories, placeDelegate);
storedPurchaseTerritories = null;
}
ProLogger.info(player.getName() + " time for place=" + (System.currentTimeMillis() - start));
}
@Override
protected void tech(final ITechDelegate techDelegate, final GameData data, final PlayerID player) {
ProTechAI.tech(techDelegate, data, player);
}
@Override
public Territory retreatQuery(final GUID battleID, final boolean submerge, final Territory battleTerritory,
final Collection<Territory> possibleTerritories, final String message) {
initializeData();
// Get battle data
final GameData data = getGameData();
final PlayerID player = getPlayerID();
final BattleDelegate delegate = DelegateFinder.battleDelegate(data);
final IBattle battle = delegate.getBattleTracker().getPendingBattle(battleID);
// If battle is null or amphibious then don't retreat
if (battle == null || battleTerritory == null || battle.isAmphibious()) {
return null;
}
// If attacker with more unit strength or strafing and isn't land battle with only air left then don't retreat
final boolean isAttacker = player.equals(battle.getAttacker());
final List<Unit> attackers = (List<Unit>) battle.getAttackingUnits();
final List<Unit> defenders = (List<Unit>) battle.getDefendingUnits();
final double strengthDifference = ProBattleUtils.estimateStrengthDifference(battleTerritory, attackers, defenders);
final boolean isStrafing = isAttacker && storedStrafingTerritories.contains(battleTerritory);
ProLogger.info(player.getName() + " checking retreat from territory " + battleTerritory + ", attackers="
+ attackers.size() + ", defenders=" + defenders.size() + ", submerge=" + submerge + ", attacker=" + isAttacker
+ ", isStrafing=" + isStrafing);
if ((isStrafing || (isAttacker && strengthDifference > 50))
&& (battleTerritory.isWater() || Match.someMatch(attackers, Matches.UnitIsLand))) {
return null;
}
calc.setData(getGameData());
return retreatAI.retreatQuery(battleID, submerge, battleTerritory, possibleTerritories, message);
}
@Override
public boolean shouldBomberBomb(final Territory territory) {
return combatMoveAI.isBombing();
}
// TODO: Consider supporting this functionality
@Override
public Collection<Unit> getNumberOfFightersToMoveToNewCarrier(final Collection<Unit> fightersThatCanBeMoved,
final Territory from) {
return new ArrayList<>();
}
@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) {
initializeData();
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);
}
// Consider unit cost
final CasualtyDetails myCasualties = new CasualtyDetails(false);
myCasualties.addToDamaged(defaultCasualties.getDamaged());
final List<Unit> selectFromSorted = new ArrayList<>(selectFrom);
if (enemyUnits.isEmpty()) {
Collections.sort(selectFromSorted, ProPurchaseUtils.getCostComparator());
} else {
// Get battle data
final GameData data = getGameData();
final PlayerID player = getPlayerID();
final BattleDelegate delegate = DelegateFinder.battleDelegate(data);
final IBattle battle = delegate.getBattleTracker().getPendingBattle(battleID);
// If defender and could lose battle then don't consider unit cost as just trying to survive
boolean needToCheck = true;
final boolean isAttacker = player.equals(battle.getAttacker());
if (!isAttacker) {
final List<Unit> attackers = (List<Unit>) battle.getAttackingUnits();
final List<Unit> defenders = (List<Unit>) battle.getDefendingUnits();
defenders.removeAll(defaultCasualties.getKilled());
final double strengthDifference = ProBattleUtils.estimateStrengthDifference(battlesite, attackers, defenders);
int minStrengthDifference = 60;
if (!games.strategy.triplea.Properties.getLow_Luck(data)) {
minStrengthDifference = 55;
}
if (strengthDifference > minStrengthDifference) {
needToCheck = false;
}
}
// Use bubble sort to save expensive units
while (needToCheck) {
needToCheck = false;
for (int i = 0; i < selectFromSorted.size() - 1; i++) {
final Unit unit1 = selectFromSorted.get(i);
final Unit unit2 = selectFromSorted.get(i + 1);
final double unitCost1 = ProPurchaseUtils.getCost(unit1);
final double unitCost2 = ProPurchaseUtils.getCost(unit2);
if (unitCost1 > 1.5 * unitCost2) {
selectFromSorted.set(i, unit2);
selectFromSorted.set(i + 1, unit1);
needToCheck = true;
}
}
}
}
// Interleave carriers and planes
final List<Unit> interleavedTargetList =
new ArrayList<>(ProTransportUtils.InterleaveUnits_CarriersAndPlanes(selectFromSorted, 0));
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;
}
/**
* Ask the player which units, if any, they want to scramble to defend against the attacker.
*
* @param scrambleTo
* - the territory we are scrambling to defend in, where the units will end up if scrambled
* @param possibleScramblers
* - possible units which we could scramble, with where they are from and how many allowed from that location
* @return a list of units to scramble mapped to where they are coming from
*/
@Override
public HashMap<Territory, Collection<Unit>> scrambleUnitsQuery(final Territory scrambleTo,
final Map<Territory, Tuple<Collection<Unit>, Collection<Unit>>> possibleScramblers) {
initializeData();
// Get battle data
final GameData data = getGameData();
final PlayerID player = getPlayerID();
final BattleDelegate delegate = DelegateFinder.battleDelegate(data);
final IBattle battle = delegate.getBattleTracker().getPendingBattle(scrambleTo, false, BattleType.NORMAL);
// If battle is null then don't scramble
if (battle == null) {
return null;
}
final List<Unit> attackers = (List<Unit>) battle.getAttackingUnits();
final List<Unit> defenders = (List<Unit>) battle.getDefendingUnits();
ProLogger.info(player.getName() + " checking scramble to " + scrambleTo + ", attackers=" + attackers.size()
+ ", defenders=" + defenders.size() + ", possibleScramblers=" + possibleScramblers);
calc.setData(getGameData());
return scrambleAI.scrambleUnitsQuery(scrambleTo, possibleScramblers);
}
@Override
public boolean selectAttackSubs(final Territory unitTerritory) {
initializeData();
// Get battle data
final GameData data = getGameData();
final PlayerID player = getPlayerID();
final BattleDelegate delegate = DelegateFinder.battleDelegate(data);
final IBattle battle = delegate.getBattleTracker().getPendingBattle(unitTerritory, false, BattleType.NORMAL);
// If battle is null then don't attack
if (battle == null) {
return false;
}
final List<Unit> attackers = (List<Unit>) battle.getAttackingUnits();
final List<Unit> defenders = (List<Unit>) battle.getDefendingUnits();
ProLogger.info(player.getName() + " checking sub attack in " + unitTerritory + ", attackers=" + attackers
+ ", defenders=" + defenders);
calc.setData(getGameData());
// Calculate battle results
final ProBattleResult result =
calc.calculateBattleResults(player, unitTerritory, attackers, defenders, new HashSet<>(), true);
ProLogger.debug(player.getName() + " sub attack TUVSwing=" + result.getTUVSwing());
return result.getTUVSwing() > 0;
}
@Override
public void politicalActions() {
initializeData();
if (storedPoliticalActions == null) {
politicsAI.politicalActions();
} else {
politicsAI.doActions(storedPoliticalActions);
storedPoliticalActions = null;
}
}
}