package games.strategy.triplea.oddsCalculator.ta; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.Callable; import games.strategy.engine.data.Change; import games.strategy.engine.data.CompositeChange; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Territory; import games.strategy.engine.data.TerritoryEffect; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitHitsChange; import games.strategy.engine.data.UnitType; import games.strategy.engine.data.UnitTypeList; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.engine.display.IDisplay; import games.strategy.engine.framework.GameDataUtils; import games.strategy.engine.framework.IGameModifiedChannel; import games.strategy.engine.gamePlayer.IRemotePlayer; import games.strategy.engine.history.DelegateHistoryWriter; import games.strategy.engine.history.IDelegateHistoryWriter; import games.strategy.engine.random.IRandomStats.DiceType; import games.strategy.engine.random.PlainRandomSource; import games.strategy.net.GUID; import games.strategy.sound.HeadlessSoundChannel; import games.strategy.sound.ISound; import games.strategy.triplea.ai.AIUtils; import games.strategy.triplea.ai.AbstractAI; import games.strategy.triplea.delegate.BattleTracker; import games.strategy.triplea.delegate.DiceRoll; import games.strategy.triplea.delegate.GameDelegateBridge; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.MustFightBattle; 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.ui.display.HeadlessDisplay; import games.strategy.triplea.ui.display.ITripleADisplay; import games.strategy.util.CompositeMatch; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.Match; import games.strategy.util.Tuple; public class OddsCalculator implements IOddsCalculator, Callable<AggregateResults> { public static final String OOL_ALL = "*"; public static final String OOL_ALL_REGEX = "\\*"; public static final String OOL_SEPARATOR = ";"; public static final String OOL_SEPARATOR_REGEX = ";"; public static final String OOL_AMOUNT_DESCRIPTOR = "^"; public static final String OOL_AMOUNT_DESCRIPTOR_REGEX = "\\^"; private GameData m_data = null; private PlayerID m_attacker = null; private PlayerID m_defender = null; private Territory m_location = null; private Collection<Unit> m_attackingUnits = new ArrayList<>(); private Collection<Unit> m_defendingUnits = new ArrayList<>(); private Collection<Unit> m_bombardingUnits = new ArrayList<>(); private Collection<TerritoryEffect> m_territoryEffects = new ArrayList<>(); private boolean m_keepOneAttackingLandUnit = false; private boolean m_amphibious = false; private int m_retreatAfterRound = -1; private int m_retreatAfterXUnitsLeft = -1; private boolean m_retreatWhenOnlyAirLeft = false; private String m_attackerOrderOfLosses = null; private String m_defenderOrderOfLosses = null; private int m_runCount = 0; private volatile boolean m_cancelled = false; private volatile boolean m_isDataSet = false; private volatile boolean m_isCalcSet = false; private volatile boolean m_isRunning = false; private final List<OddsCalculatorListener> m_listeners = new ArrayList<>(); public OddsCalculator(final GameData data) { this(data, false); } public OddsCalculator(final GameData data, final boolean dataHasAlreadyBeenCloned) { m_data = data == null ? null : (dataHasAlreadyBeenCloned ? data : GameDataUtils.cloneGameData(data, false)); if (data != null) { m_isDataSet = true; notifyListenersGameDataIsSet(); } } @Override public void setGameData(final GameData data) { if (m_isRunning) { return; } m_isDataSet = false; m_isCalcSet = false; m_data = (data == null ? null : GameDataUtils.cloneGameData(data, false)); // reset old data m_attacker = null; m_defender = null; m_location = null; m_attackingUnits = new ArrayList<>(); m_defendingUnits = new ArrayList<>(); m_bombardingUnits = new ArrayList<>(); m_territoryEffects = new ArrayList<>(); m_runCount = 0; if (data != null) { m_isDataSet = true; notifyListenersGameDataIsSet(); } } /** * Calculates odds using the stored game data. */ @Override @SuppressWarnings("unchecked") public void setCalculateData(final PlayerID attacker, final PlayerID defender, final Territory location, final Collection<Unit> attacking, final Collection<Unit> defending, final Collection<Unit> bombarding, final Collection<TerritoryEffect> territoryEffects, final int runCount) throws IllegalStateException { if (m_isRunning) { return; } m_isCalcSet = false; if (!m_isDataSet) { throw new IllegalStateException("Called set calculation before setting game data!"); } m_attacker = m_data.getPlayerList().getPlayerID((attacker == null ? PlayerID.NULL_PLAYERID.getName() : attacker.getName())); m_defender = m_data.getPlayerList().getPlayerID((defender == null ? PlayerID.NULL_PLAYERID.getName() : defender.getName())); m_location = m_data.getMap().getTerritory(location.getName()); m_attackingUnits = (Collection<Unit>) GameDataUtils.translateIntoOtherGameData(attacking, m_data); m_defendingUnits = (Collection<Unit>) GameDataUtils.translateIntoOtherGameData(defending, m_data); m_bombardingUnits = (Collection<Unit>) GameDataUtils.translateIntoOtherGameData(bombarding, m_data); m_territoryEffects = (Collection<TerritoryEffect>) GameDataUtils.translateIntoOtherGameData(territoryEffects, m_data); m_data.performChange(ChangeFactory.removeUnits(m_location, m_location.getUnits().getUnits())); m_data.performChange(ChangeFactory.addUnits(m_location, m_attackingUnits)); m_data.performChange(ChangeFactory.addUnits(m_location, m_defendingUnits)); m_runCount = runCount; m_isCalcSet = true; } @Override public AggregateResults setCalculateDataAndCalculate(final PlayerID attacker, final PlayerID defender, final Territory location, final Collection<Unit> attacking, final Collection<Unit> defending, final Collection<Unit> bombarding, final Collection<TerritoryEffect> territoryEffects, final int runCount) { setCalculateData(attacker, defender, location, attacking, defending, bombarding, territoryEffects, runCount); return calculate(); } @Override public AggregateResults calculate() { if (!getIsReady()) { throw new IllegalStateException("Called calculate before setting calculate data!"); } return calculate(m_runCount); } @Override public AggregateResults call() throws Exception { return calculate(); } @Override public boolean getIsReady() { return m_isDataSet && m_isCalcSet; } @Override public int getRunCount() { return m_runCount; } @Override public void setKeepOneAttackingLandUnit(final boolean bool) { m_keepOneAttackingLandUnit = bool; } @Override public void setAmphibious(final boolean bool) { m_amphibious = bool; } @Override public void setRetreatAfterRound(final int value) { m_retreatAfterRound = value; } @Override public void setRetreatAfterXUnitsLeft(final int value) { m_retreatAfterXUnitsLeft = value; } @Override public void setRetreatWhenOnlyAirLeft(final boolean value) { m_retreatWhenOnlyAirLeft = value; } @Override public void setAttackerOrderOfLosses(final String attackerOrderOfLosses) { m_attackerOrderOfLosses = attackerOrderOfLosses; } @Override public void setDefenderOrderOfLosses(final String defenderOrderOfLosses) { m_defenderOrderOfLosses = defenderOrderOfLosses; } @Override public void cancel() { m_cancelled = true; } @Override public void shutdown() { cancel(); synchronized (m_listeners) { m_listeners.clear(); } } @Override public int getThreadCount() { return 1; } private AggregateResults calculate(final int count) { m_isRunning = true; final long start = System.currentTimeMillis(); final AggregateResults rVal = new AggregateResults(count); final BattleTracker battleTracker = new BattleTracker(); // CasualtySortingCaching can cause issues if there is more than 1 one battle being calced at the same time (like if // the AI and a human // are both using the calc) // TODO: first, see how much it actually speeds stuff up by, and if it does make a difference then convert it to a // per-thread, per-calc // caching final List<Unit> attackerOrderOfLosses = OddsCalculator.getUnitListByOrderOfLoss(m_attackerOrderOfLosses, m_attackingUnits, m_data); final List<Unit> defenderOrderOfLosses = OddsCalculator.getUnitListByOrderOfLoss(m_defenderOrderOfLosses, m_defendingUnits, m_data); for (int i = 0; i < count && !m_cancelled; i++) { final CompositeChange allChanges = new CompositeChange(); final DummyDelegateBridge bridge1 = new DummyDelegateBridge(m_attacker, m_data, allChanges, attackerOrderOfLosses, defenderOrderOfLosses, m_keepOneAttackingLandUnit, m_retreatAfterRound, m_retreatAfterXUnitsLeft, m_retreatWhenOnlyAirLeft); final GameDelegateBridge bridge = new GameDelegateBridge(bridge1); final MustFightBattle battle = new MustFightBattle(m_location, m_attacker, m_data, battleTracker); battle.setHeadless(true); battle.isAmphibious(); battle.setUnits(m_defendingUnits, m_attackingUnits, m_bombardingUnits, (m_amphibious ? m_attackingUnits : new ArrayList<>()), m_defender, m_territoryEffects); // battle.setAttackingFromAndMap(attackingFromMap); bridge1.setBattle(battle); battle.fight(bridge); rVal.addResult(new BattleResults(battle, m_data)); // restore the game to its original state m_data.performChange(allChanges.invert()); battleTracker.clear(); battleTracker.clearBattleRecords(); } // BattleCalculator.DisableCasualtySortingCaching(); rVal.setTime(System.currentTimeMillis() - start); m_isRunning = false; m_cancelled = false; return rVal; } public static boolean isValidOrderOfLoss(final String orderOfLoss, final GameData data) { if (orderOfLoss == null || orderOfLoss.trim().length() == 0) { return true; } try { final String[] sections; if (orderOfLoss.contains(OOL_SEPARATOR)) { sections = orderOfLoss.trim().split(OOL_SEPARATOR_REGEX); } else { sections = new String[1]; sections[0] = orderOfLoss.trim(); } final UnitTypeList unitTypes; try { data.acquireReadLock(); unitTypes = data.getUnitTypeList(); } finally { data.releaseReadLock(); } for (final String section : sections) { if (section.length() == 0) { continue; } final String[] amountThenType = section.split(OOL_AMOUNT_DESCRIPTOR_REGEX); if (amountThenType.length != 2) { return false; } if (!amountThenType[0].equals(OOL_ALL)) { final int amount = Integer.parseInt(amountThenType[0]); if (amount <= 0) { return false; } } final UnitType type = unitTypes.getUnitType(amountThenType[1]); if (type == null) { return false; } } } catch (final Exception e) { return false; } return true; } private static List<Unit> getUnitListByOrderOfLoss(final String ool, final Collection<Unit> units, final GameData data) { if (ool == null || ool.trim().length() == 0) { return null; } final List<Tuple<Integer, UnitType>> map = new ArrayList<>(); final String[] sections; if (ool.contains(OOL_SEPARATOR)) { sections = ool.trim().split(OOL_SEPARATOR_REGEX); } else { sections = new String[1]; sections[0] = ool.trim(); } for (final String section : sections) { if (section.length() == 0) { continue; } final String[] amountThenType = section.split(OOL_AMOUNT_DESCRIPTOR_REGEX); final int amount = amountThenType[0].equals(OOL_ALL) ? Integer.MAX_VALUE : Integer.parseInt(amountThenType[0]); final UnitType type = data.getUnitTypeList().getUnitType(amountThenType[1]); map.add(Tuple.of(amount, type)); } Collections.reverse(map); final Set<Unit> unitsLeft = new HashSet<>(units); final List<Unit> order = new ArrayList<>(); for (final Tuple<Integer, UnitType> section : map) { final List<Unit> unitsOfType = Match.getNMatches(unitsLeft, section.getFirst(), Matches.unitIsOfType(section.getSecond())); order.addAll(unitsOfType); unitsLeft.removeAll(unitsOfType); } Collections.reverse(order); return order; } @Override public void addOddsCalculatorListener(final OddsCalculatorListener listener) { synchronized (m_listeners) { m_listeners.add(listener); } } @Override public void removeOddsCalculatorListener(final OddsCalculatorListener listener) { synchronized (m_listeners) { m_listeners.remove(listener); } } private void notifyListenersGameDataIsSet() { synchronized (m_listeners) { for (final OddsCalculatorListener listener : m_listeners) { listener.dataReady(); } } } } class DummyDelegateBridge implements IDelegateBridge { private final PlainRandomSource m_randomSource = new PlainRandomSource(); private final ITripleADisplay m_display = new HeadlessDisplay(); private final ISound m_soundChannel = new HeadlessSoundChannel(); private final DummyPlayer m_attackingPlayer; private final DummyPlayer m_defendingPlayer; private final PlayerID m_attacker; private final DelegateHistoryWriter m_writer = new DelegateHistoryWriter(new DummyGameModifiedChannel()); private final CompositeChange m_allChanges; private final GameData m_data; private MustFightBattle m_battle = null; public DummyDelegateBridge(final PlayerID attacker, final GameData data, final CompositeChange allChanges, final List<Unit> attackerOrderOfLosses, final List<Unit> defenderOrderOfLosses, final boolean attackerKeepOneLandUnit, final int retreatAfterRound, final int retreatAfterXUnitsLeft, final boolean retreatWhenOnlyAirLeft) { m_attackingPlayer = new DummyPlayer(this, true, "battle calc dummy", "None (AI)", attackerOrderOfLosses, attackerKeepOneLandUnit, retreatAfterRound, retreatAfterXUnitsLeft, retreatWhenOnlyAirLeft); m_defendingPlayer = new DummyPlayer(this, false, "battle calc dummy", "None (AI)", defenderOrderOfLosses, false, retreatAfterRound, -1, false); m_data = data; m_attacker = attacker; m_allChanges = allChanges; } @Override public GameData getData() { return m_data; } @Override public void leaveDelegateExecution() {} @Override public Properties getStepProperties() { throw new UnsupportedOperationException(); } @Override public String getStepName() { throw new UnsupportedOperationException(); } @Override public IRemotePlayer getRemotePlayer(final PlayerID id) { if (id.equals(m_attacker)) { return m_attackingPlayer; } else { return m_defendingPlayer; } } @Override public IRemotePlayer getRemotePlayer() { // the current player is attacker return m_attackingPlayer; } @Override public int[] getRandom(final int max, final int count, final PlayerID player, final DiceType diceType, final String annotation) { return m_randomSource.getRandom(max, count, annotation); } @Override public int getRandom(final int max, final PlayerID player, final DiceType diceType, final String annotation) { return m_randomSource.getRandom(max, annotation); } @Override public PlayerID getPlayerID() { return m_attacker; } @Override public IDelegateHistoryWriter getHistoryWriter() { return m_writer; } @Override public IDisplay getDisplayChannelBroadcaster() { return m_display; } @Override public ISound getSoundChannelBroadcaster() { return m_soundChannel; } @Override public void enterDelegateExecution() {} @Override public void addChange(final Change aChange) { if (!(aChange instanceof UnitHitsChange)) { return; } m_allChanges.add(aChange); m_data.performChange(aChange); } @Override public void stopGameSequence() {} public MustFightBattle getBattle() { return m_battle; } public void setBattle(final MustFightBattle battle) { m_battle = battle; } } class DummyGameModifiedChannel implements IGameModifiedChannel { @Override public void addChildToEvent(final String text, final Object renderingData) {} @Override public void gameDataChanged(final Change aChange) {} @Override public void shutDown() {} @Override public void startHistoryEvent(final String event) {} @Override public void stepChanged(final String stepName, final String delegateName, final PlayerID player, final int round, final String displayName, final boolean loadedFromSavedGame) {} @Override public void startHistoryEvent(final String event, final Object renderingData) {} } class DummyPlayer extends AbstractAI { private final boolean m_keepAtLeastOneLand; // negative = do not retreat private final int m_retreatAfterRound; // negative = do not retreat private final int m_retreatAfterXUnitsLeft; private final boolean m_retreatWhenOnlyAirLeft; private final DummyDelegateBridge m_bridge; private final boolean m_isAttacker; private final List<Unit> m_orderOfLosses; public DummyPlayer(final DummyDelegateBridge dummyDelegateBridge, final boolean attacker, final String name, final String type, final List<Unit> orderOfLosses, final boolean keepAtLeastOneLand, final int retreatAfterRound, final int retreatAfterXUnitsLeft, final boolean retreatWhenOnlyAirLeft) { super(name, type); m_keepAtLeastOneLand = keepAtLeastOneLand; m_retreatAfterRound = retreatAfterRound; m_retreatAfterXUnitsLeft = retreatAfterXUnitsLeft; m_retreatWhenOnlyAirLeft = retreatWhenOnlyAirLeft; m_bridge = dummyDelegateBridge; m_isAttacker = attacker; m_orderOfLosses = orderOfLosses; } private MustFightBattle getBattle() { return m_bridge.getBattle(); } private List<Unit> getOurUnits() { final MustFightBattle battle = getBattle(); if (battle == null) { return null; } return new ArrayList<>((m_isAttacker ? battle.getAttackingUnits() : battle.getDefendingUnits())); } private List<Unit> getEnemyUnits() { final MustFightBattle battle = getBattle(); if (battle == null) { return null; } return new ArrayList<>((m_isAttacker ? battle.getDefendingUnits() : battle.getAttackingUnits())); } @Override protected void move(final boolean nonCombat, final IMoveDelegate moveDel, final GameData data, final PlayerID player) {} @Override protected void place(final boolean placeForBid, final IAbstractPlaceDelegate placeDelegate, final GameData data, final PlayerID player) {} @Override protected void purchase(final boolean purcahseForBid, final int PUsToSpend, final IPurchaseDelegate purchaseDelegate, final GameData data, final PlayerID player) {} @Override protected void tech(final ITechDelegate techDelegate, final GameData data, final PlayerID player) {} @Override public boolean confirmMoveInFaceOfAA(final Collection<Territory> aaFiringTerritories) { throw new UnsupportedOperationException(); } @Override public Collection<Unit> getNumberOfFightersToMoveToNewCarrier(final Collection<Unit> fightersThatCanBeMoved, final Territory from) { throw new UnsupportedOperationException(); } /** * The battle calc doesn't actually care if you have available territories to retreat to or not. * It will always let you retreat to the 'current' territory (the battle territory), even if that is illegal. * This is because the battle calc does not know where the attackers are actually coming from. */ @Override public Territory retreatQuery(final GUID battleID, final boolean submerge, final Territory battleSite, final Collection<Territory> possibleTerritories, final String message) { // null = do not retreat if (possibleTerritories.isEmpty()) { return null; } if (submerge) { // submerge if all air vs subs final CompositeMatch<Unit> seaSub = new CompositeMatchAnd<>(Matches.UnitIsSea, Matches.UnitIsSub); final CompositeMatch<Unit> planeNotDestroyer = new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.UnitIsDestroyer.invert()); final List<Unit> ourUnits = getOurUnits(); final List<Unit> enemyUnits = getEnemyUnits(); if (ourUnits == null || enemyUnits == null) { return null; } if (enemyUnits.size() > 0 && Match.allMatch(ourUnits, seaSub) && Match.allMatch(enemyUnits, planeNotDestroyer)) { return possibleTerritories.iterator().next(); } return null; } else { final MustFightBattle battle = getBattle(); if (battle == null) { return null; } if (m_retreatAfterRound > -1 && battle.getBattleRound() >= m_retreatAfterRound) { return possibleTerritories.iterator().next(); } if (!m_retreatWhenOnlyAirLeft && m_retreatAfterXUnitsLeft <= -1) { return null; } final Collection<Unit> unitsLeft = m_isAttacker ? battle.getAttackingUnits() : battle.getDefendingUnits(); final Collection<Unit> airLeft = Match.getMatches(unitsLeft, Matches.UnitIsAir); if (m_retreatWhenOnlyAirLeft) { // lets say we have a bunch of 3 attack air unit, and a 4 attack non-air unit, // and we want to retreat when we have all air units left + that 4 attack non-air (cus it gets taken casualty // last) // then we add the number of air, to the retreat after X left number (which we would set to '1') int retreatNum = airLeft.size(); if (m_retreatAfterXUnitsLeft > 0) { retreatNum += m_retreatAfterXUnitsLeft; } if (retreatNum >= unitsLeft.size()) { return possibleTerritories.iterator().next(); } } if (m_retreatAfterXUnitsLeft > -1 && m_retreatAfterXUnitsLeft >= unitsLeft.size()) { return possibleTerritories.iterator().next(); } return null; } } // Added new collection autoKilled to handle killing units prior to casualty selection @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) { final List<Unit> rDamaged = new ArrayList<>(defaultCasualties.getDamaged()); final List<Unit> rKilled = new ArrayList<>(defaultCasualties.getKilled()); if (m_keepAtLeastOneLand) { final List<Unit> notKilled = new ArrayList<>(selectFrom); notKilled.removeAll(rKilled); // no land units left, but we have a non land unit to kill and land unit was killed if (!Match.someMatch(notKilled, Matches.UnitIsLand) && Match.someMatch(notKilled, Matches.UnitIsNotLand) && Match.someMatch(rKilled, Matches.UnitIsLand)) { final List<Unit> notKilledAndNotLand = Match.getMatches(notKilled, Matches.UnitIsNotLand); // sort according to cost Collections.sort(notKilledAndNotLand, AIUtils.getCostComparator()); // remove the last killed unit, this should be the strongest rKilled.remove(rKilled.size() - 1); // add the cheapest unit rKilled.add(notKilledAndNotLand.get(0)); } } if (m_orderOfLosses != null && !m_orderOfLosses.isEmpty() && !rKilled.isEmpty()) { final List<Unit> orderOfLosses = new ArrayList<>(m_orderOfLosses); orderOfLosses.retainAll(selectFrom); if (!orderOfLosses.isEmpty()) { int killedSize = rKilled.size(); rKilled.clear(); while (killedSize > 0 && !orderOfLosses.isEmpty()) { rKilled.add(orderOfLosses.get(0)); orderOfLosses.remove(0); killedSize--; } if (killedSize > 0) { final List<Unit> defaultKilled = new ArrayList<>(defaultCasualties.getKilled()); defaultKilled.removeAll(rKilled); while (killedSize > 0) { rKilled.add(defaultKilled.get(0)); defaultKilled.remove(0); killedSize--; } } } } final CasualtyDetails casualtyDetails = new CasualtyDetails(rKilled, rDamaged, false); return casualtyDetails; } @Override public Territory selectTerritoryForAirToLand(final Collection<Territory> candidates, final Territory currentTerritory, final String unitMessage) { throw new UnsupportedOperationException(); } @Override public boolean shouldBomberBomb(final Territory territory) { throw new UnsupportedOperationException(); } @Override public Unit whatShouldBomberBomb(final Territory territory, final Collection<Unit> potentialTargets, final Collection<Unit> bombers) { throw new UnsupportedOperationException(); } }