Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix stalemate logic to end 0 att plane vs. transport fights. #11736

Merged
merged 4 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand All @@ -68,18 +72,43 @@ protected boolean hasSideLost(final BattleState.Side side) {
.noneMatch(Matches.unitIsNotInfrastructure());
}

private Predicate<Unit> inAnyFiringGroup(Iterable<FiringGroup> 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<FiringGroup> 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<Unit> attackers =
CollectionUtils.getMatches(
battleState.filterUnits(ALIVE, OFFENSE), inAnyFiringGroup(attackerFiringGroups));
final Iterable<FiringGroup> 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<Unit> 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<Unit> myUnits,
final Collection<Unit> 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())
Expand All @@ -91,14 +120,19 @@ private boolean hasNoStrengthOrRolls(final BattleState.Side side) {
.hasStrengthOrRolls();
}

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 Iterable<FiringGroup> getAllFiringGroups(final BattleState.Side side) {
return Iterables.concat(
getFiringGroup(side, FiringGroupSplitterGeneral.Type.NORMAL),
getFiringGroup(side, FiringGroupSplitterGeneral.Type.FIRST_STRIKE));
}

private List<FiringGroup> getFiringGroup(
final BattleState.Side side, final FiringGroupSplitterGeneral.Type type) {
return FiringGroupSplitterGeneral.of(side, type, "stalemate").apply(battleState);
}

private boolean hasNoTargets(Iterable<FiringGroup> firingGroups) {
return Iterables.isEmpty(firingGroups);
}

protected boolean canAttackerRetreatInStalemate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,8 @@ public List<String> 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)
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -38,6 +40,16 @@ class CheckGeneralBattleEndTest {
@Mock GamePlayer attacker;
@Mock GamePlayer defender;

@BeforeEach
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);
}

private CheckGeneralBattleEnd givenCheckGeneralBattleEnd(final BattleState battleState) {
return new CheckGeneralBattleEnd(battleState, battleActions);
}
Expand Down Expand Up @@ -130,12 +142,13 @@ void nobodyWinsIfBothHaveUnitsButMaxRound() {
}

@Test
void battleIsNotDoneIfOffenseHasUnitWithPower() {
void battleDoesNotEndIfOffenseHasUnitWithPower() {
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))
Expand All @@ -158,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);
Expand Down Expand Up @@ -252,8 +265,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))
Expand Down Expand Up @@ -303,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();
Expand Down