package games.strategy.triplea.delegate;
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.Collections;
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 games.strategy.engine.data.GameData;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.TerritoryEffect;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.UnitType;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.engine.random.IRandomStats.DiceType;
import games.strategy.triplea.Constants;
import games.strategy.triplea.TripleAUnit;
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.formatter.MyFormatter;
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;
/**
* Used to store information about a dice roll.
*
* <p>
* # of rolls at 5, at 4, etc.
* </p>
*
* <p>
* Externalizable so we can efficiently write out our dice as ints rather than as full objects.
* </p>
*/
public class DiceRoll implements Externalizable {
private static final long serialVersionUID = -1167204061937566271L;
private List<Die> m_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 m_hits;
/**
* Returns a Tuple with 2 values, 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> getAAattackAndMaxDiceSides(final Collection<Unit> defendingEnemyAA,
final GameData data, final boolean defending) {
int highestAttack = 0;
final int diceSize = data.getDiceSides();
int chosenDiceSize = diceSize;
for (final Unit u : defendingEnemyAA) {
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 (attack > uaDiceSides) {
attack = uaDiceSides;
}
if ((((float) attack) / ((float) uaDiceSides)) > (((float) highestAttack) / ((float) chosenDiceSize))) {
highestAttack = attack;
chosenDiceSize = uaDiceSides;
}
}
if (highestAttack > chosenDiceSize / 2 && chosenDiceSize > 1) {
// TODO: sadly the whole low luck section falls apart if AA are hitting at greater than half the
// value of dice, and I don't feel like rewriting it
highestAttack = chosenDiceSize / 2;
}
return Tuple.of(highestAttack, chosenDiceSize);
}
public static int getTotalAAattacks(final Collection<Unit> defendingEnemyAA,
final Collection<Unit> validAttackingUnitsForThisRoll) {
if (defendingEnemyAA.isEmpty() || validAttackingUnitsForThisRoll.isEmpty()) {
return 0;
}
int totalAAattacksNormal = 0;
int totalAAattacksSurplus = 0;
for (final Unit aa : defendingEnemyAA) {
final UnitAttachment ua = UnitAttachment.get(aa.getType());
if (ua.getMaxAAattacks() == -1) {
totalAAattacksNormal = validAttackingUnitsForThisRoll.size();
} else {
if (ua.getMayOverStackAA()) {
totalAAattacksSurplus += ua.getMaxAAattacks();
} else {
totalAAattacksNormal += ua.getMaxAAattacks();
}
}
}
totalAAattacksNormal = Math.min(totalAAattacksNormal, validAttackingUnitsForThisRoll.size());
return totalAAattacksNormal + totalAAattacksSurplus;
}
public static DiceRoll rollAA(final Collection<Unit> validAttackingUnitsForThisRoll,
final Collection<Unit> defendingAAForThisRoll, final IDelegateBridge bridge, final Territory location,
final boolean defending) {
{
final Set<Unit> duplicatesCheckSet1 = new HashSet<>(validAttackingUnitsForThisRoll);
if (validAttackingUnitsForThisRoll.size() != duplicatesCheckSet1.size()) {
throw new IllegalStateException("Duplicate Units Detected: Original List:" + validAttackingUnitsForThisRoll
+ " HashSet:" + duplicatesCheckSet1);
}
final Set<Unit> duplicatesCheckSet2 = new HashSet<>(defendingAAForThisRoll);
if (defendingAAForThisRoll.size() != duplicatesCheckSet2.size()) {
throw new IllegalStateException(
"Duplicate Units Detected: Original List:" + defendingAAForThisRoll + " HashSet:" + duplicatesCheckSet2);
}
}
final List<Unit> defendingAA = Match.getMatches(defendingAAForThisRoll,
(defending ? Matches.UnitAttackAAisGreaterThanZeroAndMaxAAattacksIsNotZero
: Matches.UnitOffensiveAttackAAisGreaterThanZeroAndMaxAAattacksIsNotZero));
if (defendingAA.isEmpty()) {
return new DiceRoll(new ArrayList<>(0), 0);
}
final GameData data = bridge.getData();
final int totalAAattacksTotal = getTotalAAattacks(defendingAA, validAttackingUnitsForThisRoll);
if (totalAAattacksTotal <= 0) {
return new DiceRoll(new ArrayList<>(0), 0);
}
// determine dicesides for everyone (we are not going to consider the possibility of different dicesides within the
// same typeAA)
final Tuple<Integer, Integer> attackThenDiceSidesForAll = getAAattackAndMaxDiceSides(defendingAA, data, defending);
// final int highestAttackPower = attackThenDiceSidesForAll.getFirst();
final int chosenDiceSizeForAll = attackThenDiceSidesForAll.getSecond();
int hits = 0;
final List<Die> sortedDice = new ArrayList<>();
final String typeAA = UnitAttachment.get(defendingAA.get(0).getType()).getTypeAA();
// LOW LUCK
if (games.strategy.triplea.Properties.getLow_Luck(data) || games.strategy.triplea.Properties.getLL_AA_ONLY(data)) {
final String annotation = "Roll " + typeAA + " in " + location.getName();
final Triple<Integer, Integer, Boolean> triple = getTotalAAPowerThenHitsAndFillSortedDiceThenIfAllUseSameAttack(
null, null, defending, defendingAA, validAttackingUnitsForThisRoll, data, false);
final int totalPower = triple.getFirst();
hits += getLowLuckHits(bridge, sortedDice, totalPower, chosenDiceSizeForAll, defendingAA.get(0).getOwner(),
annotation);
} else {
final String annotation = "Roll " + typeAA + " in " + location.getName();
final int[] dice = bridge.getRandom(chosenDiceSizeForAll, totalAAattacksTotal, defendingAA.get(0).getOwner(),
DiceType.COMBAT, annotation);
hits += getTotalAAPowerThenHitsAndFillSortedDiceThenIfAllUseSameAttack(dice, sortedDice, defending, defendingAA,
validAttackingUnitsForThisRoll, data, true).getSecond();
}
final DiceRoll roll = new DiceRoll(sortedDice, hits);
final String annotation = typeAA + " fire in " + location + " : " + MyFormatter.asDice(roll);
bridge.getHistoryWriter().addChildToEvent(annotation, roll);
return roll;
}
/**
* Basically I wanted 1 single method for both Low Luck and Dice, because if we have 2 methods then there is a chance
* they will go out of
* sync.
*
* @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 defendingAA 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 Collection<Unit> defendingAAForThisRoll, final Collection<Unit> validAttackingUnitsForThisRoll,
final GameData data, final boolean fillInSortedDiceAndRecordHits) {
final List<Unit> defendingAA = Match.getMatches(defendingAAForThisRoll,
(defending ? Matches.UnitAttackAAisGreaterThanZeroAndMaxAAattacksIsNotZero
: Matches.UnitOffensiveAttackAAisGreaterThanZeroAndMaxAAattacksIsNotZero));
if (defendingAA.size() <= 0) {
return Triple.of(0, 0, false);
}
// we want to make sure the higher powers fire
sortAAHighToLow(defendingAA, data, defending);
// this is confusing, but what we want to do is the following:
// 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])
// 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.
// the total number of shots from these first 2 groups cannot exceed the number of air units being shot at
// last, any aa that can overstack will fire after, individually
// (an aa guns that is both infinite, and overstacks, ignores the overstack part because that totally doesn't make
// any sense)
// set up all 3 groups of aa guns
final List<Unit> normalNonInfiniteAA = new ArrayList<>(defendingAA);
final List<Unit> infiniteAA = Match.getMatches(defendingAA, Matches.UnitMaxAAattacksIsInfinite);
final List<Unit> overstackAA = Match.getMatches(defendingAA, Matches.UnitMayOverStackAA);
overstackAA.removeAll(infiniteAA);
normalNonInfiniteAA.removeAll(infiniteAA);
normalNonInfiniteAA.removeAll(overstackAA);
// determine maximum total attacks
final int totalAAattacksTotal = getTotalAAattacks(defendingAA, validAttackingUnitsForThisRoll);
// determine individual totals
final int normalNonInfiniteAAtotalAAattacks =
getTotalAAattacks(normalNonInfiniteAA, validAttackingUnitsForThisRoll);
final int infiniteAAtotalAAattacks =
Math.min((validAttackingUnitsForThisRoll.size() - normalNonInfiniteAAtotalAAattacks),
getTotalAAattacks(infiniteAA, validAttackingUnitsForThisRoll));
final int overstackAAtotalAAattacks = getTotalAAattacks(overstackAA, validAttackingUnitsForThisRoll);
if (totalAAattacksTotal != (normalNonInfiniteAAtotalAAattacks + infiniteAAtotalAAattacks
+ overstackAAtotalAAattacks)) {
throw new IllegalStateException("Total attacks should be: " + totalAAattacksTotal + " but instead is: "
+ (normalNonInfiniteAAtotalAAattacks + infiniteAAtotalAAattacks + overstackAAtotalAAattacks));
// determine dicesides for everyone (we are not going to consider the possibility of different dicesides within
// the same typeAA)
// final Tuple<Integer, Integer> attackThenDiceSidesForAll = getAAattackAndMaxDiceSides(defendingAA, data);
// final int chosenDiceSizeForAll = attackThenDiceSidesForAll.getSecond();
}
// determine highest attack for infinite group
final Tuple<Integer, Integer> attackThenDiceSidesForInfinite =
getAAattackAndMaxDiceSides(infiniteAA, data, defending);
// not zero based
final int hitAtForInfinite = attackThenDiceSidesForInfinite.getFirst();
// not zero based
// final int powerForInfinite = highestAttackForInfinite;
// if we are low luck, we only want to know the power and total attacks, while if we are 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();
// should be > 0 at this point
int numAttacks = UnitAttachment.get(aaGun.getType()).getMaxAAattacks();
final int hitAt = getAAattackAndMaxDiceSides(Collections.singleton(aaGun), data, defending).getFirst();
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) {
// we 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();
// should be > 0 at this point
int numAttacks = UnitAttachment.get(aaGun.getType()).getMaxAAattacks();
// zero based, so subtract 1
final int hitAt = getAAattackAndMaxDiceSides(Collections.singleton(aaGun), data, defending).getFirst();
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) {
final Comparator<Unit> comparator = (u1, u2) -> {
final Tuple<Integer, Integer> tuple1 = getAAattackAndMaxDiceSides(Collections.singleton(u1), data, defending);
final Tuple<Integer, Integer> tuple2 = getAAattackAndMaxDiceSides(Collections.singleton(u2), data, defending);
if (tuple1.getFirst() == 0) {
if (tuple2.getFirst() == 0) {
return 0;
}
return 1;
} else if (tuple2.getFirst() == 0) {
return -1;
}
final float value1 = ((float) tuple1.getFirst()) / ((float) tuple1.getSecond());
final float value2 = ((float) tuple2.getFirst()) / ((float) tuple2.getSecond());
if (value1 < value2) {
return 1;
} else if (value1 > value2) {
return -1;
}
return 0;
};
Collections.sort(units, comparator);
}
private static int getLowLuckHits(final IDelegateBridge bridge, final List<Die> sortedDice, final int totalPower,
final int chosenDiceSize, final PlayerID 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;
}
/**
* Roll dice for units.
*/
public static DiceRoll rollDice(final List<Unit> units, final boolean defending, final PlayerID player,
final IDelegateBridge bridge, final IBattle battle, final String annotation,
final Collection<TerritoryEffect> territoryEffects, final List<Unit> allEnemyUnitsAliveOrWaitingToDie) {
// Decide whether to use low luck rules or normal rules.
if (games.strategy.triplea.Properties.getLow_Luck(bridge.getData())) {
return rollDiceLowLuck(units, defending, player, bridge, battle, annotation, territoryEffects,
allEnemyUnitsAliveOrWaitingToDie);
} else {
return rollDiceNormal(units, defending, player, bridge, battle, annotation, territoryEffects,
allEnemyUnitsAliveOrWaitingToDie);
}
}
/**
* 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 PlayerID playerRolling, final DiceType diceType, final String annotation) {
if (rollCount == 0) {
return new DiceRoll(new ArrayList<>(), 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);
}
/**
* @param unitsGettingPowerFor
* should be sorted from weakest to strongest, before the method is called, for the actual battle.
*/
public static Map<Unit, Tuple<Integer, Integer>> getUnitPowerAndRollsForNormalBattles(
final List<Unit> unitsGettingPowerFor, final List<Unit> allEnemyUnitsAliveOrWaitingToDie,
final boolean defending, final boolean bombing, final GameData data, final Territory location,
final Collection<TerritoryEffect> territoryEffects, final boolean isAmphibiousBattle,
final Collection<Unit> amphibiousLandAttackers) {
return getUnitPowerAndRollsForNormalBattles(unitsGettingPowerFor,
allEnemyUnitsAliveOrWaitingToDie, defending, bombing, data, location, territoryEffects,
isAmphibiousBattle, amphibiousLandAttackers, new HashMap<>(),
new HashMap<>());
}
/**
* @param unitsGettingPowerFor
* should be sorted from weakest to strongest, before the method is called, for the actual battle.
*/
protected static Map<Unit, Tuple<Integer, Integer>> getUnitPowerAndRollsForNormalBattles(
final List<Unit> unitsGettingPowerFor,
final List<Unit> allEnemyUnitsAliveOrWaitingToDie, final boolean defending, final boolean bombing,
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, Tuple<Integer, Integer>> rVal = new HashMap<>();
if (unitsGettingPowerFor == null || unitsGettingPowerFor.isEmpty()) {
return rVal;
}
// get all supports, friendly and enemy
final Set<List<UnitSupportAttachment>> supportRulesFriendly = new HashSet<>();
final IntegerMap<UnitSupportAttachment> supportLeftFriendly = new IntegerMap<>();
final Map<UnitSupportAttachment, LinkedIntegerMap<Unit>> supportUnitsLeftFriendly =
new HashMap<>();
getSupport(unitsGettingPowerFor, supportRulesFriendly, supportLeftFriendly, supportUnitsLeftFriendly,
data, defending, true);
final Set<List<UnitSupportAttachment>> supportRulesEnemy = new HashSet<>();
final IntegerMap<UnitSupportAttachment> supportLeftEnemy = new IntegerMap<>();
final Map<UnitSupportAttachment, LinkedIntegerMap<Unit>> supportUnitsLeftEnemy =
new HashMap<>();
getSupport(allEnemyUnitsAliveOrWaitingToDie, supportRulesEnemy, supportLeftEnemy, supportUnitsLeftEnemy, data,
!defending, false);
// copy for rolls
final IntegerMap<UnitSupportAttachment> supportLeftFriendlyRolls =
new IntegerMap<>(supportLeftFriendly);
final IntegerMap<UnitSupportAttachment> supportLeftEnemyRolls =
new IntegerMap<>(supportLeftEnemy);
final Map<UnitSupportAttachment, LinkedIntegerMap<Unit>> supportUnitsLeftFriendlyRolls =
new HashMap<>();
for (final UnitSupportAttachment usa : supportUnitsLeftFriendly.keySet()) {
supportUnitsLeftFriendlyRolls.put(usa, new LinkedIntegerMap<>(supportUnitsLeftFriendly.get(usa)));
}
final Map<UnitSupportAttachment, LinkedIntegerMap<Unit>> supportUnitsLeftEnemyRolls =
new HashMap<>();
for (final UnitSupportAttachment usa : supportUnitsLeftEnemy.keySet()) {
supportUnitsLeftEnemyRolls.put(usa, new LinkedIntegerMap<>(supportUnitsLeftEnemy.get(usa)));
}
final int diceSides = data.getDiceSides();
for (final Unit current : unitsGettingPowerFor) {
// find our initial strength
int strength;
final UnitAttachment ua = UnitAttachment.get(current.getType());
if (defending) {
strength = ua.getDefense(current.getOwner());
if (isFirstTurnLimitedRoll(current.getOwner(), data)) {
strength = Math.min(1, strength);
} else {
strength += getSupport(current, supportRulesFriendly, supportLeftFriendly, supportUnitsLeftFriendly,
unitSupportPowerMap, true, false);
}
strength += getSupport(current, supportRulesEnemy, supportLeftEnemy, supportUnitsLeftEnemy, unitSupportPowerMap,
true, false);
} else {
strength = ua.getAttack(current.getOwner());
if (ua.getIsMarine() != 0 && isAmphibiousBattle) {
if (amphibiousLandAttackers.contains(current)) {
strength += ua.getIsMarine();
}
}
if (ua.getIsSea() && isAmphibiousBattle && Matches.TerritoryIsLand.match(location)) {
// change the strength to be bombard, not attack/defense, because this is a
strength = ua.getBombard(current.getOwner());
// bombarding naval unit
}
strength += getSupport(current, supportRulesFriendly, supportLeftFriendly, supportUnitsLeftFriendly,
unitSupportPowerMap, true, false);
strength += getSupport(current, supportRulesEnemy, supportLeftEnemy, supportUnitsLeftEnemy, unitSupportPowerMap,
true, false);
}
strength += TerritoryEffectHelper.getTerritoryCombatBonus(current.getType(), territoryEffects, defending);
strength = Math.min(Math.max(strength, 0), diceSides);
// now determine our rolls
int rolls;
if (!bombing && strength == 0) {
rolls = 0;
} else {
if (defending) {
rolls = ua.getDefenseRolls(current.getOwner());
} else {
rolls = ua.getAttackRolls(current.getOwner());
}
rolls += getSupport(current, supportRulesFriendly, supportLeftFriendlyRolls, supportUnitsLeftFriendlyRolls,
unitSupportRollsMap, false, true);
rolls += getSupport(current, supportRulesEnemy, supportLeftEnemyRolls, supportUnitsLeftEnemyRolls,
unitSupportRollsMap, false, true);
rolls = Math.max(0, rolls);
if (rolls == 0) {
strength = 0;
}
}
rVal.put(current, Tuple.of(strength, rolls));
}
return rVal;
}
public static int getTotalPower(final Map<Unit, Tuple<Integer, Integer>> unitPowerAndRollsMap,
final GameData data) {
return getTotalPowerAndRolls(unitPowerAndRollsMap, data).getFirst();
}
private static Tuple<Integer, Integer> getTotalPowerAndRolls(
final Map<Unit, Tuple<Integer, Integer>> unitPowerAndRollsMap, final GameData data) {
final int diceSides = data.getDiceSides();
final boolean lowLuck = games.strategy.triplea.Properties.getLow_Luck(data);
final boolean lhtrBombers = games.strategy.triplea.Properties.getLHTR_Heavy_Bombers(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, Tuple<Integer, Integer>> entry : unitPowerAndRollsMap.entrySet()) {
int unitStrength = Math.min(Math.max(0, entry.getValue().getFirst()), diceSides);
final int unitRolls = entry.getValue().getSecond();
if (unitStrength <= 0 || unitRolls <= 0) {
continue;
}
if (unitRolls == 1) {
totalPower += unitStrength;
totalRolls += unitRolls;
} else {
final UnitAttachment ua = UnitAttachment.get(entry.getKey().getType());
if (lowLuck) {
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;
}
} else {
if (lhtrBombers || ua.getChooseBestRoll()) {
// Even though we are DICE, we still have to wait for actual dice to be thrown before we can pick the best
// die. So actually for
// dice this totalPower method is basically useless, so lets just use the approximation of adding on +1 to
// power for now.
unitStrength += extraRollBonus * (unitRolls - 1);
totalPower += Math.min(unitStrength, diceSides);
totalRolls += unitRolls;
} else {
totalPower += unitRolls * unitStrength;
totalRolls += unitRolls;
}
}
}
}
return Tuple.of(totalPower, totalRolls);
}
/**
* Roll dice for units using low luck rules. Low luck rules based on rules in DAAK.
*/
private static DiceRoll rollDiceLowLuck(final List<Unit> unitsList, final boolean defending, final PlayerID player,
final IDelegateBridge bridge, final IBattle battle, final String annotation,
final Collection<TerritoryEffect> territoryEffects, final List<Unit> allEnemyUnitsAliveOrWaitingToDie) {
final List<Unit> units = new ArrayList<>(unitsList);
{
final Set<Unit> duplicatesCheckSet = new HashSet<>(unitsList);
if (units.size() != duplicatesCheckSet.size()) {
throw new IllegalStateException(
"Duplicate Units Detected: Original List:" + units + " HashSet:" + duplicatesCheckSet);
}
}
final GameData data = bridge.getData();
final Territory location = battle.getTerritory();
final boolean isAmphibiousBattle = battle.isAmphibious();
final Collection<Unit> amphibiousLandAttackers = battle.getAmphibiousLandAttackers();
final Map<Unit, Tuple<Integer, Integer>> unitPowerAndRollsMap =
DiceRoll.getUnitPowerAndRollsForNormalBattles(units, allEnemyUnitsAliveOrWaitingToDie, defending, false,
data, location, territoryEffects, isAmphibiousBattle, amphibiousLandAttackers);
final int power = getTotalPower(unitPowerAndRollsMap, data);
if (power == 0) {
return new DiceRoll(new ArrayList<>(0), 0);
}
int hitCount = power / data.getDiceSides();
final List<Die> dice = new ArrayList<>();
// We need to roll dice for the fractional part of the dice.
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 DiceRoll rVal = new DiceRoll(dice, hitCount);
bridge.getHistoryWriter().addChildToEvent(annotation + " : " + MyFormatter.asDice(random), rVal);
return rVal;
}
/**
* Fills a set and map with the support possibly given by these units.
*
* @param supportsAvailable
* an empty set that will be filled with all support rules grouped into lists of non-stacking rules
* @param supportLeft
* an empty map that will be filled with all the support that can be given in the form of counters
* @param supportUnitsLeft
* an empty map that will be filled with all the support that can be given in the form of counters
* @param defence
* are the receiving units defending?
* @param allies
* are the receiving units allied to the giving units?
*/
public static void getSupport(final List<Unit> unitsGivingTheSupport,
final Set<List<UnitSupportAttachment>> supportsAvailable, final IntegerMap<UnitSupportAttachment> supportLeft,
final Map<UnitSupportAttachment, LinkedIntegerMap<Unit>> supportUnitsLeft, final GameData data,
final boolean defence, final boolean allies) {
if (unitsGivingTheSupport == null || unitsGivingTheSupport.isEmpty()) {
return;
}
for (final UnitSupportAttachment rule : UnitSupportAttachment.get(data)) {
if (rule.getPlayers().isEmpty()) {
continue;
}
if (!((defence && rule.getDefence()) || (!defence && rule.getOffence()))) {
continue;
}
if (!((allies && rule.getAllied()) || (!allies && rule.getEnemy()))) {
continue;
}
final CompositeMatchAnd<Unit> canSupport = new CompositeMatchAnd<>(
Matches.unitIsOfType((UnitType) rule.getAttachedTo()), Matches.unitOwnedBy(rule.getPlayers()));
final List<Unit> supporters = Match.getMatches(unitsGivingTheSupport, canSupport);
int numSupport = supporters.size();
if (numSupport <= 0) {
continue;
}
final List<Unit> impArtTechUnits = new ArrayList<>();
if (rule.getImpArtTech()) {
impArtTechUnits.addAll(Match.getMatches(supporters, Matches.unitOwnerHasImprovedArtillerySupportTech()));
}
numSupport += impArtTechUnits.size();
supportLeft.put(rule, numSupport * rule.getNumber());
supportUnitsLeft.put(rule, new LinkedIntegerMap<>(supporters, rule.getNumber()));
supportUnitsLeft.get(rule).addAll(impArtTechUnits, rule.getNumber());
final Iterator<List<UnitSupportAttachment>> iter2 = supportsAvailable.iterator();
List<UnitSupportAttachment> ruleType = null;
boolean found = false;
final String bonusType = rule.getBonusType();
while (iter2.hasNext()) {
ruleType = iter2.next();
if (ruleType.get(0).getBonusType().equals(bonusType)) {
found = true;
break;
}
}
if (!found) {
ruleType = new ArrayList<>();
supportsAvailable.add(ruleType);
}
if (ruleType != null) {
ruleType.add(rule);
}
}
sortSupportRules(supportsAvailable, defence, allies);
}
/**
* Returns the support for this unit type, and decrements the supportLeft counters.
*
* @return the bonus given to the unit
*/
public static int getSupport(final Unit unit, final Set<List<UnitSupportAttachment>> supportsAvailable,
final IntegerMap<UnitSupportAttachment> supportLeft,
final Map<UnitSupportAttachment, LinkedIntegerMap<Unit>> supportUnitsLeft,
final Map<Unit, IntegerMap<Unit>> unitSupportMap, final boolean strength, final boolean rolls) {
int givenSupport = 0;
for (final List<UnitSupportAttachment> bonusType : supportsAvailable) {
for (final UnitSupportAttachment rule : bonusType) {
if (!((strength && rule.getStrength()) || (rolls && rule.getRoll()))) {
continue;
}
final HashSet<UnitType> types = rule.getUnitType();
if (types != null && types.contains(unit.getType()) && supportLeft.getInt(rule) > 0) {
givenSupport += rule.getBonus();
supportLeft.add(rule, -1);
final LinkedIntegerMap<Unit> supportersLeft = supportUnitsLeft.get(rule);
if (supportersLeft != null) {
final Set<Unit> supporters = supportersLeft.keySet();
if (!supporters.isEmpty()) {
final Unit u = supporters.iterator().next();
supportUnitsLeft.get(rule).add(u, -1);
if (supportUnitsLeft.get(rule).getInt(u) <= 0) {
supportUnitsLeft.get(rule).removeKey(u);
}
if (unitSupportMap.containsKey(u)) {
unitSupportMap.get(u).add(unit, rule.getBonus());
} else {
unitSupportMap.put(u, new IntegerMap<>(unit, rule.getBonus()));
}
}
}
break;
}
}
}
return givenSupport;
}
public static void sortByStrength(final List<Unit> units, final boolean defending) {
final Comparator<Unit> comp = (u1, u2) -> {
Integer v1;
Integer v2;
if (defending) {
v1 = UnitAttachment.get(u1.getType()).getDefense(u1.getOwner());
v2 = UnitAttachment.get(u2.getType()).getDefense(u2.getOwner());
} else {
v1 = UnitAttachment.get(u1.getType()).getAttack(u1.getOwner());
v2 = UnitAttachment.get(u2.getType()).getAttack(u2.getOwner());
}
return v1.compareTo(v2);
};
Collections.sort(units, comp);
}
private static void sortSupportRules(final Set<List<UnitSupportAttachment>> support, final boolean defense,
final boolean friendly) {
// first, sort the lists inside each set
final Comparator<UnitSupportAttachment> compList = (u1, u2) -> {
int compareTo = 0;
// we want to apply the biggest bonus first
// Make sure stronger supports are ordered first if friendly, and worst are ordered first if enemy
// TODO: it is possible that we will waste negative support if we reduce a units power to less than zero.
// We should actually apply enemy negative support in order from worst to least bad, on a unit list that is
// ordered from strongest
// to weakest.
final boolean u1CanBonus = defense ? u1.getDefence() : u1.getOffence();
final boolean u2CanBonus = defense ? u2.getDefence() : u2.getOffence();
if (friendly) {
// favor rolls over strength
if (u1.getRoll() || u2.getRoll()) {
final int u1Bonus = u1.getRoll() && u1CanBonus ? u1.getBonus() : 0;
final Integer u2Bonus = u2.getRoll() && u2CanBonus ? u2.getBonus() : 0;
compareTo = u2Bonus.compareTo(u1Bonus);
if (compareTo != 0) {
return compareTo;
}
}
if (u1.getStrength() || u2.getStrength()) {
final int u1Bonus = u1.getStrength() && u1CanBonus ? u1.getBonus() : 0;
final Integer u2Bonus = u2.getStrength() && u2CanBonus ? u2.getBonus() : 0;
compareTo = u2Bonus.compareTo(u1Bonus);
if (compareTo != 0) {
return compareTo;
}
}
} else {
if (u1.getRoll() || u2.getRoll()) {
final Integer u1Bonus = u1.getRoll() && u1CanBonus ? u1.getBonus() : 0;
final int u2Bonus = u2.getRoll() && u2CanBonus ? u2.getBonus() : 0;
compareTo = u1Bonus.compareTo(u2Bonus);
if (compareTo != 0) {
return compareTo;
}
}
if (u1.getStrength() || u2.getStrength()) {
final Integer u1Bonus = u1.getStrength() && u1CanBonus ? u1.getBonus() : 0;
final int u2Bonus = u2.getStrength() && u2CanBonus ? u2.getBonus() : 0;
compareTo = u1Bonus.compareTo(u2Bonus);
if (compareTo != 0) {
return compareTo;
}
}
}
// if the bonuses are the same, we want to make sure any support which only supports 1 single unittype goes
// first
// the reason being that we could have Support1 which supports both infantry and mech infantry, and Support2
// which only supports
// mech infantry
// if the Support1 goes first, and the mech infantry is first in the unit list (highly probable), then Support1
// will end up using
// all of itself up on the mech infantry
// then when the Support2 comes up, all the mech infantry are used up, and it does nothing.
// instead, we want Support2 to come first, support all mech infantry that it can, then have Support1 come in
// and support whatever
// is left, that way no support is wasted
// TODO: this breaks down completely if we have Support1 having a higher bonus than Support2, because it will
// come first. It should
// come first, unless we would have support wasted otherwise. This ends up being a pretty tricky math puzzle.
final HashSet<UnitType> types1 = u1.getUnitType();
final HashSet<UnitType> types2 = u2.getUnitType();
final Integer s1 = types1 == null ? 0 : types1.size();
final int s2 = types2 == null ? 0 : types2.size();
compareTo = s1.compareTo(s2);
if (compareTo != 0) {
return compareTo;
}
// Now we need to sort so that the supporters who are the most powerful go before the less powerful
// This is not necessary for the providing of support, but is necessary for our default casualty selection
// method
final UnitType unitType1 = (UnitType) u1.getAttachedTo();
final UnitType unitType2 = (UnitType) u2.getAttachedTo();
final UnitAttachment ua1 = UnitAttachment.get(unitType1);
final UnitAttachment ua2 = UnitAttachment.get(unitType2);
final int unitPower1;
final Integer unitPower2;
if (u1.getDefence()) {
unitPower1 = ua1.getDefenseRolls(PlayerID.NULL_PLAYERID) * ua1.getDefense(PlayerID.NULL_PLAYERID);
unitPower2 = ua2.getDefenseRolls(PlayerID.NULL_PLAYERID) * ua2.getDefense(PlayerID.NULL_PLAYERID);
} else {
unitPower1 = ua1.getAttackRolls(PlayerID.NULL_PLAYERID) * ua1.getAttack(PlayerID.NULL_PLAYERID);
unitPower2 = ua2.getAttackRolls(PlayerID.NULL_PLAYERID) * ua2.getAttack(PlayerID.NULL_PLAYERID);
}
return unitPower2.compareTo(unitPower1);
};
final Iterator<List<UnitSupportAttachment>> iter = support.iterator();
while (iter.hasNext()) {
Collections.sort(iter.next(), compList);
}
}
public static DiceRoll airBattle(final List<Unit> unitsList, final boolean defending, final PlayerID player,
final IDelegateBridge bridge, final String annotation) {
{
final Set<Unit> duplicatesCheckSet1 = new HashSet<>(unitsList);
if (unitsList.size() != duplicatesCheckSet1.size()) {
throw new IllegalStateException(
"Duplicate Units Detected: Original List:" + unitsList + " HashSet:" + duplicatesCheckSet1);
}
}
final GameData data = bridge.getData();
final boolean lhtrBombers = games.strategy.triplea.Properties.getLHTR_Heavy_Bombers(data);
final List<Unit> units = new ArrayList<>(unitsList);
final int rollCount = AirBattle.getAirBattleRolls(unitsList, defending);
if (rollCount == 0) {
return new DiceRoll(new ArrayList<>(), 0);
}
int[] random;
final List<Die> dice = new ArrayList<>();
int hitCount = 0;
if (games.strategy.triplea.Properties.getLow_Luck(data)) {
// bonus is normally 1 for most games
final int extraRollBonus = Math.max(1, data.getDiceSides() / 6);
final Iterator<Unit> iter = units.iterator();
int power = 0;
// We iterate through the units to find the total strength of the units
while (iter.hasNext()) {
final Unit current = iter.next();
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;
}
power += Math.min(Math.max(totalStrength, 0), data.getDiceSides());
}
// Get number of hits
hitCount = power / data.getDiceSides();
random = new int[0];
// We need to roll dice for the fractional part of the dice.
power = power % 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);
final Iterator<Unit> iter = units.iterator();
int diceIndex = 0;
while (iter.hasNext()) {
final Unit current = iter.next();
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 DiceRoll rVal = new DiceRoll(dice, hitCount);
bridge.getHistoryWriter().addChildToEvent(annotation + " : " + MyFormatter.asDice(random), rVal);
return rVal;
}
/**
* Roll dice for units per normal rules.
*/
private static DiceRoll rollDiceNormal(final List<Unit> unitsList, final boolean defending, final PlayerID player,
final IDelegateBridge bridge, final IBattle battle, final String annotation,
final Collection<TerritoryEffect> territoryEffects, final List<Unit> allEnemyUnitsAliveOrWaitingToDie) {
final List<Unit> units = new ArrayList<>(unitsList);
{
final Set<Unit> duplicatesCheckSet = new HashSet<>(unitsList);
if (units.size() != duplicatesCheckSet.size()) {
throw new IllegalStateException(
"Duplicate Units Detected: Original List:" + units + " HashSet:" + duplicatesCheckSet);
}
}
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, Tuple<Integer, Integer>> unitPowerAndRollsMap =
DiceRoll.getUnitPowerAndRollsForNormalBattles(units, allEnemyUnitsAliveOrWaitingToDie, defending, false,
data, location, territoryEffects, isAmphibiousBattle, amphibiousLandAttackers);
final Tuple<Integer, Integer> totalPowerAndRolls = getTotalPowerAndRolls(unitPowerAndRollsMap, data);
final int rollCount = totalPowerAndRolls.getSecond();
if (rollCount == 0) {
return new DiceRoll(new ArrayList<>(), 0);
}
final int[] random = bridge.getRandom(data.getDiceSides(), rollCount, player, DiceType.COMBAT, annotation);
final boolean lhtrBombers = games.strategy.triplea.Properties.getLHTR_Heavy_Bombers(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 Tuple<Integer, Integer> powerAndRolls = unitPowerAndRollsMap.get(current);
final int strength = powerAndRolls.getFirst();
final int rolls = powerAndRolls.getSecond();
// 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++) {
// 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 DiceRoll rVal = new DiceRoll(dice, hitCount);
bridge.getHistoryWriter().addChildToEvent(annotation + " : " + MyFormatter.asDice(random), rVal);
return rVal;
}
private static boolean isFirstTurnLimitedRoll(final PlayerID player, final GameData data) {
// If player is null, Round > 1, or player has negate rule set: return false
if (player.isNull() || data.getSequence().getRound() != 1 || isNegateDominatingFirstRoundAttack(player)) {
return false;
}
return isDominatingFirstRoundAttack(data.getSequence().getStep().getPlayerID());
}
private static boolean isDominatingFirstRoundAttack(final PlayerID player) {
if (player == null) {
return false;
}
final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME);
if (ra == null) {
return false;
}
return ra.getDominatingFirstRoundAttack();
}
private static boolean isNegateDominatingFirstRoundAttack(final PlayerID player) {
final RulesAttachment ra = (RulesAttachment) player.getAttachment(Constants.RULES_ATTACHMENT_NAME);
if (ra == null) {
return false;
}
return ra.getNegateDominatingFirstRoundAttack();
}
public static boolean isAmphibious(final Collection<Unit> m_units) {
final Iterator<Unit> unitIter = m_units.iterator();
while (unitIter.hasNext()) {
final TripleAUnit checkedUnit = (TripleAUnit) unitIter.next();
if (checkedUnit.getWasAmphibious()) {
return true;
}
}
return false;
}
public static String getAnnotation(final List<Unit> units, final PlayerID player, final IBattle battle) {
final StringBuilder buffer = new StringBuilder(80);
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();
}
/**
* @param dice
* int[] the dice, 0 based
* @param hits
* int - the number of hits
* @param rollAt
* int - what we roll at, [0,Constants.MAX_DICE]
* @param hitOnlyIfEquals
* boolean - 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) {
m_hits = hits;
m_rolls = new ArrayList<>(dice.length);
for (final int element : dice) {
boolean hit;
if (hitOnlyIfEquals) {
hit = (rollAt == element);
} else {
hit = element <= rollAt;
}
m_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) {
m_rolls = new ArrayList<>(dice);
m_hits = hits;
}
public int getHits() {
return m_hits;
}
/**
* @param rollAt
* the strength of the roll, eg infantry roll at 2, expecting a
* number in [1,6]
* @return in int[] which shouldnt be modifed, the int[] is 0 based, ie
* 0..MAX_DICE
*/
public List<Die> getRolls(final int rollAt) {
final List<Die> rVal = new ArrayList<>();
for (final Die die : m_rolls) {
if (die.getRolledAt() == rollAt) {
rVal.add(die);
}
}
return rVal;
}
public int size() {
return m_rolls.size();
}
public Die getDie(final int index) {
return m_rolls.get(index);
}
@Override
public void writeExternal(final ObjectOutput out) throws IOException {
final int[] dice = new int[m_rolls.size()];
for (int i = 0; i < m_rolls.size(); i++) {
dice[i] = m_rolls.get(i).getCompressedValue();
}
out.writeObject(dice);
out.writeInt(m_hits);
}
@Override
public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
final int[] dice = (int[]) in.readObject();
m_rolls = new ArrayList<>(dice.length);
for (final int element : dice) {
m_rolls.add(Die.getFromWriteValue(element));
}
m_hits = in.readInt();
}
@Override
public String toString() {
return "DiceRoll dice:" + m_rolls + " hits:" + m_hits;
}
}