package games.strategy.triplea.delegate; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import games.strategy.engine.data.GameData; import games.strategy.engine.data.GamePlayer; 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.triplea.Constants; import games.strategy.triplea.Properties; import games.strategy.triplea.attachments.RulesAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.attachments.UnitSupportAttachment; import games.strategy.triplea.delegate.Die.DieType; import games.strategy.triplea.delegate.battle.AirBattle; import games.strategy.triplea.delegate.battle.IBattle; import games.strategy.triplea.delegate.power.calculator.AvailableSupportCalculator; import games.strategy.triplea.delegate.power.calculator.SupportBonusCalculator; import games.strategy.triplea.delegate.power.calculator.SupportCalculationResult; import games.strategy.triplea.formatter.MyFormatter; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.Nonnull; import lombok.Builder; import lombok.Value; import org.triplea.java.collections.CollectionUtils; import org.triplea.java.collections.IntegerMap; import org.triplea.util.Triple; import org.triplea.util.Tuple; /** * Used to store information about a dice roll. * * <p># of rolls at 5, at 4, etc. * * <p>Externalizable so we can efficiently write out our dice as ints rather than as full objects. */ public class DiceRoll implements Externalizable { private static final long serialVersionUID = -1167204061937566271L; private List<Die> rolls; // this does not need to match the Die with isHit true since for low luck we get many hits with // few dice private int hits; private double expectedHits; /** * Initializes a new instance of the DiceRoll class. * * @param dice the dice, 0 based * @param hits the number of hits * @param rollAt what we roll at, [0,Constants.MAX_DICE] * @param hitOnlyIfEquals Do we get a hit only if we are equals, or do we hit when we are equal or * less than for example a 5 is a hit when rolling at 6 for equal and less than, but is not * for equals. */ public DiceRoll( final int[] dice, final int hits, final int rollAt, final boolean hitOnlyIfEquals) { this.hits = hits; expectedHits = 0; rolls = new ArrayList<>(dice.length); for (final int element : dice) { final boolean hit; if (hitOnlyIfEquals) { hit = (rollAt == element); } else { hit = element <= rollAt; } rolls.add(new Die(element, rollAt, hit ? DieType.HIT : DieType.MISS)); } } // only for externalizable public DiceRoll() {} private DiceRoll(final List<Die> dice, final int hits, final double expectedHits) { rolls = new ArrayList<>(dice); this.hits = hits; this.expectedHits = expectedHits; } public static Tuple<Integer, Integer> getMaxAaAttackAndDiceSides( final Collection<Unit> aaUnits, final GameData data, final boolean defending) { return getMaxAaAttackAndDiceSides(aaUnits, data, defending, new HashMap<>()); } /** * Returns a Tuple, the first is the max attack, the second is the max dice sides for the AA unit * with that attack value. */ public static Tuple<Integer, Integer> getMaxAaAttackAndDiceSides( final Collection<Unit> aaUnits, final GameData data, final boolean defending, final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap) { int highestAttack = 0; final int diceSize = data.getDiceSides(); int chosenDiceSize = diceSize; for (final Unit u : aaUnits) { final UnitAttachment ua = UnitAttachment.get(u.getType()); int uaDiceSides = defending ? ua.getAttackAaMaxDieSides() : ua.getOffensiveAttackAaMaxDieSides(); if (uaDiceSides < 1) { uaDiceSides = diceSize; } int attack = defending ? ua.getAttackAa(u.getOwner()) : ua.getOffensiveAttackAa(u.getOwner()); if (unitPowerAndRollsMap.containsKey(u)) { attack = unitPowerAndRollsMap.get(u).getTotalPower(); } if (attack > uaDiceSides) { attack = uaDiceSides; } if ((((float) attack) / ((float) uaDiceSides)) > (((float) highestAttack) / ((float) chosenDiceSize))) { highestAttack = attack; chosenDiceSize = uaDiceSides; } } return Tuple.of(highestAttack, chosenDiceSize); } /** * Finds total number of AA attacks that a group of units can roll against targets taking into * account infinite roll and overstack AA. */ public static int getTotalAaAttacks( final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap, final Collection<Unit> validTargets) { if (unitPowerAndRollsMap.isEmpty() || validTargets.isEmpty()) { return 0; } int totalAAattacksNormal = 0; int totalAAattacksSurplus = 0; for (final Entry<Unit, TotalPowerAndTotalRolls> entry : unitPowerAndRollsMap.entrySet()) { if (entry.getValue().getTotalPower() == 0 || entry.getValue().getTotalRolls() == 0) { continue; } final UnitAttachment ua = UnitAttachment.get(entry.getKey().getType()); if (entry.getValue().getTotalRolls() == -1) { totalAAattacksNormal = validTargets.size(); } else { if (ua.getMayOverStackAa()) { totalAAattacksSurplus += entry.getValue().getTotalRolls(); } else { totalAAattacksNormal += entry.getValue().getTotalRolls(); } } } totalAAattacksNormal = Math.min(totalAAattacksNormal, validTargets.size()); return totalAAattacksNormal + totalAAattacksSurplus; } /** Used only for rolling SBR or fly over AA as they don't currently take into account support. */ public static DiceRoll rollSbrOrFlyOverAa( final Collection<Unit> validTargets, final Collection<Unit> aaUnits, final IDelegateBridge bridge, final Territory location, final boolean defending) { return rollAa( validTargets, aaUnits, new ArrayList<>(), new ArrayList<>(), bridge, location, defending); } /** * Used to roll AA for battles, SBR, and fly over. * * @param validTargets - potential AA targets * @param aaUnits - AA units that could potentially be rolling * @param allEnemyUnitsAliveOrWaitingToDie - all enemy units to check for support * @param allFriendlyUnitsAliveOrWaitingToDie - all allied units to check for support * @param bridge - delegate bridge * @param location - battle territory * @param defending - whether AA units are defending or attacking * @return DiceRoll result which includes total hits and dice that were rolled */ public static DiceRoll rollAa( final Collection<Unit> validTargets, final Collection<Unit> aaUnits, final Collection<Unit> allEnemyUnitsAliveOrWaitingToDie, final Collection<Unit> allFriendlyUnitsAliveOrWaitingToDie, final IDelegateBridge bridge, final Territory location, final boolean defending) { final GameData data = bridge.getData(); final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap = getAaUnitPowerAndRollsForNormalBattles( aaUnits, allEnemyUnitsAliveOrWaitingToDie, allFriendlyUnitsAliveOrWaitingToDie, defending, data); // Check that there are valid AA and targets to roll for final int totalAaAttacks = getTotalAaAttacks(unitPowerAndRollsMap, validTargets); if (totalAaAttacks <= 0) { return new DiceRoll(List.of(), 0, 0); } // Determine dice sides (doesn't handle the possibility of different dice sides within the same // typeAA) final int diceSides = getMaxAaAttackAndDiceSides(aaUnits, data, defending, unitPowerAndRollsMap).getSecond(); // Roll AA dice for LL or regular int hits = 0; final List<Die> sortedDice = new ArrayList<>(); final String typeAa = UnitAttachment.get(aaUnits.iterator().next().getType()).getTypeAa(); final int totalPower = getTotalAaPowerThenHitsAndFillSortedDiceThenIfAllUseSameAttack( null, null, defending, unitPowerAndRollsMap, validTargets, data, false) .getFirst(); final GamePlayer player = aaUnits.iterator().next().getOwner(); final String annotation = "Roll " + typeAa + " in " + location.getName(); if (Properties.getLowLuck(data) || Properties.getLowLuckAaOnly(data)) { hits += getAaLowLuckHits(bridge, sortedDice, totalPower, diceSides, player, annotation); } else { final int[] dice = bridge.getRandom(diceSides, totalAaAttacks, player, DiceType.COMBAT, annotation); hits += getTotalAaPowerThenHitsAndFillSortedDiceThenIfAllUseSameAttack( dice, sortedDice, defending, unitPowerAndRollsMap, validTargets, data, true) .getSecond(); } // Add dice results to history final double expectedHits = ((double) totalPower) / diceSides; final DiceRoll roll = new DiceRoll(sortedDice, hits, expectedHits); final String historyMessage = player.getName() + " roll " + typeAa + " dice in " + location + " : " + MyFormatter.asDice(roll); bridge.getHistoryWriter().addChildToEvent(historyMessage, roll); return roll; } /** * Single method for both LL and Dice, because if we have 2 methods then there is a chance they * will go out of sync. <br> * <br> * The following is complex, but should do the following: * * <ol> * <li>Any aa that are NOT infinite attacks, and NOT overstack, will fire first individually * ((because their power/dicesides might be different [example: radar tech on a german aa * gun, in the same territory as an italian aagun without radar, neither is infinite]) * <li>All aa that have "infinite attacks" will have the one with the highest power/dicesides of * them all, fire at whatever aa units have not yet been fired at. HOWEVER, if the * non-infinite attackers are less powerful than the infinite attacker, then the * non-infinite will not fire, and the infinite one will do all the attacks for both groups. * <li>The total number of shots from these first 2 groups cannot exceed the number of air units * being shot at * <li>Any aa that can overstack will fire after, individually (aa guns that is both infinite, * and overstacks, ignores the overstack part because that totally doesn't make any sense) * </ol> * * @param dice Rolled Dice numbers from bridge. Can be null if we do not want to return hits or * fill the sortedDice * @param sortedDice List of dice we are recording. Can be null if we do not want to return hits * or fill the sortedDice * @return An object containing 3 things: first is the total power of the aaUnits who will be * rolling, second is number of hits, third is true/false are all rolls using the same hitAt * (example: if all the rolls are at 1, we would return true, but if one roll is at 1 and * another roll is at 2, then we return false) */ public static Triple<Integer, Integer, Boolean> getTotalAaPowerThenHitsAndFillSortedDiceThenIfAllUseSameAttack( final int[] dice, final List<Die> sortedDice, final boolean defending, final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap, final Collection<Unit> validTargets, final GameData data, final boolean fillInSortedDiceAndRecordHits) { // Check that there are valid AA and targets to roll for if (unitPowerAndRollsMap.isEmpty()) { return Triple.of(0, 0, false); } // Make sure the higher powers fire final List<Unit> aaToRoll = new ArrayList<>(unitPowerAndRollsMap.keySet()); sortAaHighToLow(aaToRoll, data, defending, unitPowerAndRollsMap); // Setup all 3 groups of aa guns final List<Unit> normalNonInfiniteAa = new ArrayList<>(aaToRoll); final List<Unit> infiniteAa = CollectionUtils.getMatches(aaToRoll, Matches.unitMaxAaAttacksIsInfinite()); final List<Unit> overstackAa = CollectionUtils.getMatches(aaToRoll, Matches.unitMayOverStackAa()); overstackAa.removeAll(infiniteAa); normalNonInfiniteAa.removeAll(infiniteAa); normalNonInfiniteAa.removeAll(overstackAa); // Determine maximum total attacks final int totalAAattacksTotal = getTotalAaAttacks(unitPowerAndRollsMap, validTargets); // Determine individual totals final Map<Unit, TotalPowerAndTotalRolls> normalNonInfiniteAaMap = new HashMap<>(unitPowerAndRollsMap); normalNonInfiniteAaMap.keySet().retainAll(normalNonInfiniteAa); final int normalNonInfiniteAAtotalAAattacks = getTotalAaAttacks(normalNonInfiniteAaMap, validTargets); final Map<Unit, TotalPowerAndTotalRolls> infiniteAaMap = new HashMap<>(unitPowerAndRollsMap); infiniteAaMap.keySet().retainAll(infiniteAa); final int infiniteAAtotalAAattacks = Math.min( (validTargets.size() - normalNonInfiniteAAtotalAAattacks), getTotalAaAttacks(infiniteAaMap, validTargets)); final Map<Unit, TotalPowerAndTotalRolls> overstackAaMap = new HashMap<>(unitPowerAndRollsMap); overstackAaMap.keySet().retainAll(overstackAa); final int overstackAAtotalAAattacks = getTotalAaAttacks(overstackAaMap, validTargets); if (totalAAattacksTotal != (normalNonInfiniteAAtotalAAattacks + infiniteAAtotalAAattacks + overstackAAtotalAAattacks)) { throw new IllegalStateException( "Total attacks should be: " + totalAAattacksTotal + " but instead is: " + (normalNonInfiniteAAtotalAAattacks + infiniteAAtotalAAattacks + overstackAAtotalAAattacks)); } // Determine highest attack for infinite group final int hitAtForInfinite = getMaxAaAttackAndDiceSides(infiniteAa, data, defending, unitPowerAndRollsMap).getFirst(); // If LL, the power and total attacks, else if dice we will be filling the sorted dice final boolean recordSortedDice = fillInSortedDiceAndRecordHits && dice != null && dice.length > 0 && sortedDice != null; int totalPower = 0; int hits = 0; int i = 0; final Set<Integer> rolledAt = new HashSet<>(); // Non-infinite, non-overstack aa int runningMaximum = normalNonInfiniteAAtotalAAattacks; final Iterator<Unit> normalAAiter = normalNonInfiniteAa.iterator(); while (i < runningMaximum && normalAAiter.hasNext()) { final Unit aaGun = normalAAiter.next(); int numAttacks = unitPowerAndRollsMap.get(aaGun).getTotalRolls(); final int hitAt = unitPowerAndRollsMap.get(aaGun).getTotalPower(); if (hitAt < hitAtForInfinite) { continue; } while (i < runningMaximum && numAttacks > 0) { if (recordSortedDice) { // Dice are zero based final boolean hit = dice[i] < hitAt; sortedDice.add(new Die(dice[i], hitAt, hit ? DieType.HIT : DieType.MISS)); if (hit) { hits++; } } i++; numAttacks--; totalPower += hitAt; rolledAt.add(hitAt); } } // Infinite aa runningMaximum += infiniteAAtotalAAattacks; while (i < runningMaximum) { // Use the highest attack of this group, since each is infinite. (this is the default behavior // in revised) if (recordSortedDice) { // Dice are zero based final boolean hit = dice[i] < hitAtForInfinite; sortedDice.add(new Die(dice[i], hitAtForInfinite, hit ? DieType.HIT : DieType.MISS)); if (hit) { hits++; } } i++; totalPower += hitAtForInfinite; rolledAt.add(hitAtForInfinite); } // Overstack aa runningMaximum += overstackAAtotalAAattacks; final Iterator<Unit> overstackAAiter = overstackAa.iterator(); while (i < runningMaximum && overstackAAiter.hasNext()) { final Unit aaGun = overstackAAiter.next(); int numAttacks = unitPowerAndRollsMap.get(aaGun).getTotalRolls(); final int hitAt = unitPowerAndRollsMap.get(aaGun).getTotalPower(); while (i < runningMaximum && numAttacks > 0) { if (recordSortedDice) { // Dice are zero based final boolean hit = dice[i] < hitAt; sortedDice.add(new Die(dice[i], hitAt, hit ? DieType.HIT : DieType.MISS)); if (hit) { hits++; } } i++; numAttacks--; totalPower += hitAt; rolledAt.add(hitAt); } } return Triple.of(totalPower, hits, (rolledAt.size() == 1)); } private static void sortAaHighToLow( final List<Unit> units, final GameData data, final boolean defending) { sortAaHighToLow(units, data, defending, new HashMap<>()); } @VisibleForTesting static void sortAaHighToLow( final List<Unit> units, final GameData data, final boolean defending, final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap) { units.sort( Comparator.comparing( unit -> getMaxAaAttackAndDiceSides(Set.of(unit), data, defending, unitPowerAndRollsMap), Comparator.<Tuple<Integer, Integer>, Boolean>comparing(tuple -> tuple.getFirst() == 0) .thenComparingDouble(tuple -> -tuple.getFirst() / (float) tuple.getSecond()))); } /** * Returns the AA power (strength) and rolls for each of the AA units in the specified list. The * power is either attackAA or offensiveAttackAA plus any support. The rolls is maxAAattacks plus * any support if it isn't infinite (-1). * * @param aaUnits should be sorted from weakest to strongest, before the method is called, for the * actual battle. */ public static Map<Unit, TotalPowerAndTotalRolls> getAaUnitPowerAndRollsForNormalBattles( final Collection<Unit> aaUnits, final Collection<Unit> allEnemyUnitsAliveOrWaitingToDie, final Collection<Unit> allFriendlyUnitsAliveOrWaitingToDie, final boolean defending, final GameData data) { final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRolls = new HashMap<>(); if (aaUnits == null || aaUnits.isEmpty()) { return unitPowerAndRolls; } // Get all friendly supports final SupportCalculationResult friendlySupports = AvailableSupportCalculator.getSortedAaSupport( allFriendlyUnitsAliveOrWaitingToDie, // data, defending, true); final Set<List<UnitSupportAttachment>> supportRulesFriendly = friendlySupports.getSupportRules(); final IntegerMap<UnitSupportAttachment> supportLeftFriendly = friendlySupports.getSupportLeft(); final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftFriendly = friendlySupports.getSupportUnits(); // Get all enemy supports final SupportCalculationResult enemySupports = AvailableSupportCalculator.getSortedAaSupport( allEnemyUnitsAliveOrWaitingToDie, // data, !defending, false); final Set<List<UnitSupportAttachment>> supportRulesEnemy = enemySupports.getSupportRules(); final IntegerMap<UnitSupportAttachment> supportLeftEnemy = enemySupports.getSupportLeft(); final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftEnemy = enemySupports.getSupportUnits(); // Copy for rolls final IntegerMap<UnitSupportAttachment> supportLeftFriendlyRolls = new IntegerMap<>(supportLeftFriendly); final IntegerMap<UnitSupportAttachment> supportLeftEnemyRolls = new IntegerMap<>(supportLeftEnemy); final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftFriendlyRolls = new HashMap<>(); for (final UnitSupportAttachment usa : supportUnitsLeftFriendly.keySet()) { supportUnitsLeftFriendlyRolls.put(usa, new IntegerMap<>(supportUnitsLeftFriendly.get(usa))); } final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftEnemyRolls = new HashMap<>(); for (final UnitSupportAttachment usa : supportUnitsLeftEnemy.keySet()) { supportUnitsLeftEnemyRolls.put(usa, new IntegerMap<>(supportUnitsLeftEnemy.get(usa))); } // Sort units strongest to weakest to give support to the best units first final List<Unit> sortedAaUnits = new ArrayList<>(aaUnits); sortAaHighToLow(sortedAaUnits, data, defending); for (final Unit unit : sortedAaUnits) { // Find unit's AA strength final UnitAttachment ua = UnitAttachment.get(unit.getType()); int strength = defending ? ua.getAttackAa(unit.getOwner()) : ua.getOffensiveAttackAa(unit.getOwner()); strength += SupportBonusCalculator.getSupport( unit, supportRulesFriendly, supportLeftFriendly, supportUnitsLeftFriendly, new HashMap<>(), UnitSupportAttachment::getAaStrength); strength += SupportBonusCalculator.getSupport( unit, supportRulesEnemy, supportLeftEnemy, supportUnitsLeftEnemy, new HashMap<>(), UnitSupportAttachment::getAaStrength); strength = Math.min(Math.max(strength, 0), data.getDiceSides()); // Find unit's AA rolls int rolls; if (strength == 0) { rolls = 0; } else { rolls = ua.getMaxAaAttacks(); if (rolls > -1) { rolls += SupportBonusCalculator.getSupport( unit, supportRulesFriendly, supportLeftFriendlyRolls, supportUnitsLeftFriendlyRolls, new HashMap<>(), UnitSupportAttachment::getAaRoll); rolls += SupportBonusCalculator.getSupport( unit, supportRulesEnemy, supportLeftEnemyRolls, supportUnitsLeftEnemyRolls, new HashMap<>(), UnitSupportAttachment::getAaRoll); rolls = Math.max(0, rolls); } if (rolls == 0) { strength = 0; } } unitPowerAndRolls.put( unit, // TotalPowerAndTotalRolls.builder() // .totalPower(strength) .totalRolls(rolls) .build()); } return unitPowerAndRolls; } private static int getAaLowLuckHits( final IDelegateBridge bridge, final List<Die> sortedDice, final int totalPower, final int chosenDiceSize, final GamePlayer playerRolling, final String annotation) { int hits = totalPower / chosenDiceSize; final int hitsFractional = totalPower % chosenDiceSize; if (hitsFractional > 0) { final int[] dice = bridge.getRandom(chosenDiceSize, 1, playerRolling, DiceType.COMBAT, annotation); final boolean hit = hitsFractional > dice[0]; if (hit) { hits++; } final Die die = new Die(dice[0], hitsFractional, hit ? DieType.HIT : DieType.MISS); sortedDice.add(die); } return hits; } @VisibleForTesting static DiceRoll rollDice( final List<Unit> units, final boolean defending, final GamePlayer player, final IDelegateBridge bridge, final IBattle battle, final Collection<TerritoryEffect> territoryEffects) { return rollDice( units, defending, player, bridge, battle, "", territoryEffects, List.of(), units); } /** * Used to roll dice for attackers and defenders in battles. * * @param units - units that could potentially be rolling * @param defending - whether units are defending or attacking * @param player - that will be rolling the dice * @param bridge - delegate bridge * @param battle - which the dice are being rolled for * @param annotation - description of the battle being rolled for * @param territoryEffects - list of territory effects for the battle * @param allEnemyUnitsAliveOrWaitingToDie - all enemy units to check for support * @param allFriendlyUnitsAliveOrWaitingToDie - all allied units to check for support * @return DiceRoll result which includes total hits and dice that were rolled */ public static DiceRoll rollDice( final List<Unit> units, final boolean defending, final GamePlayer player, final IDelegateBridge bridge, final IBattle battle, final String annotation, final Collection<TerritoryEffect> territoryEffects, final Collection<Unit> allEnemyUnitsAliveOrWaitingToDie, final Collection<Unit> allFriendlyUnitsAliveOrWaitingToDie) { if (Properties.getLowLuck(bridge.getData())) { return rollDiceLowLuck( units, defending, player, bridge, battle, annotation, territoryEffects, allEnemyUnitsAliveOrWaitingToDie, allFriendlyUnitsAliveOrWaitingToDie); } return rollDiceNormal( units, defending, player, bridge, battle, annotation, territoryEffects, allEnemyUnitsAliveOrWaitingToDie, allFriendlyUnitsAliveOrWaitingToDie); } /** * Roll n-sided dice. * * @param annotation 0 based, add 1 to get actual die roll */ public static DiceRoll rollNDice( final IDelegateBridge bridge, final int rollCount, final int sides, final GamePlayer playerRolling, final DiceType diceType, final String annotation) { if (rollCount == 0) { return new DiceRoll(new ArrayList<>(), 0, 0); } final int[] random = bridge.getRandom(sides, rollCount, playerRolling, diceType, annotation); final List<Die> dice = new ArrayList<>(); for (int i = 0; i < rollCount; i++) { dice.add(new Die(random[i], 1, DieType.IGNORED)); } return new DiceRoll(dice, rollCount, rollCount); } /** * Returns the power (strength) and rolls for each of the specified units. * * @param unitsGettingPowerFor should be sorted from weakest to strongest, before the method is * called, for the actual battle. */ public static Map<Unit, TotalPowerAndTotalRolls> getUnitPowerAndRollsForNormalBattles( final Collection<Unit> unitsGettingPowerFor, final Collection<Unit> allEnemyUnitsAliveOrWaitingToDie, final Collection<Unit> allFriendlyUnitsAliveOrWaitingToDie, final boolean defending, final GameData data, final Territory location, final Collection<TerritoryEffect> territoryEffects, final boolean isAmphibiousBattle, final Collection<Unit> amphibiousLandAttackers) { return getUnitPowerAndRollsForNormalBattles( unitsGettingPowerFor, allEnemyUnitsAliveOrWaitingToDie, allFriendlyUnitsAliveOrWaitingToDie, defending, data, location, territoryEffects, isAmphibiousBattle, amphibiousLandAttackers, new HashMap<>(), new HashMap<>()); } /** * Returns the power (strength) and rolls for each of the specified units. * * @param unitsGettingPowerFor should be sorted from weakest to strongest, before the method is * called, for the actual battle. */ public static Map<Unit, TotalPowerAndTotalRolls> getUnitPowerAndRollsForNormalBattles( final Collection<Unit> unitsGettingPowerFor, final Collection<Unit> allEnemyUnitsAliveOrWaitingToDie, final Collection<Unit> allFriendlyUnitsAliveOrWaitingToDie, final boolean defending, final GameData data, final Territory location, final Collection<TerritoryEffect> territoryEffects, final boolean isAmphibiousBattle, final Collection<Unit> amphibiousLandAttackers, final Map<Unit, IntegerMap<Unit>> unitSupportPowerMap, final Map<Unit, IntegerMap<Unit>> unitSupportRollsMap) { final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRolls = new HashMap<>(); if (unitsGettingPowerFor == null || unitsGettingPowerFor.isEmpty()) { return unitPowerAndRolls; } // Get all friendly supports final SupportCalculationResult friendlySupport = AvailableSupportCalculator.getSortedSupport( allFriendlyUnitsAliveOrWaitingToDie, data.getUnitTypeList().getSupportRules(), defending, true); final Set<List<UnitSupportAttachment>> supportRulesFriendly = friendlySupport.getSupportRules(); final IntegerMap<UnitSupportAttachment> supportLeftFriendly = friendlySupport.getSupportLeft(); final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftFriendly = friendlySupport.getSupportUnits(); // Get all enemy supports final SupportCalculationResult enemySupport = AvailableSupportCalculator.getSortedSupport( allEnemyUnitsAliveOrWaitingToDie, data.getUnitTypeList().getSupportRules(), !defending, false); final Set<List<UnitSupportAttachment>> supportRulesEnemy = enemySupport.getSupportRules(); final IntegerMap<UnitSupportAttachment> supportLeftEnemy = enemySupport.getSupportLeft(); final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftEnemy = enemySupport.getSupportUnits(); // Copy for rolls final IntegerMap<UnitSupportAttachment> supportLeftFriendlyRolls = new IntegerMap<>(supportLeftFriendly); final IntegerMap<UnitSupportAttachment> supportLeftEnemyRolls = new IntegerMap<>(supportLeftEnemy); final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftFriendlyRolls = new HashMap<>(); for (final UnitSupportAttachment usa : supportUnitsLeftFriendly.keySet()) { supportUnitsLeftFriendlyRolls.put(usa, new IntegerMap<>(supportUnitsLeftFriendly.get(usa))); } final Map<UnitSupportAttachment, IntegerMap<Unit>> supportUnitsLeftEnemyRolls = new HashMap<>(); for (final UnitSupportAttachment usa : supportUnitsLeftEnemy.keySet()) { supportUnitsLeftEnemyRolls.put(usa, new IntegerMap<>(supportUnitsLeftEnemy.get(usa))); } for (final Unit unit : unitsGettingPowerFor) { // Find unit's strength int strength; final UnitAttachment ua = UnitAttachment.get(unit.getType()); if (defending) { strength = ua.getDefense(unit.getOwner()); if (isFirstTurnLimitedRoll(unit.getOwner(), data)) { strength = Math.min(1, strength); } else { strength += SupportBonusCalculator.getSupport( unit, supportRulesFriendly, supportLeftFriendly, supportUnitsLeftFriendly, unitSupportPowerMap, UnitSupportAttachment::getStrength); } strength += SupportBonusCalculator.getSupport( unit, supportRulesEnemy, supportLeftEnemy, supportUnitsLeftEnemy, unitSupportPowerMap, UnitSupportAttachment::getStrength); } else { strength = ua.getAttack(unit.getOwner()); if (ua.getIsMarine() != 0 && isAmphibiousBattle && amphibiousLandAttackers.contains(unit)) { strength += ua.getIsMarine(); } if (ua.getIsSea() && Matches.territoryIsLand().test(location)) { // Change the strength to be bombard, not attack/defense, because this is a bombarding // naval unit strength = ua.getBombard(); } strength += SupportBonusCalculator.getSupport( unit, supportRulesFriendly, supportLeftFriendly, supportUnitsLeftFriendly, unitSupportPowerMap, UnitSupportAttachment::getStrength); strength += SupportBonusCalculator.getSupport( unit, supportRulesEnemy, supportLeftEnemy, supportUnitsLeftEnemy, unitSupportPowerMap, UnitSupportAttachment::getStrength); } strength += TerritoryEffectHelper.getTerritoryCombatBonus( unit.getType(), territoryEffects, defending); strength = Math.min(Math.max(strength, 0), data.getDiceSides()); // Find unit's rolls int rolls; if (strength == 0) { rolls = 0; } else { if (defending) { rolls = ua.getDefenseRolls(unit.getOwner()); } else { rolls = ua.getAttackRolls(unit.getOwner()); } rolls += SupportBonusCalculator.getSupport( unit, supportRulesFriendly, supportLeftFriendlyRolls, supportUnitsLeftFriendlyRolls, unitSupportRollsMap, UnitSupportAttachment::getRoll); rolls += SupportBonusCalculator.getSupport( unit, supportRulesEnemy, supportLeftEnemyRolls, supportUnitsLeftEnemyRolls, unitSupportRollsMap, UnitSupportAttachment::getRoll); rolls = Math.max(0, rolls); if (rolls == 0) { strength = 0; } } unitPowerAndRolls.put( unit, TotalPowerAndTotalRolls.builder().totalPower(strength).totalRolls(rolls).build()); } return unitPowerAndRolls; } public static int getTotalPower( final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap, final GameData data) { return getTotalPowerAndRolls(unitPowerAndRollsMap, data).getTotalPower(); } @Value @Builder public static class TotalPowerAndTotalRolls { @Nonnull Integer totalPower; @Nonnull Integer totalRolls; /** Returns the product of power and dice rolls. */ public int getEffectivePower() { return totalPower * totalRolls; } public TotalPowerAndTotalRolls subtractPower(final int powerToSubtract) { return TotalPowerAndTotalRolls.builder() .totalPower(totalPower - powerToSubtract) .totalRolls(totalRolls) .build(); } public TotalPowerAndTotalRolls subtractRolls(final int rollsToSubtract) { return TotalPowerAndTotalRolls.builder() .totalPower(totalPower) .totalRolls(totalRolls - rollsToSubtract) .build(); } } public static TotalPowerAndTotalRolls getTotalPowerAndRolls( final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap, final GameData data) { final int diceSides = data.getDiceSides(); final boolean lhtrBombers = Properties.getLhtrHeavyBombers(data); // Bonus is normally 1 for most games final int extraRollBonus = Math.max(1, data.getDiceSides() / 6); int totalPower = 0; int totalRolls = 0; for (final Entry<Unit, TotalPowerAndTotalRolls> entry : unitPowerAndRollsMap.entrySet()) { int unitStrength = Math.min(Math.max(0, entry.getValue().getTotalPower()), diceSides); final int unitRolls = entry.getValue().getTotalRolls(); if (unitStrength <= 0 || unitRolls <= 0) { continue; } if (unitRolls == 1) { totalPower += unitStrength; totalRolls += unitRolls; } else { final UnitAttachment ua = UnitAttachment.get(entry.getKey().getType()); if (lhtrBombers || ua.getChooseBestRoll()) { // LHTR means pick the best dice roll, which doesn't really make sense in LL. So instead, // we will just add +1 onto the power to simulate the gains of having the best die picked. unitStrength += extraRollBonus * (unitRolls - 1); totalPower += Math.min(unitStrength, diceSides); totalRolls += unitRolls; } else { totalPower += unitRolls * unitStrength; totalRolls += unitRolls; } } } return TotalPowerAndTotalRolls.builder().totalPower(totalPower).totalRolls(totalRolls).build(); } /** Roll dice for units using low luck rules. */ private static DiceRoll rollDiceLowLuck( final Collection<Unit> unitsList, final boolean defending, final GamePlayer player, final IDelegateBridge bridge, final IBattle battle, final String annotation, final Collection<TerritoryEffect> territoryEffects, final Collection<Unit> allEnemyUnitsAliveOrWaitingToDie, final Collection<Unit> allFriendlyUnitsAliveOrWaitingToDie) { final List<Unit> units = new ArrayList<>(unitsList); final GameData data = bridge.getData(); final Territory location = battle.getTerritory(); final boolean isAmphibiousBattle = battle.isAmphibious(); final Collection<Unit> amphibiousLandAttackers = battle.getAmphibiousLandAttackers(); final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap = DiceRoll.getUnitPowerAndRollsForNormalBattles( units, allEnemyUnitsAliveOrWaitingToDie, allFriendlyUnitsAliveOrWaitingToDie, defending, data, location, territoryEffects, isAmphibiousBattle, amphibiousLandAttackers); final int power = getTotalPower(unitPowerAndRollsMap, data); if (power == 0) { return new DiceRoll(List.of(), 0, 0); } // Roll dice for the fractional part of the dice int hitCount = power / data.getDiceSides(); final List<Die> dice = new ArrayList<>(); final int rollFor = power % data.getDiceSides(); final int[] random; if (rollFor == 0) { random = new int[0]; } else { random = bridge.getRandom(data.getDiceSides(), 1, player, DiceType.COMBAT, annotation); // Zero based final boolean hit = rollFor > random[0]; if (hit) { hitCount++; } dice.add(new Die(random[0], rollFor, hit ? DieType.HIT : DieType.MISS)); } // Create DiceRoll object final double expectedHits = ((double) power) / data.getDiceSides(); final DiceRoll diceRoll = new DiceRoll(dice, hitCount, expectedHits); bridge .getHistoryWriter() .addChildToEvent(annotation + " : " + MyFormatter.asDice(random), diceRoll); return diceRoll; } /** * Sorts the specified collection of units in ascending order of their attack or defense strength. * * @param defending {@code true} if the units should be sorted by their defense strength; * otherwise the units will be sorted by their attack strength. */ public static void sortByStrength(final List<Unit> units, final boolean defending) { // Pre-compute unit strength information to speed up the sort. final Table<UnitType, GamePlayer, Integer> strengthTable = HashBasedTable.create(); for (final Unit unit : units) { final UnitType type = unit.getType(); final GamePlayer owner = unit.getOwner(); if (!strengthTable.contains(type, owner)) { if (defending) { strengthTable.put(type, owner, UnitAttachment.get(type).getDefense(owner)); } else { strengthTable.put(type, owner, UnitAttachment.get(type).getAttack(owner)); } } } final Comparator<Unit> comp = (u1, u2) -> { final int v1 = strengthTable.get(u1.getType(), u1.getOwner()); final int v2 = strengthTable.get(u2.getType(), u2.getOwner()); return Integer.compare(v1, v2); }; units.sort(comp); } public static DiceRoll airBattle( final Collection<Unit> unitsList, final boolean defending, final GamePlayer player, final IDelegateBridge bridge, final String annotation) { final GameData data = bridge.getData(); final boolean lhtrBombers = Properties.getLhtrHeavyBombers(data); final List<Unit> units = new ArrayList<>(unitsList); final int rollCount = AirBattle.getAirBattleRolls(unitsList, defending); if (rollCount == 0) { return new DiceRoll(new ArrayList<>(), 0, 0); } int[] random; final List<Die> dice = new ArrayList<>(); int hitCount = 0; // bonus is normally 1 for most games final int extraRollBonus = Math.max(1, data.getDiceSides() / 6); int totalPower = 0; // We iterate through the units to find the total strength of the units for (final Unit current : units) { final UnitAttachment ua = UnitAttachment.get(current.getType()); final int rolls = AirBattle.getAirBattleRolls(current, defending); int totalStrength = 0; final int strength = Math.min( data.getDiceSides(), Math.max( 0, (defending ? ua.getAirDefense(current.getOwner()) : ua.getAirAttack(current.getOwner())))); for (int i = 0; i < rolls; i++) { // LHTR means pick the best dice roll, which doesn't really make sense in LL. So instead, we // will just add +1 // onto the power to simulate the gains of having the best die picked. if (i > 1 && (lhtrBombers || ua.getChooseBestRoll())) { totalStrength += extraRollBonus; continue; } totalStrength += strength; } totalPower += Math.min(Math.max(totalStrength, 0), data.getDiceSides()); } if (Properties.getLowLuck(data)) { // Get number of hits hitCount = totalPower / data.getDiceSides(); random = new int[0]; // We need to roll dice for the fractional part of the dice. final int power = totalPower % data.getDiceSides(); if (power != 0) { random = bridge.getRandom(data.getDiceSides(), 1, player, DiceType.COMBAT, annotation); final boolean hit = power > random[0]; if (hit) { hitCount++; } dice.add(new Die(random[0], power, hit ? DieType.HIT : DieType.MISS)); } } else { random = bridge.getRandom(data.getDiceSides(), rollCount, player, DiceType.COMBAT, annotation); int diceIndex = 0; for (final Unit current : units) { final UnitAttachment ua = UnitAttachment.get(current.getType()); final int strength = Math.min( data.getDiceSides(), Math.max( 0, (defending ? ua.getAirDefense(current.getOwner()) : ua.getAirAttack(current.getOwner())))); final int rolls = AirBattle.getAirBattleRolls(current, defending); // lhtr heavy bombers take best of n dice for both attack and defense if (rolls > 1 && (lhtrBombers || ua.getChooseBestRoll())) { int minIndex = 0; int min = data.getDiceSides(); for (int i = 0; i < rolls; i++) { if (random[diceIndex + i] < min) { min = random[diceIndex + i]; minIndex = i; } } final boolean hit = strength > random[diceIndex + minIndex]; dice.add( new Die(random[diceIndex + minIndex], strength, hit ? DieType.HIT : DieType.MISS)); for (int i = 0; i < rolls; i++) { if (i != minIndex) { dice.add(new Die(random[diceIndex + i], strength, DieType.IGNORED)); } } if (hit) { hitCount++; } diceIndex += rolls; } else { for (int i = 0; i < rolls; i++) { final boolean hit = strength > random[diceIndex]; dice.add(new Die(random[diceIndex], strength, hit ? DieType.HIT : DieType.MISS)); if (hit) { hitCount++; } diceIndex++; } } } } final double expectedHits = ((double) totalPower) / data.getDiceSides(); final DiceRoll diceRoll = new DiceRoll(dice, hitCount, expectedHits); bridge .getHistoryWriter() .addChildToEvent(annotation + " : " + MyFormatter.asDice(random), diceRoll); return diceRoll; } /** Roll dice for units per normal rules. */ private static DiceRoll rollDiceNormal( final Collection<Unit> unitsList, final boolean defending, final GamePlayer player, final IDelegateBridge bridge, final IBattle battle, final String annotation, final Collection<TerritoryEffect> territoryEffects, final Collection<Unit> allEnemyUnitsAliveOrWaitingToDie, final Collection<Unit> allFriendlyUnitsAliveOrWaitingToDie) { final List<Unit> units = new ArrayList<>(unitsList); final GameData data = bridge.getData(); sortByStrength(units, defending); final Territory location = battle.getTerritory(); final boolean isAmphibiousBattle = battle.isAmphibious(); final Collection<Unit> amphibiousLandAttackers = battle.getAmphibiousLandAttackers(); final Map<Unit, TotalPowerAndTotalRolls> unitPowerAndRollsMap = DiceRoll.getUnitPowerAndRollsForNormalBattles( units, allEnemyUnitsAliveOrWaitingToDie, allFriendlyUnitsAliveOrWaitingToDie, defending, data, location, territoryEffects, isAmphibiousBattle, amphibiousLandAttackers); final TotalPowerAndTotalRolls totalPowerAndRolls = getTotalPowerAndRolls(unitPowerAndRollsMap, data); final int rollCount = totalPowerAndRolls.getTotalRolls(); if (rollCount == 0) { return new DiceRoll(new ArrayList<>(), 0, 0); } final int[] random = bridge.getRandom(data.getDiceSides(), rollCount, player, DiceType.COMBAT, annotation); final boolean lhtrBombers = Properties.getLhtrHeavyBombers(data); final List<Die> dice = new ArrayList<>(); int hitCount = 0; int diceIndex = 0; for (final Unit current : units) { final UnitAttachment ua = UnitAttachment.get(current.getType()); final TotalPowerAndTotalRolls powerAndRolls = unitPowerAndRollsMap.get(current); final int strength = powerAndRolls.getTotalPower(); final int rolls = powerAndRolls.getTotalRolls(); // lhtr heavy bombers take best of n dice for both attack and defense if (rolls <= 0 || strength <= 0) { continue; } if (rolls > 1 && (lhtrBombers || ua.getChooseBestRoll())) { int smallestDieIndex = 0; int smallestDie = data.getDiceSides(); for (int i = 0; i < rolls; i++) { if (random[diceIndex + i] < smallestDie) { smallestDie = random[diceIndex + i]; smallestDieIndex = i; } } // Zero based final boolean hit = strength > random[diceIndex + smallestDieIndex]; dice.add( new Die( random[diceIndex + smallestDieIndex], strength, hit ? DieType.HIT : DieType.MISS)); for (int i = 0; i < rolls; i++) { if (i != smallestDieIndex) { dice.add(new Die(random[diceIndex + i], strength, DieType.IGNORED)); } } if (hit) { hitCount++; } diceIndex += rolls; } else { for (int i = 0; i < rolls; i++) { if (diceIndex >= random.length) { break; } // Zero based final boolean hit = strength > random[diceIndex]; dice.add(new Die(random[diceIndex], strength, hit ? DieType.HIT : DieType.MISS)); if (hit) { hitCount++; } diceIndex++; } } } final int totalPower = totalPowerAndRolls.getTotalPower(); final double expectedHits = ((double) totalPower) / data.getDiceSides(); final DiceRoll diceRoll = new DiceRoll(dice, hitCount, expectedHits); bridge .getHistoryWriter() .addChildToEvent(annotation + " : " + MyFormatter.asDice(random), diceRoll); return diceRoll; } private static boolean isFirstTurnLimitedRoll(final GamePlayer player, final GameData data) { // If player is null, Round > 1, or player has negate rule set: return false return !player.isNull() && data.getSequence().getRound() == 1 && !isNegateDominatingFirstRoundAttack(player) && isDominatingFirstRoundAttack(data.getSequence().getStep().getPlayerId()); } private static boolean isDominatingFirstRoundAttack(final GamePlayer player) { if (player == null) { return false; } final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME); return ra != null && ra.getDominatingFirstRoundAttack(); } private static boolean isNegateDominatingFirstRoundAttack(final GamePlayer player) { final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME); return ra != null && ra.getNegateDominatingFirstRoundAttack(); } /** * Parses the player name from the given annotation that has been produced by getAnnotation(). * * @param annotation The annotation string. * @return The player's name. */ public static String getPlayerNameFromAnnotation(final String annotation) { // This parses the "Germans roll dice for " format produced by getAnnotation() below. return annotation.split(" ", 2)[0]; } public static String getAnnotation( final Collection<Unit> units, final GamePlayer player, final IBattle battle) { final StringBuilder buffer = new StringBuilder(80); // Note: This pattern is parsed when loading saved games to restore dice stats to get the player // name via the // getPlayerNameFromAnnotation() function above. When changing this format, update // getPlayerNameFromAnnotation(), // preferably in a way that is backwards compatible (can parse previous formats too). buffer .append(player.getName()) .append(" roll dice for ") .append(MyFormatter.unitsToTextNoOwner(units)); if (battle != null) { buffer .append(" in ") .append(battle.getTerritory().getName()) .append(", round ") .append((battle.getBattleRound() + 1)); } return buffer.toString(); } public int getHits() { return hits; } public double getExpectedHits() { return expectedHits; } /** * Returns all rolls that are equal to the specified value. * * @param rollAt the strength of the roll, eg infantry roll at 2, expecting a number in [1,6] */ public List<Die> getRolls(final int rollAt) { final List<Die> dice = new ArrayList<>(); for (final Die die : rolls) { if (die.getRolledAt() == rollAt) { dice.add(die); } } return dice; } public int size() { return rolls.size(); } public boolean isEmpty() { return rolls.isEmpty(); } public Die getDie(final int index) { return rolls.get(index); } @Override public void writeExternal(final ObjectOutput out) throws IOException { final int[] dice = new int[rolls.size()]; for (int i = 0; i < rolls.size(); i++) { dice[i] = rolls.get(i).getCompressedValue(); } out.writeObject(dice); out.writeInt(hits); out.writeDouble(expectedHits); } @Override public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException { final int[] dice = (int[]) in.readObject(); rolls = new ArrayList<>(dice.length); for (final int element : dice) { rolls.add(Die.getFromWriteValue(element)); } hits = in.readInt(); expectedHits = in.readDouble(); } @Override public String toString() { return "DiceRoll dice:" + rolls + " hits:" + hits + " expectedHits:" + expectedHits; } }