From c4c685f34a8666a9de712e85966cb2869c62b8b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:56:40 -0400 Subject: [PATCH 1/6] Bump com.diffplug.spotless from 6.20.0 to 6.21.0 (#11923) Bumps com.diffplug.spotless from 6.20.0 to 6.21.0. --- updated-dependencies: - dependency-name: com.diffplug.spotless dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3d506595f1e..2afa8463b33 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' id 'com.github.ben-manes.versions' version '0.47.0' id 'io.franzbecker.gradle-lombok' version '5.0.0' apply false - id 'com.diffplug.spotless' version '6.20.0' apply false + id 'com.diffplug.spotless' version '6.21.0' apply false } apply plugin: 'eclipse' From 42ed82d7c4d97e6aff02a6d6f60f36f185b038f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:56:52 -0400 Subject: [PATCH 2/6] Bump io.github.openfeign:feign-gson from 12.4 to 12.5 (#11922) Bumps [io.github.openfeign:feign-gson](https://github.com/openfeign/feign) from 12.4 to 12.5. - [Release notes](https://github.com/openfeign/feign/releases) - [Changelog](https://github.com/OpenFeign/feign/blob/master/CHANGELOG.md) - [Commits](https://github.com/openfeign/feign/compare/12.4...12.5) --- updated-dependencies: - dependency-name: io.github.openfeign:feign-gson dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2afa8463b33..569f5a30b85 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,7 @@ subprojects { dropwizardWebsocketsVersion = '1.3.14' equalsVerifierVersion = '3.15.1' feignCoreVersion = '12.4' - feignGsonVersion = '12.4' + feignGsonVersion = '12.5' javaWebSocketVersion = '1.5.3' gsonVersion = '2.10.1' guavaVersion = '32.1.2-jre' @@ -81,7 +81,7 @@ subprojects { junitPlatformLauncherVersion = '1.10.0' logbackClassicVersion = '1.2.11' mockitoVersion = '5.5.0' - openFeignVersion = '12.4' + openFeignVersion = '12.5' postgresqlVersion = '42.6.0' snakeYamlVersion = '2.7' sonatypeGoodiesPrefsVersion = '2.3.6' From b8122ac9b21a9ba23a671169caeb9727c2a616e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:57:27 -0400 Subject: [PATCH 3/6] Bump io.github.openfeign:feign-core from 12.4 to 12.5 (#11921) Bumps [io.github.openfeign:feign-core](https://github.com/openfeign/feign) from 12.4 to 12.5. - [Release notes](https://github.com/openfeign/feign/releases) - [Changelog](https://github.com/OpenFeign/feign/blob/master/CHANGELOG.md) - [Commits](https://github.com/openfeign/feign/compare/12.4...12.5) --- updated-dependencies: - dependency-name: io.github.openfeign:feign-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 569f5a30b85..39ae3f3e447 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ subprojects { dropwizardVersion = '2.1.0' dropwizardWebsocketsVersion = '1.3.14' equalsVerifierVersion = '3.15.1' - feignCoreVersion = '12.4' + feignCoreVersion = '12.5' feignGsonVersion = '12.5' javaWebSocketVersion = '1.5.3' gsonVersion = '2.10.1' From c9610e5f805124a8b567c568d559faf0cc7a5ead Mon Sep 17 00:00:00 2001 From: asvitkine Date: Wed, 30 Aug 2023 21:55:39 -0400 Subject: [PATCH 4/6] 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())); + } } } From 96f1679220d548febe24b10bf6ea7f36f9cf8ffc Mon Sep 17 00:00:00 2001 From: asvitkine Date: Sat, 2 Sep 2023 14:46:52 -0400 Subject: [PATCH 5/6] Fix runtime crash related to scrambling. (#11932) --- .../games/strategy/triplea/delegate/battle/AirBattle.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java index bff57ac71ee..c645b539755 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java @@ -99,7 +99,9 @@ public void updateDefendingUnits() { @Override public Change addAttackChange( final Route route, final Collection units, final Map> targets) { - attackingUnits.addAll(units); + // Avoid duplicates. Note: This is needed as scrambling code calls addAirBattle(), but the + // battle may have already been created with the same attackers. They should not be added twice. + units.stream().filter(not(attackingUnits::contains)).forEach(attackingUnits::add); return ChangeFactory.EMPTY_CHANGE; } From 8735de9b7dc5fa0ef78a9dd277da749c7e20194f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 00:49:51 -0400 Subject: [PATCH 6/6] Bump com.github.database-rider:rider-junit5 from 1.39.0 to 1.40.0 (#11933) Bumps [com.github.database-rider:rider-junit5](https://github.com/database-rider/database-rider) from 1.39.0 to 1.40.0. - [Release notes](https://github.com/database-rider/database-rider/releases) - [Changelog](https://github.com/database-rider/database-rider/blob/master/CHANGELOG.adoc) - [Commits](https://github.com/database-rider/database-rider/compare/1.39.0...1.40.0) --- updated-dependencies: - dependency-name: com.github.database-rider:rider-junit5 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 39ae3f3e447..e2d07f94e9b 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ subprojects { commonsIoVersion = '2.13.0' commonsMathVersion = '3.6.1' commonsTextVersion = '1.10.0' - databaseRiderVersion = '1.39.0' + databaseRiderVersion = '1.40.0' dropwizardVersion = '2.1.0' dropwizardWebsocketsVersion = '1.3.14' equalsVerifierVersion = '3.15.1'