- Refactored the card-specific AIs implemented so far to its own class SpecialCardAi, will consider moving other card-specific hardcoded logic there later.

- Implemented rudimentary AI logic for Necropotence and Yawgmoth's Bargain, could use improvement but should be fine in typical situations.
- Promoted Necropotence and Yawgmoth's Bargain from RemAIDeck to RemRandomDeck.
- Propagated the SA parameter to checkLifeCost (and closed the previous TODO associated with that), was also necessary to support the Necro AI logic.
This commit is contained in:
Agetian
2017-01-07 14:11:57 +00:00
parent 8556c8575f
commit bd0e4fb2bc
32 changed files with 316 additions and 119 deletions

1
.gitattributes vendored
View File

@@ -32,6 +32,7 @@ forge-ai/src/main/java/forge/ai/CreatureEvaluator.java -text
forge-ai/src/main/java/forge/ai/GameState.java -text
forge-ai/src/main/java/forge/ai/LobbyPlayerAi.java -text
forge-ai/src/main/java/forge/ai/PlayerControllerAi.java -text
forge-ai/src/main/java/forge/ai/SpecialCardAi.java -text
forge-ai/src/main/java/forge/ai/SpellAbilityAi.java -text
forge-ai/src/main/java/forge/ai/SpellApiToAi.java -text
forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java -text

View File

