From c9610e5f805124a8b567c568d559faf0cc7a5ead Mon Sep 17 00:00:00 2001 From: asvitkine Date: Wed, 30 Aug 2023 21:55:39 -0400 Subject: [PATCH] Improve logic for choosing default players in battle calc. (#11924) This attempts to address some regressions from 2.5 where in a number of cases, the chosen sides did not correspond to what the user likely wanted. See https://github.com/triplea-game/triplea/issues/11664 for details. Some additional tests are added, while others are updated to clarify their intentions and in some cases, provide updated expectations. --- .../games/strategy/triplea/UnitUtils.java | 18 + .../delegate/battle/AbstractBattle.java | 17 - .../delegate/battle/BattleDelegate.java | 3 +- .../delegate/battle/MustFightBattle.java | 3 +- .../AttackerAndDefenderSelector.java | 50 +- .../triplea/delegate/GameDataTestUtil.java | 6 +- .../AttackerAndDefenderSelectorTest.java | 764 ++++++++++-------- 7 files changed, 501 insertions(+), 360 deletions(-) diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/UnitUtils.java b/game-app/game-core/src/main/java/games/strategy/triplea/UnitUtils.java index b42e26e686a..a52a8eb29e8 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/UnitUtils.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/UnitUtils.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.function.Predicate; +import javax.annotation.Nullable; import lombok.experimental.UtilityClass; import org.triplea.java.collections.CollectionUtils; import org.triplea.java.collections.IntegerMap; @@ -241,4 +242,21 @@ private static CompositeChange translateHitPointsAndDamageToOtherUnit( } return unitChange; } + + public static @Nullable GamePlayer findPlayerWithMostUnits(final Iterable units) { + final IntegerMap playerUnitCount = new IntegerMap<>(); + for (final Unit unit : units) { + playerUnitCount.add(unit.getOwner(), 1); + } + int max = -1; + GamePlayer player = null; + for (final GamePlayer current : playerUnitCount.keySet()) { + final int count = playerUnitCount.getInt(current); + if (count > max) { + max = count; + player = current; + } + } + return player; + } } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AbstractBattle.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AbstractBattle.java index 73a988562f8..023a50936b6 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AbstractBattle.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AbstractBattle.java @@ -315,23 +315,6 @@ static GamePlayer findDefender( return defender; } - static GamePlayer findPlayerWithMostUnits(final Collection units) { - final IntegerMap playerUnitCount = new IntegerMap<>(); - for (final Unit unit : units) { - playerUnitCount.add(unit.getOwner(), 1); - } - int max = -1; - GamePlayer player = null; - for (final GamePlayer current : playerUnitCount.keySet()) { - final int count = playerUnitCount.getInt(current); - if (count > max) { - max = count; - player = current; - } - } - return player; - } - void markDamaged(final Collection damaged, final IDelegateBridge bridge) { BattleDelegate.markDamaged(damaged, bridge, battleSite); } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/BattleDelegate.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/BattleDelegate.java index 1188a070c1d..534dd5a5fbb 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/BattleDelegate.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/BattleDelegate.java @@ -24,6 +24,7 @@ import games.strategy.engine.player.Player; import games.strategy.engine.random.IRandomStats.DiceType; import games.strategy.triplea.Properties; +import games.strategy.triplea.UnitUtils; import games.strategy.triplea.attachments.PlayerAttachment; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; @@ -625,7 +626,7 @@ private static void setupTerritoriesAbandonedToTheEnemy( for (final Territory territory : battleTerritories) { final List abandonedToUnits = territory.getUnitCollection().getMatches(Matches.enemyUnit(player)); - final GamePlayer abandonedToPlayer = AbstractBattle.findPlayerWithMostUnits(abandonedToUnits); + final GamePlayer abandonedToPlayer = UnitUtils.findPlayerWithMostUnits(abandonedToUnits); // now make sure to add any units that must move with these units, so that they get included // as dependencies diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/MustFightBattle.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/MustFightBattle.java index 8ec6a9e37c4..dedeb5ed915 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/MustFightBattle.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/MustFightBattle.java @@ -28,6 +28,7 @@ import games.strategy.engine.history.change.units.TransformDamagedUnitsHistoryChange; import games.strategy.engine.player.Player; import games.strategy.triplea.Properties; +import games.strategy.triplea.UnitUtils; import games.strategy.triplea.delegate.ExecutionStack; import games.strategy.triplea.delegate.IExecutable; import games.strategy.triplea.delegate.Matches; @@ -1368,7 +1369,7 @@ private void defenderWins(final IDelegateBridge bridge) { battleSite.getUnitCollection().getMatches(Matches.unitIsNotInfrastructure()); if (!allyOfAttackerUnits.isEmpty()) { final GamePlayer abandonedToPlayer = - AbstractBattle.findPlayerWithMostUnits(allyOfAttackerUnits); + UnitUtils.findPlayerWithMostUnits(allyOfAttackerUnits); bridge .getHistoryWriter() .addChildToEvent( 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 90618a17152..37d4c551dcc 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,10 +4,12 @@ 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.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; @@ -33,10 +35,10 @@ public static class AttackerAndDefender { /** NONE = No attacker, no defender and no units. */ public static final AttackerAndDefender NONE = AttackerAndDefender.builder().build(); - @Nullable private final GamePlayer attacker; - @Nullable private final GamePlayer defender; - @Builder.Default private final List attackingUnits = List.of(); - @Builder.Default private final List defendingUnits = List.of(); + @Nullable GamePlayer attacker; + @Nullable GamePlayer defender; + @Builder.Default List attackingUnits = List.of(); + @Builder.Default List defendingUnits = List.of(); public Optional getAttacker() { return Optional.ofNullable(attacker); @@ -82,10 +84,11 @@ public AttackerAndDefender getAttackerAndDefender() { if (!territoryOwner.isNull()) { playersWithUnits.add(territoryOwner); } + final GamePlayer attacker = currentPlayer; // Attacker fights alone; the defender can also use all the allied units. final GamePlayer defender = - getOpponentWithPriorityList(attacker, playersWithUnits).orElse(null); + getOpponentWithPriorityList(territory, attacker, playersWithUnits).orElse(null); final List attackingUnits = territory.getUnitCollection().getMatches(Matches.unitIsOwnedBy(attacker)); final List defendingUnits = @@ -94,7 +97,7 @@ public AttackerAndDefender getAttackerAndDefender() { : territory.getUnitCollection().getMatches(Matches.alliedUnit(defender)); return AttackerAndDefender.builder() - .attacker(currentPlayer) + .attacker(attacker) .defender(defender) .attackingUnits(attackingUnits) .defendingUnits(defendingUnits) @@ -141,7 +144,8 @@ private AttackerAndDefender getAttackerAndDefenderWithPriorityList( return AttackerAndDefender.NONE; } // Defender - final GamePlayer defender = getOpponentWithPriorityList(attacker, priorityPlayers).orElse(null); + final GamePlayer defender = + getOpponentWithPriorityList(territory, attacker, priorityPlayers).orElse(null); return AttackerAndDefender.builder().attacker(attacker).defender(defender).build(); } @@ -150,7 +154,8 @@ private AttackerAndDefender getAttackerAndDefenderWithPriorityList( * 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. * - *

The opponent is chosen with the following priorities + *

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} @@ -165,7 +170,26 @@ private AttackerAndDefender getAttackerAndDefenderWithPriorityList( * @return an opponent. An empty optional is returned if the game has no players */ private Optional getOpponentWithPriorityList( - final GamePlayer player, final List priorityPlayers) { + 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; + } + } + } + } final Stream enemiesPriority = priorityPlayers.stream().filter(Matches.isAtWar(player)); final Stream neutralsPriority = @@ -173,6 +197,7 @@ private Optional getOpponentWithPriorityList( .filter(Matches.isAtWar(player).negate()) .filter(Matches.isAllied(player).negate()); return Stream.of( + Optional.ofNullable(bestDefender).stream(), enemiesPriority, playersAtWarWith(player), neutralsPriority, @@ -182,6 +207,13 @@ private Optional getOpponentWithPriorityList( .findFirst(); } + private @Nullable GamePlayer getEnemyWithMostUnits(Territory territory) { + return UnitUtils.findPlayerWithMostUnits( + territory.getUnits().stream() + .filter(Matches.unitIsEnemyOf(currentPlayer)) + .collect(Collectors.toList())); + } + /** * Returns a stream of all players which are at war with player {@code p}. * diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/GameDataTestUtil.java b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/GameDataTestUtil.java index cd4e87c13e3..0f96f056b2a 100644 --- a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/GameDataTestUtil.java +++ b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/GameDataTestUtil.java @@ -57,7 +57,7 @@ public static GamePlayer germany(final GameState data) { * * @return A italian PlayerId. */ - static GamePlayer italians(final GameState data) { + public static GamePlayer italians(final GameState data) { return data.getPlayerList().getPlayerId(Constants.PLAYER_NAME_ITALIANS); } @@ -105,6 +105,10 @@ public static GamePlayer britain(final GameState data) { return data.getPlayerList().getPlayerId("Britain"); } + public static GamePlayer french(final GameState data) { + return data.getPlayerList().getPlayerId(Constants.PLAYER_NAME_FRENCH); + } + /** * Get the japanese PlayerId for the given GameData object. * 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 a5c4f1409b8..3a2ded913d9 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 @@ -6,8 +6,10 @@ import static games.strategy.triplea.delegate.GameDataTestUtil.addTo; import static games.strategy.triplea.delegate.GameDataTestUtil.americans; import static games.strategy.triplea.delegate.GameDataTestUtil.british; +import static games.strategy.triplea.delegate.GameDataTestUtil.french; import static games.strategy.triplea.delegate.GameDataTestUtil.germans; import static games.strategy.triplea.delegate.GameDataTestUtil.infantry; +import static games.strategy.triplea.delegate.GameDataTestUtil.italians; import static games.strategy.triplea.delegate.GameDataTestUtil.japanese; import static games.strategy.triplea.delegate.GameDataTestUtil.russians; import static org.hamcrest.MatcherAssert.assertThat; @@ -26,165 +28,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class AttackerAndDefenderSelectorTest { - private final GameData gameData = TestMapGameData.REVISED.getGameData(); - private final GamePlayer russians = russians(gameData); - private final GamePlayer germans = germans(gameData); - private final GamePlayer british = british(gameData); - private final GamePlayer japanese = japanese(gameData); - private final GamePlayer americans = americans(gameData); - private final Territory germany = gameData.getMap().getTerritory("Germany"); - private final Territory japan = gameData.getMap().getTerritory("Japan"); - private final Territory unitedKingdom = gameData.getMap().getTerritory("United Kingdom"); - private final Territory seaZone32 = gameData.getMap().getTerritory("32 Sea Zone"); - private final Territory kenya = gameData.getMap().getTerritory("Kenya"); - private final Territory russia = gameData.getMap().getTerritory("Russia"); - - private final List players = - List.of( - russians, - germans, - british, - japanese, - americans, - gameData.getPlayerList().getNullPlayer()); - - @Test - void testNoCurrentPlayer() { - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(null) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(germany) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isEmpty()); - assertThat(attAndDef.getDefender(), isEmpty()); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat(attAndDef.getDefendingUnits(), is(empty())); - } - - @Test - void testNoTerritory() { - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(null) - .build(); - - // Algorithm only ensures "some enemy" as defender - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - final Optional attacker = attAndDef.getAttacker(); - final Optional defender = attAndDef.getDefender(); - assertThat(attacker, isPresentAndIs(russians)); - assertThat(defender, isPresent()); - assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat(attAndDef.getDefendingUnits(), is(empty())); - } - - @Test - void testSingleDefender1() { - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(germany) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); - // Fight in Germany -> Germans defend - assertThat(attAndDef.getDefender(), isPresentAndIs(germans)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat( - attAndDef.getDefendingUnits(), - containsInAnyOrder(germany.getUnitCollection().toArray(Unit[]::new))); - } - - @Test - void testSingleDefender2() { - // Fight in Japan -> Japans defend - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(japan) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); - assertThat(attAndDef.getDefender(), isPresentAndIs(japanese)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat( - attAndDef.getDefendingUnits(), - containsInAnyOrder(japan.getUnitCollection().toArray(Unit[]::new))); - } - - @Test - void testSingleDefender3() { - // Fight in Britain -> "some enemy" defends (British are allied) - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(unitedKingdom) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - final Optional attacker = attAndDef.getAttacker(); - final Optional defender = attAndDef.getDefender(); - assertThat(attacker, isPresentAndIs(russians)); - assertThat(defender, isPresent()); - assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat(attAndDef.getDefendingUnits(), is(empty())); - } - - @Test - void testMultipleDefenders1() { - // Fight in Germany -> 100 Japanese defend - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(germany) - .build(); - - addTo(germany, infantry(gameData).create(100, japanese)); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); - assertThat(attAndDef.getDefender(), isPresentAndIs(japanese)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat( - attAndDef.getDefendingUnits(), - containsInAnyOrder(filterTerritoryUnitsByOwner(germany, germans, japanese))); - } - private static Unit[] filterTerritoryUnitsByOwner( final Territory territory, final GamePlayer... gamePlayers) { final List units = new ArrayList<>(); @@ -194,189 +42,443 @@ private static Unit[] filterTerritoryUnitsByOwner( return units.toArray(Unit[]::new); } - @Test - void testMultipleDefenders2() { - // Fight in Japan -> 100 Germans defend - addTo(japan, infantry(gameData).create(100, germans)); - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(japan) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); - assertThat(attAndDef.getDefender(), isPresentAndIs(germans)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat( - attAndDef.getDefendingUnits(), - containsInAnyOrder(filterTerritoryUnitsByOwner(japan, japanese, germans))); - } + @Nested + public class Revised { + private final GameData gameData = TestMapGameData.REVISED.getGameData(); + private final GamePlayer russians = russians(gameData); + private final GamePlayer germans = germans(gameData); + private final GamePlayer british = british(gameData); + private final GamePlayer japanese = japanese(gameData); + private final GamePlayer americans = americans(gameData); + private final Territory germany = gameData.getMap().getTerritory("Germany"); + private final Territory japan = gameData.getMap().getTerritory("Japan"); + private final Territory unitedKingdom = gameData.getMap().getTerritory("United Kingdom"); + private final Territory seaZone32 = gameData.getMap().getTerritory("32 Sea Zone"); + private final Territory kenya = gameData.getMap().getTerritory("Kenya"); + private final Territory russia = gameData.getMap().getTerritory("Russia"); + + private final List players = + List.of( + russians, + germans, + british, + japanese, + americans, + gameData.getPlayerList().getNullPlayer()); + + @Test + void testNoCurrentPlayer() { + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(null) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(germany) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isEmpty()); + assertThat(attAndDef.getDefender(), isEmpty()); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat(attAndDef.getDefendingUnits(), is(empty())); + } - @Test - void testMultipleDefenders3() { - // Fight in Britain -> Germans defend (British & Americans are allied; Germans are the first - // enemy) - addTo(unitedKingdom, infantry(gameData).create(100, americans)); - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(unitedKingdom) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); - assertThat(attAndDef.getDefender(), isPresentAndIs(germans)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat(attAndDef.getDefendingUnits(), is(empty())); - } + @Test + void testNoTerritory() { + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(null) + .build(); + + // Algorithm only ensures "some enemy" as defender + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional attacker = attAndDef.getAttacker(); + final Optional defender = attAndDef.getDefender(); + assertThat(attacker, isPresentAndIs(russians)); + assertThat(defender, isPresent()); + assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat(attAndDef.getDefendingUnits(), is(empty())); + } - @Test - void testMixedDefendersAlliesAndEnemies1() { - // Fight in Germany -> Japanese defend, Americans are allied - addTo(germany, infantry(gameData).create(200, americans)); - addTo(germany, infantry(gameData).create(100, japanese)); - - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(germany) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); - assertThat(attAndDef.getDefender(), isPresentAndIs(japanese)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat( - attAndDef.getDefendingUnits(), - containsInAnyOrder(filterTerritoryUnitsByOwner(germany, germans, japanese))); - } + @Test + void testSingleDefender1() { + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(germany) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + // Fight in Germany -> Germans defend + assertThat(attAndDef.getDefender(), isPresentAndIs(germans)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat(attAndDef.getDefendingUnits(), is(germany.getUnits())); + } - @Test - void testMixedDefendersAlliesAndEnemies2() { - // Fight in Japan -> Japanese defend, Americans are allied - addTo(japan, infantry(gameData).create(100, americans)); - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(japan) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); - assertThat(attAndDef.getDefender(), isPresentAndIs(japanese)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat( - attAndDef.getDefendingUnits(), - containsInAnyOrder(filterTerritoryUnitsByOwner(japan, japanese))); - } + @Test + void testSingleDefender2() { + // Fight in Japan -> Japans defend + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(japan) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + assertThat(attAndDef.getDefender(), isPresentAndIs(japanese)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat(attAndDef.getDefendingUnits(), is(japan.getUnits())); + } - @Test - void testNoDefender() { - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(seaZone32) - .build(); - - // Algorithm only ensures "some enemy" as defender - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - final Optional attacker = attAndDef.getAttacker(); - final Optional defender = attAndDef.getDefender(); - assertThat(attacker, isPresentAndIs(russians)); - assertThat(defender, isPresent()); - assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); - assertThat(defender.orElseThrow(), is(not(gameData.getPlayerList().getNullPlayer()))); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat(attAndDef.getDefendingUnits(), is(empty())); - } + @Test + void testSingleDefender3() { + // Fight in Britain; Germany defends (British are allied) + // (Germany has some units there, but doesn't own the territory) + addTo(unitedKingdom, infantry(gameData).create(5, germans)); + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(unitedKingdom) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional attacker = attAndDef.getAttacker(); + final Optional defender = attAndDef.getDefender(); + assertThat(attacker, isPresentAndIs(russians)); + assertThat(defender, isPresentAndIs(germans)); + assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); + assertThat( + attAndDef.getAttackingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(unitedKingdom, russians))); + assertThat( + attAndDef.getDefendingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(unitedKingdom, germans))); + } - @Test - void testNoDefenderOnEnemyTerritory() { - // Fight in Kenya -> British (territory owner) defend - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(germans) // An Enemy of the British - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(kenya) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - assertThat(kenya.getOwner(), is(equalTo(british))); - assertThat(attAndDef.getAttacker(), isPresentAndIs(germans)); - assertThat(attAndDef.getDefender(), isPresentAndIs(british)); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat(attAndDef.getDefendingUnits(), is(empty())); - } + @Test + void testMultipleDefenders1() { + // Fight in Germany, Germany defends along with 100 Japanese units + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(germany) + .build(); + + addTo(germany, infantry(gameData).create(100, japanese)); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + assertThat(attAndDef.getDefender(), isPresentAndIs(germans)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat( + attAndDef.getDefendingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(germany, germans, japanese))); + } + + @Test + void testMultipleDefenders2() { + // Fight in Japan, Japanese defend along with 100 Germans units + addTo(japan, infantry(gameData).create(100, germans)); + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(japan) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + assertThat(attAndDef.getDefender(), isPresentAndIs(japanese)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat( + attAndDef.getDefendingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(japan, japanese, germans))); + } + + @Test + void testMultipleDefenders3() { + // Fight in Britain; British defends (British & Americans are allied) + addTo(unitedKingdom, infantry(gameData).create(5, americans)); + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(unitedKingdom) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + assertThat(attAndDef.getDefender(), isPresentAndIs(british)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat( + attAndDef.getDefendingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(unitedKingdom, british, americans))); + } + + @Test + void testMixedDefendersAlliesAndEnemies1() { + // Fight in Germany -> Germany defend along with Japanese units, Americans are allied + addTo(germany, infantry(gameData).create(200, americans)); + addTo(germany, infantry(gameData).create(100, japanese)); + + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(germany) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + assertThat(attAndDef.getDefender(), isPresentAndIs(germans)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat( + attAndDef.getDefendingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(germany, germans, japanese))); + } + + @Test + void testMixedDefendersAlliesAndEnemies2() { + // Fight in Japan -> Japanese defend, Americans are allied + addTo(japan, infantry(gameData).create(100, americans)); + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(japan) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(attAndDef.getAttacker(), isPresentAndIs(russians)); + assertThat(attAndDef.getDefender(), isPresentAndIs(japanese)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat( + attAndDef.getDefendingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(japan, japanese))); + } - @Test - void testNoDefenderAllPlayersAllied() { - // Every player is allied with every other player, i.e. there are no enemies. In this case the - // algorithm only ensures "some player" as defender. - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(List.of(russians, british, americans)) // only allies - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(seaZone32) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - final Optional attacker = attAndDef.getAttacker(); - final Optional defender = attAndDef.getDefender(); - assertThat(attacker, isPresentAndIs(russians)); - assertThat(defender, isPresent()); - assertThat(attAndDef.getAttackingUnits(), is(empty())); - assertThat(attAndDef.getDefendingUnits(), is(empty())); + @Test + void testNoDefender() { + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(seaZone32) + .build(); + + // Algorithm only ensures "some enemy" as defender + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional attacker = attAndDef.getAttacker(); + final Optional defender = attAndDef.getDefender(); + assertThat(attacker, isPresentAndIs(russians)); + assertThat(defender, isPresent()); + assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); + assertThat(defender.orElseThrow(), is(not(gameData.getPlayerList().getNullPlayer()))); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat(attAndDef.getDefendingUnits(), is(empty())); + } + + @Test + void testNoDefenderOnEnemyTerritory() { + // Fight in Kenya -> British (territory owner) defend + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(germans) // An Enemy of the British + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(kenya) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + assertThat(kenya.getOwner(), is(equalTo(british))); + assertThat(attAndDef.getAttacker(), isPresentAndIs(germans)); + assertThat(attAndDef.getDefender(), isPresentAndIs(british)); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat(attAndDef.getDefendingUnits(), is(empty())); + } + + @Test + void testNoDefenderAllPlayersAllied() { + // Every player is allied with every other player, i.e. there are no enemies. In this case the + // algorithm only ensures "some player" as defender. + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(List.of(russians, british, americans)) // only allies + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(seaZone32) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional attacker = attAndDef.getAttacker(); + final Optional defender = attAndDef.getDefender(); + assertThat(attacker, isPresentAndIs(russians)); + assertThat(defender, isPresent()); + assertThat(attAndDef.getAttackingUnits(), is(empty())); + assertThat(attAndDef.getDefendingUnits(), is(empty())); + } + + @Test + void testAttackOnOwnTerritory() { + // Fight in Russia, containing German and Russian units. Territory is under Russian control, + // even if there are German units present (e.g. can happen with e.g. limited combat rounds). + addTo(russia, infantry(gameData).create(10, germans)); + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(players) + .currentPlayer(russians) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(russia) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional attacker = attAndDef.getAttacker(); + final Optional defender = attAndDef.getDefender(); + assertThat(attacker, isPresentAndIs(russians)); + assertThat(defender, isPresentAndIs(germans)); + assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); + assertThat( + attAndDef.getAttackingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(russia, russians))); + assertThat( + attAndDef.getDefendingUnits(), + containsInAnyOrder(filterTerritoryUnitsByOwner(russia, germans))); + } } - @Test - void testAttackOnOwnTerritory() { - // Fight in Russia -> russian army attacks, "some enemy" defends - final AttackerAndDefenderSelector attackerAndDefenderSelector = - AttackerAndDefenderSelector.builder() - .players(players) - .currentPlayer(russians) - .relationshipTracker(gameData.getRelationshipTracker()) - .territory(russia) - .build(); - - final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = - attackerAndDefenderSelector.getAttackerAndDefender(); - - final Optional attacker = attAndDef.getAttacker(); - final Optional defender = attAndDef.getDefender(); - assertThat(attacker, isPresentAndIs(russians)); - assertThat(defender, isPresent()); - assertThat(attacker.orElseThrow().isAtWar(defender.orElseThrow()), is(true)); - assertThat( - attAndDef.getAttackingUnits(), - containsInAnyOrder(filterTerritoryUnitsByOwner(russia, russians))); - assertThat(attAndDef.getDefendingUnits(), is(empty())); + @Nested + public class Global { + private final GameData gameData = TestMapGameData.GLOBAL1940.getGameData(); + private final GamePlayer germans = germans(gameData); + private final GamePlayer italians = italians(gameData); + private final GamePlayer russians = russians(gameData); + private final GamePlayer british = british(gameData); + private final GamePlayer french = french(gameData); + private final Territory northernItaly = gameData.getMap().getTerritory("Northern Italy"); + private final Territory balticStates = gameData.getMap().getTerritory("Baltic States"); + private final Territory sz97 = gameData.getMap().getTerritory("97 Sea Zone"); + private final Territory uk = gameData.getMap().getTerritory("United Kingdom"); + + @Test + void alliedTerritory() { + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(gameData.getPlayerList().getPlayers()) + .currentPlayer(germans) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(northernItaly) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional defender = attAndDef.getDefender(); + assertThat(defender, isPresentAndIs(italians)); + assertThat(attAndDef.getDefendingUnits(), equalTo(northernItaly.getUnits())); + } + + @Test + void seaZoneWithAlliedUnits() { + assertThat(sz97.getUnits(), not(empty())); + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(gameData.getPlayerList().getPlayers()) + .currentPlayer(germans) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(sz97) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional defender = attAndDef.getDefender(); + assertThat(defender, isPresentAndIs(italians)); + assertThat(attAndDef.getDefendingUnits(), equalTo(sz97.getUnits())); + } + + @Test + void neutralTerritory() { + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(gameData.getPlayerList().getPlayers()) + .currentPlayer(germans) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(balticStates) + .build(); + + final AttackerAndDefenderSelector.AttackerAndDefender attAndDef = + attackerAndDefenderSelector.getAttackerAndDefender(); + + final Optional defender = attAndDef.getDefender(); + assertThat(defender, isPresentAndIs(russians)); + assertThat(attAndDef.getDefendingUnits(), equalTo(balticStates.getUnits())); + } + + @Test + void ownTerritoryWithAlliedUnits() { + final AttackerAndDefenderSelector attackerAndDefenderSelector = + AttackerAndDefenderSelector.builder() + .players(gameData.getPlayerList().getPlayers()) + .currentPlayer(british) + .relationshipTracker(gameData.getRelationshipTracker()) + .territory(uk) + .build(); + + 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.getDefendingUnits(), equalTo(uk.getUnits())); + } } }