From 4505bcecafdd0b776076cafc745cd124b605e09a Mon Sep 17 00:00:00 2001 From: frigoref Date: Sun, 4 Aug 2024 21:23:45 +0200 Subject: [PATCH] battleCalculator #1 (#12816) issue 12688 (battle calc on own territory shows own player as attacker and defender) Desired: 1. Attacker: Current player always, except when only allied units present (then next enemy) 2. Defender: As enemy to attacker with most units (fallback: whomever is at war and has the next turn) AttackerAndDefenderSelectorTest.java - simplification of method getAttackerAndDefender() - new methods getBestAttacker() and getBestAttacker() - removed now unused methods getOpponentWithPriorityList, playersAtWarWith, neutralPlayersTowards, getEnemyWithMostUnits UnitCollection.java - rename method getPlayersByUnitCount to getPlayersSortedByUnitCount BattleCalculatorDialog.java - extract common logic from method addAttackers and addDefenders to new method adjustBattleCalculatorPanel - extract static attribute modifications from instance method dispose into new static method disposeInstance (especially pack also for adding attacker) BattleCalculatorPanel.java - introduce new methods setAttackerWithUnits and setDefenderWithUnits to limit attacker/defender change to own instance - make setter of attribute attacker/defender private - rename methods hasAttackingUnitsAdded and hasDefendingUnitsAdded to hasAttackingUnits and hasDefendingUnits - rename attribute attackerUnitsTotalHitpoints to attackerUnitsTotalHitPoints --- .../strategy/engine/data/UnitCollection.java | 2 +- .../AttackerAndDefenderSelector.java | 229 +++++++----------- .../calculator/BattleCalculatorPanel.java | 40 +-- .../AttackerAndDefenderSelectorTest.java | 10 +- .../calculator/BattleCalculatorDialog.java | 96 +++++--- 5 files changed, 173 insertions(+), 204 deletions(-) diff --git a/game-app/game-core/src/main/java/games/strategy/engine/data/UnitCollection.java b/game-app/game-core/src/main/java/games/strategy/engine/data/UnitCollection.java index 3ddce516d6b..a4b7af12d8c 100644 --- a/game-app/game-core/src/main/java/games/strategy/engine/data/UnitCollection.java +++ b/game-app/game-core/src/main/java/games/strategy/engine/data/UnitCollection.java @@ -162,7 +162,7 @@ public IntegerMap getPlayerUnitCounts() { return count; } - public List getPlayersByUnitCount() { + public List getPlayersSortedByUnitCount() { final IntegerMap map = getPlayerUnitCounts(); final List players = new ArrayList<>(map.keySet()); players.sort(Comparator.comparingInt(map::getInt).reversed()); diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelector.java b/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelector.java index 063cbc4cd19..2e52b8148eb 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelector.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelector.java @@ -4,13 +4,11 @@ import games.strategy.engine.data.RelationshipTracker; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; -import games.strategy.triplea.UnitUtils; +import games.strategy.triplea.ai.pro.util.ProUtils; import games.strategy.triplea.delegate.Matches; import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.AccessLevel; @@ -54,44 +52,24 @@ public Optional getDefender() { @Nonnull private final RelationshipTracker relationshipTracker; @Nullable private final Territory territory; - /** - * Set initial attacker and defender. - * - *

