forge.learnedai.ComputerUtil.java Source code

Java tutorial

Introduction

Here is the source code for forge.learnedai.ComputerUtil.java

Source

/*
 * 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.learnedai;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.apache.commons.lang3.StringUtils;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;

import forge.learnedai.ability.ProtectAi;
import forge.card.CardType;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayment;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostSacrifice;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.collect.FCollection;
import forge.util.MyRandom;

/**
 * <p>
 * ComputerUtil class.
 * </p>
 *
 * @author Forge
 * @version $Id: ComputerUtil.java 29811 2015-07-19 11:38:59Z Sloth $
 */
public class ComputerUtil {
    public static boolean handlePlayingSpellAbility(final Player ai, final SpellAbility sa, final Game game) {
        game.getStack().freezeStack();
        final Card source = sa.getHostCard();

        if (sa.isSpell() && !source.isCopiedSpell()) {
            sa.setHostCard(game.getAction().moveToStack(source));
        }

        if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
            CharmEffect.makeChoices(sa);
        }

        if (sa.hasParam("Bestow")) {
            sa.getHostCard().animateBestow();
        }

        final Cost cost = sa.getPayCosts();
        // TODO: update mana color conversion for Daxos of Meletis
        if (cost == null) {
            if (ComputerUtilMana.payManaCost(ai, sa)) {
                game.getStack().addAndUnfreeze(sa);
                return true;
            }
        } else {
            final CostPayment pay = new CostPayment(cost, sa);
            if (pay.payComputerCosts(new AiCostDecision(ai, sa))) {
                game.getStack().addAndUnfreeze(sa);
                if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty()) {
                    game.getAction().reveal(sa.getSplicedCards(), ai, true, "Computer reveals spliced cards from ");
                }
                return true;
            }
        }
        //Should not arrive here
        System.out.println("AI failed to play " + sa.getHostCard());
        return false;
    }

    private static boolean hasDiscardHandCost(final Cost cost) {
        if (cost == null) {
            return false;
        }
        for (final CostPart part : cost.getCostParts()) {
            if (part instanceof CostDiscard) {
                final CostDiscard disc = (CostDiscard) part;
                if (disc.getType().equals("Hand")) {
                    return true;
                }
            }
        }
        return false;
    }

    public static int counterSpellRestriction(final Player ai, final SpellAbility sa) {
        // Move this to AF?
        // Restriction Level is Based off a handful of factors

        int restrict = 0;

        final Card source = sa.getHostCard();
        final TargetRestrictions tgt = sa.getTargetRestrictions();

        // Play higher costing spells first?
        final Cost cost = sa.getPayCosts();
        // Convert cost to CMC
        // String totalMana = source.getSVar("PayX"); // + cost.getCMC()

        // Consider the costs here for relative "scoring"
        if (hasDiscardHandCost(cost)) {
            // Null Brooch aid
            restrict -= (ai.getCardsIn(ZoneType.Hand).size() * 20);
        }

        // Abilities before Spells (card advantage)
        if (sa.isAbility()) {
            restrict += 40;
        }

        // TargetValidTargeting gets biggest bonus
        if (tgt.getSAValidTargeting() != null) {
            restrict += 35;
        }

        // Unless Cost gets significant bonus + 10-Payment Amount
        final String unless = sa.getParam("UnlessCost");
        if (unless != null && !unless.endsWith(">")) {
            final int amount = AbilityUtils.calculateAmount(source, unless, sa);

            final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();

            // If the Unless isn't enough, this should be less likely to be used
            if (amount > usableManaSources) {
                restrict += 20 - (2 * amount);
            } else {
                restrict -= (10 - (2 * amount));
            }
        }

        // Then base on Targeting Restriction
        final String[] validTgts = tgt.getValidTgts();
        if ((validTgts.length != 1) || !validTgts[0].equals("Card")) {
            restrict += 10;
        }

        // And lastly give some bonus points to least restrictive TargetType
        // (Spell,Ability,Triggered)
        final String tgtType = sa.getParam("TargetType");
        if (tgtType != null) {
            restrict -= (5 * tgtType.split(",").length);
        }
        return restrict;
    }

    // this is used for AI's counterspells
    public static final void playStack(final SpellAbility sa, final Player ai, final Game game) {
        sa.setActivatingPlayer(ai);
        if (!ComputerUtilCost.canPayCost(sa, ai))
            return;

        final Card source = sa.getHostCard();
        if (sa.isSpell() && !source.isCopiedSpell()) {
            sa.setHostCard(game.getAction().moveToStack(source));
        }
        final Cost cost = sa.getPayCosts();
        if (cost == null) {
            ComputerUtilMana.payManaCost(ai, sa);
            game.getStack().add(sa);
        } else {
            final CostPayment pay = new CostPayment(cost, sa);
            if (pay.payComputerCosts(new AiCostDecision(ai, sa))) {
                game.getStack().add(sa);
            }
        }
    }

    public static final void playSpellAbilityForFree(final Player ai, final SpellAbility sa) {
        sa.setActivatingPlayer(ai);

        final Card source = sa.getHostCard();
        if (sa.isSpell() && !source.isCopiedSpell()) {
            sa.setHostCard(ai.getGame().getAction().moveToStack(source));
        }

        ai.getGame().getStack().add(sa);
    }

    public static final void playSpellAbilityWithoutPayingManaCost(final Player ai, final SpellAbility sa,
            final Game game) {
        final SpellAbility newSA = sa.copyWithNoManaCost();
        newSA.setActivatingPlayer(ai);

        if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA)) {
            return;
        }

        final Card source = newSA.getHostCard();
        if (newSA.isSpell() && !source.isCopiedSpell()) {
            newSA.setHostCard(game.getAction().moveToStack(source));
        }

        final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
        pay.payComputerCosts(new AiCostDecision(ai, sa));

        game.getStack().add(newSA);
    }

    public static final void playNoStack(final Player ai, final SpellAbility sa, final Game game) {
        sa.setActivatingPlayer(ai);
        // TODO: We should really restrict what doesn't use the Stack
        if (ComputerUtilCost.canPayCost(sa, ai)) {
            final Card source = sa.getHostCard();
            if (sa.isSpell() && !source.isCopiedSpell()) {
                sa.setHostCard(game.getAction().moveToStack(source));
            }

            final Cost cost = sa.getPayCosts();
            if (cost == null) {
                ComputerUtilMana.payManaCost(ai, sa);
            } else {
                final CostPayment pay = new CostPayment(cost, sa);
                pay.payComputerCosts(new AiCostDecision(ai, sa));
            }

            AbilityUtils.resolve(sa);

            // destroys creatures if they have lethal damage, etc..
            //game.getAction().checkStateEffects();
        }
    }

    public static Card getCardPreference(final Player ai, final Card activate, final String pref,
            final CardCollection typeList) {
        final Game game = ai.getGame();
        if (activate != null) {
            final String[] prefValid = activate.getSVar("AIPreference").split("\\$");
            if (prefValid[0].equals(pref)) {
                final CardCollection prefList = CardLists.getValidCards(typeList, prefValid[1].split(","),
                        activate.getController(), activate);
                if (prefList.size() != 0) {
                    CardLists.shuffle(prefList);
                    return prefList.get(0);
                }
            }
        }
        if (pref.contains("SacCost")) {
            // search for permanents with SacMe. priority 1 is the lowest, priority 5 the highest
            for (int ip = 0; ip < 6; ip++) {
                final int priority = 6 - ip;
                if (priority == 2 && ai.isCardInPlay("Crucible of Worlds")) {
                    CardCollection landsInPlay = CardLists.getType(typeList, "Land");
                    if (!landsInPlay.isEmpty()) {
                        // Don't need more land.
                        return ComputerUtilCard.getWorstLand(landsInPlay);
                    }
                }
                final CardCollection sacMeList = CardLists.filter(typeList, new Predicate<Card>() {
                    @Override
                    public boolean apply(final Card c) {
                        return (c.hasSVar("SacMe") && (Integer.parseInt(c.getSVar("SacMe")) == priority));
                    }
                });
                if (!sacMeList.isEmpty()) {
                    CardLists.shuffle(sacMeList);
                    return sacMeList.get(0);
                }
            }

            // Sac lands
            final CardCollection landsInPlay = CardLists.getType(typeList, "Land");
            if (!landsInPlay.isEmpty()) {
                final int landsInHand = Math.min(2, CardLists.getType(ai.getCardsIn(ZoneType.Hand), "Land").size());
                final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
                nonLandsInHand.addAll(ai.getCardsIn(ZoneType.Library));
                final int highestCMC = Math.max(6,
                        Aggregates.max(nonLandsInHand, CardPredicates.Accessors.fnGetCmc));
                if (landsInPlay.size() + landsInHand >= highestCMC) {
                    // Don't need more land.
                    return ComputerUtilCard.getWorstLand(landsInPlay);
                }
            }

            // try everything when about to die
            if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS)
                    && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
                final CardCollection nonCreatures = CardLists.getNotType(typeList, "Creature");
                if (!nonCreatures.isEmpty()) {
                    return ComputerUtilCard.getWorstAI(nonCreatures);
                } else if (!typeList.isEmpty()) {
                    return ComputerUtilCard.getWorstAI(typeList);
                }
            }
        } else if (pref.contains("DiscardCost")) { // search for permanents with DiscardMe
            for (int ip = 0; ip < 6; ip++) { // priority 0 is the lowest, priority 5 the highest
                final int priority = 6 - ip;
                for (Card c : typeList) {
                    if (priority == 3 && c.isLand() && ai.isCardInPlay("Crucible of Worlds")) {
                        return c;
                    }
                    if (c.hasSVar("DiscardMe") && Integer.parseInt(c.getSVar("DiscardMe")) == priority) {
                        return c;
                    }
                }
            }

            // Discard lands
            final CardCollection landsInHand = CardLists.getType(typeList, "Land");
            if (!landsInHand.isEmpty()) {
                final CardCollection landsInPlay = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Land");
                final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
                final int highestCMC = Math.max(6,
                        Aggregates.max(nonLandsInHand, CardPredicates.Accessors.fnGetCmc));
                if (landsInPlay.size() >= highestCMC
                        || (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)) {
                    // Don't need more land.
                    return ComputerUtilCard.getWorstLand(landsInHand);
                }
            }

            // try everything when about to die
            if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS)
                    && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
                if (!typeList.isEmpty()) {
                    return ComputerUtilCard.getWorstAI(typeList);
                }
            }
        }
        return null;
    }

    public static CardCollection chooseSacrificeType(final Player ai, final String type, final Card source,
            final Card target, final int amount) {
        CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"),
                source.getController(), source);
        if (ai.hasKeyword("You can't sacrifice creatures to cast spells or activate abilities.")) {
            typeList = CardLists.getNotType(typeList, "Creature");
        }

        if ((target != null) && target.getController() == ai && typeList.contains(target)) {
            typeList.remove(target); // don't sacrifice the card we're pumping
        }

        if (typeList.size() < amount) {
            return null;
        }

        final CardCollection sacList = new CardCollection();
        int count = 0;

        while (count < amount) {
            Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
            if (prefCard == null) {
                prefCard = ComputerUtilCard.getWorstAI(typeList);
            }
            if (prefCard == null) {
                return null;
            }
            sacList.add(prefCard);
            typeList.remove(prefCard);
            count++;
        }
        return sacList;
    }

    public static CardCollection chooseExileFrom(final Player ai, final ZoneType zone, final String type,
            final Card activate, final Card target, final int amount) {
        CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"),
                activate.getController(), activate);

        if ((target != null) && target.getController() == ai && typeList.contains(target)) {
            typeList.remove(target); // don't exile the card we're pumping
        }

        if (typeList.size() < amount) {
            return null;
        }

        CardLists.sortByPowerAsc(typeList);
        final CardCollection exileList = new CardCollection();

        for (int i = 0; i < amount; i++) {
            exileList.add(typeList.get(i));
        }
        return exileList;
    }

    public static CardCollection choosePutToLibraryFrom(final Player ai, final ZoneType zone, final String type,
            final Card activate, final Card target, final int amount) {
        CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"),
                activate.getController(), activate);

        if ((target != null) && target.getController() == ai && typeList.contains(target)) {
            typeList.remove(target); // don't move the card we're pumping
        }

        if (typeList.size() < amount) {
            return null;
        }

        CardLists.sortByPowerAsc(typeList);
        final CardCollection list = new CardCollection();

        if (zone != ZoneType.Hand) {
            Collections.reverse(typeList);
        }

        for (int i = 0; i < amount; i++) {
            list.add(typeList.get(i));
        }
        return list;
    }

    public static CardCollection chooseTapType(final Player ai, final String type, final Card activate,
            final boolean tap, final int amount) {
        CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"),
                activate.getController(), activate);

        // is this needed?
        typeList = CardLists.filter(typeList, Presets.UNTAPPED);

        if (tap) {
            typeList.remove(activate);
        }

        if (typeList.size() < amount) {
            return null;
        }

        CardLists.sortByPowerAsc(typeList);

        final CardCollection tapList = new CardCollection();

        for (int i = 0; i < amount; i++) {
            tapList.add(typeList.get(i));
        }
        return tapList;
    }

    public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate,
            final boolean untap, final int amount) {
        CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"),
                activate.getController(), activate);

        // is this needed?
        typeList = CardLists.filter(typeList, Presets.TAPPED);

        if (untap) {
            typeList.remove(activate);
        }

        if (typeList.size() < amount) {
            return null;
        }

        CardLists.sortByPowerDesc(typeList);

        final CardCollection untapList = new CardCollection();

        for (int i = 0; i < amount; i++) {
            untapList.add(typeList.get(i));
        }
        return untapList;
    }

    public static CardCollection chooseReturnType(final Player ai, final String type, final Card activate,
            final Card target, final int amount) {
        final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield),
                type.split(";"), activate.getController(), activate);
        if ((target != null) && target.getController() == ai && typeList.contains(target)) {
            // don't bounce the card we're pumping
            typeList.remove(target);
        }

        if (typeList.size() < amount) {
            return new CardCollection();
        }

        CardLists.sortByPowerAsc(typeList);
        final CardCollection returnList = new CardCollection();

        for (int i = 0; i < amount; i++) {
            returnList.add(typeList.get(i));
        }
        return returnList;
    }

    public static CardCollection choosePermanentsToSacrifice(final Player ai, final CardCollectionView cardlist,
            final int amount, SpellAbility source, final boolean destroy, final boolean isOptional) {
        CardCollection remaining = new CardCollection(cardlist);
        final CardCollection sacrificed = new CardCollection();
        final Card host = source.getHostCard();

        if ("OpponentOnly".equals(source.getParam("AILogic"))) {
            if (!source.getActivatingPlayer().isOpponentOf(ai)) {
                return sacrificed; // sacrifice none
            }
        } else if (isOptional && source.getActivatingPlayer().isOpponentOf(ai)) {
            return sacrificed; // sacrifice none
        }

        if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit")) {
            if (source.hasParam("Exploit")) {
                for (Trigger t : host.getTriggers()) {
                    if (t.getMode() == TriggerType.Exploited) {
                        final String execute = t.getMapParams().get("Execute");
                        if (execute == null) {
                            continue;
                        }
                        final SpellAbility exSA = AbilityFactory.getAbility(host.getSVar(execute), host);

                        exSA.setActivatingPlayer(ai);
                        exSA.setTrigger(true);

                        // Run non-mandatory trigger.
                        // These checks only work if the Executing SpellAbility is an Ability_Sub.
                        if ((exSA instanceof AbilitySub)
                                && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) {
                            // AI would not run this trigger if given the chance
                            return sacrificed;
                        }
                    }
                }
            }
            remaining = CardLists.filter(remaining, new Predicate<Card>() {
                @Override
                public boolean apply(final Card c) {
                    if (c.hasSVar("SacMe") || ComputerUtilCard.evaluateCreature(c) < 190
                            || c.hasKeyword("Undying")) {
                        return true;
                    }
                    return false;
                }
            });
        }

        final int max = Math.min(remaining.size(), amount);

        for (int i = 0; i < max; i++) {
            Card c = chooseCardToSacrifice(remaining, ai, destroy);
            remaining.remove(c);
            sacrificed.add(c);
        }
        return sacrificed;
    }

    // Precondition it wants: remaining are reverse-sorted by CMC
    private static Card chooseCardToSacrifice(final CardCollection remaining, final Player ai,
            final boolean destroy) {
        // If somehow ("Drop of Honey") they suggest to destroy opponent's card - use the chance!
        for (Card c : remaining) { // first compare is fast, second is precise
            if (c.getController() != ai && ai.getOpponents().contains(c.getController()))
                return c;
        }

        if (destroy) {
            final CardCollection indestructibles = CardLists.getKeyword(remaining, "Indestructible");
            if (!indestructibles.isEmpty()) {
                return indestructibles.get(0);
            }
        }
        for (int ip = 0; ip < 6; ip++) { // priority 0 is the lowest, priority 5 the highest
            final int priority = 6 - ip;
            for (Card card : remaining) {
                if (card.hasSVar("SacMe") && Integer.parseInt(card.getSVar("SacMe")) == priority) {
                    return card;
                }
            }
        }

        Card c = null;
        if (CardLists.getNotType(remaining, "Creature").isEmpty()) {
            c = ComputerUtilCard.getWorstCreatureAI(remaining);
        } else if (CardLists.getNotType(remaining, "Land").isEmpty()) {
            c = ComputerUtilCard.getWorstLand(CardLists.filter(remaining, CardPredicates.Presets.LANDS));
        } else {
            c = ComputerUtilCard.getWorstPermanentAI(remaining, false, false, false, false);
        }

        if (c != null && c.isEnchanted()) {
            // TODO: choose "worst" controlled enchanting Aura
            for (Card aura : c.getEnchantedBy(false)) {
                if (aura.getController().equals(c.getController()) && remaining.contains(aura)) {
                    return aura;
                }
            }
        }
        return c;
    }

    public static boolean canRegenerate(Player ai, final Card card) {
        if (card.hasKeyword("CARDNAME can't be regenerated.")) {
            return false;
        }

        final Player controller = card.getController();
        final Game game = controller.getGame();
        final CardCollectionView l = controller.getCardsIn(ZoneType.Battlefield);
        for (final Card c : l) {
            for (final SpellAbility sa : c.getSpellAbilities()) {
                // This try/catch should fix the "computer is thinking" bug
                try {

                    if (!sa.isAbility() || sa.getApi() != ApiType.Regenerate) {
                        continue; // Not a Regenerate ability
                    }
                    sa.setActivatingPlayer(controller);
                    if (!(sa.canPlay() && ComputerUtilCost.canPayCost(sa, controller))) {
                        continue; // Can't play ability
                    }

                    if (controller == ai) {
                        final Cost abCost = sa.getPayCosts();
                        if (abCost != null) {
                            if (!ComputerUtilCost.checkLifeCost(controller, abCost, c, 4, null)) {
                                continue; // Won't play ability
                            }

                            if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c)) {
                                continue; // Won't play ability
                            }

                            if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c)) {
                                continue; // Won't play ability
                            }
                        }
                    }

                    final TargetRestrictions tgt = sa.getTargetRestrictions();
                    if (tgt != null) {
                        if (CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(),
                                controller, sa.getHostCard()).contains(card)) {
                            return true;
                        }
                    } else if (AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa)
                            .contains(card)) {
                        return true;
                    }

                } catch (final Exception ex) {
                    throw new RuntimeException(String.format("There is an error in the card code for %s:%s",
                            c.getName(), ex.getMessage()), ex);
                }
            }
        }
        return false;
    }

    public static int possibleDamagePrevention(final Card card) {
        int prevented = 0;

        final Player controller = card.getController();
        final Game game = controller.getGame();

        final CardCollectionView l = controller.getCardsIn(ZoneType.Battlefield);
        for (final Card c : l) {
            for (final SpellAbility sa : c.getSpellAbilities()) {
                // if SA is from AF_Counter don't add to getPlayable
                // This try/catch should fix the "computer is thinking" bug
                try {
                    if (sa.getApi() == null || !sa.isAbility()) {
                        continue;
                    }

                    if (sa.getApi() == ApiType.PreventDamage && sa.canPlay()
                            && ComputerUtilCost.canPayCost(sa, controller)) {
                        if (AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa)
                                .contains(card)) {
                            prevented += AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa);
                        }
                        final TargetRestrictions tgt = sa.getTargetRestrictions();
                        if (tgt != null) {
                            if (CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(),
                                    controller, sa.getHostCard()).contains(card)) {
                                prevented += AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"),
                                        sa);
                            }

                        }
                    }
                } catch (final Exception ex) {
                    throw new RuntimeException(String.format("There is an error in the card code for %s:%s",
                            c.getName(), ex.getMessage()), ex);
                }
            }
        }
        return prevented;
    }

    public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
        final Card card = sa.getHostCard();

        if ("True".equals(card.getSVar("NonStackingEffect")) && card.getController().isCardInPlay(card.getName())) {
            return false;
        }

        if (card.hasSVar("PlayMain1")) {
            if (card.getSVar("PlayMain1").equals("ALWAYS") || sa.getPayCosts().hasNoManaCost()) {
                return true;
            } else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) {
                //Only play these main1 when the opponent has creatures (stealing and giving them haste)
                if (!card.getController().getOpponent().getCreaturesInPlay().isEmpty()) {
                    return true;
                }
            } else if (!card.getController().getCreaturesInPlay().isEmpty()) {
                return true;
            }
        }

        if (card.getManaCost().isZero()) {
            return true;
        }

        if (card.isCreature()
                && (ComputerUtil.hasACardGivingHaste(ai) || card.hasKeyword("Haste") || sa.isDash())) {
            return true;
        }

        if (card.hasKeyword("Exalted")) {
            return true;
        }

        //cast equipments in Main1 when there are creatures to equip and no other unequipped equipment
        if (card.isEquipment()) {
            boolean playNow = false;
            for (Card c : card.getController().getCardsIn(ZoneType.Battlefield)) {
                if (c.isEquipment() && !c.isEquipping()) {
                    playNow = false;
                    break;
                }
                if (!playNow && c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c)
                        && c.canBeEquippedBy(card)) {
                    playNow = true;
                }
            }
            if (playNow) {
                return true;
            }
        }

        // get all cards the computer controls with BuffedBy
        final CardCollectionView buffed = ai.getCardsIn(ZoneType.Battlefield);
        for (Card buffedcard : buffed) {
            if (buffedcard.hasSVar("BuffedBy")) {
                final String buffedby = buffedcard.getSVar("BuffedBy");
                final String[] bffdby = buffedby.split(",");
                if (card.isValid(bffdby, buffedcard.getController(), buffedcard)) {
                    return true;
                }
            }
            if (card.isEquipment() && buffedcard.isCreature()
                    && CombatUtil.canAttack(buffedcard, ai.getOpponent())) {
                return true;
            }
            if (card.isCreature()) {
                if (buffedcard.hasKeyword("Soulbond") && !buffedcard.isPaired()) {
                    return true;
                }
                if (buffedcard.hasKeyword("Evolve")) {
                    if (buffedcard.getNetPower() < card.getNetPower()
                            || buffedcard.getNetToughness() < card.getNetToughness()) {
                        return true;
                    }
                }
            }
            if (card.hasKeyword("Soulbond") && buffedcard.isCreature() && !buffedcard.isPaired()) {
                return true;
            }

        } // BuffedBy

        // get all cards the human controls with AntiBuffedBy
        final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
        for (Card buffedcard : antibuffed) {
            if (buffedcard.hasSVar("AntiBuffedBy")) {
                final String buffedby = buffedcard.getSVar("AntiBuffedBy");
                final String[] bffdby = buffedby.split(",");
                if (card.isValid(bffdby, buffedcard.getController(), buffedcard)) {
                    return true;
                }
            }
        } // AntiBuffedBy

        final CardCollectionView vengevines = ai.getCardsIn(ZoneType.Graveyard, "Vengevine");
        if (!vengevines.isEmpty()) {
            final CardCollectionView creatures = ai.getCardsIn(ZoneType.Hand);
            final CardCollection creatures2 = new CardCollection();
            for (int i = 0; i < creatures.size(); i++) {
                if (creatures.get(i).isCreature() && creatures.get(i).getManaCost().getCMC() <= 3) {
                    creatures2.add(creatures.get(i));
                }
            }
            if (((creatures2.size() + CardUtil.getThisTurnCast("Creature.YouCtrl", vengevines.get(0)).size()) > 1)
                    && card.isCreature() && card.getManaCost().getCMC() <= 3) {
                return true;
            }
        }
        return false;
    }

    /**
     * Is it OK to cast this for less than the Max Targets?
     * @param source the source Card
     * @return true if it's OK to cast this Card for less than the max targets
     */
    public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
        boolean ret = true;
        if (source.getManaCost().countX() > 0) {
            // If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by mana available.
            return ret;
        } else {
            // Otherwise, if life is possibly in danger, then this is fine.
            Combat combat = new Combat(ai.getOpponent());
            CardCollectionView attackers = ai.getOpponent().getCreaturesInPlay();
            for (Card att : attackers) {
                if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
                    combat.addAttacker(att, att.getController().getOpponent());
                }
            }
            AiBlockController aiBlock = new AiBlockController(ai);
            aiBlock.assignBlockersForCombat(combat);
            if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
                // Otherwise, return false. Do not play now.
                ret = false;
            }
        }
        return ret;
    }

    /**
     * Is this discard probably worse than a random draw?
     * @param discard Card to discard
     * @return boolean
     */
    public static boolean isWorseThanDraw(final Player ai, Card discard) {
        if (discard.hasSVar("DiscardMe")) {
            return true;
        }

        final Game game = ai.getGame();
        final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
                CardPredicates.Presets.LANDS);
        final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand),
                CardPredicates.Presets.LANDS);
        final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
        final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, CardPredicates.Accessors.fnGetCmc));
        final int discardCMC = discard.getCMC();
        if (discard.isLand()) {
            if (landsInPlay.size() >= highestCMC
                    || (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
                    || (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
                // Don't need more land.
                return true;
            }
        } else { //non-land
            if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
                // not castable for some time.
                return true;
            } else if (!game.getPhaseHandler().isPlayerTurn(ai)
                    && game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
                    && discardCMC > landsInPlay.size() + landsInHand.size() && discardCMC > landsInPlay.size() + 1
                    && nonLandsInHand.size() > 1) {
                // not castable for at least one other turn.
                return true;
            } else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
                    && !discard.hasProperty("hasXCost", ai, null)) {
                // Probably don't need small stuff now.
                return true;
            }
        }
        return false;
    }

    // returns true if it's better to wait until blockers are declared
    public static boolean waitForBlocking(final SpellAbility sa) {
        final Game game = sa.getActivatingPlayer().getGame();
        final PhaseHandler ph = game.getPhaseHandler();

        return (sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()
                && (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
                        || !ph.getNextTurn().equals(sa.getActivatingPlayer()))
                && !sa.getHostCard().hasSVar("EndOfTurnLeavePlay") && !sa.hasParam("ActivationPhases"));
    }

    //returns true if it's better to wait until blockers are declared).
    public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
        final Card source = sa.getHostCard();
        final SpellAbility sub = sa.getSubAbility();

        // Cipher spells
        if (sub != null) {
            final ApiType api = sub.getApi();
            if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
                return true;
            }
            if (ApiType.PumpAll == api && !ai.getCreaturesInPlay().isEmpty()) {
                return true;
            }
            if (ApiType.Pump == api) {
                return true;
            }
        }
        final CardCollectionView buffed = ai.getCardsIn(ZoneType.Battlefield);
        boolean checkThreshold = sa.isSpell() && !ai.hasThreshold()
                && !sa.getHostCard().isInZone(ZoneType.Graveyard);
        for (Card buffedCard : buffed) {
            if (buffedCard.hasSVar("BuffedBy")) {
                final String buffedby = buffedCard.getSVar("BuffedBy");
                final String[] bffdby = buffedby.split(",");
                if (source.isValid(bffdby, buffedCard.getController(), buffedCard)) {
                    return true;
                }
            }
            //Fill the graveyard for Threshold
            if (checkThreshold) {
                for (StaticAbility stAb : buffedCard.getStaticAbilities()) {
                    if ("Threshold".equals(stAb.getMapParams().get("Condition"))) {
                        return true;
                    }
                }
            }
        }

        // get all cards the human controls with AntiBuffedBy
        final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
        for (Card buffedcard : antibuffed) {
            if (buffedcard.hasSVar("AntiBuffedBy")) {
                final String buffedby = buffedcard.getSVar("AntiBuffedBy");
                final String[] bffdby = buffedby.split(",");
                if (source.isValid(bffdby, buffedcard.getController(), buffedcard)) {
                    return true;
                }
            }
        } // AntiBuffedBy

        if (sub != null) {
            return castSpellInMain1(ai, sub);
        }

        return false;
    }

    // returns true if the AI should stop using the ability
    public static boolean preventRunAwayActivations(final SpellAbility sa) {
        int activations = sa.getRestrictions().getNumberTurnActivations();

        if (sa.isTemporary()) {
            final Random r = MyRandom.getRandom();
            return r.nextFloat() >= .95; // Abilities created by static abilities have no memory
        }

        if (activations < 10) { //10 activations per turn should still be acceptable
            return false;
        }

        final Random r = MyRandom.getRandom();
        return r.nextFloat() >= Math.pow(.95, activations);
    }

    public static boolean activateForCost(SpellAbility sa, final Player ai) {
        final Cost abCost = sa.getPayCosts();
        final Card source = sa.getHostCard();
        if (abCost == null) {
            return false;
        }
        if (abCost.hasTapCost()) {
            for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
                if (c.hasSVar("AITapDown")) {
                    if (source.isValid(c.getSVar("AITapDown"), c.getController(), c)) {
                        return true;
                    }
                }
            }
        } else if (sa.hasParam("Planeswalker") && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
            for (final CostPart part : abCost.getCostParts()) {
                if (part instanceof CostPutCounter) {
                    return true;
                }
            }
        }
        for (final CostPart part : abCost.getCostParts()) {
            if (part instanceof CostSacrifice) {
                final CostSacrifice sac = (CostSacrifice) part;

                final String type = sac.getType();

                if (type.equals("CARDNAME")) {
                    if (source.getSVar("SacMe").equals("6")) {
                        return true;
                    }
                    continue;
                }

                final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield),
                        type.split(","), source.getController(), source);
                for (Card c : typeList) {
                    if (c.getSVar("SacMe").equals("6")) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public static boolean hasACardGivingHaste(final Player ai) {
        final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));

        for (final Card c : all) {
            if (c.isEquipment()) {
                for (StaticAbility stAb : c.getStaticAbilities()) {
                    Map<String, String> params = stAb.getMapParams();
                    if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
                            && params.get("AddKeyword").contains("Haste") && c.getEquipping() == null) {
                        return true;
                    }
                }
            }
        }

        all.addAll(ai.getCardsActivableInExternalZones(true));
        all.addAll(ai.getCardsIn(ZoneType.Hand));

        for (final Card c : all) {
            for (final SpellAbility sa : c.getSpellAbilities()) {
                if (sa.getApi() == ApiType.Pump && sa.hasParam("KW") && sa.getParam("KW").contains("Haste")) {
                    return true;
                }
            }
        }
        return false;
    }

    public static boolean hasAFogEffect(final Player ai) {
        final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));

        all.addAll(ai.getCardsActivableInExternalZones(true));
        all.addAll(ai.getCardsIn(ZoneType.Hand));

        for (final Card c : all) {
            for (final SpellAbility sa : c.getSpellAbilities()) {
                if (sa.getApi() != ApiType.Fog) {
                    continue;
                }
                if (!ComputerUtilCost.canPayCost(sa, ai)) {
                    continue;
                }
                return true;
            }
        }
        return false;
    }

    public static int possibleNonCombatDamage(Player ai) {
        int damage = 0;
        final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
        all.addAll(ai.getCardsActivableInExternalZones(true));
        all.addAll(ai.getCardsIn(ZoneType.Hand));

        for (final Card c : all) {
            for (final SpellAbility sa : c.getSpellAbilities()) {
                if (sa.getApi() != ApiType.DealDamage) {
                    continue;
                }
                final String numDam = sa.getParam("NumDmg");
                int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), numDam, sa);
                if (dmg <= damage) {
                    continue;
                }
                final TargetRestrictions tgt = sa.getTargetRestrictions();
                if (tgt == null) {
                    continue;
                }
                final Player enemy = ai.getOpponent();
                if (!sa.canTarget(enemy)) {
                    continue;
                }
                if (!ComputerUtilCost.canPayCost(sa, ai)) {
                    continue;
                }
                damage = dmg;
            }
        }
        return damage;
    }

    public static List<GameObject> predictThreatenedObjects(final Player aiPlayer, final SpellAbility sa) {
        final Game game = aiPlayer.getGame();
        final List<GameObject> objects = new ArrayList<GameObject>();
        if (game.getStack().isEmpty()) {
            return objects;
        }

        // check stack for something that will kill this
        for (SpellAbilityStackInstance si : game.getStack()) {
            // iterate from top of stack to find SpellAbility, including sub-abilities,
            // that does not match "sa"
            SpellAbility spell = si.getSpellAbility(true), sub = spell.getSubAbility();
            while (sub != null && sub != sa) {
                sub = sub.getSubAbility();
            }
            if (sa != spell && sa != sub) {
                Iterables.addAll(objects, ComputerUtil.predictThreatenedObjects(aiPlayer, sa, spell));
            }
        }

        return objects;
    }

    private static Iterable<? extends GameObject> predictThreatenedObjects(final Player aiPlayer,
            final SpellAbility saviour, final SpellAbility topStack) {
        Iterable<? extends GameObject> objects = new ArrayList<GameObject>();
        final List<GameObject> threatened = new ArrayList<GameObject>();
        ApiType saviourApi = saviour == null ? null : saviour.getApi();
        int toughness = 0;
        boolean grantIndestructible = false;
        boolean grantShroud = false;

        if (topStack == null) {
            return objects;
        }

        final Card source = topStack.getHostCard();
        final ApiType threatApi = topStack.getApi();

        // Can only Predict things from AFs
        if (threatApi == null) {
            return threatened;
        }
        final TargetRestrictions tgt = topStack.getTargetRestrictions();

        if (tgt == null) {
            if (topStack.hasParam("Defined")) {
                objects = AbilityUtils.getDefinedObjects(source, topStack.getParam("Defined"), topStack);
            } else if (topStack.hasParam("ValidCards")) {
                CardCollectionView battleField = aiPlayer.getCardsIn(ZoneType.Battlefield);
                objects = CardLists.getValidCards(battleField, topStack.getParam("ValidCards").split(","),
                        source.getController(), source);
            } else {
                return threatened;
            }
        } else {
            objects = topStack.getTargets().getTargets();
            final List<GameObject> canBeTargeted = new ArrayList<GameObject>();
            for (Object o : objects) {
                if (o instanceof Card) {
                    final Card c = (Card) o;
                    if (c.canBeTargetedBy(topStack)) {
                        canBeTargeted.add(c);
                    }
                }
            }
            if (canBeTargeted.isEmpty()) {
                return threatened;
            }
            objects = canBeTargeted;
        }

        if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
            toughness = saviour.hasParam("NumDef")
                    ? AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("NumDef"), saviour)
                    : 0;
            final List<String> keywords = saviour.hasParam("KW")
                    ? Arrays.asList(saviour.getParam("KW").split(" & "))
                    : new ArrayList<String>();
            if (keywords.contains("Indestructible")) {
                grantIndestructible = true;
            }
            if (keywords.contains("Hexproof") || keywords.contains("Shroud")) {
                grantShroud = true;
            }
        }

        if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
            if (saviour.getParam("CounterType").equals("P1P1")) {
                toughness = AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("CounterNum"),
                        saviour);
            } else {
                return threatened;
            }
        }

        // Determine if Defined Objects are "threatened" will be destroyed
        // due to this SA

        // Lethal Damage => prevent damage/regeneration/bounce/shroud
        if (threatApi == ApiType.DealDamage || threatApi == ApiType.DamageAll) {
            // If PredictDamage is >= Lethal Damage
            final int dmg = AbilityUtils.calculateAmount(topStack.getHostCard(), topStack.getParam("NumDmg"),
                    topStack);
            final SpellAbility sub = topStack.getSubAbility();
            boolean noRegen = false;
            if (sub != null && sub.getApi() == ApiType.Pump) {
                final List<String> keywords = sub.hasParam("KW") ? Arrays.asList(sub.getParam("KW").split(" & "))
                        : new ArrayList<String>();
                for (String kw : keywords) {
                    if (kw.contains("can't be regenerated")) {
                        noRegen = true;
                        break;
                    }
                }
            }
            for (final Object o : objects) {
                if (o instanceof Card) {
                    final Card c = (Card) o;

                    // indestructible
                    if (c.hasKeyword("Indestructible")) {
                        continue;
                    }

                    // already regenerated
                    if (c.getShieldCount() > 0) {
                        continue;
                    }

                    // don't use it on creatures that can't be regenerated
                    if ((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll)
                            && (!c.canBeShielded() || noRegen)) {
                        continue;
                    }

                    if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
                        boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source,
                                false) < ComputerUtilCombat.getDamageToKill(c);
                        if ((tgt == null && !grantIndestructible && !canSave)
                                || (!grantIndestructible && !grantShroud && !canSave)) {
                            continue;
                        }
                    }

                    if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
                        boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source,
                                false) < ComputerUtilCombat.getDamageToKill(c);
                        if (!canSave) {
                            continue;
                        }
                    }

                    // cannot protect against source
                    if (saviourApi == ApiType.Protection && (ProtectAi.toProtectFrom(source, saviour) == null)) {
                        continue;
                    }

                    // don't bounce or blink a permanent that the human
                    // player owns or is a token
                    if (saviourApi == ApiType.ChangeZone && (c.getOwner().isOpponentOf(aiPlayer) || c.isToken())) {
                        continue;
                    }

                    if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat
                            .getDamageToKill(c)) {
                        threatened.add(c);
                    }
                } else if (o instanceof Player) {
                    final Player p = (Player) o;

                    if (source.hasKeyword("Infect")) {
                        if (ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= p.getPoisonCounters()) {
                            threatened.add(p);
                        }
                    } else if (ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= p.getLife()) {
                        threatened.add(p);
                    }
                }
            }
        }
        // -Toughness Curse
        else if ((threatApi == ApiType.Pump || threatApi == ApiType.PumpAll && topStack.isCurse())
                && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
                        || saviourApi == ApiType.Protection || saviourApi == ApiType.PutCounter
                        || saviourApi == ApiType.PutCounterAll || saviourApi == null)) {
            final int dmg = -AbilityUtils.calculateAmount(topStack.getHostCard(), topStack.getParam("NumDef"),
                    topStack);
            for (final Object o : objects) {
                if (o instanceof Card) {
                    final Card c = (Card) o;
                    final boolean canRemove = (c.getNetToughness() <= dmg) || (!c.hasKeyword("Indestructible")
                            && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c)));
                    if (!canRemove) {
                        continue;
                    }

                    if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
                        final boolean cantSave = c.getNetToughness() + toughness <= dmg
                                || (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0
                                        && !grantIndestructible
                                        && (dmg >= toughness + ComputerUtilCombat.getDamageToKill(c)));
                        if (cantSave && (tgt == null || !grantShroud)) {
                            continue;
                        }
                    }

                    if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
                        boolean canSave = c.getNetToughness() + toughness > dmg;
                        if (!canSave) {
                            continue;
                        }
                    }

                    if (saviourApi == ApiType.Protection) {
                        if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) {
                            continue;
                        }
                    }

                    // don't bounce or blink a permanent that the human
                    // player owns or is a token
                    if (saviourApi == ApiType.ChangeZone && (c.getOwner().isOpponentOf(aiPlayer) || c.isToken())) {
                        continue;
                    }
                    threatened.add(c);
                }
            }
        }
        // Destroy => regeneration/bounce/shroud
        else if ((threatApi == ApiType.Destroy || threatApi == ApiType.DestroyAll)
                && (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll)
                        && !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone
                        || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
                        || saviourApi == ApiType.Protection || saviourApi == null)) {
            for (final Object o : objects) {
                if (o instanceof Card) {
                    final Card c = (Card) o;
                    // indestructible
                    if (c.hasKeyword("Indestructible")) {
                        continue;
                    }

                    // already regenerated
                    if (c.getShieldCount() > 0) {
                        continue;
                    }

                    if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
                        if ((tgt == null && !grantIndestructible) || (!grantShroud && !grantIndestructible)) {
                            continue;
                        }
                    }
                    if (saviourApi == ApiType.Protection) {
                        if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) {
                            continue;
                        }
                    }

                    // don't bounce or blink a permanent that the human
                    // player owns or is a token
                    if (saviourApi == ApiType.ChangeZone && (c.getOwner().isOpponentOf(aiPlayer) || c.isToken())) {
                        continue;
                    }

                    // don't use it on creatures that can't be regenerated
                    if (saviourApi == ApiType.Regenerate && !c.canBeShielded()) {
                        continue;
                    }
                    threatened.add(c);
                }
            }
        }
        // Exiling => bounce/shroud
        else if ((threatApi == ApiType.ChangeZone || threatApi == ApiType.ChangeZoneAll)
                && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
                        || saviourApi == ApiType.Protection || saviourApi == null)
                && topStack.hasParam("Destination") && topStack.getParam("Destination").equals("Exile")) {
            for (final Object o : objects) {
                if (o instanceof Card) {
                    final Card c = (Card) o;
                    // give Shroud to targeted creatures
                    if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll && tgt == null)
                            && !grantShroud) {
                        continue;
                    }
                    if (saviourApi == ApiType.Protection) {
                        if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) {
                            continue;
                        }
                    }

                    // don't bounce or blink a permanent that the human
                    // player owns or is a token
                    if (saviourApi == ApiType.ChangeZone && (c.getOwner().isOpponentOf(aiPlayer) || c.isToken())) {
                        continue;
                    }

                    threatened.add(c);
                }
            }
        }
        //GainControl
        else if ((threatApi == ApiType.GainControl || (threatApi == ApiType.Attach && topStack.hasParam("AILogic")
                && topStack.getParam("AILogic").equals("GainControl")))
                && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
                        || saviourApi == ApiType.Protection || saviourApi == null)) {
            for (final Object o : objects) {
                if (o instanceof Card) {
                    final Card c = (Card) o;
                    // give Shroud to targeted creatures
                    if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll && tgt == null)
                            && !grantShroud) {
                        continue;
                    }
                    if (saviourApi == ApiType.Protection) {
                        if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) {
                            continue;
                        }
                    }
                    threatened.add(c);
                }
            }
        }

        Iterables.addAll(threatened,
                ComputerUtil.predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility()));
        return threatened;
    }

    public static boolean playImmediately(Player ai, SpellAbility sa) {
        final Card source = sa.getHostCard();
        final Zone zone = source.getZone();
        final Game game = source.getGame();

        if (sa.isTrigger()) {
            return true;
        }

        if (zone.getZoneType() == ZoneType.Battlefield) {
            if (predictThreatenedObjects(ai, null).contains(source)) {
                return true;
            }
            if (game.getPhaseHandler().inCombat()
                    && ComputerUtilCombat.combatantWouldBeDestroyed(ai, source, game.getCombat())) {
                return true;
            }
        }
        return false;
    }

    // Computer mulligans if there are no cards with converted mana cost of 0 in its hand
    public static boolean wantMulligan(Player ai) {
        final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand);
        final LearnedAiController aic = ((LearnedPlayerControllerAi) ai.getController()).getAi();

        // don't mulligan when already too low
        if (handList.size() < aic.getIntProperty(AiProps.MULLIGAN_THRESHOLD)) {
            return false;
        }

        final CardCollectionView lands = CardLists.filter(handList, new Predicate<Card>() {
            @Override
            public boolean apply(final Card c) {
                if (c.getManaCost().getCMC() > 0 || c.hasSVar("NeedsToPlay")
                        || (!c.getType().isLand() && !c.getType().isArtifact())) {
                    return false;
                }
                return true;
            }
        });

        // mulligan when at the threshold, with a no land hand
        if (handList.size() == aic.getIntProperty(AiProps.MULLIGAN_THRESHOLD) && !lands.isEmpty()) {
            return false;
        }

        return lands.size() < 2;
    }

    public static CardCollection getPartialParisCandidates(Player ai) {
        final CardCollection candidates = new CardCollection();
        final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand);

        final CardCollection lands = CardLists.getValidCards(handList, "Card.Land", ai, null);
        final CardCollection nonLands = CardLists.getValidCards(handList, "Card.nonLand", ai, null);
        CardLists.sortByCmcDesc(nonLands);

        if (lands.size() >= 3 && lands.size() <= 4) {
            return candidates;
        }
        if (lands.size() < 3) {
            //Not enough lands!
            int tgtCandidates = Math.max(Math.abs(lands.size() - nonLands.size()), 3);
            System.out.println("Partial Paris: " + ai.getName() + " lacks lands, aiming to exile " + tgtCandidates
                    + " cards.");

            for (int i = 0; i < tgtCandidates; i++) {
                candidates.add(nonLands.get(i));
            }
        } else {
            //Too many lands!
            //Init
            int cntColors = MagicColor.WUBRG.length;
            List<CardCollection> numProducers = new ArrayList<CardCollection>(cntColors);
            for (byte col : MagicColor.WUBRG) {
                numProducers.add(col, new CardCollection());
            }

            for (Card c : lands) {
                for (SpellAbility sa : c.getManaAbilities()) {
                    AbilityManaPart abmana = sa.getManaPart();
                    for (byte col : MagicColor.WUBRG) {
                        if (abmana.canProduce(MagicColor.toLongString(col))) {
                            numProducers.get(col).add(c);
                        }
                    }
                }
            }
        }

        System.out.print("Partial Paris: " + ai.getName() + " may exile ");
        for (Card c : candidates) {
            System.out.print(c.toString() + ", ");
        }
        System.out.println();

        if (candidates.size() < 2) {
            candidates.clear();
        }
        return candidates;
    }

    public static boolean scryWillMoveCardToBottomOfLibrary(Player player, Card c) {
        boolean bottom = false;
        if (c.isBasicLand()) {
            CardCollection cl = CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
                    CardPredicates.Presets.LANDS);
            bottom = cl.size() > 5; // if control more than 5 Basic land, probably don't need more
        } else if (c.isCreature()) {
            CardCollection cl = CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
                    CardPredicates.Presets.CREATURES);
            bottom = cl.size() > 5; // if control more than 5 Creatures, probably don't need more
        }
        return bottom;
    }

    public static CardCollection getCardsToDiscardFromOpponent(Player chooser, Player discarder, SpellAbility sa,
            CardCollection validCards, int min, int max) {
        CardCollection goodChoices = CardLists.filter(validCards, new Predicate<Card>() {
            @Override
            public boolean apply(final Card c) {
                if (c.hasSVar("DiscardMeByOpp") || c.hasSVar("DiscardMe")) {
                    return false;
                }
                return true;
            }
        });
        if (goodChoices.isEmpty()) {
            goodChoices = validCards;
        }
        final CardCollection dChoices = new CardCollection();
        if (sa.hasParam("DiscardValid")) {
            final String validString = sa.getParam("DiscardValid");
            if (validString.contains("Creature") && !validString.contains("nonCreature")) {
                final Card c = ComputerUtilCard.getBestCreatureAI(goodChoices);
                if (c != null) {
                    dChoices.add(ComputerUtilCard.getBestCreatureAI(goodChoices));
                }
            }
        }

        Collections.sort(goodChoices, CardLists.TextLenComparator);

        CardLists.sortByCmcDesc(goodChoices);
        dChoices.add(goodChoices.get(0));

        return Aggregates.random(goodChoices, min, new CardCollection());
    }

    public static CardCollection getCardsToDiscardFromFriend(Player aiChooser, Player p, SpellAbility sa,
            CardCollection validCards, int min, int max) {
        if (p == aiChooser) { // ask that ai player what he would like to discard
            final LearnedAiController aic = ((LearnedPlayerControllerAi) p.getController()).getAi();
            return aic.getCardsToDiscard(min, max, validCards, sa);
        }
        // no special options for human or remote friends
        return getCardsToDiscardFromOpponent(aiChooser, p, sa, validCards, min, max);
    }

    public static String chooseSomeType(Player ai, String kindOfType, String logic, List<String> invalidTypes) {
        if (invalidTypes == null) {
            invalidTypes = ImmutableList.<String>of();
        }

        final Game game = ai.getGame();
        String chosen = "";
        if (kindOfType.equals("Card")) {
            // TODO
            // computer will need to choose a type
            // based on whether it needs a creature or land,
            // otherwise, lib search for most common type left
            // then, reveal chosenType to Human
            if (game.getPhaseHandler().is(PhaseType.UNTAP) && logic == null) { // Storage Matrix
                double amount = 0;
                for (String type : CardType.getAllCardTypes()) {
                    if (!invalidTypes.contains(type)) {
                        CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
                                CardPredicates.isType(type), Presets.TAPPED);
                        double i = type.equals("Creature") ? list.size() * 1.5 : list.size();
                        if (i > amount) {
                            amount = i;
                            chosen = type;
                        }
                    }
                }
            }
            if (StringUtils.isEmpty(chosen)) {
                chosen = "Creature";
            }
        } else if (kindOfType.equals("Creature")) {
            Player opp = ai.getOpponent();
            if (logic != null) {
                if (logic.equals("MostProminentOnBattlefield")) {
                    chosen = ComputerUtilCard.getMostProminentCreatureType(game.getCardsIn(ZoneType.Battlefield));
                } else if (logic.equals("MostProminentComputerControls")) {
                    chosen = ComputerUtilCard.getMostProminentCreatureType(ai.getCardsIn(ZoneType.Battlefield));
                } else if (logic.equals("MostProminentHumanControls")) {
                    chosen = ComputerUtilCard.getMostProminentCreatureType(opp.getCardsIn(ZoneType.Battlefield));
                    if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
                        chosen = ComputerUtilCard.getMostProminentCreatureType(
                                CardLists.filterControlledBy(game.getCardsInGame(), opp));
                    }
                } else if (logic.equals("MostProminentInComputerDeck")) {
                    chosen = ComputerUtilCard
                            .getMostProminentCreatureType(CardLists.filterControlledBy(game.getCardsInGame(), ai));
                } else if (logic.equals("MostProminentInComputerGraveyard")) {
                    chosen = ComputerUtilCard.getMostProminentCreatureType(ai.getCardsIn(ZoneType.Graveyard));
                }
            }
            if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
                chosen = "Sliver";
            }

        } else if (kindOfType.equals("Basic Land")) {
            if (logic != null) {
                if (logic.equals("MostNeededType")) {
                    // Choose a type that is in the deck, but not in hand or on the battlefield
                    final List<String> basics = new ArrayList<String>();
                    basics.addAll(CardType.Constant.BASIC_TYPES);
                    CardCollectionView presentCards = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield),
                            ai.getCardsIn(ZoneType.Hand));
                    CardCollectionView possibleCards = ai.getAllCards();

                    for (String b : basics) {
                        if (!Iterables.any(presentCards, CardPredicates.isType(b))
                                && Iterables.any(possibleCards, CardPredicates.isType(b))) {
                            chosen = b;
                        }
                    }
                    if (chosen.equals("")) {
                        for (String b : basics) {
                            if (Iterables.any(possibleCards, CardPredicates.isType(b))) {
                                chosen = b;
                            }
                        }
                    }
                } else if (logic.equals("ChosenLandwalk")) {
                    for (Card c : ai.getOpponent().getLandsInPlay()) {
                        for (String t : c.getType()) {
                            if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
                                chosen = t;
                                break;
                            }
                        }
                    }
                }
            }

            if (!CardType.isABasicLandType(chosen) || invalidTypes.contains(chosen)) {
                chosen = "Island";
            }
        } else if (kindOfType.equals("Land")) {
            if (logic != null) {
                if (logic.equals("ChosenLandwalk")) {
                    for (Card c : ai.getOpponent().getLandsInPlay()) {
                        for (String t : c.getType().getLandTypes()) {
                            if (!invalidTypes.contains(t)) {
                                chosen = t;
                                break;
                            }
                        }
                    }
                }
            }
            if (StringUtils.isEmpty(chosen)) {
                chosen = "Island";
            }
        }
        return chosen;
    }

    public static Object vote(Player ai, List<Object> options, SpellAbility sa, Multimap<Object, Player> votes) {
        if (!sa.hasParam("AILogic")) {
            return Aggregates.random(options);
        } else {
            String logic = sa.getParam("AILogic");
            switch (logic) {
            case "Torture":
                return "Torture";
            case "GraceOrCondemnation":
                return ai.getCreaturesInPlay().size() > ai.getOpponent().getCreaturesInPlay().size() ? "Grace"
                        : "Condemnation";
            case "CarnageOrHomage":
                CardCollection cardsInPlay = CardLists
                        .getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land");
                CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
                CardCollection computerlist = CardLists.filterControlledBy(cardsInPlay, ai);
                return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard
                        .evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
            case "Judgment":
                if (votes.isEmpty()) {
                    CardCollection list = new CardCollection();
                    for (Object o : options) {
                        if (o instanceof Card) {
                            list.add((Card) o);
                        }
                    }
                    return ComputerUtilCard.getBestAI(list);
                } else {
                    return Iterables.getFirst(votes.keySet(), null);
                }
            case "Protection":
                if (votes.isEmpty()) {
                    List<String> restrictedToColors = new ArrayList<String>();
                    for (Object o : options) {
                        if (o instanceof String) {
                            restrictedToColors.add((String) o);
                        }
                    }
                    CardCollection lists = CardLists.filterControlledBy(ai.getGame().getCardsInGame(),
                            ai.getOpponents());
                    return StringUtils
                            .capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
                } else {
                    return Iterables.getFirst(votes.keySet(), null);
                }
            default:
                return Iterables.getFirst(options, null);
            }
        }
    }

    public static CardCollection getSafeTargets(final Player ai, SpellAbility sa, CardCollectionView validCards) {
        CardCollection safeCards = new CardCollection(validCards);
        safeCards = CardLists.filter(safeCards, new Predicate<Card>() {
            @Override
            public boolean apply(final Card c) {
                if (c.getController() == ai) {
                    if (c.getSVar("Targeting").equals("Dies") || c.getSVar("Targeting").equals("Counter"))
                        return false;
                }
                return true;
            }
        });
        return safeCards;
    }

    public static Card getKilledByTargeting(final SpellAbility sa, CardCollectionView validCards) {
        CardCollection killables = new CardCollection(validCards);
        killables = CardLists.filter(killables, new Predicate<Card>() {
            @Override
            public boolean apply(final Card c) {
                return c.getController() != sa.getActivatingPlayer() && c.getSVar("Targeting").equals("Dies");
            }
        });
        return ComputerUtilCard.getBestCreatureAI(killables);
    }

    public static int getDamageForPlaying(final Player player, final SpellAbility sa) {

        // check for bad spell cast triggers
        int damage = 0;
        final Game game = player.getGame();
        final Card card = sa.getHostCard();
        final FCollection<Trigger> theTriggers = new FCollection<Trigger>();

        for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
            theTriggers.addAll(c.getTriggers());
        }
        for (Trigger trigger : theTriggers) {
            Map<String, String> trigParams = trigger.getMapParams();
            final Card source = trigger.getHostCard();

            if (!trigger.zonesCheck(game.getZoneOf(source))) {
                continue;
            }
            if (!trigger.requirementsCheck(game)) {
                continue;
            }
            TriggerType mode = trigger.getMode();
            if (mode != TriggerType.SpellCast) {
                continue;
            }
            if (trigParams.containsKey("ValidCard")) {
                if (!card.isValid(trigParams.get("ValidCard"), source.getController(), source)) {
                    continue;
                }
            }

            if (trigParams.containsKey("ValidActivatingPlayer")) {
                if (!player.isValid(trigParams.get("ValidActivatingPlayer"), source.getController(), source)) {
                    continue;
                }
            }

            String ability = source.getSVar(trigParams.get("Execute"));
            if (ability.isEmpty()) {
                continue;
            }

            final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
            if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("DealDamage"))
                    || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("DealDamage"))) {
                if (!"TriggeredActivator".equals(abilityParams.get("Defined"))) {
                    continue;
                }
                if (!abilityParams.containsKey("NumDmg")) {
                    continue;
                }
                damage += ComputerUtilCombat.predictDamageTo(player,
                        AbilityUtils.calculateAmount(source, abilityParams.get("NumDmg"), null), source, false);
            } else if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("LoseLife"))
                    || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("LoseLife"))) {
                if (!"TriggeredActivator".equals(abilityParams.get("Defined"))) {
                    continue;
                }
                if (!abilityParams.containsKey("LifeAmount")) {
                    continue;
                }
                damage += AbilityUtils.calculateAmount(source, abilityParams.get("LifeAmount"), null);
            }
        }

        return damage;
    }

    public static int getDamageFromETB(final Player player, final Card permanent) {
        int damage = 0;
        final Game game = player.getGame();
        final FCollection<Trigger> theTriggers = new FCollection<Trigger>();

        for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
            theTriggers.addAll(card.getTriggers());
        }
        for (Trigger trigger : theTriggers) {
            Map<String, String> trigParams = trigger.getMapParams();
            final Card source = trigger.getHostCard();

            if (!trigger.zonesCheck(game.getZoneOf(source))) {
                continue;
            }
            if (!trigger.requirementsCheck(game)) {
                continue;
            }
            if (trigParams.containsKey("CheckOnTriggeredCard")
                    && AbilityUtils
                            .getDefinedCards(permanent,
                                    source.getSVar(trigParams.get("CheckOnTriggeredCard").split(" ")[0]), null)
                            .isEmpty()) {
                continue;
            }
            TriggerType mode = trigger.getMode();
            if (mode != TriggerType.ChangesZone) {
                continue;
            }
            if (!"Battlefield".equals(trigParams.get("Destination"))) {
                continue;
            }
            if (trigParams.containsKey("ValidCard")) {
                if (!permanent.isValid(trigParams.get("ValidCard"), source.getController(), source)) {
                    continue;
                }
            }

            String ability = source.getSVar(trigParams.get("Execute"));
            if (ability.isEmpty()) {
                continue;
            }

            final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
            // Destroy triggers
            if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("DealDamage"))
                    || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("DealDamage"))) {
                if (!"TriggeredCardController".equals(abilityParams.get("Defined"))) {
                    continue;
                }
                if (!abilityParams.containsKey("NumDmg")) {
                    continue;
                }
                damage += ComputerUtilCombat.predictDamageTo(player,
                        AbilityUtils.calculateAmount(source, abilityParams.get("NumDmg"), null), source, false);
            } else if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("LoseLife"))
                    || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("LoseLife"))) {
                if (!"TriggeredCardController".equals(abilityParams.get("Defined"))) {
                    continue;
                }
                if (!abilityParams.containsKey("LifeAmount")) {
                    continue;
                }
                damage += AbilityUtils.calculateAmount(source, abilityParams.get("LifeAmount"), null);
            }
        }
        return damage;
    }

    public static boolean isNegativeCounter(CounterType type, Card c) {
        return type == CounterType.AGE || type == CounterType.BLAZE || type == CounterType.BRIBERY
                || type == CounterType.DOOM || type == CounterType.ICE || type == CounterType.M1M1
                || type == CounterType.M0M2 || type == CounterType.M0M1 || type == CounterType.M1M0
                || type == CounterType.M2M1 || type == CounterType.M2M2 || type == CounterType.MUSIC
                || type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
                || type == CounterType.SLEIGHT || (type == CounterType.TIME && !c.isInPlay())
                || type == CounterType.WAGE;
    }

    public static Player evaluateBoardPosition(final List<Player> listToEvaluate) {
        Player bestBoardPosition = listToEvaluate.get(0);
        int bestBoardRating = 0;

        for (final Player p : listToEvaluate) {
            int pRating = p.getLife() * 3;
            pRating += p.getLandsInPlay().size() * 2;

            for (final Card c : p.getCardsIn(ZoneType.Battlefield)) {
                pRating += ComputerUtilCard.evaluateCreature(c) / 3;
            }

            if (p.getCardsIn(ZoneType.Library).size() < 3) {
                pRating /= 5;
            }

            System.out.println("Board position evaluation for " + p + ": " + pRating);

            if (pRating > bestBoardRating) {
                bestBoardRating = pRating;
                bestBoardPosition = p;
            }
        }
        return bestBoardPosition;
    }

}