Skip to content

Commit 63e6481

Browse files
committed
Merge remote-tracking branch 'upstream/master' into hover_comments
2 parents 3e98b1c + 7911b1a commit 63e6481

5,572 files changed

Lines changed: 210106 additions & 690 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/Card-scripting-API/AbilityFactory.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ This follows our general approach where we try to find a reasonable middle groun
318318

319319
## Fight
320320

321-
## FlipACoin
321+
## FlipCoin
322322

323323
## Fog
324324
This AF is based on the original *Fog* spell: "Prevent all combat damage that would be dealt this turn." While this could be done with an Effect, the specialized nature of the AI gives it its own AF.

forge-ai/src/main/java/forge/ai/SpellApiToAi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public enum SpellApiToAi {
103103
.put(ApiType.ExchangeZone, ZoneExchangeAi.class)
104104
.put(ApiType.Explore, ExploreAi.class)
105105
.put(ApiType.Fight, FightAi.class)
106-
.put(ApiType.FlipACoin, FlipACoinAi.class)
106+
.put(ApiType.FlipCoin, FlipCoinAi.class)
107107
.put(ApiType.FlipOntoBattlefield, FlipOntoBattlefieldAi.class)
108108
.put(ApiType.Fog, FogAi.class)
109109
.put(ApiType.GainControl, ControlGainAi.class)

forge-ai/src/main/java/forge/ai/ability/CharmAi.java

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
6060
* bonus choice(s) for the AI otherwise it might be too hard to ever fulfil
6161
* minimum choice requirements with canPlayAi() alone.
6262
*/
63-
chosenList = min > 1 ? chooseMultipleOptionsAi(choices, ai, min)
63+
chosenList = min > 1 ? chooseMultipleOptionsAi(sa, choices, ai, min)
6464
: chooseOptionsAi(sa, choices, ai, timingRight, num, min);
6565
}
6666

@@ -92,12 +92,11 @@ protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
9292
}
9393

9494
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
95-
List<AbilitySub> chosenList = Lists.newArrayList();
95+
List<AbilitySub> chosen = Lists.newArrayList();
9696
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
9797
// TODO unused for now, the AI doesn't know how to effectively handle repeated choices
9898
boolean allowRepeat = sa.hasParam("CanRepeatModes");
9999