@@ -751,7 +751,7 @@ public class ComputerUtil {
if (controller == ai) {
final Cost abCost = sa.getPayCosts();
if (abCost != null) {
if (!ComputerUtilCost.checkLifeCost(controller, abCost, c, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(controller, abCost, c, 4, sa)) {
continue; // Won't play ability
}

View File

@@ -195,8 +195,7 @@ public class ComputerUtilCost {
* @param sourceAbility TODO
* @return true, if successful
*/
public static boolean checkLifeCost(final Player ai, final Cost cost, final Card source, final int remainingLife, SpellAbility sourceAbility) {
// TODO - Pass in SA for everything else that calls this function
public static boolean checkLifeCost(final Player ai, final Cost cost, final Card source, int remainingLife, SpellAbility sourceAbility) {
if (cost == null) {
return true;
}
@@ -209,6 +208,11 @@ public class ComputerUtilCost {
amount = AbilityUtils.calculateAmount(source, payLife.getAmount(), sourceAbility);
}
// check if there's override for the remainingLife threshold
if (sourceAbility != null && sourceAbility.hasParam("AIMinLifeThreshold")) {
remainingLife = Integer.parseInt(sourceAbility.getParam("AIMinLifeThreshold"));
}
if (ai.getLife() - amount < remainingLife && !ai.cantLoseForZeroOrLessLife()) {
return false;
}

View File

@@ -1166,7 +1166,7 @@ public class ComputerUtilMana {
// don't kill yourself
final Cost abCost = m.getPayCosts();
if (!ComputerUtilCost.checkLifeCost(ai, abCost, sourceCard, 1, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, sourceCard, 1, m)) {
continue;
}

View File

@@ -0,0 +1,256 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.ai;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Collections;
import java.util.List;
/**
* Special logic for individual cards
*
* Specific methods for each card that requires special handling are stored in inner classes
* Each class should have a name based on the name of the card and ideally preceded with a
* single-line comment with the full English card name to make searching for them easier.
*
* Class methods should return "true" if they are successful and have completed their task in full,
* otherwise should return "false" to signal that the AI should not use the card under current
* circumstances. A good convention to follow is to call the method "consider" if it's the only
* method necessary, or considerXXXX if several methods do different tasks, and use at least two
* mandatory parameters (Player ai, SpellAbility sa, in this order) and, if necessary, additional
* parameters later.
*
* If this class ends up being busy, consider splitting it into individual classes, each in its
* own file, inside its own package, for example, forge.ai.cards.
*/
public class SpecialCardAi {
// Donate
public static class Donate {
public static boolean considerTargetingOpponent(Player ai, SpellAbility sa) {
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
// first filter for opponents which can be targeted by SA
final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
// filter for player who does not have donate target already
Iterable<Player> oppTarget = Iterables.filter(oppList,
PlayerPredicates.isNotCardInPlay(donateTarget.getName()));
// fall back to previous list
if (Iterables.isEmpty(oppTarget)) {
oppTarget = oppList;
}
// select player with less lands on the field (helpful for Illusions of Grandeur and probably Pacts too)
Player opp = Collections.min(Lists.newArrayList(oppTarget),
PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.LANDS));
if (opp != null) {
sa.resetTargets();
sa.getTargets().add(opp);
return true;
}
return true;
}
// No targets found to donate, so do nothing.
return false;
}
public static boolean considerDonatingPermanent(Player ai, SpellAbility sa) {
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
sa.resetTargets();
sa.getTargets().add(donateTarget);
return true;
}
// Should never get here because targetOpponent, called before targetPermanentToDonate, should already have made the AI bail
System.err.println("Warning: Donate AI failed at SpecialCardAi.Donate#targetPermanentToDonate despite successfully targeting an opponent first.");
return false;
}
}
// Necropotence
public static class Necropotence {
public static boolean consider(Player ai, SpellAbility sa) {
Game game = ai.getGame();
int computerHandSize = ai.getZone(ZoneType.Hand).size();
int maxHandSize = ai.getMaxHandSize();
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain")).size() > 0) {
// Prefer Yawgmoth's Bargain because AI is generally better with it
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
// (not sure how to detect the presence of such effects yet)
return false;
}
PhaseHandler ph = game.getPhaseHandler();
int exiledWithNecro = 1; // start with 1 because if this succeeds, one extra card will be exiled with Necro
for (Card c : ai.getCardsIn(ZoneType.Exile)) {
if (c.getExiledWith() != null && "Necropotence".equals(c.getExiledWith().getName()) && c.isFaceDown()) {
exiledWithNecro++;
}
}
// TODO: Any other bad effects like that?
boolean blackViseOTB = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).size() > 0;
if (!ph.isPlayerTurn(ai) && ph.is(PhaseType.MAIN2)
&& ai.getSpellsCastLastTurn() == 0
&& ai.getSpellsCastThisTurn() == 0
&& ai.getLandsPlayedLastTurn() == 0) {
// We're in a situation when we have nothing castable in hand, something needs to be done
if (!blackViseOTB) {
// exile-loot +1 card when a max hand size, hoping to get a workable spell or land
return computerHandSize + exiledWithNecro - 1 == maxHandSize;
} else {
// Loot to 7 in presence of Black Vise, hoping to find what to do
// NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead?
return computerHandSize + exiledWithNecro <= maxHandSize;
}
} else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) {
// try not to overdraw in presence of Black Vise
return false;
} else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) {
// Only draw until we reach max hand size
return false;
} else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) {
// Only activate in AI's own turn (sans the exception above)
return false;
}
return true;
}
}
// Nykthos, Shrine to Nyx
public static class NykthosShrineToNyx {
public static boolean consider(Player ai, SpellAbility sa) {
Game game = ai.getGame();
PhaseHandler ph = game.getPhaseHandler();
if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) {
// TODO: currently limited to Main 2, somehow improve to let the AI use this SA at other time?
return false;
}
String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield));
int devotion = CardFactoryUtil.xCount(sa.getHostCard(), "Count$Devotion." + prominentColor);
int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0);
// do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage)
if (devotion < activationCost + 1) {
return false;
}
final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command});
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai);
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableMana(ai, true), CardPredicates.Presets.UNTAPPED).size();
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana();
byte colorProfile = cost.getColorProfile();
if ((cost.getCMC() == 0)) {
// no mana cost, no need to activate this SA then (additional mana not needed)
continue;
} else if (colorProfile != 0 && (cost.getColorProfile() & MagicColor.fromName(prominentColor)) == 0) {
// does not feature prominent color, won't be able to pay for it with SA activated for this color
continue;
} else if ((testSa.getPayCosts().getTotalMana().getCMC() > devotion + numManaSrcs - activationCost)) {
// the cost may be too high even if we activate this SA
continue;
}
if (testSa.getHostCard().getName().equals(sa.getHostCard().getName())) {
// prevent infinitely recursing own ability when testing AI play decision
continue;
}
testSa.setActivatingPlayer(ai);
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSa) == AiPlayDecision.WillPlay) {
// the AI is willing to play the spell
return true;
}
}
return false; // haven't found anything to play with the excess generated mana
}
}
// Yawgmoth's Bargain
public static class YawgmothsBargain {
public static boolean consider(Player ai, SpellAbility sa) {
Game game = ai.getGame();
PhaseHandler ph = game.getPhaseHandler();
int computerHandSize = ai.getZone(ZoneType.Hand).size();
int maxHandSize = ai.getMaxHandSize();
// TODO: Any other bad effects like that?
boolean blackViseOTB = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).size() > 0;
// TODO: Consider effects like "whenever a player draws a card, he loses N life" (e.g. Nekusar, the Mindraiser),
// and effects that draw an additional card whenever a card is drawn.
if (!ph.isPlayerTurn(ai) && ph.is(PhaseType.END_OF_TURN)
&& ai.getSpellsCastLastTurn() == 0
&& ai.getSpellsCastThisTurn() == 0
&& ai.getLandsPlayedLastTurn() == 0) {
// We're in a situation when we have nothing castable in hand, something needs to be done
if (!blackViseOTB) {
// draw +1 card when a max hand size, hoping to draw a workable spell or land
return computerHandSize == maxHandSize;
} else {
// draw cards hoping to draw answers even in presence of Black Vise if there's no valid play
// TODO: maybe limit to 1 or 2 cards at a time?
return computerHandSize + 1 <= maxHandSize; // currently draws to 7 cards
}
} else if (blackViseOTB && computerHandSize + 1 > 4) {
// try not to overdraw in presence of Black Vise
return false;
} else if (computerHandSize + 1 > maxHandSize) {
// Only draw until we reach max hand size
return false;
} else if (!ph.isPlayerTurn(ai)) {
// Only activate in AI's own turn (sans the exception above)
return false;
}
return true;
}
}
}

