Skip to content
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
4 changes: 2 additions & 2 deletions docs/Card-scripting-API/AbilityFactory.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ Only needed if the card text doesn't contain the type/color words but the abilit
## Generic AI params
- `IsCurse$ True` - for effects that are normally treated positive e.g. Pump
- `AICheckSVar$ {Count}`
- `AILogic$ {String}`
- `AITgts$ BetterThanEvalRating.130` -Normally the AI will only prefer targeting cards that satisfy the constraint. However, you can add `AITgtsStrict$ True` if playing it should only happen when enough of these cards are available, e.g. *Rootwater Matriarch*.
- `AILogic$ {String}` - this is a special param since it supports some globally available values, however many AI API classes also have very individual logic paths which will be mentioned in their sections
- `AITgts$ BetterThanEvalRating.130` - Normally the AI will only prefer targeting cards that satisfy the constraint. However, you can add `AITgtsStrict$ True` if playing it should only happen when enough of these cards are available, e.g. *Rootwater Matriarch*.

# Factories (in Alphabetical Order)

Expand Down
9 changes: 4 additions & 5 deletions forge-ai/src/main/java/forge/ai/AiAbilityDecision.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;

public record AiAbilityDecision(int rating, AiPlayDecision decision, SpellAbility sa) {
public record AiAbilityDecision(int rating, AiPlayDecision decision) {
private static int MIN_RATING = 30;

public AiAbilityDecision(int rating, AiPlayDecision decision) {
this(rating, decision, null);
}

public boolean willingToPlay() {
return willingToPlay(null);
}
public boolean willingToPlay(SpellAbility sa) {
if (!decision.willingToPlay()) {
return false;
}
Expand Down
32 changes: 15 additions & 17 deletions forge-ai/src/main/java/forge/ai/AiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -813,34 +813,32 @@ public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean

private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) {
final Card host = sa.getHostCard();
Card altHost = host;

if (sa instanceof Spell sp) {
altHost = sp.canPlayFromHost();
Card altHost = sp.canPlayFromHost();
if (altHost == null) {
return AiPlayDecision.CantPlaySa;
}
// state needs to be switched here so API checks evaluate the right face
if (host != altHost) {
sa.setHostCard(altHost);
}
altHost.setCastSA(sa);
} else if (!sa.canPlay()) {
return AiPlayDecision.CantPlaySa;
}

// state needs to be switched here so API checks evaluate the right face
if (host != altHost) {
sa.setHostCard(altHost);
}

AiPlayDecision decision = canPlayAndPayForFace(sa);

if (host != altHost) {
sa.setHostCard(host);
}

if (sa.isSpell()) {
altHost.setCastSA(null);
try {
return canPlayAndPayForFace(sa);
} finally {
// in addition to engine some AI api can also switch host
if (sa.getHostCard() != host) {
sa.setHostCard(host);
}
if (sa.isSpell()) {
host.setCastSA(null);
}
}

return decision;
}

// This is for playing spells regularly (no Cascade/Ripple etc.)
Expand Down
14 changes: 3 additions & 11 deletions forge-ai/src/main/java/forge/ai/ComputerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,8 @@ public static boolean activateForCost(SpellAbility sa, final Player ai) {
}
if (abCost.hasTapCost() && source.hasSVar("AITapDown")) {
return true;
} else if (sa.getRootAbility().isPwAbility() && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
}
if (sa.getRootAbility().isPwAbility() && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
for (final CostPart part : sa.getRootAbility().getPayCosts().getCostParts()) {
if (part instanceof CostPutCounter) {
return part.convertAmount() == null || part.convertAmount() > 0 || ai.isCardInPlay("Carth the Lion");
Expand All @@ -1390,16 +1391,7 @@ public static boolean activateForCost(SpellAbility sa, final Player ai) {
}
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice sac) {
if (sac.payCostFromSource()) {
if (source.getSVar("SacMe").equals("6")) {
return true;
} else if (shouldSacrificeThreatenedCard(ai, source, sa)) {
return true;
}
continue;
}

final CardCollection typeList =
final List<Card> typeList = sac.payCostFromSource() ? List.of(source) :
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), sac.getType(), source.getController(), source, sa);
for (Card c : typeList) {
if (c.getSVar("SacMe").equals("6")) {
Expand Down
21 changes: 20 additions & 1 deletion forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import forge.game.GameEntity;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCopyService;
import forge.game.card.CardState;
import forge.game.card.CounterType;
Expand Down Expand Up @@ -157,7 +158,8 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Never".equals(aiLogic)) {
return false;
} else if ("Once".equals(aiLogic)) {
}
if ("Once".equals(aiLogic)) {
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
}
return true;
Expand Down Expand Up @@ -272,6 +274,7 @@ public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
}

if (subAb == null) {
// TODO this should result in the average rating of each decision
return decision;
}

Expand Down Expand Up @@ -503,4 +506,20 @@ protected static boolean playReusable(final Player ai, final SpellAbility sa) {

return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
}

protected boolean setAiEvaluationHost(final SpellAbility sa, final CardCollection remember) {
if (sa.isTrigger() || sa.isCastFromPlayEffect()) {
// reset not supported yet
return false;
}
Card host = sa.getHostCard();
if (!host.isLKI()) {
host = CardCopyService.getLKICopy(host);
sa.getRootAbility().setHostCard(host);
}
if (remember != null) {
host.addRemembered(remember);
}
return true;
}
}
2 changes: 1 addition & 1 deletion forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean
&& amount == 0 // And counter amount wasn't set previously by something (e.g. Wildborn Preserver)
&& sa.hasSVar(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Spend all remaining mana to add X counters (eg. Hero of Leina Tower)
int payX = ComputerUtilCost.setMaxXValue(sa, ai, true);
ComputerUtilCost.setMaxXValue(sa, ai, true);
}

if (!mandatory) {
Expand Down
8 changes: 2 additions & 6 deletions forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
final Game game = ai.getGame();
final int life = ai.getLife();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);

boolean lifeCritical = life <= 5;
lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE)
Expand All @@ -102,18 +101,15 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
}

// Sacrificing in response to something dangerous is generally good in any phase
boolean isSacCost = false;
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
isSacCost = true;
}
boolean isSacCost = sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class);

