Skip to content

Commit 4505bce

Browse files
authored
battleCalculator #1 (#12816)
issue 12688 (battle calc on own territory shows own player as attacker and defender) Desired: 1. Attacker: Current player always, except when only allied units present (then next enemy) 2. Defender: As enemy to attacker with most units (fallback: whomever is at war and has the next turn) AttackerAndDefenderSelectorTest.java - simplification of method getAttackerAndDefender() - new methods getBestAttacker() and getBestAttacker() - removed now unused methods getOpponentWithPriorityList, playersAtWarWith, neutralPlayersTowards, getEnemyWithMostUnits UnitCollection.java - rename method getPlayersByUnitCount to getPlayersSortedByUnitCount BattleCalculatorDialog.java - extract common logic from method addAttackers and addDefenders to new method adjustBattleCalculatorPanel - extract static attribute modifications from instance method dispose into new static method disposeInstance (especially pack also for adding attacker) BattleCalculatorPanel.java - introduce new methods setAttackerWithUnits and setDefenderWithUnits to limit attacker/defender change to own instance - make setter of attribute attacker/defender private - rename methods hasAttackingUnitsAdded and hasDefendingUnitsAdded to hasAttackingUnits and hasDefendingUnits - rename attribute attackerUnitsTotalHitpoints to attackerUnitsTotalHitPoints
1 parent ba0653d commit 4505bce

File tree

5 files changed

+173
-204
lines changed

5 files changed

+173
-204
lines changed

game-app/game-core/src/main/java/games/strategy/engine/data/UnitCollection.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public IntegerMap<GamePlayer> getPlayerUnitCounts() {
162162
return count;
163163
}
164164