View File

@@ -86,7 +86,7 @@ public abstract class SpellAbilityAi {
* Evaluated costs are: life, discard, sacrifice and counter-removal
*/
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {

View File

@@ -41,7 +41,7 @@ public class AttachAi extends SpellAbilityAi {
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}

View File

@@ -137,6 +137,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
return true;
}
}
if (aiLogic.equals("Necropotence")) {
return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
}
}
String origin = null;
if (sa.hasParam("Origin")) {
@@ -245,7 +248,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
@@ -654,7 +657,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -41,7 +41,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -7,6 +7,7 @@ import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
@@ -41,53 +42,7 @@ public class ChooseColorAi extends SpellAbilityAi {
}
if ("Nykthos, Shrine to Nyx".equals(source.getName())) {
PhaseHandler ph = game.getPhaseHandler();
if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) {
// TODO: currently limited to Main 2, somehow improve to let the AI use this SA at other time?
return false;
}
String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield));
int devotion = CardFactoryUtil.xCount(source, "Count$Devotion." + prominentColor);
int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0);
// do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage)
if (devotion < activationCost + 1) {
return false;
}
final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command});
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai);
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableMana(ai, true), CardPredicates.Presets.UNTAPPED).size();
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana();
byte colorProfile = cost.getColorProfile();
if ((cost.getCMC() == 0)) {
// no mana cost, no need to activate this SA then (additional mana not needed)
continue;
} else if (colorProfile != 0 && (cost.getColorProfile() & MagicColor.fromName(prominentColor)) == 0) {
// does not feature prominent color, won't be able to pay for it with SA activated for this color
continue;
} else if ((testSa.getPayCosts().getTotalMana().getCMC() > devotion + numManaSrcs - activationCost)) {
// the cost may be too high even if we activate this SA
continue;
}
if (testSa.getHostCard().getName().equals(source.getName())) {
// prevent infinitely recursing own ability when testing AI play decision
continue;
}
testSa.setActivatingPlayer(ai);
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSa) == AiPlayDecision.WillPlay) {
// the AI is willing to play the spell
return true;
}
}
return false; // haven't found anything to play with the excess generated mana
return SpecialCardAi.NykthosShrineToNyx.consider(ai, sa);
}
if ("Oona, Queen of the Fae".equals(source.getName())) {

View File

@@ -43,7 +43,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -35,7 +35,7 @@ public class CounterAi extends SpellAbilityAi {
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}

View File

@@ -66,7 +66,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -43,7 +43,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, sa)) {
return false;
}