100-
// Pawprint
101100
final int pawprintLimit = sa.hasParam("Pawprint") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Pawprint"), sa) : 0;
102101
if (pawprintLimit > 0) {
103102
// try to pay for the more expensive subs first
@@ -107,6 +106,7 @@ private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choic
107106

108107
// First pass using standard canPlayAi() for good choices
109108
for (AbilitySub sub : choices) {
109+
handleDependentModes(sa, chosen, sub);
110110
sub.setActivatingPlayer(ai);
111111
// TODO refactor to obtain the AiAbilityDecision instead, then we can check all to sort by value
112112
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
@@ -117,42 +117,45 @@ private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choic
117117
}
118118
pawprintAmount += curPawprintAmount;
119119
}
120-
chosenList.add(sub);
121-
if (chosenList.size() == num) {
120+
chosen.add(sub);
121+
if (chosen.size() == num) {
122122
// maximum choices reached
123-
return chosenList;
123+
break;
124124
}
125125
}
126126
}
127-
if (isTrigger && chosenList.size() < min) {
127+
if (isTrigger && chosen.size() < min) {
128128
// Second pass using doTrigger(false) to fulfill minimum choice
129-
choices.removeAll(chosenList);
129+
choices.removeAll(chosen);
130130
for (AbilitySub sub : choices) {
131+
handleDependentModes(sa, chosen, sub);
131132
if (aic.doTrigger(sub, false)) {
132-
chosenList.add(sub);
133-
if (chosenList.size() == min) {
134-
return chosenList;
133+
chosen.add(sub);
134+
if (chosen.size() == min) {
135+
break;
135136
}
136137
}
137138
}
138139
// Third pass using doTrigger(true) to force fill minimum choices
139-
if (chosenList.size() < min) {
140-
choices.removeAll(chosenList);
140+
if (chosen.size() < min) {
141+
choices.removeAll(chosen);
141142
for (AbilitySub sub : choices) {
143+
handleDependentModes(sa, chosen, sub);
142144
if (aic.doTrigger(sub, true)) {
143-
chosenList.add(sub);
144-
if (chosenList.size() == min) {
145+
chosen.add(sub);
146+
if (chosen.size() == min) {
145147
break;
146148
}
147149
}
148150
}
149151
}
150152
}
151-
if (chosenList.size() < min) {
153+
if (chosen.size() < min) {
152154
// not enough choices
153-
chosenList.clear();
155+
chosen.clear();
154156
}
155-
return chosenList;
157+
sa.setSubAbility(null);
158+
return chosen;
156159
}
157160

158161
private List<AbilitySub> chooseTriskaidekaphobia(List<AbilitySub> choices, final Player ai) {
@@ -242,32 +245,43 @@ else if (aiLife < 13 || ((aiLife - 13) % 2) == 1) {
242245
return chosenList;
243246
}
244247

245-
// Choice selection for charms that require multiple choices (eg. Cryptic Command, DTK commands)
246-
private List<AbilitySub> chooseMultipleOptionsAi(List<AbilitySub> choices, final Player ai, int min) {
248+
// Choice selection for charms that require multiple choices (e.g. Cryptic Command)
249+
private List<AbilitySub> chooseMultipleOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, int min) {
247250
AbilitySub goodChoice = null;
248-
List<AbilitySub> chosenList = Lists.newArrayList();
251+
List<AbilitySub> chosen = Lists.newArrayList();
249252
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
250253
for (AbilitySub sub : choices) {
254+
handleDependentModes(sa, chosen, sub);
251255
sub.setActivatingPlayer(ai);
252256
// Assign generic good choice to fill up choices if necessary
253257
if ("Good".equals(sub.getParam("AILogic")) && aic.doTrigger(sub, false)) {
254258
goodChoice = sub;
255259
} else if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
256-
chosenList.add(sub);
257-
if (chosenList.size() == min) {
260+
chosen.add(sub);
261+
if (chosen.size() == min) {
258262
break; // enough choices
259263
}
260264
}
261265
}
262266
// Add generic good choice if one more choice is needed
263-
if (chosenList.size() == min - 1 && goodChoice != null) {
264-
chosenList.add(0, goodChoice); // hack to make Dromoka's Command fight targets work
267+
if (chosen.size() == min - 1 && goodChoice != null) {
268+
chosen.add(0, goodChoice); // hack to make Dromoka's Command fight targets work
265269
}
266-
if (chosenList.size() != min) {
267-
chosenList.clear();
270+
if (chosen.size() != min) {
271+
chosen.clear();
268272
}
269-
return chosenList;
270-
}
273+
sa.setSubAbility(null);
274+
return chosen;
275+
}
276+
277+
private void handleDependentModes(SpellAbility sa, List<AbilitySub> chosen, AbilitySub sub) {
278+
if (sub.hasParam("TargetUnique") && !chosen.isEmpty()) {
279+
// support "Each mode must target a different..."
280+
sa.setSubAbility(null);
281+
CharmEffect.chainAbilities(sa, chosen);
282+
sa.appendSubAbility(sub);
283+
}
284+
}
271285

272286
@Override
273287
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents, Map<String, Object> params) {

forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -335,17 +335,14 @@ protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
335335
amount = 1; // TODO: improve this to possibly account for some variability depending on the roll outcome (e.g. 4 for 1d8, perhaps)
336336
}
337337

338-
if (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
339-
Combat combat = game.getCombat();
340-
if (sourceName.equals("Psychic Frog")) {
341-
return doCombatAdaptLogic(source, amount, combat);
342-
}
343-
if (sa.hasParam("Adapt")) {
344-
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
345-
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
346-
}
347-
return doCombatAdaptLogic(source, amount, combat);
348-
}
338+
if (sa.hasParam("Adapt") &&
339+
(!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0)) {
340+
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
341+
}
342+
343+
if (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) &&
344+
(sa.hasParam("Adapt") || sourceName.equals("Psychic Frog"))) {
345+
return doCombatAdaptLogic(source, amount, game.getCombat());
349346
}
350347

351348
if ("Fight".equals(logic) || "PowerDmg".equals(logic)) {

forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,7 @@ protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAb
139139

140140
for (final Player p : players) {
141141
if (sa.canTarget(p)) {
142-
boolean preferred = false;
143-
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
142+
boolean preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
144143
sa.resetTargets();
145144
sa.getTargets().add(p);
146145
if (preferred) {
@@ -149,9 +148,8 @@ protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAb
149148

150149
if (mandatory) {
151150
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
152-
} else {
153-
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
154151
}
152+
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
155153
}
156154
}
157155
}

forge-ai/src/main/java/forge/ai/ability/DrawAi.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,23 +289,19 @@ private boolean targetAI(final Player ai, final SpellAbility sa, final boolean m
289289
// TODO: if xPaid and one of the below reasons would fail, instead of
290290
// bailing reduce toPay amount to acceptable level
291291
if (sa.usesTargeting()) {
292-
// ability is targeted
293292
sa.resetTargets();
294293

295294
// if it wouldn't draw anything and its not mandatory, skip it
296295
if (numCards == 0 && !mandatory && !drawback) {
297296
return false;
298297
}
299298

300-
// filter player that can be targeted
301299
PlayerCollection players = game.getPlayers().filter(PlayerPredicates.isTargetableBy(sa));
302300

303-
// no targets skip it
304301
if (players.isEmpty()) {
305302
return false;
306303
}
307304

308-
// filter opponents
309305
PlayerCollection opps = players.filter(PlayerPredicates.isOpponentOf(ai));
310306

311307
for (Player oppA : opps) {
@@ -530,7 +526,7 @@ private boolean targetAI(final Player ai, final SpellAbility sa, final boolean m
530526
}
531527
}
532528
return true;
533-
} // drawTargetAI()
529+
}
534530

535531
@Override
536532
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {

forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java renamed to forge-ai/src/main/java/forge/ai/ability/FlipCoinAi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import forge.game.player.Player;
1010
import forge.game.spellability.SpellAbility;
1111

12-
public class FlipACoinAi extends SpellAbilityAi {
12+
public class FlipCoinAi extends SpellAbilityAi {
1313

1414
/* (non-Javadoc)
1515
* @see forge.card.abilityfactory.SpellAiLogic#checkApiLogic(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)

forge-ai/src/main/java/forge/ai/ability/TokenAi.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,20 +256,21 @@ protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean
256256
if (tgtRoleAura(ai, sa, actualToken, mandatory)) {
257257
// Targeting handled in tgtRoleAura
258258
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
259-
} else {
260-
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
261259
}
260+
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
262261
}
263262

264-
if (tgt.canOnlyTgtOpponent()) {
263+
if (sa.canTarget(ai)) {
264+
sa.getTargets().add(ai);
265+
} else if (mandatory || tgt.canOnlyTgtOpponent()) {
265266
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
266-
if (mandatory && targetableOpps.isEmpty()) {
267-
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
267+
if (targetableOpps.isEmpty()) {
268+
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
268269
}
269270
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
270271
sa.getTargets().add(opp);
271272
} else {
272-
sa.getTargets().add(ai);
273+
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
273274
}
274275
}
275276

forge-game/src/main/java/forge/game/GameLogFormatter.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,16 @@ public GameLogEntry visit(GameEventCardChangeZone ev) {
290290
if (ev.from() == null || ev.to() == null) {
291291
return null;
292292
}
293-
// Only log Battlefield → Graveyard/Exile to avoid duplicating entries
294-
// already covered by other events (land played, spell cast, discard, etc.)
295293
final ZoneType from = ev.from().zoneType();
296294
final ZoneType to = ev.to().zoneType();
295+
// Log mid-game ante additions (e.g. Contract from Below, Demonic Attorney)
296+
if (to == ZoneType.Ante && from != ZoneType.Ante) {
297+
final CardView c = ev.card();
298+
return new GameLogEntry(GameLogEntryType.ANTE,
299+
(c != null ? c.getOwner() + " anted " + c : "a card was anted"));
300+
}
301+
// Only log Battlefield -> Graveyard/Exile to avoid duplicating entries
302+
// already covered by other events (land played, spell cast, discard, etc.)
297303
if (from != ZoneType.Battlefield || (to != ZoneType.Graveyard && to != ZoneType.Exile)) {
298304
return null;
299305
}

forge-game/src/main/java/forge/game/ability/AbilityUtils.java

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
import java.util.*;
4545
import java.util.Map.Entry;
46+
import java.util.function.Predicate;
4647
import java.util.regex.Matcher;
4748
import java.util.regex.Pattern;
4849
import java.util.stream.Collectors;
@@ -1829,18 +1830,13 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) {
18291830
}
18301831

18311832
if (sq[0].startsWith("TotalManaSpent ")) {
1832-
final String[] k = sq[0].split(" ");
1833-
int v = 0;
1834-
if (sa.getRootAbility().getPayingMana() != null) {
1835-
for (Mana m : sa.getRootAbility().getPayingMana()) {
1836-
Card source = m.getSourceCard();
1837-
if (source != null) {
1838-
if (source.isValid(k[1].split(","), player, c, sa)) {
1839-
v += 1;
1840-
}
1841-
}
1842-
}
1833+
if (sa.getRootAbility().getPayingMana() == null) {
1834+
return doXMath(0, expr, c, ctb);
18431835
}
1836+
final String[] k = sq[0].split(" ");
1837+
int v = (int) sa.getRootAbility().getPayingMana().stream().map(Mana::getSourceCard)
1838+
.filter(Predicate.<Card>not(Objects::isNull).and(CardPredicates.restriction(k[1].split(","), player, c, ctb)))
1839+
.count();
18441840
return doXMath(v, expr, c, ctb);
18451841
}
18461842

@@ -1878,17 +1874,12 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) {
18781874
}
18791875
if (sq[0].startsWith("CastTotalManaSpent ")) {
18801876
final String[] k = sq[0].split(" ");
1881-
int v = 0;
1882-
if (c.getCastSA() != null) {
1883-
for (Mana m : c.getCastSA().getPayingMana()) {
1884-
Card source = m.getSourceCard();
1885-
if (source != null) {
1886-
if (source.isValid(k[1].split(","), player, c, ctb)) {
1887-
v += 1;
1888-
}
1889-
}
1890-
}
1877+
if (c.getCastSA() == null) {
1878+
return doXMath(0, expr, c, ctb);
18911879
}
1880+
int v = (int) c.getCastSA().getPayingMana().stream().map(Mana::getSourceCard)
1881+
.filter(Predicate.<Card>not(Objects::isNull).and(CardPredicates.restriction(k[1].split(","), player, c, ctb)))
1882+
.count();
18921883
return doXMath(v, expr, c, ctb);
18931884
}
18941885

0 commit comments

Comments
 (0)