private Card chooseCardOnStack(SpellAbility sa, Player ai, Game game) { for (SpellAbilityStackInstance si : game.getStack()) { final Card source = si.getSourceCard(); final SpellAbility abilityOnStack = si.getSpellAbility(true); if (sa.hasParam("Choices") && !abilityOnStack.getHostCard().isValid(sa.getParam("Choices"), ai, sa.getHostCard())) { continue; } final ApiType threatApi = abilityOnStack.getApi(); if (threatApi != ApiType.DealDamage && threatApi != ApiType.DamageAll) { continue; } List<? extends GameObject> objects = getTargets(abilityOnStack); if (!abilityOnStack.usesTargeting() && !abilityOnStack.hasParam("Defined") && abilityOnStack.hasParam("ValidPlayers")) objects = AbilityUtils.getDefinedPlayers( source, abilityOnStack.getParam("ValidPlayers"), abilityOnStack); if (!objects.contains(ai) || abilityOnStack.hasParam("NoPrevention")) { continue; } int dmg = AbilityUtils.calculateAmount(source, abilityOnStack.getParam("NumDmg"), abilityOnStack); if (ComputerUtilCombat.predictDamageTo(ai, dmg, source, false) <= 0) { continue; } return source; } return null; }
/** {@inheritDoc} */ @Override public boolean canPlay() { final Card card = this.getHostCard(); Player activator = this.getActivatingPlayer(); if (activator == null) { activator = card.getController(); if (activator == null) { return false; } } final Game game = activator.getGame(); if (game.getStack().isSplitSecondOnStack()) { return false; } if (!(card.isInstant() || activator.canCastSorcery() || card.hasKeyword("Flash") || this.getRestrictions().isInstantSpeed() || activator.hasKeyword("You may cast nonland cards as though they had flash.") || card.hasStartOfKeyword("You may cast CARDNAME as though it had flash."))) { return false; } if (!this.getRestrictions().canPlay(card, this)) { return false; } // for uncastables like lotus bloom, check if manaCost is blank (except for morph spells) if (!isCastFaceDown() && isBasicSpell() && card.getState(card.isFaceDown() ? CardStateName.Original : card.getCurrentStateName()) .getManaCost() .isNoCost()) { return false; } if (this.getPayCosts() != null) { if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this)) { return false; } } return checkOtherRestrictions(); } // canPlay()
@Override public Card chooseSingleCard( final Player aiChoser, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) { if ("NeedsPrevention".equals(sa.getParam("AILogic"))) { final Player ai = sa.getActivatingPlayer(); final Game game = ai.getGame(); if (!game.getStack().isEmpty()) { Card choseCard = chooseCardOnStack(sa, ai, game); if (choseCard != null) { return choseCard; } } final Combat combat = game.getCombat(); List<Card> permanentSources = CardLists.filter( options, new Predicate<Card>() { @Override public boolean apply(final Card c) { if (c == null || c.getZone() == null || c.getZone().getZoneType() != ZoneType.Battlefield || combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) { return false; } return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0; } }); return ComputerUtilCard.getBestCreatureAI(permanentSources); } else { return ComputerUtilCard.getBestAI(options); } }
private void passPriority(final Runnable runnable) { if (FModel.getPreferences().getPrefBoolean(FPref.UI_MANA_LOST_PROMPT)) { // if gui player has mana floating that will be lost if phase ended right now, prompt before // passing priority final Game game = player.getGame(); if (game.getStack().isEmpty()) { // phase can't end right now if stack isn't empty Player player = game.getPhaseHandler().getPriorityPlayer(); if (player != null && player.getManaPool().willManaBeLostAtEndOfPhase() && player.getLobbyPlayer() == GamePlayerUtil.getGuiPlayer()) { ThreadUtil.invokeInGameThread( new Runnable() { // must invoke in game thread so dialog can be shown on mobile game @Override public void run() { String message = "You have mana floating in your mana pool that could be lost if you pass priority now."; if (FModel.getPreferences().getPrefBoolean(FPref.UI_MANABURN)) { message += " You will take mana burn damage equal to the amount of floating mana lost this way."; } if (SOptionPane.showOptionDialog( message, "Mana Floating", SOptionPane.WARNING_ICON, new String[] {"OK", "Cancel"}) == 0) { runnable.run(); } } }); return; } } } runnable.run(); // just pass priority immediately if no mana floating that would be lost }
/* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final Cost cost = sa.getPayCosts(); final Game game = ai.getGame(); final PhaseHandler ph = game.getPhaseHandler(); final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<String>(); final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : ""; final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : ""; if (!ComputerUtilCost.checkLifeCost(ai, cost, sa.getHostCard(), 4, null)) { return false; } if (!ComputerUtilCost.checkDiscardCost(ai, cost, sa.getHostCard())) { return false; } if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, sa.getHostCard())) { return false; } if (!ComputerUtilCost.checkRemoveCounterCost(cost, sa.getHostCard())) { return false; } if (!ComputerUtilCost.checkTapTypeCost(ai, cost, sa.getHostCard())) { return false; } if (game.getStack().isEmpty() && hasTapCost(cost, sa.getHostCard())) { if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) { return false; } if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.isPlayerTurn(ai.getOpponent())) { return false; } } if (ComputerUtil.preventRunAwayActivations(sa)) { return false; } // Phase Restrictions if (game.getStack().isEmpty() && ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) { // Instant-speed pumps should not be cast outside of combat when the // stack is empty if (!sa.isCurse() && !SpellAbilityAi.isSorcerySpeed(sa)) { return false; } } else if (!game.getStack().isEmpty() && !sa.isCurse()) { return pumpAgainstRemoval(ai, sa); } if (sa.hasParam("ActivationNumberSacrifice")) { final SpellAbilityRestriction restrict = sa.getRestrictions(); final int sacActivations = Integer.parseInt(sa.getParam("ActivationNumberSacrifice").substring(2)); final int activations = restrict.getNumberTurnActivations(); // don't risk sacrificing a creature just to pump it if (activations >= sacActivations - 1) { return false; } } final Card source = sa.getHostCard(); if (source.getSVar("X").equals("Count$xPaid")) { source.setSVar("PayX", ""); } int defense; if (numDefense.contains("X") && source.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); source.setSVar("PayX", Integer.toString(xPay)); defense = xPay; if (numDefense.equals("-X")) { defense = -xPay; } } else { defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa); } int attack; if (numAttack.contains("X") && source.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. final String toPay = source.getSVar("PayX"); if (toPay.equals("")) { final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); source.setSVar("PayX", Integer.toString(xPay)); attack = xPay; } else { attack = Integer.parseInt(toPay); } } else { attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa); } if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0)) { return false; } // Untargeted if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); if (cards.size() == 0) { return false; } // when this happens we need to expand AI to consider if its ok for // everything? for (final Card card : cards) { if (sa.isCurse()) { if (!card.getController().isOpponentOf(ai)) { return false; } if (!containsUsefulKeyword(ai, keywords, card, sa, attack)) { continue; } return true; } if (!card.getController().isOpponentOf(ai) && shouldPumpCard(ai, sa, card, defense, attack, keywords)) { return true; } } return false; } // Targeted if (!this.pumpTgtAI(ai, sa, defense, attack, false)) { return false; } return true; } // pumpPlayAI()
private boolean pumpTgtAI( final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory) { final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<String>(); final Game game = ai.getGame(); final Card source = sa.getHostCard(); if (!mandatory && !sa.isTrigger() && game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) && !(sa.isCurse() && defense < 0) && !this.containsNonCombatKeyword(keywords) && !sa.hasParam("UntilYourNextTurn")) { return false; } final Player opp = ai.getOpponent(); final TargetRestrictions tgt = sa.getTargetRestrictions(); sa.resetTargets(); if (sa.hasParam("TargetingPlayer") && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) { Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); sa.setTargetingPlayer(targetingPlayer); return targetingPlayer.getController().chooseTargetsFor(sa); } List<Card> list = new ArrayList<Card>(); if (sa.hasParam("AILogic")) { if (sa.getParam("AILogic").equals("HighestPower")) { list = CardLists.getValidCards( CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source); list = CardLists.getTargetableCards(list, sa); CardLists.sortByPowerDesc(list); if (!list.isEmpty()) { sa.getTargets().add(list.get(0)); return true; } else { return false; } } if (sa.getParam("AILogic").equals("Fight") || sa.getParam("AILogic").equals("PowerDmg")) { final AbilitySub tgtFight = sa.getSubAbility(); List<Card> aiCreatures = ai.getCreaturesInPlay(); aiCreatures = CardLists.getTargetableCards(aiCreatures, sa); aiCreatures = ComputerUtil.getSafeTargets(ai, sa, aiCreatures); ComputerUtilCard.sortByEvaluateCreature(aiCreatures); // sort is suboptimal due to conflicting needs depending on game state: // -deathtouch for deal damage // -max power for damage to player // -survivability for generic "fight" // -no support for "heroic" List<Card> humCreatures = ai.getOpponent().getCreaturesInPlay(); humCreatures = CardLists.getTargetableCards(humCreatures, tgtFight); ComputerUtilCard.sortByEvaluateCreature(humCreatures); if (humCreatures.isEmpty() || aiCreatures.isEmpty()) { return false; } int buffedAtk = attack, buffedDef = defense; for (Card humanCreature : humCreatures) { for (Card aiCreature : aiCreatures) { if (sa.isSpell()) { // heroic triggers adding counters for (Trigger t : aiCreature.getTriggers()) { if (t.getMode() == TriggerType.SpellCast) { final Map<String, String> params = t.getMapParams(); if ("Card.Self".equals(params.get("TargetsValid")) && "You".equals(params.get("ValidActivatingPlayer")) && params.containsKey("Execute")) { SpellAbility heroic = AbilityFactory.getAbility( aiCreature.getSVar(params.get("Execute")), aiCreature); if ("Self".equals(heroic.getParam("Defined")) && "P1P1".equals(heroic.getParam("CounterType"))) { int amount = AbilityUtils.calculateAmount( aiCreature, heroic.getParam("CounterNum"), heroic); buffedAtk += amount; buffedDef += amount; } break; } } } } if (sa.getParam("AILogic").equals("PowerDmg")) { if (FightAi.canKill(aiCreature, humanCreature, buffedAtk)) { sa.getTargets().add(aiCreature); tgtFight.getTargets().add(humanCreature); return true; } } else { if (FightAi.shouldFight(aiCreature, humanCreature, buffedAtk, buffedDef)) { sa.getTargets().add(aiCreature); tgtFight.getTargets().add(humanCreature); return true; } } } } } return false; } else if (sa.isCurse()) { if (sa.canTarget(opp)) { sa.getTargets().add(opp); return true; } list = this.getCurseCreatures(ai, sa, defense, attack, keywords); } else { if (!tgt.canTgtCreature()) { ZoneType zone = tgt.getZone().get(0); list = game.getCardsIn(zone); } else { list = this.getPumpCreatures(ai, sa, defense, attack, keywords); } if (sa.canTarget(ai)) { sa.getTargets().add(ai); return true; } } list = CardLists.getValidCards(list, tgt.getValidTgts(), ai, source); if (game.getStack().isEmpty()) { // If the cost is tapping, don't activate before declare // attack/block if (sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && game.getPhaseHandler().isPlayerTurn(ai)) { list.remove(sa.getHostCard()); } if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && game.getPhaseHandler().isPlayerTurn(opp)) { list.remove(sa.getHostCard()); } } } if (list.isEmpty()) { return mandatory && this.pumpMandatoryTarget(ai, sa, mandatory); } if (!sa.isCurse()) { // Don't target cards that will die. list = ComputerUtil.getSafeTargets(ai, sa, list); } while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { Card t = null; // boolean goodt = false; if (list.isEmpty()) { if ((sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa)) || (sa.getTargets().getNumTargeted() == 0)) { if (mandatory) { return this.pumpMandatoryTarget(ai, sa, mandatory); } sa.resetTargets(); return false; } else { // TODO is this good enough? for up to amounts? break; } } t = ComputerUtilCard.getBestAI(list); // option to hold removal instead only applies for single targeted removal if (!sa.isTrigger() && tgt.getMaxTargets(source, sa) == 1 && sa.isCurse()) { if (!ComputerUtilCard.useRemovalNow(sa, t, -defense, ZoneType.Graveyard)) { return false; } } sa.getTargets().add(t); list.remove(t); } return true; } // pumpTgtAI()
/* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override protected boolean canPlayAI(final Player ai, SpellAbility sa) { // TODO: AI Support! Currently this is copied from AF ChooseCard. // When implementing AI, I believe AI also needs to be made aware of the damage sources // chosen // to be prevented (e.g. so the AI doesn't attack with a creature that will not deal any // damage // to the player because a CoP was pre-activated on it - unless, of course, there's // another // possible reason to attack with that creature). final Card host = sa.getHostCard(); final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); if (abCost != null) { // AI currently disabled for these costs if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { return false; } if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) { return false; } if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { return false; } if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) { return false; } } final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); if (sa.canTarget(ai.getOpponent())) { sa.getTargets().add(ai.getOpponent()); } else { return false; } } if (sa.hasParam("AILogic")) { final Game game = ai.getGame(); if (sa.getParam("AILogic").equals("NeedsPrevention")) { if (!game.getStack().isEmpty()) { final SpellAbility topStack = game.getStack().peekAbility(); if (sa.hasParam("Choices") && !topStack.getHostCard().isValid(sa.getParam("Choices"), ai, source)) { return false; } final ApiType threatApi = topStack.getApi(); if (threatApi != ApiType.DealDamage && threatApi != ApiType.DamageAll) { return false; } final Card threatSource = topStack.getHostCard(); List<? extends GameObject> objects = getTargets(topStack); if (!topStack.usesTargeting() && topStack.hasParam("ValidPlayers") && !topStack.hasParam("Defined")) { objects = AbilityUtils.getDefinedPlayers( threatSource, topStack.getParam("ValidPlayers"), topStack); } if (!objects.contains(ai) || topStack.hasParam("NoPrevention")) { return false; } int dmg = AbilityUtils.calculateAmount(threatSource, topStack.getParam("NumDmg"), topStack); if (ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) <= 0) { return false; } return true; } if (game.getPhaseHandler().getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) { return false; } CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield); if (sa.hasParam("Choices")) { choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host); } final Combat combat = game.getCombat(); choices = CardLists.filter( choices, new Predicate<Card>() { @Override public boolean apply(final Card c) { if (combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) { return false; } return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0; } }); if (choices.isEmpty()) { return false; } } } return true; }
/** {@inheritDoc} */ @Override public final boolean performTest(final java.util.Map<String, Object> runParams2) { final SpellAbility spellAbility = (SpellAbility) runParams2.get("CastSA"); if (spellAbility == null) { System.out.println( "TriggerSpellAbilityCast performTest encountered spellAbility == null. runParams2 = " + runParams2); return false; } final Card cast = spellAbility.getHostCard(); final Game game = cast.getGame(); final SpellAbilityStackInstance si = game.getStack().getInstanceFromSpellAbility(spellAbility); if (this.getMode() == TriggerType.SpellCast) { if (!spellAbility.isSpell()) { return false; } } else if (this.getMode() == TriggerType.AbilityCast) { if (!spellAbility.isAbility()) { return false; } } else if (this.getMode() == TriggerType.SpellAbilityCast) { // Empty block for readability. } if (this.mapParams.containsKey("ActivatedOnly")) { if (spellAbility.isTrigger()) { return false; } } if (this.mapParams.containsKey("ValidControllingPlayer")) { if (!matchesValid( cast.getController(), this.mapParams.get("ValidControllingPlayer").split(","), this.getHostCard())) { return false; } } if (this.mapParams.containsKey("ValidActivatingPlayer")) { if (si == null || !matchesValid( si.getSpellAbility(true).getActivatingPlayer(), this.mapParams.get("ValidActivatingPlayer").split(","), this.getHostCard())) { return false; } if (this.mapParams.containsKey("ActivatorThisTurnCast")) { String compare = this.mapParams.get("ActivatorThisTurnCast"); List<Card> thisTurnCast = CardUtil.getThisTurnCast( this.mapParams.containsKey("ValidCard") ? this.mapParams.get("ValidCard") : "Card", this.getHostCard()); thisTurnCast = CardLists.filterControlledBy( thisTurnCast, si.getSpellAbility(true).getActivatingPlayer()); int left = thisTurnCast.size(); int right = Integer.parseInt(compare.substring(2)); if (!Expressions.compare(left, compare, right)) { return false; } } } if (this.mapParams.containsKey("ValidCard")) { if (!matchesValid(cast, this.mapParams.get("ValidCard").split(","), this.getHostCard())) { return false; } } if (this.mapParams.containsKey("TargetsValid")) { SpellAbility sa = spellAbility; if (si != null) { sa = si.getSpellAbility(true); } boolean validTgtFound = false; while (sa != null && !validTgtFound) { for (final Card tgt : sa.getTargets().getTargetCards()) { if (tgt.isValid( this.mapParams.get("TargetsValid").split(","), this.getHostCard().getController(), this.getHostCard())) { validTgtFound = true; break; } } for (final Player p : sa.getTargets().getTargetPlayers()) { if (matchesValid(p, this.mapParams.get("TargetsValid").split(","), this.getHostCard())) { validTgtFound = true; break; } } sa = sa.getSubAbility(); } if (!validTgtFound) { return false; } } if (this.mapParams.containsKey("NonTapCost")) { final Cost cost = (Cost) (runParams2.get("Cost")); if (cost.hasTapCost()) { return false; } } if (this.mapParams.containsKey("Conspire")) { if (!spellAbility.isOptionalCostPaid(OptionalCost.Conspire)) { return false; } if (spellAbility.getConspireInstances() == 0) { return false; } else { spellAbility.subtractConspireInstance(); // System.out.println("Conspire instances left = " + spellAbility.getConspireInstances()); } } if (this.mapParams.containsKey("Outlast")) { if (!spellAbility.isOutlast()) { return false; } } if (this.mapParams.containsKey("IsSingleTarget")) { int numTargeted = 0; for (TargetChoices tc : spellAbility.getAllTargetChoices()) { numTargeted += tc.getNumTargeted(); } if (numTargeted != 1) { return false; } } if (this.mapParams.containsKey("SpellSpeed")) { if (this.mapParams.get("SpellSpeed").equals("NotSorcerySpeed")) { if (this.getHostCard().getController().couldCastSorcery(spellAbility)) { return false; } if (this.getHostCard() .hasKeyword( "You may cast CARDNAME as though it had flash. If you cast it any time a " + "sorcery couldn't have been cast, the controller of the permanent it becomes sacrifices it at the beginning" + " of the next cleanup step.")) { // for these cards the trigger must only fire if using their own ability to cast at // instant speed if (this.getHostCard().hasKeyword("Flash") || this.getHostCard() .getController() .hasKeyword("You may cast nonland cards as though they had flash.")) { return false; } } return true; } } return true; }