View File

@@ -34,7 +34,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -33,7 +33,7 @@ public class DamageAllAi extends SpellAbilityAi {
final Cost abCost = sa.getPayCosts();
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}

View File

@@ -112,7 +112,7 @@ public class DamageDealAi extends DamageAiBase {
}
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -34,7 +34,7 @@ public class DamagePreventAi extends SpellAbilityAi {
final Cost cost = sa.getPayCosts();
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) {
return false;
}

View File

@@ -22,7 +22,7 @@ public class DamagePreventAllAi extends SpellAbilityAi {
final Cost cost = sa.getPayCosts();
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) {
return false;
}

View File

@@ -46,7 +46,7 @@ public class DebuffAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, sa)) {
return false;
}

View File

@@ -43,7 +43,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -112,7 +112,7 @@ public class DestroyAllAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}

View File

@@ -32,7 +32,7 @@ public class DiscardAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -29,6 +29,7 @@ import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.PaymentDecision;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -60,7 +61,7 @@ public class DrawAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
@@ -119,7 +120,8 @@ public class DrawAi extends SpellAbilityAi {
|| game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa)
&& ai.getCardsIn(ZoneType.Hand).size() > 1
&& !ComputerUtil.activateForCost(sa, ai)) {
&& !ComputerUtil.activateForCost(sa, ai)
&& !"YawgmothsBargain".equals(sa.getParam("AILogic"))) {
return false;
}
@@ -221,9 +223,18 @@ public class DrawAi extends SpellAbilityAi {
}
//if (n)
// Logic for cards that require special handling
if (sa.hasParam("AILogic")) {
if ("YawgmothsBargain".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
}
}
// Generic logic for all cards that do not need any special handling
// TODO: if xPaid and one of the below reasons would fail, instead of
// bailing reduce toPay amount to acceptable level
if (tgt != null) {
// ability is targeted
sa.resetTargets();

View File

@@ -68,7 +68,7 @@ public class LifeGainAi extends SpellAbilityAi {
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, false)) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}

View File

@@ -69,7 +69,7 @@ public class LifeLoseAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, amount, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, amount, sa)) {
return false;
}

View File

@@ -39,7 +39,7 @@ public class PoisonAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 1, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 1, sa)) {
return false;
}

View File

@@ -21,7 +21,7 @@ public class ProtectAllAi extends SpellAbilityAi {
final Cost cost = sa.getPayCosts();
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) {
return false;
}

View File