Please read the source code for the order of the players and conditions involved. - */ + /** Set initial attacker as current player and defender according to priority. */ public AttackerAndDefender getAttackerAndDefender() { - // If there is no current player, we cannot choose an opponent. - if (currentPlayer == null) { + + final GamePlayer attacker = getBestAttacker(); + if (attacker == null) { return AttackerAndDefender.NONE; } - if (territory == null) { - // Without territory, we cannot prioritize any players (except current player); no units to - // select. - return getAttackerAndDefenderWithPriorityList(List.of(currentPlayer)); + // determine potential defenders (sub set of all players) + final GamePlayer defender = getBestDefender(attacker); + if (defender == null) { + return AttackerAndDefender.NONE; } - // Select the defender to be an enemy of the current player if possible, preferring enemies - // in the given territory. When deciding for an enemy, usually a player with more units is - // more important and more likely to be meant, e.g. a territory with 10 units of player A and - // 1 unit of player B. Thus, we use lists and ordered streams. - final List playersWithUnits = territory.getUnitCollection().getPlayersByUnitCount(); - // Add the territory owner add the end of the priority list. This way, when attacking an - // empty territory, the owner gets preferred even though they have no units in their land. In - // case the owner has units in the land, then they are already in the list but adding a second - // entry to the list doesn't impact the algorithm. - final GamePlayer territoryOwner = territory.getOwner(); - if (!territoryOwner.isNull()) { - playersWithUnits.add(territoryOwner); + if (territory == null) { + return AttackerAndDefender.builder().attacker(attacker).defender(defender).build(); } - - final GamePlayer attacker = currentPlayer; - // Attacker fights alone; the defender can also use all the allied units. - final GamePlayer defender = - getOpponentWithPriorityList(territory, attacker, playersWithUnits).orElse(null); final List attackingUnits = territory.getMatches(Matches.unitIsOwnedBy(attacker)); - final List defendingUnits = - defender == null ? List.of() : territory.getMatches(Matches.alliedUnit(defender)); - + final List defendingUnits = territory.getMatches(Matches.alliedUnit(defender)); return AttackerAndDefender.builder() .attacker(attacker) .defender(defender) @@ -101,131 +79,90 @@ public AttackerAndDefender getAttackerAndDefender() { } /** - * First pick an attacker and then a suitable defender while prioritising players in {@code - * priorityPlayers}. The order in {@code priorityPlayers} determines the priority for those - * players included in that list. Players not in the list are at the bottom without any order. + * Returns the "best" attacker, i.e. the current player or next enemy when there are only allied + * units. * - *

The attacker is picked with following priorities - * - *

    - *
  1. the players in {@code priorityPlayers} (in that order) - *
  2. any player - *
- * - *

The defender is chosen with the following priorities - * - *

    - *
  1. the first player in {@code priorityPlayers} who is an enemy of the attacker - *
  2. any enemy of the attacker - *
  3. the first player {@code priorityPlayers} who is neutral towards the attacker - *
  4. any neutral player (with respect to the attacker) - *
  5. any player - *
- * - *

If the game has no players, empty optionals are returned. - * - * @param priorityPlayers an ordered list of players which should be considered first - * @return attacker and defender + * @return best attacker */ - private AttackerAndDefender getAttackerAndDefenderWithPriorityList( - final List priorityPlayers) { - // Attacker - final GamePlayer attacker = - Stream.of(priorityPlayers.stream(), players.stream()) - .flatMap(s -> s) + @Nullable + private GamePlayer getBestAttacker() { + if (currentPlayer == null) { + return null; + } + if (territory != null) { + final Collection units = territory.getUnits(); + if (!units.isEmpty() + && units.stream().map(Unit::getOwner).allMatch(Matches.isAllied(currentPlayer))) { + return ProUtils.getEnemyPlayersInTurnOrder(currentPlayer).stream() .findFirst() - .orElse(null); - if (attacker == null) { - return AttackerAndDefender.NONE; + .orElseThrow(); + } } - // Defender - final GamePlayer defender = - getOpponentWithPriorityList(territory, attacker, priorityPlayers).orElse(null); - return AttackerAndDefender.builder().attacker(attacker).defender(defender).build(); + return currentPlayer; } /** - * Return a suitable opponent for player {@code p} with players in {@code priorityPlayers} given - * priority. The order in {@code priorityPlayers} determines the priority for those players - * included in that list. Players not in the list are at the bottom without any order. + * Returns the "best" defender, i.e. an enemy of the current player. If possible, prefers enemies + * in the given territory with the most units. * - *

Some additional prioritisation is given based on the territory owner and players with units. - * Otherwise, the opponent is chosen with the following priorities - * - *

    - *
  1. the first player in {@code priorityPlayers} who is an enemy of {@code p} - *
  2. any enemy of {@code p} - *
  3. the first player {@code priorityPlayers} who is neutral towards {@code p} - *
  4. any neutral player (with respect to {@code p}) - *
  5. any player - *
- * - * @param player the player to find an opponent for - * @param priorityPlayers an ordered list of players which should be considered first - * @return an opponent. An empty optional is returned if the game has no players + * @return best defender */ - private Optional getOpponentWithPriorityList( - Territory territory, final GamePlayer player, final List priorityPlayers) { - GamePlayer bestDefender = null; - // Handle some special cases that the priority ordering logic doesn't handle. See tests. - if (territory != null) { - if (territory.isWater()) { - bestDefender = getEnemyWithMostUnits(territory); - if (bestDefender == null) { - bestDefender = UnitUtils.findPlayerWithMostUnits(territory.getUnits()); - } - } else { - bestDefender = territory.getOwner(); - // If we're not at war with the owner and there are enemies, fight them. - if (!bestDefender.isAtWar(currentPlayer)) { - GamePlayer enemyWithMostUnits = getEnemyWithMostUnits(territory); - if (enemyWithMostUnits != null) { - bestDefender = enemyWithMostUnits; - } - } - } + @Nullable + private GamePlayer getBestDefender(final GamePlayer attacker) { + assert currentPlayer + != null; // should not occur checked in calling method getAttackerAndDefender + + final List potentialDefenders = ProUtils.getEnemyPlayers(attacker); + if (potentialDefenders.isEmpty()) { + return null; } - final Stream enemiesPriority = - priorityPlayers.stream().filter(Matches.isAtWar(player)); - final Stream neutralsPriority = - priorityPlayers.stream() - .filter(Matches.isAtWar(player).negate()) - .filter(Matches.isAllied(player).negate()); - return Stream.of( - Optional.ofNullable(bestDefender).stream(), - enemiesPriority, - playersAtWarWith(player), - neutralsPriority, - neutralPlayersTowards(player), - players.stream()) - .flatMap(s -> s) - .findFirst(); - } + // determine next player after current player who could be a defender + final GamePlayer nextPlayerAsDefender = + ProUtils.getOtherPlayersInTurnOrder(attacker).stream() + .filter(potentialDefenders::contains) + .findFirst() + .orElseThrow(); - private @Nullable GamePlayer getEnemyWithMostUnits(Territory territory) { - return UnitUtils.findPlayerWithMostUnits( - territory.getUnits().stream() - .filter(Matches.unitIsEnemyOf(currentPlayer)) - .collect(Collectors.toList())); - } + if (territory == null) { + // Without territory, we cannot prioritize defender by number of units + return nextPlayerAsDefender; + } - /** - * Returns a stream of all players which are at war with player {@code p}. - * - *

The returned stream might be empty. - */ - private Stream playersAtWarWith(final GamePlayer p) { - return players.stream().filter(Matches.isAtWar(p)); + return getBestDefenderFromTerritory(potentialDefenders, nextPlayerAsDefender); } - /** - * Returns a stream of all players which are neither allied nor at war with player {@code p}. - * - *

The returned stream might be empty. - */ - private Stream neutralPlayersTowards(final GamePlayer p) { - return players.stream() - .filter(Matches.isAtWar(p).negate()) - .filter(Matches.isAllied(p).negate()); + private GamePlayer getBestDefenderFromTerritory( + final List potentialDefenders, final GamePlayer nextPlayerAsDefender) { + // Select the defender to be an enemy of the current player, if possible, prefer enemies + // in the given territory. + assert territory != null; // should not occur as checked in calling method getBestDefender + + final GamePlayer territoryOwner = territory.getOwner(); + if (!territoryOwner.isNull() && potentialDefenders.contains(territoryOwner)) { + // In case the owner is a potential defender and has units in the land it's the defender + if (territory.getUnits().stream() + .map(Unit::getOwner) + .filter(territoryOwner::equals) + .findAny() + .isPresent()) { + return territoryOwner; + } + } + + // When deciding for an enemy, usually a player having more units is + // more important and more likely to be meant, e.g. a territory with 10 units of player A + // and 1 unit of player B. Thus, we use lists and ordered streams. + final List sortedPlayersForDefender = + territory.getUnitCollection().getPlayersSortedByUnitCount(); + + // Add the territory owner at the end of the priority list. This way, when attacking an + // empty territory, the owner gets preferred even though they have no units in their land. + sortedPlayersForDefender.add(territoryOwner); + + sortedPlayersForDefender.removeIf(p -> !potentialDefenders.contains(p)); + sortedPlayersForDefender.add(nextPlayerAsDefender); + + // Attacker fights alone for this selector; the defender can also use all the allied units. + return sortedPlayersForDefender.get(0); } } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorPanel.java b/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorPanel.java index c4c61f1a3ba..2d76a2b69a0 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorPanel.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorPanel.java @@ -97,8 +97,8 @@ class BattleCalculatorPanel extends JPanel { private final JLabel defenderUnitsTotalNumber = new JLabel(); private final JLabel attackerUnitsTotalTuv = new JLabel(); private final JLabel defenderUnitsTotalTuv = new JLabel(); - private final JLabel attackerUnitsTotalHitpoints = new JLabel(); - private final JLabel defenderUnitsTotalHitpoints = new JLabel(); + private final JLabel attackerUnitsTotalHitPoints = new JLabel(); + private final JLabel defenderUnitsTotalHitPoints = new JLabel(); private final JLabel attackerUnitsTotalPower = new JLabel(); private final JLabel defenderUnitsTotalPower = new JLabel(); private String attackerOrderOfLosses = null; @@ -223,12 +223,12 @@ class BattleCalculatorPanel extends JPanel { attackAndDefend.add( defenderUnitsTotalTuv, builder0.gridX(3).insets(0, gap / 2, 0, gap * 2).build()); attackAndDefend.add( - attackerUnitsTotalHitpoints, + attackerUnitsTotalHitPoints, builder0.gridY(row0).gridX(0).insets(0, gap, gap / 2, 0).build()); attackAndDefend.add( attackerUnitsTotalPower, builder0.gridX(1).insets(0, gap / 2, gap / 2, gap * 2).build()); attackAndDefend.add( - defenderUnitsTotalHitpoints, builder0.gridX(2).insets(0, gap, gap / 2, 0).build()); + defenderUnitsTotalHitPoints, builder0.gridX(2).insets(0, gap, gap / 2, 0).build()); attackAndDefend.add( defenderUnitsTotalPower, builder0.gridX(3).insets(0, gap / 2, gap / 2, gap * 2).build()); final JPanel attackAndDefendAlignLeft = new JPanel(); @@ -411,7 +411,7 @@ class BattleCalculatorPanel extends JPanel { attackerOrderOfLosses = null; defenderOrderOfLosses = null; final GamePlayer newAttacker = getDefender(); - final List newAttackers = + final List newAttackerUnits = CollectionUtils.getMatches( defendingUnitsPanel.getUnits(), Matches.unitIsOwnedBy(getDefender()) @@ -424,14 +424,12 @@ class BattleCalculatorPanel extends JPanel { true, List.of()))); final GamePlayer newDefender = getAttacker(); - final List newDefenders = + final List newDefenderUnits = CollectionUtils.getMatches( attackingUnitsPanel.getUnits(), Matches.unitCanBeInBattle(true, isLandBattle(), 1, true)); - setAttacker(newAttacker); - setDefender(newDefender); - setAttackingUnits(newAttackers); - setDefendingUnits(newDefenders); + setAttackerWithUnits(newAttacker, newAttackerUnits); + setDefenderWithUnits(newDefender, newDefenderUnits); setWidgetActivation(); }); orderOfLossesButton.addActionListener( @@ -516,7 +514,7 @@ GamePlayer getAttacker() { return (GamePlayer) attackerCombo.getSelectedItem(); } - void setAttacker(final GamePlayer gamePlayer) { + private void setAttacker(final GamePlayer gamePlayer) { attackerCombo.setSelectedItem(gamePlayer); } @@ -524,7 +522,7 @@ GamePlayer getDefender() { return (GamePlayer) defenderCombo.getSelectedItem(); } - void setDefender(final GamePlayer gamePlayer) { + private void setDefender(final GamePlayer gamePlayer) { defenderCombo.setSelectedItem(gamePlayer); } @@ -684,6 +682,11 @@ private static String formatValue(final double value) { return new DecimalFormat("#0.##").format(value); } + public void setAttackerWithUnits(final GamePlayer attacker, final List initialUnits) { + setAttacker(attacker); + setAttackingUnits(initialUnits); + } + void addAttackingUnits(final List unitsToAdd) { final List units = attackingUnitsPanel.getUnits(); units.addAll(unitsToAdd); @@ -703,6 +706,11 @@ true, isLandBattle(), 1, hasMaxRounds(isLandBattle(), data), false, List.of())), location); } + public void setDefenderWithUnits(final GamePlayer defender, final List initialUnits) { + setDefender(defender); + setDefendingUnits(initialUnits); + } + void addDefendingUnits(final List unitsToAdd) { final List units = defendingUnitsPanel.getUnits(); units.addAll(unitsToAdd); @@ -720,11 +728,11 @@ private void setDefendingUnits(final List initialUnits) { location); } - public boolean hasAttackingUnitsAdded() { + public boolean hasAttackingUnits() { return !attackingUnitsPanel.isEmpty(); } - public boolean hasDefendingUnitsAdded() { + public boolean hasDefendingUnits() { return !defendingUnitsPanel.isEmpty(); } @@ -770,8 +778,8 @@ private void setWidgetActivation() { defenders, getDefender(), tuvCalculator.getCostsForTuv(getDefender()), data)); final int attackHitPoints = CasualtyUtil.getTotalHitpointsLeft(attackers); final int defenseHitPoints = CasualtyUtil.getTotalHitpointsLeft(defenders); - attackerUnitsTotalHitpoints.setText("HP: " + attackHitPoints); - defenderUnitsTotalHitpoints.setText("HP: " + defenseHitPoints); + attackerUnitsTotalHitPoints.setText("HP: " + attackHitPoints); + defenderUnitsTotalHitPoints.setText("HP: " + defenseHitPoints); final Collection territoryEffects = getTerritoryEffects(); if (isAmphibiousBattle()) { attackers.stream() diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelectorTest.java b/game-app/game-core/src/test/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelectorTest.java index 3a2ded913d9..905f2f147b2 100644 --- a/game-app/game-core/src/test/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelectorTest.java +++ b/game-app/game-core/src/test/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelectorTest.java @@ -239,7 +239,7 @@ void testMultipleDefenders3() { final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = attackerAndDefenderSelector.getAttackerAndDefender(); - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + assertThat(attAndDef.getAttacker(), isPresentAndIs(germans)); // case: all units allied assertThat(attAndDef.getDefender(), isPresentAndIs(british)); assertThat(attAndDef.getAttackingUnits(), is(empty())); assertThat( @@ -439,7 +439,7 @@ void seaZoneWithAlliedUnits() { attackerAndDefenderSelector.getAttackerAndDefender(); final Optional defender = attAndDef.getDefender(); - assertThat(defender, isPresentAndIs(italians)); + assertThat(defender, isPresentAndIs(italians)); // only italy units present assertThat(attAndDef.getDefendingUnits(), equalTo(sz97.getUnits())); } @@ -474,11 +474,13 @@ void ownTerritoryWithAlliedUnits() { final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = attackerAndDefenderSelector.getAttackerAndDefender(); - final Optional defender = attAndDef.getDefender(); - assertThat(defender, isPresentAndIs(british)); assertThat(uk.getUnits().stream().anyMatch(Matches.unitIsOwnedBy(french)), is(true)); assertThat(uk.getUnits().stream().anyMatch(Matches.unitIsOwnedBy(british)), is(true)); + assertThat(attAndDef.getDefender(), isPresentAndIs(british)); // only allied units assertThat(attAndDef.getDefendingUnits(), equalTo(uk.getUnits())); + assertThat( + attAndDef.getAttacker(), isPresentAndIs(germans)); // next in turn after current init step + assertThat(attAndDef.getAttackingUnits(), is(empty())); } } } diff --git a/game-app/game-headed/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorDialog.java b/game-app/game-headed/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorDialog.java index 0fd0797dd8a..bd406712528 100644 --- a/game-app/game-headed/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorDialog.java +++ b/game-app/game-headed/src/main/java/games/strategy/triplea/odds/calculator/BattleCalculatorDialog.java @@ -16,11 +16,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; -import org.triplea.java.collections.CollectionUtils; import org.triplea.java.collections.IntegerMap; import org.triplea.swing.key.binding.KeyCode; import org.triplea.swing.key.binding.SwingKeyBinding; @@ -103,59 +104,80 @@ public void componentResized(final ComponentEvent e) { // existing battle calculator dialog. Oddly enough, calling toFront() directly here (before or // after setVisible(true)) has no effect either, but delaying the call to the end of the queue // of the Event Dispatch Thread solves the issue (though you can see the new dialog in the - // background for an blink of an eye). + // background for a blink of an eye). // Tested with Cinnamon Desktop 4.8.5. SwingUtilities.invokeLater(dialog::toFront); taFrame.getUiContext().addShutdownWindow(dialog); } - public static void addAttackers(final Territory t) { + private static void adjustBattleCalculatorPanel( + final Territory t, Consumer battleCalculatorPanelConsumer) { if (instances.isEmpty() || t == null) { return; } - final BattleCalculatorPanel currentPanel = instances.get(instances.size() - 1).panel; - // if there are no units set on the battle calculator panel, and if there are no units - // matching the current attacker, then we'll set the attacker to be the enemy player - // with the most units in the selected territory. - if (!currentPanel.hasAttackingUnitsAdded() - && t.getUnitCollection().stream() - .noneMatch(Matches.unitIsOwnedBy(currentPanel.getAttacker()))) { - // Find possible attackers (enemies) of the current defender. - // Count how many units each one has and find the max. - final List units = t.getMatches(Matches.enemyUnit(currentPanel.getDefender())); + final BattleCalculatorDialog currentDialog = instances.get(instances.size() - 1); + battleCalculatorPanelConsumer.accept(currentDialog.panel); + currentDialog.pack(); + } - final GamePlayer gamePlayer = new IntegerMap<>(units, Unit::getOwner).maxKey(); - if (gamePlayer != null) { - currentPanel.setAttacker(gamePlayer); - } - } - currentPanel.addAttackingUnits(t.getMatches(Matches.unitIsOwnedBy(currentPanel.getAttacker()))); + public static void addAttackers(final Territory t) { + adjustBattleCalculatorPanel( + t, + panel -> { + // if there are no units set on the battle calculator panel yet, + // then we'll determine the attacker to be the defender's enemy player + // with the most units in the selected territory. + if (panel.hasAttackingUnits()) { + panel.addAttackingUnits(t.getMatches(Matches.unitIsOwnedBy(panel.getAttacker()))); + } else { + // Find possible attacker (enemy) units for the current defender. + final List units = + t.getUnitCollection().stream() + .filter(Matches.enemyUnit(panel.getDefender())) + .collect(Collectors.toList()); + + if (!units.isEmpty()) { + // Count how many units each one has and find the max to update the panel + final IntegerMap unitCountMap = new IntegerMap<>(units, Unit::getOwner); + final GamePlayer newAttacker = unitCountMap.maxKey(); + final List attackingUnits = + units.stream() + .filter(Matches.unitIsOwnedBy(newAttacker)) + .collect(Collectors.toList()); + panel.setAttackerWithUnits(newAttacker, attackingUnits); + } + } + }); } public static void addDefenders(final Territory t) { - if (instances.isEmpty() || t == null) { - return; - } - final BattleCalculatorDialog currentDialog = instances.get(instances.size() - 1); - - // if there are no units added to the dialog, then we'll automatically - // select the defending side to match any unit in the current territory - if (!currentDialog.panel.hasAttackingUnitsAdded() - && !currentDialog.panel.hasDefendingUnitsAdded()) { - Optional.ofNullable(CollectionUtils.getAny(t.getUnitCollection())) - .map(Unit::getOwner) - .ifPresent(currentDialog.panel::setDefender); - } - currentDialog.panel.addDefendingUnits( - t.getMatches(Matches.alliedUnit(currentDialog.panel.getDefender()))); - currentDialog.pack(); + adjustBattleCalculatorPanel( + t, + panel -> { + // if there are no units added to the dialog, then we'll automatically + // select the defending side to match any unit in the current territory + if (!panel.hasAttackingUnits() && !panel.hasDefendingUnits()) { + final Optional defender = + t.getUnitCollection().stream().map(Unit::getOwner).findAny(); + if (defender.isPresent()) { + panel.setDefenderWithUnits( + defender.get(), t.getMatches(Matches.alliedUnit(defender.get()))); + } + } else { + panel.addDefendingUnits(t.getMatches(Matches.alliedUnit(panel.getDefender()))); + } + }); } @Override public void dispose() { - instances.remove(this); - lastPosition = new Point(getLocation()); + disposeInstance(this); super.dispose(); } + + private static synchronized void disposeInstance(BattleCalculatorDialog currentDialog) { + instances.remove(currentDialog); + lastPosition = new Point(currentDialog.getLocation()); + } }