package games.strategy.triplea.delegate;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import com.google.common.annotations.VisibleForTesting;
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.RelationshipTracker;
import games.strategy.engine.data.RelationshipType;
import games.strategy.engine.data.Resource;
import games.strategy.engine.data.Route;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.UnitType;
import games.strategy.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.net.GUID;
import games.strategy.sound.SoundPath;
import games.strategy.triplea.Constants;
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.dataObjects.BattleRecords;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.oddsCalculator.ta.BattleResults;
import games.strategy.util.CompositeMatch;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.CompositeMatchOr;
import games.strategy.util.IntegerMap;
import games.strategy.util.InverseMatch;
import games.strategy.util.Match;
import games.strategy.util.Tuple;
/**
* Used to keep track of where battles have occurred.
*/
public class BattleTracker implements java.io.Serializable {
private static final long serialVersionUID = 8806010984321554662L;
// List of pending battles
private final Set<IBattle> m_pendingBattles = new HashSet<>();
// List of battle dependencies
// maps blocked -> Collection of battles that must precede
private final Map<IBattle, HashSet<IBattle>> m_dependencies = new HashMap<>();
// enemy and neutral territories that have been conquered
// blitzed is a subset of this
private final Set<Territory> m_conquered = new HashSet<>();
// blitzed territories
private final Set<Territory> m_blitzed = new HashSet<>();
// territories where a battle occurred
// TODO: fix typo in name, 'fough' -> 'fought'
private final Set<Territory> m_foughBattles = new HashSet<>();
// these territories have had battleships bombard during a naval invasion
// used to make sure that the same battleship doesn't bombard twice
private final Set<Territory> m_bombardedFromTerritories = new HashSet<>();
// list of territory we have conquered in a FinishedBattle and where from and if amphibious
private final HashMap<Territory, Map<Territory, Collection<Unit>>> m_finishedBattlesUnitAttackFromMap =
new HashMap<>();
// things like kamikaze suicide attacks disallow bombarding from that sea zone for that turn
private final Set<Territory> m_noBombardAllowed = new HashSet<>();
private final Map<Territory, Collection<Unit>> m_defendingAirThatCanNotLand =
new HashMap<>();
private BattleRecords m_battleRecords = null;
// to keep track of all relationships that have changed this turn
// (so we can validate things like transports loading in newly created hostile zones)
private final Collection<Tuple<Tuple<PlayerID, PlayerID>, Tuple<RelationshipType, RelationshipType>>> m_relationshipChangesThisTurn =
new ArrayList<>();
/**
* @param t
* referring territory.
* @return whether a battle is to be fought in the given territory
*/
public boolean hasPendingBattle(final Territory t, final boolean bombing) {
return getPendingBattle(t, bombing, null) != null;
}
/**
* add to the conquered.
*/
void addToConquered(final Collection<Territory> territories) {
m_conquered.addAll(territories);
}
void addToConquered(final Territory territory) {
m_conquered.add(territory);
}
/**
* @param t
* referring territory.
* @return whether territory was conquered
*/
public boolean wasConquered(final Territory t) {
return m_conquered.contains(t);
}
public Set<Territory> getConquered() {
return m_conquered;
}
/**
* @param t
* referring territory.
* @return whether territory was conquered by blitz
*/
public boolean wasBlitzed(final Territory t) {
return m_blitzed.contains(t);
}
public boolean wasBattleFought(final Territory t) {
return m_foughBattles.contains(t);
}
public boolean noBombardAllowedFromHere(final Territory t) {
return m_noBombardAllowed.contains(t);
}
public void addNoBombardAllowedFromHere(final Territory t) {
m_noBombardAllowed.add(t);
}
public HashMap<Territory, Map<Territory, Collection<Unit>>> getFinishedBattlesUnitAttackFromMap() {
return m_finishedBattlesUnitAttackFromMap;
}
public void addRelationshipChangesThisTurn(final PlayerID p1, final PlayerID p2, final RelationshipType oldRelation,
final RelationshipType newRelation) {
m_relationshipChangesThisTurn.add(Tuple.of(
Tuple.of(p1, p2),
Tuple.of(oldRelation, newRelation)));
}
public boolean didAllThesePlayersJustGoToWarThisTurn(final PlayerID p1, final Collection<Unit> enemyUnits,
final GameData data) {
final Set<PlayerID> enemies = new HashSet<>();
for (final Unit u : Match.getMatches(enemyUnits, Matches.unitIsEnemyOf(data, p1))) {
enemies.add(u.getOwner());
}
for (final PlayerID e : enemies) {
if (!didThesePlayersJustGoToWarThisTurn(p1, e)) {
return false;
}
}
return true;
}
public boolean didThesePlayersJustGoToWarThisTurn(final PlayerID p1, final PlayerID p2) {
// check all relationship changes that are p1 and p2, to make sure that oldRelation is not war,
// and newRelation is war
for (final Tuple<Tuple<PlayerID, PlayerID>, Tuple<RelationshipType, RelationshipType>> t : m_relationshipChangesThisTurn) {
final Tuple<PlayerID, PlayerID> players = t.getFirst();
if (players.getFirst().equals(p1)) {
if (!players.getSecond().equals(p2)) {
continue;
}
} else if (players.getSecond().equals(p1)) {
if (!players.getFirst().equals(p2)) {
continue;
}
} else {
continue;
}
final Tuple<RelationshipType, RelationshipType> relations = t.getSecond();
if (!Matches.RelationshipTypeIsAtWar.match(relations.getFirst())) {
if (Matches.RelationshipTypeIsAtWar.match(relations.getSecond())) {
return true;
}
}
}
return false;
}
void clearFinishedBattles(final IDelegateBridge bridge) {
for (final IBattle battle : new ArrayList<>(m_pendingBattles)) {
if (FinishedBattle.class.isAssignableFrom(battle.getClass())) {
final FinishedBattle finished = (FinishedBattle) battle;
m_finishedBattlesUnitAttackFromMap.put(finished.getTerritory(), finished.getAttackingFromMap());
finished.fight(bridge);
}
}
}
public void clearEmptyAirBattleAttacks(final IDelegateBridge bridge) {
for (final IBattle battle : new ArrayList<>(m_pendingBattles)) {
if (AirBattle.class.isAssignableFrom(battle.getClass())) {
final AirBattle airBattle = (AirBattle) battle;
airBattle.updateDefendingUnits();
if (airBattle.getDefendingUnits().isEmpty()) {
airBattle.finishBattleAndRemoveFromTrackerHeadless(bridge);
}
}
}
}
public void undoBattle(final Route route, final Collection<Unit> units, final PlayerID player,
final IDelegateBridge bridge) {
for (final IBattle battle : new ArrayList<>(m_pendingBattles)) {
if (battle.getTerritory().equals(route.getEnd())) {
battle.removeAttack(route, units);
if (battle.isEmpty()) {
removeBattleForUndo(player, battle);
}
}
}
final RelationshipTracker relationshipTracker = bridge.getData().getRelationshipTracker();
// if we have no longer conquered it, clear the blitz state
// We must look at all territories,
// because we could have conquered the end territory if there are no units there
for (final Territory current : route.getAllTerritories()) {
if (!relationshipTracker.isAllied(current.getOwner(), player) && m_conquered.contains(current)) {
m_conquered.remove(current);
m_blitzed.remove(current);
}
}
// say they weren't in combat
final CompositeChange change = new CompositeChange();
final Iterator<Unit> attackIter = units.iterator();
while (attackIter.hasNext()) {
change.add(ChangeFactory.unitPropertyChange(attackIter.next(), false, TripleAUnit.WAS_IN_COMBAT));
}
bridge.addChange(change);
}
private void removeBattleForUndo(final PlayerID player, final IBattle battle) {
if (m_battleRecords != null) {
m_battleRecords.removeBattle(player, battle.getBattleID());
}
m_pendingBattles.remove(battle);
m_dependencies.remove(battle);
for (final Collection<IBattle> battles : m_dependencies.values()) {
battles.remove(battle);
}
}
void addBombingBattle(final Route route, final Collection<Unit> units, final PlayerID id,
final IDelegateBridge bridge, final UndoableMove changeTracker,
final Collection<Unit> unitsNotUnloadedTilEndOfRoute) {
this.addBattle(route, units, true, id, bridge, changeTracker, unitsNotUnloadedTilEndOfRoute, null, false);
}
public void addBattle(final Route route, final Collection<Unit> units, final PlayerID id,
final IDelegateBridge bridge, final UndoableMove changeTracker,
final Collection<Unit> unitsNotUnloadedTilEndOfRoute) {
this.addBattle(route, units, false, id, bridge, changeTracker, unitsNotUnloadedTilEndOfRoute, null, false);
}
void addBattle(final Route route, final Collection<Unit> units, final boolean bombing, final PlayerID id,
final IDelegateBridge bridge, final UndoableMove changeTracker,
final Collection<Unit> unitsNotUnloadedTilEndOfRoute, final HashMap<Unit, HashSet<Unit>> targets,
final boolean airBattleCompleted) {
final GameData data = bridge.getData();
if (bombing) {
// create only either an air battle OR a bombing battle.
// (the air battle will create a bombing battle when done, if needed)
if (!airBattleCompleted && games.strategy.triplea.Properties.getRaidsMayBePreceededByAirBattles(data)
&& AirBattle.territoryCouldPossiblyHaveAirBattleDefenders(route.getEnd(), id, data, bombing)) {
addAirBattle(route, units, id, data, true);
} else {
addBombingBattle(route, units, id, data, targets);
}
// say they were in combat
markWasInCombat(units, bridge, changeTracker);
} else {
// create both an air battle and a normal battle
if (!airBattleCompleted && games.strategy.triplea.Properties.getBattlesMayBePreceededByAirBattles(data)
&& AirBattle.territoryCouldPossiblyHaveAirBattleDefenders(route.getEnd(), id, data, bombing)) {
addAirBattle(route, Match.getMatches(units, AirBattle.attackingGroundSeaBattleEscorts(id, data)), id, data,
false);
}
final Change change = addMustFightBattleChange(route, units, id, data);
bridge.addChange(change);
if (changeTracker != null) {
changeTracker.addChange(change);
}
if (games.strategy.util.Match.someMatch(units, Matches.UnitIsLand)
|| games.strategy.util.Match.someMatch(units, Matches.UnitIsSea)) {
addEmptyBattle(route, units, id, bridge, changeTracker, unitsNotUnloadedTilEndOfRoute);
}
}
}
private void markWasInCombat(final Collection<Unit> units, final IDelegateBridge bridge,
final UndoableMove changeTracker) {
if (units == null) {
return;
}
final CompositeChange change = new CompositeChange();
final Iterator<Unit> attackIter = units.iterator();
while (attackIter.hasNext()) {
change.add(ChangeFactory.unitPropertyChange(attackIter.next(), true, TripleAUnit.WAS_IN_COMBAT));
}
bridge.addChange(change);
if (changeTracker != null) {
changeTracker.addChange(change);
}
}
private void addBombingBattle(final Route route, final Collection<Unit> units, final PlayerID attacker,
final GameData data, final HashMap<Unit, HashSet<Unit>> targets) {
IBattle battle = getPendingBattle(route.getEnd(), true, BattleType.BOMBING_RAID);
if (battle == null) {
battle = new StrategicBombingRaidBattle(route.getEnd(), data, attacker, this);
m_pendingBattles.add(battle);
getBattleRecords().addBattle(attacker, battle.getBattleID(), route.getEnd(), battle.getBattleType());
}
final Change change = battle.addAttackChange(route, units, targets);
// when state is moved to the game data, this will change
if (!change.isEmpty()) {
throw new IllegalStateException("Non empty change");
}
// dont let land battles in the same territory occur before bombing battles
final IBattle dependent = getPendingBattle(route.getEnd(), false, BattleType.NORMAL);
if (dependent != null) {
addDependency(dependent, battle);
}
final IBattle dependentAirBattle = getPendingBattle(route.getEnd(), false, BattleType.AIR_BATTLE);
if (dependentAirBattle != null) {
addDependency(dependentAirBattle, battle);
}
}
private void addAirBattle(final Route route, final Collection<Unit> units, final PlayerID attacker,
final GameData data, final boolean bombingRun) {
if (units.isEmpty()) {
return;
}
IBattle battle =
getPendingBattle(route.getEnd(), bombingRun, (bombingRun ? BattleType.AIR_RAID : BattleType.AIR_BATTLE));
if (battle == null) {
battle = new AirBattle(route.getEnd(), bombingRun, data, attacker, this);
m_pendingBattles.add(battle);
getBattleRecords().addBattle(attacker, battle.getBattleID(), route.getEnd(), battle.getBattleType());
}
final Change change = battle.addAttackChange(route, units, null);
// when state is moved to the game data, this will change
if (!change.isEmpty()) {
throw new IllegalStateException("Non empty change");
}
// dont let land battles in the same territory occur before bombing battles
if (bombingRun) {
final IBattle dependentAirBattle = getPendingBattle(route.getEnd(), false, BattleType.AIR_BATTLE);
if (dependentAirBattle != null) {
addDependency(dependentAirBattle, battle);
}
} else {
final IBattle airRaid = getPendingBattle(route.getEnd(), true, BattleType.AIR_RAID);
if (airRaid != null) {
addDependency(battle, airRaid);
}
final IBattle raid = getPendingBattle(route.getEnd(), true, BattleType.BOMBING_RAID);
if (raid != null) {
addDependency(battle, raid);
}
}
final IBattle dependent = getPendingBattle(route.getEnd(), false, BattleType.NORMAL);
if (dependent != null) {
addDependency(dependent, battle);
}
}
/**
* No enemies.
*/
private void addEmptyBattle(final Route route, final Collection<Unit> units, final PlayerID id,
final IDelegateBridge bridge, final UndoableMove changeTracker,
final Collection<Unit> unitsNotUnloadedTilEndOfRoute) {
final GameData data = bridge.getData();
final Collection<Unit> canConquer = Match.getMatches(units,
Matches.unitIsBeingTransportedByOrIsDependentOfSomeUnitInThisList(units, route, id, data, false).invert());
if (Match.noneMatch(canConquer, Matches.UnitIsNotAir)) {
return;
}
final Collection<Unit> presentFromStartTilEnd = new ArrayList<>(canConquer);
if (unitsNotUnloadedTilEndOfRoute != null) {
presentFromStartTilEnd.removeAll(unitsNotUnloadedTilEndOfRoute);
}
final boolean canConquerMiddleSteps = Match.someMatch(presentFromStartTilEnd, Matches.UnitIsNotAir);
final boolean scramblingEnabled = games.strategy.triplea.Properties.getScramble_Rules_In_Effect(data);
final CompositeMatch<Territory> conquerable = new CompositeMatchAnd<>();
conquerable.add(Matches.territoryIsEmptyOfCombatUnits(data, id));
conquerable.add(new CompositeMatchOr<>(
Matches.territoryIsOwnedByPlayerWhosRelationshipTypeCanTakeOverOwnedTerritoryAndPassableAndNotWater(id),
Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(id, data)));
final Collection<Territory> conquered = new ArrayList<>();
if (canConquerMiddleSteps) {
conquered.addAll(route.getMatches(conquerable));
// in case we begin in enemy territory, and blitz out of it, check the first territory
if (route.getStart() != route.getEnd() && conquerable.match(route.getStart())) {
conquered.add(route.getStart());
}
}
// we handle the end of the route later
conquered.remove(route.getEnd());
final Collection<Territory> blitzed = Match.getMatches(conquered, Matches.TerritoryIsBlitzable(id, data));
m_blitzed.addAll(Match.getMatches(blitzed, Matches.isTerritoryEnemy(id, data)));
m_conquered.addAll(Match.getMatches(conquered, Matches.isTerritoryEnemy(id, data)));
for (final Territory current : conquered) {
IBattle nonFight = getPendingBattle(current, false, BattleType.NORMAL);
// TODO: if we ever want to scramble to a blitzed territory, then we need to fix this
if (nonFight == null) {
nonFight = new FinishedBattle(current, id, this, false, BattleType.NORMAL, data,
BattleRecord.BattleResultDescription.CONQUERED, WhoWon.ATTACKER);
m_pendingBattles.add(nonFight);
getBattleRecords().addBattle(id, nonFight.getBattleID(), current, nonFight.getBattleType());
}
final Change change = nonFight.addAttackChange(route, units, null);
bridge.addChange(change);
if (changeTracker != null) {
changeTracker.addChange(change);
}
takeOver(current, id, bridge, changeTracker, units);
// }
}
// check the last territory
if (conquerable.match(route.getEnd())) {
IBattle precede = getDependentAmphibiousAssault(route);
if (precede == null) {
precede = getPendingBattle(route.getEnd(), true, null);
}
// if we have a preceding battle, then we must use a non-fighting-battle
// if we have scrambling on, and this is an amphibious attack,
// we may wish to scramble to kill the transports, so must use non-fighting-battle also
if (precede != null || (scramblingEnabled && route.isUnload() && route.hasExactlyOneStep())) {
IBattle nonFight = getPendingBattle(route.getEnd(), false, BattleType.NORMAL);
if (nonFight == null) {
nonFight = new NonFightingBattle(route.getEnd(), id, this, data);
m_pendingBattles.add(nonFight);
getBattleRecords().addBattle(id, nonFight.getBattleID(), route.getEnd(), nonFight.getBattleType());
}
final Change change = nonFight.addAttackChange(route, units, null);
bridge.addChange(change);
if (changeTracker != null) {
changeTracker.addChange(change);
}
if (precede != null) {
addDependency(nonFight, precede);
}
} else {
if (Matches.isTerritoryEnemy(id, data).match(route.getEnd())) {
if (Matches.TerritoryIsBlitzable(id, data).match(route.getEnd())) {
m_blitzed.add(route.getEnd());
}
m_conquered.add(route.getEnd());
}
IBattle nonFight = getPendingBattle(route.getEnd(), false, BattleType.NORMAL);
if (nonFight == null) {
nonFight = new FinishedBattle(route.getEnd(), id, this, false, BattleType.NORMAL, data,
BattleRecord.BattleResultDescription.CONQUERED, WhoWon.ATTACKER);
m_pendingBattles.add(nonFight);
getBattleRecords().addBattle(id, nonFight.getBattleID(), route.getEnd(), nonFight.getBattleType());
}
final Change change = nonFight.addAttackChange(route, units, null);
bridge.addChange(change);
if (changeTracker != null) {
changeTracker.addChange(change);
}
takeOver(route.getEnd(), id, bridge, changeTracker, units);
}
}
// TODO: else what?
}
public void takeOver(final Territory territory, final PlayerID id, final IDelegateBridge bridge,
final UndoableMove changeTracker, final Collection<Unit> arrivingUnits) {
// This could be NULL if unowned water
final TerritoryAttachment ta = TerritoryAttachment.get(territory);
if (ta == null) {
// TODO: allow capture/destroy of infrastructure on unowned water
return;
}
final GameData data = bridge.getData();
final Collection<Unit> arrivedUnits = (arrivingUnits == null ? null : new ArrayList<>(arrivingUnits));
final RelationshipTracker relationshipTracker = data.getRelationshipTracker();
final boolean isTerritoryOwnerAnEnemy = relationshipTracker.canTakeOverOwnedTerritory(id, territory.getOwner());
// If this is a convoy (we wouldn't be in this method otherwise)
// check to make sure attackers have more than just transports. If they don't, exit here.
if (territory.isWater() && arrivedUnits != null) {
int totalMatches = 0;
// Total Attacking Sea units = all units - land units - air units - submerged subs
// Also subtract transports & subs (if they can't control sea zones)
totalMatches = arrivedUnits.size() - Match.countMatches(arrivedUnits, Matches.UnitIsLand)
- Match.countMatches(arrivedUnits, Matches.UnitIsAir)
- Match.countMatches(arrivedUnits, Matches.UnitIsSubmerged);
// If transports are restricted from controlling sea zones, subtract them
final CompositeMatch<Unit> transportsCanNotControl = new CompositeMatchAnd<>();
transportsCanNotControl.add(Matches.UnitIsTransportAndNotDestroyer);
transportsCanNotControl.add(Matches.UnitIsTransportButNotCombatTransport);
if (!games.strategy.triplea.Properties.getTransportControlSeaZone(data)) {
totalMatches -= Match.countMatches(arrivedUnits, transportsCanNotControl);
}
// TODO check if istrn and NOT isDD
// If subs are restricted from controlling sea zones, subtract them
if (games.strategy.triplea.Properties.getSubControlSeaZoneRestricted(data)) {
totalMatches -= Match.countMatches(arrivedUnits, Matches.UnitIsSub);
}
if (totalMatches == 0) {
return;
}
}
// If it was a Convoy Route- check ownership of the associated neighboring territory and set message
if (ta != null && ta.getConvoyRoute()) {
// we could be part of a convoy route for another territory
final Collection<Territory> attachedConvoyTo =
TerritoryAttachment.getWhatTerritoriesThisIsUsedInConvoysFor(territory, data);
for (final Territory convoy : attachedConvoyTo) {
final TerritoryAttachment cta = TerritoryAttachment.get(convoy);
if (!cta.getConvoyRoute()) {
continue;
}
final PlayerID convoyOwner = convoy.getOwner();
if (relationshipTracker.isAllied(id, convoyOwner)) {
if (Match.getMatches(cta.getConvoyAttached(), Matches.isTerritoryAllied(convoyOwner, data)).size() <= 0) {
bridge.getHistoryWriter()
.addChildToEvent(convoyOwner.getName() + " gains " + cta.getProduction() + " production in "
+ convoy.getName() + " for the liberation the convoy route in " + territory.getName());
}
} else if (relationshipTracker.isAtWar(id, convoyOwner)) {
if (Match.getMatches(cta.getConvoyAttached(), Matches.isTerritoryAllied(convoyOwner, data)).size() == 1) {
bridge.getHistoryWriter()
.addChildToEvent(convoyOwner.getName() + " loses " + cta.getProduction() + " production in "
+ convoy.getName() + " due to the capture of the convoy route in " + territory.getName());
}
}
}
}
// if neutral, we may charge money to enter
if (territory.getOwner().isNull() && !territory.isWater()
&& games.strategy.triplea.Properties.getNeutralCharge(data) >= 0) {
final Resource PUs = data.getResourceList().getResource(Constants.PUS);
final int PUChargeIdeal = -games.strategy.triplea.Properties.getNeutralCharge(data);
final int PUChargeReal = Math.min(0, Math.max(PUChargeIdeal, -id.getResources().getQuantity(PUs)));
final Change neutralFee = ChangeFactory.changeResourcesChange(id, PUs, PUChargeReal);
bridge.addChange(neutralFee);
if (changeTracker != null) {
changeTracker.addChange(neutralFee);
}
if (PUChargeIdeal == PUChargeReal) {
bridge.getHistoryWriter().addChildToEvent(id.getName() + " loses " + -PUChargeReal + " "
+ MyFormatter.pluralize("PU", -PUChargeReal) + " for violating " + territory.getName() + "s neutrality.");
} else {
System.out.println("Player, " + id.getName() + " attacks a Neutral territory, and should have had to pay "
+ PUChargeIdeal + ", but did not have enough PUs to pay! This is a bug.");
bridge.getHistoryWriter()
.addChildToEvent(id.getName() + " loses " + -PUChargeReal + " " + MyFormatter.pluralize("PU", -PUChargeReal)
+ " for violating " + territory.getName() + "s neutrality. Correct amount to charge is: "
+ PUChargeIdeal + ". Player should not have been able to make this attack!");
}
}
// if its a capital we take the money
// NOTE: this is not checking to see if it is an enemy.
// instead it is relying on the fact that the capital should be owned by the person it is attached to
if (ta != null && isTerritoryOwnerAnEnemy && ta.getCapital() != null) {
// if the capital is owned by the capitols player take the money
final PlayerID whoseCapital = data.getPlayerList().getPlayerID(ta.getCapital());
final PlayerAttachment pa = PlayerAttachment.get(id);
final PlayerAttachment paWhoseCapital = PlayerAttachment.get(whoseCapital);
final List<Territory> capitalsList =
new ArrayList<>(TerritoryAttachment.getAllCurrentlyOwnedCapitals(whoseCapital, data));
// we are losing one right now, so it is < not <=
if (paWhoseCapital != null && paWhoseCapital.getRetainCapitalNumber() < capitalsList.size()) {
// do nothing, we keep our money since we still control enough capitals
bridge.getHistoryWriter()
.addChildToEvent(id.getName() + " captures one of " + whoseCapital.getName() + " capitals");
} else if (whoseCapital.equals(territory.getOwner())) {
final Resource PUs = data.getResourceList().getResource(Constants.PUS);
final int capturedPUCount = whoseCapital.getResources().getQuantity(PUs);
if (pa != null) {
if (isPacificTheater(data)) {
final Change changeVP =
ChangeFactory.attachmentPropertyChange(pa, (capturedPUCount + pa.getCaptureVps()), "captureVps");
bridge.addChange(changeVP);
if (changeTracker != null) {
changeTracker.addChange(changeVP);
}
}
}
final Change remove = ChangeFactory.changeResourcesChange(whoseCapital, PUs, -capturedPUCount);
bridge.addChange(remove);
if (paWhoseCapital != null && paWhoseCapital.getDestroysPUs()) {
bridge.getHistoryWriter().addChildToEvent(id.getName() + " destroys " + capturedPUCount
+ MyFormatter.pluralize("PU", capturedPUCount) + " while taking " + whoseCapital.getName() + " capital");
if (changeTracker != null) {
changeTracker.addChange(remove);
}
} else {
bridge.getHistoryWriter().addChildToEvent(id.getName() + " captures " + capturedPUCount
+ MyFormatter.pluralize("PU", capturedPUCount) + " while taking " + whoseCapital.getName() + " capital");
if (changeTracker != null) {
changeTracker.addChange(remove);
}
final Change add = ChangeFactory.changeResourcesChange(id, PUs, capturedPUCount);
bridge.addChange(add);
if (changeTracker != null) {
changeTracker.addChange(add);
}
}
// remove all the tokens of the captured player
final Resource tokens = data.getResourceList().getResource(Constants.TECH_TOKENS);
if (tokens != null) {
final int m_currTokens = whoseCapital.getResources().getQuantity(Constants.TECH_TOKENS);
final Change removeTokens = ChangeFactory.changeResourcesChange(whoseCapital, tokens, -m_currTokens);
bridge.addChange(removeTokens);
if (changeTracker != null) {
changeTracker.addChange(removeTokens);
}
}
}
}
// is this an allied territory, revert to original owner if it is, unless they dont own there captital
final PlayerID terrOrigOwner = OriginalOwnerTracker.getOriginalOwner(territory);
PlayerID newOwner = id;
// if the original owner is the current owner, and the current owner is our enemy or canTakeOver,
// then we do not worry about this.
if (isTerritoryOwnerAnEnemy && terrOrigOwner != null && relationshipTracker.isAllied(terrOrigOwner, id)
&& !terrOrigOwner.equals(territory.getOwner())) {
final List<Territory> capitalsListOwned =
new ArrayList<>(TerritoryAttachment.getAllCurrentlyOwnedCapitals(terrOrigOwner, data));
if (!capitalsListOwned.isEmpty()) {
newOwner = terrOrigOwner;
} else {
newOwner = id;
final List<Territory> capitalsListOriginal =
new ArrayList<>(TerritoryAttachment.getAllCapitals(terrOrigOwner, data));
for (final Territory current : capitalsListOriginal) {
if (territory.equals(current) || current.getOwner().equals(PlayerID.NULL_PLAYERID)) {
// if a neutral controls our capital, our territories get liberated (ie: china in ww2v3)
newOwner = terrOrigOwner;
}
}
}
}
// if we have specially set this territory to have whenCapturedByGoesTo,
// then we set that here (except we don't set it if we are liberating allied owned territory)
if (ta != null && isTerritoryOwnerAnEnemy && newOwner.equals(id)
&& Matches.TerritoryHasWhenCapturedByGoesTo().match(territory)) {
for (final String value : ta.getWhenCapturedByGoesTo()) {
final String[] s = value.split(":");
final PlayerID capturingPlayer = data.getPlayerList().getPlayerID(s[0]);
final PlayerID goesToPlayer = data.getPlayerList().getPlayerID(s[1]);
if (capturingPlayer.equals(goesToPlayer)) {
continue;
}
if (capturingPlayer.equals(id)) {
newOwner = goesToPlayer;
break;
}
}
}
if (isTerritoryOwnerAnEnemy && ta != null) {
final Change takeOver = ChangeFactory.changeOwner(territory, newOwner);
bridge.getHistoryWriter().addChildToEvent(takeOver.toString());
bridge.addChange(takeOver);
if (changeTracker != null) {
changeTracker.addChange(takeOver);
changeTracker.addToConquered(territory);
}
// play a sound
if (territory.isWater()) {
// should probably see if there is something actually happening for water
bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_TERRITORY_CAPTURE_SEA, id);
} else if (ta != null && ta.getCapital() != null) {
bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_TERRITORY_CAPTURE_CAPITAL, id);
} else if (m_blitzed.contains(territory) && Match.someMatch(arrivedUnits, Matches.UnitCanBlitz)) {
bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_TERRITORY_CAPTURE_BLITZ, id);
} else {
bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_TERRITORY_CAPTURE_LAND, id);
}
}
// Remove any bombing raids against captured territory
// TODO: see if necessary
if (Match.someMatch(territory.getUnits().getUnits(),
new CompositeMatchAnd<>(Matches.unitIsEnemyOf(data, id), Matches.UnitCanBeDamaged))) {
final IBattle bombingBattle = getPendingBattle(territory, true, null);
if (bombingBattle != null) {
final BattleResults results = new BattleResults(bombingBattle, WhoWon.DRAW, data);
getBattleRecords().addResultToBattle(id, bombingBattle.getBattleID(), null, 0, 0,
BattleRecord.BattleResultDescription.NO_BATTLE, results);
bombingBattle.cancelBattle(bridge);
removeBattle(bombingBattle);
throw new IllegalStateException(
"Bombing Raids should be dealt with first! Be sure the battle has dependencies set correctly!");
}
}
captureOrDestroyUnits(territory, id, newOwner, bridge, changeTracker);
// is this territory our capitol or a capitol of our ally
// Also check to make sure playerAttachment even HAS a capital to fix abend
if (isTerritoryOwnerAnEnemy && terrOrigOwner != null && ta != null && ta.getCapital() != null
&& TerritoryAttachment.getAllCapitals(terrOrigOwner, data).contains(territory)
&& relationshipTracker.isAllied(terrOrigOwner, id)) {
// if it is give it back to the original owner
final Collection<Territory> originallyOwned = OriginalOwnerTracker.getOriginallyOwned(data, terrOrigOwner);
final List<Territory> friendlyTerritories =
Match.getMatches(originallyOwned, Matches.isTerritoryAllied(terrOrigOwner, data));
// give back the factories as well.
for (final Territory item : friendlyTerritories) {
if (item.getOwner() == terrOrigOwner) {
continue;
}
final Change takeOverFriendlyTerritories = ChangeFactory.changeOwner(item, terrOrigOwner);
bridge.addChange(takeOverFriendlyTerritories);
bridge.getHistoryWriter().addChildToEvent(takeOverFriendlyTerritories.toString());
if (changeTracker != null) {
changeTracker.addChange(takeOverFriendlyTerritories);
}
final Collection<Unit> units = Match.getMatches(item.getUnits().getUnits(), Matches.UnitIsInfrastructure);
if (!units.isEmpty()) {
final Change takeOverNonComUnits = ChangeFactory.changeOwner(units, terrOrigOwner, territory);
bridge.addChange(takeOverNonComUnits);
if (changeTracker != null) {
changeTracker.addChange(takeOverNonComUnits);
}
}
}
}
// say they were in combat
// if the territory being taken over is water, then do not say any land units were in combat
// (they may want to unload from the transport and attack)
if (Matches.TerritoryIsWater.match(territory) && arrivedUnits != null) {
arrivedUnits.removeAll(Match.getMatches(arrivedUnits, Matches.UnitIsLand));
}
markWasInCombat(arrivedUnits, bridge, changeTracker);
}
public static void captureOrDestroyUnits(final Territory territory, final PlayerID id, final PlayerID newOwner,
final IDelegateBridge bridge, final UndoableMove changeTracker) {
final GameData data = bridge.getData();
// destroy any units that should be destroyed on capture
if (games.strategy.triplea.Properties.getUnitsCanBeDestroyedInsteadOfCaptured(data)) {
final CompositeMatch<Unit> enemyToBeDestroyed =
new CompositeMatchAnd<>(Matches.enemyUnit(id, data), Matches.UnitDestroyedWhenCapturedByOrFrom(id));
final Collection<Unit> destroyed = territory.getUnits().getMatches(enemyToBeDestroyed);
if (!destroyed.isEmpty()) {
final Change destroyUnits = ChangeFactory.removeUnits(territory, destroyed);
bridge.getHistoryWriter().addChildToEvent("Some non-combat units are destroyed: ", destroyed);
bridge.addChange(destroyUnits);
if (changeTracker != null) {
changeTracker.addChange(destroyUnits);
}
}
}
// destroy any capture on entering units, IF the property to destroy them instead of capture is turned on
if (games.strategy.triplea.Properties.getOnEnteringUnitsDestroyedInsteadOfCaptured(data)) {
final Collection<Unit> destroyed =
territory.getUnits().getMatches(Matches.UnitCanBeCapturedOnEnteringToInThisTerritory(id, territory, data));
if (!destroyed.isEmpty()) {
final Change destroyUnits = ChangeFactory.removeUnits(territory, destroyed);
bridge.getHistoryWriter().addChildToEvent(id.getName() + " destroys some units instead of capturing them",
destroyed);
bridge.addChange(destroyUnits);
if (changeTracker != null) {
changeTracker.addChange(destroyUnits);
}
}
}
// destroy any disabled units owned by the enemy that are NOT infrastructure or factories
final CompositeMatch<Unit> enemyToBeDestroyed = new CompositeMatchAnd<>(Matches.enemyUnit(id, data),
Matches.UnitIsDisabled, Matches.UnitIsInfrastructure.invert());
final Collection<Unit> destroyed = territory.getUnits().getMatches(enemyToBeDestroyed);
if (!destroyed.isEmpty()) {
final Change destroyUnits = ChangeFactory.removeUnits(territory, destroyed);
bridge.getHistoryWriter().addChildToEvent(id.getName() + " destroys some disabled combat units", destroyed);
bridge.addChange(destroyUnits);
if (changeTracker != null) {
changeTracker.addChange(destroyUnits);
}
}
// take over non combatants
final CompositeMatch<Unit> enemyNonCom =
new CompositeMatchAnd<>(Matches.enemyUnit(id, data), Matches.UnitIsInfrastructure);
final CompositeMatch<Unit> willBeCaptured = new CompositeMatchOr<>(enemyNonCom,
Matches.UnitCanBeCapturedOnEnteringToInThisTerritory(id, territory, data));
final Collection<Unit> nonCom = territory.getUnits().getMatches(willBeCaptured);
// change any units that change unit types on capture
if (games.strategy.triplea.Properties.getUnitsCanBeChangedOnCapture(data)) {
final Collection<Unit> toReplace =
Match.getMatches(nonCom, Matches.UnitWhenCapturedChangesIntoDifferentUnitType());
for (final Unit u : toReplace) {
final LinkedHashMap<String, Tuple<String, IntegerMap<UnitType>>> map =
UnitAttachment.get(u.getType()).getWhenCapturedChangesInto();
final PlayerID currentOwner = u.getOwner();
for (final String value : map.keySet()) {
final String[] s = value.split(":");
if (!(s[0].equals("any") || data.getPlayerList().getPlayerID(s[0]).equals(currentOwner))) {
continue;
}
// we could use "id" or "newOwner" here... not sure which to use
if (!(s[1].equals("any") || data.getPlayerList().getPlayerID(s[1]).equals(id))) {
continue;
}
final CompositeChange changes = new CompositeChange();
final Collection<Unit> toAdd = new ArrayList<>();
final Tuple<String, IntegerMap<UnitType>> toCreate = map.get(value);
final boolean translateAttributes = toCreate.getFirst().equalsIgnoreCase("true");
for (final UnitType ut : toCreate.getSecond().keySet()) {
toAdd.addAll(ut.create(toCreate.getSecond().getInt(ut), newOwner));
}
if (!toAdd.isEmpty()) {
if (translateAttributes) {
final Change translate = TripleAUnit.translateAttributesToOtherUnits(u, toAdd, territory);
if (!translate.isEmpty()) {
changes.add(translate);
}
}
changes.add(ChangeFactory.removeUnits(territory, Collections.singleton(u)));
changes.add(ChangeFactory.addUnits(territory, toAdd));
changes.add(ChangeFactory.markNoMovementChange(toAdd));
bridge.getHistoryWriter()
.addChildToEvent(id.getName() + " converts " + u.toStringNoOwner() + " into different units", toAdd);
bridge.addChange(changes);
if (changeTracker != null) {
changeTracker.addChange(changes);
}
// don't forget to remove this unit from the list
nonCom.remove(u);
break;
}
}
}
}
if (!nonCom.isEmpty()) {
// FYI: a dummy delegate will not do anything with this change,
// meaning that the battle calculator will think this unit lived,
// even though it died or was captured, etc!
final Change capture = ChangeFactory.changeOwner(nonCom, newOwner, territory);
bridge.addChange(capture);
if (changeTracker != null) {
changeTracker.addChange(capture);
}
final Change noMovementChange = ChangeFactory.markNoMovementChange(nonCom);
bridge.addChange(noMovementChange);
if (changeTracker != null) {
changeTracker.addChange(noMovementChange);
}
}
}
private Change addMustFightBattleChange(final Route route, final Collection<Unit> units, final PlayerID id,
final GameData data) {
// it is possible to add a battle with a route that is just
// the start territory, ie the units did not move into the country
// they were there to start with
// this happens when you have submerged subs emerging
Territory site = route.getEnd();
if (site == null) {
site = route.getStart();
}
// this will be taken care of by the non fighting battle
if (!Matches.territoryHasEnemyUnits(id, data).match(site)) {
return ChangeFactory.EMPTY_CHANGE;
}
// if just an enemy factory &/or AA then no battle
final Collection<Unit> enemyUnits = Match.getMatches(site.getUnits().getUnits(), Matches.enemyUnit(id, data));
if (route.getEnd() != null && Match.allMatch(enemyUnits, Matches.UnitIsInfrastructure)) {
return ChangeFactory.EMPTY_CHANGE;
}
IBattle battle = getPendingBattle(site, false, BattleType.NORMAL);
// If there are no pending battles- add one for units already in the combat zone
if (battle == null) {
battle = new MustFightBattle(site, id, data, this);
m_pendingBattles.add(battle);
getBattleRecords().addBattle(id, battle.getBattleID(), site, battle.getBattleType());
}
// Add the units that moved into the battle
final Change change = battle.addAttackChange(route, units, null);
// make amphibious assaults dependent on possible naval invasions
// its only a dependency if we are unloading
final IBattle precede = getDependentAmphibiousAssault(route);
if (precede != null && Match.someMatch(units, Matches.UnitIsLand)) {
addDependency(battle, precede);
}
// dont let land battles in the same territory occur before bombing
// battles
final IBattle bombing = getPendingBattle(route.getEnd(), true, null);
if (bombing != null) {
addDependency(battle, bombing);
}
final IBattle airBattle = getPendingBattle(route.getEnd(), false, BattleType.AIR_BATTLE);
if (airBattle != null) {
addDependency(battle, airBattle);
}
return change;
}
private IBattle getDependentAmphibiousAssault(final Route route) {
if (!route.isUnload()) {
return null;
}
return getPendingBattle(route.getStart(), false, BattleType.NORMAL);
}
public IBattle getPendingBattle(final Territory t, final boolean bombing, final BattleType type) {
for (final IBattle battle : m_pendingBattles) {
if (battle.getTerritory().equals(t) && battle.isBombingRun() == bombing) {
if (type == null || type.equals(battle.getBattleType())) {
return battle;
}
}
}
return null;
}
public Collection<IBattle> getPendingBattles(final Territory t, final BattleType type) {
final Collection<IBattle> battles = new HashSet<>();
for (final IBattle battle : m_pendingBattles) {
if (battle.getTerritory().equals(t) && (type == null || type.equals(battle.getBattleType()))) {
battles.add(battle);
}
}
return battles;
}
public IBattle getPendingBattle(final GUID guid) {
if (guid == null) {
return null;
}
for (final IBattle battle : m_pendingBattles) {
if (guid.equals(battle.getBattleID())) {
return battle;
}
}
return null;
}
/**
* @param bombing
* whether only battles where there is bombing.
* @return a collection of territories where battles are pending
*/
public Collection<Territory> getPendingBattleSites(final boolean bombing) {
final Collection<IBattle> pending = new HashSet<>(m_pendingBattles);
final Collection<Territory> battles = new ArrayList<>();
for (final IBattle battle : pending) {
if (battle != null && !battle.isEmpty() && battle.isBombingRun() == bombing) {
battles.add(battle.getTerritory());
}
}
return battles;
}
public BattleListing getPendingBattleSites() {
final Map<BattleType, Collection<Territory>> battles = new HashMap<>();
final Collection<IBattle> pending = new HashSet<>(m_pendingBattles);
for (final IBattle battle : pending) {
if (battle != null && !battle.isEmpty()) {
Collection<Territory> territories = battles.get(battle.getBattleType());
if (territories == null) {
territories = new HashSet<>();
}
territories.add(battle.getTerritory());
battles.put(battle.getBattleType(), territories);
}
}
return new BattleListing(battles);
}
/**
* @param blocked
* the battle that is blocked.
* @return the battle that must occur before dependent can occur
*/
public Collection<IBattle> getDependentOn(final IBattle blocked) {
final Collection<IBattle> dependent = m_dependencies.get(blocked);
if (dependent == null) {
return Collections.emptyList();
}
return Match.getMatches(dependent, new InverseMatch<>(Matches.BattleIsEmpty));
}
/**
* @param blocking
* the battle that is blocking the other battles.
* @return the battles that cannot occur until the given battle occurs
*/
public Collection<IBattle> getBlocked(final IBattle blocking) {
final Iterator<IBattle> iter = m_dependencies.keySet().iterator();
final Collection<IBattle> allBlocked = new ArrayList<>();
while (iter.hasNext()) {
final IBattle current = iter.next();
final Collection<IBattle> currentBlockedBy = getDependentOn(current);
if (currentBlockedBy.contains(blocking)) {
allBlocked.add(current);
}
}
return allBlocked;
}
public void addDependency(final IBattle blocked, final IBattle blocking) {
m_dependencies.putIfAbsent(blocked, new HashSet<>());
m_dependencies.get(blocked).add(blocking);
}
private void removeDependency(final IBattle blocked, final IBattle blocking) {
final Collection<IBattle> dependencies = m_dependencies.get(blocked);
dependencies.remove(blocking);
if (dependencies.isEmpty()) {
m_dependencies.remove(blocked);
}
}
public void removeBattle(final IBattle battle) {
if (battle != null) {
final Iterator<IBattle> blocked = getBlocked(battle).iterator();
while (blocked.hasNext()) {
final IBattle current = blocked.next();
removeDependency(current, battle);
}
m_pendingBattles.remove(battle);
m_foughBattles.add(battle.getTerritory());
}
}
/**
* Marks the set of territories as having been the source of a naval
* bombardment.
*
* @param territories
* a collection of territories
*/
public void addPreviouslyNavalBombardmentSource(final Collection<Territory> territories) {
m_bombardedFromTerritories.addAll(territories);
}
public boolean wasNavalBombardmentSource(final Territory territory) {
return m_bombardedFromTerritories.contains(territory);
}
private boolean isPacificTheater(final GameData data) {
return data.getProperties().get(Constants.PACIFIC_THEATER, false);
}
public void clear() {
m_finishedBattlesUnitAttackFromMap.clear();
m_bombardedFromTerritories.clear();
m_pendingBattles.clear();
m_blitzed.clear();
m_foughBattles.clear();
m_conquered.clear();
m_dependencies.clear();
m_defendingAirThatCanNotLand.clear();
m_noBombardAllowed.clear();
m_relationshipChangesThisTurn.clear();
}
public void addToDefendingAirThatCanNotLand(final Collection<Unit> units, final Territory szTerritoryTheyAreIn) {
Collection<Unit> current = m_defendingAirThatCanNotLand.get(szTerritoryTheyAreIn);
if (current == null) {
current = new ArrayList<>();
}
current.addAll(units);
m_defendingAirThatCanNotLand.put(szTerritoryTheyAreIn, current);
}
public Map<Territory, Collection<Unit>> getDefendingAirThatCanNotLand() {
return m_defendingAirThatCanNotLand;
}
public void clearBattleRecords() {
if (m_battleRecords != null) {
m_battleRecords.clear();
m_battleRecords = null;
}
}
public BattleRecords getBattleRecords() {
if (m_battleRecords == null) {
m_battleRecords = new BattleRecords();
}
return m_battleRecords;
}
public void sendBattleRecordsToGameData(final IDelegateBridge aBridge) {
if (m_battleRecords != null && !m_battleRecords.isEmpty()) {
aBridge.getHistoryWriter().startEvent("Recording Battle Statistics");
aBridge.addChange(ChangeFactory.addBattleRecords(m_battleRecords, aBridge.getData()));
}
}
/**
* 'Auto-fight' all of the air battles and strategic bombing runs.
* Auto fight means we automatically begin the fight without user action. This is to avoid clicks during the
* air battle and SBR phase, and to enforce game rules that these phases are fought first before any other combat.
*/
void fightAirRaidsAndStrategicBombing(final IDelegateBridge delegateBridge) {
boolean bombing = true;
fightAirRaidsAndStrategicBombing(delegateBridge, () -> getPendingBattleSites(bombing),
(territory, battleType) -> getPendingBattle(territory, bombing, battleType));
}
@VisibleForTesting
void fightAirRaidsAndStrategicBombing(final IDelegateBridge delegateBridge,
Supplier<Collection<Territory>> pendingBattleSiteSupplier,
BiFunction<Territory, BattleType, IBattle> pendingBattleFunction) {
// First we'll fight all of the air battles (air raids)
// Then we will have a wave of battles for the SBR. AA guns will shoot, and we'll roll for damage.
// CAUTION: air raid battles when completed will potentially spawn new bombing raids. Would be good to refactor
// that out, in the meantime be aware there are mass side effects in these calls..
for (final Territory t : pendingBattleSiteSupplier.get()) {
final IBattle airRaid = pendingBattleFunction.apply(t, BattleType.AIR_RAID);
if (airRaid != null) {
airRaid.fight(delegateBridge);
}
}
// now that we've done all of the air battles, do all of the SBR's as a second wave.
for (final Territory t : pendingBattleSiteSupplier.get()) {
final IBattle bombingRaid = pendingBattleFunction.apply(t, BattleType.BOMBING_RAID);
if (bombingRaid != null) {
bombingRaid.fight(delegateBridge);
}
}
}
/**
* Kill undefended transports. Done first to remove potentially dependent sea battles
* Which could block amphibious assaults later
*/
public void fightDefenselessBattles(final IDelegateBridge bridge) {
final GameData gameData = bridge.getData();
// Here and below parameter "false" to getPendingBattleSites & getPendingBattle denote non-SBR battles
for (final Territory territory : getPendingBattleSites(false)) {
final IBattle battle = getPendingBattle(territory, false, BattleType.NORMAL);
final List<Unit> defenders = new ArrayList<>();
defenders.addAll(battle.getDefendingUnits());
final List<Unit> sortedUnitsList = getSortedDefendingUnits(bridge, gameData, territory, defenders);
if (getDependentOn(battle).isEmpty() && DiceRoll.getTotalPower(
DiceRoll.getUnitPowerAndRollsForNormalBattles(sortedUnitsList, defenders, false, false, gameData,
territory, TerritoryEffectHelper.getEffects(territory), false, null), gameData) == 0) {
battle.fight(bridge);
}
}
getPendingBattleSites(false).stream()
.map(territory -> getPendingBattle(territory, false, BattleType.NORMAL))
.forEach(battle -> {
if (battle instanceof NonFightingBattle && getDependentOn(battle).isEmpty()) {
battle.fight(bridge);
}
});
}
private List<Unit> getSortedDefendingUnits(final IDelegateBridge bridge, final GameData gameData,
final Territory territory, final List<Unit> defenders) {
final List<Unit> sortedUnitsList = new ArrayList<>(Match.getMatches(defenders,
Matches.UnitCanBeInBattle(true, !territory.isWater(), 1, false, true, true)));
Collections.sort(sortedUnitsList,
new UnitBattleComparator(false, BattleCalculator.getCostsForTUV(bridge.getPlayerID(), gameData),
TerritoryEffectHelper.getEffects(territory), gameData, false, false));
Collections.reverse(sortedUnitsList);
return sortedUnitsList;
}
/**
* Fight battle automatically if there is only one left to pick from.
*/
public void fightBattleIfOnlyOne(final IDelegateBridge bridge) {
final Collection<Territory> territories = getPendingBattleSites(false);
if (territories.size() == 1) {
final IBattle battle = getPendingBattle(territories.iterator().next(), false, BattleType.NORMAL);
battle.fight(bridge);
}
}
@Override
public String toString() {
return "BattleTracker:" + "\n" + "Conquered:" + m_conquered + "\n" + "Blitzed:" + m_blitzed + "\n" + "Fought:"
+ m_foughBattles + "\n" + "Pending:" + m_pendingBattles;
}
}