@@ -17,6 +17,7 @@ import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
@@ -277,38 +278,8 @@ public class PumpAi extends PumpAiBase {
return true;
}
} else if (sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("Donate")) {
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
// Donate, step 1 - target the opponent.
if (sa.getParam("AILogic").equals("DonateTargetPlayer")) {
// first filter for opponents which can be targeted by SA
final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
// filter for player with does not have donate target
// already
Iterable<Player> oppTarget = Iterables.filter(oppList,
PlayerPredicates.isNotCardInPlay(donateTarget.getName()));
// fall back to previous list
if (Iterables.isEmpty(oppTarget)) {
oppTarget = oppList;
}
// select player with less lands on the field
Player opp = Collections.min(Lists.newArrayList(oppTarget),
PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.LANDS));
if (opp != null) {
sa.resetTargets();
sa.getTargets().add(opp);
return true;
}
return true;
}
}
// No targets found to donate, so do nothing.
return false;
// Donate step 1 - try to target an opponent, preferably one who does not have a donate target yet
return SpecialCardAi.Donate.considerTargetingOpponent(ai, sa);
}
if (ComputerUtil.preventRunAwayActivations(sa)) {
@@ -449,12 +420,8 @@ public class PumpAi extends PumpAiBase {
return false;
}
} else if (sa.getParam("AILogic").equals("DonateTargetPerm")) {
// Donate, step 2 - target a donatable permanent.
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
sa.getTargets().add(donateTarget);
return true;
}
// Donate step 2 - target a donatable permanent.
return SpecialCardAi.Donate.considerDonatingPermanent(ai, sa);
}
if (isFight) {
return FightAi.canFightAi(ai, sa, attack, defense);

View File

@@ -44,7 +44,7 @@ public class SacrificeAllAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) {
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}

View File

@@ -4,10 +4,10 @@ Types:Enchantment
S:Mode$ Continuous | Affected$ You | AddKeyword$ Skip your draw step. | Description$ Skip your draw step.
T:Mode$ Discarded | ValidCard$ Card.YouCtrl | TriggerZones$ Battlefield | Execute$ TrigChange | TriggerDescription$ Whenever you discard a card, exile that card from your graveyard.
SVar:TrigChange:AB$ChangeZone | Cost$ 0 | Defined$ TriggeredCard | Origin$ Graveyard | Destination$ Exile
A:AB$ ChangeZone | Cost$ PayLife<1> | Defined$ TopOfLibrary | Origin$ Library | Destination$ Exile | ExileFaceDown$ True | RememberChanged$ True | SubAbility$ DelayedReturn | SpellDescription$ Exile the top card of your library face down. Put that card into your hand at the beginning of your next end step.
A:AB$ ChangeZone | Cost$ PayLife<1> | Defined$ TopOfLibrary | Origin$ Library | Destination$ Exile | ExileFaceDown$ True | RememberChanged$ True | SubAbility$ DelayedReturn | AILogic$ Necropotence | AIMinLifeThreshold$ 1 | SpellDescription$ Exile the top card of your library face down. Put that card into your hand at the beginning of your next end step.
SVar:DelayedReturn:DB$ DelayedTrigger | Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | Execute$ TrigReturn | RememberObjects$ Remembered | TriggerDescription$ Put the exiled card into your hand. | SubAbility$ DBCleanup
SVar:TrigReturn:DB$ ChangeZone | Origin$ Exile | Destination$ Hand | Defined$ DelayTriggerRemembered
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:RemAIDeck:True
SVar:RemRandomDeck:True
SVar:Picture:http://www.wizards.com/global/images/magic/general/necropotence.jpg
Oracle:Skip your draw step.\nWhenever you discard a card, exile that card from your graveyard.\nPay 1 life: Exile the top card of your library face down. Put that card into your hand at the beginning of your next end step.

View File

@@ -2,7 +2,7 @@ Name:Yawgmoth's Bargain
ManaCost:4 B B
Types:Enchantment
S:Mode$ Continuous | Affected$ You | AddKeyword$ Skip your draw step. | Description$ Skip your draw step.
A:AB$ Draw | Cost$ PayLife<1> | NumCards$ 1 | SpellDescription$ Draw a card.
SVar:RemAIDeck:True
A:AB$ Draw | Cost$ PayLife<1> | NumCards$ 1 | AILogic$ YawgmothsBargain | AIMinLifeThreshold$ 1 | SpellDescription$ Draw a card.
SVar:RemRandomDeck:True
SVar:Picture:http://www.wizards.com/global/images/magic/general/yawgmoths_bargain.jpg
Oracle:Skip your draw step.\nPay 1 life: Draw a card.