package games.strategy.triplea.delegate;
import java.io.Serializable;
import java.util.ArrayList;
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 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.Resource;
import games.strategy.engine.data.Route;
import games.strategy.engine.data.RouteScripted;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.UnitType;
import games.strategy.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.delegate.AutoSave;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.engine.message.IRemote;
import games.strategy.engine.random.IRandomStats.DiceType;
import games.strategy.triplea.MapSupport;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.PlayerAttachment;
import games.strategy.triplea.attachments.TerritoryAttachment;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.delegate.IBattle.BattleType;
import games.strategy.triplea.delegate.IBattle.WhoWon;
import games.strategy.triplea.delegate.dataObjects.BattleListing;
import games.strategy.triplea.delegate.dataObjects.BattleRecord;
import games.strategy.triplea.delegate.remote.IBattleDelegate;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.oddsCalculator.ta.BattleResults;
import games.strategy.triplea.player.ITripleAPlayer;
import games.strategy.util.CompositeMatch;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.CompositeMatchOr;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
import games.strategy.util.Tuple;
@MapSupport
@AutoSave(beforeStepStart = true, afterStepEnd = true)
public class BattleDelegate extends BaseTripleADelegate implements IBattleDelegate {
protected BattleTracker m_battleTracker = new BattleTracker();
// private OriginalOwnerTracker m_originalOwnerTracker = new OriginalOwnerTracker();
private boolean m_needToInitialize = true;
private boolean m_needToScramble = true;
private boolean m_needToKamikazeSuicideAttacks = true;
private boolean m_needToClearEmptyAirBattleAttacks = true;
private boolean m_needToAddBombardmentSources = true;
private boolean m_needToRecordBattleStatistics = true;
private boolean m_needToCheckDefendingPlanesCanLand = true;
private boolean m_needToCleanup = true;
protected IBattle m_currentBattle = null;
/**
* Called before the delegate will run, AND before "start" is called.
*/
@Override
public void setDelegateBridgeAndPlayer(final IDelegateBridge iDelegateBridge) {
super.setDelegateBridgeAndPlayer(new GameDelegateBridge(iDelegateBridge));
}
/**
* Called before the delegate will run.
*/
@Override
public void start() {
super.start();
// we may start multiple times due to loading after saving
// only initialize once
if (m_needToInitialize) {
doInitialize(m_battleTracker, m_bridge);
m_needToInitialize = false;
}
// do pre-combat stuff, like scrambling, after we have setup all battles, but before we have bombardment, etc.
// the order of all of this stuff matters quite a bit.
if (m_needToScramble) {
doScrambling();
m_needToScramble = false;
}
if (m_needToKamikazeSuicideAttacks) {
doKamikazeSuicideAttacks();
m_needToKamikazeSuicideAttacks = false;
}
if (m_needToClearEmptyAirBattleAttacks) {
clearEmptyAirBattleAttacks(m_battleTracker, m_bridge);
m_needToClearEmptyAirBattleAttacks = false;
}
if (m_needToAddBombardmentSources) {
addBombardmentSources();
m_needToAddBombardmentSources = false;
}
m_battleTracker.fightAirRaidsAndStrategicBombing(m_bridge);
m_battleTracker.fightDefenselessBattles(m_bridge);
m_battleTracker.fightBattleIfOnlyOne(m_bridge);
}
/**
* Called before the delegate will stop running.
*/
@Override
public void end() {
if (m_needToRecordBattleStatistics) {
getBattleTracker().sendBattleRecordsToGameData(m_bridge);
m_needToRecordBattleStatistics = false;
}
if (m_needToCleanup) {
getBattleTracker().clearBattleRecords();
scramblingCleanup();
airBattleCleanup();
m_needToCleanup = false;
}
if (m_needToCheckDefendingPlanesCanLand) {
checkDefendingPlanesCanLand();
m_needToCheckDefendingPlanesCanLand = false;
}
super.end();
m_needToInitialize = true;
m_needToScramble = true;
m_needToKamikazeSuicideAttacks = true;
m_needToClearEmptyAirBattleAttacks = true;
m_needToAddBombardmentSources = true;
m_needToRecordBattleStatistics = true;
m_needToCleanup = true;
m_needToCheckDefendingPlanesCanLand = true;
}
@Override
public Serializable saveState() {
final BattleExtendedDelegateState state = new BattleExtendedDelegateState();
state.superState = super.saveState();
// add other variables to state here:
state.m_battleTracker = m_battleTracker;
// state.m_originalOwnerTracker = m_originalOwnerTracker;
state.m_needToInitialize = m_needToInitialize;
state.m_needToScramble = m_needToScramble;
state.m_needToKamikazeSuicideAttacks = m_needToKamikazeSuicideAttacks;
state.m_needToClearEmptyAirBattleAttacks = m_needToClearEmptyAirBattleAttacks;
state.m_needToAddBombardmentSources = m_needToAddBombardmentSources;
state.m_needToRecordBattleStatistics = m_needToRecordBattleStatistics;
state.m_needToCheckDefendingPlanesCanLand = m_needToCheckDefendingPlanesCanLand;
state.m_needToCleanup = m_needToCleanup;
state.m_currentBattle = m_currentBattle;
return state;
}
@Override
public void loadState(final Serializable state) {
final BattleExtendedDelegateState s = (BattleExtendedDelegateState) state;
super.loadState(s.superState);
m_battleTracker = s.m_battleTracker;
m_needToInitialize = s.m_needToInitialize;
m_needToScramble = s.m_needToScramble;
m_needToKamikazeSuicideAttacks = s.m_needToKamikazeSuicideAttacks;
m_needToClearEmptyAirBattleAttacks = s.m_needToClearEmptyAirBattleAttacks;
m_needToAddBombardmentSources = s.m_needToAddBombardmentSources;
m_needToRecordBattleStatistics = s.m_needToRecordBattleStatistics;
m_needToCheckDefendingPlanesCanLand = s.m_needToCheckDefendingPlanesCanLand;
m_needToCleanup = s.m_needToCleanup;
m_currentBattle = s.m_currentBattle;
}
@Override
public boolean delegateCurrentlyRequiresUserInput() {
final BattleListing battles = getBattles();
if (battles.isEmpty()) {
final IBattle battle = getCurrentBattle();
return battle != null;
}
return true;
}
public static void doInitialize(final BattleTracker battleTracker, final IDelegateBridge aBridge) {
setupUnitsInSameTerritoryBattles(battleTracker, aBridge);
setupTerritoriesAbandonedToTheEnemy(battleTracker, aBridge);
// these are "blitzed" and "conquered" territories without a fight, without a pending
// battle
battleTracker.clearFinishedBattles(aBridge);
resetMaxScrambleCount(aBridge);
}
public static void clearEmptyAirBattleAttacks(final BattleTracker battleTracker, final IDelegateBridge aBridge) {
// these are air battle and air raids where there is no defender, probably because no
// air is in range to defend
battleTracker.clearEmptyAirBattleAttacks(aBridge);
}
@Override
public String fightCurrentBattle() {
if (m_currentBattle == null) {
return null;
}
// fight the battle
m_currentBattle.fight(m_bridge);
m_currentBattle = null;
// and were done
return null;
}
@Override
public String fightBattle(final Territory territory, final boolean bombing, final BattleType type) {
final IBattle battle = m_battleTracker.getPendingBattle(territory, bombing, type);
if (m_currentBattle != null && m_currentBattle != battle) {
return "Must finish " + getFightingWord(m_currentBattle) + " in " + m_currentBattle.getTerritory() + " first";
}
// does the battle exist
if (battle == null) {
return "No pending battle in" + territory.getName();
}
// are there battles that must occur first
final Collection<IBattle> allMustPrecede = m_battleTracker.getDependentOn(battle);
if (!allMustPrecede.isEmpty()) {
final IBattle firstPrecede = allMustPrecede.iterator().next();
final String name = firstPrecede.getTerritory().getName();
return "Must complete " + getFightingWord(firstPrecede) + " in " + name + " first";
}
m_currentBattle = battle;
// fight the battle
battle.fight(m_bridge);
m_currentBattle = null;
// and were done
return null;
}
private String getFightingWord(final IBattle battle) {
return battle.getBattleType().toString();
}
@Override
public BattleListing getBattles() {
return m_battleTracker.getPendingBattleSites();
}
private boolean isShoreBombardPerGroundUnitRestricted(final GameData data) {
return games.strategy.triplea.Properties.getShoreBombardPerGroundUnitRestricted(data);
}
public BattleTracker getBattleTracker() {
return m_battleTracker;
}
public IDelegateBridge getBattleBridge() {
return getBridge();
}
/**
* Add bombardment units to battles. Made public for test purposes only.
*/
void addBombardmentSources() {
final PlayerID attacker = m_bridge.getPlayerID();
final ITripleAPlayer remotePlayer = getRemotePlayer();
final Match<Unit> ownedAndCanBombard =
new CompositeMatchAnd<>(Matches.unitCanBombard(attacker), Matches.unitIsOwnedBy(attacker));
final Map<Territory, Collection<IBattle>> adjBombardment = getPossibleBombardingTerritories();
final Iterator<Territory> territories = adjBombardment.keySet().iterator();
final boolean shoreBombardPerGroundUnitRestricted = isShoreBombardPerGroundUnitRestricted(getData());
while (territories.hasNext()) {
final Territory t = territories.next();
if (!m_battleTracker.hasPendingBattle(t, false)) {
Collection<IBattle> battles = adjBombardment.get(t);
battles = Match.getMatches(battles, Matches.BattleIsAmphibious);
if (!battles.isEmpty()) {
final Collection<Unit> bombardUnits = t.getUnits().getMatches(ownedAndCanBombard);
final List<Unit> ListedBombardUnits = new ArrayList<>();
ListedBombardUnits.addAll(bombardUnits);
sortUnitsToBombard(ListedBombardUnits, attacker);
final Iterator<Unit> bombarding = ListedBombardUnits.iterator();
if (!bombardUnits.isEmpty()) {
// ask if they want to bombard
if (!remotePlayer.selectShoreBombard(t)) {
continue;
}
}
while (bombarding.hasNext()) {
final Unit u = bombarding.next();
final IBattle battle = selectBombardingBattle(u, t, battles);
if (battle != null) {
if (shoreBombardPerGroundUnitRestricted) {
if (battle.getAmphibiousLandAttackers().size() <= battle.getBombardingUnits().size()) {
battles.remove(battle);
break;
}
}
battle.addBombardingUnit(u);
}
}
}
}
}
}
/**
* Sort the specified units in preferred movement or unload order.
*/
private void sortUnitsToBombard(final List<Unit> units, final PlayerID player) {
if (units.isEmpty()) {
return;
}
Collections.sort(units, UnitComparator.getDecreasingAttackComparator(player));
}
/**
* Return map of adjacent territories along attack routes in battles where fighting will occur.
*/
private Map<Territory, Collection<IBattle>> getPossibleBombardingTerritories() {
final Map<Territory, Collection<IBattle>> possibleBombardingTerritories =
new HashMap<>();
final Iterator<Territory> battleTerritories = m_battleTracker.getPendingBattleSites(false).iterator();
while (battleTerritories.hasNext()) {
final Territory t = battleTerritories.next();
final IBattle battle = m_battleTracker.getPendingBattle(t, false, BattleType.NORMAL);
// we only care about battles where we must fight
// this check is really to avoid implementing getAttackingFrom() in other battle subclasses
if (!(battle instanceof MustFightBattle)) {
continue;
}
// bombarding can only occur in territories from which at least 1 land unit attacked
final Map<Territory, Collection<Unit>> attackingFromMap = ((MustFightBattle) battle).getAttackingFromMap();
final Iterator<Territory> bombardingTerritories = ((MustFightBattle) battle).getAttackingFrom().iterator();
while (bombardingTerritories.hasNext()) {
final Territory neighbor = bombardingTerritories.next();
// we do not allow bombarding from certain sea zones (like if there was a kamikaze suicide attack there, etc)
if (m_battleTracker.noBombardAllowedFromHere(neighbor)) {
continue;
}
// If all units from a territory are air- no bombard
if (Match.allMatch(attackingFromMap.get(neighbor), Matches.UnitIsAir)) {
continue;
}
Collection<IBattle> battles = possibleBombardingTerritories.get(neighbor);
if (battles == null) {
battles = new ArrayList<>();
possibleBombardingTerritories.put(neighbor, battles);
}
battles.add(battle);
}
}
return possibleBombardingTerritories;
}
/**
* Select which territory to bombard.
*/
private IBattle selectBombardingBattle(final Unit u, final Territory uTerritory, final Collection<IBattle> battles) {
final boolean bombardRestricted = isShoreBombardPerGroundUnitRestricted(getData());
// If only one battle to select from just return that battle
if ((battles.size() == 1)) {
return battles.iterator().next();
}
final List<Territory> territories = new ArrayList<>();
final Map<Territory, IBattle> battleTerritories = new HashMap<>();
final Iterator<IBattle> battlesIter = battles.iterator();
while (battlesIter.hasNext()) {
final IBattle battle = battlesIter.next();
// If Restricted & # of bombarding units => landing units, don't add territory to list to bombard
if (bombardRestricted) {
if (battle.getBombardingUnits().size() < battle.getAmphibiousLandAttackers().size()) {
territories.add(battle.getTerritory());
}
} else {
territories.add(battle.getTerritory());
}
battleTerritories.put(battle.getTerritory(), battle);
}
final ITripleAPlayer remotePlayer = getRemotePlayer();
Territory bombardingTerritory = null;
if (!territories.isEmpty()) {
bombardingTerritory = remotePlayer.selectBombardingTerritory(u, uTerritory, territories, true);
}
if (bombardingTerritory != null) {
return battleTerritories.get(bombardingTerritory);
}
// User elected not to bombard with this unit
return null;
}
private static void landParatroopers(final PlayerID player, final Territory battleSite,
final IDelegateBridge bridge) {
if (TechTracker.hasParatroopers(player)) {
final Collection<Unit> airTransports =
Match.getMatches(battleSite.getUnits().getUnits(), Matches.UnitIsAirTransport);
final Collection<Unit> paratroops =
Match.getMatches(battleSite.getUnits().getUnits(), Matches.UnitIsAirTransportable);
if (!airTransports.isEmpty() && !paratroops.isEmpty()) {
final CompositeChange change = new CompositeChange();
for (final Unit u : paratroops) {
final TripleAUnit taUnit = (TripleAUnit) u;
final Unit transport = taUnit.getTransportedBy();
if (transport == null || !airTransports.contains(transport)) {
continue;
}
change.add(TransportTracker.unloadAirTransportChange(taUnit, battleSite, player, false));
}
if (!change.isEmpty()) {
bridge.getHistoryWriter().startEvent(player.getName() + " lands units in " + battleSite.getName());
bridge.addChange(change);
}
}
}
}
/**
* Setup the battles where the battle occurs because units are in the
* same territory. This happens when subs emerge (after being submerged), and
* when naval units are placed in enemy occupied sea zones, and also
* when political relationships change and potentially leave units in now-hostile territories.
*/
private static void setupUnitsInSameTerritoryBattles(final BattleTracker battleTracker,
final IDelegateBridge aBridge) {
final PlayerID player = aBridge.getPlayerID();
final GameData data = aBridge.getData();
final boolean ignoreTransports = isIgnoreTransportInMovement(data);
final boolean ignoreSubs = isIgnoreSubInMovement(data);
final CompositeMatchAnd<Unit> seaTransports =
new CompositeMatchAnd<>(Matches.UnitIsTransportButNotCombatTransport, Matches.UnitIsSea);
final CompositeMatchOr<Unit> seaTranportsOrSubs = new CompositeMatchOr<>(seaTransports, Matches.UnitIsSub);
// we want to match all sea zones with our units and enemy units
final CompositeMatch<Territory> anyTerritoryWithOwnAndEnemy = new CompositeMatchAnd<>(
Matches.territoryHasUnitsOwnedBy(player), Matches.territoryHasEnemyUnits(player, data));
final CompositeMatch<Territory> enemyTerritoryAndOwnUnits = new CompositeMatchAnd<>(
Matches.isTerritoryEnemyAndNotUnownedWater(player, data), Matches.territoryHasUnitsOwnedBy(player));
final CompositeMatch<Territory> enemyUnitsOrEnemyTerritory =
new CompositeMatchOr<>(anyTerritoryWithOwnAndEnemy, enemyTerritoryAndOwnUnits);
final Iterator<Territory> battleTerritories =
Match.getMatches(data.getMap().getTerritories(), enemyUnitsOrEnemyTerritory).iterator();
while (battleTerritories.hasNext()) {
final Territory territory = battleTerritories.next();
final List<Unit> attackingUnits = territory.getUnits().getMatches(Matches.unitIsOwnedBy(player));
// now make sure to add any units that must move with these attacking units, so that they get included as
// dependencies
final Map<Unit, Collection<Unit>> transportMap = TransportTracker.transporting(territory.getUnits().getUnits());
final HashSet<Unit> dependants = new HashSet<>();
for (final Entry<Unit, Collection<Unit>> entry : transportMap.entrySet()) {
// only consider those transports that we are attacking with. allied and enemy transports are not added.
if (attackingUnits.contains(entry.getKey())) {
dependants.addAll(entry.getValue());
}
}
// no duplicates
dependants.removeAll(attackingUnits);
// add the dependants to the attacking list
attackingUnits.addAll(dependants);
final List<Unit> enemyUnits = territory.getUnits().getMatches(Matches.enemyUnit(player, data));
final IBattle bombingBattle = battleTracker.getPendingBattle(territory, true, null);
if (bombingBattle != null) {
// we need to remove any units which are participating in bombing raids
attackingUnits.removeAll(bombingBattle.getAttackingUnits());
}
if (attackingUnits.isEmpty() || Match.allMatch(attackingUnits, Matches.UnitIsInfrastructure)) {
continue;
}
IBattle battle = battleTracker.getPendingBattle(territory, false, BattleType.NORMAL);
if (battle == null) {
// we must land any paratroopers here, but only if there is not going to be a battle (cus battles land them
// separately, after aa
// fires)
if (enemyUnits.isEmpty() || Match.allMatch(enemyUnits, Matches.UnitIsInfrastructure)) {
landParatroopers(player, territory, aBridge);
}
aBridge.getHistoryWriter().startEvent(player.getName() + " creates battle in territory " + territory.getName());
battleTracker.addBattle(new RouteScripted(territory), attackingUnits, player, aBridge, null, null);
battle = battleTracker.getPendingBattle(territory, false, BattleType.NORMAL);
}
if (battle == null) {
continue;
}
if (bombingBattle != null) {
battleTracker.addDependency(battle, bombingBattle);
}
if (battle.isEmpty()) {
battle.addAttackChange(new RouteScripted(territory), attackingUnits, null);
}
if (!battle.getAttackingUnits().containsAll(attackingUnits)) {
List<Unit> attackingUnitsNeedToBeAdded = new ArrayList<>(attackingUnits);
attackingUnitsNeedToBeAdded.removeAll(battle.getAttackingUnits());
attackingUnitsNeedToBeAdded.removeAll(battle.getDependentUnits(battle.getAttackingUnits()));
if (territory.isWater()) {
attackingUnitsNeedToBeAdded = Match.getMatches(attackingUnitsNeedToBeAdded, Matches.UnitIsLand.invert());
} else {
attackingUnitsNeedToBeAdded = Match.getMatches(attackingUnitsNeedToBeAdded, Matches.UnitIsSea.invert());
}
if (!attackingUnitsNeedToBeAdded.isEmpty()) {
battle.addAttackChange(new RouteScripted(territory), attackingUnitsNeedToBeAdded, null);
}
}
// Reach stalemate if all attacking and defending units are transports
if ((ignoreTransports && Match.allMatch(attackingUnits, seaTransports)
&& Match.allMatch(enemyUnits, seaTransports))
|| ((Match.allMatch(attackingUnits, Matches.unitHasAttackValueOfAtLeast(1).invert()))
&& Match.allMatch(enemyUnits, Matches.unitHasDefendValueOfAtLeast(1).invert()))) {
final BattleResults results = new BattleResults(battle, WhoWon.DRAW, data);
battleTracker.getBattleRecords().addResultToBattle(player, battle.getBattleID(), null, 0, 0,
BattleRecord.BattleResultDescription.STALEMATE, results);
battle.cancelBattle(aBridge);
battleTracker.removeBattle(battle);
continue;
}
// possibility to ignore battle altogether
if (!attackingUnits.isEmpty()) {
final ITripleAPlayer remotePlayer = getRemotePlayer(aBridge);
if (territory.isWater() && games.strategy.triplea.Properties.getSeaBattlesMayBeIgnored(data)) {
if (!remotePlayer.selectAttackUnits(territory)) {
final BattleResults results = new BattleResults(battle, WhoWon.NOTFINISHED, data);
battleTracker.getBattleRecords().addResultToBattle(player, battle.getBattleID(), null, 0, 0,
BattleRecord.BattleResultDescription.NO_BATTLE, results);
battle.cancelBattle(aBridge);
battleTracker.removeBattle(battle);
}
continue;
}
// Check for ignored units
if (ignoreTransports || ignoreSubs) {
// TODO check if incoming units can attack before asking
// if only enemy transports... attack them?
if (ignoreTransports && Match.allMatch(enemyUnits, seaTransports)) {
if (!remotePlayer.selectAttackTransports(territory)) {
final BattleResults results = new BattleResults(battle, WhoWon.NOTFINISHED, data);
battleTracker.getBattleRecords().addResultToBattle(player, battle.getBattleID(), null, 0, 0,
BattleRecord.BattleResultDescription.NO_BATTLE, results);
battle.cancelBattle(aBridge);
battleTracker.removeBattle(battle);
}
continue;
}
// if only enemy subs... attack them?
if (ignoreSubs && Match.allMatch(enemyUnits, Matches.UnitIsSub)) {
if (!remotePlayer.selectAttackSubs(territory)) {
final BattleResults results = new BattleResults(battle, WhoWon.NOTFINISHED, data);
battleTracker.getBattleRecords().addResultToBattle(player, battle.getBattleID(), null, 0, 0,
BattleRecord.BattleResultDescription.NO_BATTLE, results);
battle.cancelBattle(aBridge);
battleTracker.removeBattle(battle);
}
continue;
}
// if only enemy transports and subs... attack them?
if (ignoreSubs && ignoreTransports && Match.allMatch(enemyUnits, seaTranportsOrSubs)) {
if (!remotePlayer.selectAttackUnits(territory)) {
final BattleResults results = new BattleResults(battle, WhoWon.NOTFINISHED, data);
battleTracker.getBattleRecords().addResultToBattle(player, battle.getBattleID(), null, 0, 0,
BattleRecord.BattleResultDescription.NO_BATTLE, results);
battle.cancelBattle(aBridge);
battleTracker.removeBattle(battle);
}
}
}
}
}
}
/**
* Setup the battles where we have abandoned a contested territory during combat move to the enemy.
* The enemy then takes over the territory in question.
*/
private static void setupTerritoriesAbandonedToTheEnemy(final BattleTracker battleTracker,
final IDelegateBridge aBridge) {
final GameData data = aBridge.getData();
if (!games.strategy.triplea.Properties.getAbandonedTerritoriesMayBeTakenOverImmediately(data)) {
return;
}
final PlayerID player = aBridge.getPlayerID();
final Iterator<Territory> battleTerritories = Match.getMatches(data.getMap().getTerritories(), Matches
.territoryHasEnemyUnitsThatCanCaptureTerritoryAndTerritoryOwnedByTheirEnemyAndIsNotUnownedWater(player, data))
.iterator();
// all territories that contain enemy units, where the territory is owned by an enemy of these units
while (battleTerritories.hasNext()) {
final Territory territory = battleTerritories.next();
final List<Unit> abandonedToUnits = territory.getUnits().getMatches(Matches.enemyUnit(player, data));
final PlayerID abandonedToPlayer = AbstractBattle.findPlayerWithMostUnits(abandonedToUnits);
{
// now make sure to add any units that must move with these units, so that they get included as dependencies
final Map<Unit, Collection<Unit>> transportMap = TransportTracker.transporting(territory.getUnits().getUnits());
final HashSet<Unit> dependants = new HashSet<>();
for (final Entry<Unit, Collection<Unit>> entry : transportMap.entrySet()) {
// only consider those transports that are part of our group
if (abandonedToUnits.contains(entry.getKey())) {
dependants.addAll(entry.getValue());
}
}
// no duplicates
dependants.removeAll(abandonedToUnits);
// add the dependants to the attacking list
abandonedToUnits.addAll(dependants);
}
// either we have abandoned the territory (so there are no more units that are enemy units of our enemy units)
// or we are possibly bombing the territory (so we may have units there still)
final Set<Unit> enemyUnitsOfAbandonedToUnits = new HashSet<>();
final Set<PlayerID> enemyPlayers = new HashSet<>();
for (final Unit u : abandonedToUnits) {
enemyPlayers.add(u.getOwner());
}
for (final PlayerID p : enemyPlayers) {
final CompositeMatchAnd<Unit> canPreventCapture = new CompositeMatchAnd<>(Matches.unitIsEnemyOf(data, p),
Matches.UnitIsNotAir, Matches.UnitIsNotInfrastructure);
enemyUnitsOfAbandonedToUnits.addAll(territory.getUnits().getMatches(canPreventCapture));
}
// only look at bombing battles, because otherwise the normal attack will determine the ownership of the territory
final IBattle bombingBattle = battleTracker.getPendingBattle(territory, true, null);
if (bombingBattle != null) {
enemyUnitsOfAbandonedToUnits.removeAll(bombingBattle.getAttackingUnits());
}
if (!enemyUnitsOfAbandonedToUnits.isEmpty()) {
continue;
}
final IBattle nonFightingBattle = battleTracker.getPendingBattle(territory, false, BattleType.NORMAL);
if (nonFightingBattle != null) {
throw new IllegalStateException("Should not be possible to have a normal battle in: " + territory.getName()
+ " and have abandoned or only bombing there too.");
}
aBridge.getHistoryWriter().startEvent(
player.getName() + " has abandoned " + territory.getName() + " to " + abandonedToPlayer.getName(),
abandonedToUnits);
battleTracker.takeOver(territory, abandonedToPlayer, aBridge, null, abandonedToUnits);
// TODO: if there are multiple defending unit owners, allow picking which one takes over the territory
}
}
private void doScrambling() {
// first, figure out all the territories where scrambling units could scramble to
// then ask the defending player if they wish to scramble units there, and actually move the units there
final GameData data = getData();
if (!games.strategy.triplea.Properties.getScramble_Rules_In_Effect(data)) {
return;
}
final boolean fromIslandOnly = games.strategy.triplea.Properties.getScramble_From_Island_Only(data);
final boolean toSeaOnly = games.strategy.triplea.Properties.getScramble_To_Sea_Only(data);
final boolean toAnyAmphibious = games.strategy.triplea.Properties.getScrambleToAnyAmphibiousAssault(data);
final boolean toSBR = games.strategy.triplea.Properties.getCanScrambleIntoAirBattles(data);
int maxScrambleDistance = 0;
final Iterator<UnitType> utIter = data.getUnitTypeList().iterator();
while (utIter.hasNext()) {
final UnitAttachment ua = UnitAttachment.get(utIter.next());
if (ua.getCanScramble() && maxScrambleDistance < ua.getMaxScrambleDistance()) {
maxScrambleDistance = ua.getMaxScrambleDistance();
}
}
final Match<Unit> airbasesCanScramble = new CompositeMatchAnd<>(Matches.unitIsEnemyOf(data, m_player),
Matches.UnitIsAirBase, Matches.UnitIsNotDisabled, Matches.unitIsBeingTransported().invert());
final CompositeMatchAnd<Territory> canScramble = new CompositeMatchAnd<>(
new CompositeMatchOr<Territory>(Matches.TerritoryIsWater, Matches.isTerritoryEnemy(m_player, data)),
Matches.territoryHasUnitsThatMatch(new CompositeMatchAnd<>(Matches.UnitCanScramble,
Matches.unitIsEnemyOf(data, m_player), Matches.UnitIsNotDisabled)),
Matches.territoryHasUnitsThatMatch(airbasesCanScramble));
if (fromIslandOnly) {
canScramble.add(Matches.TerritoryIsIsland);
}
final HashMap<Territory, HashSet<Territory>> scrambleTerrs = new HashMap<>();
final Set<Territory> territoriesWithBattles =
m_battleTracker.getPendingBattleSites().getNormalBattlesIncludingAirBattles();
if (toSBR) {
territoriesWithBattles
.addAll(m_battleTracker.getPendingBattleSites().getStrategicBombingRaidsIncludingAirBattles());
}
final Set<Territory> territoriesWithBattlesWater = new HashSet<>();
final Set<Territory> territoriesWithBattlesLand = new HashSet<>();
territoriesWithBattlesWater.addAll(Match.getMatches(territoriesWithBattles, Matches.TerritoryIsWater));
territoriesWithBattlesLand.addAll(Match.getMatches(territoriesWithBattles, Matches.TerritoryIsLand));
for (final Territory battleTerr : territoriesWithBattlesWater) {
final HashSet<Territory> canScrambleFrom = new HashSet<>(
Match.getMatches(data.getMap().getNeighbors(battleTerr, maxScrambleDistance), canScramble));
if (!canScrambleFrom.isEmpty()) {
scrambleTerrs.put(battleTerr, canScrambleFrom);
}
}
for (final Territory battleTerr : territoriesWithBattlesLand) {
if (!toSeaOnly) {
final HashSet<Territory> canScrambleFrom = new HashSet<>(
Match.getMatches(data.getMap().getNeighbors(battleTerr, maxScrambleDistance), canScramble));
if (!canScrambleFrom.isEmpty()) {
scrambleTerrs.put(battleTerr, canScrambleFrom);
}
}
final IBattle battle = m_battleTracker.getPendingBattle(battleTerr, false, BattleType.NORMAL);
// do not forget we may already have the territory in the list, so we need to add to the collection, not overwrite
// it.
if (battle != null && battle.isAmphibious() && battle instanceof DependentBattle) {
final Collection<Territory> amphibFromTerrs = ((DependentBattle) battle).getAmphibiousAttackTerritories();
amphibFromTerrs.removeAll(territoriesWithBattlesWater);
for (final Territory amphibFrom : amphibFromTerrs) {
HashSet<Territory> canScrambleFrom = scrambleTerrs.get(amphibFrom);
if (canScrambleFrom == null) {
canScrambleFrom = new HashSet<>();
}
if (toAnyAmphibious) {
canScrambleFrom
.addAll(Match.getMatches(data.getMap().getNeighbors(amphibFrom, maxScrambleDistance), canScramble));
} else if (canScramble.match(battleTerr)) {
canScrambleFrom.add(battleTerr);
}
if (!canScrambleFrom.isEmpty()) {
scrambleTerrs.put(amphibFrom, canScrambleFrom);
}
}
}
}
// now scrambleTerrs is a list of places we can scramble from
if (scrambleTerrs.isEmpty()) {
return;
}
final HashMap<Tuple<Territory, PlayerID>, Collection<HashMap<Territory, Tuple<Collection<Unit>, Collection<Unit>>>>> scramblersByTerritoryPlayer =
new HashMap<>();
for (final Territory to : scrambleTerrs.keySet()) {
final HashMap<Territory, Tuple<Collection<Unit>, Collection<Unit>>> scramblers =
new HashMap<>();
// find who we should ask
PlayerID defender = null;
if (m_battleTracker.hasPendingBattle(to, false)) {
defender = AbstractBattle.findDefender(to, m_player, data);
}
for (final Territory from : scrambleTerrs.get(to)) {
if (defender == null) {
defender = AbstractBattle.findDefender(from, m_player, data);
}
// find how many is the max this territory can scramble
final Collection<Unit> airbases = from.getUnits().getMatches(airbasesCanScramble);
final int maxCanScramble = getMaxScrambleCount(airbases);
final Route toBattleRoute = data.getMap().getRoute_IgnoreEnd(from, to, Matches.TerritoryIsNotImpassable);
final Collection<Unit> canScrambleAir = from.getUnits()
.getMatches(new CompositeMatchAnd<>(Matches.unitIsEnemyOf(data, m_player), Matches.UnitCanScramble,
Matches.UnitIsNotDisabled, Matches.UnitWasScrambled.invert(),
Matches.unitCanScrambleOnRouteDistance(toBattleRoute)));
if (maxCanScramble > 0 && !canScrambleAir.isEmpty()) {
scramblers.put(from, Tuple.of(airbases, canScrambleAir));
}
}
if (defender == null || scramblers.isEmpty()) {
continue;
}
final Tuple<Territory, PlayerID> terrPlayer = Tuple.of(to, defender);
Collection<HashMap<Territory, Tuple<Collection<Unit>, Collection<Unit>>>> tempScrambleList =
scramblersByTerritoryPlayer.get(terrPlayer);
if (tempScrambleList == null) {
tempScrambleList = new ArrayList<>();
}
tempScrambleList.add(scramblers);
scramblersByTerritoryPlayer.put(terrPlayer, tempScrambleList);
}
// now scramble them
for (final Tuple<Territory, PlayerID> terrPlayer : scramblersByTerritoryPlayer.keySet()) {
final Territory to = terrPlayer.getFirst();
final PlayerID defender = terrPlayer.getSecond();
if (defender == null || defender.isNull()) {
continue;
}
boolean scrambledHere = false;
for (final HashMap<Territory, Tuple<Collection<Unit>, Collection<Unit>>> scramblers : scramblersByTerritoryPlayer
.get(terrPlayer)) {
// verify that we didn't already scramble any of these units
final Iterator<Territory> tIter = scramblers.keySet().iterator();
while (tIter.hasNext()) {
final Territory t = tIter.next();
scramblers.get(t).getSecond().retainAll(t.getUnits().getUnits());
if (scramblers.get(t).getSecond().isEmpty()) {
tIter.remove();
}
}
if (scramblers.isEmpty()) {
continue;
}
final HashMap<Territory, Collection<Unit>> toScramble =
getRemotePlayer(defender).scrambleUnitsQuery(to, scramblers);
if (toScramble == null) {
continue;
}
// verify max allowed
if (!scramblers.keySet().containsAll(toScramble.keySet())) {
throw new IllegalStateException("Trying to scramble from illegal territory");
}
for (final Territory t : scramblers.keySet()) {
if (toScramble.get(t) == null) {
continue;
}
if (toScramble.get(t).size() > getMaxScrambleCount(scramblers.get(t).getFirst())) {
throw new IllegalStateException("Trying to scramble " + toScramble.get(t).size() + " out of " + t.getName()
+ ", but max allowed is " + scramblers.get(t).getFirst());
}
}
final CompositeChange change = new CompositeChange();
for (final Territory t : toScramble.keySet()) {
final Collection<Unit> scrambling = toScramble.get(t);
if (scrambling == null || scrambling.isEmpty()) {
continue;
}
int numberScrambled = scrambling.size();
final Collection<Unit> airbases = t.getUnits().getMatches(airbasesCanScramble);
final int maxCanScramble = getMaxScrambleCount(airbases);
if (maxCanScramble != Integer.MAX_VALUE) {
// TODO: maybe sort from biggest to smallest first?
for (final Unit airbase : airbases) {
final int allowedScramble = ((TripleAUnit) airbase).getMaxScrambleCount();
int newAllowed = allowedScramble;
if (allowedScramble > 0) {
if (allowedScramble >= numberScrambled) {
newAllowed = allowedScramble - numberScrambled;
numberScrambled = 0;
} else {
newAllowed = 0;
numberScrambled -= allowedScramble;
}
change.add(ChangeFactory.unitPropertyChange(airbase, newAllowed, TripleAUnit.MAX_SCRAMBLE_COUNT));
}
if (numberScrambled <= 0) {
break;
}
}
}
for (final Unit u : scrambling) {
change.add(ChangeFactory.unitPropertyChange(u, t, TripleAUnit.ORIGINATED_FROM));
change.add(ChangeFactory.unitPropertyChange(u, true, TripleAUnit.WAS_SCRAMBLED));
}
// should we mark combat, or call setupUnitsInSameTerritoryBattles again?
change.add(ChangeFactory.moveUnits(t, to, scrambling));
m_bridge.getHistoryWriter().startEvent(defender.getName() + " scrambles " + scrambling.size()
+ " units out of " + t.getName() + " to defend against the attack in " + to.getName(), scrambling);
scrambledHere = true;
}
if (!change.isEmpty()) {
m_bridge.addChange(change);
}
}
if (!scrambledHere) {
continue;
}
// make sure the units join the battle, or create a new battle.
final IBattle bombing = m_battleTracker.getPendingBattle(to, true, null);
IBattle battle = m_battleTracker.getPendingBattle(to, false, BattleType.NORMAL);
if (battle == null) {
final List<Unit> attackingUnits = to.getUnits().getMatches(Matches.unitIsOwnedBy(m_player));
if (bombing != null) {
attackingUnits.removeAll(bombing.getAttackingUnits());
}
// no need to create a "bombing" battle or air battle, because those are set up automatically whenever the map
// allows scrambling
// into an air battle / air raid
if (attackingUnits.isEmpty()) {
continue;
}
m_bridge.getHistoryWriter()
.startEvent(defender.getName() + " scrambles to create a battle in territory " + to.getName());
// TODO: the attacking sea units do not remember where they came from, so they cannot retreat anywhere. Need to
// fix.
m_battleTracker.addBattle(new RouteScripted(to), attackingUnits, m_player, m_bridge, null, null);
battle = m_battleTracker.getPendingBattle(to, false, BattleType.NORMAL);
if (battle instanceof MustFightBattle) {
// this is an ugly mess of hacks, but will have to stay here till all transport related code is gutted and
// refactored.
final MustFightBattle mfb = (MustFightBattle) battle;
final Collection<Territory> neighborsLand = data.getMap().getNeighbors(to, Matches.TerritoryIsLand);
if (Match.someMatch(attackingUnits, Matches.UnitIsTransport)) {
// first, we have to reset the "transportedBy" setting for all the land units that were offloaded
final CompositeChange change1 = new CompositeChange();
mfb.reLoadTransports(attackingUnits, change1);
if (!change1.isEmpty()) {
m_bridge.addChange(change1);
}
// after that is applied, we have to make a map of all dependencies
final Map<Territory, Map<Unit, Collection<Unit>>> dependencies =
new HashMap<>();
final Map<Unit, Collection<Unit>> dependenciesForMFB =
TransportTracker.transporting(attackingUnits, attackingUnits);
for (final Unit transport : Match.getMatches(attackingUnits, Matches.UnitIsTransport)) {
// however, the map we add to the newly created battle, cannot hold any units that are NOT in this
// territory.
// BUT it must still hold all transports
if (!dependenciesForMFB.containsKey(transport)) {
dependenciesForMFB.put(transport, new ArrayList<>());
}
}
dependencies.put(to, dependenciesForMFB);
for (final Territory t : neighborsLand) {
// All other maps, must hold only the transported units that in their territory
final Collection<Unit> allNeighborUnits =
new ArrayList<>(Match.getMatches(attackingUnits, Matches.UnitIsTransport));
allNeighborUnits.addAll(t.getUnits().getMatches(Matches.unitIsLandAndOwnedBy(m_player)));
final Map<Unit, Collection<Unit>> dependenciesForNeighbors =
TransportTracker.transporting(Match.getMatches(allNeighborUnits, Matches.UnitIsTransport),
Match.getMatches(allNeighborUnits, Matches.UnitIsTransport.invert()));
dependencies.put(t, dependenciesForNeighbors);
}
mfb.addDependentUnits(dependencies.get(to));
for (final Territory territoryNeighborToNewBattle : neighborsLand) {
final IBattle battleInTerritoryNeighborToNewBattle =
m_battleTracker.getPendingBattle(territoryNeighborToNewBattle, false, BattleType.NORMAL);
if (battleInTerritoryNeighborToNewBattle != null
&& battleInTerritoryNeighborToNewBattle instanceof MustFightBattle) {
final MustFightBattle mfbattleInTerritoryNeighborToNewBattle =
(MustFightBattle) battleInTerritoryNeighborToNewBattle;
mfbattleInTerritoryNeighborToNewBattle
.addDependentUnits(dependencies.get(territoryNeighborToNewBattle));
} else if (battleInTerritoryNeighborToNewBattle != null
&& battleInTerritoryNeighborToNewBattle instanceof NonFightingBattle) {
final NonFightingBattle nfbattleInTerritoryNeighborToNewBattle =
(NonFightingBattle) battleInTerritoryNeighborToNewBattle;
nfbattleInTerritoryNeighborToNewBattle
.addDependentUnits(dependencies.get(territoryNeighborToNewBattle));
}
}
}
if (Match.someMatch(attackingUnits, Matches.UnitIsAir.invert())) {
// TODO: for now, we will hack and say that the attackers came from Everywhere, and hope the user will
// choose the correct place
// to retreat to! (TODO: Fix this)
final Map<Territory, Collection<Unit>> attackingFromMap = new HashMap<>();
final Collection<Territory> neighbors = data.getMap().getNeighbors(to,
(Matches.TerritoryIsLand.match(to) ? Matches.TerritoryIsLand : Matches.TerritoryIsWater));
// neighbors.removeAll(territoriesWithBattles);
// neighbors.removeAll(Match.getMatches(neighbors, Matches.territoryHasEnemyUnits(m_player, data)));
for (final Territory t : neighbors) {
attackingFromMap.put(t, attackingUnits);
}
mfb.setAttackingFromAndMap(attackingFromMap);
}
}
} else if (battle instanceof MustFightBattle) {
((MustFightBattle) battle).resetDefendingUnits(m_player, data);
}
// now make sure any amphibious battles that are dependent on this 'new' sea battle have their dependencies set.
if (to.isWater()) {
for (final Territory t : data.getMap().getNeighbors(to, Matches.TerritoryIsLand)) {
final IBattle battleAmphib = m_battleTracker.getPendingBattle(t, false, BattleType.NORMAL);
if (battleAmphib != null) {
if (!m_battleTracker.getDependentOn(battle).contains(battleAmphib)) {
m_battleTracker.addDependency(battleAmphib, battle);
}
if (battleAmphib instanceof MustFightBattle) {
// and we want to reset the defenders if the scrambling air has left that battle
((MustFightBattle) battleAmphib).resetDefendingUnits(m_player, data);
}
}
}
}
}
}
public static int getMaxScrambleCount(final Collection<Unit> airbases) {
if (!Match.allMatch(airbases, new CompositeMatchAnd<>(Matches.UnitIsAirBase, Matches.UnitIsNotDisabled))) {
throw new IllegalStateException("All units must be viable airbases");
}
// find how many is the max this territory can scramble
int maxScrambled = 0;
for (final Unit base : airbases) {
final int baseMax = ((TripleAUnit) base).getMaxScrambleCount();
if (baseMax == -1) {
return Integer.MAX_VALUE;
}
maxScrambled += baseMax;
}
return maxScrambled;
}
private void scramblingCleanup() {
// return scrambled units to their original territories, or let them move 1 or x to a new territory.
final GameData data = getData();
if (!games.strategy.triplea.Properties.getScramble_Rules_In_Effect(data)) {
return;
}
final boolean mustReturnToBase = games.strategy.triplea.Properties.getScrambled_Units_Return_To_Base(data);
for (final Territory t : data.getMap().getTerritories()) {
int carrierCostOfCurrentTerr = 0;
final Collection<Unit> wasScrambled = t.getUnits().getMatches(Matches.UnitWasScrambled);
for (final Unit u : wasScrambled) {
final CompositeChange change = new CompositeChange();
final Territory originatedFrom = TripleAUnit.get(u).getOriginatedFrom();
Territory landingTerr = null;
String historyText = "";
if (!mustReturnToBase || !Matches.isTerritoryAllied(u.getOwner(), data).match(originatedFrom)) {
final Collection<Territory> possible = whereCanAirLand(Collections.singletonList(u), t, u.getOwner(), data,
m_battleTracker, carrierCostOfCurrentTerr, 1, !mustReturnToBase);
if (possible.size() > 1) {
landingTerr = getRemotePlayer(u.getOwner()).selectTerritoryForAirToLand(possible, t,
"Select territory for air units to land. (Current territory is " + t.getName() + "): "
+ MyFormatter.unitsToText(Collections.singletonList(u)));
} else if (possible.size() == 1) {
landingTerr = possible.iterator().next();
}
if (landingTerr == null || landingTerr.equals(t)) {
carrierCostOfCurrentTerr += AirMovementValidator.carrierCost(Collections.singletonList(u));
historyText = "Scrambled unit stays in territory " + t.getName();
} else {
historyText = "Moving scrambled unit from " + t.getName() + " to " + landingTerr.getName();
}
} else {
landingTerr = originatedFrom;
historyText =
"Moving scrambled unit from " + t.getName() + " back to originating territory: " + landingTerr.getName();
}
// if null, we leave it to die
if (landingTerr != null) {
change.add(ChangeFactory.moveUnits(t, landingTerr, Collections.singletonList(u)));
}
change.add(ChangeFactory.unitPropertyChange(u, null, TripleAUnit.ORIGINATED_FROM));
change.add(ChangeFactory.unitPropertyChange(u, false, TripleAUnit.WAS_SCRAMBLED));
if (!change.isEmpty()) {
m_bridge.getHistoryWriter().startEvent(historyText, u);
m_bridge.addChange(change);
}
}
}
}
private static void resetMaxScrambleCount(final IDelegateBridge aBridge) {
// reset the tripleaUnit property for all airbases that were used
final GameData data = aBridge.getData();
if (!games.strategy.triplea.Properties.getScramble_Rules_In_Effect(data)) {
return;
}
final CompositeChange change = new CompositeChange();
for (final Territory t : data.getMap().getTerritories()) {
final Collection<Unit> airbases = t.getUnits().getMatches(Matches.UnitIsAirBase);
for (final Unit u : airbases) {
final UnitAttachment ua = UnitAttachment.get(u.getType());
final int currentMax = ((TripleAUnit) u).getMaxScrambleCount();
final int allowedMax = ua.getMaxScrambleCount();
if (currentMax != allowedMax) {
change.add(ChangeFactory.unitPropertyChange(u, allowedMax, TripleAUnit.MAX_SCRAMBLE_COUNT));
}
}
}
if (!change.isEmpty()) {
aBridge.getHistoryWriter().startEvent("Preparing Airbases for Possible Scrambling");
aBridge.addChange(change);
}
}
private void airBattleCleanup() {
final GameData data = getData();
if (!games.strategy.triplea.Properties.getRaidsMayBePreceededByAirBattles(data)) {
return;
}
final CompositeChange change = new CompositeChange();
for (final Territory t : data.getMap().getTerritories()) {
for (final Unit u : t.getUnits().getMatches(Matches.UnitWasInAirBattle)) {
change.add(ChangeFactory.unitPropertyChange(u, false, TripleAUnit.WAS_IN_AIR_BATTLE));
}
}
if (!change.isEmpty()) {
m_bridge.getHistoryWriter().startEvent("Cleaning up after air battles");
m_bridge.addChange(change);
}
}
private void checkDefendingPlanesCanLand() {
final GameData data = getData();
final Map<Territory, Collection<Unit>> defendingAirThatCanNotLand = m_battleTracker.getDefendingAirThatCanNotLand();
final boolean isWW2v2orIsSurvivingAirMoveToLand = games.strategy.triplea.Properties.getWW2V2(data)
|| games.strategy.triplea.Properties.getSurvivingAirMoveToLand(data);
final CompositeMatch<Unit> alliedDefendingAir =
new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.UnitWasScrambled.invert());
for (final Entry<Territory, Collection<Unit>> entry : defendingAirThatCanNotLand.entrySet()) {
final Territory battleSite = entry.getKey();
final Collection<Unit> defendingAir = entry.getValue();
if (defendingAir == null || defendingAir.isEmpty()) {
continue;
}
defendingAir.retainAll(battleSite.getUnits().getUnits());
if (defendingAir.isEmpty()) {
continue;
}
final PlayerID defender = AbstractBattle.findDefender(battleSite, m_player, data);
// Get all land territories where we can land
final Set<Territory> neighbors = data.getMap().getNeighbors(battleSite);
final CompositeMatch<Territory> alliedLandTerritories =
new CompositeMatchAnd<>(Matches.airCanLandOnThisAlliedNonConqueredLandTerritory(defender, data));
// Get those that are neighbors
final Collection<Territory> canLandHere = Match.getMatches(neighbors, alliedLandTerritories);
// Get all sea territories where there are allies
final CompositeMatch<Territory> neighboringSeaZonesWithAlliedUnits =
new CompositeMatchAnd<>(Matches.TerritoryIsWater, Matches.territoryHasAlliedUnits(defender, data));
// Get those that are neighbors
final Collection<Territory> areSeaNeighbors = Match.getMatches(neighbors, neighboringSeaZonesWithAlliedUnits);
// Set up match criteria for allied carriers
final CompositeMatch<Unit> alliedCarrier = new CompositeMatchAnd<>();
alliedCarrier.add(Matches.UnitIsCarrier);
alliedCarrier.add(Matches.alliedUnit(defender, data));
// Set up match criteria for allied planes
final CompositeMatch<Unit> alliedPlane = new CompositeMatchAnd<>();
alliedPlane.add(Matches.UnitIsAir);
alliedPlane.add(Matches.alliedUnit(defender, data));
// See if neighboring carriers have any capacity available
for (final Territory currentTerritory : areSeaNeighbors) {
// get the capacity of the carriers and cost of fighters
final Collection<Unit> alliedCarriers = currentTerritory.getUnits().getMatches(alliedCarrier);
final Collection<Unit> alliedPlanes = currentTerritory.getUnits().getMatches(alliedPlane);
final int alliedCarrierCapacity = AirMovementValidator.carrierCapacity(alliedCarriers, currentTerritory);
final int alliedPlaneCost = AirMovementValidator.carrierCost(alliedPlanes);
// if there is free capacity, add the territory to landing possibilities
if (alliedCarrierCapacity - alliedPlaneCost >= 1) {
canLandHere.add(currentTerritory);
}
}
if (isWW2v2orIsSurvivingAirMoveToLand) {
Territory territory = null;
while (canLandHere.size() > 1 && defendingAir.size() > 0) {
territory = getRemotePlayer(defender).selectTerritoryForAirToLand(canLandHere, battleSite,
"Select territory for air units to land. (Current territory is " + battleSite.getName() + "): "
+ MyFormatter.unitsToText(defendingAir));
// added for test script
if (territory == null) {
territory = canLandHere.iterator().next();
}
if (territory.isWater()) {
landPlanesOnCarriers(m_bridge, alliedDefendingAir, defendingAir, alliedCarrier, alliedPlane,
territory, battleSite);
} else {
moveAirAndLand(m_bridge, defendingAir, defendingAir, territory, battleSite);
continue;
}
// remove the territory from those available
canLandHere.remove(territory);
}
// Land in the last remaining territory
if (canLandHere.size() > 0 && defendingAir.size() > 0) {
territory = canLandHere.iterator().next();
if (territory.isWater()) {
landPlanesOnCarriers(m_bridge, alliedDefendingAir, defendingAir, alliedCarrier, alliedPlane,
territory, battleSite);
} else {
moveAirAndLand(m_bridge, defendingAir, defendingAir, territory, battleSite);
continue;
}
}
} else if (canLandHere.size() > 0) {
// now defending air has what cant stay, is there a place we can go?
// check for an island in this sea zone
for (final Territory currentTerritory : canLandHere) {
// only one neighbor, its an island.
if (data.getMap().getNeighbors(currentTerritory).size() == 1) {
moveAirAndLand(m_bridge, defendingAir, defendingAir, currentTerritory, battleSite);
}
}
}
if (defendingAir.size() > 0) {
// no where to go, they must die
m_bridge.getHistoryWriter().addChildToEvent(
MyFormatter.unitsToText(defendingAir) + " could not land and were killed",
new ArrayList<>(defendingAir));
final Change change = ChangeFactory.removeUnits(battleSite, defendingAir);
m_bridge.addChange(change);
}
}
}
private static void landPlanesOnCarriers(final IDelegateBridge bridge, final CompositeMatch<Unit> alliedDefendingAir,
final Collection<Unit> defendingAir, final CompositeMatch<Unit> alliedCarrier,
final CompositeMatch<Unit> alliedPlane, final Territory newTerritory, final Territory battleSite) {
// Get the capacity of the carriers in the selected zone
final Collection<Unit> alliedCarriersSelected = newTerritory.getUnits().getMatches(alliedCarrier);
final Collection<Unit> alliedPlanesSelected = newTerritory.getUnits().getMatches(alliedPlane);
final int alliedCarrierCapacitySelected =
AirMovementValidator.carrierCapacity(alliedCarriersSelected, newTerritory);
final int alliedPlaneCostSelected = AirMovementValidator.carrierCost(alliedPlanesSelected);
// Find the available capacity of the carriers in that territory
final int territoryCapacity = alliedCarrierCapacitySelected - alliedPlaneCostSelected;
if (territoryCapacity > 0) {
// move that number of planes from the battlezone
// TODO: this seems to assume that the air units all have 1 carrier cost!! fixme
final Collection<Unit> movingAir = Match.getNMatches(defendingAir, territoryCapacity, alliedDefendingAir);
moveAirAndLand(bridge, movingAir, defendingAir, newTerritory, battleSite);
}
}
private static void moveAirAndLand(final IDelegateBridge bridge, final Collection<Unit> defendingAirBeingMoved,
final Collection<Unit> defendingAirTotal, final Territory newTerritory, final Territory battleSite) {
bridge.getHistoryWriter().addChildToEvent(
MyFormatter.unitsToText(defendingAirBeingMoved) + " forced to land in " + newTerritory.getName(),
new ArrayList<>(defendingAirBeingMoved));
final Change change = ChangeFactory.moveUnits(battleSite, newTerritory, defendingAirBeingMoved);
bridge.addChange(change);
// remove those that landed in case it was a carrier
defendingAirTotal.removeAll(defendingAirBeingMoved);
}
/**
* KamikazeSuicideAttacks are attacks that are made during an Opponent's turn, using Resources that you own that have
* been designated.
* The resources are designated in PlayerAttachment, and hold information like the attack power of the resource.
* KamikazeSuicideAttacks are done in any territory that is a kamikazeZone, and the attacks are done by the original
* owner of that
* territory.
* The user has the option not to do any attacks, and they make target any number of units with any number of resource
* tokens.
* The units are then attacked individually by each resource token (meaning that casualties do not get selected
* because the attacks are
* targeted).
* The enemies of current player should decide all their attacks before the attacks are rolled.
*/
private void doKamikazeSuicideAttacks() {
final GameData data = getData();
if (!games.strategy.triplea.Properties.getUseKamikazeSuicideAttacks(data)) {
return;
}
// the current player is not the one who is doing these attacks, it is the all the enemies of this player who will
// do attacks
final Collection<PlayerID> enemies =
Match.getMatches(data.getPlayerList().getPlayers(), Matches.isAtWar(m_player, data));
if (enemies.isEmpty()) {
return;
}
final Match<Unit> canBeAttackedDefault = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(m_player),
Matches.UnitIsSea, Matches.UnitIsNotTransportButCouldBeCombatTransport, Matches.UnitIsNotSub);
final boolean onlyWhereThereAreBattlesOrAmphibious =
games.strategy.triplea.Properties.getKamikazeSuicideAttacksOnlyWhereBattlesAre(data);
final Collection<Territory> pendingBattles = m_battleTracker.getPendingBattleSites(false);
// create a list of all kamikaze zones, listed by enemy
final HashMap<PlayerID, Collection<Territory>> kamikazeZonesByEnemy =
new HashMap<>();
for (final Territory t : data.getMap().getTerritories()) {
final TerritoryAttachment ta = TerritoryAttachment.get(t);
if (ta == null) {
continue;
}
if (!ta.getKamikazeZone()) {
continue;
}
final PlayerID owner;
if (!games.strategy.triplea.Properties.getKamikazeSuicideAttacksDoneByCurrentTerritoryOwner(data)) {
owner = ta.getOriginalOwner();
if (owner == null) {
continue;
}
} else {
owner = t.getOwner();
if (owner == null) {
continue;
}
}
if (enemies.contains(owner)) {
if (Match.noneMatch(t.getUnits().getUnits(), Matches.unitIsOwnedBy(m_player))) {
continue;
}
if (onlyWhereThereAreBattlesOrAmphibious) {
// if no battle or amphibious from here, ignore it
if (!pendingBattles.contains(t)) {
if (!Matches.TerritoryIsWater.match(t)) {
continue;
}
boolean amphib = false;
final Collection<Territory> landNeighbors = data.getMap().getNeighbors(t, Matches.TerritoryIsLand);
for (final Territory neighbor : landNeighbors) {
final IBattle battle = m_battleTracker.getPendingBattle(neighbor, false, BattleType.NORMAL);
if (battle == null) {
final Map<Territory, Collection<Unit>> whereFrom =
m_battleTracker.getFinishedBattlesUnitAttackFromMap().get(neighbor);
if (whereFrom != null && whereFrom.containsKey(t)) {
amphib = true;
break;
}
continue;
}
if (battle.isAmphibious() && ((battle instanceof MustFightBattle
&& ((MustFightBattle) battle).getAmphibiousAttackTerritories().contains(t))
|| (battle instanceof NonFightingBattle
&& ((NonFightingBattle) battle).getAmphibiousAttackTerritories().contains(t)))) {
amphib = true;
break;
}
}
if (amphib == false) {
continue;
}
}
}
Collection<Territory> currentTerrs = kamikazeZonesByEnemy.get(owner);
if (currentTerrs == null) {
currentTerrs = new ArrayList<>();
}
currentTerrs.add(t);
kamikazeZonesByEnemy.put(owner, currentTerrs);
}
}
if (kamikazeZonesByEnemy.isEmpty()) {
return;
}
for (final Entry<PlayerID, Collection<Territory>> entry : kamikazeZonesByEnemy.entrySet()) {
final PlayerID currentEnemy = entry.getKey();
final PlayerAttachment pa = PlayerAttachment.get(currentEnemy);
if (pa == null) {
continue;
}
Match<Unit> canBeAttacked = canBeAttackedDefault;
final Set<UnitType> suicideAttackTargets = pa.getSuicideAttackTargets();
if (suicideAttackTargets != null) {
canBeAttacked =
new CompositeMatchAnd<>(Matches.unitIsOwnedBy(m_player), Matches.unitIsOfTypes(suicideAttackTargets));
}
// See if the player has any attack tokens
final IntegerMap<Resource> resourcesAndAttackValues = pa.getSuicideAttackResources();
if (resourcesAndAttackValues.size() <= 0) {
continue;
}
final IntegerMap<Resource> playerResourceCollection = currentEnemy.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) {
continue;
}
// now let the enemy decide if they will do attacks
final Collection<Territory> kamikazeZones = entry.getValue();
final HashMap<Territory, Collection<Unit>> possibleUnitsToAttack = new HashMap<>();
for (final Territory t : kamikazeZones) {
final List<Unit> validTargets = t.getUnits().getMatches(canBeAttacked);
if (!validTargets.isEmpty()) {
possibleUnitsToAttack.put(t, validTargets);
}
}
final HashMap<Territory, HashMap<Unit, IntegerMap<Resource>>> attacks =
getRemotePlayer(currentEnemy).selectKamikazeSuicideAttacks(possibleUnitsToAttack);
if (attacks == null || attacks.isEmpty()) {
continue;
}
// now validate that we have the resources and those units are valid targets
for (final Entry<Territory, HashMap<Unit, IntegerMap<Resource>>> territoryEntry : attacks.entrySet()) {
final Territory t = territoryEntry.getKey();
final Collection<Unit> possibleUnits = possibleUnitsToAttack.get(t);
if (possibleUnits == null || !possibleUnits.containsAll(territoryEntry.getValue().keySet())) {
throw new IllegalStateException("Player has chosen illegal units during Kamikaze Suicide Attacks");
}
for (final IntegerMap<Resource> rMap : territoryEntry.getValue().values()) {
attackTokens.subtract(rMap);
}
}
if (!attackTokens.isPositive()) {
throw new IllegalStateException("Player has chosen illegal resource during Kamikaze Suicide Attacks");
}
for (final Entry<Territory, HashMap<Unit, IntegerMap<Resource>>> territoryEntry : attacks.entrySet()) {
final Territory location = territoryEntry.getKey();
for (final Entry<Unit, IntegerMap<Resource>> unitEntry : territoryEntry.getValue().entrySet()) {
final Unit unitUnderFire = unitEntry.getKey();
final IntegerMap<Resource> numberOfAttacks = unitEntry.getValue();
if (numberOfAttacks != null && numberOfAttacks.size() > 0 && numberOfAttacks.totalValues() > 0) {
fireKamikazeSuicideAttacks(unitUnderFire, numberOfAttacks, resourcesAndAttackValues, currentEnemy,
location);
}
}
}
}
}
/**
* This rolls the dice and validates them to see if units died or not.
* It will use LowLuck or normal dice.
* If any units die, we remove them from the game, and if units take damage but live, we also do that here.
*/
private void fireKamikazeSuicideAttacks(final Unit unitUnderFire, final IntegerMap<Resource> numberOfAttacks,
final IntegerMap<Resource> resourcesAndAttackValues, final PlayerID firingEnemy, final Territory location) {
// TODO: find a way to autosave after each dice roll.
final GameData data = getData();
final int diceSides = data.getDiceSides();
final CompositeChange change = new CompositeChange();
int hits = 0;
int[] rolls = null;
if (games.strategy.triplea.Properties.getLow_Luck(data)) {
int power = 0;
for (final Entry<Resource, Integer> entry : numberOfAttacks.entrySet()) {
final Resource r = entry.getKey();
final int num = entry.getValue();
change.add(ChangeFactory.changeResourcesChange(firingEnemy, r, -num));
power += num * resourcesAndAttackValues.getInt(r);
}
if (power > 0) {
hits = power / diceSides;
final int remainder = power % diceSides;
if (remainder > 0) {
rolls = m_bridge.getRandom(diceSides, 1, firingEnemy, DiceType.COMBAT,
"Rolling for remainder in Kamikaze Suicide Attack on unit: " + unitUnderFire.getType().getName());
if (remainder > rolls[0]) {
hits++;
}
}
}
} else {
// avoid multiple calls of getRandom, so just do it once at the beginning
final int numTokens = numberOfAttacks.totalValues();
rolls = m_bridge.getRandom(diceSides, numTokens, firingEnemy, DiceType.COMBAT,
"Rolling for Kamikaze Suicide Attack on unit: " + unitUnderFire.getType().getName());
final int[] powerOfTokens = new int[numTokens];
int j = 0;
for (final Entry<Resource, Integer> entry : numberOfAttacks.entrySet()) {
final Resource r = entry.getKey();
int num = entry.getValue();
change.add(ChangeFactory.changeResourcesChange(firingEnemy, r, -num));
final int power = resourcesAndAttackValues.getInt(r);
while (num > 0) {
powerOfTokens[j] = power;
j++;
num--;
}
}
for (int i = 0; i < rolls.length; i++) {
if (powerOfTokens[i] > rolls[i]) {
hits++;
}
}
}
final String title =
"Kamikaze Suicide Attack attacks " + MyFormatter.unitsToText(Collections.singleton(unitUnderFire));
final String dice = " scoring " + hits + " hits. Rolls: " + MyFormatter.asDice(rolls);
m_bridge.getHistoryWriter().startEvent(title + dice, unitUnderFire);
if (hits > 0) {
final UnitAttachment ua = UnitAttachment.get(unitUnderFire.getType());
final int currentHits = unitUnderFire.getHits();
if (ua.getHitPoints() <= currentHits + hits) {
// TODO: kill dependents
change.add(ChangeFactory.removeUnits(location, Collections.singleton(unitUnderFire)));
} else {
final IntegerMap<Unit> hitMap = new IntegerMap<>();
hitMap.put(unitUnderFire, hits + unitUnderFire.getHits());
change.add(ChangeFactory.unitsHit(hitMap));
m_bridge.getHistoryWriter().addChildToEvent(
"Units damaged: " + MyFormatter.unitsToText(Collections.singleton(unitUnderFire)), unitUnderFire);
}
}
if (!change.isEmpty()) {
m_bridge.addChange(change);
}
// kamikaze suicide attacks, even if unsuccessful, deny the ability to bombard from this sea zone
m_battleTracker.addNoBombardAllowedFromHere(location);
// TODO: display this as actual dice for both players
final Collection<PlayerID> playersInvolved = new ArrayList<>();
playersInvolved.add(m_player);
playersInvolved.add(firingEnemy);
this.getDisplay().reportMessageToPlayers(playersInvolved, null, title + dice, title);
}
static void markDamaged(final Collection<Unit> damaged, final IDelegateBridge bridge) {
if (damaged.size() == 0) {
return;
}
final IntegerMap<Unit> damagedMap = new IntegerMap<>();
for (final Unit u : damaged) {
damagedMap.add(u, 1);
}
markDamaged(damagedMap, bridge, true);
}
public static void markDamaged(final IntegerMap<Unit> damagedMap, final IDelegateBridge bridge,
final boolean addPreviousHits) {
final Set<Unit> units = new HashSet<>(damagedMap.keySet());
if (addPreviousHits) {
for (final Unit u : units) {
damagedMap.add(u, u.getHits());
}
}
final Change damagedChange = ChangeFactory.unitsHit(damagedMap);
bridge.getHistoryWriter().addChildToEvent("Units damaged: " + MyFormatter.unitsToText(units), units);
bridge.addChange(damagedChange);
}
private static Collection<Territory> whereCanAirLand(final Collection<Unit> strandedAir, final Territory currentTerr,
final PlayerID alliedPlayer, final GameData data, final BattleTracker battleTracker,
final int carrierCostForCurrentTerr, final int allowedMovement,
final boolean useMaxScrambleDistance) {
final HashSet<Territory> whereCanLand = new HashSet<>();
int maxDistance = allowedMovement;
if ((maxDistance > 1) || useMaxScrambleDistance) {
UnitType ut = null;
for (final Unit u : strandedAir) {
if (ut == null) {
ut = u.getType();
} else if (!ut.equals(u.getType())) {
throw new IllegalStateException(
"whereCanAirLand can only accept 1 UnitType if byMovementCost or scrambled is true");
}
}
if (useMaxScrambleDistance) {
maxDistance = UnitAttachment.get(ut).getMaxScrambleDistance();
}
}
if (maxDistance < 1 || strandedAir == null || strandedAir.isEmpty()) {
return Collections.singletonList(currentTerr);
}
final boolean areNeutralsPassableByAir = (games.strategy.triplea.Properties.getNeutralFlyoverAllowed(data)
&& !games.strategy.triplea.Properties.getNeutralsImpassable(data));
final HashSet<Territory> canNotLand = new HashSet<>();
canNotLand.addAll(battleTracker.getPendingBattleSites(false));
canNotLand
.addAll(Match.getMatches(data.getMap().getTerritories(), Matches.territoryHasEnemyUnits(alliedPlayer, data)));
final Collection<Territory> possibleTerrs =
new ArrayList<>(data.getMap().getNeighbors(currentTerr, maxDistance));
if (maxDistance > 1) {
final Iterator<Territory> possibleIter = possibleTerrs.iterator();
while (possibleIter.hasNext()) {
final Route route = data.getMap().getRoute(currentTerr, possibleIter.next(),
Matches.airCanFlyOver(alliedPlayer, data, areNeutralsPassableByAir));
if (route == null || route.getMovementCost(strandedAir.iterator().next()) > maxDistance) {
possibleIter.remove();
}
}
}
possibleTerrs.add(currentTerr);
final HashSet<Territory> availableLand = new HashSet<>();
availableLand.addAll(Match.getMatches(possibleTerrs,
new CompositeMatchAnd<>(Matches.isTerritoryAllied(alliedPlayer, data), Matches.TerritoryIsLand)));
availableLand.removeAll(canNotLand);
whereCanLand.addAll(availableLand);
// now for carrier-air-landing validation
if (Match.allMatch(strandedAir, Matches.UnitCanLandOnCarrier)) {
final HashSet<Territory> availableWater = new HashSet<>();
availableWater.addAll(Match.getMatches(possibleTerrs,
new CompositeMatchAnd<>(
Matches.territoryHasUnitsThatMatch(Matches.UnitIsAlliedCarrier(alliedPlayer, data)),
Matches.TerritoryIsWater)));
availableWater.removeAll(battleTracker.getPendingBattleSites(false));
// a rather simple calculation, either we can take all the air, or we can't, nothing in the middle
final int carrierCost = AirMovementValidator.carrierCost(strandedAir);
final Iterator<Territory> waterIter = availableWater.iterator();
while (waterIter.hasNext()) {
final Territory t = waterIter.next();
int carrierCapacity = AirMovementValidator
.carrierCapacity(t.getUnits().getMatches(Matches.UnitIsAlliedCarrier(alliedPlayer, data)), t);
if (!t.equals(currentTerr)) {
carrierCapacity -= AirMovementValidator.carrierCost(t.getUnits().getMatches(
new CompositeMatchAnd<>(Matches.UnitCanLandOnCarrier, Matches.alliedUnit(alliedPlayer, data))));
} else {
carrierCapacity -= carrierCostForCurrentTerr;
}
if (carrierCapacity < carrierCost) {
waterIter.remove();
}
}
whereCanLand.addAll(availableWater);
}
return whereCanLand;
}
private static boolean isIgnoreTransportInMovement(final GameData data) {
return games.strategy.triplea.Properties.getIgnoreTransportInMovement(data);
}
private static boolean isIgnoreSubInMovement(final GameData data) {
return games.strategy.triplea.Properties.getIgnoreSubInMovement(data);
}
@Override
public Class<? extends IRemote> getRemoteType() {
return IBattleDelegate.class;
}
@Override
public Territory getCurrentBattleTerritory() {
final IBattle b = m_currentBattle;
if (b != null) {
return b.getTerritory();
} else {
return null;
}
}
@Override
public IBattle getCurrentBattle() {
return m_currentBattle;
}
}
class BattleExtendedDelegateState implements Serializable {
private static final long serialVersionUID = 7899007486408723505L;
Serializable superState;
// add other variables here:
BattleTracker m_battleTracker = new BattleTracker();
// public OriginalOwnerTracker m_originalOwnerTracker = new OriginalOwnerTracker();
public boolean m_needToInitialize;
boolean m_needToScramble;
boolean m_needToKamikazeSuicideAttacks;
boolean m_needToClearEmptyAirBattleAttacks;
boolean m_needToAddBombardmentSources;
boolean m_needToRecordBattleStatistics;
boolean m_needToCheckDefendingPlanesCanLand;
boolean m_needToCleanup;
IBattle m_currentBattle;
}