package games.strategy.triplea.delegate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.NamedAttachable;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.ProductionFrontier;
import games.strategy.engine.data.ProductionRule;
import games.strategy.engine.data.Resource;
import games.strategy.engine.data.ResourceCollection;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.TerritoryEffect;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.UnitType;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.engine.random.IRandomStats.DiceType;
import games.strategy.net.GUID;
import games.strategy.triplea.Constants;
import games.strategy.triplea.Properties;
import games.strategy.triplea.TripleA;
import games.strategy.triplea.ai.weakAI.WeakAI;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.attachments.UnitSupportAttachment;
import games.strategy.triplea.delegate.Die.DieType;
import games.strategy.triplea.delegate.dataObjects.CasualtyDetails;
import games.strategy.triplea.delegate.dataObjects.CasualtyList;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.player.ITripleAPlayer;
import games.strategy.triplea.util.UnitCategory;
import games.strategy.triplea.util.UnitSeperator;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.IntegerMap;
import games.strategy.util.LinkedIntegerMap;
import games.strategy.util.Match;
import games.strategy.util.Triple;
import games.strategy.util.Tuple;
/**
* Utiltity class for determing casualties and selecting casualties. The code
* was being dduplicated all over the place.
*/
public class BattleCalculator {
private static Map<String, List<UnitType>> oolCache = new ConcurrentHashMap<>();
public static void clearOOLCache() {
oolCache.clear();
}
// There is a problem with this variable, that it isn't
// private static IntegerMap<UnitType> s_costsForTuvForAllPlayersMergedAndAveraged;
// being cleared out when we switch maps.
// we want to sort in a determined way so that those looking at the dice results
// can tell what dice is for who
// we also want to sort by movement, so casualties will be choosen as the
// units with least movement
public static void sortPreBattle(final List<Unit> units) {
final Comparator<Unit> comparator = (u1, u2) -> {
if (u1.getUnitType().equals(u2.getUnitType())) {
return UnitComparator.getLowestToHighestMovementComparator().compare(u1, u2);
}
return u1.getUnitType().getName().compareTo(u2.getUnitType().getName());
};
Collections.sort(units, comparator);
}
public static int getTotalHitpointsLeft(final Collection<Unit> units) {
if (units == null || units.isEmpty()) {
return 0;
}
int rVal = 0;
for (final Unit u : units) {
final UnitAttachment ua = UnitAttachment.get(u.getType());
rVal += ua.getHitPoints();
rVal -= u.getHits();
}
return rVal;
}
public static int getTotalHitpointsLeft(final Unit unit) {
if (unit == null) {
return 0;
}
final UnitAttachment ua = UnitAttachment.get(unit.getType());
return ua.getHitPoints() - unit.getHits();
}
/**
* Choose plane casualties according to specified rules.
*/
public static CasualtyDetails getAACasualties(final boolean defending, final Collection<Unit> planes,
final Collection<Unit> allFriendlyUnits, final Collection<Unit> defendingAA, final Collection<Unit> allEnemyUnits,
final DiceRoll dice, final IDelegateBridge bridge, final PlayerID firingPlayer, final PlayerID hitPlayer,
final GUID battleID, final Territory terr, final Collection<TerritoryEffect> territoryEffects,
final boolean amphibious, final Collection<Unit> amphibiousLandAttackers) {
if (planes.isEmpty()) {
return new CasualtyDetails();
}
final GameData data = bridge.getData();
final boolean allowMultipleHitsPerUnit =
Match.allMatch(defendingAA, Matches.UnitAAShotDamageableInsteadOfKillingInstantly);
if (isChooseAA(data)) {
final String text = "Select " + dice.getHits() + " casualties from aa fire in " + terr.getName();
return selectCasualties(null, hitPlayer, planes, allFriendlyUnits, firingPlayer, allEnemyUnits, amphibious,
amphibiousLandAttackers, terr, territoryEffects, bridge, text, dice, defending, battleID, false,
dice.getHits(), allowMultipleHitsPerUnit);
} else {
if (Properties.getLow_Luck(data) || Properties.getLL_AA_ONLY(data)) {
return getLowLuckAACasualties(defending, planes, defendingAA, dice, bridge, allowMultipleHitsPerUnit);
} else {
// priority goes: choose -> individually -> random
// if none are set, we roll individually
if (isRollAAIndividually(data)) {
return IndividuallyFiredAACasualties(defending, planes, defendingAA, dice, bridge, allowMultipleHitsPerUnit);
}
if (isRandomAACasualties(data)) {
return RandomAACasualties(planes, dice, bridge, allowMultipleHitsPerUnit);
}
return IndividuallyFiredAACasualties(defending, planes, defendingAA, dice, bridge, allowMultipleHitsPerUnit);
}
}
}
/**
* http://triplea.sourceforge.net/mywiki/Forum#nabble-td4658925%7Ca4658925
* returns two lists, the first list is the air units that can be evenly divided into groups of 3 or 6 (depending on
* radar)
* the second list is all the air units that do not fit in the first list
*/
private static Tuple<List<List<Unit>>, List<Unit>> categorizeLowLuckAirUnits(final Collection<Unit> units,
final int groupSize) {
final Collection<UnitCategory> categorizedAir = UnitSeperator.categorize(units, null, false, true);
final List<List<Unit>> groupsOfSize = new ArrayList<>();
final List<Unit> toRoll = new ArrayList<>();
for (final UnitCategory uc : categorizedAir) {
final int remainder = uc.getUnits().size() % groupSize;
final int splitPosition = uc.getUnits().size() - remainder;
final List<Unit> group = new ArrayList<>(uc.getUnits().subList(0, splitPosition));
if (!group.isEmpty()) {
for (int i = 0; i < splitPosition; i += groupSize) {
final List<Unit> miniGroup = new ArrayList<>(uc.getUnits().subList(i, i + groupSize));
if (!miniGroup.isEmpty()) {
groupsOfSize.add(miniGroup);
}
}
}
toRoll.addAll(uc.getUnits().subList(splitPosition, uc.getUnits().size()));
}
return Tuple.of(groupsOfSize, toRoll);
}
private static CasualtyDetails getLowLuckAACasualties(final boolean defending, final Collection<Unit> planes,
final Collection<Unit> defendingAA, final DiceRoll dice, final IDelegateBridge bridge,
final boolean allowMultipleHitsPerUnit) {
{
final Set<Unit> duplicatesCheckSet1 = new HashSet<>(planes);
if (planes.size() != duplicatesCheckSet1.size()) {
throw new IllegalStateException(
"Duplicate Units Detected: Original List:" + planes + " HashSet:" + duplicatesCheckSet1);
}
final Set<Unit> duplicatesCheckSet2 = new HashSet<>(defendingAA);
if (defendingAA.size() != duplicatesCheckSet2.size()) {
throw new IllegalStateException(
"Duplicate Units Detected: Original List:" + defendingAA + " HashSet:" + duplicatesCheckSet2);
}
}
int hitsLeft = dice.getHits();
if (hitsLeft <= 0) {
return new CasualtyDetails();
}
// if we can damage units, do it now
final CasualtyDetails finalCasualtyDetails = new CasualtyDetails();
final GameData data = bridge.getData();
final Tuple<Integer, Integer> attackThenDiceSides =
DiceRoll.getAAattackAndMaxDiceSides(defendingAA, data, !defending);
final int highestAttack = attackThenDiceSides.getFirst();
if (highestAttack < 1) {
return new CasualtyDetails();
}
final int chosenDiceSize = attackThenDiceSides.getSecond();
final Triple<Integer, Integer, Boolean> triple =
DiceRoll.getTotalAAPowerThenHitsAndFillSortedDiceThenIfAllUseSameAttack(null, null, !defending, defendingAA,
planes, data, false);
// final int totalPower = triple.getFirst();
final boolean allSameAttackPower = triple.getThird();
// multiple HP units need to be counted multiple times:
final List<Unit> planesList = new ArrayList<>();
for (final Unit plane : planes) {
final int hpLeft =
allowMultipleHitsPerUnit ? (UnitAttachment.get(plane.getType()).getHitPoints() - plane.getHits())
: (Math.min(1, UnitAttachment.get(plane.getType()).getHitPoints() - plane.getHits()));
for (int hp = 0; hp < hpLeft; ++hp) {
// if allowMultipleHitsPerUnit, then because the number of rolls exactly equals the hitpoints of all units,
// we roll multiple times for any unit with multiple hitpoints
planesList.add(plane);
}
}
// killing the air by groups does not work if the the attack power is different for some of the rolls
// also, killing by groups does not work if some of the aa guns have 'MayOverStackAA' and we have more hits than the
// total number of
// groups (including the remainder group)
// (when i mean, 'does not work', i mean that it is no longer a mathematically fair way to find casualties)
// find group size (if no groups, do dice sides)
final int groupSize;
if (allSameAttackPower) {
groupSize = chosenDiceSize / highestAttack;
} else {
groupSize = chosenDiceSize;
}
final int numberOfGroupsByDiceSides = (int) Math.ceil((double) planesList.size() / (double) groupSize);
final boolean tooManyHitsToDoGroups = hitsLeft > numberOfGroupsByDiceSides;
if (!allSameAttackPower || tooManyHitsToDoGroups || chosenDiceSize % highestAttack != 0) {
// we have too many hits, so just pick randomly
return RandomAACasualties(planes, dice, bridge, allowMultipleHitsPerUnit);
} else {
// if we have a group of 6 fighters and 2 bombers, and dicesides is 6, and attack was 1, then we would want 1
// fighter to die for sure.
// this is what groupsize is for.
// if the attack is greater than 1 though, and all use the same attack power, then the group size can be smaller
// (ie: attack is 2, and
// we have 3 fighters and 2 bombers, we would want 1 fighter to die for sure).
// categorize with groupSize
final Tuple<List<List<Unit>>, List<Unit>> airSplit = categorizeLowLuckAirUnits(planesList, groupSize);
// the non rolling air units
// if we are less hits than the number of groups, OR we have equal hits to number of groups but we also have a
// remainder that is equal
// to or greater than group size,
// THEN we need to make sure to pick randomly, and include the remainder group. (reason we do not do this with any
// remainder size, is
// because we might have missed the dice roll to hit the remainder)
if (hitsLeft < (airSplit.getFirst().size()
+ ((int) Math.ceil((double) airSplit.getSecond().size() / (double) groupSize)))) {
// fewer hits than groups.
final List<Unit> tempPossibleHitUnits = new ArrayList<>();
for (final List<Unit> group : airSplit.getFirst()) {
tempPossibleHitUnits.add(group.get(0));
}
if (airSplit.getSecond().size() > 0) {
// if we have a remainder group, we need to add some of them into the mix
// but we have to do so randomly.
final List<Unit> remainders = new ArrayList<>(airSplit.getSecond());
if (remainders.size() == 1) {
tempPossibleHitUnits.add(remainders.remove(0));
} else {
final int numberOfRemainderGroups = (int) Math.ceil((double) remainders.size() / (double) groupSize);
final int[] randomRemainder = bridge.getRandom(remainders.size(), numberOfRemainderGroups, null,
DiceType.ENGINE, "Deciding which planes should die due to AA fire");
int pos2 = 0;
for (final int element : randomRemainder) {
pos2 += element;
tempPossibleHitUnits.add(remainders.remove(pos2 % remainders.size()));
}
}
}
final int[] hitRandom = bridge.getRandom(tempPossibleHitUnits.size(), hitsLeft, null, DiceType.ENGINE,
"Deciding which planes should die due to AA fire");
// now we find the
int pos = 0;
for (final int element : hitRandom) {
pos += element;
final Unit unitHit = tempPossibleHitUnits.remove(pos % tempPossibleHitUnits.size());
if (allowMultipleHitsPerUnit && (Collections.frequency(finalCasualtyDetails.getDamaged(),
unitHit) < (getTotalHitpointsLeft(unitHit) - 1))) {
finalCasualtyDetails.addToDamaged(unitHit);
} else {
finalCasualtyDetails.addToKilled(unitHit);
}
}
hitsLeft = 0;
} else {
// kill one in every group
for (final List<Unit> group : airSplit.getFirst()) {
final Unit unitHit = group.get(0);
if (allowMultipleHitsPerUnit && (Collections.frequency(finalCasualtyDetails.getDamaged(),
unitHit) < (getTotalHitpointsLeft(unitHit) - 1))) {
finalCasualtyDetails.addToDamaged(unitHit);
} else {
finalCasualtyDetails.addToKilled(unitHit);
}
hitsLeft--;
}
// for any hits left over...
if (hitsLeft == airSplit.getSecond().size()) {
for (final Unit unitHit : airSplit.getSecond()) {
if (allowMultipleHitsPerUnit && (Collections.frequency(finalCasualtyDetails.getDamaged(),
unitHit) < (getTotalHitpointsLeft(unitHit) - 1))) {
finalCasualtyDetails.addToDamaged(unitHit);
} else {
finalCasualtyDetails.addToKilled(unitHit);
}
}
hitsLeft = 0;
} else if (hitsLeft != 0) {
// the remainder
// roll all at once to prevent frequent random calls, important for pbem games
final int[] hitRandom = bridge.getRandom(airSplit.getSecond().size(), hitsLeft, null, DiceType.ENGINE,
"Deciding which planes should die due to AA fire");
int pos = 0;
for (final int element : hitRandom) {
pos += element;
final Unit unitHit = airSplit.getSecond().remove(pos % airSplit.getSecond().size());
if (allowMultipleHitsPerUnit && (Collections.frequency(finalCasualtyDetails.getDamaged(),
unitHit) < (getTotalHitpointsLeft(unitHit) - 1))) {
finalCasualtyDetails.addToDamaged(unitHit);
} else {
finalCasualtyDetails.addToKilled(unitHit);
}
}
hitsLeft = 0;
}
}
}
// double check
if (finalCasualtyDetails.size() != dice.getHits()) {
throw new IllegalStateException(
"wrong number of casulaties, expected:" + dice + " but got: " + finalCasualtyDetails);
}
return finalCasualtyDetails;
}
/**
* Choose plane casualties randomly.
*/
public static CasualtyDetails RandomAACasualties(final Collection<Unit> planes, final DiceRoll dice,
final IDelegateBridge bridge, final boolean allowMultipleHitsPerUnit) {
{
final Set<Unit> duplicatesCheckSet1 = new HashSet<>(planes);
if (planes.size() != duplicatesCheckSet1.size()) {
throw new IllegalStateException(
"Duplicate Units Detected: Original List:" + planes + " HashSet:" + duplicatesCheckSet1);
}
}
final int hitsLeft = dice.getHits();
if (hitsLeft <= 0) {
return new CasualtyDetails();
}
final CasualtyDetails finalCasualtyDetails = new CasualtyDetails();
// normal behavior is instant kill, which means planes.size()
final int planeHP = (allowMultipleHitsPerUnit ? getTotalHitpointsLeft(planes) : planes.size());
final List<Unit> planesList = new ArrayList<>();
for (final Unit plane : planes) {
final int hpLeft =
allowMultipleHitsPerUnit ? (UnitAttachment.get(plane.getType()).getHitPoints() - plane.getHits())
: (Math.min(1, UnitAttachment.get(plane.getType()).getHitPoints() - plane.getHits()));
for (int hp = 0; hp < hpLeft; ++hp) {
// if allowMultipleHitsPerUnit, then because the number of rolls exactly equals the hitpoints of all units,
// we roll multiple times for any unit with multiple hitpoints
planesList.add(plane);
}
}
// We need to choose which planes die randomly
if (hitsLeft < planeHP) {
// roll all at once to prevent frequent random calls, important for pbem games
final int[] hitRandom =
bridge.getRandom(planeHP, hitsLeft, null, DiceType.ENGINE, "Deciding which planes should die due to AA fire");
int pos = 0;
for (final int element : hitRandom) {
pos += element;
final Unit unitHit = planesList.remove(pos % planesList.size());
if (allowMultipleHitsPerUnit && (Collections.frequency(finalCasualtyDetails.getDamaged(),
unitHit) < (getTotalHitpointsLeft(unitHit) - 1))) {
finalCasualtyDetails.addToDamaged(unitHit);
} else {
finalCasualtyDetails.addToKilled(unitHit);
}
}
} else {
for (final Unit plane : planesList) {
if (finalCasualtyDetails.getKilled().contains(plane)) {
finalCasualtyDetails.addToDamaged(plane);
} else {
finalCasualtyDetails.addToKilled(plane);
}
}
}
return finalCasualtyDetails;
}
/**
* Choose plane casualties based on individual AA shots at each aircraft.
*/
private static CasualtyDetails IndividuallyFiredAACasualties(final boolean defending, final Collection<Unit> planes,
final Collection<Unit> defendingAA, final DiceRoll dice, final IDelegateBridge bridge,
final boolean allowMultipleHitsPerUnit) {
// if we have aa guns that are not infinite, then we need to randomly decide the aa casualties since there are not
// enough rolls to have
// a single roll for each aircraft, or too many rolls
// normal behavior is instant kill, which means planes.size()
final int planeHP = (allowMultipleHitsPerUnit ? getTotalHitpointsLeft(planes) : planes.size());
if (DiceRoll.getTotalAAattacks(defendingAA, planes) != planeHP) {
return RandomAACasualties(planes, dice, bridge, allowMultipleHitsPerUnit);
}
final Triple<Integer, Integer, Boolean> triple =
DiceRoll.getTotalAAPowerThenHitsAndFillSortedDiceThenIfAllUseSameAttack(null, null, !defending, defendingAA,
planes, bridge.getData(), false);
final boolean allSameAttackPower = triple.getThird();
if (!allSameAttackPower) {
return RandomAACasualties(planes, dice, bridge, allowMultipleHitsPerUnit);
}
final Tuple<Integer, Integer> attackThenDiceSides =
DiceRoll.getAAattackAndMaxDiceSides(defendingAA, bridge.getData(), !defending);
final int highestAttack = attackThenDiceSides.getFirst();
// int chosenDiceSize = attackThenDiceSides[1];
final CasualtyDetails finalCasualtyDetails = new CasualtyDetails();
final int hits = dice.getHits();
final List<Unit> planesList = new ArrayList<>();
for (final Unit plane : planes) {
final int hpLeft =
allowMultipleHitsPerUnit ? (UnitAttachment.get(plane.getType()).getHitPoints() - plane.getHits())
: (Math.min(1, UnitAttachment.get(plane.getType()).getHitPoints() - plane.getHits()));
for (int hp = 0; hp < hpLeft; ++hp) {
// if allowMultipleHitsPerUnit, then because the number of rolls exactly equals the hitpoints of all units,
// we roll multiple times for any unit with multiple hitpoints
planesList.add(plane);
}
}
// We need to choose which planes die based on their position in the list and the individual AA rolls
if (hits > planeHP) {
throw new IllegalStateException("Cannot have more hits than number of die rolls");
}
if (hits < planeHP) {
final List<Die> rolls = dice.getRolls(highestAttack);
for (int i = 0; i < rolls.size(); i++) {
final Die die = rolls.get(i);
if (die.getType() == DieType.HIT) {
final Unit unit = planesList.get(i);
if (allowMultipleHitsPerUnit
&& (Collections.frequency(finalCasualtyDetails.getDamaged(), unit) < (getTotalHitpointsLeft(unit) - 1))) {
finalCasualtyDetails.addToDamaged(unit);
} else {
finalCasualtyDetails.addToKilled(unit);
}
}
}
} else {
for (final Unit plane : planesList) {
if (finalCasualtyDetails.getKilled().contains(plane)) {
finalCasualtyDetails.addToDamaged(plane);
} else {
finalCasualtyDetails.addToKilled(plane);
}
}
}
return finalCasualtyDetails;
}
/**
* @param battleID
* may be null if we are not in a battle (eg, if this is an aa fire due to moving).
*/
public static CasualtyDetails selectCasualties(final String step, final PlayerID player,
final Collection<Unit> targetsToPickFrom, final Collection<Unit> friendlyUnits, final PlayerID enemyPlayer,
final Collection<Unit> enemyUnits, final boolean amphibious, final Collection<Unit> amphibiousLandAttackers,
final Territory battlesite, final Collection<TerritoryEffect> territoryEffects, final IDelegateBridge bridge,
final String text, final DiceRoll dice, final boolean defending, final GUID battleID, final boolean headLess,
final int extraHits, final boolean allowMultipleHitsPerUnit) {
if (targetsToPickFrom.isEmpty()) {
return new CasualtyDetails();
}
if (!friendlyUnits.containsAll(targetsToPickFrom)) {
throw new IllegalStateException("friendlyUnits should but does not contain all units from targetsToPickFrom");
}
final GameData data = bridge.getData();
final boolean isEditMode = BaseEditDelegate.getEditMode(data);
ITripleAPlayer tripleaPlayer;
if (player.isNull()) {
tripleaPlayer = new WeakAI(player.getName(), TripleA.WEAK_COMPUTER_PLAYER_TYPE);
} else {
tripleaPlayer = (ITripleAPlayer) bridge.getRemotePlayer(player);
}
Map<Unit, Collection<Unit>> dependents;
if (headLess) {
dependents = Collections.emptyMap();
} else {
dependents = getDependents(targetsToPickFrom);
}
if (isEditMode && !headLess) {
final CasualtyDetails editSelection = tripleaPlayer.selectCasualties(targetsToPickFrom, dependents, 0, text, dice,
player, friendlyUnits, enemyPlayer, enemyUnits, amphibious, amphibiousLandAttackers, new CasualtyList(),
battleID, battlesite, allowMultipleHitsPerUnit);
List<Unit> killed = editSelection.getKilled();
// if partial retreat is possible, kill amphibious units first
if (isPartialAmphibiousRetreat(data)) {
killed = killAmphibiousFirst(killed, targetsToPickFrom);
}
return editSelection;
}
if (dice.getHits() == 0) {
return new CasualtyDetails(Collections.emptyList(), Collections.emptyList(), true);
}
int hitsRemaining = dice.getHits();
if (isTransportCasualtiesRestricted(data)) {
hitsRemaining = extraHits;
}
if (!isEditMode && allTargetsOneTypeOneHitPoint(targetsToPickFrom, dependents)) {
final List<Unit> killed = new ArrayList<>();
final Iterator<Unit> iter = targetsToPickFrom.iterator();
for (int i = 0; i < hitsRemaining; i++) {
if (i >= targetsToPickFrom.size()) {
break;
}
killed.add(iter.next());
}
return new CasualtyDetails(killed, Collections.emptyList(), true);
}
// Create production cost map, Maybe should do this elsewhere, but in case prices change, we do it here.
final IntegerMap<UnitType> costs = getCostsForTUV(player, data);
final Tuple<CasualtyList, List<Unit>> defaultCasualtiesAndSortedTargets = getDefaultCasualties(targetsToPickFrom,
hitsRemaining, defending, player, enemyUnits, amphibious, amphibiousLandAttackers,
battlesite, costs, territoryEffects, data, allowMultipleHitsPerUnit, true);
final CasualtyList defaultCasualties = defaultCasualtiesAndSortedTargets.getFirst();
final List<Unit> sortedTargetsToPickFrom = defaultCasualtiesAndSortedTargets.getSecond();
if (sortedTargetsToPickFrom.size() != targetsToPickFrom.size()
|| !targetsToPickFrom.containsAll(sortedTargetsToPickFrom)
|| !sortedTargetsToPickFrom.containsAll(targetsToPickFrom)) {
throw new IllegalStateException("sortedTargetsToPickFrom must contain the same units as targetsToPickFrom list");
}
final int totalHitpoints =
(allowMultipleHitsPerUnit ? getTotalHitpointsLeft(sortedTargetsToPickFrom) : sortedTargetsToPickFrom.size());
final CasualtyDetails casualtySelection;
if (hitsRemaining >= totalHitpoints) {
casualtySelection = new CasualtyDetails(defaultCasualties, true);
} else {
casualtySelection = tripleaPlayer.selectCasualties(sortedTargetsToPickFrom, dependents, hitsRemaining, text, dice,
player, friendlyUnits, enemyPlayer, enemyUnits, amphibious, amphibiousLandAttackers, defaultCasualties,
battleID, battlesite, allowMultipleHitsPerUnit);
}
List<Unit> killed = casualtySelection.getKilled();
// if partial retreat is possible, kill amphibious units first
if (isPartialAmphibiousRetreat(data)) {
killed = killAmphibiousFirst(killed, sortedTargetsToPickFrom);
}
final List<Unit> damaged = casualtySelection.getDamaged();
int numhits = killed.size();
if (!allowMultipleHitsPerUnit) {
damaged.clear();
} else {
for (final Unit unit : killed) {
final UnitAttachment ua = UnitAttachment.get(unit.getType());
final int damageToUnit = Collections.frequency(damaged, unit);
// allowed damage
numhits += Math.max(0, Math.min(damageToUnit, (ua.getHitPoints() - (1 + unit.getHits()))));
final Iterator<Unit> iter = damaged.iterator();
while (iter.hasNext()) {
if (unit.equals(iter.next())) {
// remove from damaged list, since they will die
iter.remove();
}
}
}
}
// check right number
if (!isEditMode
&& !(numhits + damaged.size() == (hitsRemaining > totalHitpoints ? totalHitpoints : hitsRemaining))) {
tripleaPlayer.reportError("Wrong number of casualties selected");
if (headLess) {
System.err.println("Possible Infinite Loop: Wrong number of casualties selected: number of hits on units "
+ (numhits + damaged.size()) + " != number of hits to take "
+ (hitsRemaining > totalHitpoints ? totalHitpoints : hitsRemaining) + ", for "
+ casualtySelection.toString());
}
return selectCasualties(step, player, sortedTargetsToPickFrom, friendlyUnits, enemyPlayer, enemyUnits, amphibious,
amphibiousLandAttackers, battlesite, territoryEffects, bridge, text, dice, defending, battleID, headLess,
extraHits, allowMultipleHitsPerUnit);
}
// check we have enough of each type
if (!sortedTargetsToPickFrom.containsAll(killed) || !sortedTargetsToPickFrom.containsAll(damaged)) {
tripleaPlayer.reportError("Cannot remove enough units of those types");
if (headLess) {
System.err.println("Possible Infinite Loop: Cannot remove enough units of those types: targets "
+ MyFormatter.unitsToTextNoOwner(sortedTargetsToPickFrom) + ", for " + casualtySelection.toString());
}
return selectCasualties(step, player, sortedTargetsToPickFrom, friendlyUnits, enemyPlayer, enemyUnits, amphibious,
amphibiousLandAttackers, battlesite, territoryEffects, bridge, text, dice, defending, battleID, headLess,
extraHits, allowMultipleHitsPerUnit);
}
return casualtySelection;
}
private static List<Unit> killAmphibiousFirst(final List<Unit> killed, final Collection<Unit> targets) {
final Collection<Unit> allAmphibUnits = new ArrayList<>();
final Collection<Unit> killedNonAmphibUnits = new ArrayList<>();
final Collection<UnitType> amphibTypes = new ArrayList<>();
// Get a list of all selected killed units that are NOT amphibious
final Match<Unit> aMatch = new CompositeMatchAnd<>(Matches.UnitIsLand, Matches.UnitWasNotAmphibious);
killedNonAmphibUnits.addAll(Match.getMatches(killed, aMatch));
// If all killed units are amphibious, just return them
if (killedNonAmphibUnits.isEmpty()) {
return killed;
}
// Get a list of all units that are amphibious and remove those that are killed
allAmphibUnits.addAll(Match.getMatches(targets, Matches.UnitWasAmphibious));
allAmphibUnits.removeAll(Match.getMatches(killed, Matches.UnitWasAmphibious));
final Iterator<Unit> allAmphibUnitsIter = allAmphibUnits.iterator();
// Get a collection of the unit types of the amphib units
while (allAmphibUnitsIter.hasNext()) {
final Unit unit = allAmphibUnitsIter.next();
final UnitType ut = unit.getType();
if (!amphibTypes.contains(ut)) {
amphibTypes.add(ut);
}
}
// For each killed unit- see if there is an amphib unit that can be killed instead
for (final Unit unit : killedNonAmphibUnits) {
if (amphibTypes.contains(unit.getType())) { // add a unit from the collection
final List<Unit> oneAmphibUnit = Match.getNMatches(allAmphibUnits, 1, Matches.unitIsOfType(unit.getType()));
if (oneAmphibUnit.size() > 0) {
final Unit amphibUnit = oneAmphibUnit.iterator().next();
killed.remove(unit);
killed.add(amphibUnit);
allAmphibUnits.remove(amphibUnit);
} else { // If there are no more units of that type, remove the type from the collection
amphibTypes.remove(unit.getType());
}
}
}
return killed;
}
/**
* A unit with two hitpoints will be listed twice if they will die. The first time they are listed it is as damaged.
* The second time they
* are listed, it is dead.
*/
private static Tuple<CasualtyList, List<Unit>> getDefaultCasualties(final Collection<Unit> targetsToPickFrom,
final int hits, final boolean defending, final PlayerID player, final Collection<Unit> enemyUnits,
final boolean amphibious, final Collection<Unit> amphibiousLandAttackers, final Territory battlesite,
final IntegerMap<UnitType> costs, final Collection<TerritoryEffect> territoryEffects, final GameData data,
final boolean allowMultipleHitsPerUnit, final boolean bonus) {
final CasualtyList defaultCasualtySelection = new CasualtyList();
// Sort units by power and cost in ascending order
final List<Unit> sorted = sortUnitsForCasualtiesWithSupport(targetsToPickFrom, defending, player,
enemyUnits, amphibious, amphibiousLandAttackers, battlesite, costs,
territoryEffects, data, bonus);
// Remove two hit bb's selecting them first for default casualties
int numSelectedCasualties = 0;
if (allowMultipleHitsPerUnit) {
for (final Unit unit : sorted) {
// Stop if we have already selected as many hits as there are targets
if (numSelectedCasualties >= hits) {
return Tuple.of(defaultCasualtySelection, sorted);
}
final UnitAttachment ua = UnitAttachment.get(unit.getType());
final int extraHP = Math.min((hits - numSelectedCasualties), (ua.getHitPoints() - (1 + unit.getHits())));
for (int i = 0; i < extraHP; i++) {
numSelectedCasualties++;
defaultCasualtySelection.addToDamaged(unit);
}
}
}
// Select units
for (final Unit unit : sorted) {
// Stop if we have already selected as many hits as there are targets
if (numSelectedCasualties >= hits) {
return Tuple.of(defaultCasualtySelection, sorted);
}
defaultCasualtySelection.addToKilled(unit);
numSelectedCasualties++;
}
return Tuple.of(defaultCasualtySelection, sorted);
}
/**
* The purpose of this is to return a list in the PERFECT order of which units should be selected to die first,
* And that means that certain units MUST BE INTERLEAVED.
* This list assumes that you have already taken any extra hit points away from any 2 hitpoint units.
* Example: You have a 1 attack Artillery unit that supports, and a 1 attack infantry unit that can receive support.
* The best selection of units to die is first to take whichever unit has excess, then cut that down til they are both
* the same size,
* then to take 1 artillery followed by 1 infantry, followed by 1 artillery, then 1 inf, etc, until everyone is dead.
* If you just return all infantry followed by all artillery, or the other way around, you will be missing out on some
* important support
* provided.
* (Veqryn)
*/
private static List<Unit> sortUnitsForCasualtiesWithSupport(final Collection<Unit> targetsToPickFrom,
final boolean defending, final PlayerID player, final Collection<Unit> enemyUnits, final boolean amphibious,
final Collection<Unit> amphibiousLandAttackers, final Territory battlesite, final IntegerMap<UnitType> costs,
final Collection<TerritoryEffect> territoryEffects, final GameData data, final boolean bonus) {
// Convert unit lists to unit type lists
final List<UnitType> targetTypes = new ArrayList<>();
for (final Unit u : targetsToPickFrom) {
targetTypes.add(u.getType());
}
final List<UnitType> amphibTypes = new ArrayList<>();
if (amphibiousLandAttackers != null) {
for (final Unit u : amphibiousLandAttackers) {
amphibTypes.add(u.getType());
}
}
// Calculate hashes and cache key
int targetsHashCode = 1;
for (final UnitType ut : targetTypes) {
targetsHashCode += ut.hashCode();
}
targetsHashCode *= 31;
int amphibHashCode = 1;
for (final UnitType ut : amphibTypes) {
amphibHashCode += ut.hashCode();
}
amphibHashCode *= 31;
String key = player.getName() + "|" + battlesite.getName() + "|" + defending + "|" + amphibious + "|"
+ targetsHashCode + "|" + amphibHashCode;
// Check OOL cache
final List<UnitType> stored = oolCache.get(key);
if (stored != null) {
// System.out.println("Hit with cacheSize=" + oolCache.size() + ", key=" + key);
final List<Unit> result = new ArrayList<>();
final List<Unit> selectFrom = new ArrayList<>(targetsToPickFrom);
for (final UnitType ut : stored) {
for (final Iterator<Unit> it = selectFrom.iterator(); it.hasNext();) {
final Unit u = it.next();
if (ut.equals(u.getType())) {
result.add(u);
it.remove();
}
}
}
return result;
}
// System.out.println("Miss with cacheSize=" + oolCache.size() + ", key=" + key);
// Sort enough units to kill off
final List<Unit> sortedUnitsList = new ArrayList<>(targetsToPickFrom);
Collections.sort(sortedUnitsList, new UnitBattleComparator(defending, costs, territoryEffects, data, bonus, false));
// Sort units starting with strongest so that support gets added to them first
Collections.reverse(sortedUnitsList);
final UnitBattleComparator unitComparatorWithoutPrimaryPower =
new UnitBattleComparator(defending, costs, territoryEffects, data, bonus, true);
final List<Unit> sortedWellEnoughUnitsList = new ArrayList<>();
final Map<Unit, IntegerMap<Unit>> unitSupportPowerMap = new HashMap<>();
final Map<Unit, IntegerMap<Unit>> unitSupportRollsMap = new HashMap<>();
final Map<Unit, Tuple<Integer, Integer>> unitPowerAndRollsMap = DiceRoll.getUnitPowerAndRollsForNormalBattles(
sortedUnitsList, new ArrayList<>(enemyUnits), defending, false, data, battlesite,
territoryEffects, amphibious, amphibiousLandAttackers, unitSupportPowerMap, unitSupportRollsMap);
// Sort units starting with weakest for finding the worst units
Collections.reverse(sortedUnitsList);
for (int i = 0; i < sortedUnitsList.size(); ++i) {
// Loop through all target units to find the best unit to take as casualty
Unit worstUnit = null;
int minPower = Integer.MAX_VALUE;
final Set<UnitType> unitTypes = new HashSet<>();
for (final Unit u : sortedUnitsList) {
if (unitTypes.contains(u.getType())) {
continue;
}
unitTypes.add(u.getType());
// Find unit power
final Map<Unit, Tuple<Integer, Integer>> currentUnitMap = new HashMap<>();
currentUnitMap.put(u, unitPowerAndRollsMap.get(u));
int power = DiceRoll.getTotalPower(currentUnitMap, data);
// Add any support power that it provides to other units
final IntegerMap<Unit> unitSupportPowerMapForUnit = unitSupportPowerMap.get(u);
if (unitSupportPowerMapForUnit != null) {
for (final Unit supportedUnit : unitSupportPowerMapForUnit.keySet()) {
Tuple<Integer, Integer> strengthAndRolls = unitPowerAndRollsMap.get(supportedUnit);
if (strengthAndRolls == null) {
continue;
}
// Remove any rolls provided by this support so they aren't counted twice
final IntegerMap<Unit> unitSupportRollsMapForUnit = unitSupportRollsMap.get(u);
if (unitSupportRollsMapForUnit != null) {
strengthAndRolls = Tuple.of(strengthAndRolls.getFirst(),
strengthAndRolls.getSecond() - unitSupportRollsMapForUnit.getInt(supportedUnit));
}
// If one roll then just add the power
if (strengthAndRolls.getSecond() == 1) {
power += unitSupportPowerMapForUnit.getInt(supportedUnit);
continue;
}
// Find supported unit power with support
final Map<Unit, Tuple<Integer, Integer>> supportedUnitMap = new HashMap<>();
supportedUnitMap.put(supportedUnit, strengthAndRolls);
final int powerWithSupport = DiceRoll.getTotalPower(supportedUnitMap, data);
// Find supported unit power without support
final int strengthWithoutSupport =
strengthAndRolls.getFirst() - unitSupportPowerMapForUnit.getInt(supportedUnit);
final Tuple<Integer, Integer> strengthAndRollsWithoutSupport =
Tuple.of(strengthWithoutSupport, strengthAndRolls.getSecond());
supportedUnitMap.put(supportedUnit, strengthAndRollsWithoutSupport);
final int powerWithoutSupport = DiceRoll.getTotalPower(supportedUnitMap, data);
// Add the actual power provided by the support
final int addedPower = powerWithSupport - powerWithoutSupport;
power += addedPower;
}
}
// Add any power from support rolls that it provides to other units
final IntegerMap<Unit> unitSupportRollsMapForUnit = unitSupportRollsMap.get(u);
if (unitSupportRollsMapForUnit != null) {
for (final Unit supportedUnit : unitSupportRollsMapForUnit.keySet()) {
final Tuple<Integer, Integer> strengthAndRolls = unitPowerAndRollsMap.get(supportedUnit);
if (strengthAndRolls == null) {
continue;
}
// Find supported unit power with support
final Map<Unit, Tuple<Integer, Integer>> supportedUnitMap = new HashMap<>();
supportedUnitMap.put(supportedUnit, strengthAndRolls);
final int powerWithSupport = DiceRoll.getTotalPower(supportedUnitMap, data);
// Find supported unit power without support
final int rollsWithoutSupport =
strengthAndRolls.getSecond() - unitSupportRollsMap.get(u).getInt(supportedUnit);
final Tuple<Integer, Integer> strengthAndRollsWithoutSupport =
Tuple.of(strengthAndRolls.getFirst(), rollsWithoutSupport);
supportedUnitMap.put(supportedUnit, strengthAndRollsWithoutSupport);
final int powerWithoutSupport = DiceRoll.getTotalPower(supportedUnitMap, data);
// Add the actual power provided by the support
final int addedPower = powerWithSupport - powerWithoutSupport;
power += addedPower;
}
}
// Check if unit has lower power
if (power < minPower || (power == minPower && unitComparatorWithoutPrimaryPower.compare(u, worstUnit) < 0)) {
worstUnit = u;
minPower = power;
}
}
// Add worst unit to sorted list, update any units it supported, and remove from other collections
final IntegerMap<Unit> unitSupportPowerMapForUnit = unitSupportPowerMap.get(worstUnit);
if (unitSupportPowerMapForUnit != null) {
for (final Unit supportedUnit : unitSupportPowerMapForUnit.keySet()) {
final Tuple<Integer, Integer> strengthAndRolls = unitPowerAndRollsMap.get(supportedUnit);
if (strengthAndRolls == null) {
continue;
}
final int strengthWithoutSupport =
strengthAndRolls.getFirst() - unitSupportPowerMapForUnit.getInt(supportedUnit);
final Tuple<Integer, Integer> strengthAndRollsWithoutSupport =
Tuple.of(strengthWithoutSupport, strengthAndRolls.getSecond());
unitPowerAndRollsMap.put(supportedUnit, strengthAndRollsWithoutSupport);
sortedUnitsList.remove(supportedUnit);
sortedUnitsList.add(0, supportedUnit);
}
}
final IntegerMap<Unit> unitSupportRollsMapForUnit = unitSupportRollsMap.get(worstUnit);
if (unitSupportRollsMapForUnit != null) {
for (final Unit supportedUnit : unitSupportRollsMapForUnit.keySet()) {
final Tuple<Integer, Integer> strengthAndRolls = unitPowerAndRollsMap.get(supportedUnit);
if (strengthAndRolls == null) {
continue;
}
final int rollsWithoutSupport =
strengthAndRolls.getSecond() - unitSupportRollsMapForUnit.getInt(supportedUnit);
final Tuple<Integer, Integer> strengthAndRollsWithoutSupport =
Tuple.of(strengthAndRolls.getFirst(), rollsWithoutSupport);
unitPowerAndRollsMap.put(supportedUnit, strengthAndRollsWithoutSupport);
sortedUnitsList.remove(supportedUnit);
sortedUnitsList.add(0, supportedUnit);
}
}
sortedWellEnoughUnitsList.add(worstUnit);
sortedUnitsList.remove(worstUnit);
unitPowerAndRollsMap.remove(worstUnit);
unitSupportPowerMap.remove(worstUnit);
unitSupportRollsMap.remove(worstUnit);
}
sortedWellEnoughUnitsList.addAll(sortedUnitsList);
// Cache result and all subsets of the result
final List<UnitType> unitTypes = new ArrayList<>();
for (final Unit u : sortedWellEnoughUnitsList) {
unitTypes.add(u.getType());
}
for (final Iterator<UnitType> it = unitTypes.iterator(); it.hasNext();) {
oolCache.put(key, new ArrayList<>(unitTypes));
final UnitType unitTypeToRemove = it.next();
targetTypes.remove(unitTypeToRemove);
if (Collections.frequency(targetTypes, unitTypeToRemove) < Collections.frequency(amphibTypes, unitTypeToRemove)) {
amphibTypes.remove(unitTypeToRemove);
}
targetsHashCode = 1;
for (final UnitType ut : targetTypes) {
targetsHashCode += ut.hashCode();
}
targetsHashCode *= 31;
amphibHashCode = 1;
for (final UnitType ut : amphibTypes) {
amphibHashCode += ut.hashCode();
}
amphibHashCode *= 31;
key = player.getName() + "|" + battlesite.getName() + "|" + defending + "|" + amphibious + "|" + targetsHashCode
+ "|" + amphibHashCode;
it.remove();
}
return sortedWellEnoughUnitsList;
}
public static Map<Unit, Collection<Unit>> getDependents(final Collection<Unit> targets) {
// just worry about transports
final Map<Unit, Collection<Unit>> dependents = new HashMap<>();
for (final Unit target : targets) {
dependents.put(target, TransportTracker.transportingAndUnloaded(target));
}
return dependents;
}
/**
* Return map where keys are unit types and values are PU costs of that unit type, based on a player.
* Any production rule that produces multiple units
* (like artillery in NWO, costs 7 but makes 2 artillery, meaning effective price is 3.5 each)
* will have their costs rounded up on a per unit basis (so NWO artillery will become 4).
* Therefore, this map should NOT be used for Purchasing information!
*
* @param player
* The player to get costs schedule for
* @param data
* The game data.
* @return a map of unit types to PU cost
*/
public static IntegerMap<UnitType> getCostsForTUV(final PlayerID player, final GameData data) {
final Resource PUS;
data.acquireReadLock();
try {
PUS = data.getResourceList().getResource(Constants.PUS);
} finally {
data.releaseReadLock();
}
final IntegerMap<UnitType> costs = new IntegerMap<>();
final ProductionFrontier frontier = player.getProductionFrontier();
// any one will do then
if (frontier == null) {
return getCostsForTuvForAllPlayersMergedAndAveraged(data);
}
for (final ProductionRule rule : frontier.getRules()) {
final int costPerGroup = rule.getCosts().getInt(PUS);
final NamedAttachable resourceOrUnit = rule.getResults().keySet().iterator().next();
if (!(resourceOrUnit instanceof UnitType)) {
continue;
}
final UnitType type = (UnitType) resourceOrUnit;
final int numberProduced = rule.getResults().getInt(type);
// we average the cost for a single unit, rounding up
final int roundedCostPerSingle = (int) Math.ceil((double) costPerGroup / (double) numberProduced);
costs.put(type, roundedCostPerSingle);
}
// since our production frontier may not cover all the units we control, and not the enemy units,
// we will add any unit types not in our list, based on the list for everyone
final IntegerMap<UnitType> costsAll = getCostsForTuvForAllPlayersMergedAndAveraged(data);
for (final UnitType ut : costsAll.keySet()) {
if (!costs.keySet().contains(ut)) {
costs.put(ut, costsAll.getInt(ut));
}
}
return costs;
}
/**
* Return a map where key are unit types and values are the AVERAGED for all RULES (not for all players).
* Any production rule that produces multiple units
* (like artillery in NWO, costs 7 but makes 2 artillery, meaning effective price is 3.5 each)
* will have their costs rounded up on a per unit basis.
* Therefore, this map should NOT be used for Purchasing information!
*/
public static IntegerMap<UnitType> getCostsForTuvForAllPlayersMergedAndAveraged(final GameData data) {
/*
* if (s_costsForTuvForAllPlayersMergedAndAveraged != null && s_costsForTuvForAllPlayersMergedAndAveraged.size() >
* 0)
* return s_costsForTuvForAllPlayersMergedAndAveraged;
*/
final Resource PUS;
data.acquireReadLock();
try {
PUS = data.getResourceList().getResource(Constants.PUS);
} finally {
data.releaseReadLock();
}
final IntegerMap<UnitType> costs = new IntegerMap<>();
final HashMap<UnitType, List<Integer>> differentCosts = new HashMap<>();
for (final ProductionRule rule : data.getProductionRuleList().getProductionRules()) {
// only works for the first result, so we are assuming each purchase frontier only gives one type of unit
final NamedAttachable resourceOrUnit = rule.getResults().keySet().iterator().next();
if (!(resourceOrUnit instanceof UnitType)) {
continue;
}
final UnitType ut = (UnitType) resourceOrUnit;
final int numberProduced = rule.getResults().getInt(ut);
final int costPerGroup = rule.getCosts().getInt(PUS);
// we round up the cost
final int roundedCostPerSingle = (int) Math.ceil((double) costPerGroup / (double) numberProduced);
if (differentCosts.containsKey(ut)) {
differentCosts.get(ut).add(roundedCostPerSingle);
} else {
final List<Integer> listTemp = new ArrayList<>();
listTemp.add(roundedCostPerSingle);
differentCosts.put(ut, listTemp);
}
}
for (final UnitType ut : differentCosts.keySet()) {
int totalCosts = 0;
final List<Integer> costsForType = differentCosts.get(ut);
for (final int cost : costsForType) {
totalCosts += cost;
}
final int averagedCost = (int) Math.round(((double) totalCosts / (double) costsForType.size()));
costs.put(ut, averagedCost);
}
// There is a problem with this variable, that it isn't being cleared out when we
// s_costsForTuvForAllPlayersMergedAndAveraged = costs;
// switch maps.
return costs;
}
/**
* Return map where keys are unit types and values are resource costs of that unit type, based on a player.
* Any production rule that produces multiple units
* (like artillery in NWO, costs 7 but makes 2 artillery, meaning effective price is 3.5 each)
* will have their costs rounded up on a per unit basis.
* Therefore, this map should NOT be used for Purchasing information!
*/
public static Map<PlayerID, Map<UnitType, ResourceCollection>> getResourceCostsForTUV(final GameData data,
final boolean includeAverageForMissingUnits) {
final LinkedHashMap<PlayerID, Map<UnitType, ResourceCollection>> rVal =
new LinkedHashMap<>();
final Map<UnitType, ResourceCollection> average = includeAverageForMissingUnits
? getResourceCostsForTUVForAllPlayersMergedAndAveraged(data) : new HashMap<>();
final List<PlayerID> players = data.getPlayerList().getPlayers();
players.add(PlayerID.NULL_PLAYERID);
for (final PlayerID p : players) {
final ProductionFrontier frontier = p.getProductionFrontier();
// any one will do then
if (frontier == null) {
rVal.put(p, average);
continue;
}
Map<UnitType, ResourceCollection> current = rVal.get(p);
if (current == null) {
current = new LinkedHashMap<>();
rVal.put(p, current);
}
for (final ProductionRule rule : frontier.getRules()) {
if (rule == null || rule.getResults() == null || rule.getResults().isEmpty() || rule.getCosts() == null
|| rule.getCosts().isEmpty()) {
continue;
}
final IntegerMap<NamedAttachable> unitMap = rule.getResults();
final ResourceCollection costPerGroup = new ResourceCollection(data, rule.getCosts());
final Set<UnitType> units = new HashSet<>();
for (final NamedAttachable resourceOrUnit : unitMap.keySet()) {
if (!(resourceOrUnit instanceof UnitType)) {
continue;
}
units.add((UnitType) resourceOrUnit);
}
if (units.isEmpty()) {
continue;
}
final int totalProduced = unitMap.totalValues();
if (totalProduced == 1) {
current.put(units.iterator().next(), costPerGroup);
} else if (totalProduced > 1) {
costPerGroup.discount((double) 1 / (double) totalProduced);
for (final UnitType ut : units) {
current.put(ut, costPerGroup);
}
}
}
// since our production frontier may not cover all the units we control, and not the enemy units,
// we will add any unit types not in our list, based on the list for everyone
for (final UnitType ut : average.keySet()) {
if (!current.keySet().contains(ut)) {
current.put(ut, average.get(ut));
}
}
}
rVal.put(null, average);
return rVal;
}
/**
* Return a map where key are unit types and values are the AVERAGED for all players.
* Any production rule that produces multiple units
* (like artillery in NWO, costs 7 but makes 2 artillery, meaning effective price is 3.5 each)
* will have their costs rounded up on a per unit basis.
* Therefore, this map should NOT be used for Purchasing information!
*/
private static Map<UnitType, ResourceCollection> getResourceCostsForTUVForAllPlayersMergedAndAveraged(
final GameData data) {
final Map<UnitType, ResourceCollection> average = new HashMap<>();
final Resource PUS;
data.acquireReadLock();
try {
PUS = data.getResourceList().getResource(Constants.PUS);
} finally {
data.releaseReadLock();
}
final IntegerMap<Resource> defaultMap = new IntegerMap<>();
defaultMap.put(PUS, 1);
final ResourceCollection defaultResources = new ResourceCollection(data, defaultMap);
final Map<UnitType, List<ResourceCollection>> backups = new HashMap<>();
final Map<UnitType, ResourceCollection> backupAveraged = new HashMap<>();
for (final ProductionRule rule : data.getProductionRuleList().getProductionRules()) {
if (rule == null || rule.getResults() == null || rule.getResults().isEmpty() || rule.getCosts() == null
|| rule.getCosts().isEmpty()) {
continue;
}
final IntegerMap<NamedAttachable> unitMap = rule.getResults();
final ResourceCollection costPerGroup = new ResourceCollection(data, rule.getCosts());
final Set<UnitType> units = new HashSet<>();
for (final NamedAttachable resourceOrUnit : unitMap.keySet()) {
if (!(resourceOrUnit instanceof UnitType)) {
continue;
}
units.add((UnitType) resourceOrUnit);
}
if (units.isEmpty()) {
continue;
}
final int totalProduced = unitMap.totalValues();
if (totalProduced == 1) {
final UnitType ut = units.iterator().next();
List<ResourceCollection> current = backups.get(ut);
if (current == null) {
current = new ArrayList<>();
backups.put(ut, current);
}
current.add(costPerGroup);
} else if (totalProduced > 1) {
costPerGroup.discount((double) 1 / (double) totalProduced);
for (final UnitType ut : units) {
List<ResourceCollection> current = backups.get(ut);
if (current == null) {
current = new ArrayList<>();
backups.put(ut, current);
}
current.add(costPerGroup);
}
}
}
for (final Entry<UnitType, List<ResourceCollection>> entry : backups.entrySet()) {
final ResourceCollection avgCost =
new ResourceCollection(entry.getValue().toArray(new ResourceCollection[entry.getValue().size()]), data);
if (entry.getValue().size() > 1) {
avgCost.discount((double) 1 / (double) entry.getValue().size());
}
backupAveraged.put(entry.getKey(), avgCost);
}
final Map<PlayerID, Map<UnitType, ResourceCollection>> allPlayersCurrent = getResourceCostsForTUV(data, false);
allPlayersCurrent.remove(null);
for (final UnitType ut : data.getUnitTypeList().getAllUnitTypes()) {
final List<ResourceCollection> costs = new ArrayList<>();
for (final Map<UnitType, ResourceCollection> entry : allPlayersCurrent.values()) {
if (entry.get(ut) != null) {
costs.add(entry.get(ut));
}
}
if (costs.isEmpty()) {
final ResourceCollection backup = backupAveraged.get(ut);
if (backup != null) {
costs.add(backup);
} else {
costs.add(defaultResources);
}
}
final ResourceCollection avgCost =
new ResourceCollection(costs.toArray(new ResourceCollection[costs.size()]), data);
if (costs.size() > 1) {
avgCost.discount((double) 1 / (double) costs.size());
}
average.put(ut, avgCost);
}
return average;
}
/**
* Return the total unit value
*
* @param units
* A collection of units
* @param costs
* An integer map of unit types to costs.
* @return the total unit value.
*/
public static int getTUV(final Collection<Unit> units, final IntegerMap<UnitType> costs) {
int tuv = 0;
for (final Unit u : units) {
final int unitValue = costs.getInt(u.getType());
tuv += unitValue;
}
return tuv;
}
/**
* Return the total unit value for a certain player and his allies
*
* @param units
* A collection of units
* @param player
* The player to calculate the TUV for.
* @param costs
* An integer map of unit types to costs
* @return the total unit value.
*/
public static int getTUV(final Collection<Unit> units, final PlayerID player, final IntegerMap<UnitType> costs,
final GameData data) {
final Collection<Unit> playerUnits = Match.getMatches(units, Matches.alliedUnit(player, data));
return getTUV(playerUnits, costs);
}
/**
* Checks if the given collections target are all of one category as defined
* by UnitSeperator.categorize and they are not two hit units.
*
* @param targets
* a collection of target units
* @param dependents
* map of depend units for target units
*/
private static boolean allTargetsOneTypeOneHitPoint(final Collection<Unit> targets,
final Map<Unit, Collection<Unit>> dependents) {
final Set<UnitCategory> categorized = UnitSeperator.categorize(targets, dependents, false, false);
if (categorized.size() == 1) {
final UnitCategory unitCategory = categorized.iterator().next();
if (unitCategory.getHitPoints() - unitCategory.getDamaged() <= 1) {
return true;
}
}
return false;
}
public static int getRolls(final Collection<Unit> units, final PlayerID id,
final boolean defend, final boolean bombing, final Set<List<UnitSupportAttachment>> supportRulesFriendly,
final IntegerMap<UnitSupportAttachment> supportLeftFriendlyCopy,
final Set<List<UnitSupportAttachment>> supportRulesEnemy,
final IntegerMap<UnitSupportAttachment> supportLeftEnemyCopy,
final Collection<TerritoryEffect> territoryEffects) {
int count = 0;
for (final Unit unit : units) {
final int unitRoll = getRolls(unit, id, defend, bombing, supportRulesFriendly, supportLeftFriendlyCopy,
supportRulesEnemy, supportLeftEnemyCopy, territoryEffects);
count += unitRoll;
}
return count;
}
public static int getRolls(final Collection<Unit> units, final PlayerID id, final boolean defend,
final boolean bombing, final Collection<TerritoryEffect> territoryEffects) {
return getRolls(units, id, defend, bombing, new HashSet<>(),
new IntegerMap<>(), new HashSet<>(),
new IntegerMap<>(), territoryEffects);
}
public static int getRolls(final Unit unit, final PlayerID id, final boolean defend,
final boolean bombing, final Set<List<UnitSupportAttachment>> supportRulesFriendly,
final IntegerMap<UnitSupportAttachment> supportLeftFriendlyCopy,
final Set<List<UnitSupportAttachment>> supportRulesEnemy,
final IntegerMap<UnitSupportAttachment> supportLeftEnemyCopy,
final Collection<TerritoryEffect> territoryEffects) {
final UnitAttachment unitAttachment = UnitAttachment.get(unit.getType());
int rolls;
if (defend) {
rolls = unitAttachment.getDefenseRolls(id);
} else {
rolls = unitAttachment.getAttackRolls(id);
}
final Map<UnitSupportAttachment, LinkedIntegerMap<Unit>> dummyEmptyMap =
new HashMap<>();
rolls += DiceRoll.getSupport(unit, supportRulesFriendly, supportLeftFriendlyCopy, dummyEmptyMap, null, false, true);
rolls += DiceRoll.getSupport(unit, supportRulesEnemy, supportLeftEnemyCopy, dummyEmptyMap, null, false, true);
rolls = Math.max(0, rolls);
// if we are strategic bombing, we do not care what the strength of the unit is...
if (bombing) {
return rolls;
}
int strength;
if (defend) {
strength = unitAttachment.getDefense(unit.getOwner());
} else {
strength = unitAttachment.getAttack(unit.getOwner());
}
// TODO: we should add in isMarine bonus too...
strength +=
DiceRoll.getSupport(unit, supportRulesFriendly, supportLeftFriendlyCopy, dummyEmptyMap, null, true, false);
strength += DiceRoll.getSupport(unit, supportRulesEnemy, supportLeftEnemyCopy, dummyEmptyMap, null, true, false);
strength += TerritoryEffectHelper.getTerritoryCombatBonus(unit.getType(), territoryEffects, defend);
if (strength <= 0) {
rolls = 0;
}
return rolls;
}
public static int getRolls(final Unit unit, final PlayerID id, final boolean defend,
final boolean bombing, final Collection<TerritoryEffect> territoryEffects) {
return getRolls(unit, id, defend, bombing, new HashSet<>(),
new IntegerMap<>(), new HashSet<>(),
new IntegerMap<>(), territoryEffects);
}
/**
* @return Can transports be used as cannon fodder.
*/
private static boolean isTransportCasualtiesRestricted(final GameData data) {
return games.strategy.triplea.Properties.getTransportCasualtiesRestricted(data);
}
/**
* @return Random AA Casualties - casualties randomly assigned.
*/
private static boolean isRandomAACasualties(final GameData data) {
return games.strategy.triplea.Properties.getRandomAACasualties(data);
}
/**
* @return Roll AA Individually - roll against each aircraft.
*/
private static boolean isRollAAIndividually(final GameData data) {
return games.strategy.triplea.Properties.getRollAAIndividually(data);
}
/**
* @return Choose AA - attacker selects casualties.
*/
private static boolean isChooseAA(final GameData data) {
return games.strategy.triplea.Properties.getChoose_AA_Casualties(data);
}
/**
* @return Can the attacker retreat non-amphibious units.
*/
private static boolean isPartialAmphibiousRetreat(final GameData data) {
return games.strategy.triplea.Properties.getPartialAmphibiousRetreat(data);
}
// nothing but static
private BattleCalculator() {}
/**
* This returns the exact Power that a unit has according to what DiceRoll.rollDiceLowLuck() would give it.
* As such, it needs to exactly match DiceRoll, otherwise this method will become useless.
* It does NOT take into account SUPPORT.
* It DOES take into account ROLLS.
* It needs to be updated to take into account isMarine.
*/
public static int getUnitPowerForSorting(final Unit current, final boolean defending, final GameData data,
final Collection<TerritoryEffect> territoryEffects) {
final boolean lhtrBombers = games.strategy.triplea.Properties.getLHTR_Heavy_Bombers(data);
final UnitAttachment ua = UnitAttachment.get(current.getType());
int rolls;
if (defending) {
rolls = ua.getDefenseRolls(current.getOwner());
} else {
rolls = ua.getAttackRolls(current.getOwner());
}
// int strength = 0;
int strengthWithoutSupport = 0;
// Find the strength the unit has without support
// lhtr heavy bombers take best of n dice for both attack and defense
if (rolls > 1 && (lhtrBombers || ua.getChooseBestRoll())) {
if (defending) {
strengthWithoutSupport = ua.getDefense(current.getOwner());
} else {
strengthWithoutSupport = ua.getAttack(current.getOwner());
}
strengthWithoutSupport +=
TerritoryEffectHelper.getTerritoryCombatBonus(current.getType(), territoryEffects, defending);
// just add one like LL if we are LHTR bombers
strengthWithoutSupport = Math.min(Math.max(strengthWithoutSupport + 1, 0), data.getDiceSides());
} else {
for (int i = 0; i < rolls; i++) {
int tempStrength;
if (defending) {
tempStrength = ua.getDefense(current.getOwner());
} else {
tempStrength = ua.getAttack(current.getOwner());
}
strengthWithoutSupport +=
TerritoryEffectHelper.getTerritoryCombatBonus(current.getType(), territoryEffects, defending);
strengthWithoutSupport += Math.min(Math.max(tempStrength, 0), data.getDiceSides());
}
}
return strengthWithoutSupport;
}
}