165-
public List<GamePlayer> getPlayersByUnitCount() {
165+
public List<GamePlayer> getPlayersSortedByUnitCount() {
166166
final IntegerMap<GamePlayer> map = getPlayerUnitCounts();
167167
final List<GamePlayer> players = new ArrayList<>(map.keySet());
168168
players.sort(Comparator.comparingInt(map::getInt).reversed());

game-app/game-core/src/main/java/games/strategy/triplea/odds/calculator/AttackerAndDefenderSelector.java

+83-146
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
import games.strategy.engine.data.RelationshipTracker;
55
import games.strategy.engine.data.Territory;
66
import games.strategy.engine.data.Unit;
7-
import games.strategy.triplea.UnitUtils;
7+
import games.strategy.triplea.ai.pro.util.ProUtils;
88
import games.strategy.triplea.delegate.Matches;
99
import java.util.Collection;
1010
import java.util.List;
1111
import java.util.Optional;
12-
import java.util.stream.Collectors;
13-
import java.util.stream.Stream;
1412
import javax.annotation.Nonnull;
1513
import javax.annotation.Nullable;
1614
import lombok.AccessLevel;
@@ -54,44 +52,24 @@ public Optional<GamePlayer> getDefender() {
5452
@Nonnull private final RelationshipTracker relationshipTracker;
5553
@Nullable private final Territory territory;
5654

57-
/**
58-
* Set initial attacker and defender.
59-
*
60-
* <p>Please read the source code for the order of the players and conditions involved.
61-
*/
55+
/** Set initial attacker as current player and defender according to priority. */
6256
public AttackerAndDefender getAttackerAndDefender() {
63-
// If there is no current player, we cannot choose an opponent.
64-
if (currentPlayer == null) {
57+
58+
final GamePlayer attacker = getBestAttacker();
59+
if (attacker == null) {
6560
return AttackerAndDefender.NONE;
6661
}
6762

68-
if (territory == null) {
69-
// Without territory, we cannot prioritize any players (except current player); no units to
70-
// select.
71-
return getAttackerAndDefenderWithPriorityList(List.of(currentPlayer));
63+
// determine potential defenders (sub set of all players)
64+
final GamePlayer defender = getBestDefender(attacker);
65+
if (defender == null) {
66+
return AttackerAndDefender.NONE;
7267
}
73-
// Select the defender to be an enemy of the current player if possible, preferring enemies
74-
// in the given territory. When deciding for an enemy, usually a player with more units is
75-
// more important and more likely to be meant, e.g. a territory with 10 units of player A and
76-
// 1 unit of player B. Thus, we use lists and ordered streams.
77-
final List<GamePlayer> playersWithUnits = territory.getUnitCollection().getPlayersByUnitCount();
78-
// Add the territory owner add the end of the priority list. This way, when attacking an
79-
// empty territory, the owner gets preferred even though they have no units in their land. In
80-
// case the owner has units in the land, then they are already in the list but adding a second
81-
// entry to the list doesn't impact the algorithm.
82-
final GamePlayer territoryOwner = territory.getOwner();
83-
if (!territoryOwner.isNull()) {
84-
playersWithUnits.add(territoryOwner);
68+
if (territory == null) {
69+
return AttackerAndDefender.builder().attacker(attacker).defender(defender).build();
8570
}
86-
87-
final GamePlayer attacker = currentPlayer;
88-
// Attacker fights alone; the defender can also use all the allied units.
89-
final GamePlayer defender =
90-
getOpponentWithPriorityList(territory, attacker, playersWithUnits).orElse(null);
9171
final List<Unit> attackingUnits = territory.getMatches(Matches.unitIsOwnedBy(attacker));
92-
final List<Unit> defendingUnits =
93-
defender == null ? List.of() : territory.getMatches(Matches.alliedUnit(defender));
94-
72+
final List<Unit> defendingUnits = territory.getMatches(Matches.alliedUnit(defender));
9573
return AttackerAndDefender.builder()
9674
.attacker(attacker)
9775
.defender(defender)
@@ -101,131 +79,90 @@ public AttackerAndDefender getAttackerAndDefender() {
10179
}
10280

10381
/**
104-
* First pick an attacker and then a suitable defender while prioritising players in {@code
105-
* priorityPlayers}. The order in {@code priorityPlayers} determines the priority for those
106-
* players included in that list. Players not in the list are at the bottom without any order.
82+
* Returns the "best" attacker, i.e. the current player or next enemy when there are only allied
83+
* units.
10784
*
108-
* <p>The attacker is picked with following priorities
109-
*
110-
* <ol>
111-
* <li>the players in {@code priorityPlayers} (in that order)
112-
* <li>any player
113-
* </ol>
114-
*
115-
* <p>The defender is chosen with the following priorities
116-
*
117-
* <ol>
118-
* <li>the first player in {@code priorityPlayers} who is an enemy of the attacker
119-
* <li>any enemy of the attacker
120-
* <li>the first player {@code priorityPlayers} who is neutral towards the attacker
121-
* <li>any neutral player (with respect to the attacker)
122-
* <li>any player
123-
* </ol>
124-
*
125-
* <p>If the game has no players, empty optionals are returned.
126-
*
127-
* @param priorityPlayers an ordered list of players which should be considered first
128-
* @return attacker and defender
85+
* @return best attacker
12986
*/
130-
private AttackerAndDefender getAttackerAndDefenderWithPriorityList(
131-
final List<GamePlayer> priorityPlayers) {
132-
// Attacker
133-
final GamePlayer attacker =
134-
Stream.of(priorityPlayers.stream(), players.stream())
135-
.flatMap(s -> s)
87+
@Nullable
88+
private GamePlayer getBestAttacker() {
89+
if (currentPlayer == null) {
90+
return null;
91+
}
92+
if (territory != null) {
93+
final Collection<Unit> units = territory.getUnits();
94+
if (!units.isEmpty()
95+
&& units.stream().map(Unit::getOwner).allMatch(Matches.isAllied(currentPlayer))) {
96+
return ProUtils.getEnemyPlayersInTurnOrder(currentPlayer).stream()
13697
.findFirst()
137-
.orElse(null);
138-
if (attacker == null) {
139-
return AttackerAndDefender.NONE;
98+
.orElseThrow();
99+
}
140100
}
141-
// Defender
142-
final GamePlayer defender =
143-
getOpponentWithPriorityList(territory, attacker, priorityPlayers).orElse(null);
144-
return AttackerAndDefender.builder().attacker(attacker).defender(defender).build();
101+
return currentPlayer;
145102
}
146103

147104
/**
148-
* Return a suitable opponent for player {@code p} with players in {@code priorityPlayers} given
149-
* priority. The order in {@code priorityPlayers} determines the priority for those players
150-
* included in that list. Players not in the list are at the bottom without any order.
105+
* Returns the "best" defender, i.e. an enemy of the current player. If possible, prefers enemies
106+
* in the given territory with the most units.
151107
*
152-
* <p>Some additional prioritisation is given based on the territory owner and players with units.
153-
* Otherwise, the opponent is chosen with the following priorities
154-
*
155-
* <ol>
156-
* <li>the first player in {@code priorityPlayers} who is an enemy of {@code p}
157-
* <li>any enemy of {@code p}
158-
* <li>the first player {@code priorityPlayers} who is neutral towards {@code p}
159-
* <li>any neutral player (with respect to {@code p})
160-
* <li>any player
161-
* </ol>
162-
*
163-
* @param player the player to find an opponent for
164-
* @param priorityPlayers an ordered list of players which should be considered first
165-
* @return an opponent. An empty optional is returned if the game has no players
108+
* @return best defender
166109
*/
167-
private Optional<GamePlayer> getOpponentWithPriorityList(
168-
Territory territory, final GamePlayer player, final List<GamePlayer> priorityPlayers) {
169-
GamePlayer bestDefender = null;
170-
// Handle some special cases that the priority ordering logic doesn't handle. See tests.
171-
if (territory != null) {
172-
if (territory.isWater()) {
173-
bestDefender = getEnemyWithMostUnits(territory);
174-
if (bestDefender == null) {
175-
bestDefender = UnitUtils.findPlayerWithMostUnits(territory.getUnits());
176-
}
177-
} else {
178-
bestDefender = territory.getOwner();
179-
// If we're not at war with the owner and there are enemies, fight them.
180-
if (!bestDefender.isAtWar(currentPlayer)) {
181-
GamePlayer enemyWithMostUnits = getEnemyWithMostUnits(territory);
182-
if (enemyWithMostUnits != null) {
183-
bestDefender = enemyWithMostUnits;
184-
}
185-
}
186-
}
110+
@Nullable
111+
private GamePlayer getBestDefender(final GamePlayer attacker) {
112+
assert currentPlayer
113+
!= null; // should not occur checked in calling method getAttackerAndDefender
114+
115+
final List<GamePlayer> potentialDefenders = ProUtils.getEnemyPlayers(attacker);
116+
if (potentialDefenders.isEmpty()) {
117+
return null;
187118
}
188-
final Stream<GamePlayer> enemiesPriority =
189-
priorityPlayers.stream().filter(Matches.isAtWar(player));
190-
final Stream<GamePlayer> neutralsPriority =
191-
priorityPlayers.stream()
192-
.filter(Matches.isAtWar(player).negate())
193-
.filter(Matches.isAllied(player).negate());
194-
return Stream.of(
195-
Optional.ofNullable(bestDefender).stream(),
196-
enemiesPriority,
197-
playersAtWarWith(player),
198-
neutralsPriority,
199-
neutralPlayersTowards(player),
200-
players.stream())
201-
.flatMap(s -> s)
202-
.findFirst();
203-
}
119+
// determine next player after current player who could be a defender
120+
final GamePlayer nextPlayerAsDefender =
121+
ProUtils.getOtherPlayersInTurnOrder(attacker).stream()
122+
.filter(potentialDefenders::contains)
123+
.findFirst()
124+
.orElseThrow();
204125

205-
private @Nullable GamePlayer getEnemyWithMostUnits(Territory territory) {
206-
return UnitUtils.findPlayerWithMostUnits(
207-
territory.getUnits().stream()
208-
.filter(Matches.unitIsEnemyOf(currentPlayer))
209-
.collect(Collectors.toList()));
210-
}
126+
if (territory == null) {
127+
// Without territory, we cannot prioritize defender by number of units
128+
return nextPlayerAsDefender;
129+
}
211130

212-
/**
213-
* Returns a stream of all players which are at war with player {@code p}.
214-
*
215-
* <p>The returned stream might be empty.
216-
*/
217-
private Stream<GamePlayer> playersAtWarWith(final GamePlayer p) {
218-
return players.stream().filter(Matches.isAtWar(p));
131+
return getBestDefenderFromTerritory(potentialDefenders, nextPlayerAsDefender);
219132
}
220133

221-
/**
222-
* Returns a stream of all players which are neither allied nor at war with player {@code p}.
223-
*
224-
* <p>The returned stream might be empty.
225-
*/
226-
private Stream<GamePlayer> neutralPlayersTowards(final GamePlayer p) {
227-
return players.stream()
228-
.filter(Matches.isAtWar(p).negate())
229-
.filter(Matches.isAllied(p).negate());
134+
private GamePlayer getBestDefenderFromTerritory(
135+
final List<GamePlayer> potentialDefenders, final GamePlayer nextPlayerAsDefender) {
136+
// Select the defender to be an enemy of the current player, if possible, prefer enemies
137+
// in the given territory.
138+
assert territory != null; // should not occur as checked in calling method getBestDefender
139+
140+
final GamePlayer territoryOwner = territory.getOwner();
141+
if (!territoryOwner.isNull() && potentialDefenders.contains(territoryOwner)) {
142+
// In case the owner is a potential defender and has units in the land it's the defender
143+
if (territory.getUnits().stream()
144+
.map(Unit::getOwner)
145+
.filter(territoryOwner::equals)
146+
.findAny()
147+
.isPresent()) {
148+
return territoryOwner;
149+
}
150+
}
151+
152+
// When deciding for an enemy, usually a player having more units is
153+
// more important and more likely to be meant, e.g. a territory with 10 units of player A
154+
// and 1 unit of player B. Thus, we use lists and ordered streams.
155+
final List<GamePlayer> sortedPlayersForDefender =
156+
territory.getUnitCollection().getPlayersSortedByUnitCount();
157+
158+
// Add the territory owner at the end of the priority list. This way, when attacking an
159+
// empty territory, the owner gets preferred even though they have no units in their land.
160+
sortedPlayersForDefender.add(territoryOwner);
161+
162+
sortedPlayersForDefender.removeIf(p -> !potentialDefenders.contains(p));
163+
sortedPlayersForDefender.add(nextPlayerAsDefender);
164+
165+
// Attacker fights alone for this selector; the defender can also use all the allied units.
166+
return sortedPlayersForDefender.get(0);
230167
}
231168
}

0 commit comments

Comments
 (0)