From 1e969082ba000933affa67087f9afd2c5d9eb550 Mon Sep 17 00:00:00 2001 From: asvitkine Date: Wed, 5 Jul 2023 14:00:43 -0400 Subject: [PATCH 1/4] Fix stalemate logic to end stratBomber vs. transport fights. --- .../delegate/battle/MustFightBattle.java | 4 ++ .../steps/change/CheckGeneralBattleEnd.java | 69 ++++++++++++++----- 2 files changed, 55 insertions(+), 18 deletions(-) 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 bc549d713e1..a02f7dd5396 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 @@ -1301,6 +1301,10 @@ public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { throw new IllegalStateException( "Round 10,000 reached in a battle. Something must be wrong." + " Please report this to TripleA.\n" + + " Territory: " + + battleSite + + " Attacker: " + + attacker.getName() + " Attacking unit types: " + attackingUnits.stream() .map(Unit::getType) diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java index 96c789b1f69..b6bb391edfb 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java @@ -4,6 +4,7 @@ import static games.strategy.triplea.delegate.battle.BattleState.Side.OFFENSE; import static games.strategy.triplea.delegate.battle.BattleState.UnitBattleFilter.ALIVE; +import com.google.common.collect.Iterables; import games.strategy.engine.data.Unit; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.triplea.Properties; @@ -15,14 +16,19 @@ import games.strategy.triplea.delegate.battle.IBattle; import games.strategy.triplea.delegate.battle.steps.BattleStep; import games.strategy.triplea.delegate.battle.steps.RetreatChecks; +import games.strategy.triplea.delegate.battle.steps.fire.FiringGroup; import games.strategy.triplea.delegate.battle.steps.fire.general.FiringGroupSplitterGeneral; import games.strategy.triplea.delegate.power.calculator.CombatValueBuilder; import games.strategy.triplea.delegate.power.calculator.PowerStrengthAndRolls; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import lombok.AllArgsConstructor; +import org.triplea.java.collections.CollectionUtils; @AllArgsConstructor public class CheckGeneralBattleEnd implements BattleStep { @@ -54,10 +60,8 @@ public Order getOrder() { public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (hasSideLost(OFFENSE)) { battleActions.endBattle(IBattle.WhoWon.DEFENDER, bridge); - } else if (hasSideLost(DEFENSE)) { battleActions.endBattle(IBattle.WhoWon.ATTACKER, bridge); - } else if (isStalemate() && !canAttackerRetreatInStalemate()) { battleActions.endBattle(IBattle.WhoWon.DRAW, bridge); } @@ -68,18 +72,43 @@ protected boolean hasSideLost(final BattleState.Side side) { .noneMatch(Matches.unitIsNotInfrastructure()); } + private Predicate inAnyFiringGroup(Iterable firingGroups) { + return u -> + StreamSupport.stream(firingGroups.spliterator(), false) + .anyMatch(fg -> fg.getFiringUnits().contains(u)); + } + protected boolean isStalemate() { + if (battleState.getStatus().isLastRound()) { + return true; + } + final Iterable attackerFiringGroups = getAllFiringGroups(OFFENSE); + // Filter attackers to only units that are in firing groups, to eliminate units + // that can technically roll dice in abstract, but not against any current enemies. + final Collection attackers = + CollectionUtils.getMatches( + battleState.filterUnits(ALIVE, OFFENSE), inAnyFiringGroup(attackerFiringGroups)); + final Iterable defendersFiringGroups = getAllFiringGroups(DEFENSE); + // Filter defenders to only units that are in firing groups, to eliminate units + // that can technically roll dice in abstract, but not against any current enemies. + final Collection defenders = + CollectionUtils.getMatches( + battleState.filterUnits(ALIVE, DEFENSE), inAnyFiringGroup(defendersFiringGroups)); return battleState.getStatus().isLastRound() - || (hasNoStrengthOrRolls(OFFENSE) && hasNoStrengthOrRolls(DEFENSE)) - || (hasNoTargets(OFFENSE) && hasNoTargets(DEFENSE)); + || (hasNoStrengthOrRolls(OFFENSE, attackers, defenders) + && hasNoStrengthOrRolls(DEFENSE, defenders, attackers)) + || (hasNoTargets(attackerFiringGroups) && hasNoTargets(defendersFiringGroups)); } - private boolean hasNoStrengthOrRolls(final BattleState.Side side) { + private boolean hasNoStrengthOrRolls( + final BattleState.Side side, + final Collection myUnits, + final Collection enemyUnits) { return !PowerStrengthAndRolls.buildWithPreSortedUnits( - battleState.filterUnits(ALIVE, side), + myUnits, CombatValueBuilder.mainCombatValue() - .enemyUnits(battleState.filterUnits(ALIVE, side.getOpposite())) - .friendlyUnits(battleState.filterUnits(ALIVE, side)) + .enemyUnits(enemyUnits) + .friendlyUnits(myUnits) .side(side) .gameSequence(battleState.getGameData().getSequence()) .supportAttachments(battleState.getGameData().getUnitTypeList().getSupportRules()) @@ -87,18 +116,22 @@ private boolean hasNoStrengthOrRolls(final BattleState.Side side) { Properties.getLhtrHeavyBombers(battleState.getGameData().getProperties())) .gameDiceSides(battleState.getGameData().getDiceSides()) .territoryEffects(battleState.getTerritoryEffects()) - .build()) - .hasStrengthOrRolls(); + .build()).hasStrengthOrRolls(); + } + + private Iterable getAllFiringGroups(final BattleState.Side side) { + return Iterables.concat( + getFiringGroup(side, FiringGroupSplitterGeneral.Type.NORMAL), + getFiringGroup(side, FiringGroupSplitterGeneral.Type.FIRST_STRIKE)); + } + + private List getFiringGroup( + final BattleState.Side side, final FiringGroupSplitterGeneral.Type type) { + return FiringGroupSplitterGeneral.of(side, type, "stalemate").apply(battleState); } - private boolean hasNoTargets(final BattleState.Side side) { - return FiringGroupSplitterGeneral.of(side, FiringGroupSplitterGeneral.Type.NORMAL, "stalemate") - .apply(battleState) - .isEmpty() - && FiringGroupSplitterGeneral.of( - side, FiringGroupSplitterGeneral.Type.FIRST_STRIKE, "stalemate") - .apply(battleState) - .isEmpty(); + private boolean hasNoTargets(Iterable firingGroups) { + return Iterables.isEmpty(firingGroups); } protected boolean canAttackerRetreatInStalemate() { From 4d6e5ac02af61e8b89bcb6ffa4fce33f8a8a4014 Mon Sep 17 00:00:00 2001 From: asvitkine Date: Wed, 5 Jul 2023 17:25:44 -0400 Subject: [PATCH 2/4] Format. --- .../delegate/battle/steps/change/CheckGeneralBattleEnd.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java index b6bb391edfb..0b398343c14 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEnd.java @@ -116,7 +116,8 @@ private boolean hasNoStrengthOrRolls( Properties.getLhtrHeavyBombers(battleState.getGameData().getProperties())) .gameDiceSides(battleState.getGameData().getDiceSides()) .territoryEffects(battleState.getTerritoryEffects()) - .build()).hasStrengthOrRolls(); + .build()) + .hasStrengthOrRolls(); } private Iterable getAllFiringGroups(final BattleState.Side side) { From 90cb1a20957947118cf26977fd04e370656903c8 Mon Sep 17 00:00:00 2001 From: asvitkine Date: Wed, 5 Jul 2023 19:30:54 -0400 Subject: [PATCH 3/4] Fix existing tests. --- .../delegate/battle/FakeBattleState.java | 19 ++++++++++++------- .../change/CheckGeneralBattleEndTest.java | 17 +++++++++++++++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/FakeBattleState.java b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/FakeBattleState.java index ccfb7d61274..9d982e1d229 100644 --- a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/FakeBattleState.java +++ b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/FakeBattleState.java @@ -178,12 +178,8 @@ public List getStepStrings() { return List.of(); } - public static FakeBattleState.FakeBattleStateBuilder givenBattleStateBuilder() { - final GameData gameData = givenGameData().build(); - final GamePlayer attacker = mock(GamePlayer.class); - lenient().when(attacker.getData()).thenReturn(gameData); - final GamePlayer defender = mock(GamePlayer.class); - lenient().when(defender.getData()).thenReturn(gameData); + public static FakeBattleState.FakeBattleStateBuilder givenBattleStateBuilder( + final GamePlayer attacker, final GamePlayer defender) { return FakeBattleState.builder() .battleRound(2) .maxBattleRounds(-1) @@ -199,9 +195,18 @@ public static FakeBattleState.FakeBattleStateBuilder givenBattleStateBuilder() { .dependentUnits(List.of()) .killed(List.of()) .retreatUnits(new ArrayList<>()) - .gameData(gameData) + .gameData(attacker.getData()) .amphibious(false) .over(false) .attackerRetreatTerritories(List.of()); } + + public static FakeBattleState.FakeBattleStateBuilder givenBattleStateBuilder() { + final GameData gameData = givenGameData().build(); + final GamePlayer attacker = mock(GamePlayer.class); + lenient().when(attacker.getData()).thenReturn(gameData); + final GamePlayer defender = mock(GamePlayer.class); + lenient().when(defender.getData()).thenReturn(gameData); + return givenBattleStateBuilder(attacker, defender); + } } diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java index 42ae0c8a7f1..5babe4812ac 100644 --- a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java +++ b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java @@ -7,6 +7,7 @@ import static games.strategy.triplea.delegate.battle.steps.BattleStepsTest.givenUnitSeaTransport; import static games.strategy.triplea.delegate.battle.steps.MockGameData.givenGameData; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -24,6 +25,7 @@ import games.strategy.triplea.delegate.battle.IBattle; import java.util.List; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -38,6 +40,14 @@ class CheckGeneralBattleEndTest { @Mock GamePlayer attacker; @Mock GamePlayer defender; + @BeforeEach + void setUp() { + final GameData gameData = new GameData(); + + lenient().when(attacker.getData()).thenReturn(gameData); + lenient().when(defender.getData()).thenReturn(gameData); + } + private CheckGeneralBattleEnd givenCheckGeneralBattleEnd(final BattleState battleState) { return new CheckGeneralBattleEnd(battleState, battleActions); } @@ -133,9 +143,10 @@ void nobodyWinsIfBothHaveUnitsButMaxRound() { void battleIsNotDoneIfOffenseHasUnitWithPower() { final Unit attackerUnit = givenUnitWithAttackPower(attacker); final Unit defenderUnit = givenAnyUnit(); + lenient().when(defenderUnit.getOwner()).thenReturn(defender); final BattleState battleState = - givenBattleStateBuilder() + givenBattleStateBuilder(attacker, defender) .gameData(givenGameData().withDiceSides(6).build()) .attackingUnits(List.of(attackerUnit)) .defendingUnits(List.of(defenderUnit)) @@ -252,8 +263,10 @@ void stalemateRetreatPossibleIfOnlyNonCombatTransports() { givenBattleStateBuilder() .gameData( givenGameData() - .withLhtrHeavyBombers(false) + .withAlliedAirIndependent(false) + .withDefendingSuicideAndMunitionUnitsDoNotFire(false) .withTransportCasualtiesRestricted(true) + .withLhtrHeavyBombers(false) .build()) .attackingUnits(List.of(attackerUnit)) .defendingUnits(List.of(defenderUnit)) From 4fbe681ad584a90694e74e311e6ac53d1409d5f9 Mon Sep 17 00:00:00 2001 From: asvitkine Date: Wed, 5 Jul 2023 21:22:07 -0400 Subject: [PATCH 4/4] Add test coverage. --- .../change/CheckGeneralBattleEndTest.java | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java index 5babe4812ac..51dd61471d5 100644 --- a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java +++ b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/battle/steps/change/CheckGeneralBattleEndTest.java @@ -44,7 +44,9 @@ class CheckGeneralBattleEndTest { void setUp() { final GameData gameData = new GameData(); + lenient().when(attacker.getName()).thenReturn("attacker"); lenient().when(attacker.getData()).thenReturn(gameData); + lenient().when(defender.getName()).thenReturn("defender"); lenient().when(defender.getData()).thenReturn(gameData); } @@ -140,7 +142,7 @@ void nobodyWinsIfBothHaveUnitsButMaxRound() { } @Test - void battleIsNotDoneIfOffenseHasUnitWithPower() { + void battleDoesNotEndIfOffenseHasUnitWithPower() { final Unit attackerUnit = givenUnitWithAttackPower(attacker); final Unit defenderUnit = givenAnyUnit(); lenient().when(defenderUnit.getOwner()).thenReturn(defender); @@ -169,7 +171,7 @@ private Unit givenUnitWithAttackPower(final GamePlayer player) { } @Test - void battleIsNotDoneIfDefenseHasUnitWithPower() { + void battleDoesNotEndIfDefenseHasUnitWithPower() { final Unit attackerUnit = givenAnyUnit(); when(attackerUnit.getOwner()).thenReturn(attacker); final Unit defenderUnit = givenUnitWithDefensePower(defender); @@ -316,6 +318,51 @@ void nobodyWinsIfBothCanNotTargetEachOtherInGeneralCombat() { .endBattle(IBattle.WhoWon.DRAW, delegateBridge); } + @Test + void nobodyWinsIfBothCanNotTargetEachOtherInGeneralCombat2() { + final GameData gameData = givenGameData().withDiceSides(6).build(); + + final UnitType attackerUnitType = new UnitType("attacker", gameData); + final UnitAttachment attackerUnitAttachment = + new UnitAttachment("attacker", attackerUnitType, gameData); + attackerUnitAttachment.setAttack(0).setAttackRolls(0); + attackerUnitType.addAttachment(UNIT_ATTACHMENT_NAME, attackerUnitAttachment); + + final UnitType defenderUnitType = new UnitType("defender", gameData); + final UnitAttachment defenderUnitAttachment = + new UnitAttachment("defender", defenderUnitType, gameData); + defenderUnitAttachment.setDefense(1).setDefenseRolls(1); + // A unit that can't target attacker. + defenderUnitAttachment.setCanNotTarget(Set.of(attackerUnitType)); + defenderUnitType.addAttachment(UNIT_ATTACHMENT_NAME, defenderUnitAttachment); + + final UnitType defenderUnitType2 = new UnitType("defender2", gameData); + final UnitAttachment defenderUnitAttachment2 = + new UnitAttachment("defender2", defenderUnitType2, gameData); + // A unit that can target attacker, but has no defense power. + defenderUnitAttachment2.setDefense(0).setDefenseRolls(1); + defenderUnitType2.addAttachment(UNIT_ATTACHMENT_NAME, defenderUnitAttachment2); + + final Unit attackerUnit = attackerUnitType.createTemp(1, attacker).get(0); + final Unit defenderUnit = defenderUnitType.createTemp(1, defender).get(0); + final Unit defenderUnit2 = defenderUnitType2.createTemp(1, defender).get(0); + + final BattleState battleState = + givenBattleStateBuilder() + .gameData(gameData) + .attacker(attacker) + .defender(defender) + .attackingUnits(List.of(attackerUnit)) + .defendingUnits(List.of(defenderUnit, defenderUnit2)) + .build(); + + final CheckGeneralBattleEnd checkGeneralBattleEnd = givenCheckGeneralBattleEnd(battleState); + checkGeneralBattleEnd.execute(executionStack, delegateBridge); + + verify(battleActions, times(1).description("No one can hit each other so it is a stalemate")) + .endBattle(IBattle.WhoWon.DRAW, delegateBridge); + } + @Test void battleIsNotOverIfOffenseCanStillTargetInGeneralCombat() { final GameData gameData = givenGameData().withDiceSides(6).build();