mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 11:18:01 +00:00
- 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:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
256
forge-ai/src/main/java/forge/ai/SpecialCardAi.java
Normal file
256
forge-ai/src/main/java/forge/ai/SpecialCardAi.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user