// Don't use lifegain before main 2 if possible
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) {
return false;
}

return lifeCritical || activateForCost
return lifeCritical || ComputerUtil.activateForCost(sa, ai)
|| (ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|| sa.hasParam("PlayerTurn") || isSorcerySpeed(sa, ai);
}
Expand Down
25 changes: 25 additions & 0 deletions forge-ai/src/main/java/forge/ai/ability/RevealAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@
import forge.ai.PlayerControllerAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;

public class RevealAi extends RevealAiBase {

@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (isRememberedSelfRevealAnyNumber(sa)) {
CardCollection revealable = getRevealableCards(ai, sa);
if (revealable.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
setAiEvaluationHost(sa, revealable);
}

if (!revealHandTargetAI(ai, sa, false)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
Expand All @@ -26,6 +37,20 @@ protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa
return super.checkApiLogic(ai, sa);
}

private static boolean isRememberedSelfRevealAnyNumber(final SpellAbility sa) {
return sa.hasParam("AnyNumber") && sa.hasParam("RememberRevealed") && !sa.usesTargeting()
&& (!sa.hasParam("Defined") || "You".equals(sa.getParam("Defined")));
}

private static CardCollection getRevealableCards(final Player ai, final SpellAbility sa) {
final CardCollection cards = sa.hasParam("RevealValid")
? CardLists.getValidCards(ai.getCardsIn(ZoneType.Hand), sa.getParam("RevealValid"),
ai, sa.getHostCard(), sa)
: new CardCollection(ai.getCardsIn(ZoneType.Hand));
cards.remove(sa.getHostCard());
return cards;
}

@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
// logic to see if it should reveal Miracle Card
Expand Down
4 changes: 4 additions & 0 deletions forge-game/src/main/java/forge/game/cost/CostAdjustment.java
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final
untappedCards = CardLists.filter(untappedCards, CardPredicates.CREATURES);
}

if (untappedCards.isEmpty()) {
return;
}

Map<Card, ManaCostShard> convokedCards = payer.getController().chooseCardsForConvokeOrImprovise(sa,
cost.toManaCost(), untappedCards, artifacts, creatures, maxReduction);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,8 @@
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;

/**
* The Class StaticAbilityCantTarget.
*/
public class StaticAbilityCantTarget {

static String MODE = "CantTarget";

public static StaticAbility cantTarget(final GameEntity entity, final SpellAbility spellAbility) {
final Game game = entity.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
Expand Down
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/b/brine_seer.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 U T | Defined$ You | RevealValid$ Card.Blue | AnyNumber$
SVar:DBCounter:DB$ Counter | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | UnlessCost$ Y | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:Y:Remembered$Amount
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:{2}{U}, {T}: Reveal any number of blue cards in your hand. Counter target spell unless its controller pays {1} for each card revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/c/cinder_seer.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 R T | Defined$ You | RevealValid$ Card.Red | AnyNumber$ T
SVar:DBCinderDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ X | SubAbility$ DBCinderCleanup
SVar:X:Remembered$Amount
SVar:DBCinderCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:{2}{R}, {T}: Reveal any number of red cards in your hand. Cinder Seer deals X damage to any target, where X is the number of cards revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/i/ivy_seer.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 G T | Defined$ You | RevealValid$ Card.Green | AnyNumber$
SVar:DBIvyPump:DB$ Pump | ValidTgts$ Creature | NumAtt$ +IvyX | NumDef$ +IvyX | SubAbility$ DBIvyCleanup
SVar:IvyX:Remembered$Amount
SVar:DBIvyCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:{2}{G}, {T}: Reveal any number of green cards in your hand. Target creature gets +X/+X until end of turn, where X is the number of cards revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/j/jasmine_seer.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 W T | Defined$ You | RevealValid$ Card.White | AnyNumber$
SVar:DBJasmineLife:DB$ GainLife | LifeAmount$ JasmineX | SubAbility$ DBJasmineCleanup
SVar:JasmineX:Remembered$Amount/Twice
SVar:DBJasmineCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:{2}{W}, {T}: Reveal any number of white cards in your hand. You gain 2 life for each card revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/n/nightshade_seer.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 B T | Defined$ You | RevealValid$ Card.Black | AnyNumber$
SVar:DBNightshadePump:DB$ Pump | ValidTgts$ Creature | NumAtt$ -NightshadeX | NumDef$ -NightshadeX | IsCurse$ True | SubAbility$ DBNightshadeCleanup
SVar:DBNightshadeCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:NightshadeX:Remembered$Amount
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:{2}{B}, {T}: Reveal any number of black cards in your hand. Target creature gets -X/-X until end of turn, where X is the number of cards revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/p/phosphorescent_feast.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ A:SP$ Reveal | AnyNumber$ True | RememberRevealed$ True | SubAbility$ DBGainLife
SVar:DBGainLife:DB$ GainLife | LifeAmount$ X | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Remembered$ChromaSource.Green/Twice
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Chroma — Reveal any number of cards in your hand. You gain 2 life for each green mana symbol in those cards' mana costs.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/r/rofelloss_gift.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ A:SP$ Reveal | RevealValid$ Card.Green+YouCtrl | AnyNumber$ True | RememberRevea
SVar:DBChangeZone:DB$ ChangeZone | Hidden$ True | Mandatory$ True | ChangeType$ Card.Enchantment+YouOwn | ChangeNum$ X | Origin$ Graveyard | Destination$ Hand | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Remembered$Amount
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Reveal any number of green cards in your hand. Return an enchantment card from your graveyard to your hand for each card revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/s/scent_of_brine.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.Blue | AnyNumber$ True | Remembe
SVar:DBScentOfBrineCounter:DB$ Counter | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | UnlessCost$ ScentOfBrineX | SubAbility$ DBScentOfBrineCleanup
SVar:DBScentOfBrineCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:ScentOfBrineX:Remembered$Amount
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Reveal any number of blue cards in your hand. Counter target spell unless its controller pays {1} for each card revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/s/scent_of_cinder.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.Red | AnyNumber$ True | Remember
SVar:DBScentOfCinderDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ ScentOfCinderX | SubAbility$ DBScentOfCinderCleanup
SVar:ScentOfCinderX:Remembered$Amount
SVar:DBScentOfCinderCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Reveal any number of red cards in your hand. Scent of Cinder deals X damage to any target, where X is the number of cards revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/s/scent_of_ivy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.Green | AnyNumber$ True | Rememb
SVar:DBScentOfIvyPump:DB$ Pump | ValidTgts$ Creature | NumAtt$ +ScentOfIvyX | NumDef$ +ScentOfIvyX | SubAbility$ DBScentOfIvyCleanup
SVar:ScentOfIvyX:Remembered$Amount
SVar:DBScentOfIvyCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Reveal any number of green cards in your hand. Target creature gets +X/+X until end of turn, where X is the number of cards revealed this way.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/s/scent_of_jasmine.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.White | AnyNumber$ True | Rememb
SVar:DBScentOfJasmineLife:DB$ GainLife | LifeAmount$ ScentOfJasmineX | SubAbility$ DBScentOfJasmineCleanup
SVar:ScentOfJasmineX:Remembered$Amount/Twice
SVar:DBScentOfJasmineCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Reveal any number of white cards in your hand. You gain 2 life for each card revealed this way.
4 changes: 2 additions & 2 deletions forge-gui/res/cardsfolder/s/scent_of_nightshade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ Name:Scent of Nightshade
ManaCost:1 B
Types:Instant
A:SP$ Reveal | Defined$ You | RevealValid$ Card.Black | AnyNumber$ True | RememberRevealed$ True | SubAbility$ DBScentOfNightshadePump | SpellDescription$ Reveal any number of black cards in your hand. Target creature gets -X/-X until end of turn, where X is the number of cards revealed this way.
SVar:DBScentOfNightshadePump:DB$ Pump | ValidTgts$ Creature | NumAtt$ -ScentOfNightshadeX | NumDef$ -ScentOfNightshadeX | SubAbility$ DBScentOfNightshadeCleanup
SVar:DBScentOfNightshadePump:DB$ Pump | ValidTgts$ Creature | NumAtt$ -ScentOfNightshadeX | NumDef$ -ScentOfNightshadeX | IsCurse$ True | SubAbility$ DBScentOfNightshadeCleanup
SVar:DBScentOfNightshadeCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:ScentOfNightshadeX:Remembered$Amount
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Reveal any number of black cards in your hand. Target creature gets -X/-X until end of turn, where X is the number of cards revealed this way.
Loading