4
4
import games .strategy .engine .data .RelationshipTracker ;
5
5
import games .strategy .engine .data .Territory ;
6
6
import games .strategy .engine .data .Unit ;
7
- import games .strategy .triplea .UnitUtils ;
7
+ import games .strategy .triplea .ai . pro . util . ProUtils ;
8
8
import games .strategy .triplea .delegate .Matches ;
9
9
import java .util .Collection ;
10
10
import java .util .List ;
11
11
import java .util .Optional ;
12
- import java .util .stream .Collectors ;
13
- import java .util .stream .Stream ;
14
12
import javax .annotation .Nonnull ;
15
13
import javax .annotation .Nullable ;
16
14
import lombok .AccessLevel ;
@@ -54,44 +52,24 @@ public Optional<GamePlayer> getDefender() {
54
52
@ Nonnull private final RelationshipTracker relationshipTracker ;
55
53
@ Nullable private final Territory territory ;
56
54
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. */
62
56
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 ) {
65
60
return AttackerAndDefender .NONE ;
66
61
}
67
62
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 ;
72
67
}
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 ();
85
70
}
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 );
91
71
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 ));
95
73
return AttackerAndDefender .builder ()
96
74
.attacker (attacker )
97
75
.defender (defender )
@@ -101,131 +79,90 @@ public AttackerAndDefender getAttackerAndDefender() {
101
79
}
102
80
103
81
/**
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.
107
84
*
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
129
86
*/
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 ()
136
97
.findFirst ()
137
- .orElse (null );
138
- if (attacker == null ) {
139
- return AttackerAndDefender .NONE ;
98
+ .orElseThrow ();
99
+ }
140
100
}
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 ;
145
102
}
146
103
147
104
/**
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.
151
107
*
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
166
109
*/
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 ;
187
118
}
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 ();
204
125
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
+ }
211
130
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 );
219
132
}
220
133
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 );
230
167
}
231
168
}
0 commit comments