diff --git a/.gitignore b/.gitignore index ae96e35663f..1f236e77908 100644 --- a/.gitignore +++ b/.gitignore @@ -1,238 +1,77 @@ -/*.idea -/*.iml -/*.tmp -/.metadata -/.recommenders -forge-ai/forge-ai.iml -forge-ai/target -forge-core/forge-core.iml -forge-core/target -forge-game/*.iml -forge-game/target -forge-gui-android/*.iml +# Ignore IDEA config files + +*.idea +*.iml +*.tmp +.metadata +.recommenders + + +# Ignore Eclipse config files + +.settings +.classpath +.project + +# Ignore VS Code config files + +.vscode/settings.json +.vscode/launch.json + + +# Ignore NetBeans config files + +nbactions.xml + + +# Ignore binaries, temp files and test output, everywhere + +target +test-output +bin +gen +*.log + + +# TODO: specify what these ignores are for (releasing?) + +forge.profile.properties +/jgv.txt +pom.xml.next +pom.xml.releaseBackup +pom.xml.tag +release.properties + + +# Ignore mobile-related resources + +forge-gui-android/res/*/* +!forge-gui-android/res/*/ic_launcher.png +!forge-gui-android/res/*/ic_launcher*.png +!forge-gui-android/res/layout/main.xml forge-gui-android/*.keystore -forge-gui-android/assets/fallback_skin/Thumbs.db -forge-gui-android/bin -forge-gui-android/gen -forge-gui-android/res/Thumbs.db -forge-gui-android/res/bin -forge-gui-android/res/drawable-hdpi/Thumbs.db -forge-gui-android/res/drawable-hdpi/bin -forge-gui-android/res/drawable-hdpi/gen -forge-gui-android/res/drawable-hdpi/target -forge-gui-android/res/drawable-ldpi/Thumbs.db -forge-gui-android/res/drawable-ldpi/bin -forge-gui-android/res/drawable-ldpi/gen -forge-gui-android/res/drawable-ldpi/target -forge-gui-android/res/drawable-mdpi/Thumbs.db -forge-gui-android/res/drawable-mdpi/bin -forge-gui-android/res/drawable-mdpi/gen -forge-gui-android/res/drawable-mdpi/target -forge-gui-android/res/drawable-xhdpi/Thumbs.db -forge-gui-android/res/drawable-xhdpi/bin -forge-gui-android/res/drawable-xhdpi/gen -forge-gui-android/res/drawable-xhdpi/target -forge-gui-android/res/drawable-xxhdpi/Thumbs.db -forge-gui-android/res/drawable-xxhdpi/bin -forge-gui-android/res/drawable-xxhdpi/gen -forge-gui-android/res/drawable-xxhdpi/target -forge-gui-android/res/gen -forge-gui-android/res/layout/Thumbs.db -forge-gui-android/res/layout/bin -forge-gui-android/res/layout/gen -forge-gui-android/res/layout/target -forge-gui-android/res/target -forge-gui-android/res/values/Thumbs.db -forge-gui-android/res/values/bin -forge-gui-android/res/values/gen -forge-gui-android/res/values/target -forge-gui-android/target -forge-gui-desktop/*.iml -forge-gui-desktop/target -forge-gui-ios/*.iml -forge-gui-ios/target -forge-gui-mobile-dev/*.iml -forge-gui-mobile-dev/bin -forge-gui-mobile-dev/fallback_skin/Thumbs.db +forge-gui-android/**/Thumbs.db +forge-gui-mobile-dev/**/Thumbs.db forge-gui-mobile-dev/res -forge-gui-mobile-dev/target forge-gui-mobile-dev/testAssets -forge-gui-mobile/*.iml -forge-gui-mobile/bin -forge-gui-mobile/target -forge-gui/forge-gui.iml -forge-gui/forge.profile.properties -forge-gui/res/*.log -forge-gui/res/PerSetTrackingResults + forge-gui/res/cardsfolder/*.bat + +forge-gui/res/PerSetTrackingResults forge-gui/res/decks forge-gui/res/layouts forge-gui/res/pics* forge-gui/res/pics_product + +forge-gui/res/skins/**/PerSetTrackingResults +forge-gui/res/skins/**/decks +forge-gui/res/skins/**/layouts +forge-gui/res/skins/**/pics* +forge-gui/res/skins/**/pics_product + forge-gui/res/quest/world/1996-05[!!-~]Ice[!!-~]Age/duels/.directory -forge-gui/res/skins/*.log -forge-gui/res/skins/PerSetTrackingResults -forge-gui/res/skins/Thumbs.db -forge-gui/res/skins/arabian_nights/*.log -forge-gui/res/skins/arabian_nights/PerSetTrackingResults -forge-gui/res/skins/arabian_nights/Thumbs.db -forge-gui/res/skins/arabian_nights/decks -forge-gui/res/skins/arabian_nights/layouts -forge-gui/res/skins/arabian_nights/pics* -forge-gui/res/skins/arabian_nights/pics_product -forge-gui/res/skins/comic/*.log -forge-gui/res/skins/comic/PerSetTrackingResults -forge-gui/res/skins/comic/Thumbs.db -forge-gui/res/skins/comic/decks -forge-gui/res/skins/comic/layouts -forge-gui/res/skins/comic/pics* -forge-gui/res/skins/comic/pics_product -forge-gui/res/skins/dark_ascension/*.log -forge-gui/res/skins/dark_ascension/PerSetTrackingResults -forge-gui/res/skins/dark_ascension/Thumbs.db -forge-gui/res/skins/dark_ascension/decks -forge-gui/res/skins/dark_ascension/layouts -forge-gui/res/skins/dark_ascension/pics* -forge-gui/res/skins/dark_ascension/pics_product -forge-gui/res/skins/decks -forge-gui/res/skins/default/*.log -forge-gui/res/skins/default/PerSetTrackingResults -forge-gui/res/skins/default/Thumbs.db -forge-gui/res/skins/default/decks -forge-gui/res/skins/default/layouts -forge-gui/res/skins/default/pics* -forge-gui/res/skins/default/pics_product -forge-gui/res/skins/firebloom/*.log -forge-gui/res/skins/firebloom/PerSetTrackingResults -forge-gui/res/skins/firebloom/Thumbs.db -forge-gui/res/skins/firebloom/decks -forge-gui/res/skins/firebloom/layouts -forge-gui/res/skins/firebloom/pics* -forge-gui/res/skins/firebloom/pics_product -forge-gui/res/skins/inferno/*.log -forge-gui/res/skins/inferno/PerSetTrackingResults -forge-gui/res/skins/inferno/Thumbs.db -forge-gui/res/skins/inferno/decks -forge-gui/res/skins/inferno/layouts -forge-gui/res/skins/inferno/pics* -forge-gui/res/skins/inferno/pics_product -forge-gui/res/skins/innistrad/*.log -forge-gui/res/skins/innistrad/PerSetTrackingResults -forge-gui/res/skins/innistrad/Thumbs.db -forge-gui/res/skins/innistrad/decks -forge-gui/res/skins/innistrad/layouts -forge-gui/res/skins/innistrad/pics* -forge-gui/res/skins/innistrad/pics_product -forge-gui/res/skins/journeyman/*.log -forge-gui/res/skins/journeyman/PerSetTrackingResults -forge-gui/res/skins/journeyman/Thumbs.db -forge-gui/res/skins/journeyman/decks -forge-gui/res/skins/journeyman/layouts -forge-gui/res/skins/journeyman/pics* -forge-gui/res/skins/journeyman/pics_product -forge-gui/res/skins/kamigawa/*.log -forge-gui/res/skins/kamigawa/PerSetTrackingResults -forge-gui/res/skins/kamigawa/Thumbs.db -forge-gui/res/skins/kamigawa/decks -forge-gui/res/skins/kamigawa/layouts -forge-gui/res/skins/kamigawa/pics* -forge-gui/res/skins/kamigawa/pics_product -forge-gui/res/skins/layouts -forge-gui/res/skins/marble_blue/*.log -forge-gui/res/skins/marble_blue/PerSetTrackingResults -forge-gui/res/skins/marble_blue/Thumbs.db -forge-gui/res/skins/marble_blue/decks -forge-gui/res/skins/marble_blue/layouts -forge-gui/res/skins/marble_blue/pics* -forge-gui/res/skins/marble_blue/pics_product -forge-gui/res/skins/metalcraft/*.log -forge-gui/res/skins/metalcraft/PerSetTrackingResults -forge-gui/res/skins/metalcraft/Thumbs.db -forge-gui/res/skins/metalcraft/decks -forge-gui/res/skins/metalcraft/layouts -forge-gui/res/skins/metalcraft/pics* -forge-gui/res/skins/metalcraft/pics_product -forge-gui/res/skins/mythic_rare/*.log -forge-gui/res/skins/mythic_rare/PerSetTrackingResults -forge-gui/res/skins/mythic_rare/Thumbs.db -forge-gui/res/skins/mythic_rare/decks -forge-gui/res/skins/mythic_rare/layouts -forge-gui/res/skins/mythic_rare/pics* -forge-gui/res/skins/mythic_rare/pics_product -forge-gui/res/skins/phyrexia/*.log -forge-gui/res/skins/phyrexia/PerSetTrackingResults -forge-gui/res/skins/phyrexia/Thumbs.db -forge-gui/res/skins/phyrexia/decks -forge-gui/res/skins/phyrexia/layouts -forge-gui/res/skins/phyrexia/pics* -forge-gui/res/skins/phyrexia/pics_product -forge-gui/res/skins/pics* -forge-gui/res/skins/pics_product -forge-gui/res/skins/ravnica/*.log -forge-gui/res/skins/ravnica/PerSetTrackingResults -forge-gui/res/skins/ravnica/Thumbs.db -forge-gui/res/skins/ravnica/decks -forge-gui/res/skins/ravnica/layouts -forge-gui/res/skins/ravnica/pics* -forge-gui/res/skins/ravnica/pics_product -forge-gui/res/skins/rebel/*.log -forge-gui/res/skins/rebel/PerSetTrackingResults -forge-gui/res/skins/rebel/Thumbs.db -forge-gui/res/skins/rebel/decks -forge-gui/res/skins/rebel/layouts -forge-gui/res/skins/rebel/pics* -forge-gui/res/skins/rebel/pics_product -forge-gui/res/skins/sleeping_forest/*.log -forge-gui/res/skins/sleeping_forest/PerSetTrackingResults -forge-gui/res/skins/sleeping_forest/Thumbs.db -forge-gui/res/skins/sleeping_forest/decks -forge-gui/res/skins/sleeping_forest/layouts -forge-gui/res/skins/sleeping_forest/pics* -forge-gui/res/skins/sleeping_forest/pics_product -forge-gui/res/skins/smith/*.log -forge-gui/res/skins/smith/PerSetTrackingResults -forge-gui/res/skins/smith/Thumbs.db -forge-gui/res/skins/smith/decks -forge-gui/res/skins/smith/layouts -forge-gui/res/skins/smith/pics* -forge-gui/res/skins/smith/pics_product -forge-gui/res/skins/the_dale/*.log -forge-gui/res/skins/the_dale/PerSetTrackingResults -forge-gui/res/skins/the_dale/Thumbs.db -forge-gui/res/skins/the_dale/decks -forge-gui/res/skins/the_dale/layouts -forge-gui/res/skins/the_dale/pics* -forge-gui/res/skins/the_dale/pics_product -forge-gui/res/skins/the_simpsons/*.log -forge-gui/res/skins/the_simpsons/PerSetTrackingResults -forge-gui/res/skins/the_simpsons/Thumbs.db -forge-gui/res/skins/the_simpsons/decks -forge-gui/res/skins/the_simpsons/layouts -forge-gui/res/skins/the_simpsons/pics* -forge-gui/res/skins/the_simpsons/pics_product -forge-gui/res/skins/zendikar/*.log -forge-gui/res/skins/zendikar/PerSetTrackingResults -forge-gui/res/skins/zendikar/Thumbs.db -forge-gui/res/skins/zendikar/decks -forge-gui/res/skins/zendikar/layouts -forge-gui/res/skins/zendikar/pics* -forge-gui/res/skins/zendikar/pics_product -forge-gui/target + forge-gui/tools/AllCards.json forge-gui/tools/EditionTrackingResults forge-gui/tools/PerSetTrackingResults -forge-gui/tools/oracleScript.log -/forge.profile.properties -/jgv.txt -/nbactions.xml -/pom.xml.next -/pom.xml.releaseBackup -/pom.xml.tag -/release.properties -/target -/test-output -.settings -.classpath -.project -.vscode/settings.json -.vscode/launch.json + diff --git a/forge-ai/pom.xml b/forge-ai/pom.xml index 3f176653a65..5c5fa9ebce3 100644 --- a/forge-ai/pom.xml +++ b/forge-ai/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.39-SNAPSHOT forge-ai diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 51798704399..0920da8b4d8 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -23,7 +23,7 @@ import com.google.common.collect.Lists; import forge.ai.ability.AnimateAi; import forge.card.CardTypeView; import forge.game.GameEntity; -import forge.game.ability.AbilityFactory; +import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.ability.effects.ProtectEffect; import forge.game.card.*; @@ -37,6 +37,7 @@ import forge.game.spellability.SpellAbility; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; +import forge.util.Aggregates; import forge.util.Expressions; import forge.util.MyRandom; import forge.util.TextUtil; @@ -194,12 +195,12 @@ public class AiAttackController { for (Card c : ai.getOpponents().getCardsIn(ZoneType.Battlefield)) { for (Trigger t : c.getTriggers()) { if (t.getMode() == TriggerType.Attacks) { - SpellAbility sa = t.getOverridingAbility(); - if (sa == null && t.hasParam("Execute")) { - sa = AbilityFactory.getAbility(c, t.getParam("Execute")); + SpellAbility sa = t.ensureAbility(); + if (sa == null) { + continue; } - if (sa != null && sa.getApi() == ApiType.EachDamage && "TriggeredAttacker".equals(sa.getParam("DefinedPlayers"))) { + if (sa.getApi() == ApiType.EachDamage && "TriggeredAttacker".equals(sa.getParam("DefinedPlayers"))) { List valid = CardLists.getValidCards(c.getController().getCreaturesInPlay(), sa.getParam("ValidCards"), c.getController(), c, sa); // TODO: this assumes that 1 damage is dealt per creature. Improve this to check the parameter/X to determine // how much damage is dealt by each of the creatures in the valid list. @@ -464,7 +465,7 @@ public class AiAttackController { final CardCollectionView beastions = ai.getCardsIn(ZoneType.Battlefield, "Beastmaster Ascension"); int minCreatures = 7; for (final Card beastion : beastions) { - final int counters = beastion.getCounters(CounterType.QUEST); + final int counters = beastion.getCounters(CounterEnumType.QUEST); minCreatures = Math.min(minCreatures, 7 - counters); } if (this.attackers.size() >= minCreatures) { @@ -739,6 +740,11 @@ public class AiAttackController { if (lightmineField) { doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro); } + // Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily + if (!doRevengeOfRavensAttackLogic(ai, defender, attackersLeft, numForcedAttackers, attackMax)) { + return; + } + if (bAssault) { if (LOG_AI_ATTACKS) @@ -1065,7 +1071,7 @@ public class AiAttackController { } } // if enough damage: switch to next planeswalker or player - if (damage >= pw.getCounters(CounterType.LOYALTY)) { + if (damage >= pw.getCounters(CounterEnumType.LOYALTY)) { List pwDefending = combat.getDefendingPlaneswalkers(); boolean found = false; // look for next planeswalker @@ -1135,7 +1141,6 @@ public class AiAttackController { // TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?) if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) { // Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine! - boolean wantability = false; for (SpellAbility sa : attacker.getSpellAbilities()) { // Do not attack if we can afford using the ability. if (sa.isAbility()) { @@ -1164,13 +1169,16 @@ public class AiAttackController { return CombatUtil.canBlock(attacker, defender); } }); + + boolean canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, CardPredicates.Accessors.fnGetNetToughness); + // used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present boolean dangerousBlockersPresent = !CardLists.filter(validBlockers, Predicates.or( CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT), CardPredicates.hasKeyword(Keyword.LIFELINK))).isEmpty(); // total power of the defending creatures, used in predicting whether a gang block can kill the attacker - int defPower = CardLists.getTotalPower(validBlockers, true); + int defPower = CardLists.getTotalPower(validBlockers, true, false); if (!hasCombatEffect) { for (KeywordInterface inst : attacker.getKeywords()) { @@ -1192,7 +1200,7 @@ public class AiAttackController { if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) { numberOfPossibleBlockers += 1; if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false) - && !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) { + && !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) { canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature // see if the defending creature is of higher or lower // value. We don't want to attack only to lose value @@ -1253,6 +1261,10 @@ public class AiAttackController { if (LOG_AI_ATTACKS) System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player"); return true; + } else if (!canBeKilled && !dangerousBlockersPresent && canTrampleOverDefenders) { + if (LOG_AI_ATTACKS) + System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through"); + return true; } if (numberOfPossibleBlockers > 2 @@ -1336,9 +1348,9 @@ public class AiAttackController { if (!TriggerType.Exerted.equals(t.getMode())) { continue; } - SpellAbility sa = t.getOverridingAbility(); + SpellAbility sa = t.ensureAbility(); if (sa == null) { - sa = AbilityFactory.getAbility(c, t.getParam("Execute")); + continue; } if (sa.usesTargeting()) { sa.setActivatingPlayer(c.getController()); @@ -1365,21 +1377,12 @@ public class AiAttackController { if (c.hasSVar("AIExertCondition")) { if (!c.getSVar("AIExertCondition").isEmpty()) { final String needsToExert = c.getSVar("AIExertCondition"); - int x = 0; - int y = 0; String sVar = needsToExert.split(" ")[0]; String comparator = needsToExert.split(" ")[1]; String compareTo = comparator.substring(2); - try { - x = Integer.parseInt(sVar); - } catch (final NumberFormatException e) { - x = CardFactoryUtil.xCount(c, c.getSVar(sVar)); - } - try { - y = Integer.parseInt(compareTo); - } catch (final NumberFormatException e) { - y = CardFactoryUtil.xCount(c, c.getSVar(compareTo)); - } + + int x = AbilityUtils.calculateAmount(c, sVar, null); + int y = AbilityUtils.calculateAmount(c, compareTo, null); if (Expressions.compare(x, comparator, y)) { shouldExert = true; } @@ -1493,4 +1496,39 @@ public class AiAttackController { attackersLeft.removeAll(attUnsafe); } + private boolean doRevengeOfRavensAttackLogic(Player ai, GameEntity defender, List attackersLeft, int numForcedAttackers, int maxAttack) { + // TODO: detect Revenge of Ravens by the trigger instead of by name + boolean revengeOfRavens = false; + if (defender instanceof Player) { + revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty(); + } else if (defender instanceof Card) { + revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty(); + } + + if (!revengeOfRavens) { + return true; + } + + int life = ai.canLoseLife() && !ai.cantLoseForZeroOrLessLife() ? ai.getLife() : Integer.MAX_VALUE; + maxAttack = maxAttack < 0 ? Integer.MAX_VALUE - 1 : maxAttack; + if (Math.min(maxAttack, numForcedAttackers) >= life) { + return false; + } + + // Remove all 1-power attackers since they usually only hurt the attacker + // TODO: improve to account for possible combat effects coming from attackers like that + CardCollection attUnsafe = new CardCollection(); + for (Card attacker : attackersLeft) { + if (attacker.getNetCombatDamage() <= 1) { + attUnsafe.add(attacker); + } + } + attackersLeft.removeAll(attUnsafe); + if (Math.min(maxAttack, attackersLeft.size()) >= life) { + return false; + } + + return true; + } + } // end class ComputerUtil_Attack2 diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index bef6eaa8961..6a238e13baf 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -70,7 +70,8 @@ public class AiBlockController { for (final Card blocker : blockersLeft) { // if the blocker can block a creature with lure it can't block a creature without if (CombatUtil.canBlock(attacker, blocker, combat)) { - if (solo && blocker.hasKeyword("CARDNAME can't attack or block alone.")) { + boolean cantBlockAlone = blocker.hasKeyword("CARDNAME can't attack or block alone.") || blocker.hasKeyword("CARDNAME can't block alone."); + if (solo && cantBlockAlone) { continue; } blockers.add(blocker); @@ -228,9 +229,9 @@ public class AiBlockController { // 3.Blockers that can destroy the attacker and have an upside when dying killingBlockers = getKillingBlockers(combat, attacker, blockers); for (Card b : killingBlockers) { - if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe") - || (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1) - || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0) + if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterEnumType.P1P1) == 0) || b.hasSVar("SacMe") + || (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1) + || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0) || b.hasSVar("EndOfTurnLeavePlay")) { blocker = b; break; @@ -299,8 +300,8 @@ public class AiBlockController { final List blockers = getPossibleBlockers(combat, attacker, blockersLeft, true); for (Card b : blockers) { - if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1) - || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0) + if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1) + || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0) || b.hasSVar("EndOfTurnLeavePlay")) { blocker = b; if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) { @@ -851,7 +852,7 @@ public class AiBlockController { damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true); } } - if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterType.LOYALTY)) { + if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterEnumType.LOYALTY)) { threatenedPWs.add((Card) def); } } @@ -909,7 +910,7 @@ public class AiBlockController { damageToPW += ComputerUtilCombat.predictDamageTo(pw, pwAtk.getNetCombatDamage(), pwAtk, true); } } - if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterType.LOYALTY)) { + if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterEnumType.LOYALTY)) { for (Card chump : pwDefenders) { if (chosenChumpBlockers.contains(chump)) { combat.removeFromCombat(chump); diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index b4b3293d108..fce2160fc27 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -31,7 +31,6 @@ import forge.deck.CardPool; import forge.deck.Deck; import forge.deck.DeckSection; import forge.game.*; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.ability.SpellApiBased; @@ -64,7 +63,6 @@ import forge.util.collect.FCollectionView; import io.sentry.Sentry; import io.sentry.event.BreadcrumbBuilder; -import java.security.InvalidParameterException; import java.util.*; import java.util.Map.Entry; @@ -177,7 +175,7 @@ public class AiController { && CardFactoryUtil.isCounterable(host)) { return true; } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host) - && host.getCMC() == c.getCounters(CounterType.CHARGE)) { + && host.getCMC() == c.getCounters(CounterEnumType.CHARGE)) { return true; } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) { String hostName = host.getName(); @@ -204,24 +202,22 @@ public class AiController { return api == null; } boolean rightapi = false; - String battlefield = ZoneType.Battlefield.toString(); Player activatingPlayer = sa.getActivatingPlayer(); // Trigger play improvements for (final Trigger tr : card.getTriggers()) { // These triggers all care for ETB effects - final Map params = tr.getMapParams(); if (tr.getMode() != TriggerType.ChangesZone) { continue; } - if (!params.get("Destination").equals(battlefield)) { + if (!ZoneType.Battlefield.toString().equals(tr.getParam("Destination"))) { continue; } - if (params.containsKey("ValidCard")) { - String validCard = params.get("ValidCard"); + if (tr.hasParam("ValidCard")) { + String validCard = tr.getParam("ValidCard"); if (!validCard.contains("Self")) { continue; } @@ -245,21 +241,11 @@ public class AiController { } // if trigger is not mandatory - no problem - if (params.get("OptionalDecider") != null && api == null) { + if (tr.hasParam("OptionalDecider") && api == null) { continue; } - SpellAbility exSA = tr.getOverridingAbility(); - - if (exSA == null) { - // Maybe better considerations - if (!params.containsKey("Execute")) { - continue; - } - exSA = AbilityFactory.getAbility(card, params.get("Execute")); - } else { - exSA = exSA.copy(); - } + SpellAbility exSA = tr.ensureAbility().copy(activatingPlayer); if (api != null) { if (exSA.getApi() != api) { @@ -273,13 +259,7 @@ public class AiController { } } - if (sa != null) { - exSA.setActivatingPlayer(activatingPlayer); - } - else { - exSA.setActivatingPlayer(player); - } - exSA.setTrigger(true); + exSA.setTrigger(tr); // for trigger test, need to ignore the conditions SpellAbilityCondition cons = exSA.getConditions(); @@ -304,18 +284,16 @@ public class AiController { // Replacement effects for (final ReplacementEffect re : card.getReplacementEffects()) { // These Replacements all care for ETB effects - - final Map params = re.getMapParams(); if (!(re instanceof ReplaceMoved)) { continue; } - if (!params.get("Destination").equals(battlefield)) { + if (!ZoneType.Battlefield.toString().equals(re.getParam("Destination"))) { continue; } - if (params.containsKey("ValidCard")) { - String validCard = params.get("ValidCard"); + if (re.hasParam("ValidCard")) { + String validCard = re.getParam("ValidCard"); if (!validCard.contains("Self")) { continue; } @@ -337,26 +315,17 @@ public class AiController { if (!re.requirementsCheck(game)) { continue; } - final SpellAbility exSA = re.getOverridingAbility(); + SpellAbility exSA = re.getOverridingAbility(); if (exSA != null) { - if (sa != null) { - exSA.setActivatingPlayer(activatingPlayer); - } - else { - exSA.setActivatingPlayer(player); - } + exSA = exSA.copy(activatingPlayer); - if (exSA.getActivatingPlayer() == null) { - throw new InvalidParameterException("Executing SpellAbility for Replacement Effect has no activating player"); + // ETBReplacement uses overriding abilities. + // These checks only work if the Executing SpellAbility is an Ability_Sub. + if ((exSA instanceof AbilitySub) && !doTrigger(exSA, false)) { + return false; } } - - // ETBReplacement uses overriding abilities. - // These checks only work if the Executing SpellAbility is an Ability_Sub. - if (exSA != null && (exSA instanceof AbilitySub) && !doTrigger(exSA, false)) { - return false; - } } return true; } @@ -460,8 +429,7 @@ public class AiController { byte color = MagicColor.fromName(c); for (Card land : landList) { for (final SpellAbility m : ComputerUtilMana.getAIPlayableMana(land)) { - AbilityManaPart mp = m.getManaPart(); - if (mp.canProduce(MagicColor.toShortString(color), m)) { + if (m.canProduce(MagicColor.toShortString(color))) { return land; } } @@ -514,8 +482,7 @@ public class AiController { return land; } for (final SpellAbility m : ComputerUtilMana.getAIPlayableMana(land)) { - AbilityManaPart mp = m.getManaPart(); - if (mp.canProduce(MagicColor.toShortString(color), m)) { + if (m.canProduce(MagicColor.toShortString(color))) { return land; } } @@ -728,6 +695,8 @@ public class AiController { public AiPlayDecision canPlaySa(SpellAbility sa) { final Card card = sa.getHostCard(); + final boolean isRightTiming = sa.canCastTiming(player); + if (!checkAiSpecificRestrictions(sa)) { return AiPlayDecision.CantPlayAi; } @@ -769,22 +738,24 @@ public class AiController { return AiPlayDecision.CantPlayAi; } } - else if (sa.getPayCosts() != null){ + else { Cost payCosts = sa.getPayCosts(); - ManaCost mana = payCosts.getTotalMana(); - if (mana != null) { - if(mana.countX() > 0) { - // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, player); - if (xPay <= 0) { - return AiPlayDecision.CantAffordX; - } - card.setSVar("PayX", Integer.toString(xPay)); - } else if (mana.isZero()) { - // if mana is zero, but card mana cost does have X, then something is wrong - ManaCost cardCost = card.getManaCost(); - if (cardCost != null && cardCost.countX() > 0) { - return AiPlayDecision.CantPlayAi; + if(payCosts != null) { + ManaCost mana = payCosts.getTotalMana(); + if (mana != null) { + if(mana.countX() > 0) { + // Set PayX here to maximum value. + final int xPay = ComputerUtilCost.getMaxXValue(sa, player); + if (xPay <= 0) { + return AiPlayDecision.CantAffordX; + } + sa.setXManaCostPaid(xPay); + } else if (mana.isZero()) { + // if mana is zero, but card mana cost does have X, then something is wrong + ManaCost cardCost = card.getManaCost(); + if (cardCost != null && cardCost.countX() > 0) { + return AiPlayDecision.CantPlayAi; + } } } } @@ -792,7 +763,20 @@ public class AiController { if (checkCurseEffects(sa)) { return AiPlayDecision.CurseEffects; } + Card spellHost = card; + if (sa.isSpell()) { + spellHost = CardUtil.getLKICopy(spellHost); + spellHost.setLKICMC(-1); // to reset the cmc + spellHost.setLastKnownZone(game.getStackZone()); // need to add to stack to make check Restrictions respect stack cmc + spellHost.setCastFrom(card.getZone().getZoneType()); + } + if (!sa.checkRestrictions(spellHost, player)) { + return AiPlayDecision.AnotherTime; + } if (sa instanceof SpellPermanent) { + if (!isRightTiming) { + return AiPlayDecision.AnotherTime; + } return canPlayFromEffectAI((SpellPermanent)sa, false, true); } if (sa.usesTargeting() && !sa.isTargetNumberValid()) { @@ -805,8 +789,14 @@ public class AiController { && !player.cantLoseForZeroOrLessLife() && player.canLoseLife()) { return AiPlayDecision.CurseEffects; } + if (!isRightTiming) { + return AiPlayDecision.AnotherTime; + } return canPlaySpellBasic(card, sa); } + if (!isRightTiming) { + return AiPlayDecision.AnotherTime; + } return AiPlayDecision.WillPlay; } @@ -858,7 +848,7 @@ public class AiController { int neededMana = 0; boolean dangerousRecurringCost = false; - Cost costWithBuyback = sa.getPayCosts() != null ? sa.getPayCosts().copy() : Cost.Zero; + Cost costWithBuyback = sa.getPayCosts().copy(); for (OptionalCostValue opt : GameActionUtil.getOptionalCostValues(sa)) { if (opt.getType() == OptionalCost.Buyback) { costWithBuyback.add(opt.getCost()); @@ -907,8 +897,8 @@ public class AiController { public int compare(final SpellAbility a, final SpellAbility b) { // sort from highest cost to lowest // we want the highest costs first - int a1 = a.getPayCosts() == null ? 0 : a.getPayCosts().getTotalMana().getCMC(); - int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC(); + int a1 = a.getPayCosts().getTotalMana().getCMC(); + int b1 = b.getPayCosts().getTotalMana().getCMC(); // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { @@ -927,12 +917,12 @@ public class AiController { // deprioritize pump spells with pure energy cost (can be activated last, // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) int a2 = 0, b2 = 0; - if (a.getApi() == ApiType.Pump && a.getPayCosts() != null && a.getPayCosts().getCostEnergy() != null) { + if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { a2 = a.getPayCosts().getCostEnergy().convertAmount(); } } - if (b.getApi() == ApiType.Pump && b.getPayCosts() != null && b.getPayCosts().getCostEnergy() != null) { + if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { b2 = b.getPayCosts().getCostEnergy().convertAmount(); } @@ -956,8 +946,7 @@ public class AiController { return 1; } - if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi() - && a.getPayCosts() != null && b.getPayCosts() != null) { + if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { // Cheaper Spectacle costs should be preferred // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) @@ -998,6 +987,10 @@ public class AiController { if (source.isEquipment() && noCreatures) { p -= 9; } + // don't equip stuff in main 2 if there's more stuff to cast at the moment + if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + p -= 1; + } // 1. increase chance of using Surge effects // 2. non-surged versions are usually inefficient if (source.getOracleText().contains("surge cost") && !sa.isSurged()) { @@ -1076,8 +1069,8 @@ public class AiController { } else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) { return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa); } else if ("DiscardCMCX".equals(sa.getParam("AILogic"))) { - final int CMC = Integer.parseInt(sourceCard.getSVar("PayX")); - CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(CMC)); + final int cmc = sa.getXManaCostPaid(); + CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc)); if (discards.isEmpty()) { return null; } else { @@ -1403,6 +1396,7 @@ public class AiController { final List abilities = Lists.newArrayList(); LandAbility la = new LandAbility(land, player, null); + la.setCardState(land.getCurrentState()); if (la.canPlay()) { abilities.add(la); } @@ -1410,6 +1404,7 @@ public class AiController { // add mayPlay option for (CardPlayOption o : land.mayPlay(player)) { la = new LandAbility(land, player, o.getAbility()); + la.setCardState(land.getCurrentState()); if (la.canPlay()) { abilities.add(la); } @@ -1462,9 +1457,11 @@ public class AiController { int totalCMCInHand = Aggregates.sum(inHand, CardPredicates.Accessors.fnGetCmc); int minCMCInHand = Aggregates.min(inHand, CardPredicates.Accessors.fnGetCmc); + if (minCMCInHand == Integer.MAX_VALUE) + minCMCInHand = 0; int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true); - boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && !isTapLand; + boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && minCMCInHand > 0 && !isTapLand; boolean cantCastAnythingNow = predictedMana < minCMCInHand; boolean hasRelevantAbsOTB = !CardLists.filter(otb, new Predicate() { @@ -1479,7 +1476,7 @@ public class AiController { } for (SpellAbility sa : card.getSpellAbilities()) { - if (sa.getPayCosts() != null && sa.isAbility() + if (sa.isAbility() && sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().getCMC() > 0 && (!sa.getPayCosts().hasTapCost() || !isTapLand) @@ -1544,11 +1541,11 @@ public class AiController { top = game.getStack().peekAbility(); } final boolean topOwnedByAI = top != null && top.getActivatingPlayer().equals(player); + final boolean mustRespond = top != null && top.hasParam("AIRespondsToOwnAbility"); if (topOwnedByAI) { // AI's own spell: should probably let my stuff resolve first, but may want to copy the SA or respond to it // in a scripted timed fashion. - final boolean mustRespond = top.hasParam("AIRespondsToOwnAbility"); if (!mustRespond) { saList = ComputerUtilAbility.getSpellAbilities(cards, player); // get the SA list early to check for copy SAs @@ -1572,8 +1569,19 @@ public class AiController { saList = ComputerUtilAbility.getSpellAbilities(cards, player); } + Iterables.removeIf(saList, new Predicate() { + @Override + public boolean apply(final SpellAbility spellAbility) { + return spellAbility instanceof LandAbility; + } + }); + SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true); + if (topOwnedByAI && !mustRespond && chosenSa != ComputerUtilAbility.getFirstCopySASpell(saList)) { + return null; // not planning to copy the spell and not marked as something the AI would respond to + } + return chosenSa; } @@ -1606,10 +1614,18 @@ public class AiController { } sa.setActivatingPlayer(player); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); + SpellAbility root = sa.getRootAbility(); + + if (root.isSpell() || root.isTrigger() || root.isReplacementAbility()) { + sa.setLastStateBattlefield(game.getLastStateBattlefield()); + sa.setLastStateGraveyard(game.getLastStateGraveyard()); + } AiPlayDecision opinion = canPlayAndPayFor(sa); + + // reset LastStateBattlefield + sa.setLastStateBattlefield(CardCollection.EMPTY); + sa.setLastStateGraveyard(CardCollection.EMPTY); // PhaseHandler ph = game.getPhaseHandler(); // System.out.printf("Ai thinks '%s' of %s -> %s @ %s %s >>> \n", opinion, sa.getHostCard(), sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase()); @@ -1790,6 +1806,8 @@ public class AiController { return Math.max(remaining, min) / 2; } else if ("LowestLoseLife".equals(logic)) { return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1; + } else if ("HighestLoseLife".equals(logic)) { + return MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1; } else if ("HighestGetCounter".equals(logic)) { return MyRandom.getRandom().nextInt(3); } else if (source.hasSVar("EnergyToPay")) { @@ -1802,11 +1820,14 @@ public class AiController { throw new UnsupportedOperationException("AI is not supposed to reach this code at the moment"); } - public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional) { + public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional, Map params) { if (sa == null || sa.getApi() == null) { throw new UnsupportedOperationException(); } CardCollection result = new CardCollection(); + if (sa.hasParam("AIMaxAmount")) { + max = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("AIMaxAmount"), sa); + } switch(sa.getApi()) { case TwoPiles: // TODO: improve AI @@ -1835,7 +1856,7 @@ public class AiController { default: CardCollection editablePool = new CardCollection(pool); for (int i = 0; i < max; i++) { - Card c = player.getController().chooseSingleEntityForEffect(editablePool, sa, null, isOptional); + Card c = player.getController().chooseSingleEntityForEffect(editablePool, sa, null, isOptional, params); if (c != null) { result.add(c); editablePool.remove(c); @@ -1986,6 +2007,35 @@ public class AiController { return MyRandom.getRandom().nextBoolean(); } + public boolean chooseEvenOdd(SpellAbility sa) { + String aiLogic = sa.getParamOrDefault("AILogic", ""); + + if (aiLogic.equals("AlwaysEven")) { + return false; // false is Even + } else if (aiLogic.equals("AlwaysOdd")) { + return true; // true is Odd + } else if (aiLogic.equals("Random")) { + return MyRandom.getRandom().nextBoolean(); + } else if (aiLogic.equals("CMCInHand")) { + CardCollectionView hand = sa.getActivatingPlayer().getCardsIn(ZoneType.Hand); + int numEven = CardLists.filter(hand, CardPredicates.evenCMC()).size(); + int numOdd = CardLists.filter(hand, CardPredicates.oddCMC()).size(); + return numOdd > numEven; + } else if (aiLogic.equals("CMCOppControls")) { + CardCollectionView hand = sa.getActivatingPlayer().getOpponents().getCardsIn(ZoneType.Battlefield); + int numEven = CardLists.filter(hand, CardPredicates.evenCMC()).size(); + int numOdd = CardLists.filter(hand, CardPredicates.oddCMC()).size(); + return numOdd > numEven; + } else if (aiLogic.equals("CMCOppControlsByPower")) { + // TODO: improve this to check for how dangerous those creatures actually are relative to host card + CardCollectionView hand = sa.getActivatingPlayer().getOpponents().getCardsIn(ZoneType.Battlefield); + int powerEven = Aggregates.sum(CardLists.filter(hand, CardPredicates.evenCMC()), Accessors.fnGetNetPower); + int powerOdd = Aggregates.sum(CardLists.filter(hand, CardPredicates.oddCMC()), Accessors.fnGetNetPower); + return powerOdd > powerEven; + } + return MyRandom.getRandom().nextBoolean(); // outside of any specific logic, choose randomly + } + public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List origin, SpellAbility sa, CardCollection fetchList, Player player2, Player decider) { if (useSimulation) { @@ -2071,9 +2121,9 @@ public class AiController { return filterList(list, CardTraitPredicates.hasParam("AiLogic", logic)); } - public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { + public List chooseModeForAbility(SpellAbility sa, List possible, int min, int num, boolean allowRepeat) { if (simPicker != null) { - return simPicker.chooseModeForAbility(sa, min, num, allowRepeat); + return simPicker.chooseModeForAbility(sa, possible, min, num, allowRepeat); } return null; } diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index aa6f83699ca..646f53fe467 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -4,31 +4,39 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.ObjectUtils; + import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import com.google.common.collect.Lists; import forge.card.CardType; import forge.game.Game; +import forge.game.GameEntityCounterTable; import forge.game.ability.AbilityUtils; 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.CounterEnumType; import forge.game.card.CounterType; import forge.game.cost.*; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; import forge.game.zone.ZoneType; +import forge.util.Aggregates; import forge.util.TextUtil; import forge.util.collect.FCollectionView; +import static forge.ai.ComputerUtilCard.getBestCreatureAI; + public class AiCostDecision extends CostDecisionMakerBase { private final SpellAbility ability; private final Card source; - + private final CardCollection discarded; private final CardCollection tapped; @@ -44,11 +52,11 @@ public class AiCostDecision extends CostDecisionMakerBase { @Override public PaymentDecision visit(CostAddMana cost) { Integer c = cost.convertAmount(); - + if (c == null) { c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } - + return PaymentDecision.number(c); } @@ -90,10 +98,6 @@ public class AiCostDecision extends CostDecisionMakerBase { } Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - if (sVar.equals("XChoice")) { - return null; - } c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } @@ -104,6 +108,24 @@ public class AiCostDecision extends CostDecisionMakerBase { } return PaymentDecision.card(randomSubset); } + else if (type.equals("DifferentNames")) { + CardCollection differentNames = new CardCollection(); + CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe")); + while (c > 0) { + Card chosen; + if (!discardMe.isEmpty()) { + chosen = Aggregates.random(discardMe); + discardMe = CardLists.filter(discardMe, Predicates.not(CardPredicates.sharesNameWith(chosen))); + } else { + final Card worst = ComputerUtilCard.getWorstAI(hand); + chosen = worst != null ? worst : Aggregates.random(hand); + } + differentNames.add(chosen); + hand = CardLists.filter(hand, Predicates.not(CardPredicates.sharesNameWith(chosen))); + c--; + } + return PaymentDecision.card(differentNames); + } else { final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); @@ -120,13 +142,7 @@ public class AiCostDecision extends CostDecisionMakerBase { Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - // Generalize cost - if (sVar.equals("XChoice")) { - return null; // cannot pay - } else { - c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); - } + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } return PaymentDecision.number(c); @@ -158,11 +174,6 @@ public class AiCostDecision extends CostDecisionMakerBase { Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - // Generalize cost - if (sVar.equals("XChoice")) { - return null; - } c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } @@ -184,11 +195,6 @@ public class AiCostDecision extends CostDecisionMakerBase { Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - // Generalize cost - if (sVar.equals("XChoice")) { - return null; - } c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } List chosen = Lists.newArrayList(); @@ -234,10 +240,6 @@ public class AiCostDecision extends CostDecisionMakerBase { Integer c = cost.convertAmount(); if (c == null) { - if (ability.getSVar(cost.getAmount()).equals("XChoice")) { - return null; - } - c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } @@ -260,11 +262,6 @@ public class AiCostDecision extends CostDecisionMakerBase { public PaymentDecision visit(CostFlipCoin cost) { Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - // Generalize cost - if (sVar.equals("XChoice")) { - return null; - } c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } return PaymentDecision.number(c); @@ -300,17 +297,17 @@ public class AiCostDecision extends CostDecisionMakerBase { @Override public PaymentDecision visit(CostGainLife cost) { final List oppsThatCanGainLife = Lists.newArrayList(); - + for (final Player opp : cost.getPotentialTargets(player, source)) { if (opp.canGainLife()) { oppsThatCanGainLife.add(opp); } } - + if (oppsThatCanGainLife.size() == 0) { return null; } - + return PaymentDecision.players(oppsThatCanGainLife); } @@ -319,17 +316,11 @@ public class AiCostDecision extends CostDecisionMakerBase { public PaymentDecision visit(CostMill cost) { Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - // Generalize cost - if (sVar.equals("XChoice")) { - return null; - } - c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } CardCollectionView topLib = player.getCardsIn(ZoneType.Library, c); - return topLib.size() < c ? null : PaymentDecision.card(topLib); + return topLib.size() < c ? null : PaymentDecision.number(c); } @Override @@ -341,19 +332,8 @@ public class AiCostDecision extends CostDecisionMakerBase { public PaymentDecision visit(CostPayLife cost) { Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); // Generalize cost - if (sVar.equals("XChoice")) { - if (source.getName().equals("Maralen of the Mornsong Avatar")) { - return PaymentDecision.number(2); - } - if (source.getName().equals("Necrologia")) { - return PaymentDecision.number(Integer.parseInt(ability.getSVar("ChosenX"))); - } - return null; - } else { - c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); - } + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } if (!player.canPayLife(c)) { return null; @@ -366,13 +346,7 @@ public class AiCostDecision extends CostDecisionMakerBase { public PaymentDecision visit(CostPayEnergy cost) { Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - // Generalize cost - if (sVar.equals("XChoice")) { - return null; - } else { - c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); - } + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } if (!player.canPayEnergy(c)) { return null; @@ -399,11 +373,6 @@ public class AiCostDecision extends CostDecisionMakerBase { } if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - // Generalize cost - if (sVar.equals("XChoice")) { - return null; - } c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } @@ -463,24 +432,8 @@ public class AiCostDecision extends CostDecisionMakerBase { CardCollection exclude = new CardCollection(); exclude.addAll(tapped); - if (c == null) { - final String sVar = ability.getSVar(amount); - if (sVar.equals("XChoice")) { - CardCollectionView typeList = - CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"), - ability.getActivatingPlayer(), ability.getHostCard(), ability); - typeList = CardLists.filter(typeList, Presets.UNTAPPED); - c = typeList.size(); - // account for the fact that the activated card may be tapped in the process - if (ability.getPayCosts().hasTapCost() && typeList.contains(ability.getHostCard())) { - c--; - } - source.setSVar("ChosenX", "Number$" + c); - } else { - if (!isVehicle) { - c = AbilityUtils.calculateAmount(source, amount, ability); - } - } + if (c == null && !isVehicle) { + c = AbilityUtils.calculateAmount(source, amount, ability); } if (type.contains("sharesCreatureTypeWith")) { return null; @@ -494,7 +447,7 @@ public class AiCostDecision extends CostDecisionMakerBase { @Override public boolean apply(Card card) { for (final SpellAbility sa : card.getSpellAbilities()) { - if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) { return true; } } @@ -533,29 +486,11 @@ public class AiCostDecision extends CostDecisionMakerBase { return null; } + final String amount = cost.getAmount(); Integer c = cost.convertAmount(); + if (c == null) { - if (ability.getSVar(cost.getAmount()).equals("XChoice")) { - String logic = ability.getParamOrDefault("AILogic", ""); - if ("SacToReduceCost".equals(logic)) { - // e.g. Torgaar, Famine Incarnate - // TODO: currently returns an empty list, so the AI doesn't sacrifice anything. Trying to make - // the AI decide on creatures to sac makes the AI sacrifice them, but the cost is not reduced and the - // AI pays the full mana cost anyway (despite sacrificing creatures). - return PaymentDecision.card(new CardCollection()); - } else if (!logic.isEmpty() && !logic.equals("Never")) { - // If at least some other AI logic is specified, assume that the AI for that API knows how - // to define ChosenX and thus honor that value. - // Cards which have no special logic for this yet but which do work in a simple/suboptimal way - // are currently conventionally flagged with AILogic$ DoSacrifice. - c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null); - } else { - // Other cards are assumed to be flagged AI:RemoveDeck:All for now - return null; - } - } else { - c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); - } + c = AbilityUtils.calculateAmount(source, amount, ability); } final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c); @@ -566,7 +501,7 @@ public class AiCostDecision extends CostDecisionMakerBase { public PaymentDecision visit(CostReturn cost) { if (cost.payCostFromSource()) return PaymentDecision.card(source); - + Integer c = cost.convertAmount(); if (c == null) { c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); @@ -595,76 +530,177 @@ public class AiCostDecision extends CostDecisionMakerBase { if (cost.getType().equals("SameColor")) { return null; } - - hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability); + + if (cost.getRevealFrom().equals(ZoneType.Exile)) { + hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability); + return PaymentDecision.card(getBestCreatureAI(hand)); + } + Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(cost.getAmount()); - if (sVar.equals("XChoice")) { - c = hand.size(); - } else { - c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); - } + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); } final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); return PaymentDecision.card(aic.getCardsToDiscard(c, type.split(";"), ability)); } + protected int removeCounter(GameEntityCounterTable table, List prefs, CounterEnumType cType, int stillToRemove) { + int removed = 0; + if (!prefs.isEmpty() && stillToRemove > 0) { + Collections.sort(prefs, CardPredicates.compareByCounterType(cType)); + + for (Card prefCard : prefs) { + // already enough removed + if (stillToRemove <= removed) { + break; + } + int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove); + if (thisRemove > 0) { + removed += thisRemove; + table.put(prefCard, CounterType.get(cType), thisRemove); + } + } + } + return removed; + } + @Override public PaymentDecision visit(CostRemoveAnyCounter cost) { final String amount = cost.getAmount(); final int c = AbilityUtils.calculateAmount(source, amount, ability); - final String type = cost.getType(); + final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source); - CardCollectionView typeList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"), player, source, ability); + if (c <= 0) { + return null; + } + + CardCollectionView typeList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, source, ability); + // only cards with counters are of interest + typeList = CardLists.filter(typeList, CardPredicates.hasCounters()); // no target if (typeList.isEmpty()) { return null; } + // TODO fill up a GameEntityCounterTable + // cost now has counter type or null + // the amount might be different from 1, could be X + // currently if amount is bigger than one, + // it tries to remove all counters from one source and type at once + + + int toRemove = 0; + final GameEntityCounterTable table = new GameEntityCounterTable(); + + // currently the only one using remove any counter using a type uses p1p1 + // the first things are benefit from removing counters - // try to remove +1/+1 counter from undying creature - List prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.P1P1, c), - CardPredicates.hasKeyword("Undying")); - - if (!prefs.isEmpty()) { - Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.P1P1)); - PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.P1P1; - return result; - } - // try to remove -1/-1 counter from persist creature - prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.M1M1, c), - CardPredicates.hasKeyword("Persist")); + if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.M1M1))) { + List prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.M1M1), CardPredicates.hasKeyword(Keyword.PERSIST)); - if (!prefs.isEmpty()) { - Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.M1M1)); - PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.M1M1; - return result; + toRemove += removeCounter(table, prefs, CounterEnumType.M1M1, c - toRemove); } - // try to remove Time counter from Chronozoa, it will generate more - prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.TIME, c), - CardPredicates.nameEquals("Chronozoa")); + // try to remove +1/+1 counter from undying creature + if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.P1P1))) { + List prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.P1P1), CardPredicates.hasKeyword(Keyword.UNDYING)); - if (!prefs.isEmpty()) { - Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.TIME)); - PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.TIME; - return result; + toRemove += removeCounter(table, prefs, CounterEnumType.P1P1, c - toRemove); + } + + if (c > toRemove && cost.counter == null && originalHost.hasSVar("AIRemoveCounterCostPriority") && !"ANY".equalsIgnoreCase(originalHost.getSVar("AIRemoveCounterCostPriority"))) { + String[] counters = TextUtil.split(originalHost.getSVar("AIRemoveCounterCostPriority"), ','); + + for (final String ctr : counters) { + CounterType ctype = CounterType.getType(ctr); + // ctype == null means any type + // any type is just used to return null for this + + for (Card card : CardLists.filter(typeList, CardPredicates.hasCounter(ctype))) { + int thisRemove = Math.min(card.getCounters(ctype), c - toRemove); + if (thisRemove > 0) { + toRemove += thisRemove; + table.put(card, ctype, thisRemove); + } + } + } + } + + // filter for negative counters + if (c > toRemove && cost.counter == null) { + List negatives = CardLists.filter(typeList, new Predicate() { + @Override + public boolean apply(final Card crd) { + for (CounterType cType : table.filterToRemove(crd).keySet()) { + if (ComputerUtil.isNegativeCounter(cType, crd)) { + return true; + } + } + return false; + } + }); + + if (!negatives.isEmpty()) { + // TODO sort negatives to remove from best Cards first? + for (final Card crd : negatives) { + for (Map.Entry e : table.filterToRemove(crd).entrySet()) { + if (ComputerUtil.isNegativeCounter(e.getKey(), crd)) { + int over = Math.min(e.getValue(), c - toRemove); + if (over > 0) { + toRemove += over; + table.put(crd, e.getKey(), over); + } + } + } + } + } + } + + // filter for useless counters + // they have no effect on the card, if they are there or removed + if (c > toRemove && cost.counter == null) { + List useless = CardLists.filter(typeList, new Predicate() { + @Override + public boolean apply(final Card crd) { + for (CounterType ctype : table.filterToRemove(crd).keySet()) { + if (ComputerUtil.isUselessCounter(ctype, crd)) { + return true; + } + } + return false; + } + }); + + if (!useless.isEmpty()) { + for (final Card crd : useless) { + for (Map.Entry e : table.filterToRemove(crd).entrySet()) { + if (ComputerUtil.isUselessCounter(e.getKey(), crd)) { + int over = Math.min(e.getValue(), c - toRemove); + if (over > 0) { + toRemove += over; + table.put(crd, e.getKey(), over); + } + } + } + } + } + } + + // try to remove Time counter from Chronozoa, it will generate more token + if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.TIME))) { + List prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.TIME), CardPredicates.nameEquals("Chronozoa")); + + toRemove += removeCounter(table, prefs, CounterEnumType.TIME, c - toRemove); } // try to remove Quest counter on something with enough counters for the // effect to continue - prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.QUEST, c)); - - if (!prefs.isEmpty()) { - prefs = CardLists.filter(prefs, new Predicate() { + if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.QUEST))) { + List prefs = CardLists.filter(typeList, new Predicate() { @Override public boolean apply(final Card crd) { // a Card without MaxQuestEffect doesn't need any Quest @@ -673,130 +709,65 @@ public class AiCostDecision extends CostDecisionMakerBase { if (crd.hasSVar("MaxQuestEffect")) { e = Integer.parseInt(crd.getSVar("MaxQuestEffect")); } - return crd.getCounters(CounterType.QUEST) >= e + c; + return crd.getCounters(CounterEnumType.QUEST) > e; } }); - Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterType.QUEST))); - PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.QUEST; - return result; + Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST))); + + for (final Card crd : prefs) { + int e = 0; + if (crd.hasSVar("MaxQuestEffect")) { + e = Integer.parseInt(crd.getSVar("MaxQuestEffect")); + } + int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove); + if (over > 0) { + toRemove += over; + table.put(crd, CounterType.get(CounterEnumType.QUEST), over); + } + } } - // filter for only cards with enough counters - typeList = CardLists.filter(typeList, new Predicate() { - @Override - public boolean apply(final Card crd) { - for (Integer i : crd.getCounters().values()) { - if (i >= c) { - return true; + // remove Lore counters from Sagas to keep them longer + if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.LORE))) { + List prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.LORE), CardPredicates.isType("Saga")); + // TODO add Svars and other stuff to keep the Sagas on specific levels + // also add a way for the AI to respond to the last Chapter ability to keep the Saga on the field if wanted + toRemove += removeCounter(table, prefs, CounterEnumType.LORE, c - toRemove); + } + + + // TODO add logic to remove positive counters? + if (c > toRemove && cost.counter != null) { + // TODO add logic for Ooze Flux, should probably try to make a token as big as possible + // without killing own non undying creatures in the process + // the amount of X should probably be tweaked for this + List withCtr = CardLists.filter(typeList, CardPredicates.hasCounter(cost.counter)); + for (Card card : withCtr) { + int thisRemove = Math.min(card.getCounters(cost.counter), c - toRemove); + if (thisRemove > 0) { + toRemove += thisRemove; + table.put(card, cost.counter, thisRemove); + } + } + } + + // Used to not return null + // Special part for CostPriority Any + if (c > toRemove && cost.counter == null && originalHost.hasSVar("AIRemoveCounterCostPriority") && "ANY".equalsIgnoreCase(originalHost.getSVar("AIRemoveCounterCostPriority"))) { + for (Card card : typeList) { + // TODO try not to remove to much positive counters from the same card + for (Map.Entry e : table.filterToRemove(card).entrySet()) { + int thisRemove = Math.min(e.getValue(), c - toRemove); + if (thisRemove > 0) { + toRemove += thisRemove; + table.put(card, e.getKey(), thisRemove); } } - return false; - } - }); - - // nothing with enough counters of any type - if (typeList.isEmpty()) { - return null; - } - - // filter for negative counters - List negatives = CardLists.filter(typeList, new Predicate() { - @Override - public boolean apply(final Card crd) { - for (Map.Entry e : crd.getCounters().entrySet()) { - if (e.getValue() >= c && ComputerUtil.isNegativeCounter(e.getKey(), crd)) { - return true; - } - } - return false; - } - }); - - if (!negatives.isEmpty()) { - final Card card = ComputerUtilCard.getBestAI(negatives); - PaymentDecision result = PaymentDecision.card(card); - - for (Map.Entry e : card.getCounters().entrySet()) { - if (e.getValue() >= c && ComputerUtil.isNegativeCounter(e.getKey(), card)) { - result.ct = e.getKey(); - break; - } - } - return result; - } - - // filter for useless counters - // they have no effect on the card, if they are there or removed - List useless = CardLists.filter(typeList, new Predicate() { - @Override - public boolean apply(final Card crd) { - for (Map.Entry e : crd.getCounters().entrySet()) { - if (e.getValue() >= c && ComputerUtil.isUselessCounter(e.getKey())) { - return true; - } - } - return false; - } - }); - - if (!useless.isEmpty()) { - final Card card = useless.get(0); - PaymentDecision result = PaymentDecision.card(card); - - for (Map.Entry e : card.getCounters().entrySet()) { - if (e.getValue() >= c && ComputerUtil.isUselessCounter(e.getKey())) { - result.ct = e.getKey(); - break; - } - } - return result; - } - - // try a way to pay unless cost - if ("Chisei, Heart of Oceans".equals(ComputerUtilAbility.getAbilitySourceName(ability))) { - final Card card = ComputerUtilCard.getWorstAI(typeList); - PaymentDecision result = PaymentDecision.card(card); - for (Map.Entry e : card.getCounters().entrySet()) { - if (e.getValue() >= c) { - result.ct = e.getKey(); - break; - } - } - return result; - } - - // check if the card defines its own priorities for counter removal as cost - if (source.hasSVar("AIRemoveCounterCostPriority")) { - String[] counters = TextUtil.split(source.getSVar("AIRemoveCounterCostPriority"), ','); - - for (final String ctr : counters) { - List withCtr = CardLists.filter(typeList, new Predicate() { - @Override - public boolean apply(final Card crd) { - for (Map.Entry e : crd.getCounters().entrySet()) { - if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey() == CounterType.valueOf(ctr))) { - return true; - } - } - return false; - } - }); - if (!withCtr.isEmpty()) { - final Card card = withCtr.get(0); - PaymentDecision result = PaymentDecision.card(card); - - for (Map.Entry e : card.getCounters().entrySet()) { - if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey() == CounterType.valueOf(ctr))) { - result.ct = e.getKey(); - break; - } - } - return result; - } } } - return null; + + // if table is empty, than no counter was removed + return table.isEmpty() ? null : PaymentDecision.counters(table); } @Override @@ -807,22 +778,17 @@ public class AiCostDecision extends CostDecisionMakerBase { if (c == null) { final String sVar = ability.getSVar(amount); - if (sVar.equals("XChoice")) { - c = AbilityUtils.calculateAmount(source, "ChosenX", ability); - source.setSVar("ChosenX", "Number$" + c); - } else if (amount.equals("All")) { + if (amount.equals("All")) { c = source.getCounters(cost.counter); } else if (sVar.equals("Targeted$CardManaCost")) { c = 0; - if (ability.getTargets().getNumTargeted() > 0) { + if (ability.getTargets().size() > 0) { for (Card tgt : ability.getTargets().getTargetCards()) { if (tgt.getManaCost() != null) { c += tgt.getManaCost().getCMC(); } } } - } else if (sVar.equals("Count$xPaid")) { - c = AbilityUtils.calculateAmount(source, "PayX", null); } else { c = AbilityUtils.calculateAmount(source, amount, ability); } @@ -856,19 +822,7 @@ public class AiCostDecision extends CostDecisionMakerBase { final String amount = cost.getAmount(); Integer c = cost.convertAmount(); if (c == null) { - final String sVar = ability.getSVar(amount); - if (sVar.equals("XChoice")) { - CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Battlefield), - cost.getType().split(";"), player, ability.getHostCard(), ability); - if (!cost.canUntapSource) { - typeList.remove(source); - } - typeList = CardLists.filter(typeList, Presets.TAPPED); - c = typeList.size(); - source.setSVar("ChosenX", "Number$" + c); - } else { - c = AbilityUtils.calculateAmount(source, amount, ability); - } + c = AbilityUtils.calculateAmount(source, amount, ability); } CardCollectionView list = ComputerUtil.chooseUntapType(player, cost.getType(), source, cost.canUntapSource, c); @@ -877,7 +831,7 @@ public class AiCostDecision extends CostDecisionMakerBase { System.out.println("Couldn't find a valid card to untap for: " + source.getName()); return null; } - + return PaymentDecision.card(list); } diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index 413e1c45d1f..ee162987ba9 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -35,6 +35,7 @@ public enum AiProps { /** */ MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"), MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"), PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"), + SAC_TO_REATTACH_TARGET_EVAL_THRESHOLD ("400"), PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */ RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */ PLAY_AGGRO ("false"), @@ -74,6 +75,7 @@ public enum AiProps { /** */ ALWAYS_COPY_SPELL_IF_CMC_DIFF ("2"), /** */ ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */ ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */ + ACTIVELY_PROTECT_VS_CURSE_AURAS("false"), /** */ DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */ DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */ DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */ diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index ff73d875785..41b34481c6c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -6,12 +6,12 @@ * 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 . */ @@ -19,16 +19,18 @@ package forge.ai; import com.google.common.base.Predicate; import com.google.common.base.Predicates; -import com.google.common.collect.*; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import forge.ai.ability.ChooseGenericEffectAi; import forge.ai.ability.ProtectAi; import forge.ai.ability.TokenAi; +import forge.card.CardStateName; import forge.card.CardType; import forge.card.ColorSet; import forge.card.MagicColor; -import forge.card.mana.ManaCostShard; import forge.game.*; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; @@ -45,7 +47,10 @@ import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementType; -import forge.game.spellability.*; +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; @@ -65,7 +70,7 @@ import java.util.*; *

* ComputerUtil class. *

- * + * * @author Forge * @version $Id$ */ @@ -91,9 +96,6 @@ public class ComputerUtil { } } - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -104,16 +106,19 @@ public class ComputerUtil { sa = GameActionUtil.addExtraKeywordCost(sa); if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { - CharmEffect.makeChoices(sa); + if (!CharmEffect.makeChoices(sa)) { + return false; + } } if (chooseTargets != null) { chooseTargets.run(); } - if (sa.isBestow()) { - sa.getHostCard().animateBestow(); - } final Cost cost = sa.getPayCosts(); + + // Remember the now-forgotten kicker cost? Why is this needed? + sa.getHostCard().setKickerMagnitude(source.getKickerMagnitude()); + // TODO: update mana color conversion for Daxos of Meletis if (cost == null) { if (ComputerUtilMana.payManaCost(ai, sa)) { @@ -162,8 +167,6 @@ public class ComputerUtil { // 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)) { @@ -216,12 +219,9 @@ public class ComputerUtil { sa.setActivatingPlayer(ai); if (!ComputerUtilCost.canPayCost(sa, ai)) return false; - + final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -246,9 +246,6 @@ public class ComputerUtil { final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -267,13 +264,12 @@ public class ComputerUtil { final Card source = newSA.getHostCard(); if (newSA.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(newSA); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); newSA.setHostCard(game.getAction().moveToStack(source, sa)); if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) { - CharmEffect.makeChoices(newSA); + if (!CharmEffect.makeChoices(sa)) { + return false; + } } } @@ -290,9 +286,6 @@ public class ComputerUtil { if (ComputerUtilCost.canPayCost(sa, ai)) { final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -364,8 +357,8 @@ public class ComputerUtil { 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()) { + CardCollection landsInPlay = CardLists.getType(typeList, "Land"); + if (!landsInPlay.isEmpty()) { // Don't need more land. return ComputerUtilCard.getWorstLand(landsInPlay); } @@ -394,16 +387,16 @@ public class ComputerUtil { 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); - } + 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 @@ -465,14 +458,14 @@ public class ComputerUtil { return ComputerUtilCard.getWorstLand(landsInHand); } } - + // try everything when about to die if (activate != null && "Reality Smasher".equals(activate.getName()) || - game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS) - && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) { - if (!typeList.isEmpty()) { - return ComputerUtilCard.getWorstAI(typeList); - } + game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS) + && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) { + if (!typeList.isEmpty()) { + return ComputerUtilCard.getWorstAI(typeList); + } } } else if (pref.contains("DonateMe")) { // search for permanents with DonateMe. priority 1 is the lowest, priority 5 the highest @@ -555,7 +548,7 @@ public class ComputerUtil { 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, null); - + if ((target != null) && target.getController() == ai) { typeList.remove(target); // don't exile the card we're pumping } @@ -576,7 +569,7 @@ public class ComputerUtil { 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, null); - + if ((target != null) && target.getController() == ai) { typeList.remove(target); // don't move the card we're pumping } @@ -587,11 +580,11 @@ public class ComputerUtil { 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)); } @@ -651,7 +644,7 @@ public class ComputerUtil { } ComputerUtilCard.sortByEvaluateCreature(typeList); Collections.reverse(typeList); - + final CardCollection tapList = new CardCollection(); // Accumulate from "worst" creature @@ -724,18 +717,17 @@ public class ComputerUtil { return returnList; } - public static CardCollection choosePermanentsToSacrifice(final Player ai, final CardCollectionView cardlist, final int amount, final SpellAbility source, + public static CardCollection choosePermanentsToSacrifice(final Player ai, final CardCollectionView cardlist, final int amount, final SpellAbility source, final boolean destroy, final boolean isOptional) { CardCollection remaining = new CardCollection(cardlist); final CardCollection sacrificed = new CardCollection(); final Card host = source.getHostCard(); - final boolean considerSacLogic = "ConsiderSac".equals(source.getParam("AILogic")); final int considerSacThreshold = getAIPreferenceParameter(host, "CreatureEvalThreshold"); if ("OpponentOnly".equals(source.getParam("AILogic"))) { - if(!source.getActivatingPlayer().isOpponentOf(ai)) { - return sacrificed; // sacrifice none - } + if(!source.getActivatingPlayer().isOpponentOf(ai)) { + return sacrificed; // sacrifice none + } } else if ("DesecrationDemon".equals(source.getParam("AILogic"))) { if (!SpecialCardAi.DesecrationDemon.considerSacrificingCreature(ai, source)) { return sacrificed; // don't sacrifice unless in special conditions specified by DesecrationDemon AI @@ -745,51 +737,50 @@ public class ComputerUtil { if (!ai.canLoseLife() || ai.cantLose()) { return sacrificed; // sacrifice none } - } else if (!considerSacLogic) { + } else { return sacrificed; // sacrifice none } } boolean exceptSelf = "ExceptSelf".equals(source.getParam("AILogic")); boolean removedSelf = false; - if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit") || considerSacLogic) { - if (source.hasParam("Exploit")) { - for (Trigger t : host.getTriggers()) { - if (t.getMode() == TriggerType.Exploited) { - final String execute = t.getParam("Execute"); - if (execute == null) { - continue; - } - final SpellAbility exSA = AbilityFactory.getAbility(host.getSVar(execute), host); + if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit")) { + if (source.hasParam("Exploit")) { + for (Trigger t : host.getTriggers()) { + if (t.getMode() == TriggerType.Exploited) { + final SpellAbility exSA = t.ensureAbility().copy(ai); - exSA.setActivatingPlayer(ai); - exSA.setTrigger(true); + exSA.setTrigger(t); - // 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; - } - } - } - } + // 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() { @Override public boolean apply(final Card c) { int sacThreshold = 190; - if ("HeartPiercer".equals(source.getParam("SacrificeParam"))) { - if (c.getNetPower() == 0) { + String logic = source.getParamOrDefault("AILogic", ""); + if (logic.startsWith("SacForDamage")) { + if (c.getNetPower() <= 0) { return false; } else if (c.getNetPower() >= ai.getOpponentsSmallestLifeTotal()) { return true; + } else if (logic.endsWith(".GiantX2") && c.getType().hasCreatureType("Giant") + && c.getNetPower() * 2 >= ai.getOpponentsSmallestLifeTotal()) { + return true; // TODO: generalize this for any type and actually make the AI prefer giants? } } if ("DesecrationDemon".equals(source.getParam("AILogic"))) { sacThreshold = SpecialCardAi.DesecrationDemon.getSacThreshold(); - } else if (considerSacLogic && considerSacThreshold != -1) { + } else if (considerSacThreshold != -1) { sacThreshold = considerSacThreshold; } @@ -834,7 +825,7 @@ public class ComputerUtil { if (ai.isOpponentOf(c.getController())) return c; } - + if (destroy) { final CardCollection indestructibles = CardLists.getKeyword(remaining, Keyword.INDESTRUCTIBLE); if (!indestructibles.isEmpty()) { @@ -924,7 +915,7 @@ public class ComputerUtil { } catch (final Exception ex) { throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex); - } + } } } @@ -971,22 +962,23 @@ public class ComputerUtil { public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) { final Card card = sa.getHostCard(); + final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState(); 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 (!ai.getOpponents().getCreaturesInPlay().isEmpty()) { - return true; - } - } else if (!card.getController().getCreaturesInPlay().isEmpty()) { - return true; - } + 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 (!ai.getOpponents().getCreaturesInPlay().isEmpty()) { + return true; + } + } else if (!card.getController().getCreaturesInPlay().isEmpty()) { + return true; + } } // try not to cast Raid creatures in main 1 if an attack is likely - if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword(Keyword.HASTE)) { + if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !cardState.hasKeyword(Keyword.HASTE)) { for (Card potentialAtkr: ai.getCreaturesInPlay()) { if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) { return false; @@ -995,10 +987,10 @@ public class ComputerUtil { } if (card.getManaCost().isZero()) { - return true; + return true; } - if (card.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) { + if (cardState.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) { // Planning to choose Haste for Riot, so do this in Main 1 return true; } @@ -1019,13 +1011,13 @@ public class ComputerUtil { } } - if (card.isCreature() && !card.hasKeyword(Keyword.DEFENDER) - && (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) { + if (card.isCreature() && !cardState.hasKeyword(Keyword.DEFENDER) + && (cardState.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) { return true; } - - if (card.hasKeyword(Keyword.EXALTED)) { - return true; + + if (cardState.hasKeyword(Keyword.EXALTED) || cardState.hasKeyword(Keyword.EXTORT)) { + return true; } //cast equipments in Main1 when there are creatures to equip and no other unequipped equipment @@ -1068,7 +1060,13 @@ public class ComputerUtil { } } } - if (card.hasKeyword(Keyword.SOULBOND) && buffedcard.isCreature() && !buffedcard.isPaired()) { + + if (ApiType.PermanentNoncreature.equals(sa.getApi()) && buffedcard.hasKeyword(Keyword.PROWESS)) { + // non creature Permanent spell + return true; + } + + if (cardState.hasKeyword(Keyword.SOULBOND) && buffedcard.isCreature() && !buffedcard.isPaired()) { return true; } @@ -1155,7 +1153,7 @@ public class ComputerUtil { 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); @@ -1234,6 +1232,9 @@ public class ComputerUtil { return true; } } + if (ApiType.PermanentNoncreature.equals(sa.getApi()) && buffedCard.hasKeyword(Keyword.PROWESS)) { + return true; + } //Fill the graveyard for Threshold if (checkThreshold) { for (StaticAbility stAb : buffedCard.getStaticAbilities()) { @@ -1255,11 +1256,11 @@ public class ComputerUtil { } } } // AntiBuffedBy - - if (sub != null) { + + if (sub != null) { return castSpellInMain1(ai, sub); } - + return false; } @@ -1268,7 +1269,7 @@ public class ComputerUtil { int activations = sa.getActivationsThisTurn(); if (!sa.isIntrinsic()) { - return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory + return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory } if (activations < 10) { //10 activations per turn should still be acceptable @@ -1285,27 +1286,27 @@ public class ComputerUtil { return false; } if (abCost.hasTapCost() && source.hasSVar("AITapDown")) { - return true; + 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 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, sa); for (Card c : typeList) { @@ -1340,14 +1341,14 @@ public class ComputerUtil { Map params = stAb.getMapParams(); if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword") && params.get("AddKeyword").contains("Haste")) { - + if (c.isEquipment() && c.getEquipping() == null) { return true; } final String affected = params.get("Affected"); if (affected.contains("Creature.YouCtrl") - || affected.contains("Other+YouCtrl")) { + || affected.contains("Other+YouCtrl")) { return true; } else if (affected.contains("Creature.PairedWith") && !c.isPaired()) { return true; @@ -1356,10 +1357,10 @@ public class ComputerUtil { } for (Trigger t : c.getTriggers()) { - Map params = t.getMapParams(); + Map params = t.getMapParams(); if (!"ChangesZone".equals(params.get("Mode")) - || !"Battlefield".equals(params.get("Destination")) - || !params.containsKey("ValidCard")) { + || !"Battlefield".equals(params.get("Destination")) + || !params.containsKey("ValidCard")) { continue; } @@ -1367,7 +1368,7 @@ public class ComputerUtil { if (valid.contains("Creature.YouCtrl") || valid.contains("Other+YouCtrl") ) { - final SpellAbility sa = t.getTriggeredSA(); + final SpellAbility sa = t.getOverridingAbility(); if (sa != null && sa.getApi() == ApiType.Pump && sa.hasParam("KW") && sa.getParam("KW").contains("Haste")) { return true; @@ -1375,10 +1376,10 @@ public class ComputerUtil { } } } - + 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")) { @@ -1413,10 +1414,10 @@ public class ComputerUtil { 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) { @@ -1446,7 +1447,7 @@ public class ComputerUtil { final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); all.addAll(ai.getCardsActivableInExternalZones(true)); all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(Presets.PERMANENTS))); - + for (final Card c : all) { for (final SpellAbility sa : c.getSpellAbilities()) { if (sa.getApi() != ApiType.DealDamage) { @@ -1477,15 +1478,13 @@ public class ComputerUtil { // Triggered abilities if (c.isCreature() && c.isInZone(ZoneType.Battlefield) && CombatUtil.canAttack(c)) { for (final Trigger t : c.getTriggers()) { - if ("Attacks".equals(t.getParam("Mode")) && t.hasParam("Execute")) { - String exec = c.getSVar(t.getParam("Execute")); - if (!exec.isEmpty()) { - SpellAbility trigSa = AbilityFactory.getAbility(exec, c); - if (trigSa != null && trigSa.getApi() == ApiType.LoseLife - && trigSa.getParamOrDefault("Defined", "").contains("Opponent")) { - trigSa.setHostCard(c); - damage += AbilityUtils.calculateAmount(trigSa.getHostCard(), trigSa.getParam("LifeAmount"), trigSa); - } + if (TriggerType.Attacks.equals(t.getMode())) { + SpellAbility sa = t.ensureAbility(); + if (sa == null) { + continue; + } + if (sa.getApi() == ApiType.LoseLife && sa.getParamOrDefault("Defined", "").contains("Opponent")) { + damage += AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("LifeAmount"), sa); } } } @@ -1505,7 +1504,7 @@ public class ComputerUtil { /** * Returns list of objects threatened by effects on the stack - * + * * @param ai * calling player * @param sa @@ -1520,7 +1519,7 @@ public class ComputerUtil { 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, @@ -1538,8 +1537,8 @@ public class ComputerUtil { if (top) { break; // only evaluate top-stack } - } - + } + return objects; } @@ -1551,14 +1550,14 @@ public class ComputerUtil { 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; @@ -1572,10 +1571,10 @@ public class ComputerUtil { CardCollectionView battleField = aiPlayer.getCardsIn(ZoneType.Battlefield); objects = CardLists.getValidCards(battleField, topStack.getParam("ValidCards").split(","), source.getController(), source, topStack); } else { - return threatened; + return threatened; } } else { - objects = topStack.getTargets().getTargets(); + objects = topStack.getTargets(); final List canBeTargeted = new ArrayList<>(); for (Object o : objects) { if (o instanceof Card) { @@ -1586,7 +1585,7 @@ public class ComputerUtil { } } if (canBeTargeted.isEmpty()) { - return threatened; + return threatened; } objects = canBeTargeted; } @@ -1655,7 +1654,7 @@ public class ComputerUtil { } // don't use it on creatures that can't be regenerated - if ((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll) && + if ((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll) && (!c.canBeShielded() || noRegen)) { continue; } @@ -1667,14 +1666,14 @@ public class ComputerUtil { 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; @@ -1685,7 +1684,7 @@ public class ComputerUtil { if (saviourApi == ApiType.ChangeZone && (c.getOwner().isOpponentOf(aiPlayer) || c.isToken())) { continue; } - + if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c)) { threatened.add(c); } @@ -1704,7 +1703,7 @@ public class ComputerUtil { } // -Toughness Curse else if ((threatApi == ApiType.Pump || threatApi == ApiType.PumpAll && topStack.isCurse()) - && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll + && (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(), @@ -1717,7 +1716,7 @@ public class ComputerUtil { if (!canRemove) { continue; } - + if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) { final boolean cantSave = c.getNetToughness() + toughness <= dmg || (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && !grantIndestructible @@ -1726,14 +1725,14 @@ public class ComputerUtil { 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; @@ -1827,9 +1826,9 @@ public class ComputerUtil { } } //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 + 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) { @@ -1847,6 +1846,28 @@ public class ComputerUtil { } } } + //Generic curse auras + else if ((threatApi == ApiType.Attach && (topStack.isCurse() || "Curse".equals(topStack.getParam("AILogic"))))) { + AiController aic = aiPlayer.isAI() ? ((PlayerControllerAi)aiPlayer.getController()).getAi() : null; + boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false; + if (enableCurseAuraRemoval) { + 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; @@ -1946,7 +1967,7 @@ public class ComputerUtil { public static int scoreHand(CardCollectionView handList, Player ai, int cardsToReturn) { // TODO Improve hand scoring in relation to cards to return. // If final hand size is 5, score a hand based on what that 5 would be. - // Or if this is really really fast, determine what the 5 would be based on scoring + // Or if this is really really fast, determine what the 5 would be based on scoring // All of the possibilities final AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); @@ -2029,16 +2050,16 @@ public class ComputerUtil { final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand); return scoreHand(handList, ai, cardsToReturn) <= 0; } - + public static CardCollection getPartialParisCandidates(Player ai) { // Commander no longer uses partial paris. 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; } @@ -2046,7 +2067,7 @@ public class ComputerUtil { //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 manaArts = Arrays.asList("Mox Pearl", "Mox Sapphire", "Mox Jet", "Mox Ruby", "Mox Emerald"); - + // evaluate creatures available in deck CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player))); int numCards = allCreatures.size(); @@ -2200,7 +2220,7 @@ public class ComputerUtil { } Collections.sort(goodChoices, CardLists.TextLenComparator); - + CardLists.sortByCmcDesc(goodChoices); dChoices.add(goodChoices.get(0)); @@ -2211,15 +2231,18 @@ public class ComputerUtil { if (p == aiChooser) { // ask that ai player what he would like to discard final AiController aic = ((PlayerControllerAi)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 invalidTypes) { + public static String chooseSomeType(Player ai, String kindOfType, String logic, Collection validTypes, List invalidTypes) { if (invalidTypes == null) { invalidTypes = ImmutableList.of(); } + if (validTypes == null) { + validTypes = ImmutableList.of(); + } final Game game = ai.getGame(); String chosen = ""; @@ -2243,7 +2266,7 @@ public class ComputerUtil { } } if (StringUtils.isEmpty(chosen)) { - chosen = "Creature"; + chosen = validTypes.isEmpty() ? "Creature" : Aggregates.random(validTypes); } } else if (kindOfType.equals("Creature")) { if (logic != null) { @@ -2257,7 +2280,7 @@ public class ComputerUtil { chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Battlefield), valid); } else if (logic.equals("MostProminentOppControls")) { - CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); + CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); chosen = ComputerUtilCard.getMostProminentType(list, valid); if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) { list = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents()); @@ -2284,11 +2307,11 @@ public class ComputerUtil { chosen = ComputerUtilCard.getMostProminentType(list, valid); } else if (logic.equals("MostNeededType")) { - // Choose a type that is in the deck, but not in hand or on the battlefield + // Choose a type that is in the deck, but not in hand or on the battlefield final List basics = new ArrayList<>(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; @@ -2338,13 +2361,15 @@ public class ComputerUtil { return chosen; } - public static Object vote(Player ai, List options, SpellAbility sa, Multimap votes) { + public static Object vote(Player ai, List options, SpellAbility sa, Multimap votes, Player forPlayer) { final Card source = sa.getHostCard(); final Player controller = source.getController(); final Game game = controller.getGame(); boolean opponent = controller.isOpponentOf(ai); + final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1); + if (!sa.hasParam("AILogic")) { return Aggregates.random(options); } @@ -2398,7 +2423,7 @@ public class ComputerUtil { } } // is it can't receive counters, choose +1/+1 ones - if (!source.canReceiveCounters(CounterType.P1P1)) { + if (!source.canReceiveCounters(p1p1Type)) { return opponent ? "Feather" : "Quill"; } // if source is not on the battlefield anymore, choose +1/+1 @@ -2430,7 +2455,7 @@ public class ComputerUtil { Card token = TokenAi.spawnToken(controller, saToken); // is it can't receive counters, choose +1/+1 ones - if (!source.canReceiveCounters(CounterType.P1P1)) { + if (!source.canReceiveCounters(p1p1Type)) { return opponent ? "Strength" : "Numbers"; } @@ -2440,7 +2465,7 @@ public class ComputerUtil { } // token would not survive - if (token == null) { + if (token == null || !token.isCreature() || token.getNetToughness() < 1) { return opponent ? "Numbers" : "Strength"; } @@ -2453,11 +2478,11 @@ public class ComputerUtil { Card sourceNumbers = CardUtil.getLKICopy(source); Card sourceStrength = CardUtil.getLKICopy(source); - sourceNumbers.setCounters(CounterType.P1P1, sourceNumbers.getCounters(CounterType.P1P1) + numStrength); + sourceNumbers.setCounters(p1p1Type, sourceNumbers.getCounters(p1p1Type) + numStrength); sourceNumbers.setZone(source.getZone()); - sourceStrength.setCounters(CounterType.P1P1, - sourceStrength.getCounters(CounterType.P1P1) + numStrength + 1); + sourceStrength.setCounters(p1p1Type, + sourceStrength.getCounters(p1p1Type) + numStrength + 1); sourceStrength.setZone(source.getZone()); int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers; @@ -2479,7 +2504,7 @@ public class ComputerUtil { } // is it can't receive counters, choose +1/+1 ones - if (!source.canReceiveCounters(CounterType.P1P1)) { + if (!source.canReceiveCounters(p1p1Type)) { return opponent ? "Sprout" : "Harvest"; } @@ -2556,11 +2581,11 @@ public class ComputerUtil { }); return ComputerUtilCard.getBestCreatureAI(killables); } - + public static int predictDamageFromSpell(final SpellAbility sa, final Player targetPlayer) { int damage = -1; // returns -1 if the spell does not deal damage final Card card = sa.getHostCard(); - + SpellAbility ab = sa; while (ab != null) { if (ab.getApi() == ApiType.DealDamage) { @@ -2579,12 +2604,12 @@ public class ComputerUtil { } ab = ab.getSubAbility(); } - + return damage; } - + public static int getDamageForPlaying(final Player player, final SpellAbility sa) { - + // check for bad spell cast triggers int damage = 0; final Game game = player.getGame(); @@ -2595,7 +2620,6 @@ public class ComputerUtil { theTriggers.addAll(c.getTriggers()); } for (Trigger trigger : theTriggers) { - Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); @@ -2605,76 +2629,46 @@ public class ComputerUtil { if (!trigger.requirementsCheck(game)) { continue; } - TriggerType mode = trigger.getMode(); - if (mode != TriggerType.SpellCast) { + if (trigger.getMode() != TriggerType.SpellCast) { continue; } - if (trigParams.containsKey("ValidCard")) { - if (!card.isValid(trigParams.get("ValidCard"), source.getController(), source, sa)) { - continue; - } - } - - if (trigParams.containsKey("ValidActivatingPlayer")) { - if (!player.isValid(trigParams.get("ValidActivatingPlayer"), source.getController(), source, sa)) { + if (trigger.hasParam("ValidCard")) { + if (!card.isValid(trigger.getParam("ValidCard"), source.getController(), source, sa)) { continue; } } - if (!trigParams.containsKey("Execute")) { - // fall back for OverridingAbility - SpellAbility trigSa = trigger.getOverridingAbility(); - if (trigSa == null) { - continue; - } - if (trigSa.getApi() == ApiType.DealDamage) { - if (!"TriggeredActivator".equals(trigSa.getParam("Defined"))) { - continue; - } - if (!trigSa.hasParam("NumDmg")) { - continue; - } - damage += ComputerUtilCombat.predictDamageTo(player, - AbilityUtils.calculateAmount(source, trigSa.getParam("NumDmg"), trigSa), source, false); - } else if (trigSa.getApi() == ApiType.LoseLife) { - if (!"TriggeredActivator".equals(trigSa.getParam("Defined"))) { - continue; - } - if (!trigSa.hasParam("LifeAmount")) { - continue; - } - damage += AbilityUtils.calculateAmount(source, trigSa.getParam("LifeAmount"), trigSa); - } - } else { - String ability = source.getSVar(trigParams.get("Execute")); - if (ability.isEmpty()) { + if (trigger.hasParam("ValidActivatingPlayer")) { + if (!player.isValid(trigger.getParam("ValidActivatingPlayer"), source.getController(), source, sa)) { continue; } + } - final Map 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); + // fall back for OverridingAbility + SpellAbility trigSa = trigger.ensureAbility(); + if (trigSa == null) { + continue; + } + if (trigSa.getApi() == ApiType.DealDamage) { + if (!"TriggeredActivator".equals(trigSa.getParam("Defined"))) { + continue; } + if (!trigSa.hasParam("NumDmg")) { + continue; + } + damage += ComputerUtilCombat.predictDamageTo(player, + AbilityUtils.calculateAmount(source, trigSa.getParam("NumDmg"), trigSa), source, false); + } else if (trigSa.getApi() == ApiType.LoseLife) { + if (!"TriggeredActivator".equals(trigSa.getParam("Defined"))) { + continue; + } + if (!trigSa.hasParam("LifeAmount")) { + continue; + } + damage += AbilityUtils.calculateAmount(source, trigSa.getParam("LifeAmount"), trigSa); } } - + return damage; } @@ -2687,7 +2681,6 @@ public class ComputerUtil { theTriggers.addAll(card.getTriggers()); } for (Trigger trigger : theTriggers) { - Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); @@ -2697,101 +2690,80 @@ public class ComputerUtil { if (!trigger.requirementsCheck(game)) { continue; } - if (trigParams.containsKey("CheckOnTriggeredCard") - && AbilityUtils.getDefinedCards(permanent, source.getSVar(trigParams.get("CheckOnTriggeredCard").split(" ")[0]), null).isEmpty()) { + if (trigger.hasParam("CheckOnTriggeredCard") + && AbilityUtils.getDefinedCards(permanent, source.getSVar(trigger.getParam("CheckOnTriggeredCard").split(" ")[0]), null).isEmpty()) { continue; } - TriggerType mode = trigger.getMode(); - if (mode != TriggerType.ChangesZone) { + if (trigger.getMode() != TriggerType.ChangesZone) { continue; } - if (!"Battlefield".equals(trigParams.get("Destination"))) { + if (!"Battlefield".equals(trigger.getParam("Destination"))) { continue; } - if (trigParams.containsKey("ValidCard")) { - if (!permanent.isValid(trigParams.get("ValidCard"), source.getController(), source, null)) { + if (trigger.hasParam("ValidCard")) { + if (!permanent.isValid(trigger.getParam("ValidCard"), source.getController(), source, null)) { continue; } } - if (!trigParams.containsKey("Execute")) { - // fall back for OverridingAbility - SpellAbility trigSa = trigger.getOverridingAbility(); - if (trigSa == null) { + // fall back for OverridingAbility + SpellAbility trigSa = trigger.ensureAbility(); + if (trigSa == null) { + continue; + } + if (trigSa.getApi() == ApiType.DealDamage) { + if (!"TriggeredCardController".equals(trigSa.getParam("Defined"))) { continue; } - if (trigSa.getApi() == ApiType.DealDamage) { - if (!"TriggeredCardController".equals(trigSa.getParam("Defined"))) { - continue; - } - if (!trigSa.hasParam("NumDmg")) { - continue; - } - damage += ComputerUtilCombat.predictDamageTo(player, - AbilityUtils.calculateAmount(source, trigSa.getParam("NumDmg"), trigSa), source, false); - } else if (trigSa.getApi() == ApiType.LoseLife) { - if (!"TriggeredCardController".equals(trigSa.getParam("Defined"))) { - continue; - } - if (!trigSa.hasParam("LifeAmount")) { - continue; - } - damage += AbilityUtils.calculateAmount(source, trigSa.getParam("LifeAmount"), trigSa); - } - } else { - String ability = source.getSVar(trigParams.get("Execute")); - if (ability.isEmpty()) { + if (!trigSa.hasParam("NumDmg")) { continue; } - - final Map 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); + damage += ComputerUtilCombat.predictDamageTo(player, + AbilityUtils.calculateAmount(source, trigSa.getParam("NumDmg"), trigSa), source, false); + } else if (trigSa.getApi() == ApiType.LoseLife) { + if (!"TriggeredCardController".equals(trigSa.getParam("Defined"))) { + continue; } + if (!trigSa.hasParam("LifeAmount")) { + continue; + } + damage += AbilityUtils.calculateAmount(source, trigSa.getParam("LifeAmount"), trigSa); } } return damage; } public static boolean isNegativeCounter(CounterType type, Card c) { - return type == CounterType.AGE || type == CounterType.BRIBERY || type == CounterType.DOOM - || type == CounterType.M1M1 || type == CounterType.M0M2 || type == CounterType.M0M1 - || type == CounterType.M1M0 || type == CounterType.M2M1 || type == CounterType.M2M2 + return type.is(CounterEnumType.AGE) || type.is(CounterEnumType.BRIBERY) || type.is(CounterEnumType.DOOM) + || type.is(CounterEnumType.M1M1) || type.is(CounterEnumType.M0M2) || type.is(CounterEnumType.M0M1) + || type.is(CounterEnumType.M1M0) || type.is(CounterEnumType.M2M1) || type.is(CounterEnumType.M2M2) // Blaze only hurts Lands - || (type == CounterType.BLAZE && c.isLand()) + || (type.is(CounterEnumType.BLAZE) && c.isLand()) // Iceberg does use Ice as Storage - || (type == CounterType.ICE && !"Iceberg".equals(c.getName())) + || (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName())) // some lands does use Depletion as Storage Counter - || (type == CounterType.DEPLETION && c.hasKeyword("CARDNAME doesn't untap during your untap step.")) + || (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step.")) // treat Time Counters on suspended Cards as Bad, // and also on Chronozoa - || (type == CounterType.TIME && (!c.isInPlay() || "Chronozoa".equals(c.getName()))) - || type == CounterType.GOLD || type == CounterType.MUSIC || type == CounterType.PUPA - || type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP - || type == CounterType.SLUMBER || type == CounterType.SLEIGHT || type == CounterType.WAGE; + || (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName()))) + || type.is(CounterEnumType.GOLD) || type.is(CounterEnumType.MUSIC) || type.is(CounterEnumType.PUPA) + || type.is(CounterEnumType.PARALYZATION) || type.is(CounterEnumType.SHELL) || type.is(CounterEnumType.SLEEP) + || type.is(CounterEnumType.SLUMBER) || type.is(CounterEnumType.SLEIGHT) || type.is(CounterEnumType.WAGE); } // this countertypes has no effect - public static boolean isUselessCounter(CounterType type) { - return type == CounterType.AWAKENING || type == CounterType.MANIFESTATION || type == CounterType.PETRIFICATION - || type == CounterType.TRAINING; + public static boolean isUselessCounter(CounterType type, Card c) { + + // Quest counter on a card without MaxQuestEffect are useless + if (type.is(CounterEnumType.QUEST)) { + int e = 0; + if ( c.hasSVar("MaxQuestEffect")) { + e = Integer.parseInt(c.getSVar("MaxQuestEffect")); + } + return c.getCounters(type) > e; + } + + return type.is(CounterEnumType.AWAKENING) || type.is(CounterEnumType.MANIFESTATION) || type.is(CounterEnumType.PETRIFICATION) + || type.is(CounterEnumType.TRAINING); } public static Player evaluateBoardPosition(final List listToEvaluate) { @@ -2903,13 +2875,13 @@ public class ComputerUtil { return false; } - + public static boolean targetPlayableSpellCard(final Player ai, CardCollection options, final SpellAbility sa, final boolean withoutPayingManaCost) { // determine and target a card with a SA that the AI can afford and will play AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); Card targetSpellCard = null; for (Card c : options) { - if (withoutPayingManaCost && c.getManaCost() != null && c.getManaCost().getShardCount(ManaCostShard.X) > 0) { + if (withoutPayingManaCost && c.getManaCost() != null && c.getManaCost().countX() > 0) { // The AI will otherwise cheat with the mana payment, announcing X > 0 for spells like Heat Ray when replaying them // without paying their mana cost. continue; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index 18687559682..8505ff58bed 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -81,14 +81,7 @@ public class ComputerUtilAbility { public static List getSpellAbilities(final CardCollectionView l, final Player player) { final List spellAbilities = Lists.newArrayList(); for (final Card c : l) { - for (final SpellAbility sa : c.getSpellAbilities()) { - spellAbilities.add(sa); - } - if (c.isFaceDown() && c.isInZone(ZoneType.Exile) && !c.mayPlay(player).isEmpty()) { - for (final SpellAbility sa : c.getState(CardStateName.Original).getSpellAbilities()) { - spellAbilities.add(sa); - } - } + spellAbilities.addAll(c.getAllPossibleAbilities(player, false)); } return spellAbilities; } @@ -109,9 +102,7 @@ public class ComputerUtilAbility { List priorityAltSa = Lists.newArrayList(); List otherAltSa = Lists.newArrayList(); for (SpellAbility altSa : saAltCosts) { - if (altSa.getPayCosts() == null || sa.getPayCosts() == null) { - otherAltSa.add(altSa); - } else if (sa.getPayCosts().isOnlyManaCost() + if (sa.getPayCosts().isOnlyManaCost() && altSa.getPayCosts().isOnlyManaCost() && sa.getPayCosts().getTotalMana().compareTo(altSa.getPayCosts().getTotalMana()) == 1) { // the alternative cost is strictly cheaper, so why not? (e.g. Omniscience etc.) priorityAltSa.add(altSa); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 01385c3ca4e..efd78ccad60 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -6,6 +6,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import forge.card.CardStateName; import forge.card.CardType; import forge.card.ColorSet; import forge.card.MagicColor; @@ -15,7 +16,6 @@ import forge.deck.Deck; import forge.deck.DeckSection; 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.card.*; @@ -172,30 +172,30 @@ public class ComputerUtilCard { // if no non-basic lands, target the least represented basic land type String sminBL = ""; - int iminBL = 20000; // hopefully no one will ever have more than 20000 - // lands of one type.... + int iminBL = Integer.MAX_VALUE; int n = 0; for (String name : MagicColor.Constant.BASIC_LANDS) { n = CardLists.getType(land, name).size(); - if ((n < iminBL) && (n > 0)) { - // if two or more are tied, only the - // first - // one checked will be used + if (n < iminBL && n > 0) { iminBL = n; sminBL = name; } } - if (iminBL == 20000) { - return null; // no basic land was a minimum + if (iminBL == Integer.MAX_VALUE) { + // All basic lands have no basic land type. Just return something + Iterator untapped = Iterables.filter(land, CardPredicates.Presets.UNTAPPED).iterator(); + if (untapped.hasNext()) { + return untapped.next(); + } + return land.get(0); } final List bLand = CardLists.getType(land, sminBL); - + for (Card ut : Iterables.filter(bLand, CardPredicates.Presets.UNTAPPED)) { return ut; } - return Aggregates.random(bLand); // random tapped land of least represented type } @@ -698,39 +698,21 @@ public class ComputerUtilCard { } // same for Trigger that does make Tokens for(Trigger t:c.getTriggers()){ - SpellAbility sa = t.getOverridingAbility(); - String sTokenTypes = null; + SpellAbility sa = t.ensureAbility(); if (sa != null) { if (sa.getApi() != ApiType.Token || !sa.hasParam("TokenTypes")) { continue; } - sTokenTypes = sa.getParam("TokenTypes"); - } else if (t.hasParam("Execute")) { - String name = t.getParam("Execute"); - if (!c.hasSVar(name)) { - continue; + for (String var : sa.getParam("TokenTypes").split(",")) { + if (!CardType.isACreatureType(var)) { + continue; + } + Integer count = typesInDeck.get(var); + if (count == null) { + count = 0; + } + typesInDeck.put(var, count + 1); } - - Map params = AbilityFactory.getMapParams(c.getSVar(name)); - if (!params.containsKey("TokenTypes")) { - continue; - } - sTokenTypes = params.get("TokenTypes"); - } - - if (sTokenTypes == null) { - continue; - } - - for (String var : sTokenTypes.split(",")) { - if (!CardType.isACreatureType(var)) { - continue; - } - Integer count = typesInDeck.get(var); - if (count == null) { - count = 0; - } - typesInDeck.put(var, count + 1); } } // special rule for Fabricate and Servo @@ -1423,8 +1405,8 @@ public class ComputerUtilCard { if (combat.isAttacking(c) && opp.getLife() > 0) { int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true); int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true); - int poisonOrig = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0; - int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0; + int poisonOrig = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0; + int poisonPumped = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0; // predict Infect if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) { @@ -1447,7 +1429,8 @@ public class ComputerUtilCard { } if (pumpedDmg > dmg) { if ((!c.hasKeyword(Keyword.INFECT) && pumpedDmg >= opp.getLife()) - || (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) { + || (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterEnumType.POISON) && pumpedDmg >= opp.getPoisonCounters()) + || ("PumpForTrample".equals(sa.getParam("AILogic")))) { return true; } } @@ -1474,7 +1457,7 @@ public class ComputerUtilCard { if (totalPowerUnblocked >= opp.getLife()) { return true; } else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) { - if (sa.getPayCosts() != null && sa.getPayCosts().hasNoManaCost()) { + if (sa.getPayCosts().hasNoManaCost()) { return true; // always activate abilities which cost no mana and which can increase unblocked damage } } @@ -1765,10 +1748,10 @@ public class ComputerUtilCard { } public static boolean hasActiveUndyingOrPersist(final Card c) { - if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterType.P1P1) == 0) { + if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterEnumType.P1P1) == 0) { return true; } - if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterType.M1M1) == 0) { + if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterEnumType.M1M1) == 0) { return true; } return false; @@ -1785,10 +1768,6 @@ public class ComputerUtilCard { for (Card c : otb) { for (SpellAbility sa : c.getSpellAbilities()) { - if (sa.getPayCosts() == null) { - continue; - } - CostPayEnergy energyCost = sa.getPayCosts().getCostEnergy(); if (energyCost != null) { int amount = energyCost.convertAmount(); @@ -1860,7 +1839,7 @@ public class ComputerUtilCard { public static AiPlayDecision checkNeedsToPlayReqs(final Card card, final SpellAbility sa) { Game game = card.getGame(); - boolean isRightSplit = sa != null && sa.isRightSplit(); + boolean isRightSplit = sa != null && sa.getCardState().getStateName() == CardStateName.RightSplit; String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay"; String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar"; @@ -1912,21 +1891,12 @@ public class ComputerUtilCard { } if (card.getSVar(needsToPlayVarName).length() > 0) { final String needsToPlay = card.getSVar(needsToPlayVarName); - int x = 0; - int y = 0; String sVar = needsToPlay.split(" ")[0]; String comparator = needsToPlay.split(" ")[1]; String compareTo = comparator.substring(2); - try { - x = Integer.parseInt(sVar); - } catch (final NumberFormatException e) { - x = CardFactoryUtil.xCount(card, card.getSVar(sVar)); - } - try { - y = Integer.parseInt(compareTo); - } catch (final NumberFormatException e) { - y = CardFactoryUtil.xCount(card, card.getSVar(compareTo)); - } + int x = AbilityUtils.calculateAmount(card, sVar, sa); + int y = AbilityUtils.calculateAmount(card, compareTo, sa); + if (!Expressions.compare(x, comparator, y)) { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } @@ -1943,4 +1913,7 @@ public class ComputerUtilCard { public static boolean isCardRemRandomDeck(final Card card) { return card.getRules() != null && card.getRules().getAiHints().getRemRandomDecks(); } + public static boolean isCardRemNonCommanderDeck(final Card card) { + return card.getRules() != null && card.getRules().getAiHints().getRemNonCommanderDecks(); + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index da25cab609d..fe240b141db 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -28,7 +28,6 @@ import com.google.common.collect.Maps; import forge.game.CardTraitBase; import forge.game.Game; import forge.game.GameEntity; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; @@ -43,11 +42,9 @@ import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementType; -import forge.game.spellability.AbilityActivated; import forge.game.spellability.SpellAbility; import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; -import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.MyRandom; @@ -328,7 +325,7 @@ public class ComputerUtilCombat { public static int resultingPoison(final Player ai, final Combat combat) { // ai can't get poision counters, so the value can't change - if (!ai.canReceiveCounters(CounterType.POISON)) { + if (!ai.canReceiveCounters(CounterEnumType.POISON)) { return ai.getPoisonCounters(); } @@ -931,7 +928,7 @@ public class ComputerUtilCombat { if (dealsFirstStrikeDamage(attacker, withoutAbilities, null) && (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)) && !dealsFirstStrikeDamage(blocker, withoutAbilities, null) - && !blocker.canReceiveCounters(CounterType.M1M1)) { + && !blocker.canReceiveCounters(CounterEnumType.M1M1)) { power -= attacker.getNetCombatDamage(); } @@ -973,62 +970,45 @@ public class ComputerUtilCombat { } theTriggers.addAll(attacker.getTriggers()); for (final Trigger trigger : theTriggers) { - final Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) { continue; } - Map abilityParams = null; - if (trigger.getOverridingAbility() != null) { - abilityParams = trigger.getOverridingAbility().getMapParams(); - } else if (trigParams.containsKey("Execute")) { - final String ability = source.getSVar(trigParams.get("Execute")); - abilityParams = AbilityFactory.getMapParams(ability); - } else { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } - if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump")) { + if (!ApiType.Pump.equals(sa.getApi())) { continue; } - if (abilityParams.containsKey("DB") && !abilityParams.get("DB").equals("Pump")) { + + if (sa.usesTargeting()) { continue; } - if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) { - continue; // targeted pumping not supported + + if (!sa.hasParam("NumAtt")) { + continue; } - final List list = AbilityUtils.getDefinedCards(source, abilityParams.get("Defined"), null); - if (abilityParams.containsKey("Defined") && abilityParams.get("Defined").equals("TriggeredBlocker")) { + + String defined = sa.getParam("Defined"); + final List list = AbilityUtils.getDefinedCards(source, defined, sa); + if ("TriggeredBlocker".equals(defined)) { list.add(blocker); } - if (list.isEmpty()) { - continue; - } if (!list.contains(blocker)) { continue; } - if (!abilityParams.containsKey("NumAtt")) { - continue; - } - String att = abilityParams.get("NumAtt"); - if (att.startsWith("+")) { - att = att.substring(1); - } - try { - power += Integer.parseInt(att); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) - power += 0; - } + power += AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa, true); } if (withoutAbilities) { return power; } for (SpellAbility ability : blocker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!ability.isActivatedAbility()) { continue; } if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) { @@ -1058,7 +1038,7 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && blocker.getCounters(CounterEnumType.P1P1) > 0) { continue; } @@ -1108,102 +1088,61 @@ public class ComputerUtilCombat { } theTriggers.addAll(attacker.getTriggers()); for (final Trigger trigger : theTriggers) { - final Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) { continue; } - Map abilityParams = null; - if (trigger.getOverridingAbility() != null) { - abilityParams = trigger.getOverridingAbility().getMapParams(); - } else if (trigParams.containsKey("Execute")) { - final String ability = source.getSVar(trigParams.get("Execute")); - abilityParams = AbilityFactory.getMapParams(ability); - } else { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } - String abType = ""; - if (abilityParams.containsKey("AB")) { - abType = abilityParams.get("AB"); - } else if (abilityParams.containsKey("DB")) { - abType = abilityParams.get("DB"); - } - // DealDamage triggers - if (abType.equals("DealDamage")) { - if (!abilityParams.containsKey("Defined") || !abilityParams.get("Defined").equals("TriggeredBlocker")) { - continue; - } - int damage = 0; - try { - damage = Integer.parseInt(abilityParams.get("NumDmg")); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) + if (ApiType.DealDamage.equals(sa.getApi())) { + if (!"TriggeredBlocker".equals(sa.getParam("Defined"))) { continue; } + int damage = AbilityUtils.calculateAmount(source, sa.getParam("NumDmg"), sa); toughness -= predictDamageTo(blocker, damage, 0, source, false); - continue; - } + } else // -1/-1 PutCounter triggers - if (abType.equals("PutCounter")) { - if (!abilityParams.containsKey("Defined") || !abilityParams.get("Defined").equals("TriggeredBlocker")) { + if (ApiType.PutCounter.equals(sa.getApi())) { + if (!"TriggeredBlocker".equals(sa.getParam("Defined"))) { continue; } - if (!abilityParams.containsKey("CounterType") || !abilityParams.get("CounterType").equals("M1M1")) { + if (!"M1M1".equals(sa.getParam("CounterType"))) { continue; } - int num = 0; - try { - num = Integer.parseInt(abilityParams.get("CounterNum")); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) - continue; - } - toughness -= num; - continue; - } + toughness -= AbilityUtils.calculateAmount(source, sa.getParam("CounterNum"), sa); + } else // Pump triggers - if (!abType.equals("Pump")) { - continue; - } - if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) { - continue; // targeted pumping not supported - } - final List list = AbilityUtils.getDefinedCards(source, abilityParams.get("Defined"), null); - if (abilityParams.containsKey("Defined") && abilityParams.get("Defined").equals("TriggeredBlocker")) { - list.add(blocker); - } - if (list.isEmpty()) { - continue; - } - if (!list.contains(blocker)) { - continue; - } - if (!abilityParams.containsKey("NumDef")) { - continue; - } - - String def = abilityParams.get("NumDef"); - if (def.startsWith("+")) { - def = def.substring(1); - } - try { - toughness += Integer.parseInt(def); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) + if (ApiType.Pump.equals(sa.getApi())) { + if (sa.usesTargeting()) { + continue; // targeted pumping not supported + } + final List list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), null); + if ("TriggeredBlocker".equals(sa.getParam("Defined"))) { + list.add(blocker); + } + if (list.isEmpty() || !list.contains(blocker)) { + continue; + } + if (!sa.hasParam("NumDef")) { + continue; + } + toughness += AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa, true); } } if (withoutAbilities) { return toughness; } for (SpellAbility ability : blocker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!ability.isActivatedAbility()) { continue; } @@ -1234,7 +1173,7 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && blocker.getCounters(CounterEnumType.P1P1) > 0) { continue; } @@ -1296,7 +1235,7 @@ public class ComputerUtilCombat { if (ComputerUtilCombat.dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)) && !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat) - && !attacker.canReceiveCounters(CounterType.M1M1)) { + && !attacker.canReceiveCounters(CounterEnumType.M1M1)) { power -= blocker.getNetCombatDamage(); } theTriggers.addAll(blocker.getTriggers()); @@ -1333,44 +1272,26 @@ public class ComputerUtilCombat { } for (final Trigger trigger : theTriggers) { - final Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) { continue; } - Map abilityParams = null; - if (trigger.getOverridingAbility() != null) { - abilityParams = trigger.getOverridingAbility().getMapParams(); - } else if (trigParams.containsKey("Execute")) { - final String ability = source.getSVar(trigParams.get("Execute")); - abilityParams = AbilityFactory.getMapParams(ability); - } else { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } - if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) { + if (sa.usesTargeting()) { continue; // targeted pumping not supported } - if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump") - && !abilityParams.get("AB").equals("PumpAll")) { - continue; - } - if (abilityParams.containsKey("DB") && !abilityParams.get("DB").equals("Pump") - && !abilityParams.get("DB").equals("PumpAll")) { + + if (!ApiType.Pump.equals(sa.getApi()) && !ApiType.PumpAll.equals(sa.getApi())) { continue; } - if (abilityParams.containsKey("Cost")) { - SpellAbility sa = null; - if (trigger.getOverridingAbility() != null) { - sa = trigger.getOverridingAbility(); - } else { - final String ability = source.getSVar(trigParams.get("Execute")); - sa = AbilityFactory.getAbility(ability, source); - } - + if (sa.hasParam("Cost")) { sa.setActivatingPlayer(source.getController()); if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { continue; @@ -1378,15 +1299,15 @@ public class ComputerUtilCombat { } List list = Lists.newArrayList(); - if (!abilityParams.containsKey("ValidCards")) { - list = AbilityUtils.getDefinedCards(source, abilityParams.get("Defined"), null); + if (!sa.hasParam("ValidCards")) { + list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), null); } - if (abilityParams.containsKey("Defined") && abilityParams.get("Defined").equals("TriggeredAttacker")) { + if (sa.hasParam("Defined") && sa.getParam("Defined").equals("TriggeredAttacker")) { list.add(attacker); } - if (abilityParams.containsKey("ValidCards")) { - if (attacker.isValid(abilityParams.get("ValidCards").split(","), source.getController(), source, null) - || attacker.isValid(abilityParams.get("ValidCards").replace("attacking+", "").split(","), + if (sa.hasParam("ValidCards")) { + if (attacker.isValid(sa.getParam("ValidCards").split(","), source.getController(), source, null) + || attacker.isValid(sa.getParam("ValidCards").replace("attacking+", "").split(","), source.getController(), source, null)) { list.add(attacker); } @@ -1397,11 +1318,11 @@ public class ComputerUtilCombat { if (!list.contains(attacker)) { continue; } - if (!abilityParams.containsKey("NumAtt")) { + if (!sa.hasParam("NumAtt")) { continue; } - String att = abilityParams.get("NumAtt"); + String att = sa.getParam("NumAtt"); if (att.startsWith("+")) { att = att.substring(1); } @@ -1426,7 +1347,7 @@ public class ComputerUtilCombat { return power; } for (SpellAbility ability : attacker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!ability.isActivatedAbility()) { continue; } if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) { @@ -1456,7 +1377,7 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && attacker.getCounters(CounterEnumType.P1P1) > 0) { continue; } @@ -1521,148 +1442,135 @@ public class ComputerUtilCombat { final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield); for (final Card card : cardList) { for (final StaticAbility stAb : card.getStaticAbilities()) { - final Map params = stAb.getMapParams(); - if (!params.get("Mode").equals("Continuous")) { + if (!"Continuous".equals(stAb.getParam("Mode"))) { continue; } - if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) { - final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature"); + if (!stAb.hasParam("Affected")) { + continue; + } + if (!stAb.hasParam("AddToughness")) { + continue; + } + String affected = stAb.getParam("Affected"); + String addT = stAb.getParam("AddToughness"); + if (affected.contains("attacking")) { + final String valid = TextUtil.fastReplace(affected, "attacking", "Creature"); if (!attacker.isValid(valid, card.getController(), card, null)) { continue; } - if (params.containsKey("AddToughness")) { - if (params.get("AddToughness").equals("X")) { - toughness += CardFactoryUtil.xCount(card, card.getSVar("X")); - } else if (params.get("AddToughness").equals("Y")) { - toughness += CardFactoryUtil.xCount(card, card.getSVar("Y")); - } else { - toughness += Integer.valueOf(params.get("AddToughness")); - } - } - } else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) { - final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature"); + toughness += AbilityUtils.calculateAmount(card, addT, stAb, true); + } else if (affected.contains("untapped")) { + final String valid = TextUtil.fastReplace(affected, "untapped", "Creature"); if (!attacker.isValid(valid, card.getController(), card, null) || attacker.hasKeyword(Keyword.VIGILANCE)) { continue; } // remove the bonus, because it will no longer be granted - if (params.containsKey("AddToughness")) { - toughness -= Integer.valueOf(params.get("AddToughness")); - } + toughness -= AbilityUtils.calculateAmount(card, addT, stAb, true); } } } } for (final Trigger trigger : theTriggers) { - final Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) { continue; } - Map abilityParams = null; - if (trigger.getOverridingAbility() != null) { - abilityParams = trigger.getOverridingAbility().getMapParams(); - } else if (trigParams.containsKey("Execute")) { - final String ability = source.getSVar(trigParams.get("Execute")); - abilityParams = AbilityFactory.getMapParams(ability); - } else { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } + sa.setActivatingPlayer(source.getController()); - if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) { + if (sa.usesTargeting()) { continue; // targeted pumping not supported } // DealDamage triggers - if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("DealDamage")) - || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("DealDamage"))) { - if (!abilityParams.containsKey("Defined") || !abilityParams.get("Defined").equals("TriggeredAttacker")) { - continue; - } - int damage = 0; - try { - damage = Integer.parseInt(abilityParams.get("NumDmg")); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) + if (ApiType.DealDamage.equals(sa.getApi())) { + if ("TriggeredAttacker".equals(sa.getParam("Defined"))) { continue; } + int damage = AbilityUtils.calculateAmount(source, sa.getParam("NumDmg"), sa); + toughness -= predictDamageTo(attacker, damage, 0, source, false); continue; - } + } else if (ApiType.Pump.equals(sa.getApi())) { - // Pump triggers - if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump") - && !abilityParams.get("AB").equals("PumpAll")) { - continue; - } - if (abilityParams.containsKey("DB") && !abilityParams.get("DB").equals("Pump") - && !abilityParams.get("DB").equals("PumpAll")) { - continue; - } - - if (abilityParams.containsKey("Cost")) { - SpellAbility sa = null; - if (trigger.getOverridingAbility() != null) { - sa = trigger.getOverridingAbility(); - } else { - final String ability = source.getSVar(trigParams.get("Execute")); - sa = AbilityFactory.getAbility(ability, source); + if (sa.hasParam("Cost")) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + continue; + } } - - sa.setActivatingPlayer(source.getController()); - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!sa.hasParam("NumDef")) { continue; } - } - - List list = Lists.newArrayList(); - if (!abilityParams.containsKey("ValidCards")) { - list = AbilityUtils.getDefinedCards(source, abilityParams.get("Defined"), null); - } - if (abilityParams.containsKey("Defined") && abilityParams.get("Defined").equals("TriggeredAttacker")) { - list.add(attacker); - } - if (abilityParams.containsKey("ValidCards")) { - if (attacker.isValid(abilityParams.get("ValidCards").split(","), source.getController(), source, null) - || attacker.isValid(abilityParams.get("ValidCards").replace("attacking+", "").split(","), - source.getController(), source, null)) { + CardCollection list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); + if ("TriggeredAttacker".equals(sa.getParam("Defined"))) { list.add(attacker); } - } - if (list.isEmpty()) { - continue; - } - if (!list.contains(attacker)) { - continue; - } - if (!abilityParams.containsKey("NumDef")) { - continue; - } - - String def = abilityParams.get("NumDef"); - if (def.startsWith("+")) { - def = def.substring(1); - } - if (def.matches("[0-9][0-9]?") || def.matches("-" + "[0-9][0-9]?")) { - toughness += Integer.parseInt(def); - } else { - String bonus = source.getSVar(def); - if (bonus.contains("TriggerCount$NumBlockers")) { - bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); - } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee - bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); + if (!list.contains(attacker)) { + continue; + } + + String def = sa.getParam("NumDef"); + if (def.startsWith("+")) { + def = def.substring(1); + } + if (def.matches("[0-9][0-9]?") || def.matches("-" + "[0-9][0-9]?")) { + toughness += Integer.parseInt(def); + } else { + String bonus = AbilityUtils.getSVar(sa, def); + if (bonus.contains("TriggerCount$NumBlockers")) { + bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); + } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee + bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); + } + toughness += CardFactoryUtil.xCount(source, bonus); + } + } else if (ApiType.PumpAll.equals(sa.getApi())) { + + if (sa.hasParam("Cost")) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + continue; + } + } + + if (!sa.hasParam("ValidCards")) { + continue; + } + if (!sa.hasParam("NumDef")) { + continue; + } + if (!attacker.isValid(sa.getParam("ValidCards").replace("attacking+", "").split(","), source.getController(), source, sa)) { + continue; + } + + String def = sa.getParam("NumDef"); + if (def.startsWith("+")) { + def = def.substring(1); + } + if (def.matches("[0-9][0-9]?") || def.matches("-" + "[0-9][0-9]?")) { + toughness += Integer.parseInt(def); + } else { + String bonus = AbilityUtils.getSVar(sa, def); + if (bonus.contains("TriggerCount$NumBlockers")) { + bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); + } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee + bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); + } + toughness += CardFactoryUtil.xCount(source, bonus); } - toughness += CardFactoryUtil.xCount(source, bonus); } } if (withoutAbilities) { return toughness; } for (SpellAbility ability : attacker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!ability.isActivatedAbility()) { continue; } @@ -1672,18 +1580,19 @@ public class ComputerUtilCombat { if (ability.usesTargeting() && !ability.canTarget(attacker)) { continue; } + if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) { + continue; + } + if (!ComputerUtilCost.canPayCost(ability, attacker.getController())) { + continue; + } if (ability.getApi() == ApiType.Pump) { if (!ability.hasParam("NumDef")) { continue; } - if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) { - int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability); - if (tBonus > 0) { - toughness += tBonus; - } - } + toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true); } else if (ability.getApi() == ApiType.PutCounter) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { continue; @@ -1693,15 +1602,13 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && attacker.getCounters(CounterEnumType.P1P1) > 0) { continue; } - if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) { - int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability); - if (tBonus > 0) { - toughness += tBonus; - } + int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability); + if (tBonus > 0) { + toughness += tBonus; } } } @@ -1730,35 +1637,26 @@ public class ComputerUtilCombat { theTriggers.addAll(card.getTriggers()); } for (Trigger trigger : theTriggers) { - Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) { continue; } - //consider delayed triggers - if (trigParams.containsKey("DelayedTrigger")) { - String sVarName = trigParams.get("DelayedTrigger"); - trigger = TriggerHandler.parseTrigger(source.getSVar(sVarName), trigger.getHostCard(), true); - trigParams = trigger.getMapParams(); - } - if (!trigParams.containsKey("Execute")) { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } - String ability = source.getSVar(trigParams.get("Execute")); - final Map abilityParams = AbilityFactory.getMapParams(ability); - if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("Destroy")) - || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("Destroy"))) { - if (!abilityParams.containsKey("Defined")) { + if (ApiType.Destroy.equals(sa.getApi())) { + if (!sa.hasParam("Defined")) { continue; } - if (abilityParams.get("Defined").equals("TriggeredAttacker")) { + if (sa.getParam("Defined").equals("TriggeredAttacker")) { return true; } - if (abilityParams.get("Defined").equals("Self") && source.equals(attacker)) { + if (sa.getParam("Defined").equals("Self") && source.equals(attacker)) { return true; } - if (abilityParams.get("Defined").equals("TriggeredTarget") && source.equals(blocker)) { + if (sa.getParam("Defined").equals("TriggeredTarget") && source.equals(blocker)) { return true; } } @@ -1848,10 +1746,10 @@ public class ComputerUtilCombat { if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities)) && !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))) - || (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker - .getCounters(CounterType.M1M1) == 0)) - || (attacker.hasKeyword(Keyword.UNDYING) && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker - .getCounters(CounterType.P1P1) == 0))) { + || (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterEnumType.M1M1) && (attacker + .getCounters(CounterEnumType.M1M1) == 0)) + || (attacker.hasKeyword(Keyword.UNDYING) && !attacker.canReceiveCounters(CounterEnumType.P1P1) && (attacker + .getCounters(CounterEnumType.P1P1) == 0))) { return false; } @@ -2008,36 +1906,27 @@ public class ComputerUtilCombat { theTriggers.addAll(card.getTriggers()); } for (Trigger trigger : theTriggers) { - Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) { continue; } - //consider delayed triggers - if (trigParams.containsKey("DelayedTrigger")) { - String sVarName = trigParams.get("DelayedTrigger"); - trigger = TriggerHandler.parseTrigger(source.getSVar(sVarName), trigger.getHostCard(), true); - trigParams = trigger.getMapParams(); - } - if (!trigParams.containsKey("Execute")) { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } - String ability = source.getSVar(trigParams.get("Execute")); - final Map abilityParams = AbilityFactory.getMapParams(ability); // Destroy triggers - if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("Destroy")) - || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("Destroy"))) { - if (!abilityParams.containsKey("Defined")) { + if (ApiType.Destroy.equals(sa.getApi())) { + if (!sa.hasParam("Defined")) { continue; } - if (abilityParams.get("Defined").equals("TriggeredBlocker")) { + if (sa.getParam("Defined").equals("TriggeredBlocker")) { return true; } - if (abilityParams.get("Defined").equals("Self") && source.equals(blocker)) { + if (sa.getParam("Defined").equals("Self") && source.equals(blocker)) { return true; } - if (abilityParams.get("Defined").equals("TriggeredTarget") && source.equals(attacker)) { + if (sa.getParam("Defined").equals("TriggeredTarget") && source.equals(attacker)) { return true; } } @@ -2080,10 +1969,10 @@ public class ComputerUtilCombat { if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker .hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) - || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker - .getCounters(CounterType.M1M1) == 0)) - || (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker - .getCounters(CounterType.P1P1) == 0))) { + || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterEnumType.M1M1) && (blocker + .getCounters(CounterEnumType.M1M1) == 0)) + || (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterEnumType.P1P1) && (blocker + .getCounters(CounterEnumType.P1P1) == 0))) { return false; } @@ -2517,7 +2406,7 @@ public class ComputerUtilCombat { final Player controller = combatant.getController(); for (Card c : controller.getCardsIn(ZoneType.Battlefield)) { for (SpellAbility ability : c.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!ability.isActivatedAbility()) { continue; } if (ability.getApi() != ApiType.Pump) { @@ -2651,26 +2540,24 @@ public class ComputerUtilCombat { // Test for some special triggers that can change the creature in combat for (Trigger t : attacker.getTriggers()) { - if (t.getMode() == TriggerType.Attacks && t.hasParam("Execute")) { - if (!attacker.hasSVar(t.getParam("Execute"))) { + if (t.getMode() == TriggerType.Attacks) { + SpellAbility exec = t.ensureAbility(); + if (exec == null) { continue; } - SpellAbility exec = AbilityFactory.getAbility(attacker, t.getParam("Execute")); - if (exec != null) { - if (exec.getApi() == ApiType.Clone && "Self".equals(exec.getParam("CloneTarget")) - && exec.hasParam("ValidTgts") && exec.getParam("ValidTgts").contains("Creature") - && exec.getParam("ValidTgts").contains("attacking")) { - // Tilonalli's Skinshifter and potentially other similar cards that can clone other stuff - // while attacking - if (exec.getParam("ValidTgts").contains("nonLegendary") && attacker.getType().isLegendary()) { - continue; - } - int maxPwr = 0; - for (Card c : attacker.getController().getCreaturesInPlay()) { - if (c.getNetPower() > maxPwr || (c.getNetPower() == maxPwr && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(attackerAfterTrigs))) { - maxPwr = c.getNetPower(); - attackerAfterTrigs = c; - } + if (exec.getApi() == ApiType.Clone && "Self".equals(exec.getParam("CloneTarget")) + && exec.hasParam("ValidTgts") && exec.getParam("ValidTgts").contains("Creature") + && exec.getParam("ValidTgts").contains("attacking")) { + // Tilonalli's Skinshifter and potentially other similar cards that can clone other stuff + // while attacking + if (exec.getParam("ValidTgts").contains("nonLegendary") && attacker.getType().isLegendary()) { + continue; + } + int maxPwr = 0; + for (Card c : attacker.getController().getCreaturesInPlay()) { + if (c.getNetPower() > maxPwr || (c.getNetPower() == maxPwr && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(attackerAfterTrigs))) { + maxPwr = c.getNetPower(); + attackerAfterTrigs = c; } } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index c94a5e7e16d..84ddd5791c6 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -19,6 +19,8 @@ import forge.game.zone.ZoneType; import forge.util.MyRandom; import forge.util.TextUtil; import forge.util.collect.FCollectionView; + +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import java.util.List; @@ -45,7 +47,7 @@ public class ComputerUtilCost { final CostPutCounter addCounter = (CostPutCounter) part; final CounterType type = addCounter.getCounter(); - if (type.equals(CounterType.M1M1)) { + if (type.is(CounterEnumType.M1M1)) { return false; } } @@ -53,9 +55,6 @@ public class ComputerUtilCost { return true; } - public static boolean checkRemoveCounterCost(final Cost cost, final Card source) { - return checkRemoveCounterCost(cost, source, null); - } /** * Check remove counter cost. * @@ -69,13 +68,14 @@ public class ComputerUtilCost { if (cost == null) { return true; } + final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa); for (final CostPart part : cost.getCostParts()) { if (part instanceof CostRemoveCounter) { final CostRemoveCounter remCounter = (CostRemoveCounter) part; final CounterType type = remCounter.counter; if (!part.payCostFromSource()) { - if (CounterType.P1P1.equals(type)) { + if (type.is(CounterEnumType.P1P1)) { return false; } continue; @@ -86,19 +86,8 @@ public class ComputerUtilCost { return false; } - // Remove X counters - set ChosenX to max possible value here, the SAs should correct that - // value later as the AI decides what to do (in checkApiLogic / checkAiLogic) - if (sa != null && sa.hasSVar(remCounter.getAmount())) { - final String sVar = sa.getSVar(remCounter.getAmount()); - if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) { - sa.setSVar("ChosenX", String.valueOf(source.getCounters(type))); - } - } - - // check the sa what the PaymentDecision is. // ignore Loyality abilities with Zero as Cost - if (sa != null && !CounterType.LOYALTY.equals(type)) { - final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa); + if (!type.is(CounterEnumType.LOYALTY)) { PaymentDecision pay = decision.visit(remCounter); if (pay == null || pay.c <= 0) { return false; @@ -106,19 +95,15 @@ public class ComputerUtilCost { } //don't kill the creature - if (CounterType.P1P1.equals(type) && source.getLethalDamage() <= 1 + if (type.is(CounterEnumType.P1P1) && source.getLethalDamage() <= 1 && !source.hasKeyword(Keyword.UNDYING)) { return false; } } else if (part instanceof CostRemoveAnyCounter) { - if (sa != null) { - final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part; + final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part; - PaymentDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa).visit(remCounter); - return decision != null; - } - - return false; + PaymentDecision pay = decision.visit(remCounter); + return pay != null; } } return true; @@ -467,9 +452,9 @@ public class ComputerUtilCost { if(!meetsRestriction) continue; - try { + if (StringUtils.isNumeric(parts[0])) { extraManaNeeded += Integer.parseInt(parts[0]); - } catch (final NumberFormatException e) { + } else { System.out.println("wrong SpellsNeedExtraMana SVar format on " + c); } } @@ -480,9 +465,9 @@ public class ComputerUtilCost { } final String snem = c.getSVar("SpellsNeedExtraManaEffect"); if (!StringUtils.isBlank(snem)) { - try { + if (StringUtils.isNumeric(snem)) { extraManaNeeded += Integer.parseInt(snem); - } catch (final NumberFormatException e) { + } else { System.out.println("wrong SpellsNeedExtraManaEffect SVar format on " + c); } } @@ -529,7 +514,7 @@ public class ComputerUtilCost { public boolean apply(Card card) { boolean hasManaSa = false; for (final SpellAbility sa : card.getSpellAbilities()) { - if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) { hasManaSa = true; break; } @@ -619,7 +604,8 @@ public class ComputerUtilCost { if (combat.getAttackers().isEmpty()) { return false; } - } else if ("nonToken".equals(aiLogic) && AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) { + } else if ("nonToken".equals(aiLogic) && !AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).isEmpty() + && AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) { return false; } else if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) { return false; @@ -676,4 +662,62 @@ public class ComputerUtilCost { } return false; } + + public static int getMaxXValue(SpellAbility sa, Player ai) { + final Card source = sa.getHostCard(); + final SpellAbility root = sa.getRootAbility(); + final Cost abCost = sa.getPayCosts(); + if (abCost == null || !abCost.hasXInAnyCostPart()) { + return 0; + } + + Integer val = null; + + if (sa.costHasManaX()) { + val = ComputerUtilMana.determineLeftoverMana(root, ai); + } + + if (sa.usesTargeting()) { + // if announce is used as min targets, check what the max possible number would be + if ("X".equals(sa.getTargetRestrictions().getMinTargets())) { + val = ObjectUtils.min(val, CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa).size()); + } + + if (sa.hasParam("AIMaxTgtsCount")) { + // Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified + // TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable? + val = ObjectUtils.min(val, AbilityUtils.calculateAmount(sa.getHostCard(), "Count$" + sa.getParam("AIMaxTgtsCount"), sa)); + } + } + + val = ObjectUtils.min(val, abCost.getMaxForNonManaX(root, ai)); + + if (val != null && val > 0) { + // filter cost parts for preferences, don't choose X > than possible preferences + for (final CostPart part : abCost.getCostParts()) { + if (part instanceof CostSacrifice) { + if (part.payCostFromSource()) { + continue; + } + if (!part.getAmount().equals("X")) { + continue; + } + + final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), part.getType().split(";"), source.getController(), source, null); + + int count = 0; + while (count < val) { + Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList); + if (prefCard == null) { + break; + } + typeList.remove(prefCard); + count++; + } + val = ObjectUtils.min(val, count); + } + } + } + return ObjectUtils.defaultIfNull(val, 0); + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 4102df2fefb..161e84c2dda 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -4,21 +4,21 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.*; import forge.ai.ability.AnimateAi; -import forge.card.CardStateName; import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaAtom; import forge.card.mana.ManaCost; import forge.card.mana.ManaCostParser; import forge.card.mana.ManaCostShard; +import forge.game.CardTraitPredicates; import forge.game.Game; import forge.game.GameActionUtil; -import forge.game.ability.AbilityKey; -import forge.game.ability.AbilityUtils; -import forge.game.ability.ApiType; +import forge.game.ability.*; import forge.game.card.*; +import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.*; +import forge.game.keyword.Keyword; import forge.game.mana.Mana; import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaPool; @@ -26,10 +26,14 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerPredicates; import forge.game.replacement.ReplacementEffect; +import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementType; import forge.game.spellability.AbilityManaPart; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbility; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.MyRandom; import forge.util.TextUtil; @@ -53,17 +57,17 @@ public class ComputerUtilMana { public static boolean canPayManaCost(final SpellAbility sa, final Player ai, final int extraMana) { return payManaCost(sa, ai, true, extraMana, true); } - + /** * Return the number of colors used for payment for Converge */ public static int getConvergeCount(final SpellAbility sa, final Player ai) { - ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0); - if (payManaCost(cost, sa, ai, true, true)) { - return cost.getSunburst(); - } else { - return 0; - } + ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0); + if (payManaCost(cost, sa, ai, true, true)) { + return cost.getSunburst(); + } else { + return 0; + } } // Does not check if mana sources can be used right now, just checks for potential chance. @@ -118,14 +122,14 @@ public class ComputerUtilMana { return score; } - - private static void sortManaAbilities(final Multimap manaAbilityMap) { + + private static void sortManaAbilities(final Multimap manaAbilityMap, final SpellAbility sa) { final Map manaCardMap = Maps.newHashMap(); final List orderedCards = Lists.newArrayList(); - + for (final ManaCostShard shard : manaAbilityMap.keySet()) { for (SpellAbility ability : manaAbilityMap.get(shard)) { - final Card hostCard = ability.getHostCard(); + final Card hostCard = ability.getHostCard(); if (!manaCardMap.containsKey(hostCard)) { manaCardMap.put(hostCard, scoreManaProducingCard(hostCard)); orderedCards.add(hostCard); @@ -185,9 +189,61 @@ public class ComputerUtilMana { } manaAbilityMap.replaceValues(shard, newAbilities); + + // Sort the first N abilities so that the preferred shard is selected, e.g. Adamant + String manaPref = sa.getParamOrDefault("AIManaPref", ""); + if (manaPref.isEmpty() && sa.getHostCard() != null && sa.getHostCard().hasSVar("AIManaPref")) { + manaPref = sa.getHostCard().getSVar("AIManaPref"); + } + + if (!manaPref.isEmpty()) { + final String[] prefShardInfo = manaPref.split(":"); + final String preferredShard = prefShardInfo[0]; + final int preferredShardAmount = prefShardInfo.length > 1 ? Integer.parseInt(prefShardInfo[1]) : 3; + + if (!preferredShard.isEmpty()) { + final List prefSortedAbilities = new ArrayList<>(newAbilities); + final List otherSortedAbilities = new ArrayList<>(newAbilities); + + Collections.sort(prefSortedAbilities, new Comparator() { + @Override + public int compare(final SpellAbility ability1, final SpellAbility ability2) { + if (ability1.getManaPart().mana().contains(preferredShard)) + return -1; + else if (ability2.getManaPart().mana().contains(preferredShard)) + return 1; + + return 0; + } + }); + Collections.sort(otherSortedAbilities, new Comparator() { + @Override + public int compare(final SpellAbility ability1, final SpellAbility ability2) { + if (ability1.getManaPart().mana().contains(preferredShard)) + return 1; + else if (ability2.getManaPart().mana().contains(preferredShard)) + return -1; + + return 0; + } + }); + + final List finalAbilities = new ArrayList<>(); + for (int i = 0; i < preferredShardAmount && i < prefSortedAbilities.size(); i++) { + finalAbilities.add(prefSortedAbilities.get(i)); + } + for (int i = 0; i < otherSortedAbilities.size(); i++) { + SpellAbility ab = otherSortedAbilities.get(i); + if (!finalAbilities.contains(ab)) + finalAbilities.add(ab); + } + + manaAbilityMap.replaceValues(shard, finalAbilities); + } + } } } - + public static SpellAbility chooseManaAbility(ManaCostBeingPaid cost, SpellAbility sa, Player ai, ManaCostShard toPay, Collection saList, boolean checkCosts) { for (final SpellAbility ma : saList) { @@ -265,7 +321,191 @@ public class ComputerUtilMana { } return null; } - + + public static String predictManaReplacement(SpellAbility saPayment, Player ai, ManaCostShard toPay) { + Card hostCard = saPayment.getHostCard(); + Game game = hostCard.getGame(); + String manaProduced = toPay.isSnow() && hostCard.isSnow() ? "S" : GameActionUtil.generatedTotalMana(saPayment); + //String originalProduced = manaProduced; + + final Map repParams = AbilityKey.newMap(); + repParams.put(AbilityKey.Mana, manaProduced); + repParams.put(AbilityKey.Affected, hostCard); + repParams.put(AbilityKey.Player, ai); + repParams.put(AbilityKey.AbilityMana, saPayment); // RootAbility + + // TODO Damping Sphere might replace later? + + // add flags to replacementEffects to filter better? + List reList = game.getReplacementHandler().getReplacementList(ReplacementType.ProduceMana, repParams, ReplacementLayer.Other); + + List replaceMana = Lists.newArrayList(); + List replaceType = Lists.newArrayList(); + List replaceAmount = Lists.newArrayList(); // currently only multi + + // try to guess the color the mana gets replaced to + for (ReplacementEffect re : reList) { + SpellAbility o = re.getOverridingAbility(); + + if (o == null || o.getApi() != ApiType.ReplaceMana) { + continue; + } + + // this one does replace the amount too + if (o.hasParam("ReplaceMana")) { + replaceMana.add(o); + } else if (o.hasParam("ReplaceType") || o.hasParam("ReplaceColor")) { + // this one replaces the color/type + // check if this one can be replaced into wanted mana shard + replaceType.add(o); + } else if (o.hasParam("ReplaceAmount")) { + replaceAmount.add(o); + } + } + + // it is better to apply these ones first + if (!replaceMana.isEmpty()) { + for (SpellAbility saMana : replaceMana) { + // one of then has to Any + // one of then has to C + // one of then has to B + String m = saMana.getParam("ReplaceMana"); + if ("Any".equals(m)) { + byte rs = MagicColor.GREEN; + for (byte c : MagicColor.WUBRGC) { + if (toPay.canBePaidWithManaOfColor(c)) { + rs = c; + break; + } + } + manaProduced = MagicColor.toShortString(rs); + } else { + manaProduced = m; + } + } + } + + // then apply this one + if (!replaceType.isEmpty()) { + for (SpellAbility saMana : replaceAmount) { + Card card = saMana.getHostCard(); + if (saMana.hasParam("ReplaceType")) { + // replace color and colorless + String color = saMana.getParam("ReplaceType"); + if ("Any".equals(color)) { + byte rs = MagicColor.GREEN; + for (byte c : MagicColor.WUBRGC) { + if (toPay.canBePaidWithManaOfColor(c)) { + rs = c; + break; + } + } + color = MagicColor.toShortString(rs); + } + for (byte c : MagicColor.WUBRGC) { + String s = MagicColor.toShortString(c); + manaProduced = manaProduced.replace(s, color); + } + } else if (saMana.hasParam("ReplaceColor")) { + // replace color + String color = saMana.getParam("ReplaceColor"); + if ("Chosen".equals(color)) { + if (card.hasChosenColor()) { + color = MagicColor.toShortString(card.getChosenColor()); + } + } + if (saMana.hasParam("ReplaceOnly")) { + manaProduced = manaProduced.replace(saMana.getParam("ReplaceOnly"), color); + } else { + for (byte c : MagicColor.WUBRG) { + String s = MagicColor.toShortString(c); + manaProduced = manaProduced.replace(s, color); + } + } + } + } + } + + // then multiply if able + if (!replaceAmount.isEmpty()) { + int totalAmount = 1; + for (SpellAbility saMana : replaceAmount) { + totalAmount *= Integer.valueOf(saMana.getParam("ReplaceAmount")); + } + manaProduced = StringUtils.repeat(manaProduced, " ", totalAmount); + } + + return manaProduced; + } + + public static String predictManafromSpellAbility(SpellAbility saPayment, Player ai, ManaCostShard toPay) { + Card hostCard = saPayment.getHostCard(); + + String manaProduced = predictManaReplacement(saPayment, ai, toPay); + String originalProduced = manaProduced; + + if (originalProduced.isEmpty()) { + return manaProduced; + } + + // Run triggers like Nissa + final Map runParams = AbilityKey.mapFromCard(hostCard); + runParams.put(AbilityKey.Player, ai); // assuming AI would only ever gives itself mana + runParams.put(AbilityKey.AbilityMana, saPayment); + runParams.put(AbilityKey.Produced, manaProduced); + runParams.put(AbilityKey.Activator, ai); + for (Trigger tr : ai.getGame().getTriggerHandler().getActiveTrigger(TriggerType.TapsForMana, runParams)) { + SpellAbility trSA = tr.ensureAbility(); + if (trSA == null) { + continue; + } + if (ApiType.Mana.equals(trSA.getApi())) { + int pAmount = AbilityUtils.calculateAmount(trSA.getHostCard(), trSA.getParamOrDefault("Amount", "1"), trSA); + String produced = trSA.getParam("Produced"); + if (produced.equals("Chosen")) { + produced = MagicColor.toShortString(trSA.getHostCard().getChosenColor()); + } + manaProduced += " " + StringUtils.repeat(produced, pAmount); + } else if (ApiType.ManaReflected.equals(trSA.getApi())) { + final String colorOrType = trSA.getParamOrDefault("ColorOrType", "Color"); + // currently Color or Type, Type is colors + colorless + final String reflectProperty = trSA.getParam("ReflectProperty"); + + if (reflectProperty.equals("Produced") && !originalProduced.isEmpty()) { + // check if a colorless shard can be paid from the trigger + if (toPay.equals(ManaCostShard.COLORLESS) && colorOrType.equals("Type") && originalProduced.contains("C")) { + manaProduced += " " + "C"; + } else if (originalProduced.length() == 1) { + // if length is only one, and it either is equal C == Type + if (colorOrType.equals("Type") || !originalProduced.equals("C")) { + manaProduced += " " + originalProduced; + } + } else { + // should it look for other shards too? + boolean found = false; + for (String s : originalProduced.split(" ")) { + if (colorOrType.equals("Type") || !s.equals("C") && toPay.canBePaidWithManaOfColor(MagicColor.fromName(s))) { + found = true; + manaProduced += " " + s; + break; + } + } + // no good mana found? just add the first generated color + if (!found) { + for (String s : originalProduced.split(" ")) { + if (colorOrType.equals("Type") || !s.equals("C")) { + manaProduced += " " + s; + break; + } + } + } + } + } + } + } + return manaProduced; + } + public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai) { CardCollection manaSources = new CardCollection(); @@ -310,7 +550,7 @@ public class ComputerUtilMana { // select which abilities may be used for each shard Multimap sourcesForShards = ComputerUtilMana.groupAndOrderToPayShards(ai, manaAbilityMap, cost); - sortManaAbilities(sourcesForShards); + sortManaAbilities(sourcesForShards, sa); ManaCostShard toPay; // Loop over mana needed @@ -341,29 +581,19 @@ public class ComputerUtilMana { manaSources.add(saPayment.getHostCard()); setExpressColorChoice(sa, ai, cost, toPay, saPayment); - String manaProduced = toPay.isSnow() ? "S" : GameActionUtil.generatedMana(saPayment); - manaProduced = AbilityManaPart.applyManaReplacement(saPayment, manaProduced); + String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay); + //System.out.println(manaProduced); payMultipleMana(cost, manaProduced, ai); // remove from available lists - /* - * Refactoring this code to sourcesForShards.values().removeIf((SpellAbility srcSa) -> srcSa.getHostCard().equals(saPayment.getHostCard())); - * causes Android build not to compile - * */ - Iterator itSa = sourcesForShards.values().iterator(); - while (itSa.hasNext()) { - SpellAbility srcSa = itSa.next(); - if (srcSa.getHostCard().equals(saPayment.getHostCard())) { - itSa.remove(); - } - } + Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard())); } handleOfferingsAI(sa, true, cost.isPaid()); refundMana(manaSpentToPay, ai, sa); - + return manaSources; } // getManaSourcesToPayCost() @@ -371,19 +601,30 @@ public class ComputerUtilMana { adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai); List manaSpentToPay = test ? new ArrayList<>() : sa.getPayingMana(); boolean purePhyrexian = cost.containsOnlyPhyrexianMana(); - int testEnergyPool = ai.getCounters(CounterType.ENERGY); + int testEnergyPool = ai.getCounters(CounterEnumType.ENERGY); + + boolean ignoreColor = false, ignoreType = false; + StaticAbility mayPlay = sa.getMayPlay(); + if (mayPlay != null) { + if (mayPlay.hasParam("MayPlayIgnoreColor")) { + ignoreColor = true; + } else if (mayPlay.hasParam("MayPlayIgnoreType")) { + ignoreType = true; + } + } List paymentList = Lists.newArrayList(); if (payManaCostFromPool(cost, sa, ai, test, manaSpentToPay)) { - return true; // paid all from floating mana + return true; // paid all from floating mana } - + boolean hasConverge = sa.getHostCard().hasConverge(); ListMultimap sourcesForShards = getSourcesForShards(cost, sa, ai, test, - checkPlayable, manaSpentToPay, hasConverge); + checkPlayable, manaSpentToPay, hasConverge, ignoreColor, ignoreType); + if (sourcesForShards == null && !purePhyrexian) { - return false; // no mana abilities to use for paying + return false; // no mana abilities to use for paying } final ManaPool manapool = ai.getManaPool(); @@ -392,26 +633,53 @@ public class ComputerUtilMana { // Loop over mana needed while (!cost.isPaid()) { + while (!cost.isPaid() && !manapool.isEmpty()) { + boolean found = false; + + // Apply the color/type conversion matrix if necessary + final CostPayment pay = new CostPayment(sa.getPayCosts(), sa); + if (ignoreType) { + AbilityUtils.applyManaColorConversion(pay, MagicColor.Constant.ANY_TYPE_CONVERSION); + } else if (ignoreColor) { + AbilityUtils.applyManaColorConversion(pay, MagicColor.Constant.ANY_COLOR_CONVERSION); + } + manapool.applyCardMatrix(pay); + + for (byte color : MagicColor.WUBRGC) { + if (manapool.tryPayCostWithColor(color, sa, cost)) { + found = true; + break; + } + } + if (!found) { + break; + } + } + if (cost.isPaid()) { + break; + } toPay = getNextShardToPay(cost); boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B"); Collection saList = null; - if (hasConverge && - (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) { - final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION; - for (final byte b : ColorSet.fromMask(unpaidColors)) { // try and pay other colors for converge - final ManaCostShard shard = ManaCostShard.valueOf(b); - saList = sourcesForShards.get(shard); - if (saList != null && !saList.isEmpty()) { - toPay = shard; - break; - } - } - if (saList == null || saList.isEmpty()) { // failed to converge, revert to paying generic - saList = sourcesForShards.get(toPay); - hasConverge = false; - } + if (hasConverge && + (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) { + final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION; + for (final byte b : ColorSet.fromMask(unpaidColors)) { + // try and pay other colors for converge + final ManaCostShard shard = ManaCostShard.valueOf(b); + saList = sourcesForShards.get(shard); + if (saList != null && !saList.isEmpty()) { + toPay = shard; + break; + } + } + if (saList == null || saList.isEmpty()) { + // failed to converge, revert to paying generic + saList = sourcesForShards.get(toPay); + hasConverge = false; + } } else { if (!(sourcesForShards == null && purePhyrexian)) { saList = sourcesForShards.get(toPay); @@ -428,7 +696,7 @@ public class ComputerUtilMana { SpellAbility saPayment = saList.isEmpty() ? null : chooseManaAbility(cost, sa, ai, toPay, saList, checkPlayable || !test); if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) { - if (sa.getTargets() != null && sa.getTargets().isTargeting(saPayment.getHostCard())) { + if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) { saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for continue; } @@ -478,45 +746,34 @@ public class ComputerUtilMana { setExpressColorChoice(sa, ai, cost, toPay, saPayment); if (test) { - // Check energy when testing - CostPayEnergy energyCost = saPayment.getPayCosts().getCostEnergy(); - if (energyCost != null) { - testEnergyPool -= Integer.parseInt(energyCost.getAmount()); - if (testEnergyPool < 0) { - // Can't pay energy cost - break; - } - } + // Check energy when testing + CostPayEnergy energyCost = saPayment.getPayCosts().getCostEnergy(); + if (energyCost != null) { + testEnergyPool -= Integer.parseInt(energyCost.getAmount()); + if (testEnergyPool < 0) { + // Can't pay energy cost + break; + } + } - String manaProduced = toPay.isSnow() ? "S" : GameActionUtil.generatedMana(saPayment); - manaProduced = AbilityManaPart.applyManaReplacement(saPayment, manaProduced); - //System.out.println(manaProduced); + // FIXME: if we're ignoring color or type, assume that the color/type of the mana produced will fit the case + // for the purpose of testing (since adding appropriate sources for shards in this particular case is handled + // inside getSourcesForShards) + // This is hacky and may be prone to bugs, so better implementation ideas are highly welcome. + String manaProduced = ignoreColor || ignoreType ? MagicColor.toShortString(toPay.getColorMask()) + : predictManafromSpellAbility(saPayment, ai, toPay); + + // System.out.println(manaProduced); payMultipleMana(cost, manaProduced, ai); // remove from available lists - /* - * Refactoring this code to sourcesForShards.values().removeIf((SpellAbility srcSa) -> srcSa.getHostCard().equals(saPayment.getHostCard())); - * causes Android build not to compile - * */ - Iterator itSa = sourcesForShards.values().iterator(); - while (itSa.hasNext()) { - SpellAbility srcSa = itSa.next(); - if (srcSa.getHostCard().equals(saPayment.getHostCard())) { - itSa.remove(); - } - } + Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard())); } else { - if (saPayment.getPayCosts() != null) { - final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment); - if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment))) { - saList.remove(saPayment); - continue; - } - } - else { - System.err.println("Ability " + saPayment + " from " + saPayment.getHostCard() + " had NULL as payCost"); - saPayment.getHostCard().tap(); + final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment); + if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment))) { + saList.remove(saPayment); + continue; } ai.getGame().getStack().addAndUnfreeze(saPayment); @@ -525,20 +782,10 @@ public class ComputerUtilMana { // no need to remove abilities from resource map, // once their costs are paid and consume resources, they can not be used again - - if (hasConverge) { // hack to prevent converge re-using sources - // remove from available lists - /* - * Refactoring this code to sourcesForShards.values().removeIf((SpellAbility srcSa) -> srcSa.getHostCard().equals(saPayment.getHostCard())); - * causes Android build not to compile - * */ - Iterator itSa = sourcesForShards.values().iterator(); - while (itSa.hasNext()) { - SpellAbility srcSa = itSa.next(); - if (srcSa.getHostCard().equals(saPayment.getHostCard())) { - itSa.remove(); - } - } + + if (hasConverge) { + // hack to prevent converge re-using sources + Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard())); } } } @@ -551,15 +798,6 @@ public class ComputerUtilMana { // extraMana, sa.getHostCard(), sa.toUnsuppressedString(), StringUtils.join(paymentPlan, "\n\t")); // } - // See if it's possible to pay with something that was left in the mana pool in corner cases, - // e.g. Gemstone Caverns with a Luck counter on it generating colored mana (which fails to be - // processed correctly on a per-ability basis, leaving floating mana in the pool) - if (!cost.isPaid() && !manapool.isEmpty()) { - for (byte color : MagicColor.WUBRGC) { - manapool.tryPayCostWithColor(color, sa, cost); - } - } - // The cost is still unpaid, so refund the mana and report if (!cost.isPaid()) { refundMana(manaSpentToPay, ai, sa); @@ -573,13 +811,6 @@ public class ComputerUtilMana { } } - // Note: manaSpentToPay shouldn't be cleared here, since it needs to remain - // on the SpellAbility in order for effects that check mana spent cost to work. - - sa.getHostCard().setColorsPaid(cost.getColorsPaid()); - // if (sa instanceof Spell_Permanent) // should probably add this - sa.getHostCard().setSunburstValue(cost.getSunburst()); - if (test) { refundMana(manaSpentToPay, ai, sa); resetPayment(paymentList); @@ -594,16 +825,31 @@ public class ComputerUtilMana { } } + private static void addAllSourcesForMagicColorRange(final ListMultimap manaAbilityMap, final ListMultimap sourcesForShards, final byte[] range) { + for (final byte b : range) { + final ManaCostShard shard = ManaCostShard.valueOf(b); + if (!sourcesForShards.containsKey(shard)) { + for (final byte c : range) { + for (SpellAbility saMana : manaAbilityMap.get((int) c)) { + if (!sourcesForShards.get(shard).contains(saMana)) { + sourcesForShards.get(shard).add(saMana); + } + } + } + } + } + } - /** - * Creates a mapping between the required mana shards and the available spell abilities to pay for them - */ - private static ListMultimap getSourcesForShards(final ManaCostBeingPaid cost, - final SpellAbility sa, final Player ai, final boolean test, final boolean checkPlayable, - List manaSpentToPay, final boolean hasConverge) { - // arrange all mana abilities by color produced. + /** + * Creates a mapping between the required mana shards and the available spell abilities to pay for them + */ + private static ListMultimap getSourcesForShards(final ManaCostBeingPaid cost, + final SpellAbility sa, final Player ai, final boolean test, final boolean checkPlayable, + List manaSpentToPay, final boolean hasConverge, final boolean ignoreColor, final boolean ignoreType) { + // arrange all mana abilities by color produced. final ListMultimap manaAbilityMap = ComputerUtilMana.groupSourcesByManaColor(ai, checkPlayable); - if (manaAbilityMap.isEmpty()) { // no mana abilities, bailing out + if (manaAbilityMap.isEmpty()) { + // no mana abilities, bailing out refundMana(manaSpentToPay, ai, sa); handleOfferingsAI(sa, test, cost.isPaid()); return null; @@ -614,25 +860,34 @@ public class ComputerUtilMana { // select which abilities may be used for each shard ListMultimap sourcesForShards = ComputerUtilMana.groupAndOrderToPayShards(ai, manaAbilityMap, cost); - if (hasConverge) { // add extra colors for paying converge - final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION; - for (final byte b : ColorSet.fromMask(unpaidColors)) { - final ManaCostShard shard = ManaCostShard.valueOf(b); - if (!sourcesForShards.containsKey(shard)) { - if (ai.getManaPool().canPayForShardWithColor(shard, b)) { + if (hasConverge) { + // add extra colors for paying converge + final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION; + for (final byte b : ColorSet.fromMask(unpaidColors)) { + final ManaCostShard shard = ManaCostShard.valueOf(b); + if (!sourcesForShards.containsKey(shard)) { + if (ai.getManaPool().canPayForShardWithColor(shard, b)) { for (SpellAbility saMana : manaAbilityMap.get((int)b)) { - sourcesForShards.get(shard).add(sourcesForShards.get(shard).size(), saMana); + sourcesForShards.get(shard).add(saMana); } } - } - } + } + } } - sortManaAbilities(sourcesForShards); + + // add all other types/colors if the specific type/color doesn't matter + if (ignoreType) { + addAllSourcesForMagicColorRange(manaAbilityMap, sourcesForShards, MagicColor.WUBRGC); + } else if (ignoreColor) { + addAllSourcesForMagicColorRange(manaAbilityMap, sourcesForShards, MagicColor.WUBRG); + } + + sortManaAbilities(sourcesForShards, sa); if (DEBUG_MANA_PAYMENT) { System.out.println("DEBUG_MANA_PAYMENT: sourcesForShards = " + sourcesForShards); } - return sourcesForShards; - } + return sourcesForShards; + } /** * Checks if the given mana cost can be paid from floating mana. @@ -643,9 +898,9 @@ public class ComputerUtilMana { * @param manaSpentToPay list of mana spent * @return whether the floating mana is sufficient to pay the cost fully */ - private static boolean payManaCostFromPool(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, + private static boolean payManaCostFromPool(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean test, List manaSpentToPay) { - final boolean hasConverge = sa.getHostCard().hasConverge(); + final boolean hasConverge = sa.getHostCard().hasConverge(); List unpaidShards = cost.getUnpaidShards(); Collections.sort(unpaidShards); // most difficult shards must come first for (ManaCostShard part : unpaidShards) { @@ -726,22 +981,22 @@ public class ComputerUtilMana { // if we are simulating mana payment for the human controller, use the first mana available (and avoid prompting the human player) if (!(ai.getController() instanceof PlayerControllerAi)) { - return manaChoices.get(0); + return manaChoices.get(0); } // Let them choose then return ai.getController().chooseManaFromPool(manaChoices); } - private static List> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard, - final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid) { + private static List> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard, + final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid) { final List> weightedOptions = new ArrayList<>(); for (final Mana thisMana : manapool) { if (!manapool.canPayForShardWithColor(shard, thisMana.getColor())) { continue; } - if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsManaRestrictions(saBeingPaidFor)) { + if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) { continue; } @@ -756,11 +1011,11 @@ public class ComputerUtilMana { int weight = 0; if (colorsPaid == -1) { - // prefer colorless mana to spend - weight += thisMana.isColorless() ? 5 : 0; + // prefer colorless mana to spend + weight += thisMana.isColorless() ? 5 : 0; } else { - // get more colors for converge - weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0; + // get more colors for converge + weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0; } // prefer restricted mana to spend @@ -777,7 +1032,7 @@ public class ComputerUtilMana { } return weightedOptions; } - + private static void setExpressColorChoice(final SpellAbility sa, final Player ai, ManaCostBeingPaid cost, ManaCostShard toPay, SpellAbility saPayment) { @@ -818,7 +1073,7 @@ public class ComputerUtilMana { if (isManaSourceReserved(ai, sourceCard, sa)) { return false; } - + if (toPay.isSnow() && !sourceCard.isSnow()) { return false; } @@ -837,10 +1092,9 @@ public class ComputerUtilMana { if (checkCosts) { // Check if AI can still play this mana ability ma.setActivatingPlayer(ai); - if (ma.getPayCosts() != null) { // if the AI can't pay the additional costs skip the mana ability - if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) { - return false; - } + // if the AI can't pay the additional costs skip the mana ability + if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) { + return false; } else if (sourceCard.isTapped()) { return false; @@ -965,7 +1219,7 @@ public class ComputerUtilMana { *

* getComboManaChoice. *

- * + * * @param manaAb * a {@link forge.game.spellability.SpellAbility} object. * @param saRoot @@ -989,7 +1243,7 @@ public class ComputerUtilMana { choice = abMana.getExpressChoice(); abMana.clearExpressChoice(); byte colorMask = ManaAtom.fromName(choice); - if (abMana.canProduce(choice, manaAb) && testCost.isAnyPartPayableWith(colorMask, ai.getManaPool())) { + if (manaAb.canProduce(choice) && satisfiesColorChoice(abMana, choiceString, choice) && testCost.isAnyPartPayableWith(colorMask, ai.getManaPool())) { choiceString.append(choice); payMultipleMana(testCost, choice, ai); continue; @@ -999,7 +1253,7 @@ public class ComputerUtilMana { if (!testCost.isPaid()) { // Loop over combo colors for (String color : comboColors) { - if (testCost.isAnyPartPayableWith(ManaAtom.fromName(color), ai.getManaPool())) { + if (satisfiesColorChoice(abMana, choiceString, choice) && testCost.isAnyPartPayableWith(ManaAtom.fromName(color), ai.getManaPool())) { payMultipleMana(testCost, color, ai); if (nMana != 1) { choiceString.append(" "); @@ -1014,14 +1268,18 @@ public class ComputerUtilMana { } } // check if combo mana can produce most common color in hand - String commonColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn( - ZoneType.Hand)); - if (!commonColor.isEmpty() && abMana.getComboColors().contains(MagicColor.toShortString(commonColor))) { + String commonColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Hand)); + if (!commonColor.isEmpty() && satisfiesColorChoice(abMana, choiceString, MagicColor.toShortString(commonColor)) && abMana.getComboColors().contains(MagicColor.toShortString(commonColor))) { choice = MagicColor.toShortString(commonColor); } else { - // default to first color - choice = comboColors[0]; + // default to first available color + for (String c : comboColors) { + if (satisfiesColorChoice(abMana, choiceString, c)) { + choice = c; + break; + } + } } if (nMana != 1) { choiceString.append(" "); @@ -1036,6 +1294,10 @@ public class ComputerUtilMana { abMana.setExpressChoice(choiceString.toString()); } + private static boolean satisfiesColorChoice(AbilityManaPart abMana, StringBuilder choices, String choice) { + return !abMana.getOrigProduced().contains("Different") || !choices.toString().contains(choice); + } + /** *

* payMultipleMana. @@ -1066,7 +1328,7 @@ public class ComputerUtilMana { } return unused.isEmpty() ? null : StringUtils.join(unused, ' '); } - + /** * Find all mana sources. * @param manaAbilityMap The map of SpellAbilities that produce mana. @@ -1098,7 +1360,7 @@ public class ComputerUtilMana { res.putAll(shard, manaAbilityMap.get(ManaAtom.GENERIC)); continue; } - + if (shard == ManaCostShard.GENERIC) { continue; } @@ -1125,8 +1387,8 @@ public class ComputerUtilMana { * @param extraMana extraMana * @return ManaCost */ - static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final boolean test, final int extraMana) { - Card card = sa.getHostCard(); + public static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final boolean test, final int extraMana) { + Card card = sa.getHostCard(); ZoneType castFromBackup = null; if (test && sa.isSpell()) { castFromBackup = card.getCastFrom(); @@ -1144,42 +1406,29 @@ public class ComputerUtilMana { ManaCostBeingPaid cost = new ManaCostBeingPaid(mana, restriction); // Tack xMana Payments into mana here if X is a set value - if (sa.getPayCosts() != null && (cost.getXcounter() > 0 || extraMana > 0)) { + if (cost.getXcounter() > 0 || extraMana > 0) { int manaToAdd = 0; if (test && extraMana > 0) { final int multiplicator = Math.max(cost.getXcounter(), 1); manaToAdd = extraMana * multiplicator; } else { - // For Count$xPaid set PayX in the AFs then use that here - // Else calculate it as appropriate. - final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X"; - if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar)) { - if (xSvar.equals("PayX") && (card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar))) { - // X SVar may end up being an empty string when copying a spell with no cost (e.g. Jhoira Avatar) - String xValue = card.hasSVar(xSvar) ? card.getSVar(xSvar) : card.getState(CardStateName.Original).getSVar(xSvar); - manaToAdd = xValue.isEmpty() ? 0 : Integer.parseInt(xValue) * cost.getXcounter(); // X - } else { - manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter(); - } - } + manaToAdd = AbilityUtils.calculateAmount(card, "X", sa) * cost.getXcounter(); } - String manaXColor = sa.getParam("XColor"); - ManaCostShard shardToGrow = ManaCostShard.parseNonGeneric(manaXColor == null ? "1" : manaXColor); - cost.increaseShard(shardToGrow, manaToAdd); + cost.increaseShard(ManaCostShard.parseNonGeneric(sa.getParamOrDefault("XColor", "1")), manaToAdd); if (!test) { - card.setXManaCostPaid(manaToAdd / cost.getXcounter()); + sa.setXManaCostPaid(manaToAdd / cost.getXcounter()); } } - + CostAdjustment.adjust(cost, sa, null, test); int timesMultikicked = card.getKickerMagnitude(); if (timesMultikicked > 0 && sa.hasParam("Announce") && sa.getParam("Announce").startsWith("Multikicker")) { ManaCost mkCost = sa.getMultiKickerManaCost(); for (int i = 0; i < timesMultikicked; i++) { - cost.addManaCost(mkCost); + cost.addManaCost(mkCost); } sa.setSVar("Multikicker", String.valueOf(timesMultikicked)); } @@ -1218,7 +1467,7 @@ public class ComputerUtilMana { for (SpellAbility ma : src.getManaAbilities()) { ma.setActivatingPlayer(p); if (!checkPlayable || ma.canPlay()) { - int costsToActivate = ma.getPayCosts() != null && ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0; + int costsToActivate = ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0; int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length; int producedAmount = AbilityUtils.calculateAmount(src, ma.getParamOrDefault("Amount", "1"), ma); @@ -1281,6 +1530,16 @@ public class ComputerUtilMana { // 3. Use lands that produce any color many // 4. all other sources (creature, costs, drawback, etc.) for (Card card : manaSources) { + // exclude creature sources that will tap as a part of an attack declaration + if (card.isCreature()) { + if (card.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)) { + Combat combat = card.getGame().getCombat(); + if (combat.getAttackers().indexOf(card) != -1 && !card.hasKeyword(Keyword.VIGILANCE)) { + continue; + } + } + } + if (card.isCreature() || card.isEnchanted()) { otherManaSources.add(card); continue; // don't use creatures before other permanents @@ -1363,20 +1622,6 @@ public class ComputerUtilMana { final ListMultimap manaMap = ArrayListMultimap.create(); final Game game = ai.getGame(); - List replacementEffects = new ArrayList<>(); - for (final Player p : game.getPlayers()) { - for (final Card crd : p.getAllCards()) { - for (final ReplacementEffect replacementEffect : crd.getReplacementEffects()) { - if (replacementEffect.requirementsCheck(game) - && replacementEffect.getMode() == ReplacementType.ProduceMana - && replacementEffect.hasParam("ManaReplacement") - && replacementEffect.zonesCheck(game.getZoneOf(crd))) { - replacementEffects.add(replacementEffect); - } - } - } - } - // Loop over all current available mana sources for (final Card sourceCard : getAvailableManaSources(ai, checkPlayable)) { if (DEBUG_MANA_PAYMENT) { @@ -1406,48 +1651,80 @@ public class ComputerUtilMana { } manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list - AbilityManaPart mp = m.getManaPart(); - // setup produce mana replacement effects - final Map repParams = AbilityKey.newMap(); - repParams.put(AbilityKey.Mana, mp.getOrigProduced()); - repParams.put(AbilityKey.Affected, sourceCard); - repParams.put(AbilityKey.Player, ai); - repParams.put(AbilityKey.AbilityMana, m); + SpellAbility tail = m; + while (tail != null) { + AbilityManaPart mp = m.getManaPart(); + if (mp != null && tail.metConditions()) { + // TODO Replacement Check currently doesn't work for reflected colors - for (final ReplacementEffect replacementEffect : replacementEffects) { - if (replacementEffect.canReplace(repParams)) { - Card crd = replacementEffect.getHostCard(); - String repType = crd.getSVar(replacementEffect.getParam("ManaReplacement")); - if (repType.contains("Chosen")) { - repType = TextUtil.fastReplace(repType, "Chosen", MagicColor.toShortString(crd.getChosenColor())); + // setup produce mana replacement effects + String origin = mp.getOrigProduced(); + final Map repParams = AbilityKey.newMap(); + repParams.put(AbilityKey.Mana, origin); + repParams.put(AbilityKey.Affected, sourceCard); + repParams.put(AbilityKey.Player, ai); + repParams.put(AbilityKey.AbilityMana, m); // RootAbility + + List reList = game.getReplacementHandler().getReplacementList(ReplacementType.ProduceMana, repParams, ReplacementLayer.Other); + + if (reList.isEmpty()) { + Set reflectedColors = CardUtil.getReflectableManaColors(m); + // find possible colors + for (byte color : MagicColor.WUBRG) { + if (tail.canThisProduce(MagicColor.toShortString(color)) || reflectedColors.contains(MagicColor.toLongString(color))) { + manaMap.put((int)color, m); + } + } + if (m.canThisProduce("C") || reflectedColors.contains(MagicColor.Constant.COLORLESS)) { + manaMap.put(ManaAtom.COLORLESS, m); + } + } else { + // try to guess the color the mana gets replaced to + for (ReplacementEffect re : reList) { + SpellAbility o = re.getOverridingAbility(); + String replaced = origin; + if (o == null || o.getApi() != ApiType.ReplaceMana) { + continue; + } + if (o.hasParam("ReplaceMana")) { + replaced = o.getParam("ReplaceMana"); + } else if (o.hasParam("ReplaceType")) { + String color = o.getParam("ReplaceType"); + for (byte c : MagicColor.WUBRGC) { + String s = MagicColor.toShortString(c); + replaced = replaced.replace(s, color); + } + } else if (o.hasParam("ReplaceColor")) { + String color = o.getParam("ReplaceColor"); + if (o.hasParam("ReplaceOnly")) { + replaced = replaced.replace(o.getParam("ReplaceOnly"), color); + } else { + for (byte c : MagicColor.WUBRG) { + String s = MagicColor.toShortString(c); + replaced = replaced.replace(s, color); + } + } + } + + for (byte color : MagicColor.WUBRG) { + if ("Any".equals(replaced) || replaced.contains(MagicColor.toShortString(color))) { + manaMap.put((int)color, m); + } + } + + if (replaced.contains("C")) { + manaMap.put(ManaAtom.COLORLESS, m); + } + + } } - mp.setManaReplaceType(repType); } + tail = tail.getSubAbility(); } - Set reflectedColors = CardUtil.getReflectableManaColors(m); - // find possible colors - if (mp.canProduce("W", m) || reflectedColors.contains(MagicColor.Constant.WHITE)) { - manaMap.get(ManaAtom.WHITE).add(m); - } - if (mp.canProduce("U", m) || reflectedColors.contains(MagicColor.Constant.BLUE)) { - manaMap.get(ManaAtom.BLUE).add(m); - } - if (mp.canProduce("B", m) || reflectedColors.contains(MagicColor.Constant.BLACK)) { - manaMap.get(ManaAtom.BLACK).add(m); - } - if (mp.canProduce("R", m) || reflectedColors.contains(MagicColor.Constant.RED)) { - manaMap.get(ManaAtom.RED).add(m); - } - if (mp.canProduce("G", m) || reflectedColors.contains(MagicColor.Constant.GREEN)) { - manaMap.get(ManaAtom.GREEN).add(m); - } - if (mp.canProduce("C", m) || reflectedColors.contains(MagicColor.Constant.COLORLESS)) { - manaMap.get(ManaAtom.COLORLESS).add(m); - } - if (mp.isSnow()) { - manaMap.get(ManaAtom.IS_SNOW).add(m); + if (m.getHostCard().isSnow()) { + manaMap.put(ManaAtom.IS_SNOW, m); } if (DEBUG_MANA_PAYMENT) { System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor manaMap = " + manaMap); @@ -1462,7 +1739,7 @@ public class ComputerUtilMana { *

* determineLeftoverMana. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @param player @@ -1483,7 +1760,7 @@ public class ComputerUtilMana { *

* determineLeftoverMana. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @param player @@ -1512,7 +1789,7 @@ public class ComputerUtilMana { *

* getAIPlayableMana. *

- * + * * @return a {@link java.util.List} object. */ public static List getAIPlayableMana(Card c) { @@ -1559,8 +1836,8 @@ public class ComputerUtilMana { sa.resetSacrificedAsEmerge(); } } - - + + /** * Matches list of creatures to shards in mana cost for convoking. * @param cost cost of convoked ability @@ -1592,31 +1869,4 @@ public class ComputerUtilMana { } return convoke; } - - public static int determineMaxAffordableX(Player ai, SpellAbility sa) { - if (sa.getPayCosts() == null || sa.getPayCosts().getCostMana() == null) { - return -1; - } - - int numTgts = 0; - int numX = sa.getPayCosts().getCostMana().getAmountOfX(); - - if (numX == 0) { - return -1; - } - - int testX = 1; - while (testX <= 100) { - if (ComputerUtilMana.canPayManaCost(sa, ai, testX)) { - numTgts++; - } else { - break; - } - testX++; - } - - numTgts /= numX; - - return numTgts; - } } diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java index 3b7a9c7890a..917012b236d 100644 --- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java @@ -5,7 +5,7 @@ import com.google.common.base.Function; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.cost.CostPayEnergy; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; @@ -242,11 +242,11 @@ public class CreatureEvaluator implements Function { && "+X".equals(sa.getParam("NumDef")) && !sa.usesTargeting() && (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) { - if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { // Electrostatic Pummeler, can be expanded for similar cards int initPower = getEffectivePower(sa.getHostCard()); int pumpedPower = initPower; - int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY); + int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY); if (energy > 0) { int numActivations = energy / 3; for (int i = 0; i < numActivations; i++) { diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index 2156d3f8910..768cb8743a6 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -9,7 +9,9 @@ import forge.card.CardStateName; import forge.card.MagicColor; import forge.game.Game; import forge.game.GameEntity; +import forge.game.GameObject; import forge.game.ability.AbilityFactory; +import forge.game.ability.AbilityKey; import forge.game.ability.effects.DetachedCardEffect; import forge.game.card.*; import forge.game.card.token.TokenInfo; @@ -23,7 +25,6 @@ import forge.game.player.Player; import forge.game.spellability.AbilityManaPart; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; -import forge.game.ability.AbilityKey; import forge.game.trigger.TriggerType; import forge.game.zone.PlayerZone; import forge.game.zone.ZoneType; @@ -75,9 +76,12 @@ public abstract class GameState { private final Map> cardToChosenClrs = new HashMap<>(); private final Map cardToChosenCards = new HashMap<>(); private final Map cardToChosenType = new HashMap<>(); + private final Map cardToChosenType2 = new HashMap<>(); private final Map> cardToRememberedId = new HashMap<>(); private final Map> cardToImprintedId = new HashMap<>(); + private final Map> cardToMergedCards = new HashMap<>(); private final Map cardToNamedCard = new HashMap<>(); + private final Map cardToNamedCard2 = new HashMap<>(); private final Map cardToExiledWithId = new HashMap<>(); private final Map cardAttackMap = new HashMap<>(); @@ -258,7 +262,13 @@ public abstract class GameState { if (c.getPaperCard() == null) { return; } - newText.append(c.getPaperCard().getName()); + + if (!c.getMergedCards().isEmpty()) { + // we have to go by the current top card name here + newText.append(c.getTopMergedCard().getPaperCard().getName()); + } else { + newText.append(c.getPaperCard().getName()); + } } if (c.isCommander()) { newText.append("|IsCommander"); @@ -300,6 +310,8 @@ public abstract class GameState { newText.append("|Flipped"); } else if (c.getCurrentStateName().equals(CardStateName.Meld)) { newText.append("|Meld"); + } else if (c.getCurrentStateName().equals(CardStateName.Modal)) { + newText.append("|Modal"); } if (c.isAttachedToEntity()) { newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId()); @@ -321,9 +333,15 @@ public abstract class GameState { if (!c.getChosenType().isEmpty()) { newText.append("|ChosenType:").append(c.getChosenType()); } + if (!c.getChosenType2().isEmpty()) { + newText.append("|ChosenType2:").append(c.getChosenType2()); + } if (!c.getNamedCard().isEmpty()) { newText.append("|NamedCard:").append(c.getNamedCard()); } + if (!c.getNamedCard2().isEmpty()) { + newText.append("|NamedCard2:").append(c.getNamedCard2()); + } List chosenCardIds = Lists.newArrayList(); for (Object obj : c.getChosenCards()) { @@ -355,6 +373,17 @@ public abstract class GameState { if (!imprintedCardIds.isEmpty()) { newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ",")); } + + if (!c.getMergedCards().isEmpty()) { + List mergedCardNames = new ArrayList<>(); + for (Card merged : c.getMergedCards()) { + if (c.getTopMergedCard() == merged) { + continue; + } + mergedCardNames.add(merged.getPaperCard().getName().replace(",", "^")); + } + newText.append("|MergedCards:").append(TextUtil.join(mergedCardNames, ",")); + } } if (zoneType == ZoneType.Exile) { @@ -591,10 +620,13 @@ public abstract class GameState { cardToEnchantPlayerId.clear(); cardToRememberedId.clear(); cardToExiledWithId.clear(); + cardToImprintedId.clear(); markedDamage.clear(); cardToChosenClrs.clear(); cardToChosenCards.clear(); cardToChosenType.clear(); + cardToChosenType2.clear(); + cardToMergedCards.clear(); cardToScript.clear(); cardAttackMap.clear(); @@ -627,6 +659,7 @@ public abstract class GameState { handleCardAttachments(); handleChosenEntities(); handleRememberedEntities(); + handleMergedCards(); handleScriptExecution(game); handlePrecastSpells(game); handleMarkedDamage(); @@ -781,6 +814,7 @@ public abstract class GameState { Card exiledWith = idToCard.get(Integer.parseInt(id)); c.setExiledWith(exiledWith); + c.setExiledBy(exiledWith.getController()); } } @@ -815,6 +849,12 @@ public abstract class GameState { break; } } + + if (sa.hasParam("RememberTargets")) { + for (final GameObject o : sa.getTargets()) { + sa.getHostCard().addRemembered(o); + } + } } private void handleScriptExecution(final Game game) { @@ -974,14 +1014,27 @@ public abstract class GameState { spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim(); } - PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef); + Card c = null; - if (pc == null) { - System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!"); - return; + if (StringUtils.isNumeric(spellDef)) { + // Precast from a specific host + c = idToCard.get(Integer.parseInt(spellDef)); + if (c == null) { + System.err.println("ERROR: Could not find a card with ID " + spellDef + " to precast!"); + return; + } + } else { + // Precast from a card by name + PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef); + + if (pc == null) { + System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!"); + return; + } + + c = Card.fromPaperCard(pc, activator); } - Card c = Card.fromPaperCard(pc, activator); SpellAbility sa = null; if (!scriptID.isEmpty()) { @@ -1029,12 +1082,24 @@ public abstract class GameState { c.setChosenType(entry.getValue()); } + // Chosen type 2 + for (Entry entry : cardToChosenType2.entrySet()) { + Card c = entry.getKey(); + c.setChosenType2(entry.getValue()); + } + // Named card for (Entry entry : cardToNamedCard.entrySet()) { Card c = entry.getKey(); c.setNamedCard(entry.getValue()); } + // Named card 2 + for (Entry entry : cardToNamedCard2.entrySet()) { + Card c = entry.getKey(); + c.setNamedCard2(entry.getValue()); + } + // Chosen cards for (Entry entry : cardToChosenCards.entrySet()) { Card c = entry.getKey(); @@ -1069,12 +1134,61 @@ public abstract class GameState { } } + private void handleMergedCards() { + for(Entry> entry : cardToMergedCards.entrySet()) { + Card mergedTo = entry.getKey(); + for(String mergedCardName : entry.getValue()) { + Card c; + PaperCard pc = StaticData.instance().getCommonCards().getCard(mergedCardName. replace("^", ",")); + if (pc == null) { + System.err.println("ERROR: Tried to create a non-existent card named " + mergedCardName + " (as a merged card) when loading game state!"); + continue; + } + + c = Card.fromPaperCard(pc, mergedTo.getOwner()); + emulateMergeViaMutate(mergedTo, c); + } + } + } + + private void emulateMergeViaMutate(Card top, Card bottom) { + if (top == null || bottom == null) { + System.err.println("ERROR: Tried to call emulateMergeViaMutate with a null card!"); + return; + } + + Game game = top.getGame(); + + bottom.setMergedToCard(top); + if (!top.hasMergedCard()) { + top.addMergedCard(top); + } + top.addMergedCard(bottom); + + if (top.getMutatedTimestamp() != -1) { + top.removeCloneState(top.getMutatedTimestamp()); + } + + final Long ts = game.getNextTimestamp(); + top.setMutatedTimestamp(ts); + if (top.getCurrentStateName() != CardStateName.FaceDown) { + final CardCloneStates mutatedStates = CardFactory.getMutatedCloneStates(top, null/*FIXME*/); + top.addCloneState(mutatedStates, ts); + } + bottom.setTapped(top.isTapped()); + bottom.setFlipped(top.isFlipped()); + top.setTimesMutated(top.getTimesMutated() + 1); + top.updateTokenView(); + + // TODO: Merged commanders aren't supported yet + } + private void applyCountersToGameEntity(GameEntity entity, String counterString) { - entity.setCounters(Maps.newEnumMap(CounterType.class)); + entity.setCounters(Maps.newHashMap()); String[] allCounterStrings = counterString.split(","); for (final String counterPair : allCounterStrings) { String[] pair = counterPair.split("=", 2); - entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false, null); + entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, false, null); } } @@ -1116,7 +1230,7 @@ public abstract class GameState { Map counters = c.getCounters(); // Note: Not clearCounters() since we want to keep the counters // var as-is. - c.setCounters(Maps.newEnumMap(CounterType.class)); + c.setCounters(Maps.newHashMap()); if (c.isAura()) { // dummy "enchanting" to indicate that the card will be force-attached elsewhere // (will be overridden later, so the actual value shouldn't matter) @@ -1140,6 +1254,9 @@ public abstract class GameState { zone.setCards(kv.getValue()); } } + for (Card cmd : p.getCommanders()) { + p.getZone(ZoneType.Command).add(Player.createCommanderEffect(p.getGame(), cmd)); + } } /** @@ -1210,7 +1327,10 @@ public abstract class GameState { c.setState(CardStateName.Flipped, true); } else if (info.startsWith("Meld")) { c.setState(CardStateName.Meld, true); - } else if (info.startsWith("OnAdventure")) { + } else if (info.startsWith("Modal")) { + c.setState(CardStateName.Modal, true); + } + else if (info.startsWith("OnAdventure")) { String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell"; AbilitySub saAdventure = (AbilitySub)AbilityFactory.getAbility(abAdventure, c); StringBuilder sbPlay = new StringBuilder(); @@ -1220,11 +1340,10 @@ public abstract class GameState { saAdventure.setActivatingPlayer(c.getOwner()); saAdventure.resolve(); c.setExiledWith(c); // This seems to be the way it's set up internally. Potentially not needed here? + c.setExiledBy(c.getController()); } else if (info.startsWith("IsCommander")) { - // TODO: This doesn't seem to properly restore the ability to play the commander. Why? c.setCommander(true); player.setCommanders(Lists.newArrayList(c)); - player.getZone(ZoneType.Command).add(Player.createCommanderEffect(player.getGame(), c)); } else if (info.startsWith("Id:")) { int id = Integer.parseInt(info.substring(3)); idToCard.put(id, c); @@ -1253,6 +1372,8 @@ public abstract class GameState { cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(","))); } else if (info.startsWith("ChosenType:")) { cardToChosenType.put(c, info.substring(info.indexOf(':') + 1)); + } else if (info.startsWith("ChosenType2:")) { + cardToChosenType2.put(c, info.substring(info.indexOf(':') + 1)); } else if (info.startsWith("ChosenCards:")) { CardCollection chosen = new CardCollection(); String[] idlist = info.substring(info.indexOf(':') + 1).split(","); @@ -1260,8 +1381,13 @@ public abstract class GameState { chosen.add(idToCard.get(Integer.parseInt(id))); } cardToChosenCards.put(c, chosen); + } else if (info.startsWith("MergedCards:")) { + List cardNames = Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")); + cardToMergedCards.put(c, cardNames); } else if (info.startsWith("NamedCard:")) { cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1)); + } else if (info.startsWith("NamedCard2:")) { + cardToNamedCard2.put(c, info.substring(info.indexOf(':') + 1)); } else if (info.startsWith("ExecuteScript:")) { cardToScript.put(c, info.substring(info.indexOf(':') + 1)); } else if (info.startsWith("RememberedCards:")) { diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 04a9f303832..ab50c1a806f 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -103,7 +103,7 @@ public class PlayerControllerAi extends PlayerController { } @Override - public Integer announceRequirements(SpellAbility ability, String announce, boolean allowZero) { + public Integer announceRequirements(SpellAbility ability, String announce) { // For now, these "announcements" are made within the AI classes of the appropriate SA effects if (ability.getApi() != null) { switch (ability.getApi()) { @@ -145,12 +145,12 @@ public class PlayerControllerAi extends PlayerController { } @Override - public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional) { - return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional); + public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional, Map params) { + return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional, params); } @Override - public T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer) { + public T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer, Map params) { if (delayedReveal != null) { reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); } @@ -158,13 +158,13 @@ public class PlayerControllerAi extends PlayerController { if (null == api) { throw new InvalidParameterException("SA is not api-based, this is not supported yet"); } - return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer); + return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer, params); } @Override public List chooseEntitiesForEffect( FCollectionView optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, - Player targetedPlayer) { + Player targetedPlayer, Map params) { if (delayedReveal != null) { reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); } @@ -172,7 +172,7 @@ public class PlayerControllerAi extends PlayerController { List selecteds = new ArrayList<>(); T selected; do { - selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer); + selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer, params); if ( selected != null ) { remaining.remove(selected); selecteds.add(selected); @@ -182,7 +182,23 @@ public class PlayerControllerAi extends PlayerController { } @Override - public SpellAbility chooseSingleSpellForEffect(java.util.List spells, SpellAbility sa, String title, + public List chooseSpellAbilitiesForEffect(List spells, SpellAbility sa, String title, + int num, Map params) { + List remaining = Lists.newArrayList(spells); + List selecteds = Lists.newArrayList(); + SpellAbility selected; + do { + selected = chooseSingleSpellForEffect(remaining, sa, title, params); + if ( selected != null ) { + remaining.remove(selected); + selecteds.add(selected); + } + } while ( (selected != null ) && (selecteds.size() < num) ); + return selecteds; + } + + @Override + public SpellAbility chooseSingleSpellForEffect(List spells, SpellAbility sa, String title, Map params) { ApiType api = sa.getApi(); if (null == api) { @@ -208,15 +224,13 @@ public class PlayerControllerAi extends PlayerController { } @Override - public boolean confirmTrigger(WrappedAbility wrapper, Map triggerParams, boolean isMandatory) { + public boolean confirmTrigger(WrappedAbility wrapper) { final SpellAbility sa = wrapper.getWrappedAbility(); //final Trigger regtrig = wrapper.getTrigger(); if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) { return true; } - if (triggerParams.containsKey("DelayedTrigger") || isMandatory) { - //TODO: The only card with an optional delayed trigger is Shirei, Shizo's Caretaker, - // needs to be expanded when a more difficult cards comes up + if (wrapper.isMandatory()) { return true; } // Store/replace target choices more properly to get this SA cleared. @@ -253,7 +267,7 @@ public class PlayerControllerAi extends PlayerController { } @Override - public Player chooseStartingPlayer(boolean isFirstGame) { + public Player chooseStartingPlayer(boolean isFirstgame) { return this.player; // AI is brave :) } @@ -350,7 +364,7 @@ public class PlayerControllerAi extends PlayerController { if (destinationZone == ZoneType.Graveyard) { // In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard - if (!CardLists.filter(game.getCardsInGame(), new Predicate() { + if (!CardLists.filter(getGame().getCardsInGame(), new Predicate() { @Override public boolean apply(Card card) { // need a custom predicate here since Volrath's Shapeshifter may have a different name OTB @@ -471,7 +485,7 @@ public class PlayerControllerAi extends PlayerController { public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) { if (canSetupTargets) brains.doTrigger(effectSA, true); // first parameter does not matter, since return value won't be used - ComputerUtil.playNoStack(player, effectSA, game); + ComputerUtil.playNoStack(player, effectSA, getGame()); } @Override @@ -480,7 +494,7 @@ public class PlayerControllerAi extends PlayerController { } @Override - public TargetChoices chooseNewTargetsFor(SpellAbility ability) { + public TargetChoices chooseNewTargetsFor(SpellAbility ability, Predicate filter, boolean optional) { // AI currently can't do this. But when it can it will need to be based on Ability API return null; } @@ -503,18 +517,18 @@ public class PlayerControllerAi extends PlayerController { @Override public String chooseSomeType(String kindOfType, SpellAbility sa, Collection validTypes, List invalidTypes, boolean isOptional) { - String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), invalidTypes); + String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), validTypes, invalidTypes); if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) { chosen = validTypes.iterator().next(); System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: chosen"); } - game.getAction().nofityOfValue(sa, player, chosen, player); + getGame().getAction().nofityOfValue(sa, player, chosen, player); return chosen; } @Override - public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes) { - return ComputerUtil.vote(player, options, sa, votes); + public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer) { + return ComputerUtil.vote(player, options, sa, votes, forPlayer); } @Override @@ -598,15 +612,17 @@ public class PlayerControllerAi extends PlayerController { } @Override - public void playChosenSpellAbility(SpellAbility sa) { + public boolean playChosenSpellAbility(SpellAbility sa) { // System.out.println("Playing sa: " + sa); if (sa instanceof LandAbility) { if (sa.canPlay()) { sa.resolve(); + getGame().updateLastStateForCard(sa.getHostCard()); } } else { - ComputerUtil.handlePlayingSpellAbility(player, sa, game); + ComputerUtil.handlePlayingSpellAbility(player, sa, getGame()); } + return true; } @Override @@ -644,7 +660,7 @@ public class PlayerControllerAi extends PlayerController { // - End of hack for Exile a card from library Cumulative Upkeep - if (ComputerUtilCost.canPayCost(ability, c.getController())) { - ComputerUtil.playNoStack(c.getController(), ability, game); + ComputerUtil.playNoStack(c.getController(), ability, getGame()); return true; } return false; @@ -754,6 +770,7 @@ public class PlayerControllerAi extends PlayerController { return defaultVal != null && defaultVal.booleanValue(); case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault? case LeftOrRight: return brains.chooseDirection(sa); + case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even default: return MyRandom.getRandom().nextBoolean(); } @@ -784,8 +801,8 @@ public class PlayerControllerAi extends PlayerController { } @Override - public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { - List result = brains.chooseModeForAbility(sa, min, num, allowRepeat); + public List chooseModeForAbility(SpellAbility sa, List possible, int min, int num, boolean allowRepeat) { + List result = brains.chooseModeForAbility(sa, possible, min, num, allowRepeat); if (result != null) { return result; } @@ -816,6 +833,9 @@ public class PlayerControllerAi extends PlayerController { @Override public byte chooseColor(String message, SpellAbility sa, ColorSet colors) { + if (colors.countColors() < 2) { + return Iterables.getFirst(colors, MagicColor.WHITE); + } // You may switch on sa.getApi() here and use sa.getParam("AILogic") CardCollectionView hand = player.getCardsIn(ZoneType.Hand); if (sa.getApi() == ApiType.Mana) { @@ -873,8 +893,8 @@ public class PlayerControllerAi extends PlayerController { public String chooseProtectionType(String string, SpellAbility sa, List choices) { String choice = choices.get(0); SpellAbility hostsa = null; //for Protect sub-ability - if (game.stack.size() > 1) { - for (SpellAbilityStackInstance si : game.getStack()) { + if (getGame().stack.size() > 1) { + for (SpellAbilityStackInstance si : getGame().getStack()) { SpellAbility spell = si.getSpellAbility(true); if (sa != spell && sa.getHostCard() != spell.getHostCard()) { String s = ProtectAi.toProtectFrom(spell.getHostCard(), sa); @@ -885,10 +905,10 @@ public class PlayerControllerAi extends PlayerController { } } } - final Combat combat = game.getCombat(); + final Combat combat = getGame().getCombat(); if (combat != null) { - if (game.stack.size() == 1) { - SpellAbility topstack = game.stack.peekAbility(); + if (getGame().stack.size() == 1) { + SpellAbility topstack = getGame().stack.peekAbility(); if (topstack.getSubAbility() == sa) { hostsa = topstack; } @@ -911,7 +931,7 @@ public class PlayerControllerAi extends PlayerController { } } } - final PhaseHandler ph = game.getPhaseHandler(); + final PhaseHandler ph = getGame().getPhaseHandler(); if (ph.getPlayerTurn() == sa.getActivatingPlayer() && ph.getPhase() == PhaseType.MAIN1 && sa.getTargetCard() != null) { AiAttackController aiAtk = new AiAttackController(sa.getActivatingPlayer(), sa.getTargetCard()); String s = aiAtk.toProtectAttacker(sa); @@ -926,7 +946,7 @@ public class PlayerControllerAi extends PlayerController { list.addAll(opp.getCreaturesInPlay()); } if (list.isEmpty()) { - list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents()); + list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents()); } if (!list.isEmpty()) { choice = ComputerUtilCard.getMostProminentColor(list); @@ -941,11 +961,9 @@ public class PlayerControllerAi extends PlayerController { final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } }; emptyAbility.setActivatingPlayer(player); emptyAbility.setTriggeringObjects(sa.getTriggeringObjects()); - for (String sVar : sa.getSVars()) { - emptyAbility.setSVar(sVar, sa.getSVar(sVar)); - } + emptyAbility.setSVars(sa.getSVars()); if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) { - ComputerUtil.playNoStack(player, emptyAbility, game); // AI needs something to resolve to pay that cost + ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost return true; } return false; @@ -954,8 +972,30 @@ public class PlayerControllerAi extends PlayerController { @Override public void orderAndPlaySimultaneousSa(List activePlayerSAs) { for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) { - if (prepareSingleSa(sa.getHostCard(),sa,true)) { - ComputerUtil.playStack(sa, player, game); + if (sa.isTrigger()) { + if (prepareSingleSa(sa.getHostCard(), sa, true)) { + ComputerUtil.playStack(sa, player, getGame()); + } + } else { + if (sa.isCopied()) { + if (sa.isSpell()) { + player.getGame().getStackZone().add(sa.getHostCard()); + } + + /* FIXME: the new implementation (below) requires implementing setupNewTargets in the AI controller, among other possible changes, otherwise breaks AI + if (sa.isMayChooseNewTargets()) { + sa.setupNewTargets(player); + } + */ + if (sa.isMayChooseNewTargets() && !sa.setupTargets()) { + if (sa.isSpell()) { + sa.getHostCard().ceaseToExist(); + } + continue; + } + } + // need finally add the new spell to the stack + player.getGame().getStack().add(sa); } } } @@ -973,7 +1013,7 @@ public class PlayerControllerAi extends PlayerController { @Override public void playTrigger(Card host, WrappedAbility wrapperAbility, boolean isMandatory) { if (prepareSingleSa(host, wrapperAbility, isMandatory)) { - ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, game); + ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, getGame()); } } @@ -985,9 +1025,9 @@ public class PlayerControllerAi extends PlayerController { Spell spell = (Spell) tgtSA; if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) { if (noManaCost) { - return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, game); + return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame()); } else { - return ComputerUtil.playStack(tgtSA, player, game); + return ComputerUtil.playStack(tgtSA, player, getGame()); } } else return false; // didn't play spell @@ -1122,7 +1162,8 @@ public class PlayerControllerAi extends PlayerController { CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard()); return ComputerUtilCard.getMostProminentCardName(cards); } else if (logic.equals("BestCreatureInComputerDeck")) { - return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName(); + Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiLibrary); + return bestCreature != null ? bestCreature.getName() : "Plains"; } else if (logic.equals("RandomInComputerDeck")) { return Aggregates.random(aiLibrary).getName(); } else if (logic.equals("MostProminentSpellInComputerDeck")) { @@ -1132,7 +1173,7 @@ public class PlayerControllerAi extends PlayerController { return SpecialCardAi.CursedScroll.chooseCard(player, sa); } } else { - CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents()); + CardCollectionView list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents()); list = CardLists.filter(list, Predicates.not(Presets.LANDS)); if (!list.isEmpty()) { return list.get(0).getName(); @@ -1213,7 +1254,7 @@ public class PlayerControllerAi extends PlayerController { public List chooseOptionalCosts(SpellAbility chosen, List optionalCostValues) { List chosenOptCosts = Lists.newArrayList(); - Cost costSoFar = chosen.getPayCosts() != null ? chosen.getPayCosts().copy() : Cost.Zero; + Cost costSoFar = chosen.getPayCosts().copy(); for (OptionalCostValue opt : optionalCostValues) { // Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps) @@ -1252,7 +1293,7 @@ public class PlayerControllerAi extends PlayerController { // TODO: improve the logic depending on the keyword and the playability of the cost-modified SA (enough targets present etc.) int chosenAmount = 0; - Cost costSoFar = sa.getPayCosts() != null ? sa.getPayCosts().copy() : Cost.Zero; + Cost costSoFar = sa.getPayCosts().copy(); for (int i = 0; i < max; i++) { costSoFar.add(cost); @@ -1268,13 +1309,16 @@ public class PlayerControllerAi extends PlayerController { } @Override - public CardCollection chooseCardsForEffectMultiple(Map validMap, SpellAbility sa, String title) { + public CardCollection chooseCardsForEffectMultiple(Map validMap, SpellAbility sa, String title, boolean isOptional) { CardCollection choices = new CardCollection(); for (String mapKey: validMap.keySet()) { CardCollection cc = validMap.get(mapKey); cc.removeAll(choices); - choices.add(ComputerUtilCard.getBestAI(cc)); // TODO: should the AI limit itself here with the max number of cards in hand? + Card chosen = ComputerUtilCard.getBestAI(cc); + if (chosen != null) { + choices.add(chosen); + } } return choices; diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java new file mode 100644 index 00000000000..c757133f256 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -0,0 +1,366 @@ +package forge.ai; + +import java.util.List; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; + +import forge.ai.ability.TokenAi; +import forge.game.Game; +import forge.game.ability.AbilityUtils; +import forge.game.ability.ApiType; +import forge.game.card.*; +import forge.game.combat.Combat; +import forge.game.keyword.Keyword; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.util.Aggregates; + +/* + * This class contains logic which is shared by several cards with different ability types (e.g. AF ChangeZone / AF Destroy) + * Ideally, the naming scheme for methods in this class should be doXXXLogic, where XXX is the name of the logic, + * and the signature of the method should be "public static boolean doXXXLogic(final Player ai, final SpellAbility sa), + * possibly followed with any additional necessary parameters. These AI logic routines generally do all the work, so returning + * true from them should indicate that the AI has made a decision and configured the spell ability (targeting, etc.) as it + * deemed necessary. + */ + +public class SpecialAiLogic { + // A logic for cards like Pongify, Crib Swap, Angelic Ascension + public static boolean doPongifyLogic(final Player ai, final SpellAbility sa) { + Card source = sa.getHostCard(); + Game game = source.getGame(); + PhaseHandler ph = game.getPhaseHandler(); + boolean isDestroy = ApiType.Destroy.equals(sa.getApi()); + SpellAbility tokenSA = sa.findSubAbilityByType(ApiType.Token); + if (tokenSA == null) { + // Used wrong AI logic? + return false; + } + + List targetable = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa); + + CardCollection listOpp = CardLists.filterControlledBy(targetable, ai.getOpponents()); + if (isDestroy) { + listOpp = CardLists.getNotKeyword(listOpp, Keyword.INDESTRUCTIBLE); + // TODO add handling for cards like targeting dies + } + + Card choice = null; + if (!listOpp.isEmpty()) { + choice = ComputerUtilCard.getMostExpensivePermanentAI(listOpp); + // can choice even be null? + + if (choice != null) { + final Card token = TokenAi.spawnToken(choice.getController(), tokenSA); + if (!token.isCreature() || token.getNetToughness() < 1) { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } + if (choice.isPlaneswalker()) { + if (choice.getCurrentLoyalty() * 35 > ComputerUtilCard.evaluateCreature(token)) { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } else { + return false; + } + } + if ((!choice.isCreature() || choice.isTapped()) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.isPlayerTurn(ai) // prevent surprise combatant + || ComputerUtilCard.evaluateCreature(choice) < 1.5 * ComputerUtilCard.evaluateCreature(token)) { + choice = null; + } + } + } + + // See if we have anything we can upgrade + if (choice == null) { + CardCollection listOwn = CardLists.filterControlledBy(targetable, ai); + final Card token = TokenAi.spawnToken(ai, tokenSA); + + Card bestOwnCardToUpgrade = null; + if (isDestroy) { + // just choose any Indestructible + // TODO maybe filter something that doesn't like to be targeted, or does something benefit by targeting + bestOwnCardToUpgrade = Iterables.getFirst(CardLists.getKeyword(listOwn, Keyword.INDESTRUCTIBLE), null); + } + if (bestOwnCardToUpgrade == null) { + bestOwnCardToUpgrade = ComputerUtilCard.getWorstCreatureAI(CardLists.filter(listOwn, new Predicate() { + @Override + public boolean apply(Card card) { + return card.isCreature() && (ComputerUtilCard.isUselessCreature(ai, card) + || ComputerUtilCard.evaluateCreature(token) > 2 * ComputerUtilCard.evaluateCreature(card)); + } + })); + } + if (bestOwnCardToUpgrade != null) { + if (ComputerUtilCard.isUselessCreature(ai, bestOwnCardToUpgrade) || (ph.getPhase().isAfter(PhaseType.COMBAT_END) || !ph.isPlayerTurn(ai))) { + choice = bestOwnCardToUpgrade; + } + } + } + + if (choice != null) { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } + + return false; + } + + // A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT" + public static boolean doAristocratLogic(final Player ai, final SpellAbility sa) { + final Game game = ai.getGame(); + final Combat combat = game.getCombat(); + final Card source = sa.getHostCard(); + final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); + final int powerBonus = sa.hasParam("NumAtt") ? AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa) : 0; + final int toughnessBonus = sa.hasParam("NumDef") ? AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa) : 0; + final boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible"); + final int selfEval = ComputerUtilCard.evaluateCreature(source); + final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); + + if (numOtherCreats == 0) { + return false; + } + + // Try to save the card from death by pumping it if it's threatened with a damage spell + if (isThreatened && (toughnessBonus > 0 || indestructible)) { + SpellAbility saTop = game.getStack().peekAbility(); + + if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) { + int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage(); + final int numCreatsToSac = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus)); + + if (numCreatsToSac > 1) { // probably not worth sacrificing too much + return false; + } + + if (indestructible || (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg)) { + final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(), + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || card.hasSVar("SacMe") + || ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK? + } + } + ); + return sacFodder.size() >= numCreatsToSac; + } + } + + return false; + } + + if (combat == null) { + return false; + } + + if (combat.isAttacking(source)) { + if (combat.getBlockers(source).isEmpty()) { + // Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if + // the opponent is tapped out or if we're willing to risk it (will currently risk it + // in case it sacs less than half its creatures to deal lethal damage) + + // TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully + // for potential damage prevention, various effects like reducing damage to 0, etc.) + + final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); + final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; + + final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp + int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); + + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent + } + + final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / (powerBonus != 0 ? powerBonus : 1); + + if (defTappedOut || numCreatsToSac < numOtherCreats / 2) { + return source.getNetCombatDamage() < lethalDmg + && source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg; + } else { + return false; + } + } else { + // We have already attacked. Thus, see if we have a creature to sac that is worse to lose + // than the card we attacked with. + final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(), + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || ComputerUtilCard.evaluateCreature(card) < selfEval; + } + } + ); + + if (sacTgts.isEmpty()) { + return false; + } + + final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); + final int DefP = indestructible ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower); + + // Make sure we don't over-sacrifice, only sac until we can survive and kill a creature + return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; + } + } else { + // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances + final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(), + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || card.hasSVar("SacMe") + || ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK? + } + } + ); + + return !sacFodder.isEmpty(); + } + } + + // A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat) + public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) { + final Card source = sa.getHostCard(); + final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied + final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS); + + final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); + if (numOtherCreats == 0) { + // Cut short if there's nothing to sac at all + return false; + } + + // Check if the standard Aristocrats logic applies first (if in the right conditions for it) + final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); + if (isDeclareBlockers || isThreatened) { + if (doAristocratLogic(ai, sa)) { + return true; + } + } + + // Check if anything is to be gained from the PutCounter subability + SpellAbility countersSa = null; + if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) { + if (sa.getApi() == ApiType.PutCounter) { + // called directly from CountersPutAi + countersSa = sa; + } + } else { + countersSa = sa.getSubAbility(); + } + + if (countersSa == null) { + // Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?) + System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!"); + return false; + } + + final Game game = ai.getGame(); + final Combat combat = game.getCombat(); + final int selfEval = ComputerUtilCard.evaluateCreature(source); + + String typeToGainCtr = ""; + if (logic.contains(".")) { + typeToGainCtr = logic.substring(logic.indexOf(".") + 1); + } + CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay() + : CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr)); + relevantCreats.remove(source); + if (relevantCreats.isEmpty()) { + // No relevant creatures to sac + return false; + } + + int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa); + + if (combat != null && combat.isAttacking(source) && isDeclareBlockers) { + if (combat.getBlockers(source).isEmpty()) { + // Unblocked. Check if we can deal lethal after receiving counters. + final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); + final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; + + final boolean isInfect = source.hasKeyword(Keyword.INFECT); + int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); + + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent + } + + // Check if there's anything that will die anyway that can be eaten to gain a perma-bonus + final CardCollection forcedSacTgts = CardLists.filter(relevantCreats, + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card) + || (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat)); + } + } + ); + if (!forcedSacTgts.isEmpty()) { + return true; + } + + final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs); + + if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) { + return source.getNetCombatDamage() < lethalDmg + && source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg; + } else { + return false; + } + } else { + // We have already attacked. Thus, see if we have a creature to sac that is worse to lose + // than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing + // things that are also threatened to be destroyed anyway. + final CardCollection sacTgts = CardLists.filter(relevantCreats, + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || ComputerUtilCard.evaluateCreature(card) < selfEval + || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); + } + } + ); + + if (sacTgts.isEmpty()) { + return false; + } + + final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source); + final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); + final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower); + + // Make sure we don't over-sacrifice, only sac until we can survive and kill a creature + return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; + } + } else { + // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances + final boolean isBlocking = combat != null && combat.isBlocking(source); + final CardCollection sacFodder = CardLists.filter(relevantCreats, + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || card.hasSVar("SacMe") + || (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval) + || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); + } + } + ); + + return !sacFodder.isEmpty(); + } + } +} diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index 27700e2455b..85de244dbfd 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -21,12 +21,12 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.ability.AnimateAi; import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaCost; import forge.game.Game; import forge.game.GameType; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; @@ -40,6 +40,7 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; +import forge.game.spellability.SpellAbilityPredicates; import forge.game.spellability.SpellPermanent; import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; @@ -143,6 +144,29 @@ public class SpecialCardAi { } } + // Crawling Barrens + public static class CrawlingBarrens { + public static boolean consider(final Player ai, final SpellAbility sa) { + final PhaseHandler ph = ai.getGame().getPhaseHandler(); + final Combat combat = ai.getGame().getCombat(); + + Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa); + animated.addType("Creature"); + if (sa.getHostCard().canReceiveCounters(CounterEnumType.P1P1)) { + animated.addCounter(CounterEnumType.P1P1, 2, ai, false, null); + } + boolean isOppEOT = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai; + boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated); + boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated); + + return isOppEOT || isValuableAttacker || isValuableBlocker; + } + + public static SpellAbility considerAnimating(final Player ai, final SpellAbility sa, final List options) { + return ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) ? options.get(0) : options.get(1); + } + } + // Cursed Scroll public static class CursedScroll { public static boolean consider(final Player ai, final SpellAbility sa) { @@ -194,7 +218,7 @@ public class SpecialCardAi { sa.getTargets().add(worstCreat); } - return sa.getTargets().getNumTargeted() > 0; + return sa.getTargets().size() > 0; } } @@ -327,7 +351,7 @@ public class SpecialCardAi { boolean canTrample = source.hasKeyword(Keyword.TRAMPLE); if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) { - int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterType.LOYALTY); + int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY); int totalDamageToPW = 0; for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) { if (combat.isUnblocked(atk)) { @@ -407,7 +431,7 @@ public class SpecialCardAi { } public static Pair getPumpedPT(Player ai, int power, int toughness) { - int energy = ai.getCounters(CounterType.ENERGY); + int energy = ai.getCounters(CounterEnumType.ENERGY); if (energy > 0) { int numActivations = energy / 3; for (int i = 0; i < numActivations; i++) { @@ -635,10 +659,7 @@ public class SpecialCardAi { boolean canRetFromGrave = false; String name = c.getName().replace(',', ';'); for (Trigger t : c.getTriggers()) { - SpellAbility ab = null; - if (t.hasParam("Execute")) { - ab = AbilityFactory.getAbility(c.getSVar(t.getParam("Execute")), c); - } + SpellAbility ab = t.ensureAbility(); if (ab == null) { continue; } if (ab.getApi() == ApiType.ChangeZone @@ -708,7 +729,7 @@ public class SpecialCardAi { // if there's another reanimator card currently suspended, don't cast a new one until the previous // one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End) for (Card ex : ai.getCardsIn(ZoneType.Exile)) { - if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterType.TIME) > 0) { + if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) { return false; } } @@ -756,6 +777,37 @@ public class SpecialCardAi { } } + // Maze's End + public static class MazesEnd { + public static boolean consider(final Player ai, final SpellAbility sa) { + PhaseHandler ph = ai.getGame().getPhaseHandler(); + CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate")); + + return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty(); + } + + public static Card considerCardToGet(final Player ai, final SpellAbility sa) + { + CardCollection currentGates = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Gate")); + CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate")); + + if (availableGates.isEmpty()) + return null; // shouldn't get here + + for (Card gate : availableGates) + { + if (CardLists.filter(currentGates, CardPredicates.nameEquals(gate.getName())).isEmpty()) + { + // Diversify our mana base + return gate; + } + } + + // Fetch a random gate if we already have all types + return Aggregates.random(availableGates); + } + } + // Mairsil, the Pretender public static class MairsilThePretender { // Scan the fetch list for a card with at least one activated ability. @@ -767,7 +819,7 @@ public class SpecialCardAi { Player controller = c.getController(); boolean wasCaged = false; for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile), - CardPredicates.hasCounter(CounterType.CAGE))) { + CardPredicates.hasCounter(CounterEnumType.CAGE))) { if (c.getName().equals(caged.getName())) { wasCaged = true; break; @@ -833,7 +885,7 @@ public class SpecialCardAi { } // Set PayX here to maximum value. - int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai); + int tokenSize = ComputerUtilCost.getMaxXValue(sa, ai); // Some basic strategy for Momir if (tokenSize < 2) { @@ -844,7 +896,7 @@ public class SpecialCardAi { tokenSize = 11; } - source.setSVar("PayX", Integer.toString(tokenSize)); + sa.setXManaCostPaid(tokenSize); return true; } @@ -1073,7 +1125,7 @@ public class SpecialCardAi { // Sarkhan the Mad public static class SarkhanTheMad { public static boolean considerDig(final Player ai, final SpellAbility sa) { - return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1; + return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1; } public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) { @@ -1109,7 +1161,7 @@ public class SpecialCardAi { // Sorin, Vengeful Bloodlord public static class SorinVengefulBloodlord { public static boolean consider(final Player ai, final SpellAbility sa) { - int loyalty = sa.getHostCard().getCounters(CounterType.LOYALTY); + int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY); CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.lessCMC(loyalty - 1), new Predicate() { @Override @@ -1271,7 +1323,7 @@ public class SpecialCardAi { sa.getTargets().add(worstOwnCreat); } - return sa.getTargets().getNumTargeted() > 0; + return sa.getTargets().size() > 0; } } @@ -1295,6 +1347,26 @@ public class SpecialCardAi { } } + // Timmerian Fiends + public static class TimmerianFiends { + public static boolean consider(final Player ai, final SpellAbility sa) { + final Card targeted = sa.getParentTargetingCard().getTargetCard(); + if (targeted == null) { + return false; + } + + if (targeted.isCreature()) { + if (ComputerUtil.aiLifeInDanger(ai, true, 0)) { + return true; // do it, hoping to save a valuable potential blocker etc. + } + return ComputerUtilCard.evaluateCreature(targeted) >= 200; // might need tweaking + } else { + // TODO: this currently compares purely by CMC. To be somehow improved, especially for stuff like the Power Nine etc. + return ComputerUtilCard.evaluatePermanentList(new CardCollection(targeted)) >= 3; + } + } + } + // Volrath's Shapeshifter public static class VolrathsShapeshifter { public static boolean consider(final Player ai, final SpellAbility sa) { @@ -1345,11 +1417,11 @@ public class SpecialCardAi { Card source = sa.getHostCard(); Game game = source.getGame(); - final int loyalty = source.getCounters(CounterType.LOYALTY); + final int loyalty = source.getCounters(CounterEnumType.LOYALTY); int x = -1, best = 0; Card single = null; for (int i = 0; i < loyalty; i++) { - sa.setSVar("ChosenX", "Number$" + i); + sa.setXManaCostPaid(i); oppType = CardLists.filterControlledBy(game.getCardsIn(origin), ai.getOpponents()); oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa); computerType = AbilityUtils.filterListByType(ai.getCardsIn(origin), sa.getParam("ChangeType"), sa); @@ -1366,13 +1438,8 @@ public class SpecialCardAi { } // check if +1 would be sufficient if (single != null) { - SpellAbility ugin_burn = null; - for (final SpellAbility s : source.getSpellAbilities()) { - if (s.getApi() == ApiType.DealDamage) { - ugin_burn = s; - break; - } - } + // TODO use better logic to find the right Deal Damage Effect? + SpellAbility ugin_burn = Iterables.find(source.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.DealDamage), null); if (ugin_burn != null) { // basic logic copied from DamageDealAi::dealDamageChooseTgtC if (ugin_burn.canTarget(single)) { @@ -1383,17 +1450,17 @@ public class SpecialCardAi { if (can_kill) { return false; } - } - // simple check to burn player instead of exiling planeswalker - if (single.isPlaneswalker() && single.getCurrentLoyalty() <= 3) { - return false; + // simple check to burn player instead of exiling planeswalker + if (single.isPlaneswalker() && single.getCurrentLoyalty() <= 3) { + return false; + } } } } if (x == -1) { return false; } - sa.setSVar("ChosenX", "Number$" + x); + sa.setXManaCostPaid(x); return true; } } diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index d86989d87a8..d8c185684fd 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -2,6 +2,7 @@ package forge.ai; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.card.CardStateName; import forge.card.ICardFace; import forge.card.mana.ManaCost; import forge.card.mana.ManaCostParser; @@ -77,16 +78,14 @@ public abstract class SpellAbilityAi { } } - if (sa.hasParam("AITgtBeforeCostEval")) { - // Cost payment requires a valid target to be specified, e.g. Quillmane Baku, so run the API logic first - // to set the target, then decide on paying costs (slower, so only use for cards where it matters) - return checkApiLogic(ai, sa) && (cost == null || willPayCosts(ai, sa, cost, source)); + if (!checkApiLogic(ai, sa)) { + return false; } - + // needs to be after API logic because needs to check possible X Cost? if (cost != null && !willPayCosts(ai, sa, cost, source)) { return false; } - return checkApiLogic(ai, sa); + return true; } protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) { @@ -112,7 +111,7 @@ public abstract class SpellAbilityAi { if (aiLogic.equals("CheckCondition")) { SpellAbility saCopy = sa.copy(); saCopy.setActivatingPlayer(ai); - return saCopy.getConditions().areMet(saCopy); + return saCopy.metConditions(); } return !("Never".equals(aiLogic)); @@ -167,7 +166,8 @@ public abstract class SpellAbilityAi { // a mandatory SpellAbility with targeting but without candidates, // does not need to go any deeper - if (sa.usesTargeting() && mandatory && !sa.getTargetRestrictions().hasCandidates(sa, true)) { + if (sa.usesTargeting() && mandatory && !sa.isTargetNumberValid() + && !sa.getTargetRestrictions().hasCandidates(sa, true)) { return false; } @@ -247,6 +247,7 @@ public abstract class SpellAbilityAi { protected static boolean isSorcerySpeed(final SpellAbility sa) { return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery()) || (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed()) + || (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery()) || (sa.isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed.")); } @@ -264,7 +265,7 @@ public abstract class SpellAbilityAi { // TODO probably also consider if winter orb or similar are out - if (sa.getPayCosts() == null || sa instanceof AbilitySub) { + if (sa instanceof AbilitySub) { return true; // This is only true for Drawbacks and triggers } @@ -304,7 +305,7 @@ public abstract class SpellAbilityAi { } @SuppressWarnings("unchecked") - public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer) { + public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer, Map params) { boolean hasPlayer = false; boolean hasCard = false; boolean hasPlaneswalker = false; @@ -321,11 +322,11 @@ public abstract class SpellAbilityAi { } if (hasPlayer && hasPlaneswalker) { - return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection) options); + return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection) options, params); } else if (hasCard) { - return (T) chooseSingleCard(ai, sa, (Collection) options, isOptional, targetedPlayer); + return (T) chooseSingleCard(ai, sa, (Collection) options, isOptional, targetedPlayer, params); } else if (hasPlayer) { - return (T) chooseSinglePlayer(ai, sa, (Collection) options); + return (T) chooseSinglePlayer(ai, sa, (Collection) options, params); } return null; @@ -336,17 +337,17 @@ public abstract class SpellAbilityAi { return spells.get(0); } - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleCard is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method"); return Iterables.getFirst(options, null); } - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayer is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method"); return Iterables.getFirst(options, null); } - protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options) { + protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options, Map params) { System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayerOrPlaneswalker is used for " + this.getClass().getName() + ". Consider declaring an overloaded method"); return Iterables.getFirst(options, null); } diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index f3b3421c40b..5c8276e8470 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -33,7 +33,7 @@ public enum SpellApiToAi { .put(ApiType.BidLife, BidLifeAi.class) .put(ApiType.Bond, BondAi.class) .put(ApiType.Branch, AlwaysPlayAi.class) - .put(ApiType.ChangeCombatants, CannotPlayAi.class) + .put(ApiType.ChangeCombatants, ChangeCombatantsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeX, AlwaysPlayAi.class) .put(ApiType.ChangeZone, ChangeZoneAi.class) @@ -42,6 +42,7 @@ public enum SpellApiToAi { .put(ApiType.ChooseCard, ChooseCardAi.class) .put(ApiType.ChooseColor, ChooseColorAi.class) .put(ApiType.ChooseDirection, ChooseDirectionAi.class) + .put(ApiType.ChooseEvenOdd, ChooseEvenOddAi.class) .put(ApiType.ChooseNumber, ChooseNumberAi.class) .put(ApiType.ChoosePlayer, ChoosePlayerAi.class) .put(ApiType.ChooseSource, ChooseSourceAi.class) @@ -83,14 +84,15 @@ public enum SpellApiToAi { .put(ApiType.FlipACoin, FlipACoinAi.class) .put(ApiType.Fog, FogAi.class) .put(ApiType.GainControl, ControlGainAi.class) - .put(ApiType.GainControlVariant, AlwaysPlayAi.class) + .put(ApiType.GainControlVariant, ControlGainVariantAi.class) .put(ApiType.GainLife, LifeGainAi.class) .put(ApiType.GainOwnership, CannotPlayAi.class) .put(ApiType.GameDrawn, CannotPlayAi.class) .put(ApiType.GenericChoice, ChooseGenericEffectAi.class) .put(ApiType.Goad, GoadAi.class) .put(ApiType.Haunt, HauntAi.class) - .put(ApiType.ImmediateTrigger, AlwaysPlayAi.class) + .put(ApiType.ImmediateTrigger, ImmediateTriggerAi.class) + .put(ApiType.Investigate, InvestigateAi.class) .put(ApiType.LoseLife, LifeLoseAi.class) .put(ApiType.LosesGame, GameLossAi.class) .put(ApiType.Mana, ManaEffectAi.class) @@ -103,6 +105,7 @@ public enum SpellApiToAi { .put(ApiType.MultiplyCounter, CountersMultiplyAi.class) .put(ApiType.MustAttack, MustAttackAi.class) .put(ApiType.MustBlock, MustBlockAi.class) + .put(ApiType.Mutate, MutateAi.class) .put(ApiType.NameCard, ChooseCardNameAi.class) .put(ApiType.NoteCounters, AlwaysPlayAi.class) .put(ApiType.PeekAndReveal, PeekAndRevealAi.class) @@ -139,8 +142,9 @@ public enum SpellApiToAi { .put(ApiType.Reveal, RevealAi.class) .put(ApiType.RevealHand, RevealHandAi.class) .put(ApiType.ReverseTurnOrder, AlwaysPlayAi.class) + .put(ApiType.RollDice, RollDiceAi.class) .put(ApiType.RollPlanarDice, RollPlanarDiceAi.class) - .put(ApiType.RunSVarAbility, AlwaysPlayAi.class) + .put(ApiType.RunChaos, AlwaysPlayAi.class) .put(ApiType.Sacrifice, SacrificeAi.class) .put(ApiType.SacrificeAll, SacrificeAllAi.class) .put(ApiType.Scry, ScryAi.class) diff --git a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java index 7699259abd1..ab54f83224d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.collect.Iterables; import com.google.common.collect.Sets; @@ -23,12 +25,12 @@ public class AmassAi extends SpellAbilityAi { final Game game = ai.getGame(); if (!aiArmies.isEmpty()) { - return CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterType.P1P1)) > 0; + return CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterEnumType.P1P1)) > 0; } else { final String tokenScript = "b_0_0_zombie_army"; final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa); - Card token = TokenInfo.getProtoType(tokenScript, sa); + Card token = TokenInfo.getProtoType(tokenScript, sa, false); if (token == null) { return false; @@ -44,8 +46,8 @@ public class AmassAi extends SpellAbilityAi { CardCollection preList = new CardCollection(token); game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList); - if (token.canReceiveCounters(CounterType.P1P1)) { - token.setCounters(CounterType.P1P1, amount); + if (token.canReceiveCounters(CounterEnumType.P1P1)) { + token.setCounters(CounterEnumType.P1P1, amount); } if (token.isCreature() && token.getNetToughness() < 1) { @@ -86,8 +88,8 @@ public class AmassAi extends SpellAbilityAi { } @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { - Iterable better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterType.P1P1)); + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { + Iterable better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterEnumType.P1P1)); if (Iterables.isEmpty(better)) { better = options; } diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java index baa0d00b7c5..25ec39da017 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java @@ -6,7 +6,6 @@ import com.google.common.collect.Maps; import forge.ai.*; import forge.card.CardType; import forge.game.Game; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; @@ -14,15 +13,10 @@ import forge.game.cost.CostPutCounter; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; -import forge.game.replacement.ReplacementEffect; -import forge.game.replacement.ReplacementHandler; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbilityContinuous; import forge.game.staticability.StaticAbilityLayer; -import forge.game.trigger.Trigger; -import forge.game.trigger.TriggerHandler; import forge.game.zone.ZoneType; import java.util.Arrays; @@ -43,15 +37,13 @@ import forge.game.ability.effects.AnimateEffectBase; public class AnimateAi extends SpellAbilityAi { @Override protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); - final Card source = sa.getHostCard(); final Game game = ai.getGame(); final PhaseHandler ph = game.getPhaseHandler(); if ("Attacking".equals(aiLogic)) { // Launch the Fleet if (ph.getPlayerTurn().isOpponentOf(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) { return false; } - List list = CardLists.getValidCards(ai.getCreaturesInPlay(), tgt.getValidTgts(), ai, source, sa); + List list = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa); for (Card c : list) { if (ComputerUtilCard.doesCreatureAttackAI(ai, c)) { sa.getTargets().add(c); @@ -130,7 +122,7 @@ public class AnimateAi extends SpellAbilityAi { final Card source = sa.getHostCard(); final Game game = aiPlayer.getGame(); final PhaseHandler ph = game.getPhaseHandler(); - if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) { + if (!sa.metConditions() && sa.getSubAbility() == null) { return false; // what is this for? } if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) { @@ -229,10 +221,7 @@ public class AnimateAi extends SpellAbilityAi { } else if (sa.usesTargeting() && mandatory) { // fallback if animate is mandatory sa.resetTargets(); - final TargetRestrictions tgt = sa.getTargetRestrictions(); - final Card source = sa.getHostCard(); - CardCollectionView list = aiPlayer.getGame().getCardsIn(tgt.getZone()); - list = CardLists.getValidCards(list, tgt.getValidTgts(), aiPlayer, source, sa); + List list = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa); if (list.isEmpty()) { return false; } @@ -247,24 +236,19 @@ public class AnimateAi extends SpellAbilityAi { final Player ai = sa.getActivatingPlayer(); final PhaseHandler ph = ai.getGame().getPhaseHandler(); final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker") - && sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class) && sa.getTargetRestrictions() != null && sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0; - final CardType types = new CardType(); + final CardType types = new CardType(true); if (sa.hasParam("Types")) { types.addAll(Arrays.asList(sa.getParam("Types").split(","))); } // something is used for animate into creature if (types.isCreature()) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); - final Card source = sa.getHostCard(); - CardCollectionView list = ai.getGame().getCardsIn(tgt.getZone()); - list = CardLists.getValidCards(list, tgt.getValidTgts(), ai, source, sa); - // need to targetable - list = CardLists.getTargetableCards(list, sa); + final Game game = ai.getGame(); + CardCollectionView list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); // Filter AI-specific targets if provided list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false); @@ -304,7 +288,7 @@ public class AnimateAi extends SpellAbilityAi { // evaluate their value to check if it becomes better if (c.isCreature()) { int cValue = ComputerUtilCard.evaluateCreature(c); - if (cValue <= aValue) + if (cValue >= aValue) continue; } @@ -341,9 +325,19 @@ public class AnimateAi extends SpellAbilityAi { // select the worst of the best final Card worst = ComputerUtilCard.getWorstAI(maxList); - this.rememberAnimatedThisTurn(ai, worst); - sa.getTargets().add(worst); - return true; + if (worst != null) { + if (worst.isLand()) { + // e.g. Clan Guildmage, make sure we're not using the same land we want to animate to activate the ability + this.holdAnimatedTillMain2(ai, worst); + if (!ComputerUtilMana.canPayManaCost(sa, ai, 0)) { + this.releaseHeldTillMain2(ai, worst); + return false; + } + } + this.rememberAnimatedThisTurn(ai, worst); + sa.getTargets().add(worst); + } + return true; } // This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or @@ -384,12 +378,12 @@ public class AnimateAi extends SpellAbilityAi { } } - final CardType types = new CardType(); + final CardType types = new CardType(true); if (sa.hasParam("Types")) { types.addAll(Arrays.asList(sa.getParam("Types").split(","))); } - final CardType removeTypes = new CardType(); + final CardType removeTypes = new CardType(true); if (sa.hasParam("RemoveTypes")) { removeTypes.addAll(Arrays.asList(sa.getParam("RemoveTypes").split(","))); } @@ -465,75 +459,15 @@ public class AnimateAi extends SpellAbilityAi { sVars.addAll(Arrays.asList(sa.getParam("sVars").split(","))); } - AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, keywords, removeKeywords, hiddenKeywords, timestamp); + AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, + keywords, removeKeywords, hiddenKeywords, + abilities, triggers, replacements, stAbs, + timestamp); - - // remove abilities - final List removedAbilities = Lists.newArrayList(); - boolean clearSpells = sa.hasParam("OverwriteSpells"); - boolean removeAll = sa.hasParam("RemoveAllAbilities"); - boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities"); - - if (clearSpells) { - removedAbilities.addAll(Lists.newArrayList(card.getSpells())); - } - - if (sa.hasParam("RemoveThisAbility") && !removedAbilities.contains(sa)) { - removedAbilities.add(sa); - } - - // give abilities - final List addedAbilities = Lists.newArrayList(); - if (abilities.size() > 0) { - for (final String s : abilities) { - final String actualAbility = source.getSVar(s); - addedAbilities.add(AbilityFactory.getAbility(actualAbility, card)); - } - } - - // Grant triggers - final List addedTriggers = Lists.newArrayList(); - if (triggers.size() > 0) { - for (final String s : triggers) { - final String actualTrigger = source.getSVar(s); - final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, card, false); - addedTriggers.add(parsedTrigger); - } - } - - // give replacement effects - final List addedReplacements = Lists.newArrayList(); - if (replacements.size() > 0) { - for (final String s : replacements) { - final String actualReplacement = source.getSVar(s); - final ReplacementEffect parsedReplacement = ReplacementHandler.parseReplacement(actualReplacement, card, false); - addedReplacements.add(parsedReplacement); - } - } - - // give static abilities (should only be used by cards to give - // itself a static ability) - final List addedStaticAbilities = Lists.newArrayList(); - if (stAbs.size() > 0) { - for (final String s : stAbs) { - final String actualAbility = source.getSVar(s); - addedStaticAbilities.add(new StaticAbility(actualAbility, card)); - } - } - - if (removeAll || removeIntrinsic - || !addedAbilities.isEmpty() || !removedAbilities.isEmpty() || !addedTriggers.isEmpty() - || !addedReplacements.isEmpty() || !addedStaticAbilities.isEmpty()) { - card.addChangedCardTraits(addedAbilities, removedAbilities, addedTriggers, addedReplacements, - addedStaticAbilities, removeAll, false, removeIntrinsic, timestamp); - } - - // give static abilities (should only be used by cards to give - // itself a static ability) - if (stAbs.size() > 0) { - for (final String s : stAbs) { - final String actualAbility = source.getSVar(s); - final StaticAbility stAb = card.addStaticAbility(actualAbility); + // check if animate added static Abilities + CardTraitChanges traits = card.getChangedCardTraits().get(timestamp); + if (traits != null) { + for (StaticAbility stAb : traits.getStaticAbilities()) { if ("Continuous".equals(stAb.getParam("Mode"))) { for (final StaticAbilityLayer layer : stAb.getLayers()) { StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer); @@ -565,4 +499,12 @@ public class AnimateAi extends SpellAbilityAi { public static boolean isAnimatedThisTurn(Player ai, Card c) { return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.ANIMATED_THIS_TURN); } + + private void holdAnimatedTillMain2(Player ai, Card c) { + AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2); + } + + private void releaseHeldTillMain2(Player ai, Card c) { + AiCardMemory.forgetCard(ai, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2); + } } diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java index 79770568fd3..7bdc43f40ce 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; +import forge.game.card.Card; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -8,12 +10,23 @@ public class AnimateAllAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return "Always".equals(sa.getParam("AILogic")); + String logic = sa.getParamOrDefault("AILogic", ""); + + if ("CreatureAdvantage".equals(logic) && !aiPlayer.getCreaturesInPlay().isEmpty()) { + // TODO: improve this or implement a better logic for abilities like Oko, the Trickster ultimate + for (Card c : aiPlayer.getCreaturesInPlay()) { + if (ComputerUtilCard.doesCreatureAttackAI(aiPlayer, c)) { + return true; + } + } + } + + return "Always".equals(logic); } // end animateAllCanPlayAI() @Override protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return mandatory; + return mandatory || canPlayAI(aiPlayer, sa); } } // end class AbilityFactoryAnimate diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index 23648c94401..c4eb2fd118f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -69,11 +69,6 @@ public class AttachAi extends SpellAbilityAi { return false; } - if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) - && !"Curse".equals(sa.getParam("AILogic"))) { - return false; - } - // prevent run-away activations - first time will always return true if (ComputerUtil.preventRunAwayActivations(sa)) { return false; @@ -93,20 +88,21 @@ public class AttachAi extends SpellAbilityAi { if (ai.getController().isAI()) { advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC); } - if (source.withFlash(ai) && source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) { + if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai))) + && source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) { return false; } - if (abCost.getTotalMana().countX() > 0 && source.getSVar("X").equals("Count$xPaid")) { + if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. (Endless Scream and Venarian // Gold) - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); if (xPay == 0) { return false; } - source.setSVar("PayX", Integer.toString(xPay)); + sa.setXManaCostPaid(xPay); } if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) { @@ -398,7 +394,7 @@ public class AttachAi extends SpellAbilityAi { if (!c.isCreature() && !c.getType().hasSubtype("Vehicle") && !c.isTapped()) { // try to identify if this thing can actually tap for (SpellAbility ab : c.getAllSpellAbilities()) { - if (ab.getPayCosts() != null && ab.getPayCosts().hasTapCost()) { + if (ab.getPayCosts().hasTapCost()) { return true; } } @@ -560,7 +556,7 @@ public class AttachAi extends SpellAbilityAi { @Override public boolean apply(final Card c) { for (final SpellAbility sa : c.getSpellAbilities()) { - if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isAbility() && sa.getPayCosts().hasTapCost()) { return false; } } @@ -838,7 +834,7 @@ public class AttachAi extends SpellAbilityAi { * @return the card */ private static Card attachAICursePreference(final SpellAbility sa, final List list, final boolean mandatory, - final Card attachSource) { + final Card attachSource, final Player ai) { // AI For choosing a Card to Curse of. // TODO Figure out some way to combine The "gathering of data" from @@ -930,6 +926,24 @@ public class AttachAi extends SpellAbilityAi { ); } + // If this is already attached and there's a sac cost, make sure we attach to something that's + // seriously better than whatever the attachment is currently attached to (e.g. Bound by Moonsilver) + if (sa.getHostCard().getAttachedTo() != null && sa.getHostCard().getAttachedTo().isCreature() + && sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { + final int oldEvalRating = ComputerUtilCard.evaluateCreature(sa.getHostCard().getAttachedTo()); + final int threshold = ai.isAI() ? ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.SAC_TO_REATTACH_TARGET_EVAL_THRESHOLD) : Integer.MAX_VALUE; + prefList = CardLists.filter(prefList, new Predicate() { + @Override + public boolean apply(Card card) { + if (!card.isCreature()) { + return false; + } + + return ComputerUtilCard.evaluateCreature(card) >= oldEvalRating + threshold; + } + }); + } + c = ComputerUtilCard.getBestAI(prefList); if (c == null) { @@ -959,7 +973,7 @@ public class AttachAi extends SpellAbilityAi { targets = AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa); } else { AttachAi.attachPreference(sa, tgt, mandatory); - targets = sa.getTargets().getTargets(); + targets = sa.getTargets(); } if (!mandatory && card.isEquipment() && !targets.isEmpty()) { @@ -1072,12 +1086,7 @@ public class AttachAi extends SpellAbilityAi { final Map params = t.getMapParams(); if ("Card.Self".equals(params.get("ValidCard")) && "Battlefield".equals(params.get("Destination"))) { - SpellAbility trigSa = null; - if (t.hasParam("Execute") && attachSource.hasSVar(t.getParam("Execute"))) { - trigSa = AbilityFactory.getAbility(attachSource.getSVar(params.get("Execute")), attachSource); - } else if (t.getOverridingAbility() != null) { - trigSa = t.getOverridingAbility(); - } + SpellAbility trigSa = t.ensureAbility(); if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) { for (Card target : list) { if (!target.getController().isOpponentOf(ai)) { @@ -1353,7 +1362,7 @@ public class AttachAi extends SpellAbilityAi { if (c != null && attachSource.isEquipment() && attachSource.isEquipping() && attachSource.getEquipping().getController() == aiPlayer) { - if (c.equals(attachSource.getEquipping())) { + if (c.equals(attachSource.getEquipping()) && !mandatory) { // Do not equip if equipping the same card already return null; } @@ -1364,13 +1373,13 @@ public class AttachAi extends SpellAbilityAi { boolean uselessCreature = ComputerUtilCard.isUselessCreature(aiPlayer, attachSource.getEquipping()); - if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) { + if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never") && !mandatory) { // Do not equip other creatures if the AI profile does not allow moving equipment around return null; } else if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("from_useless_only")) { // Do not equip other creatures if the AI profile only allows moving equipment from useless creatures // and the equipped creature is still useful (not non-untapping+tapped and not set to can't attack/block) - if (!uselessCreature) { + if (!uselessCreature && !mandatory) { return null; } } @@ -1386,13 +1395,13 @@ public class AttachAi extends SpellAbilityAi { } // avoid randomly moving the equipment back and forth between several creatures in one turn - if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN)) { + if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN) && !mandatory) { return null; } // do not equip if the new creature is not significantly better than the previous one (evaluates at least better by evalT) int evalT = aic.getIntProperty(AiProps.MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD); - if (!decideMoveFromUseless && ComputerUtilCard.evaluateCreature(c) - ComputerUtilCard.evaluateCreature(attachSource.getEquipping()) < evalT) { + if (!decideMoveFromUseless && ComputerUtilCard.evaluateCreature(c) - ComputerUtilCard.evaluateCreature(attachSource.getEquipping()) < evalT && !mandatory) { return null; } } @@ -1455,7 +1464,7 @@ public class AttachAi extends SpellAbilityAi { if ("GainControl".equals(logic)) { c = attachAIControlPreference(sa, prefList, mandatory, attachSource); } else if ("Curse".equals(logic)) { - c = attachAICursePreference(sa, prefList, mandatory, attachSource); + c = attachAICursePreference(sa, prefList, mandatory, attachSource, ai); } else if ("Pump".equals(logic)) { c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource); } else if ("Curiosity".equals(logic)) { @@ -1555,7 +1564,6 @@ public class AttachAi extends SpellAbilityAi { } else if (keyword.equals("Haste")) { return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped() && card.getNetCombatDamage() + powerBonus > 0 - && !card.hasKeyword("CARDNAME can attack as though it had haste.") && !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && ComputerUtilCombat.canAttackNextTurn(card); } else if (keyword.endsWith("Indestructible")) { @@ -1704,12 +1712,12 @@ public class AttachAi extends SpellAbilityAi { } @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return attachToCardAIPreferences(ai, sa, true); } @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { return attachToPlayerAIPreferences(ai, sa, true); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java index f3c44f1caa0..9efd36f9a44 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java @@ -33,7 +33,7 @@ public class BecomesBlockedAi extends SpellAbilityAi { list = CardLists.getTargetableCards(list, sa); list = CardLists.getNotKeyword(list, Keyword.TRAMPLE); - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) { Card choice = null; if (list.isEmpty()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/BondAi.java b/forge-ai/src/main/java/forge/ai/ability/BondAi.java index 03c950d3f2e..8619983df69 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BondAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BondAi.java @@ -17,6 +17,8 @@ */ package forge.ai.ability; +import java.util.Map; + import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -50,7 +52,7 @@ public final class BondAi extends SpellAbilityAi { @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return ComputerUtilCard.getBestCreatureAI(options); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java new file mode 100644 index 00000000000..1187dc81cdc --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java @@ -0,0 +1,66 @@ +package forge.ai.ability; + +import forge.ai.SpellAbilityAi; +import forge.game.GameEntity; +import forge.game.player.Player; +import forge.game.player.PlayerCollection; +import forge.game.player.PlayerPredicates; +import forge.game.spellability.SpellAbility; + +import java.util.Collection; +import java.util.Map; + +public class ChangeCombatantsAi extends SpellAbilityAi { + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) + */ + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + // TODO: Extend this if possible for cards that have this as an activated ability + return false; + } + + @Override + protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return mandatory || canPlayAI(aiPlayer, sa); + } + + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) + */ + @Override + public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + final String logic = sa.getParamOrDefault("AILogic", ""); + + if (logic.equals("WeakestOppExceptCtrl")) { + PlayerCollection targetableOpps = aiPlayer.getOpponents(); + targetableOpps.remove(sa.getHostCard().getController()); + if (targetableOpps.isEmpty()) { + return false; + } + + return true; + } + + return false; + } + + @Override + public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer, Map params) { + PlayerCollection targetableOpps = new PlayerCollection(); + for (GameEntity p : options) { + if (p instanceof Player && !p.equals(sa.getHostCard().getController())) { + Player pp = (Player)p; + if (pp.isOpponentOf(ai)) { + targetableOpps.add(pp); + } + } + } + + Player weakestTargetableOpp = targetableOpps.filter(PlayerPredicates.isTargetableBy(sa)) + .min(PlayerPredicates.compareByLife()); + + return (T)weakestTargetableOpp; + } +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java index 045fdefa6aa..dbc74f9cd4a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java @@ -41,7 +41,7 @@ public class ChangeTargetsAi extends SpellAbilityAi { return false; } - if (sa.getTargets().getNumTargeted() != 0) { + if (sa.getTargets().size() != 0) { // something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed return true; } @@ -80,7 +80,7 @@ public class ChangeTargetsAi extends SpellAbilityAi { ManaCost normalizedMana = manaCost.getNormalizedMana(); boolean canPay = ComputerUtilMana.canPayManaCost(new ManaCostBeingPaid(normalizedMana), sa, aiPlayer); if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay - && topSa.getTargets().getTargets().contains(aiPlayer)) { + && topSa.getTargets().contains(aiPlayer)) { // do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index 40c59393086..59d75014b1f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -96,6 +96,8 @@ public class ChangeZoneAi extends SpellAbilityAi { } return true; + } else if (aiLogic.equals("Pongify")) { + return SpecialAiLogic.doPongifyLogic(ai, sa); } return super.checkAiLogic(ai, sa, aiLogic); @@ -128,6 +130,10 @@ public class ChangeZoneAi extends SpellAbilityAi { // This logic only fills the multiple cards array, the decision to play is made // separately in hiddenOriginCanPlayAI later. multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa); + } else if (aiLogic.equals("MazesEnd")) { + return SpecialCardAi.MazesEnd.consider(aiPlayer, sa); + } else if (aiLogic.equals("Pongify")) { + return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic } } if (isHidden(sa)) { @@ -142,7 +148,7 @@ public class ChangeZoneAi extends SpellAbilityAi { *

* @param sa * a {@link forge.game.spellability.SpellAbility} object. - * + * * @return a boolean. */ @Override @@ -170,7 +176,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * a {@link forge.game.spellability.SpellAbility} object. * @param mandatory * a boolean. - * + * * @return a boolean. */ @Override @@ -298,10 +304,10 @@ public class ChangeZoneAi extends SpellAbilityAi { } // don't play if the conditions aren't met, unless it would trigger a beneficial sub-condition - if (!activateForCost && !sa.getConditions().areMet(sa)) { + if (!activateForCost && !sa.metConditions()) { final AbilitySub abSub = sa.getSubAbility(); if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) { - if (!abSub.getConditions().areMet(abSub)) { + if (!abSub.metConditions()) { return false; } } else { @@ -333,10 +339,10 @@ public class ChangeZoneAi extends SpellAbilityAi { String type = sa.getParam("ChangeType"); if (type != null) { - if (type.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); type = type.replace("X", Integer.toString(xPay)); } } @@ -370,22 +376,22 @@ public class ChangeZoneAi extends SpellAbilityAi { if (!activateForCost && list.isEmpty()) { return false; } - if ("Atarka's Command".equals(sourceName) - && (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) { - // be strict on playing lands off charms - return false; + if ("Atarka's Command".equals(sourceName) + && (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) { + // be strict on playing lands off charms + return false; } String num = sa.getParam("ChangeNum"); if (num != null) { - if (num.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); + int xPay = ComputerUtilCost.getMaxXValue(sa, ai); xPay = Math.min(xPay, list.size()); - source.setSVar("PayX", Integer.toString(xPay)); + sa.setXManaCostPaid(xPay); } } - + if (sourceName.equals("Temur Sabertooth")) { // activated bounce + pump if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) || @@ -400,9 +406,9 @@ public class ChangeZoneAi extends SpellAbilityAi { } } - + if (ComputerUtil.playImmediately(ai, sa)) { - return true; + return true; } // don't use fetching to top of library/graveyard before main2 @@ -418,9 +424,9 @@ public class ChangeZoneAi extends SpellAbilityAi { } if (ComputerUtil.waitForBlocking(sa)) { - return false; + return false; } - + final AbilitySub subAb = sa.getSubAbility(); return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb); } @@ -468,8 +474,6 @@ public class ChangeZoneAi extends SpellAbilityAi { // Fetching should occur fairly often as it helps cast more spells, and // have access to more mana - final Card source = sa.getHostCard(); - if (sa.hasParam("AILogic")) { if (sa.getParam("AILogic").equals("Never")) { /* @@ -490,10 +494,10 @@ public class ChangeZoneAi extends SpellAbilityAi { // this works for hidden because the mana is paid first. final String type = sa.getParam("ChangeType"); - if (type != null && type.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (type != null && type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); } Iterable pDefined; @@ -551,7 +555,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * basicManaFixing. *

* @param ai - * + * * @param list * a List object. * @return a {@link forge.game.card.Card} object. @@ -584,7 +588,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (minType != null) { result = CardLists.getType(list, minType); } - + // pick dual lands if available if (Iterables.any(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS))) { result = CardLists.filter(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS)); @@ -597,7 +601,7 @@ public class ChangeZoneAi extends SpellAbilityAi { *

* areAllBasics. *

- * + * * @param types * a {@link java.lang.String} object. * @return a boolean. @@ -617,8 +621,8 @@ public class ChangeZoneAi extends SpellAbilityAi { * @return Card */ private static Card chooseCreature(final Player ai, CardCollection list) { - // Creating a new combat for testing purposes. - final Player opponent = ai.getWeakestOpponent(); + // Creating a new combat for testing purposes. + final Player opponent = ai.getWeakestOpponent(); Combat combat = new Combat(opponent); for (Card att : opponent.getCreaturesInPlay()) { combat.addAttacker(att, ai); @@ -742,7 +746,7 @@ public class ChangeZoneAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler) @@ -781,7 +785,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return false; } } - + //don't unearth after attacking is possible if (sa.hasParam("Unearth") && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) { return false; @@ -841,7 +845,6 @@ public class ChangeZoneAi extends SpellAbilityAi { } final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Game game = ai.getGame(); final AbilitySub abSub = sa.getSubAbility(); @@ -855,8 +858,16 @@ public class ChangeZoneAi extends SpellAbilityAi { } sa.resetTargets(); - CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, source, sa); - list = CardLists.getTargetableCards(list, sa); + // X controls the minimum targets + if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) { + // Set PayX here to maximum value. + int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + + // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? + sa.setXManaCostPaid(xPay); + // TODO since change of PayX. the shouldCastLessThanMax logic might be faulty + } + CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); // Filter AI-specific targets if provided list = ComputerUtil.filterAITgts(sa, ai, list, true); @@ -892,10 +903,10 @@ public class ChangeZoneAi extends SpellAbilityAi { //System.out.println("isPreferredTarget ok " + list); } - if (list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (list.size() < sa.getMinTargets()) { return false; } - + immediately |= ComputerUtil.playImmediately(ai, sa); // Narrow down the list: @@ -917,7 +928,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } // Combat bouncing - if (tgt.getMinTargets(sa.getHostCard(), sa) <= 1) { + if (sa.getMinTargets() <= 1) { if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { Combat currCombat = game.getCombat(); CardCollection attackers = currCombat.getAttackers(); @@ -926,7 +937,7 @@ public class ChangeZoneAi extends SpellAbilityAi { CardCollection blockers = currCombat.getBlockers(attacker); // Save my attacker by bouncing a blocker if (attacker.getController().equals(ai) && attacker.getShieldCount() == 0 - && ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, currCombat) + && ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, currCombat) && !currCombat.getBlockers(attacker).isEmpty()) { ComputerUtilCard.sortByEvaluateCreature(blockers); Combat combat = new Combat(ai); @@ -960,7 +971,7 @@ public class ChangeZoneAi extends SpellAbilityAi { // if it's blink or bounce, try to save my about to die stuff final boolean blink = (destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger || "DelayedBlink".equals(sa.getParam("AILogic")) || (subApi == ApiType.ChangeZone && subAffected.equals("Remembered")))); - if ((destination.equals(ZoneType.Hand) || blink) && (tgt.getMinTargets(sa.getHostCard(), sa) <= 1)) { + if ((destination.equals(ZoneType.Hand) || blink) && (sa.getMinTargets() <= 1)) { // save my about to die stuff Card tobounce = canBouncePermanent(ai, sa, list); if (tobounce != null) { @@ -970,9 +981,9 @@ public class ChangeZoneAi extends SpellAbilityAi { sa.getTargets().add(tobounce); - boolean saheeliFelidarCombo = sa.getHostCard().getName().equals("Felidar Guardian") + boolean saheeliFelidarCombo = ComputerUtilAbility.getAbilitySourceName(sa).equals("Felidar Guardian") && tobounce.getName().equals("Saheeli Rai") - && CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Felidar Guardian")).size() < + && CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Felidar Guardian")).size() < CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Creature")).size() + ai.getOpponentsGreatestLifeTotal() + 10; // remember that the card was bounced already unless it's a special combo case @@ -985,20 +996,20 @@ public class ChangeZoneAi extends SpellAbilityAi { // bounce opponent's stuff list = CardLists.filterControlledBy(list, ai.getOpponents()); if (!CardLists.getNotType(list, "Land").isEmpty()) { - // When bouncing opponents stuff other than lands, don't bounce cards with CMC 0 - list = CardLists.filter(list, new Predicate() { - @Override - public boolean apply(final Card c) { - for (Card aura : c.getEnchantedBy()) { + // When bouncing opponents stuff other than lands, don't bounce cards with CMC 0 + list = CardLists.filter(list, new Predicate() { + @Override + public boolean apply(final Card c) { + for (Card aura : c.getEnchantedBy()) { return aura.getController().isOpponentOf(ai); - } - if (blink) { - return c.isToken(); - } else { - return c.isToken() || c.getCMC() > 0; - } - } - }); + } + if (blink) { + return c.isToken(); + } else { + return c.isToken() || c.getCMC() > 0; + } + } + }); } // TODO: Blink permanents with ETB triggers /*else if (!sa.isTrigger() && SpellAbilityAi.playReusable(ai, sa)) { @@ -1023,7 +1034,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } } else if (origin.contains(ZoneType.Graveyard)) { - if (destination.equals(ZoneType.Exile) || destination.equals(ZoneType.Library)) { + if (destination.equals(ZoneType.Exile) || destination.equals(ZoneType.Library)) { // Don't use these abilities before main 2 if possible if (!immediately && game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") && !ComputerUtil.castSpellInMain1(ai, sa)) { @@ -1035,7 +1046,7 @@ public class ChangeZoneAi extends SpellAbilityAi { && !ComputerUtil.activateForCost(sa, ai)) { return false; } - } else if (destination.equals(ZoneType.Hand)) { + } else if (destination.equals(ZoneType.Hand)) { // only retrieve cards from computer graveyard list = CardLists.filterControlledBy(list, ai); } else if (sa.hasParam("AttachedTo")) { @@ -1065,8 +1076,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (destination.equals(ZoneType.Exile) || origin.contains(ZoneType.Battlefield)) { // don't rush bouncing stuff when not going to attack - if (!immediately && sa.getPayCosts() != null - && game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) + if (!immediately && game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && game.getPhaseHandler().isPlayerTurn(ai) && ai.getCreaturesInPlay().isEmpty()) { return false; @@ -1097,14 +1107,14 @@ public class ChangeZoneAi extends SpellAbilityAi { // Only care about combatants during combat if (game.getPhaseHandler().inCombat() && origin.contains(ZoneType.Battlefield)) { - CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null); - if (!newList.isEmpty() || !sa.isTrigger()) { - list = newList; - } + CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null); + if (!newList.isEmpty() || !sa.isTrigger()) { + list = newList; + } } - boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.getTargetRestrictions() != null - && sa.getTargetRestrictions().getMinTargets(source, sa) == 0 && sa.getPayCosts() != null + boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.usesTargeting() + && sa.getMinTargets() == 0 && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class); if (list.isEmpty() && !doWithoutTarget) { @@ -1115,12 +1125,12 @@ public class ChangeZoneAi extends SpellAbilityAi { // the Unless cost (for example, Erratic Portal) list.removeAll(getSafeTargetsIfUnlessCostPaid(ai, sa, list)); - if (!mandatory && list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (!mandatory && list.size() < sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa)) { return false; } // target loop - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { // AI Targeting Card choice = null; @@ -1149,7 +1159,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } //option to hold removal instead only applies for single targeted removal - if (!immediately && tgt.getMaxTargets(source, sa) == 1) { + if (!immediately && sa.getMaxTargets() == 1) { if (!ComputerUtilCard.useRemovalNow(sa, choice, 0, destination)) { return false; } @@ -1185,7 +1195,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } } if (choice == null) { // can't find anything left - if (sa.getTargets().getNumTargeted() == 0 || sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (sa.getTargets().size() == 0 || !sa.isTargetNumberValid()) { if (!mandatory) { sa.resetTargets(); } @@ -1199,7 +1209,7 @@ public class ChangeZoneAi extends SpellAbilityAi { boolean aiTgtsOK = false; if (sa.hasParam("AIMinTgts")) { int minTgts = Integer.parseInt(sa.getParam("AIMinTgts")); - if (sa.getTargets().getNumTargeted() >= minTgts) { + if (sa.getTargets().size() >= minTgts) { aiTgtsOK = true; } } @@ -1219,8 +1229,16 @@ public class ChangeZoneAi extends SpellAbilityAi { } } + // if max power exceeded, do not choose this card (but keep looking for other options) + if (sa.hasParam("MaxTotalTargetPower")) { + if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) { + list.remove(choice); + continue; + } + } + // honor the Same Creature Type restriction - if (tgt.isWithSameCreatureType()) { + if (sa.getTargetRestrictions().isWithSameCreatureType()) { Card firstTarget = sa.getTargetCard(); if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) { list.remove(choice); @@ -1252,7 +1270,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return true; } - + /** * Checks if a permanent threatened by a stack ability or in combat can * be saved by bouncing. @@ -1325,11 +1343,11 @@ public class ChangeZoneAi extends SpellAbilityAi { Collections.sort(aiPlaneswalkers, new Comparator() { @Override public int compare(final Card a, final Card b) { - return a.getCounters(CounterType.LOYALTY) - b.getCounters(CounterType.LOYALTY); + return a.getCounters(CounterEnumType.LOYALTY) - b.getCounters(CounterEnumType.LOYALTY); } }); for (Card pw : aiPlaneswalkers) { - int curLoyalty = pw.getCounters(CounterType.LOYALTY); + int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY); int freshLoyalty = Integer.valueOf(pw.getCurrentState().getBaseLoyalty()); if (freshLoyalty - curLoyalty >= loyaltyDiff && curLoyalty <= maxLoyaltyToConsider) { return pw; @@ -1348,46 +1366,31 @@ public class ChangeZoneAi extends SpellAbilityAi { } final Card source = sa.getHostCard(); - final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0); final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); final TargetRestrictions tgt = sa.getTargetRestrictions(); - CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(origin), tgt.getValidTgts(), ai, source, sa); + CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), ai, source, sa); + list = CardLists.getTargetableCards(list, sa); - // Narrow down the list: - if (origin.equals(ZoneType.Battlefield)) { - // filter out untargetables - list = CardLists.getTargetableCards(list, sa); - - // if Destination is hand, either bounce opponents dangerous stuff - // or save my about to die stuff - - // if Destination is exile, filter out my cards - } - else if (origin.equals(ZoneType.Graveyard)) { - // Retrieve from Graveyard to: - } - - for (final Card c : sa.getTargets().getTargetCards()) { - list.remove(c); - } + list.removeAll(sa.getTargets().getTargetCards()); if (list.isEmpty()) { return false; } // target loop - while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + while (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { // AI Targeting Card choice = null; if (!list.isEmpty()) { - if (ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false).isCreature() - && (destination.equals(ZoneType.Battlefield) || origin.equals(ZoneType.Battlefield))) { + Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false); + if (mostExpensivePermanent.isCreature() + && (destination.equals(ZoneType.Battlefield) || tgt.getZone().contains(ZoneType.Battlefield))) { // if a creature is most expensive take the best choice = ComputerUtilCard.getBestCreatureToBounceAI(list); - } else if (destination.equals(ZoneType.Battlefield) || origin.equals(ZoneType.Battlefield)) { - choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false); + } else if (destination.equals(ZoneType.Battlefield) || tgt.getZone().contains(ZoneType.Battlefield)) { + choice = mostExpensivePermanent; } else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) { List nonLands = CardLists.getNotType(list, "Land"); // Prefer to pull a creature, generally more useful for AI. @@ -1419,7 +1422,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } } if (choice == null) { // can't find anything left - if (sa.getTargets().getNumTargeted() == 0 || sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (sa.getTargets().size() == 0 || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { sa.resetTargets(); return false; } else { @@ -1449,10 +1452,14 @@ public class ChangeZoneAi extends SpellAbilityAi { * @return a boolean. */ private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) { - if ("DeathgorgeScavenger".equals(sa.getParam("AILogic"))) { + final String logic = sa.getParamOrDefault("AILogic", ""); + + if ("DeathgorgeScavenger".equals(logic)) { return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa); - } else if ("ExtraplanarLens".equals(sa.getParam("AILogic"))) { + } else if ("ExtraplanarLens".equals(logic)) { return SpecialCardAi.ExtraplanarLens.consider(ai, sa); + } else if ("ExileCombatThreat".equals(logic)) { + return doExileCombatThreatLogic(ai, sa); } if (sa.getTargetRestrictions() == null) { @@ -1480,7 +1487,7 @@ public class ChangeZoneAi extends SpellAbilityAi { String logic = sa.getParam("AILogic"); if ("NeverBounceItself".equals(logic)) { Card source = sa.getHostCard(); - if (fetchList.contains(source) && (fetchList.size() > 1 && !sa.getTriggeringAbility().isMandatory())) { + if (fetchList.contains(source) && (fetchList.size() > 1 || !sa.getRootAbility().isMandatory())) { // For cards that should never be bounced back to hand with their own [e.g. triggered] abilities, such as guild lands. fetchList.remove(source); } @@ -1492,6 +1499,8 @@ public class ChangeZoneAi extends SpellAbilityAi { return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList); } else if ("SurvivalOfTheFittest".equals(logic)) { return SpecialCardAi.SurvivalOfTheFittest.considerCardToGet(decider, sa); + } else if ("MazesEnd".equals(logic)) { + return SpecialCardAi.MazesEnd.considerCardToGet(decider, sa); } else if ("Intuition".equals(logic)) { if (!multipleCardsToChoose.isEmpty()) { Card choice = multipleCardsToChoose.get(0); @@ -1507,10 +1516,10 @@ public class ChangeZoneAi extends SpellAbilityAi { if (type == null) { type = "Card"; } - + Card c = null; final Player activator = sa.getActivatingPlayer(); - + CardLists.shuffle(fetchList); // Save a card as a default, in case we can't find anything suitable. Card first = fetchList.get(0); @@ -1539,6 +1548,14 @@ public class ChangeZoneAi extends SpellAbilityAi { if (player.isOpponentOf(decider)) { c = ComputerUtilCard.getBestAI(fetchList); } else { + if (!sa.hasParam("Mandatory") && origin.contains(ZoneType.Battlefield) && sa.hasParam("ChangeNum")) { + // exclude tokens, they won't come back, and enchanted stuff, since auras will go away + fetchList = prefilterListForBounceAnyNum(fetchList, decider); + if (fetchList.isEmpty()) { + return null; + } + } + c = ComputerUtilCard.getWorstAI(fetchList); if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Temur Sabertooth")) { Card tobounce = canBouncePermanent(player, sa, fetchList); @@ -1607,6 +1624,57 @@ public class ChangeZoneAi extends SpellAbilityAi { return c; } + private static CardCollection prefilterListForBounceAnyNum(CardCollection fetchList, Player decider) { + fetchList = CardLists.filter(fetchList, new Predicate() { + @Override + public boolean apply(final Card card) { + if (card.isToken()) { + return false; + } + + if (card.isCreature() && ComputerUtilCard.isUselessCreature(decider, card)) { + return true; + } else if (card.isEquipped()) { + return false; + } else if (card.isEnchanted()) { + for (Card enc : card.getEnchantedBy()) { + if (enc.getOwner().isOpponentOf(decider)) { + return true; + } + } + return false; + } else if (card.hasCounters()) { + if (card.isPlaneswalker()) { + int maxLoyaltyToConsider = 2; + int loyaltyDiff = 2; + int chance = 30; + if (decider.getController().isAI()) { + AiController aic = ((PlayerControllerAi) decider.getController()).getAi(); + maxLoyaltyToConsider = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY); + loyaltyDiff = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF); + chance = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_CHANCE); + } + if (MyRandom.percentTrue(chance)) { + int curLoyalty = card.getCounters(CounterEnumType.LOYALTY); + int freshLoyalty = Integer.valueOf(card.getCurrentState().getBaseLoyalty()); + if (freshLoyalty - curLoyalty >= loyaltyDiff && curLoyalty <= maxLoyaltyToConsider) { + return true; + } + } + } else if (card.isCreature() && card.getCounters(CounterEnumType.M1M1) > 0) { + return true; + } + return false; // TODO: improve for other counters + } else if (card.isAura()) { + return false; + } + return true; + } + }); + + return fetchList; + } + /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @@ -1615,19 +1683,19 @@ public class ChangeZoneAi extends SpellAbilityAi { // AI was never asked return true; } - + @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Called when looking for creature to attach aura or equipment return ComputerUtilCard.getBestAI(options); } - + /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List) */ @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { // Currently only used by Curse of Misfortunes, so this branch should never get hit // But just in case it does, just select the first option return Iterables.getFirst(options, null); @@ -1790,6 +1858,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } public boolean doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) { + @SuppressWarnings("unchecked") Map originalParams = (Map)sa.getReplacingObject(AbilityKey.OriginalParams); SpellAbility causeSa = (SpellAbility)originalParams.get(AbilityKey.Cause); SpellAbility causeSub = null; @@ -1801,7 +1870,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) { ApiType subApi = causeSub.getApi(); - + if (subApi == ApiType.ChangeZone && "Exile".equals(causeSub.getParam("Origin")) && "Battlefield".equals(causeSub.getParam("Destination"))) { // A blink effect implemented using ChangeZone API @@ -1809,7 +1878,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } else // This is an intrinsic effect that blinks the card (e.g. Obzedat, Ghost Council), no need to // return the commander to the Command zone. if (subApi == ApiType.DelayedTrigger) { - SpellAbility exec = causeSub.getAdditionalAbility("Execute"); + SpellAbility exec = causeSub.getAdditionalAbility("Execute"); if (exec != null && exec.getApi() == ApiType.ChangeZone) { // A blink effect implemented using a delayed trigger return !"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination")); @@ -1817,11 +1886,56 @@ public class ChangeZoneAi extends SpellAbilityAi { } else return causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card)) || !causeSa.getActivatingPlayer().equals(aiPlayer); } - + // Normally we want the commander back in Command zone to recast him later return true; } + public static boolean doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) { + final Combat combat = aiPlayer.getGame().getCombat(); + + if (combat == null) { + return false; + } + + Card choice = null; + int highestEval = -1; + if (combat.getAttackingPlayer().isOpponentOf(aiPlayer)) { + for (Card attacker : combat.getAttackers()) { + if (sa.canTarget(attacker) && attacker.canBeTargetedBy(sa)) { + int eval = ComputerUtilCard.evaluateCreature(attacker); + if (combat.isUnblocked(attacker)) { + eval += 100; // TODO: make this smarter + } + if (eval > highestEval) { + highestEval = eval; + choice = attacker; + } + } + } + } else { + // either the current AI player or one of its teammates is attacking, the opponent(s) are blocking + for (Card blocker : combat.getAllBlockers()) { + if (sa.canTarget(blocker) && blocker.canBeTargetedBy(sa)) { + if (blocker.getController().isOpponentOf(aiPlayer)) { // TODO: unnecessary sanity check? + int eval = ComputerUtilCard.evaluateCreature(blocker); + if (eval > highestEval) { + highestEval = eval; + choice = blocker; + } + } + } + } + } + + if (choice != null) { + sa.getTargets().add(choice); + return true; + } else { + return false; + } + } + private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable potentialTgts) { // Determines if the controller of each potential target can negate the ChangeZone effect // by paying the Unless cost. Returns the list of targets that can be saved that way. @@ -1837,8 +1951,9 @@ public class ChangeZoneAi extends SpellAbilityAi { int toPay = 0; boolean setPayX = false; - if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) { + if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { setPayX = true; + // TODO use ComputerUtilCost.getMaxXValue if able toPay = ComputerUtilMana.determineLeftoverMana(sa, ai); } else { toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); @@ -1853,7 +1968,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } if (setPayX) { - source.setSVar("PayX", Integer.toString(toPay)); + sa.setXManaCostPaid(toPay); } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java index fcf0733bf0d..c2d8939dc8c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java @@ -63,7 +63,6 @@ public class ChangeZoneAllAi extends SpellAbilityAi { computerType = CardLists.filter(computerType, Predicates.not(CardPredicates.inZone(ZoneType.Library))); } - // Ugin check need to be done before filterListByType because of ChosenX // Ugin AI: always try to sweep before considering +1 if (sourceName.equals("Ugin, the Spirit Dragon")) { return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType); diff --git a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java index 1f9343599df..8835fc47e33 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java @@ -2,7 +2,9 @@ package forge.ai.ability; import com.google.common.collect.Lists; import forge.ai.*; +import forge.game.ability.AbilityUtils; import forge.game.ability.effects.CharmEffect; +import forge.game.card.Card; import forge.game.player.Player; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; @@ -11,6 +13,7 @@ import forge.util.MyRandom; import forge.util.collect.FCollection; import java.util.List; +import java.util.Map; public class CharmAi extends SpellAbilityAi { @Override @@ -18,10 +21,13 @@ public class CharmAi extends SpellAbilityAi { // sa is Entwined, no need for extra logic if (sa.isEntwine()) { return true; - } + } + + final Card source = sa.getHostCard(); + + final int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa); + final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num; - final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1"); - final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num; boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now? // Reset the chosen list otherwise it will be locked in forever by earlier calls @@ -232,7 +238,7 @@ public class CharmAi extends SpellAbilityAi { } @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable opponents) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable opponents, Map params) { return Aggregates.random(opponents); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java index 1beefd77b09..e490305988b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.Collections; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -20,7 +21,7 @@ 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.CounterType; +import forge.game.card.CounterEnumType; import forge.game.combat.Combat; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; @@ -99,11 +100,11 @@ public class ChooseCardAi extends SpellAbilityAi { }); return !choices.isEmpty(); } else if (aiLogic.equals("Ashiok")) { - final int loyalty = host.getCounters(CounterType.LOYALTY) - 1; + final int loyalty = host.getCounters(CounterEnumType.LOYALTY) - 1; for (int i = loyalty; i >= 0; i--) { - host.setSVar("ChosenX", "Number$" + i); + sa.setXManaCostPaid(i); choices = ai.getGame().getCardsIn(choiceZone); - choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host); + choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa); if (!choices.isEmpty()) { return true; } @@ -129,6 +130,12 @@ public class ChooseCardAi extends SpellAbilityAi { return (ComputerUtilCard.evaluateCreatureList(aiCreatures) + minGain) < ComputerUtilCard .evaluateCreatureList(oppCreatures); + } else if (aiLogic.equals("OwnCard")) { + CardCollectionView ownChoices = CardLists.filter(choices, CardPredicates.isController(ai)); + if (ownChoices.isEmpty()) { + ownChoices = CardLists.filter(choices, CardPredicates.isControlledByAnyOf(ai.getAllies())); + } + return !ownChoices.isEmpty(); } return true; } @@ -140,12 +147,12 @@ public class ChooseCardAi extends SpellAbilityAi { } return checkApiLogic(ai, sa); } - + /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) */ @Override - public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { final Card host = sa.getHostCard(); final Player ctrl = host.getController(); final String logic = sa.getParam("AILogic"); @@ -155,6 +162,12 @@ public class ChooseCardAi extends SpellAbilityAi { choice = ComputerUtilCard.getBestAI(options); } else if ("WorstCard".equals(logic)) { choice = ComputerUtilCard.getWorstAI(options); + } else if ("OwnCard".equals(logic)) { + CardCollectionView ownChoices = CardLists.filter(options, CardPredicates.isController(ai)); + if (ownChoices.isEmpty()) { + ownChoices = CardLists.filter(options, CardPredicates.isControlledByAnyOf(ai.getAllies())); + } + choice = ComputerUtilCard.getBestAI(ownChoices); } else if (logic.equals("BestBlocker")) { if (!CardLists.filter(options, Presets.UNTAPPED).isEmpty()) { options = CardLists.filter(options, Presets.UNTAPPED); @@ -191,7 +204,7 @@ public class ChooseCardAi extends SpellAbilityAi { if (combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) { return false; } - int ref = ComputerUtilAbility.getAbilitySourceName(sa).equals("Forcefield") ? 1 : 0; + int ref = ComputerUtilAbility.getAbilitySourceName(sa).equals("Forcefield") ? 1 : 0; return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > ref; } }); @@ -233,7 +246,7 @@ public class ChooseCardAi extends SpellAbilityAi { return false; } for (SpellAbility sa : c.getAllSpellAbilities()) { - if (sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.getPayCosts().hasTapCost()) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java index ff0bfc2ceaa..efd87798478 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -23,7 +24,6 @@ public class ChooseCardNameAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - Card source = sa.getHostCard(); if (sa.hasParam("AILogic")) { // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { @@ -54,13 +54,13 @@ public class ChooseCardNameAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { // TODO - there is no AILogic implemented yet - return false; + return mandatory; } /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) */ @Override - public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return ComputerUtilCard.getBestAI(options); } @@ -86,7 +86,7 @@ public class ChooseCardNameAi extends SpellAbilityAi { if (rules.getSplitType() == CardSplitType.Split) { Card copy = CardUtil.getLKICopy(card); - // for calcing i need only one split side + // for calcing i need only one split side if (isOther) { copy.getCurrentState().copyFrom(card.getState(CardStateName.RightSplit), true); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java index e086a64803e..b8699dab200 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java @@ -3,7 +3,7 @@ package forge.ai.ability; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilMana; +import forge.ai.ComputerUtilCost; import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.card.MagicColor; @@ -20,7 +20,6 @@ public class ChooseColorAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - final Card source = sa.getHostCard(); final Game game = ai.getGame(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final PhaseHandler ph = game.getPhaseHandler(); @@ -43,8 +42,7 @@ public class ChooseColorAi extends SpellAbilityAi { return false; } // Set PayX here to maximum value. - int x = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(x)); + sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai)); return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCompanionAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCompanionAi.java new file mode 100644 index 00000000000..1240d9ab54f --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCompanionAi.java @@ -0,0 +1,29 @@ +package forge.ai.ability; + +import com.google.common.collect.Lists; +import forge.ai.SpellAbilityAi; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ChooseCompanionAi extends SpellAbilityAi { + + /* (non-Javadoc) + * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) + */ + @Override + public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { + List cards = Lists.newArrayList(options); + if (cards.isEmpty()) { + return null; + } + + Collections.shuffle(cards); + return cards.get(0); + } +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java index b3ae74d5274..c2cccc1f1f0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java @@ -46,6 +46,6 @@ public class ChooseDirectionAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return canPlayAI(ai, sa); + return mandatory || canPlayAI(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java new file mode 100644 index 00000000000..e5ac8a99d6d --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java @@ -0,0 +1,36 @@ +package forge.ai.ability; + +import forge.ai.SpellAbilityAi; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.TargetRestrictions; +import forge.util.MyRandom; + +public class ChooseEvenOddAi extends SpellAbilityAi { + + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + if (!sa.hasParam("AILogic")) { + return false; + } + TargetRestrictions tgt = sa.getTargetRestrictions(); + if (tgt != null) { + sa.resetTargets(); + Player opp = aiPlayer.getWeakestOpponent(); + if (sa.canTarget(opp)) { + sa.getTargets().add(opp); + } else { + return false; + } + } + boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); + return chance; + } + + @Override + protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + return mandatory || canPlayAI(ai, sa); + } + +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java index 1f4e627bda8..c45a66bc32e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java @@ -41,6 +41,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { } } else if ("GideonBlackblade".equals(aiLogic)) { return SpecialCardAi.GideonBlackblade.consider(ai, sa); + } else if ("SoulEcho".equals(aiLogic)) { + return doTriggerAINoCost(ai, sa, true); + } else if ("Always".equals(aiLogic)) { + return true; } return false; } @@ -55,12 +59,12 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { */ @Override public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { - return canPlayAI(aiPlayer, sa); + return checkApiLogic(aiPlayer, sa); } @Override protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { - if ("CombustibleGearhulk".equals(sa.getParam("AILogic"))) { + if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) { for (final Player p : aiPlayer.getOpponents()) { if (p.canBeTargetedBy(sa)) { sa.resetTargets(); @@ -180,25 +184,25 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { Card imprinted = host.getImprintedCards().getFirst(); int dmg = imprinted.getCMC(); Player owner = imprinted.getOwner(); - + //useless cards in hand if (imprinted.getName().equals("Bridge from Below") || imprinted.getName().equals("Haakon, Stromgald Scourge")) { return allow; } - + //bad cards when are thrown from the library to the graveyard, but Yixlid can prevent that if (!player.getGame().isCardInPlay("Yixlid Jailer") && ( imprinted.getName().equals("Gaea's Blessing") || imprinted.getName().equals("Narcomoeba"))) { return allow; } - + // milling against Tamiyo is pointless if (owner.isCardInCommand("Emblem - Tamiyo, the Moon Sage")) { return allow; } - + // milling a land against Gitrog result in card draw if (imprinted.isLand() && owner.isCardInPlay("The Gitrog Monster")) { // try to mill owner @@ -207,19 +211,19 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { } return allow; } - + // milling a creature against Sidisi result in more creatures if (imprinted.isCreature() && owner.isCardInPlay("Sidisi, Brood Tyrant")) { return allow; } - //if Iona does prevent from casting, allow it to draw + //if Iona does prevent from casting, allow it to draw for (final Card io : player.getCardsIn(ZoneType.Battlefield, "Iona, Shield of Emeria")) { if (CardUtil.getColors(imprinted).hasAnyColor(MagicColor.fromName(io.getChosenColor()))) { return allow; } } - + if (dmg == 0) { // If CMC = 0, mill it! return deny; @@ -244,7 +248,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { SpellAbility counterSA = spells.get(0), tokenSA = spells.get(1); // check for something which might prevent the counters to be placed on host - if (!host.canReceiveCounters(CounterType.P1P1)) { + if (!host.canReceiveCounters(CounterEnumType.P1P1)) { return tokenSA; } @@ -256,7 +260,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { // need a copy for one with extra +1/+1 counter boost, // without causing triggers to run final Card copy = CardUtil.getLKICopy(host); - copy.setCounters(CounterType.P1P1, copy.getCounters(CounterType.P1P1) + n); + copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n); copy.setZone(host.getZone()); // if host would put into the battlefield attacking @@ -281,10 +285,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { // TODO check for trigger to turn token ETB into +1/+1 counter for host // TODO check for trigger to turn token ETB into damage or life loss for opponent // in this cases Token might be prefered even if they would not survive - final Card tokenCard = TokenAi.spawnToken(player, tokenSA, true); + final Card tokenCard = TokenAi.spawnToken(player, tokenSA); - // Token would not survive - if (tokenCard.getNetToughness() < 1) { + // Token would not survive + if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) { return counterSA; } @@ -328,6 +332,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { int bestGuessDamage = totalCMC * 3 / revealedCards.size(); return life <= bestGuessDamage ? spells.get(0) : spells.get(1); + } else if ("SoulEcho".equals(logic)) { + Player target = sa.getTargetingPlayer(); + int life = target.getLife(); + return life < 10 ? spells.get(0) : Aggregates.random(spells); } else if ("Pump".equals(logic) || "BestOption".equals(logic)) { List filtered = Lists.newArrayList(); // filter first for the spells which can be done @@ -336,7 +344,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { filtered.add(sp); } } - + // TODO find better way to check if (!filtered.isEmpty()) { return filtered.get(0); @@ -344,6 +352,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { } else if ("Riot".equals(logic)) { SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1); return preferHasteForRiot(sa, player) ? hasteSA : counterSA; + } else if ("CrawlingBarrens".equals(logic)) { + return SpecialCardAi.CrawlingBarrens.considerAnimating(player, sa, spells); } return spells.get(0); // return first choice if no logic found } @@ -362,7 +372,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { game.getAction().checkStaticAbilities(false); // can't gain counters, use Haste - if (!copy.canReceiveCounters(CounterType.P1P1)) { + if (!copy.canReceiveCounters(CounterEnumType.P1P1)) { return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java b/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java index ddb90e701f1..5b98b108cda 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java @@ -9,6 +9,7 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class ChoosePlayerAi extends SpellAbilityAi { @Override @@ -27,7 +28,7 @@ public class ChoosePlayerAi extends SpellAbilityAi { } @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable choices) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable choices, Map params) { Player chosen = null; if ("Curse".equals(sa.getParam("AILogic"))) { for (Player pc : choices) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java index f7a694e0c67..7781490d425 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -59,7 +60,7 @@ public class ChooseSourceAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) { + if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) { return false; } } @@ -92,7 +93,7 @@ public class ChooseSourceAi extends SpellAbilityAi { 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; } @@ -122,11 +123,11 @@ public class ChooseSourceAi extends SpellAbilityAi { return true; } - - - + + + @Override - public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { if ("NeedsPrevention".equals(sa.getParam("AILogic"))) { final Player ai = sa.getActivatingPlayer(); final Game game = ai.getGame(); @@ -138,11 +139,11 @@ public class ChooseSourceAi extends SpellAbilityAi { } final Combat combat = game.getCombat(); - + List permanentSources = CardLists.filter(options, new Predicate() { @Override public boolean apply(final Card c) { - if (c == null || c.getZone() == null || c.getZone().getZoneType() != ZoneType.Battlefield + if (c == null || c.getZone() == null || c.getZone().getZoneType() != ZoneType.Battlefield || combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) { return false; } @@ -185,12 +186,12 @@ public class ChooseSourceAi extends SpellAbilityAi { return ComputerUtilCard.getBestAI(options); } } - + 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(), sa)) { continue; } @@ -201,9 +202,9 @@ public class ChooseSourceAi extends SpellAbilityAi { List objects = getTargets(abilityOnStack); - if (!abilityOnStack.usesTargeting() && !abilityOnStack.hasParam("Defined") && abilityOnStack.hasParam("ValidPlayers")) + 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; } @@ -214,11 +215,11 @@ public class ChooseSourceAi extends SpellAbilityAi { return source; } return null; - } + } private static List getTargets(final SpellAbility sa) { return sa.usesTargeting() && (!sa.hasParam("Defined")) - ? Lists.newArrayList(sa.getTargets().getTargets()) + ? Lists.newArrayList(sa.getTargets()) : AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java index 2f3b0c5b67a..3ca712c2c0b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java @@ -1,26 +1,20 @@ package forge.ai.ability; import com.google.common.base.Predicates; -import com.google.common.collect.Lists; -import forge.ai.AiCardMemory; -import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilMana; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.card.CardType; import forge.game.ability.AbilityUtils; -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.ability.ApiType; +import forge.game.card.*; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.Aggregates; -import java.util.List; + +import java.util.HashSet; +import java.util.Set; public class ChooseTypeAi extends SpellAbilityAi { @Override @@ -31,6 +25,8 @@ public class ChooseTypeAi extends SpellAbilityAi { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) { return doMirrorEntityLogic(aiPlayer, sa); } + } else if ("MostProminentOppControls".equals(sa.getParam("AILogic"))) { + return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty(); } return doTriggerAINoCost(aiPlayer, sa, false); @@ -44,28 +40,12 @@ public class ChooseTypeAi extends SpellAbilityAi { return false; } - CardCollectionView otb = aiPlayer.getCardsIn(ZoneType.Battlefield); - List valid = Lists.newArrayList(CardType.getAllCreatureTypes()); - - String chosenType = ComputerUtilCard.getMostProminentType(otb, valid); + String chosenType = chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)); if (chosenType.isEmpty()) { - // Account for the situation when only changelings are on the battlefield - boolean allChangeling = false; - for (Card c : otb) { - if (c.isCreature() && c.hasStartOfKeyword(Keyword.CHANGELING.toString())) { - chosenType = Aggregates.random(valid); // just choose a random type for changelings - allChangeling = true; - break; - } - } - - if (!allChangeling) { - // Still empty, probably no creatures on board - return false; - } + return false; } - - int maxX = ComputerUtilMana.determineMaxAffordableX(aiPlayer, sa); + + int maxX = ComputerUtilMana.determineLeftoverMana(sa, aiPlayer); int avgPower = 0; // predict the opposition @@ -102,7 +82,7 @@ public class ChooseTypeAi extends SpellAbilityAi { } if (maxX > avgPower && maxX > maxOppPower && maxX >= maxOppToughness) { - sa.setSVar("PayX", String.valueOf(maxX)); + sa.setXManaCostPaid(maxX); AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ANIMATED_THIS_TURN); return true; } @@ -127,4 +107,40 @@ public class ChooseTypeAi extends SpellAbilityAi { return true; } + private String chooseType(SpellAbility sa, CardCollectionView cards) { + Set valid = new HashSet<>(); + + if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.PumpAll + && sa.getSubAbility().isCurse() && sa.getSubAbility().hasParam("NumDef")) { + final SpellAbility pumpSa = sa.getSubAbility(); + final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), pumpSa.getParam("NumDef"), pumpSa); + for (Card c : cards) { + if (c.isCreature() && c.getNetToughness() <= -defense) { + valid.addAll(c.getType().getCreatureTypes()); + } + } + } else { + valid.addAll(CardType.getAllCreatureTypes()); + } + + String chosenType = ComputerUtilCard.getMostProminentType(cards, valid); + if (chosenType.isEmpty()) { + // Account for the situation when only changelings are on the battlefield + boolean allChangeling = false; + for (Card c : cards) { + if (c.isCreature() && c.hasStartOfKeyword(Keyword.CHANGELING.toString())) { + chosenType = Aggregates.random(valid); // just choose a random type for changelings + allChangeling = true; + break; + } + } + + if (!allChangeling) { + // Still empty, probably no creatures on board + return ""; + } + } + + return chosenType; + } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java index 5800ecefb32..2d8859d8a4c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.collect.Iterables; import forge.ai.ComputerUtilCard; @@ -56,7 +58,7 @@ public class ClashAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility, java.lang.Iterable) */ @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { for (Player p : options) { if (p.getCardsIn(ZoneType.Library).size() == 0) return p; @@ -82,7 +84,7 @@ public class ClashAi extends SpellAbilityAi { PlayerCollection players = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); // use chooseSinglePlayer function to the select player - Player chosen = chooseSinglePlayer(ai, sa, players); + Player chosen = chooseSinglePlayer(ai, sa, players, null); if (chosen != null) { sa.resetTargets(); sa.getTargets().add(chosen); @@ -104,7 +106,7 @@ public class ClashAi extends SpellAbilityAi { } } - return sa.getTargets().getNumTargeted() > 0; + return sa.getTargets().size() > 0; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java index 3d6870d033d..3a3de604029 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java @@ -15,6 +15,7 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class CloneAi extends SpellAbilityAi { @@ -169,7 +170,7 @@ public class CloneAi extends SpellAbilityAi { */ @Override protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, - Player targetedPlayer) { + Player targetedPlayer, Map params) { final Card host = sa.getHostCard(); final Player ctrl = host.getController(); diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java index f21b327b3fd..e93f30ff733 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java @@ -81,7 +81,12 @@ public class ControlExchangeAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); - CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), + // for TrigTwoTargets logic, only get the opponents' cards for the first target + CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ? + aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) : + aiPlayer.getGame().getCardsIn(ZoneType.Battlefield); + + CardCollection list = CardLists.getValidCards(unfilteredList, tgt.getValidTgts(), aiPlayer, sa.getHostCard(), sa); // only select the cards that can be targeted @@ -106,7 +111,51 @@ public class ControlExchangeAi extends SpellAbilityAi { // add best Target sa.getTargets().add(best); + + // second target needed (the AI's own worst) + if ("TrigTwoTargets".equals(sa.getParam("AILogic"))) { + return doTrigTwoTargetsLogic(aiPlayer, sa, best); + } + return true; } - + + private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) { + final TargetRestrictions tgt = sa.getTargetRestrictions(); + final int creatureThreshold = 100; // TODO: make this configurable from the AI profile + final int nonCreatureThreshold = 2; + + CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), + tgt.getValidTgts(), ai, sa.getHostCard(), sa); + + // only select the cards that can be targeted + list = CardLists.getTargetableCards(list, sa); + + if (list.isEmpty()) { + return false; + } + + Card aiWorst = ComputerUtilCard.getWorstAI(list); + if (aiWorst == null) { + return false; + } + + if (aiWorst != null && aiWorst != bestFirstTgt) { + if (bestFirstTgt.isCreature() && aiWorst.isCreature()) { + if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) { + sa.getTargets().add(aiWorst); + return true; + } + } else { + // TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine + if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) { + sa.getTargets().add(aiWorst); + return true; + } + } + } + + sa.clearTargets(); + return false; + } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java index 630548b00bd..2e4a66fbcef 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java @@ -18,6 +18,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; @@ -34,12 +35,12 @@ import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.phase.PhaseType; import forge.game.player.Player; +import forge.game.player.PlayerCollection; import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import forge.util.Aggregates; -import forge.util.collect.FCollectionView; //AB:GainControl|ValidTgts$Creature|TgtPrompt$Select target legendary creature|LoseControl$Untap,LoseControl|SpellDescription$Gain control of target xxxxxxx @@ -54,8 +55,6 @@ import forge.util.collect.FCollectionView; // (as a "&"-separated list; like Haste, Sacrifice CARDNAME at EOT, any standard keyword) // OppChoice - set to True if opponent chooses creature (for Preacher) - not implemented yet // Untap - set to True if target card should untap when control is taken -// DestroyTgt - actions upon which the tgt should be destroyed. same list as LoseControl -// NoRegen - set if destroyed creature can't be regenerated. used only with DestroyTgt /** *

@@ -77,7 +76,7 @@ public class ControlGainAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); final Game game = ai.getGame(); - final FCollectionView opponents = ai.getOpponents(); + final PlayerCollection opponents = ai.getOpponents(); // if Defined, then don't worry about targeting if (tgt == null) { @@ -94,18 +93,19 @@ public class ControlGainAi extends SpellAbilityAi { sa.setTargetingPlayer(targetingPlayer); return targetingPlayer.getController().chooseTargetsFor(sa); } - if (tgt.isRandomTarget()) { - sa.getTargets().add(Aggregates.random(tgt.getAllCandidates(sa, false))); - } + if (tgt.canOnlyTgtOpponent()) { - List oppList = Lists - .newArrayList(Iterables.filter(opponents, PlayerPredicates.isTargetableBy(sa))); + List oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa)); if (oppList.isEmpty()) { return false; } - sa.getTargets().add(oppList.get(0)); + if (tgt.isRandomTarget()) { + sa.getTargets().add(Aggregates.random(oppList)); + } else { + sa.getTargets().add(oppList.get(0)); + } } } @@ -197,11 +197,11 @@ public class ControlGainAi extends SpellAbilityAi { } } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) { Card t = null; if (list.isEmpty()) { - if ((sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().getNumTargeted() == 0)) { + if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) { sa.resetTargets(); return false; } else { @@ -280,7 +280,7 @@ public class ControlGainAi extends SpellAbilityAi { @Override public boolean chkAIDrawback(SpellAbility sa, final Player ai) { final Game game = ai.getGame(); - if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { + if (!sa.usesTargeting()) { if (sa.hasParam("AllValid")) { CardCollectionView tgtCards = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa); @@ -303,7 +303,7 @@ public class ControlGainAi extends SpellAbilityAi { } // pumpDrawbackAI() @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { final List cards = Lists.newArrayList(); for (Player p : options) { cards.addAll(p.getCreaturesInPlay()); diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java new file mode 100644 index 00000000000..894a6951c15 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java @@ -0,0 +1,82 @@ +/* + * 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 . + */ +package forge.ai.ability; + +import java.util.List; +import java.util.Map; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; + +import forge.ai.ComputerUtilCard; +import forge.ai.SpellAbilityAi; +import forge.game.card.Card; +import forge.game.card.CardLists; +import forge.game.card.CardPredicates; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; + + +/** + *

+ * AbilityFactory_GainControlVariant class. + *

+ * + * @author Forge + * @version $Id: AbilityFactoryGainControl.java 17764 2012-10-29 11:04:18Z Sloth $ + */ +public class ControlGainVariantAi extends SpellAbilityAi { + @Override + protected boolean canPlayAI(final Player ai, final SpellAbility sa) { + + String logic = sa.getParam("AILogic"); + + if ("GainControlOwns".equals(logic)) { + List list = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), new Predicate() { + @Override + public boolean apply(final Card crd) { + return crd.isCreature() && !crd.getController().equals(crd.getOwner()); + } + }); + if (list.isEmpty()) { + return false; + } + for (final Card c : list) { + if (ai.equals(c.getController())) { + return false; + } + } + } + + return true; + + } + + @Override + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { + Iterable otherCtrl = CardLists.filter(options, Predicates.not(CardPredicates.isController(ai))); + if (Iterables.isEmpty(otherCtrl)) { + return ComputerUtilCard.getWorstAI(options); + } else { + return ComputerUtilCard.getBestAI(otherCtrl); + } + } + +} diff --git a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java index 7ce9dfbc473..3b31616a6d4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java @@ -19,6 +19,7 @@ import forge.game.zone.ZoneType; import java.util.Collection; import java.util.List; +import java.util.Map; public class CopyPermanentAi extends SpellAbilityAi { @Override @@ -131,7 +132,7 @@ public class CopyPermanentAi extends SpellAbilityAi { // target loop while (sa.canAddMoreTarget()) { if (list.isEmpty()) { - if (!sa.isTargetNumberValid() || (sa.getTargets().getNumTargeted() == 0)) { + if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) { sa.resetTargets(); return false; } else { @@ -158,7 +159,7 @@ public class CopyPermanentAi extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (!sa.isTargetNumberValid() || (sa.getTargets().getNumTargeted() == 0)) { + if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) { sa.resetTargets(); return false; } else { @@ -204,7 +205,7 @@ public class CopyPermanentAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean) */ @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Select a card to attach to CardCollection betterOptions = getBetterOptions(ai, sa, options, isOptional); if (!betterOptions.isEmpty()) { @@ -223,7 +224,7 @@ public class CopyPermanentAi extends SpellAbilityAi { } @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { final List cards = new PlayerCollection(options).getCreaturesInPlay(); Card chosen = ComputerUtilCard.getBestCreatureAI(cards); return chosen != null ? chosen.getController() : Iterables.getFirst(options, null); diff --git a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java index d130c18a666..2483d259119 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java @@ -5,7 +5,6 @@ import forge.game.Game; import forge.game.ability.ApiType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; -import forge.game.spellability.AbilityActivated; import forge.game.spellability.Spell; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; @@ -29,8 +28,8 @@ public class CopySpellAbilityAi extends SpellAbilityAi { final SpellAbility top = game.getStack().peekAbility(); if (top != null - && top.getPayCosts() != null && top.getPayCosts().getCostMana() != null - && sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null + && top.getPayCosts().getCostMana() != null + && sa.getPayCosts().getCostMana() != null && top.getPayCosts().getCostMana().getMana().getCMC() >= sa.getPayCosts().getCostMana().getMana().getCMC() + diff) { // The copied spell has a significantly higher CMC than the copy spell, consider copying chance = 100; @@ -63,7 +62,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi { } } - if (top.isWrapper() || !(top instanceof SpellAbility || top instanceof AbilityActivated)) { + if (top.isWrapper() || !(top instanceof SpellAbility || top.isActivatedAbility())) { // Shouldn't even try with triggered or wrapped abilities at this time, will crash return false; } else if (top.getApi() == ApiType.CopySpellAbility) { @@ -91,7 +90,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi { AiPlayDecision decision = AiPlayDecision.CantPlaySa; if (top instanceof Spell) { decision = ((PlayerControllerAi) aiPlayer.getController()).getAi().canPlayFromEffectAI((Spell) topCopy, true, true); - } else if (top instanceof AbilityActivated && top.getActivatingPlayer().equals(aiPlayer) + } else if (top.isActivatedAbility() && top.getActivatingPlayer().equals(aiPlayer) && logic.contains("CopyActivatedAbilities")) { decision = AiPlayDecision.WillPlay; // FIXME: we activated it once, why not again? Or bad idea? } diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java index 41edaa0d49a..4f153415e31 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -103,8 +103,9 @@ public class CounterAi extends SpellAbilityAi { int toPay = 0; boolean setPayX = false; - if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) { + if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { setPayX = true; + // TODO use ComputerUtilCost.getMaxXValue toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1); } else { toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); @@ -123,7 +124,7 @@ public class CounterAi extends SpellAbilityAi { } if (setPayX) { - source.setSVar("PayX", Integer.toString(toPay)); + sa.setXManaCostPaid(toPay); } } @@ -267,7 +268,7 @@ public class CounterAi extends SpellAbilityAi { int toPay = 0; boolean setPayX = false; - if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) { + if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { setPayX = true; toPay = ComputerUtilMana.determineLeftoverMana(sa, ai); } else { @@ -289,7 +290,7 @@ public class CounterAi extends SpellAbilityAi { } if (setPayX) { - source.setSVar("PayX", Integer.toString(toPay)); + sa.setXManaCostPaid(toPay); } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java index 2535c29505d..ad8d6335b8a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java @@ -6,12 +6,12 @@ * 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 . */ @@ -22,11 +22,7 @@ import java.util.List; import com.google.common.base.Predicate; import forge.ai.ComputerUtilCard; -import forge.game.card.Card; -import forge.game.card.CardCollection; -import forge.game.card.CardCollectionView; -import forge.game.card.CardLists; -import forge.game.card.CounterType; +import forge.game.card.*; import forge.game.keyword.Keyword; import forge.util.Aggregates; @@ -35,7 +31,7 @@ import forge.util.Aggregates; *

* AbilityFactory_Counters class. *

- * + * * @author Forge * @version $Id$ */ @@ -46,7 +42,7 @@ public abstract class CountersAi { *

* chooseCursedTarget. *

- * + * * @param list * a {@link forge.CardList} object. * @param type @@ -77,7 +73,7 @@ public abstract class CountersAi { *

* chooseBoonTarget. *

- * + * * @param list * a {@link forge.CardList} object. * @param type @@ -97,10 +93,12 @@ public abstract class CountersAi { final CardCollection boon = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return c.getCounters(CounterType.DIVINITY) == 0; + return c.getCounters(CounterEnumType.DIVINITY) == 0; } }); choice = ComputerUtilCard.getMostExpensivePermanentAI(boon, null, false); + } else if (CounterType.get(type).isKeywordCounter()) { + choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type)); } else { // The AI really should put counters on cards that can use it. // Charge counters on things with Charge abilities, etc. Expand diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java index bf142184a33..14b22f00b1d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java @@ -42,14 +42,14 @@ public class CountersMoveAi extends SpellAbilityAi { protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { final Card host = sa.getHostCard(); final String type = sa.getParam("CounterType"); - final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type); + final CounterType cType = "Any".equals(type) ? null : CounterType.getType(type); // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { return false; } - if (CounterType.P1P1.equals(cType) && sa.hasParam("Source")) { + if (cType != null && cType.is(CounterEnumType.P1P1) && sa.hasParam("Source")) { int amount = calcAmount(sa, cType); final List srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa); if (ph.getPlayerTurn().isOpponentOf(ai)) { @@ -92,7 +92,7 @@ public class CountersMoveAi extends SpellAbilityAi { // for Simic Fluxmage and other return ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN); - } else if (CounterType.P1P1.equals(cType) && sa.hasParam("Defined")) { + } else if (cType != null && cType.is(CounterEnumType.P1P1) && sa.hasParam("Defined")) { // something like Cyptoplast Root-kin if (ph.getPlayerTurn().isOpponentOf(ai)) { if (ph.inCombat() && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) { @@ -115,6 +115,7 @@ public class CountersMoveAi extends SpellAbilityAi { protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { + sa.resetTargets(); if (!moveTgtAI(ai, sa) && !mandatory) { return false; @@ -142,7 +143,7 @@ public class CountersMoveAi extends SpellAbilityAi { final Card host = sa.getHostCard(); final String type = sa.getParam("CounterType"); - final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type); + final CounterType cType = "Any".equals(type) ? null : CounterType.getType(type); final List srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa); final List destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa); @@ -189,7 +190,7 @@ public class CountersMoveAi extends SpellAbilityAi { // check for some specific AI preferences if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) { - return cType != CounterType.P1P1 || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0; + return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0; } } // no target @@ -234,7 +235,7 @@ public class CountersMoveAi extends SpellAbilityAi { final Card host = sa.getHostCard(); final Game game = ai.getGame(); final String type = sa.getParam("CounterType"); - final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type); + final CounterType cType = "Any".equals(type) || "All".equals(type) ? null : CounterType.getType(type); List tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); @@ -278,7 +279,7 @@ public class CountersMoveAi extends SpellAbilityAi { // do not steal a P1P1 from Undying if it would die // this way - if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { + if (cType != null && cType.is(CounterEnumType.P1P1) && srcCardCpy.getNetToughness() <= 0) { return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken(); } return true; @@ -321,13 +322,13 @@ public class CountersMoveAi extends SpellAbilityAi { } // try to remove P1P1 from undying or evolve - if (CounterType.P1P1.equals(cType)) { + if (cType != null && cType.is(CounterEnumType.P1P1)) { if (card.hasKeyword(Keyword.UNDYING) || card.hasKeyword(Keyword.EVOLVE) || card.hasKeyword(Keyword.ADAPT)) { return true; } } - if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) { + if (cType != null && cType.is(CounterEnumType.M1M1) && card.hasKeyword(Keyword.PERSIST)) { return true; } @@ -382,10 +383,10 @@ public class CountersMoveAi extends SpellAbilityAi { } if (cType != null) { - if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) { + if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) { return false; } - if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) { + if (cType.is(CounterEnumType.M1M1) && card.hasKeyword(Keyword.PERSIST)) { return false; } @@ -393,7 +394,7 @@ public class CountersMoveAi extends SpellAbilityAi { return false; } } - return false; + return true; } }); @@ -452,7 +453,7 @@ public class CountersMoveAi extends SpellAbilityAi { // or for source -> multiple defined @Override protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, - Player targetedPlayer) { + Player targetedPlayer, Map params) { if (sa.hasParam("AiLogic")) { String logic = sa.getParam("AiLogic"); diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java index b20e99ace3b..05bea452b91 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java @@ -16,6 +16,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -77,7 +78,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { final CounterType counterType = getCounterType(sa); - if (!CounterType.P1P1.equals(counterType) && counterType != null) { + if (counterType != null && !counterType.is(CounterEnumType.P1P1)) { if (!sa.hasParam("ActivationPhases")) { // Don't use non P1P1/M1M1 counters before main 2 if possible if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) { @@ -147,15 +148,15 @@ public class CountersMultiplyAi extends SpellAbilityAi { if (!aiList.isEmpty()) { // counter type list to check // first loyalty, then P1P!, then Charge Counter - List typeList = Lists.newArrayList(CounterType.LOYALTY, CounterType.P1P1, CounterType.CHARGE); - for (CounterType type : typeList) { + List typeList = Lists.newArrayList(CounterEnumType.LOYALTY, CounterEnumType.P1P1, CounterEnumType.CHARGE); + for (CounterEnumType type : typeList) { // enough targets if (!sa.canAddMoreTarget()) { break; } - if (counterType == null || counterType == type) { - addTargetsByCounterType(ai, sa, aiList, type); + if (counterType == null || counterType.is(type)) { + addTargetsByCounterType(ai, sa, aiList, CounterType.get(type)); } } } @@ -164,7 +165,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { if (!oppList.isEmpty()) { // not enough targets if (sa.canAddMoreTarget()) { - final CounterType type = CounterType.M1M1; + final CounterType type = CounterType.get(CounterEnumType.M1M1); if (counterType == null || counterType == type) { addTargetsByCounterType(ai, sa, oppList, type); } @@ -172,7 +173,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { } // targeting does failed - if (!sa.isTargetNumberValid() || sa.getTargets().getNumTargeted() == 0) { + if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { sa.resetTargets(); return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java index 52538a0827c..7dd9618dafe 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java @@ -15,6 +15,7 @@ import forge.game.GameEntity; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.card.CardUtil; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -32,7 +33,7 @@ public class CountersProliferateAi extends SpellAbilityAi { for (final Player p : allies) { // player has experience or energy counter - if (p.getCounters(CounterType.EXPERIENCE) + p.getCounters(CounterType.ENERGY) >= 1) { + if (p.getCounters(CounterEnumType.EXPERIENCE) + p.getCounters(CounterEnumType.ENERGY) >= 1) { allyExpOrEnergy = true; } cperms.addAll(CardLists.filter(p.getCardsIn(ZoneType.Battlefield), new Predicate() { @@ -115,17 +116,19 @@ public class CountersProliferateAi extends SpellAbilityAi { */ @SuppressWarnings("unchecked") @Override - public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer) { + public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer, Map params) { // Proliferate is always optional for all, no need to select best + final CounterType poison = CounterType.get(CounterEnumType.POISON); + // because countertype can't be chosen anymore, only look for posion counters for (final Player p : Iterables.filter(options, Player.class)) { if (p.isOpponentOf(ai)) { - if (p.getCounters(CounterType.POISON) > 0 && p.canReceiveCounters(CounterType.POISON)) { + if (p.getCounters(poison) > 0 && p.canReceiveCounters(poison)) { return (T)p; } } else { - if (p.getCounters(CounterType.POISON) <= 5 || p.canReceiveCounters(CounterType.POISON)) { + if (p.getCounters(poison) <= 5 || p.canReceiveCounters(poison)) { return (T)p; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index d139920d0a4..331067a1770 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -35,7 +35,7 @@ public class CountersPutAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#willPayCosts(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.cost.Cost, * forge.game.card.Card) @@ -56,17 +56,17 @@ public class CountersPutAi extends SpellAbilityAi { if (part instanceof CostRemoveCounter) { final CostRemoveCounter remCounter = (CostRemoveCounter) part; final CounterType counterType = remCounter.counter; - if (counterType.name().equals(type) && !aiLogic.startsWith("MoveCounter")) { + if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) { return false; } if (!part.payCostFromSource()) { - if (counterType.equals(CounterType.P1P1)) { + if (counterType.is(CounterEnumType.P1P1)) { return false; } continue; } // don't kill the creature - if (counterType.equals(CounterType.P1P1) && source.getLethalDamage() <= 1) { + if (counterType.is(CounterEnumType.P1P1) && source.getLethalDamage() <= 1) { return false; } } @@ -77,7 +77,7 @@ public class CountersPutAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler) @@ -109,7 +109,11 @@ public class CountersPutAi extends SpellAbilityAi { } } int maxLevel = Integer.parseInt(sa.getParam("MaxLevel")); - return source.getCounters(CounterType.LEVEL) < maxLevel; + return source.getCounters(CounterEnumType.LEVEL) < maxLevel; + } + + if ("CrawlingBarrens".equals(sa.getParam("AILogic"))) { + return true; } return super.checkPhaseRestrictions(ai, sa, ph); @@ -126,7 +130,7 @@ public class CountersPutAi extends SpellAbilityAi { Card choice = null; final String type = sa.getParam("CounterType"); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final String logic = sa.getParamOrDefault("AILogic", ""); PhaseHandler ph = ai.getGame().getPhaseHandler(); @@ -146,7 +150,7 @@ public class CountersPutAi extends SpellAbilityAi { if (abTgt.canTgtPlayer()) { // try to kill opponent with Poison PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); - PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterType.POISON, 9)); + PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9)); if (!poisonList.isEmpty()) { sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife())); return true; @@ -157,13 +161,13 @@ public class CountersPutAi extends SpellAbilityAi { // try to kill creature with -1/-1 counters if it can // receive counters, execpt it has undying CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa); - CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1)); + CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1)); oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING); oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate() { @Override public boolean apply(Card input) { - return input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.M1M1); + return input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)); } }); @@ -220,8 +224,10 @@ public class CountersPutAi extends SpellAbilityAi { if ("Never".equals(logic)) { return false; + } else if ("AlwaysWithNoTgt".equals(logic)) { + return true; } else if ("AristocratCounters".equals(logic)) { - return PumpAi.doAristocratWithCountersLogic(sa, ai); + return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); } else if ("PayEnergy".equals(logic)) { return true; } else if ("PayEnergyConservatively".equals(logic)) { @@ -242,7 +248,7 @@ public class CountersPutAi extends SpellAbilityAi { int totBlkPower = Aggregates.sum(blocked, CardPredicates.Accessors.fnGetNetPower); int totBlkToughness = Aggregates.min(blocked, CardPredicates.Accessors.fnGetNetToughness); - int numActivations = ai.getCounters(CounterType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount(); + int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount(); if (sa.getHostCard().getNetToughness() + numActivations > totBlkPower || sa.getHostCard().getNetPower() + numActivations >= totBlkToughness) { return true; @@ -257,7 +263,7 @@ public class CountersPutAi extends SpellAbilityAi { AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); return true; } - } else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) { + } else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) { // outside of combat, this logic only works if the relevant AI profile option is enabled // and if there is enough energy saved if (!onlyInCombat) { @@ -286,12 +292,14 @@ public class CountersPutAi extends SpellAbilityAi { } } else if (logic.startsWith("MoveCounter")) { return doMoveCounterLogic(ai, sa, ph); + } else if (logic.equals("CrawlingBarrens")) { + return SpecialCardAi.CrawlingBarrens.consider(ai, sa); } - if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) { + if (!sa.metConditions() && sa.getSubAbility() == null) { return false; } - + if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility()); if (!prot.isEmpty()) { @@ -320,7 +328,7 @@ public class CountersPutAi extends SpellAbilityAi { Game game = ai.getGame(); Combat combat = game.getCombat(); - if (!source.canReceiveCounters(CounterType.P1P1) || source.getCounters(CounterType.P1P1) > 0) { + if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) { return false; } else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return doCombatAdaptLogic(source, amount, combat); @@ -334,16 +342,16 @@ public class CountersPutAi extends SpellAbilityAi { } return FightAi.canFightAi(ai, sa, nPump, nPump); } - + if (amountStr.equals("X")) { - if (source.getSVar(amountStr).equals("Count$xPaid")) { + if (sa.getSVar(amountStr).equals("Count$xPaid")) { // By default, set PayX here to maximum value (used for most SAs of this type). - amount = ComputerUtilMana.determineLeftoverMana(sa, ai); + amount = ComputerUtilCost.getMaxXValue(sa, ai); if (isClockwork) { // Clockwork Avian and other similar cards: do not tap all mana for X, // instead only rewind to max allowed value when the power gets low enough. - int curCtrs = source.getCounters(CounterType.P1P0); + int curCtrs = source.getCounters(CounterEnumType.P1P0); int maxCtrs = Integer.parseInt(sa.getParam("MaxFromEffect")); // This will "rewind" clockwork cards when they fall to 50% power or below, consider improving @@ -357,7 +365,7 @@ public class CountersPutAi extends SpellAbilityAi { } } - source.setSVar("PayX", Integer.toString(amount)); + sa.setXManaCostPaid(amount); } else if ("ExiledCreatureFromGraveCMC".equals(logic)) { // e.g. Necropolis amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc); @@ -394,18 +402,17 @@ public class CountersPutAi extends SpellAbilityAi { return true; } } - + if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa)) { - final TargetRestrictions abTgt = sa.getTargetRestrictions(); // only evaluates case where all tokens are placed on a single target - if (sa.usesTargeting() && abTgt.getMinTargets(source, sa) < 2) { + if (sa.usesTargeting() && sa.getMinTargets() < 2) { if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) { Card c = sa.getTargets().getFirstTargetedCard(); - if (sa.getTargets().getNumTargeted() > 1) { + if (sa.getTargets().size() > 1) { sa.resetTargets(); sa.getTargets().add(c); } - abTgt.addDividedAllocation(sa.getTargetCard(), amount); + sa.addDividedAllocation(sa.getTargetCard(), amount); return true; } else { return false; @@ -415,8 +422,8 @@ public class CountersPutAi extends SpellAbilityAi { // Targeting if (sa.usesTargeting()) { - sa.resetTargets(); - + sa.resetTargets(); + final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost); if (sa.isCurse()) { @@ -433,7 +440,7 @@ public class CountersPutAi extends SpellAbilityAi { if (sacSelf && c.equals(source)) { return false; } - return sa.canTarget(c) && c.canReceiveCounters(CounterType.valueOf(type)); + return sa.canTarget(c) && c.canReceiveCounters(CounterType.getType(type)); } }); @@ -452,16 +459,14 @@ public class CountersPutAi extends SpellAbilityAi { // but try to do it in Main 2 then so that the AI has a chance to play creatures first. if (list.isEmpty() && sa.hasParam("Planeswalker") - && sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class) && sa.isTargetNumberValid() - && sa.getTargets().getNumTargeted() == 0 + && sa.getTargets().size() == 0 && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) { return true; } if (sourceName.equals("Abzan Charm")) { - final TargetRestrictions abTgt = sa.getTargetRestrictions(); // specific AI for instant with distribute two +1/+1 counters ComputerUtilCard.sortByEvaluateCreature(list); // maximise the number of targets @@ -471,11 +476,11 @@ public class CountersPutAi extends SpellAbilityAi { if (ComputerUtilCard.shouldPumpCard(ai, sa, c, i, i, Lists.newArrayList())) { sa.getTargets().add(c); - abTgt.addDividedAllocation(c, i); + sa.addDividedAllocation(c, i); left -= i; } - if (left < i || sa.getTargets().getNumTargeted() == abTgt.getMaxTargets(source, sa)) { - abTgt.addDividedAllocation(sa.getTargets().getFirstTargetedCard(), left + i); + if (left < i || sa.getTargets().size() == sa.getMaxTargets()) { + sa.addDividedAllocation(sa.getTargets().getFirstTargetedCard(), left + i); left = 0; break; } @@ -487,11 +492,11 @@ public class CountersPutAi extends SpellAbilityAi { } return false; } - + // target loop while (sa.canAddMoreTarget()) { if (list.isEmpty()) { - if (!sa.isTargetNumberValid() || (sa.getTargets().getNumTargeted() == 0)) { + if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) { sa.resetTargets(); return false; } else { @@ -529,7 +534,7 @@ public class CountersPutAi extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (!sa.isTargetNumberValid() || sa.getTargets().getNumTargeted() == 0) { + if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { sa.resetTargets(); return false; } else { @@ -541,7 +546,7 @@ public class CountersPutAi extends SpellAbilityAi { list.remove(choice); sa.getTargets().add(choice); if (divided) { - sa.getTargetRestrictions().addDividedAllocation(choice, amount); + sa.addDividedAllocation(choice, amount); break; } choice = null; @@ -557,7 +562,7 @@ public class CountersPutAi extends SpellAbilityAi { return false; } - final int currCounters = cards.get(0).getCounters(CounterType.valueOf(type)); + final int currCounters = cards.get(0).getCounters(CounterType.get(type)); // each non +1/+1 counter on the card is a 10% chance of not // activating this ability. @@ -573,11 +578,11 @@ public class CountersPutAi extends SpellAbilityAi { } boolean immediately = ComputerUtil.playImmediately(ai, sa); - + if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) { return false; } - + if (immediately) { return true; } @@ -608,10 +613,10 @@ public class CountersPutAi extends SpellAbilityAi { final String logic = sa.getParamOrDefault("AILogic", ""); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); - final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger()) + final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger()) || (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger()); if (sa.usesTargeting()) { @@ -636,7 +641,7 @@ public class CountersPutAi extends SpellAbilityAi { if (list.isEmpty()) { if (!sa.isTargetNumberValid() - || sa.getTargets().getNumTargeted() == 0) { + || sa.getTargets().size() == 0) { sa.resetTargets(); return false; } else { @@ -660,7 +665,7 @@ public class CountersPutAi extends SpellAbilityAi { if (choice == null) { // can't find anything left if ((!sa.isTargetNumberValid()) - || (sa.getTargets().getNumTargeted() == 0)) { + || (sa.getTargets().size() == 0)) { sa.resetTargets(); return false; } else { @@ -671,7 +676,7 @@ public class CountersPutAi extends SpellAbilityAi { list.remove(choice); sa.getTargets().add(choice); if (divided) { - sa.getTargetRestrictions().addDividedAllocation(choice, amount); + sa.addDividedAllocation(choice, amount); break; } } @@ -682,29 +687,30 @@ public class CountersPutAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + final SpellAbility root = sa.getRootAbility(); final Card source = sa.getHostCard(); // boolean chance = true; boolean preferred = true; CardCollection list; final String type = sa.getParam("CounterType"); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); int left = amount; - + if (!sa.usesTargeting()) { // No target. So must be defined list = new CardCollection(AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)); - - if (amountStr.equals("X") - && !source.hasSVar("PayX") /* SubAbility on something that already had set PayX, e.g. Endless One ETB counters */ - && ((sa.hasParam(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) || source.getSVar(amountStr).equals("Count$xPaid") )) { + + if (amountStr.equals("X") + && root.getXManaCostPaid() != null /* SubAbility on something that already had set PayX, e.g. Endless One ETB counters */ + && sa.hasParam(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) { // detect if there's more than one X in the cost (Hangarback Walker, Walking Ballista, etc.) SpellAbility testSa = sa; int countX = 0; int nonXGlyphs = 0; - while (testSa != null && testSa.getPayCosts() != null && countX == 0) { + while (testSa != null && countX == 0) { countX = testSa.getPayCosts().getTotalMana().countX(); nonXGlyphs = testSa.getPayCosts().getTotalMana().getGlyphCount() - countX; testSa = testSa.getSubAbility(); @@ -716,7 +722,7 @@ public class CountersPutAi extends SpellAbilityAi { } // Spend all remaining mana to add X counters (eg. Hero of Leina Tower) - int payX = ComputerUtilMana.determineLeftoverMana(sa, ai); + int payX = ComputerUtilCost.getMaxXValue(sa, ai); // Account for the possible presence of additional glyphs in cost (e.g. Mikaeus, the Lunarch; Primordial Hydra) payX -= nonXGlyphs; @@ -724,14 +730,29 @@ public class CountersPutAi extends SpellAbilityAi { // Account for the multiple X in cost if (countX > 1) { payX /= countX; } - source.setSVar("PayX", Integer.toString(payX)); + root.setXManaCostPaid(payX); } - + if (!mandatory) { // TODO - If Trigger isn't mandatory, when wouldn't we want to // put a counter? // things like Powder Keg, which are way too complex for the AI } + } else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) { + // can only target opponent + List playerList = Lists.newArrayList(Iterables.filter( + sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class)); + + if (playerList.isEmpty() && mandatory) { + return false; + } + + // try to choose player with less creatures + Player choice = Collections.min(playerList, PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.CREATURES)); + + if (choice != null) { + sa.getTargets().add(choice); + } } else { if (sa.isCurse()) { list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); @@ -801,12 +822,11 @@ public class CountersPutAi extends SpellAbilityAi { } } if (choice != null && divided) { - final TargetRestrictions abTgt = sa.getTargetRestrictions(); int alloc = Math.max(amount / totalTargets, 1); - if (sa.getTargets().getNumTargeted() == Math.min(totalTargets, abTgt.getMaxTargets(sa.getHostCard(), sa)) - 1) { - abTgt.addDividedAllocation(choice, left); + if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) { + sa.addDividedAllocation(choice, left); } else { - abTgt.addDividedAllocation(choice, alloc); + sa.addDividedAllocation(choice, alloc); left -= alloc; } } @@ -838,7 +858,7 @@ public class CountersPutAi extends SpellAbilityAi { List threatening = CardLists.filter(creats, new Predicate() { @Override public boolean apply(Card c) { - return CombatUtil.canBlock(source, c, !isHaste) + return CombatUtil.canBlock(source, c, !isHaste) && (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword(Keyword.DEATHTOUCH)); } }); @@ -873,24 +893,28 @@ public class CountersPutAi extends SpellAbilityAi { } @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { // used by Tribute, select player with lowest Life // TODO add more logic using TributeAILogic List list = Lists.newArrayList(options); return Collections.min(list, PlayerPredicates.compareByLife()); } - + @Override - protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Bolster does use this // TODO need more or less logic there? + final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1); + final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1); // no logic if there is no options or no to choice if (!isOptional && Iterables.size(options) <= 1) { return Iterables.getFirst(options, null); } - final CounterType type = CounterType.valueOf(sa.getParam("CounterType")); + final CounterType type = params.containsKey("CounterType") ? (CounterType)params.get("CounterType") + : CounterType.getType(sa.getParam("CounterType")); + final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); @@ -907,7 +931,7 @@ public class CountersPutAi extends SpellAbilityAi { return false; if (ComputerUtilCard.isUselessCreature(ai, input)) return false; - if (CounterType.M1M1.equals(type) && amount >= input.getNetToughness()) + if (type.is(CounterEnumType.M1M1) && amount >= input.getNetToughness()) return true; return ComputerUtil.isNegativeCounter(type, input); } @@ -931,6 +955,20 @@ public class CountersPutAi extends SpellAbilityAi { CardCollection filtered = mine; + // Try to filter out keywords that we already have on cards + if (type.isKeywordCounter()) { + Keyword kw = Keyword.smartValueOf(type.getName()); + final CardCollection doNotHaveKeyword = CardLists.filter(filtered, new Predicate() { + @Override + public boolean apply(Card card) { + return !card.hasKeyword(kw) && card.canBeTargetedBy(sa) && sa.canTarget(card); + } + }); + + if (doNotHaveKeyword.size() > 0) + filtered = doNotHaveKeyword; + } + final CardCollection notUseless = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { @@ -945,26 +983,26 @@ public class CountersPutAi extends SpellAbilityAi { } // some special logic to reload Persist/Undying - if (CounterType.P1P1.equals(type)) { + if (p1p1.equals(type)) { final CardCollection persist = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { if (!input.hasKeyword(Keyword.PERSIST)) return false; - return input.getCounters(CounterType.M1M1) <= amount; + return input.getCounters(m1m1) <= amount; } }); if (!persist.isEmpty()) { filtered = persist; } - } else if (CounterType.M1M1.equals(type)) { + } else if (m1m1.equals(type)) { final CardCollection undying = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { if (!input.hasKeyword(Keyword.UNDYING)) return false; - return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount; + return input.getCounters(p1p1) <= amount && input.getNetToughness() > amount; } }); @@ -987,8 +1025,8 @@ public class CountersPutAi extends SpellAbilityAi { if (e instanceof Card) { Card c = (Card) e; if (c.getController().isOpponentOf(ai)) { - if (options.contains(CounterType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) { - return CounterType.M1M1; + if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) { + return CounterType.get(CounterEnumType.M1M1); } for (CounterType type : options) { if (ComputerUtil.isNegativeCounter(type, c)) { @@ -997,7 +1035,7 @@ public class CountersPutAi extends SpellAbilityAi { } } else { for (CounterType type : options) { - if (!ComputerUtil.isNegativeCounter(type, c) && !ComputerUtil.isUselessCounter(type)) { + if (!ComputerUtil.isNegativeCounter(type, c) && !ComputerUtil.isUselessCounter(type, c)) { return type; } } @@ -1005,12 +1043,12 @@ public class CountersPutAi extends SpellAbilityAi { } else if (e instanceof Player) { Player p = (Player) e; if (p.isOpponentOf(ai)) { - if (options.contains(CounterType.POISON)) { - return CounterType.POISON; + if (options.contains(CounterType.get(CounterEnumType.POISON))) { + return CounterType.get(CounterEnumType.POISON); } } else { - if (options.contains(CounterType.EXPERIENCE)) { - return CounterType.EXPERIENCE; + if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) { + return CounterType.get(CounterEnumType.EXPERIENCE); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java index 6142b9c71ff..87d25eaa100 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java @@ -3,7 +3,6 @@ package forge.ai.ability; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import forge.ai.ComputerUtilCost; -import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -82,10 +81,10 @@ public class CountersPutAllAi extends SpellAbilityAi { // TODO improve X value to don't overpay when extra mana won't do // anything more useful final int amount; - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. - amount = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(amount)); + amount = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(amount); } else { amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); } @@ -120,7 +119,7 @@ public class CountersPutAllAi extends SpellAbilityAi { //Check for cards that could profit from the ability PhaseHandler phase = ai.getGame().getPhaseHandler(); if (type.equals("P1P1") && sa.isAbility() && source.isCreature() - && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost() + && sa.getPayCosts().hasTapCost() && sa instanceof AbilitySub && (!phase.getNextTurn().equals(ai) || phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS))) { @@ -180,6 +179,6 @@ public class CountersPutAllAi extends SpellAbilityAi { } } - return mandatory; + return mandatory || canPlayAI(aiPlayer, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java index 4195a7924b7..70e66e15c59 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java @@ -6,12 +6,12 @@ * 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 . */ @@ -36,7 +36,7 @@ import java.util.Map; *

* AbilityFactory_PutOrRemoveCountersAi class. *

- * + * * @author Forge * @version $Id$ */ @@ -44,7 +44,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, * forge.game.spellability.SpellAbility) */ @@ -75,7 +75,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { if (sa.hasParam("CounterType")) { // currently only Jhoira's Timebug - final CounterType type = CounterType.valueOf(sa.getParam("CounterType")); + final CounterType type = CounterType.getType(sa.getParam("CounterType")); CardCollection countersList = CardLists.filter(list, CardPredicates.hasCounter(type, amount)); @@ -100,7 +100,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView depthsList = CardLists.filter(countersList, - CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterType.ICE)); + CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterEnumType.ICE)); if (!depthsList.isEmpty()) { sa.getTargets().add(depthsList.getFirst()); @@ -113,7 +113,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { CardCollection planeswalkerList = CardLists.filter( CardLists.filterControlledBy(countersList, ai.getOpponents()), CardPredicates.Presets.PLANESWALKERS, - CardPredicates.hasLessCounter(CounterType.LOYALTY, amount)); + CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount)); if (!planeswalkerList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList)); @@ -123,7 +123,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { // do as M1M1 part CardCollection aiList = CardLists.filterControlledBy(countersList, ai); - CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1)); + CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1)); CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST); if (!aiPersistList.isEmpty()) { @@ -136,7 +136,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } // do as P1P1 part - CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1)); + CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.P1P1)); CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING); if (!aiUndyingList.isEmpty()) { @@ -157,7 +157,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { if (!ComputerUtil.isNegativeCounter(aType, best)) { sa.getTargets().add(best); return true; - } else if (!ComputerUtil.isUselessCounter(aType)) { + } else if (!ComputerUtil.isUselessCounter(aType, best)) { // whould remove positive counter if (best.getCounters(aType) <= amount) { sa.getTargets().add(best); @@ -183,7 +183,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#chooseCounterType(java.util.List, * forge.game.spellability.SpellAbility, java.util.Map) */ @@ -199,18 +199,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { Card tgt = (Card) params.get("Target"); // planeswalker has high priority for loyalty counters - if (tgt.isPlaneswalker() && options.contains(CounterType.LOYALTY)) { - return CounterType.LOYALTY; + if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) { + return CounterType.get(CounterEnumType.LOYALTY); } if (tgt.getController().isOpponentOf(ai)) { // creatures with BaseToughness below or equal zero might be // killed if their counters are removed if (tgt.isCreature() && tgt.getBaseToughness() <= 0) { - if (options.contains(CounterType.P1P1)) { - return CounterType.P1P1; - } else if (options.contains(CounterType.M1M1)) { - return CounterType.M1M1; + if (options.contains(CounterType.get(CounterEnumType.P1P1))) { + return CounterType.get(CounterEnumType.P1P1); + } else if (options.contains(CounterType.get(CounterEnumType.M1M1))) { + return CounterType.get(CounterEnumType.M1M1); } } @@ -222,14 +222,14 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } } else { // this counters are treat first to be removed - if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.ICE)) { + if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { - return CounterType.ICE; + return CounterType.get(CounterEnumType.ICE); } - } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.P1P1)) { - return CounterType.P1P1; - } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.M1M1)) { - return CounterType.M1M1; + } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) { + return CounterType.get(CounterEnumType.P1P1); + } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) { + return CounterType.get(CounterEnumType.M1M1); } // fallback logic, select positive counter to add more @@ -246,7 +246,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#chooseBinary(forge.game.player.PlayerController. * BinaryChoiceType, forge.game.spellability.SpellAbility, java.util.Map) @@ -262,19 +262,19 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); if (tgt.getController().isOpponentOf(ai)) { - if (type.equals(CounterType.LOYALTY) && tgt.isPlaneswalker()) { + if (type.is(CounterEnumType.LOYALTY) && tgt.isPlaneswalker()) { return false; } return ComputerUtil.isNegativeCounter(type, tgt); } else { - if (type.equals(CounterType.ICE) && "Dark Depths".equals(tgt.getName())) { + if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { return false; } - } else if (type.equals(CounterType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) { + } else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) { return false; - } else if (type.equals(CounterType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) { + } else if (type.is(CounterEnumType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java index 24a0ff0fa9d..fe9b81d8104 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -3,7 +3,7 @@ package forge.ai.ability; import com.google.common.base.Predicates; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilMana; +import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.GameEntity; @@ -33,7 +33,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler) @@ -50,7 +50,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler, @@ -68,7 +68,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, * forge.game.spellability.SpellAbility) */ @@ -82,7 +82,7 @@ public class CountersRemoveAi extends SpellAbilityAi { } if (!type.matches("Any") && !type.matches("All")) { - final int currCounters = sa.getHostCard().getCounters(CounterType.valueOf(type)); + final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type)); if (currCounters < 1) { return false; } @@ -119,7 +119,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), - CardPredicates.hasCounter(CounterType.ICE, 3)); + CardPredicates.hasCounter(CounterEnumType.ICE, 3)); if (!depthsList.isEmpty()) { sa.getTargets().add(depthsList.getFirst()); @@ -132,7 +132,7 @@ public class CountersRemoveAi extends SpellAbilityAi { list = CardLists.filter(list, CardPredicates.isTargetableBy(sa)); CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS, - CardPredicates.hasCounter(CounterType.LOYALTY, 5)); + CardPredicates.hasCounter(CounterEnumType.LOYALTY, 5)); if (!planeswalkerList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList)); @@ -143,8 +143,8 @@ public class CountersRemoveAi extends SpellAbilityAi { // variable amount for Hex Parasite int amount; boolean xPay = false; - if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) { - final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai); + if (amountStr.equals("X") && sa.getSVar("X").equals("Count$xPaid")) { + final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai); if (manaLeft == 0) { return false; @@ -159,15 +159,15 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), - CardPredicates.hasCounter(CounterType.ICE)); + CardPredicates.hasCounter(CounterEnumType.ICE)); if (!depthsList.isEmpty()) { Card depth = depthsList.getFirst(); - int ice = depth.getCounters(CounterType.ICE); + int ice = depth.getCounters(CounterEnumType.ICE); if (amount >= ice) { sa.getTargets().add(depth); if (xPay) { - source.setSVar("PayX", Integer.toString(ice)); + sa.setXManaCostPaid(ice); } return true; } @@ -180,13 +180,13 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection planeswalkerList = CardLists.filter(list, Predicates.and(CardPredicates.Presets.PLANESWALKERS, CardPredicates.isControlledByAnyOf(ai.getOpponents())), - CardPredicates.hasLessCounter(CounterType.LOYALTY, amount)); + CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount)); if (!planeswalkerList.isEmpty()) { Card best = ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList); sa.getTargets().add(best); if (xPay) { - source.setSVar("PayX", Integer.toString(best.getCurrentLoyalty())); + sa.setXManaCostPaid(best.getCurrentLoyalty()); } return true; } @@ -196,7 +196,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // do as M1M1 part CardCollection aiList = CardLists.filterControlledBy(list, ai); - CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1)); + CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1)); CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST); if (!aiPersistList.isEmpty()) { @@ -209,7 +209,7 @@ public class CountersRemoveAi extends SpellAbilityAi { } // do as P1P1 part - CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasLessCounter(CounterType.P1P1, amount)); + CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasLessCounter(CounterEnumType.P1P1, amount)); CardCollection aiUndyingList = CardLists.getKeyword(aiP1P1List, Keyword.UNDYING); if (!aiUndyingList.isEmpty()) { @@ -220,7 +220,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // remove P1P1 counters from opposing creatures CardCollection oppP1P1List = CardLists.filter(list, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents())), - CardPredicates.hasCounter(CounterType.P1P1)); + CardPredicates.hasCounter(CounterEnumType.P1P1)); if (!oppP1P1List.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List)); return true; @@ -244,7 +244,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // no special amount for that one yet int amount = AbilityUtils.calculateAmount(source, amountStr, sa); CardCollection aiList = CardLists.filterControlledBy(list, ai); - aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount)); + aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1, amount)); CardCollection aiPersist = CardLists.getKeyword(aiList, Keyword.PERSIST); if (!aiPersist.isEmpty()) { @@ -263,7 +263,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // no special amount for that one yet int amount = AbilityUtils.calculateAmount(source, amountStr, sa); - list = CardLists.filter(list, CardPredicates.hasCounter(CounterType.P1P1, amount)); + list = CardLists.filter(list, CardPredicates.hasCounter(CounterEnumType.P1P1, amount)); // currently only logic for Bloodcrazed Hoplite, but add logic for // targeting ai creatures too @@ -297,8 +297,8 @@ public class CountersRemoveAi extends SpellAbilityAi { int amount; boolean xPay = false; // Timecrafting has X R - if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) { - final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai); + if (amountStr.equals("X") && sa.getSVar("X").equals("Count$xPaid")) { + final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai); if (manaLeft == 0) { return false; @@ -309,15 +309,15 @@ public class CountersRemoveAi extends SpellAbilityAi { amount = AbilityUtils.calculateAmount(source, amountStr, sa); } - CardCollection timeList = CardLists.filter(list, CardPredicates.hasLessCounter(CounterType.TIME, amount)); + CardCollection timeList = CardLists.filter(list, CardPredicates.hasLessCounter(CounterEnumType.TIME, amount)); if (!timeList.isEmpty()) { Card best = ComputerUtilCard.getBestAI(timeList); - int timeCount = best.getCounters(CounterType.TIME); + int timeCount = best.getCounters(CounterEnumType.TIME); sa.getTargets().add(best); if (xPay) { - source.setSVar("PayX", Integer.toString(timeCount)); + sa.setXManaCostPaid(timeCount); } return true; } @@ -335,7 +335,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection outlastCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.OUTLAST)); if (!outlastCreats.isEmpty()) { // outlast cards often benefit from having +1/+1 counters, try not to remove last one - CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterType.P1P1, 2)); + CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterEnumType.P1P1, 2)); if (!betterTargets.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets)); @@ -363,7 +363,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#chooseNumber(forge.game.player.Player, * forge.game.spellability.SpellAbility, int, int, java.util.Map) */ @@ -377,8 +377,8 @@ public class CountersRemoveAi extends SpellAbilityAi { if (targetCard.getController().isOpponentOf(player)) { return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min; } else { - if (targetCard.hasKeyword(Keyword.UNDYING) && type == CounterType.P1P1 - && targetCard.getCounters(CounterType.P1P1) >= max) { + if (targetCard.hasKeyword(Keyword.UNDYING) && type.is(CounterEnumType.P1P1) + && targetCard.getCounters(CounterEnumType.P1P1) >= max) { return max; } @@ -387,9 +387,9 @@ public class CountersRemoveAi extends SpellAbilityAi { } else if (target instanceof Player) { Player targetPlayer = (Player) target; if (targetPlayer.isOpponentOf(player)) { - return !type.equals(CounterType.POISON) ? max : min; + return !type.is(CounterEnumType.POISON) ? max : min; } else { - return type.equals(CounterType.POISON) ? max : min; + return type.is(CounterEnumType.POISON) ? max : min; } } @@ -398,7 +398,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#chooseCounterType(java.util.List, * forge.game.spellability.SpellAbility, java.util.Map) */ @@ -415,7 +415,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (targetCard.getController().isOpponentOf(ai)) { // if its a Planeswalker try to remove Loyality first if (targetCard.isPlaneswalker()) { - return CounterType.LOYALTY; + return CounterType.get(CounterEnumType.LOYALTY); } for (CounterType type : options) { if (!ComputerUtil.isNegativeCounter(type, targetCard)) { @@ -423,10 +423,10 @@ public class CountersRemoveAi extends SpellAbilityAi { } } } else { - if (options.contains(CounterType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) { - return CounterType.M1M1; - } else if (options.contains(CounterType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) { - return CounterType.P1P1; + if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) { + return CounterType.get(CounterEnumType.M1M1); + } else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) { + return CounterType.get(CounterEnumType.P1P1); } for (CounterType type : options) { if (ComputerUtil.isNegativeCounter(type, targetCard)) { @@ -438,13 +438,13 @@ public class CountersRemoveAi extends SpellAbilityAi { Player targetPlayer = (Player) target; if (targetPlayer.isOpponentOf(ai)) { for (CounterType type : options) { - if (!type.equals(CounterType.POISON)) { + if (!type.is(CounterEnumType.POISON)) { return type; } } } else { for (CounterType type : options) { - if (type.equals(CounterType.POISON)) { + if (type.is(CounterEnumType.POISON)) { return type; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java index fada6ed260e..44ebb7170c5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java @@ -66,7 +66,7 @@ public abstract class DamageAiBase extends SpellAbilityAi { if (!sa.canTarget(enemy)) { return false; } - if (sa.getTargets() != null && sa.getTargets().getTargets().contains(enemy)) { + if (sa.getTargets() != null && sa.getTargets().contains(enemy)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java index cbede1c1a11..70bf867fc18 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java @@ -6,7 +6,6 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; -import forge.game.card.CounterType; import forge.game.cost.Cost; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; @@ -39,7 +38,7 @@ public class DamageAllAi extends SpellAbilityAi { if (!ai.getGame().getStack().isEmpty()) { return false; } - + int x = -1; final String damage = sa.getParam("NumDmg"); int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); @@ -47,13 +46,9 @@ public class DamageAllAi extends SpellAbilityAi { dmg = ComputerUtilMana.getConvergeCount(sa, ai); } if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) { - x = ComputerUtilMana.determineLeftoverMana(sa, ai); - } - if (damage.equals("ChosenX")) { - x = source.getCounters(CounterType.LOYALTY); + x = ComputerUtilCost.getMaxXValue(sa, ai); } if (x == -1) { - Player bestOpp = determineOppToKill(ai, sa, source, dmg); if (determineOppToKill(ai, sa, source, dmg) != null) { // we already know we can kill a player, so go for it return true; @@ -84,10 +79,7 @@ public class DamageAllAi extends SpellAbilityAi { if (best_x > 0) { if (sa.getSVar(damage).equals("Count$xPaid")) { - source.setSVar("PayX", Integer.toString(best_x)); - } - if (damage.equals("ChosenX")) { - source.setSVar("ChosenX", "Number$" + best_x); + sa.setXManaCostPaid(best_x); } return true; } @@ -138,7 +130,7 @@ public class DamageAllAi extends SpellAbilityAi { } int minGain = 200; // The minimum gain in destroyed creatures - if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) { + if (sa.getPayCosts().isReusuableResource()) { if (computerList.isEmpty()) { minGain = 10; // nothing to lose // no creatures to lose and player can be damaged @@ -199,12 +191,13 @@ public class DamageAllAi extends SpellAbilityAi { String validP = ""; final String damage = sa.getParam("NumDmg"); - int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); - + int dmg; if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) { // Set PayX here to maximum value. - dmg = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(dmg)); + dmg = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(dmg); + } else { + dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); } if (sa.hasParam("ValidPlayers")) { @@ -281,12 +274,14 @@ public class DamageAllAi extends SpellAbilityAi { String validP = ""; final String damage = sa.getParam("NumDmg"); - int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); + int dmg; if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) { // Set PayX here to maximum value. - dmg = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(dmg)); + dmg = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(dmg); + } else { + dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); } if (sa.hasParam("ValidPlayers")) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index bd5371cbb75..471cf90696f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.ai.*; @@ -11,7 +12,6 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.cost.Cost; -import forge.game.cost.CostPart; import forge.game.cost.CostPartMana; import forge.game.cost.CostRemoveCounter; import forge.game.keyword.Keyword; @@ -45,9 +45,9 @@ public class DamageDealAi extends DamageAiBase { if ("MadSarkhanDigDmg".equals(logic)) { return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa); } - + if (damage.equals("X") && sa.getSVar(damage).equals("Count$ChosenNumber")) { - int energy = ai.getCounters(CounterType.ENERGY); + int energy = ai.getCounters(CounterEnumType.ENERGY); for (SpellAbility s : source.getSpellAbilities()) { if ("PayEnergy".equals(s.getParam("AILogic"))) { energy += AbilityUtils.calculateAmount(source, s.getParam("CounterNum"), sa); @@ -74,8 +74,8 @@ public class DamageDealAi extends DamageAiBase { } // Set PayX here to maximum value. - dmg = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(dmg)); + dmg = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(dmg); } else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.isInZone(ZoneType.Hand)) { dmg--; // the card will be spent casting the spell, so actual damage is 1 less } @@ -95,7 +95,7 @@ public class DamageDealAi extends DamageAiBase { if (damage.equals("X")) { if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) { - dmg = ComputerUtilMana.determineLeftoverMana(sa, ai); + dmg = ComputerUtilCost.getMaxXValue(sa, ai); // Try not to waste spells like Blaze or Fireball on early targets, try to do more damage with them if possible if (ai.getController().isAI()) { @@ -112,7 +112,7 @@ public class DamageDealAi extends DamageAiBase { } // Set PayX here to maximum value. It will be adjusted later depending on the target. - source.setSVar("PayX", Integer.toString(dmg)); + sa.setXManaCostPaid(dmg); } else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) { dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less } else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) { @@ -144,7 +144,7 @@ public class DamageDealAi extends DamageAiBase { if (sourceName.equals("Crater's Claws") && ai.hasFerocious()) { dmg += 2; } - + String logic = sa.getParamOrDefault("AILogic", ""); if ("DiscardLands".equals(logic)) { dmg = 2; @@ -164,7 +164,7 @@ public class DamageDealAi extends DamageAiBase { List wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source); dmg = Aggregates.sum(wolves, CardPredicates.Accessors.fnGetNetPower); } else if ("Triskelion".equals(logic)) { - final int n = source.getCounters(CounterType.P1P1); + final int n = source.getCounters(CounterEnumType.P1P1); if (n > 0) { if (ComputerUtil.playImmediately(ai, sa)) { /* @@ -195,16 +195,16 @@ public class DamageDealAi extends DamageAiBase { } return false; } - + if (sourceName.equals("Sorin, Grim Nemesis")) { - int loyalty = source.getCounters(CounterType.LOYALTY); + int loyalty = source.getCounters(CounterEnumType.LOYALTY); for (; loyalty > 0; loyalty--) { if (this.damageTargetAI(ai, sa, loyalty, false)) { dmg = ComputerUtilCombat.getEnoughDamageToKill(sa.getTargetCard(), loyalty, source, false, false); if (dmg > loyalty || dmg < 1) { continue; // in case the calculation gets messed up somewhere } - source.setSVar("ChosenX", "Number$" + dmg); + sa.setXManaCostPaid(dmg); return true; } } @@ -224,16 +224,16 @@ public class DamageDealAi extends DamageAiBase { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) { + if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) { return false; } - + if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source)) { return false; } if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return false; } // Try to chain damage/debuff effects @@ -265,10 +265,10 @@ public class DamageDealAi extends DamageAiBase { } } - if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) || + if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) || sourceName.equals("Crater's Claws")){ // If I can kill my target by paying less mana, do it - if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.hasParam("DividedAsYouChoose")) { + if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.isDividedAsYouChoose()) { int actualPay = dmg; final boolean noPrevention = sa.hasParam("NoPrevention"); for (final Card c : sa.getTargets().getTargetCards()) { @@ -280,25 +280,13 @@ public class DamageDealAi extends DamageAiBase { if (sourceName.equals("Crater's Claws") && ai.hasFerocious()) { actualPay = actualPay > 2 ? actualPay - 2 : 0; } - source.setSVar("PayX", Integer.toString(actualPay)); - } - } - - if ("XCountersDamage".equals(logic) && sa.getPayCosts() != null) { - // Check to ensure that we have enough counters to remove per the defined PayX - for (CostPart part : sa.getPayCosts().getCostParts()) { - if (part instanceof CostRemoveCounter) { - if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(source.getSVar("PayX"))) { - return false; - } - break; - } + sa.setXManaCostPaid(actualPay); } } if ("DiscardCMCX".equals(sa.getParam("AILogic"))) { - final int CMC = Integer.parseInt(source.getSVar("PayX")); - return !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(CMC)).isEmpty(); + final int cmc = sa.getXManaCostPaid(); + return !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc)).isEmpty(); } return true; @@ -308,7 +296,7 @@ public class DamageDealAi extends DamageAiBase { *

* dealDamageChooseTgtC. *

- * + * * @param d * a int. * @param noPrevention @@ -444,11 +432,11 @@ public class DamageDealAi extends DamageAiBase { // As of right now, ranks planeswalkers by their Current Loyalty * 10 + Big buff if close to "Ultimate" int bestScore = 0; for (Card pw : pws) { - int curLoyalty = pw.getCounters(CounterType.LOYALTY); + int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY); int pwScore = curLoyalty * 10; for (SpellAbility sa : pw.getSpellAbilities()) { - if (sa.hasParam("Ultimate") && sa.getPayCosts() != null) { + if (sa.hasParam("Ultimate")) { Integer loyaltyCost = 0; CostRemoveCounter remLoyalty = sa.getPayCosts().getCostPartByType(CostRemoveCounter.class); if (remLoyalty != null) { @@ -472,6 +460,22 @@ public class DamageDealAi extends DamageAiBase { return bestTgt; } + private Card getWorstPlaneswalkerToDamage(final List pws) { + Card bestTgt = null; + + int bestScore = Integer.MAX_VALUE; + for (Card pw : pws) { + int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY); + + if (curLoyalty < bestScore) { + bestScore = curLoyalty; + bestTgt = pw; + } + } + + return bestTgt; + } + private List getTargetableCards(Player ai, SpellAbility sa, Player pl, TargetRestrictions tgt, Player activator, Card source, Game game) { List hPlay = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), activator, source, sa); @@ -480,7 +484,7 @@ public class DamageDealAi extends DamageAiBase { hPlay = CardLists.filterControlledBy(hPlay, pl); } - final List objects = Lists.newArrayList(sa.getTargets().getTargets()); + final List objects = Lists.newArrayList(sa.getTargets()); if (sa.hasParam("TargetUnique")) { objects.addAll(sa.getUniqueTargets()); } @@ -498,7 +502,7 @@ public class DamageDealAi extends DamageAiBase { *

* damageTargetAI. *

- * + * * @param saMe * a {@link forge.game.spellability.SpellAbility} object. * @param dmg @@ -508,8 +512,8 @@ public class DamageDealAi extends DamageAiBase { private boolean damageTargetAI(final Player ai, final SpellAbility saMe, final int dmg, final boolean immediately) { final TargetRestrictions tgt = saMe.getTargetRestrictions(); if ("Atarka's Command".equals(ComputerUtilAbility.getAbilitySourceName(saMe))) { - // playReusable in damageChooseNontargeted wrongly assumes that CharmEffect options are re-usable - return this.shouldTgtP(ai, saMe, dmg, false); + // playReusable in damageChooseNontargeted wrongly assumes that CharmEffect options are re-usable + return this.shouldTgtP(ai, saMe, dmg, false); } if (tgt == null) { return this.damageChooseNontargeted(ai, saMe, dmg); @@ -526,7 +530,7 @@ public class DamageDealAi extends DamageAiBase { *

* damageChoosingTargets. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @param tgt @@ -543,7 +547,7 @@ public class DamageDealAi extends DamageAiBase { final boolean noPrevention = sa.hasParam("NoPrevention"); final Game game = source.getGame(); final PhaseHandler phase = game.getPhaseHandler(); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer"); final String logic = sa.getParamOrDefault("AILogic", ""); @@ -556,6 +560,13 @@ public class DamageDealAi extends DamageAiBase { sa.getTargets().add(enemy); } return true; + } else if ("DamageAfterPutCounter".equals(logic) + && sa.getParent() != null + && "P1P1".equals(sa.getParent().getParam("CounterType"))) { + // assuming the SA parent is of PutCounter type. Perhaps it's possible to predict counter multipliers here somehow? + final String amountStr = sa.getParent().getParamOrDefault("CounterNum", "1"); + final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); + dmg += amount; } // AssumeAtLeastOneTarget is used for cards with funky targeting implementation like Fight with Fire which would @@ -563,10 +574,13 @@ public class DamageDealAi extends DamageAiBase { if (tgt.getMaxTargets(source, sa) <= 0 && !logic.equals("AssumeAtLeastOneTarget")) { return false; } - + immediately |= ComputerUtil.playImmediately(ai, sa); - sa.resetTargets(); + if (!(sa.getParent() != null && sa.getParent().isTargetNumberValid())) { + sa.resetTargets(); + } + // target loop TargetChoices tcs = sa.getTargets(); @@ -596,10 +610,10 @@ public class DamageDealAi extends DamageAiBase { continue; } final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(humanCreature, dmg, source, false, noPrevention); - if (assignedDamage <= dmg + if (assignedDamage <= dmg && humanCreature.getShieldCount() == 0 && !ComputerUtil.canRegenerate(humanCreature.getController(), humanCreature)) { tcs.add(humanCreature); - tgt.addDividedAllocation(humanCreature, assignedDamage); + sa.addDividedAllocation(humanCreature, assignedDamage); lastTgt = humanCreature; dmg -= assignedDamage; } @@ -611,7 +625,7 @@ public class DamageDealAi extends DamageAiBase { } } if (dmg > 0 && lastTgt != null) { - tgt.addDividedAllocation(lastTgt, tgt.getDividedValue(lastTgt) + dmg); + sa.addDividedAllocation(lastTgt, sa.getDividedValue(lastTgt) + dmg); dmg = 0; return true; } @@ -621,20 +635,20 @@ public class DamageDealAi extends DamageAiBase { continue; } tcs.add(humanCreature); - tgt.addDividedAllocation(humanCreature, dmg); + sa.addDividedAllocation(humanCreature, dmg); dmg = 0; return true; } } int totalTargetedSoFar = -1; - while (tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) { - if (totalTargetedSoFar == tcs.getNumTargeted()) { + while (sa.canAddMoreTarget()) { + if (totalTargetedSoFar == tcs.size()) { // Avoid looping endlessly when choosing targets for cards with variable target number and type // like Jaya's Immolating Inferno break; } - totalTargetedSoFar = tcs.getNumTargeted(); + totalTargetedSoFar = tcs.size(); if (oppTargetsChoice && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) { // canPlayAI (sa activated by ai) Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); @@ -650,7 +664,7 @@ public class DamageDealAi extends DamageAiBase { if (divided) { int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention); assignedDamage = Math.min(dmg, assignedDamage); - tgt.addDividedAllocation(c, assignedDamage); + sa.addDividedAllocation(c, assignedDamage); dmg = dmg - assignedDamage; if (dmg <= 0) { break; @@ -666,13 +680,13 @@ public class DamageDealAi extends DamageAiBase { if (this.shouldTgtP(ai, sa, dmg, noPrevention)) { tcs.add(enemy); if (divided) { - tgt.addDividedAllocation(enemy, dmg); + sa.addDividedAllocation(enemy, dmg); break; } continue; } if ("RoundedDown".equals(sa.getParam("DivideEvenly"))) { - dmg = dmg * sa.getTargets().getNumTargeted() / (sa.getTargets().getNumTargeted() +1); + dmg = dmg * sa.getTargets().size() / (sa.getTargets().size() +1); } // look for creature targets; currently also catches planeswalkers that can be killed immediately @@ -688,7 +702,7 @@ public class DamageDealAi extends DamageAiBase { if (divided) { final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention); if (assignedDamage <= dmg) { - tgt.addDividedAllocation(c, assignedDamage); + sa.addDividedAllocation(c, assignedDamage); } dmg = dmg - assignedDamage; if (dmg <= 0) { @@ -708,7 +722,7 @@ public class DamageDealAi extends DamageAiBase { final Cost abCost = sa.getPayCosts(); boolean freePing = immediately || abCost == null - || sa.getTargets().getNumTargeted() > 0; + || sa.getTargets().size() > 0; if (!source.isSpell()) { if (phase.is(PhaseType.END_OF_TURN) && sa.isAbility() && abCost.isReusuableResource()) { @@ -725,11 +739,11 @@ public class DamageDealAi extends DamageAiBase { if (freePing && sa.canTarget(enemy) && (!avoidTargetP(ai, sa))) { tcs.add(enemy); if (divided) { - tgt.addDividedAllocation(enemy, dmg); + sa.addDividedAllocation(enemy, dmg); break; } } - + } else if (tgt.canTgtCreature() || tgt.canTgtPlaneswalker()) { final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, mandatory); if (c != null) { @@ -743,9 +757,9 @@ public class DamageDealAi extends DamageAiBase { if (divided) { final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention); if (assignedDamage <= dmg) { - tgt.addDividedAllocation(c, assignedDamage); + sa.addDividedAllocation(c, assignedDamage); } else { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); } dmg = dmg - assignedDamage; if (dmg <= 0) { @@ -755,37 +769,36 @@ public class DamageDealAi extends DamageAiBase { continue; } } else if ("OppAtTenLife".equals(logic)) { - for (final Player p : ai.getOpponents()) { - if (sa.canTarget(p) && p.getLife() == 10 && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) { - tcs.add(p); - } - } + for (final Player p : ai.getOpponents()) { + if (sa.canTarget(p) && p.getLife() == 10 && tcs.size() < tgt.getMaxTargets(source, sa)) { + tcs.add(p); + } + } } // TODO: Improve Damage, we shouldn't just target the player just // because we can - if (sa.canTarget(enemy) && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) { + if (sa.canTarget(enemy) && tcs.size() < tgt.getMaxTargets(source, sa)) { if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai)) || (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2)) || ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai)) - || sa.getPayCosts() == null || immediately - || this.shouldTgtP(ai, sa, dmg, noPrevention)) && + || immediately || shouldTgtP(ai, sa, dmg, noPrevention)) && (!avoidTargetP(ai, sa))) { - tcs.add(enemy); + tcs.add(enemy); if (divided) { - tgt.addDividedAllocation(enemy, dmg); + sa.addDividedAllocation(enemy, dmg); break; } continue; } } // fell through all the choices, no targets left? - if (tcs.getNumTargeted() < tgt.getMinTargets(source, sa) || tcs.getNumTargeted() == 0) { + if (tcs.size() < tgt.getMinTargets(source, sa) || tcs.size() == 0) { if (!mandatory) { sa.resetTargets(); return false; } else { // If the trigger is mandatory, gotta choose my own stuff now - return this.damageChooseRequiredTargets(ai, sa, tgt, dmg, mandatory); + return this.damageChooseRequiredTargets(ai, sa, tgt, dmg); } } else { // TODO is this good enough? for up to amounts? @@ -799,8 +812,8 @@ public class DamageDealAi extends DamageAiBase { *

* damageChooseNontargeted. *

- * @param ai - * + * @param ai + * * @param saMe * a {@link forge.game.spellability.SpellAbility} object. * @param dmg @@ -855,31 +868,28 @@ public class DamageDealAi extends DamageAiBase { *

* damageChooseRequiredTargets. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @param tgt * a {@link forge.game.spellability.TargetRestrictions} object. * @param dmg * a int. - * @param mandatory - * a boolean. * @return a boolean. */ - private boolean damageChooseRequiredTargets(final Player ai, final SpellAbility sa, final TargetRestrictions tgt, final int dmg, - final boolean mandatory) { + private boolean damageChooseRequiredTargets(final Player ai, final SpellAbility sa, final TargetRestrictions tgt, final int dmg) { // this is for Triggered targets that are mandatory final boolean noPrevention = sa.hasParam("NoPrevention"); - final boolean divided = sa.hasParam("DividedAsYouChoose"); + final boolean divided = sa.isDividedAsYouChoose(); final Player opp = ai.getWeakestOpponent(); - while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { if (tgt.canTgtPlaneswalker()) { - final Card c = this.dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, ai, mandatory); + final Card c = this.dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, ai, true); if (c != null) { sa.getTargets().add(c); if (divided) { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); break; } continue; @@ -888,11 +898,11 @@ public class DamageDealAi extends DamageAiBase { // TODO: This currently also catches planeswalkers that can be killed (still necessary? Or can be removed?) if (tgt.canTgtCreature()) { - final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, ai, mandatory); + final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, ai, true); if (c != null) { sa.getTargets().add(c); if (divided) { - tgt.addDividedAllocation(c, dmg); + sa.addDividedAllocation(c, dmg); break; } continue; @@ -902,7 +912,33 @@ public class DamageDealAi extends DamageAiBase { if (sa.canTarget(opp)) { if (sa.getTargets().add(opp)) { if (divided) { - tgt.addDividedAllocation(opp, dmg); + sa.addDividedAllocation(opp, dmg); + break; + } + continue; + } + } + + // See if there's an indestructible target that can be used + CardCollection indestructible = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), + Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.Presets.PLANESWALKERS, CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE), CardPredicates.isTargetableBy(sa))); + + if (!indestructible.isEmpty()) { + Card c = ComputerUtilCard.getWorstPermanentAI(indestructible, false, false, false, false); + sa.getTargets().add(c); + if (divided) { + sa.addDividedAllocation(c, dmg); + break; + } + continue; + } + else if (tgt.canTgtPlaneswalker()) { + // Second pass for planeswalkers: choose AI's worst planeswalker + final Card c = getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.PLANESWALKERS), CardPredicates.isTargetableBy(sa))); + if (c != null) { + sa.getTargets().add(c); + if (divided) { + sa.addDividedAllocation(c, dmg); break; } continue; @@ -912,7 +948,7 @@ public class DamageDealAi extends DamageAiBase { if (sa.canTarget(ai)) { if (sa.getTargets().add(ai)) { if (divided) { - tgt.addDividedAllocation(ai, dmg); + sa.addDividedAllocation(ai, dmg); break; } continue; @@ -940,24 +976,23 @@ public class DamageDealAi extends DamageAiBase { if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) { // Set PayX here to maximum value. - dmg = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(dmg)); + dmg = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(dmg); } - final TargetRestrictions tgt = sa.getTargetRestrictions(); - if (tgt == null) { + if (!sa.usesTargeting()) { // If it's not mandatory check a few things return mandatory || this.damageChooseNontargeted(ai, sa, dmg); } else { - if (!this.damageChoosingTargets(ai, sa, tgt, dmg, mandatory, true) && !mandatory) { + if (!this.damageChoosingTargets(ai, sa, sa.getTargetRestrictions(), dmg, mandatory, true) && !mandatory) { return false; } - if (damage.equals("X") && source.getSVar(damage).equals("Count$xPaid") && !sa.hasParam("DividedAsYouChoose")) { + if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) { // If I can kill my target by paying less mana, do it int actualPay = 0; final boolean noPrevention = sa.hasParam("NoPrevention"); - + //target is a player if (!sa.getTargets().isTargetingAnyCard()) { actualPay = dmg; @@ -969,7 +1004,7 @@ public class DamageDealAi extends DamageAiBase { } } - source.setSVar("PayX", Integer.toString(actualPay)); + sa.setXManaCostPaid(actualPay); } } @@ -988,15 +1023,15 @@ public class DamageDealAi extends DamageAiBase { Player opponent = ai.getOpponents().min(PlayerPredicates.compareByLife()); - // TODO: somehow account for the possible cost reduction? + // TODO: somehow account for the possible cost reduction? int dmg = ComputerUtilMana.determineLeftoverMana(sa, ai, saTgt.getParam("XColor")); - + while (!ComputerUtilMana.canPayManaCost(sa, ai, dmg) && dmg > 0) { // TODO: ideally should never get here, currently put here as a precaution for complex mana base cases where the miscalculation might occur. Will remove later if it proves to never trigger. dmg--; System.out.println("Warning: AI could not pay mana cost for a XLifeDrain logic spell. Reducing X value to "+dmg); } - + // set the color map for black X for the purpose of Soul Burn // TODO: somehow generalize this calculation to allow other potential similar cards to function in the future if ("Soul Burn".equals(sourceName)) { @@ -1017,7 +1052,7 @@ public class DamageDealAi extends DamageAiBase { int toughness = c.getNetToughness(); boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c)); - // Currently will target creatures with toughness 3+ (or power 5+) + // Currently will target creatures with toughness 3+ (or power 5+) // and only if the creature can actually die, do not "underdrain" // unless the creature has high power if (canDie && toughness <= dmg && ((toughness == dmg && toughness >= 3) || power >= 5)) { @@ -1029,7 +1064,7 @@ public class DamageDealAi extends DamageAiBase { saTgt.resetTargets(); saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent); - source.setSVar("PayX", Integer.toString(dmg)); + sa.setXManaCostPaid(dmg); return true; } @@ -1076,8 +1111,7 @@ public class DamageDealAi extends DamageAiBase { continue; } // currently works only with cards that don't have additional costs (only mana is supported) - if (ab.getPayCosts() != null - && (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class))) { + if (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class)) { String dmgDef = "0"; if (ab.getApi() == ApiType.DealDamage) { dmgDef = ab.getParamOrDefault("NumDmg", "0"); @@ -1101,7 +1135,7 @@ public class DamageDealAi extends DamageAiBase { } // FIXME: should it also check restrictions for targeting players? - ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST; + ManaCost costSa = sa.getPayCosts().getTotalMana(); ManaCost costAb = ab.getPayCosts().getTotalMana(); // checked for null above ManaCost total = ManaCost.combine(costSa, costAb); SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false)); diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java index 2cde4f960a3..443acaadc69 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java @@ -48,7 +48,7 @@ public class DamageEachAi extends DamageAiBase { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return canPlayAI(ai, sa); + return mandatory || canPlayAI(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java index c80e1db1a93..964c427c649 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java @@ -46,7 +46,7 @@ public class DamagePreventAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) { + if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard, sa)) { return false; } @@ -139,15 +139,15 @@ public class DamagePreventAi extends SpellAbilityAi { ComputerUtilCard.sortByEvaluateCreature(combatants); for (final Card c : combatants) { - if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && tcs.getNumTargeted() < tgt.getMaxTargets(hostCard, sa)) { + if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && tcs.size() < tgt.getMaxTargets(hostCard, sa)) { tcs.add(c); chance = true; } } } } - if (tgt != null && sa.hasParam("DividedAsYouChoose") && sa.getTargets() != null && !sa.getTargets().getTargets().isEmpty()) { - tgt.addDividedAllocation(sa.getTargets().getTargets().get(0), AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); + if (sa.usesTargeting() && sa.isDividedAsYouChoose() && !sa.getTargets().isEmpty()) { + sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); } return chance; @@ -179,12 +179,11 @@ public class DamagePreventAi extends SpellAbilityAi { * @return a boolean. */ private boolean preventDamageMandatoryTarget(final Player ai, final SpellAbility sa, final boolean mandatory) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); sa.resetTargets(); // filter AIs battlefield by what I can target final Game game = ai.getGame(); CardCollectionView targetables = game.getCardsIn(ZoneType.Battlefield); - targetables = CardLists.getValidCards(targetables, tgt.getValidTgts(), ai, sa.getHostCard(), sa); + targetables = CardLists.getTargetableCards(targetables, sa); final List compTargetables = CardLists.filterControlledBy(targetables, ai); Card target = null; @@ -215,8 +214,8 @@ public class DamagePreventAi extends SpellAbilityAi { target = ComputerUtilCard.getCheapestPermanentAI(targetables, sa, true); } sa.getTargets().add(target); - if (sa.hasParam("DividedAsYouChoose")) { - tgt.addDividedAllocation(target, AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); + if (sa.isDividedAsYouChoose()) { + sa.addDividedAllocation(target, AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)); } return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java index f6f8b73f2e2..f62a7b51c25 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java @@ -34,7 +34,7 @@ public class DamagePreventAllAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) { + if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java index 37bcd7c2d33..b41018c9d35 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java @@ -50,7 +50,7 @@ public class DebuffAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(cost, source)) { + if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) { return false; } @@ -67,7 +67,7 @@ public class DebuffAi extends SpellAbilityAi { } } - if (!sa.usesTargeting() || !sa.getTargetRestrictions().doesTarget()) { + if (!sa.usesTargeting()) { List cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); @@ -93,7 +93,7 @@ public class DebuffAi extends SpellAbilityAi { @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { - if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { + if (!sa.usesTargeting()) { // TODO - copied from AF_Pump.pumpDrawbackAI() - what should be // here? } else { @@ -138,12 +138,12 @@ public class DebuffAi extends SpellAbilityAi { return mandatory && debuffMandatoryTarget(ai, sa, mandatory); } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) { Card t = null; // boolean goodt = false; if (list.isEmpty()) { - if ((sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().getNumTargeted() == 0)) { + if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) { if (mandatory) { return debuffMandatoryTarget(ai, sa, mandatory); } @@ -220,7 +220,7 @@ public class DebuffAi extends SpellAbilityAi { final CardCollection forced = CardLists.filterControlledBy(list, ai); final Card source = sa.getHostCard(); - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) { if (pref.isEmpty()) { break; } @@ -237,7 +237,7 @@ public class DebuffAi extends SpellAbilityAi { sa.getTargets().add(c); } - while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + while (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { if (forced.isEmpty()) { break; } @@ -256,7 +256,7 @@ public class DebuffAi extends SpellAbilityAi { sa.getTargets().add(c); } - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { sa.resetTargets(); return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java index c5b17785e61..bede875289d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java @@ -1,14 +1,17 @@ package forge.ai.ability; -import forge.ai.AiController; -import forge.ai.AiPlayDecision; -import forge.ai.PlayerControllerAi; -import forge.ai.SpellAbilityAi; -import forge.ai.SpellApiToAi; -import forge.game.ability.AbilityFactory; +import com.google.common.base.Predicate; +import forge.ai.*; +import forge.card.mana.ManaCost; +import forge.game.ability.ApiType; +import forge.game.card.Card; +import forge.game.card.CardLists; +import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; public class DelayedTriggerAi extends SpellAbilityAi { @@ -18,11 +21,9 @@ public class DelayedTriggerAi extends SpellAbilityAi { // TODO: improve ai return true; } - SpellAbility trigsa = null; - if (sa.hasAdditionalAbility("Execute")) { - trigsa = sa.getAdditionalAbility("Execute"); - } else { - trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute")); + SpellAbility trigsa = sa.getAdditionalAbility("Execute"); + if (trigsa == null) { + return false; } trigsa.setActivatingPlayer(ai); @@ -35,12 +36,11 @@ public class DelayedTriggerAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - SpellAbility trigsa = null; - if (sa.hasAdditionalAbility("Execute")) { - trigsa = sa.getAdditionalAbility("Execute"); - } else { - trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute")); + SpellAbility trigsa = sa.getAdditionalAbility("Execute"); + if (trigsa == null) { + return false; } + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); trigsa.setActivatingPlayer(ai); @@ -53,11 +53,95 @@ public class DelayedTriggerAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - SpellAbility trigsa = null; - if (sa.hasAdditionalAbility("Execute")) { - trigsa = sa.getAdditionalAbility("Execute"); - } else { - trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute")); + // Card-specific logic + String logic = sa.getParamOrDefault("AILogic", ""); + if (logic.equals("SpellCopy")) { + // fetch Instant or Sorcery and AI has reason to play this turn + // does not try to get itself + final ManaCost costSa = sa.getPayCosts().getTotalMana(); + final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() { + @Override + public boolean apply(final Card c) { + if (!(c.isInstant() || c.isSorcery()) || c.equals(sa.getHostCard())) { + return false; + } + for (SpellAbility ab : c.getSpellAbilities()) { + if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab)) + || ab.hasParam("AINoRecursiveCheck")) { + // prevent infinitely recursing mana ritual and other abilities with reentry + continue; + } else if ("SpellCopy".equals(ab.getParam("AILogic")) && ab.getApi() == ApiType.DelayedTrigger) { + // don't copy another copy spell, too complex for the AI + continue; + } + if (!ab.canPlay()) { + continue; + } + AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab); + // see if we can pay both for this spell and for the Effect spell we're considering + if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) { + ManaCost costAb = ab.getPayCosts().getTotalMana(); + ManaCost total = ManaCost.combine(costSa, costAb); + SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false)); + // can we pay both costs? + if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) { + return true; + } + } + } + return false; + } + }); + + if(count == 0) { + return false; + } + return true; + } else if (logic.equals("NarsetRebound")) { + // should be done in Main2, but it might broke for other cards + //if (phase.getPhase().isBefore(PhaseType.MAIN2)) { + // return false; + //} + + // fetch Instant or Sorcery without Rebound and AI has reason to play this turn + // only need count, not the list + final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() { + @Override + public boolean apply(final Card c) { + if (!(c.isInstant() || c.isSorcery()) || c.hasKeyword(Keyword.REBOUND)) { + return false; + } + for (SpellAbility ab : c.getSpellAbilities()) { + if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab)) + || ab.hasParam("AINoRecursiveCheck")) { + // prevent infinitely recursing mana ritual and other abilities with reentry + continue; + } + if (!ab.canPlay()) { + continue; + } + AiPlayDecision decision = ((PlayerControllerAi) ai.getController()).getAi().canPlaySa(ab); + if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) { + if (ComputerUtilMana.canPayManaCost(ab, ai, 0)) { + return true; + } + } + } + return false; + } + }); + + if (count == 0) { + return false; + } + + return true; + } + + // Generic logic + SpellAbility trigsa = sa.getAdditionalAbility("Execute"); + if (trigsa == null) { + return false; } trigsa.setActivatingPlayer(ai); return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index fd7632bc44d..cee64159ff9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -13,7 +13,6 @@ import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; public class DestroyAi extends SpellAbilityAi { @@ -23,89 +22,19 @@ public class DestroyAi extends SpellAbilityAi { } @Override - protected boolean canPlayAI(final Player ai, SpellAbility sa) { - // AI needs to be expanded, since this function can be pretty complex - // based on what the expected targets could be - final Cost abCost = sa.getPayCosts(); - final TargetRestrictions abTgt = sa.getTargetRestrictions(); + protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { final Card source = sa.getHostCard(); - final boolean noRegen = sa.hasParam("NoRegen"); - final String logic = sa.getParam("AILogic"); - boolean hasXCost = false; - - CardCollection list; - - if (abCost != null) { - if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) { - return false; - } - - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; - } - - if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) { - return false; - } - - hasXCost = abCost.getCostMana() != null && abCost.getCostMana().getAmountOfX() > 0; - } - - if ("AtOpponentsCombatOrAfter".equals(sa.getParam("AILogic"))) { - PhaseHandler ph = ai.getGame().getPhaseHandler(); - if (ph.getPlayerTurn() == ai || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { - return false; - } - } else if ("AtEOT".equals(sa.getParam("AILogic"))) { - PhaseHandler ph = ai.getGame().getPhaseHandler(); - if (!ph.is(PhaseType.END_OF_TURN)) { - return false; - } - } else if ("AtEOTIfNotAttacking".equals(sa.getParam("AILogic"))) { - PhaseHandler ph = ai.getGame().getPhaseHandler(); - if (!ph.is(PhaseType.END_OF_TURN) || !ai.getCreaturesAttackedThisTurn().isEmpty()) { - return false; - } - } - - if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; - } - - // Ability that's intended to destroy own useless token to trigger Grave Pacts - // should be fired at end of turn or when under attack after blocking to make opponent sac something - boolean havepact = false; - - // TODO replace it with look for a dies -> sacrifice trigger check - havepact |= ai.isCardInPlay("Grave Pact"); - havepact |= ai.isCardInPlay("Butcher of Malakir"); - havepact |= ai.isCardInPlay("Dictate of Erebos"); - if ("Pactivator".equals(logic) && havepact) { - if ((!ai.getGame().getPhaseHandler().isPlayerTurn(ai)) - && ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) || (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))) - && (ai.getOpponents().getCreaturesInPlay().size() > 0)) { - ai.getController().chooseTargetsFor(sa); - return true; - } - } - - // Targeting - if (abTgt != null) { + if (sa.usesTargeting()) { sa.resetTargets(); - if (sa.hasParam("TargetingPlayer")) { - Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); - sa.setTargetingPlayer(targetingPlayer); - return targetingPlayer.getController().chooseTargetsFor(sa); - } - if ("MadSarkhanDragon".equals(logic)) { + if ("MadSarkhanDragon".equals(aiLogic)) { return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa); - } else if (logic != null && logic.startsWith("MinLoyalty.")) { - int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1)); - if (source.getCounters(CounterType.LOYALTY) < minLoyalty) { + } else if (aiLogic.startsWith("MinLoyalty.")) { + int minLoyalty = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1)); + if (source.getCounters(CounterEnumType.LOYALTY) < minLoyalty) { return false; } - } else if ("Polymorph".equals(logic)) { - list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); + } else if ("Polymorph".equals(aiLogic)) { + CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) { return false; } @@ -124,7 +53,103 @@ public class DestroyAi extends SpellAbilityAi { } sa.getTargets().add(worst); return true; + } else if ("Pongify".equals(aiLogic)) { + return SpecialAiLogic.doPongifyLogic(ai, sa); } + } + return super.checkAiLogic(ai, sa, aiLogic); + } + + protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph, + final String logic) { + if ("AtOpponentsCombatOrAfter".equals(logic)) { + if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { + return false; + } + } else if ("AtEOT".equals(logic)) { + if (!ph.is(PhaseType.END_OF_TURN)) { + return false; + } + } else if ("AtEOTIfNotAttacking".equals(logic)) { + if (!ph.is(PhaseType.END_OF_TURN) || !ai.getCreaturesAttackedThisTurn().isEmpty()) { + return false; + } + } else if ("Pactivator".equals(logic)) { + // Ability that's intended to destroy own useless token to trigger Grave Pacts + // should be fired at end of turn or when under attack after blocking to make opponent sac something + boolean havepact = false; + + // TODO replace it with look for a dies -> sacrifice trigger check + havepact |= ai.isCardInPlay("Grave Pact"); + havepact |= ai.isCardInPlay("Butcher of Malakir"); + havepact |= ai.isCardInPlay("Dictate of Erebos"); + if (havepact) { + if ((!ph.isPlayerTurn(ai)) + && ((ph.is(PhaseType.END_OF_TURN)) || (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS))) + && (ai.getOpponents().getCreaturesInPlay().size() > 0)) { + CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); + Card worst = ComputerUtilCard.getWorstAI(list); + if (worst != null) { + sa.getTargets().add(worst); + return true; + } + return false; + } + } + } + + return true; + } + + @Override + protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + final Card source = sa.getHostCard(); + final boolean noRegen = sa.hasParam("NoRegen"); + final String logic = sa.getParam("AILogic"); + + CardCollection list; + + + + if (ComputerUtil.preventRunAwayActivations(sa)) { + return false; + } + + + // Targeting + if (sa.usesTargeting()) { + // Assume there where already enough targets chosen by AI Logic Above + if (!sa.canAddMoreTarget() && sa.isTargetNumberValid()) { + return true; + } + + // reset targets before AI Logic part + sa.resetTargets(); + int maxTargets; + + if (sa.costHasManaX()) { + // TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement. + maxTargets = ComputerUtilCost.getMaxXValue(sa, ai); + // need to set XPaid to get the right number for + sa.setXManaCostPaid(maxTargets); + // need to check for maxTargets + maxTargets = Math.min(maxTargets, sa.getMaxTargets()); + } else { + maxTargets = sa.getMaxTargets(); + } + + if (maxTargets == 0) { + // can't afford X or otherwise target anything + return false; + } + + if (sa.hasParam("TargetingPlayer")) { + Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); + sa.setTargetingPlayer(targetingPlayer); + return targetingPlayer.getController().chooseTargetsFor(sa); + } + + // AI doesn't destroy own cards if it isn't defined in AI logic list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa); if ("FatalPush".equals(logic)) { final int cmcMax = ai.hasRevolt() ? 4 : 2; @@ -161,7 +186,7 @@ public class DestroyAi extends SpellAbilityAi { return false; } //Check for undying - return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterType.P1P1) > 0); + return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterEnumType.P1P1) > 0); } }); } @@ -184,33 +209,12 @@ public class DestroyAi extends SpellAbilityAi { return false; } - int maxTargets = abTgt.getMaxTargets(sa.getHostCard(), sa); - - if (hasXCost) { - // TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement. - maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa)); - // X can't be more than the lands we have in our hand for "discard X lands"! - if ("ScorchedEarth".equals(logic)) { - int lands = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size(); - maxTargets = Math.min(maxTargets, lands); - } - } - if (sa.hasParam("AIMaxTgtsCount")) { - // Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified - // TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable? - maxTargets = Math.min(CardFactoryUtil.xCount(sa.getHostCard(), "Count$" + sa.getParam("AIMaxTgtsCount")), maxTargets); - } - - if (maxTargets == 0) { - // can't afford X or otherwise target anything - return false; - } // target loop - while (sa.getTargets().getNumTargeted() < maxTargets) { + // TODO use can add more Targets + while (sa.getTargets().size() < maxTargets) { if (list.isEmpty()) { - if ((sa.getTargets().getNumTargeted() < abTgt.getMinTargets(sa.getHostCard(), sa)) - || (sa.getTargets().getNumTargeted() == 0)) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); return false; } else { @@ -229,19 +233,6 @@ public class DestroyAi extends SpellAbilityAi { return false; } } - if ("Pongify".equals(logic)) { - final Card token = TokenAi.spawnToken(choice.getController(), sa.getSubAbility()); - if (token == null) { - return true; // becomes Terminate - } else { - if (source.getGame().getPhaseHandler().getPhase() - .isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) || // prevent surprise combatant - ComputerUtilCard.evaluateCreature(choice) < 1.5 - * ComputerUtilCard.evaluateCreature(token)) { - return false; - } - } - } } else if (CardLists.getNotType(list, "Land").isEmpty()) { choice = ComputerUtilCard.getBestLandAI(list); @@ -255,15 +246,14 @@ public class DestroyAi extends SpellAbilityAi { choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true); } //option to hold removal instead only applies for single targeted removal - if (!sa.isTrigger() && abTgt.getMaxTargets(sa.getHostCard(), sa) == 1) { - if (!ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) { + if (!sa.isTrigger() && sa.getMaxTargets() == 1) { + if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) { return false; } } if (choice == null) { // can't find anything left - if ((sa.getTargets().getNumTargeted() < abTgt.getMinTargets(sa.getHostCard(), sa)) - || (sa.getTargets().getNumTargeted() == 0)) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); return false; } else { @@ -277,6 +267,7 @@ public class DestroyAi extends SpellAbilityAi { SpellAbility sp = aura.getFirstSpellAbility(); if (sp != null && "GainControl".equals(sp.getParam("AILogic")) && aura.getController() != ai && sa.canTarget(aura)) { + list.remove(choice); choice = aura; } } @@ -306,22 +297,19 @@ public class DestroyAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); - final Card source = sa.getHostCard(); final boolean noRegen = sa.hasParam("NoRegen"); - if (tgt != null) { + if (sa.usesTargeting()) { sa.resetTargets(); CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); - list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); + + if (list.isEmpty() || list.size() < sa.getMinTargets()) { + return false; + } // Try to avoid targeting creatures that are dead on board list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa); - if (list.isEmpty() || list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) { - return false; - } - CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE); preferred = CardLists.filterControlledBy(preferred, ai.getOpponents()); if (CardLists.getNotType(preferred, "Creature").isEmpty()) { @@ -352,10 +340,9 @@ public class DestroyAi extends SpellAbilityAi { return false; } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { if (preferred.isEmpty()) { - if (sa.getTargets().getNumTargeted() == 0 - || sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (!sa.isMinTargetChosen()) { if (!mandatory) { sa.resetTargets(); return false; @@ -379,7 +366,7 @@ public class DestroyAi extends SpellAbilityAi { } } - while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { if (list.isEmpty()) { break; } else { @@ -387,7 +374,7 @@ public class DestroyAi extends SpellAbilityAi { if (CardLists.getNotType(list, "Creature").isEmpty()) { if (!sa.getUniqueTargets().isEmpty() && sa.getParent().getApi() == ApiType.Destroy && sa.getUniqueTargets().get(0) instanceof Card) { - // basic ai for Diaochan + // basic ai for Diaochan c = (Card) sa.getUniqueTargets().get(0); } else { c = ComputerUtilCard.getWorstCreatureAI(list); @@ -400,7 +387,7 @@ public class DestroyAi extends SpellAbilityAi { } } - return sa.getTargets().getNumTargeted() >= tgt.getMinTargets(sa.getHostCard(), sa); + return sa.isTargetNumberValid(); } else { return mandatory; } @@ -412,7 +399,7 @@ public class DestroyAi extends SpellAbilityAi { Player tgtPlayer = tgtLand.getController(); int oppLandsOTB = tgtPlayer.getLandsInPlay().size(); - + // AI profile-dependent properties AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); int amountNoTempoCheck = aic.getIntProperty(AiProps.STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK); @@ -435,7 +422,7 @@ public class DestroyAi extends SpellAbilityAi { // Non-basic lands are currently not ranked in any way in ComputerUtilCard#getBestLandAI, so if a non-basic land is best target, // consider killing it off unless there's too much potential tempo loss. - // TODO: actually rank non-basics in that method and then kill off the potentially dangerous (manlands, Valakut) or lucrative + // TODO: actually rank non-basics in that method and then kill off the potentially dangerous (manlands, Valakut) or lucrative // (dual/triple mana that opens access to a certain color) lands boolean nonBasicTgt = !tgtLand.isBasicLand(); @@ -447,7 +434,7 @@ public class DestroyAi extends SpellAbilityAi { boolean isHighPriority = highPriorityIfNoLandDrop && oppSkippedLandDrop; boolean timingCheck = canManaLock || canColorLock || nonBasicTgt; - boolean tempoCheck = numLandsOTB >= amountNoTempoCheck + boolean tempoCheck = numLandsOTB >= amountNoTempoCheck || ((numLandsInHand >= amountLandsInHand || isHighPriority) && ((numLandsInHand + numLandsOTB >= amountNoTimingCheck) || timingCheck)); // For Ghost Quarter, only use it if you have either more lands in play than your opponent diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java index a84aa9d8643..0748cf03489 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java @@ -1,18 +1,20 @@ package forge.ai.ability; import com.google.common.base.Predicate; - +import com.google.common.base.Predicates; import forge.ai.*; +import forge.card.MagicColor; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; +import forge.game.card.CardPredicates; +import forge.game.combat.Combat; import forge.game.cost.Cost; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; -import forge.game.combat.Combat; public class DestroyAllAi extends SpellAbilityAi { @@ -37,8 +39,7 @@ public class DestroyAllAi extends SpellAbilityAi { @Override public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { - //TODO: Check for bad outcome - return true; + return doMassRemovalLogic(aiPlayer, sa); } @Override @@ -79,10 +80,10 @@ public class DestroyAllAi extends SpellAbilityAi { valid = sa.getParam("ValidCards"); } - if (valid.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); valid = valid.replace("X", Integer.toString(xPay)); } @@ -107,6 +108,14 @@ public class DestroyAllAi extends SpellAbilityAi { } } + // Special handling for Raiding Party + if (logic.equals("RaidingParty")) { + int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size()); + int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size()); + + return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave); + } + // If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what! if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DigAi.java b/forge-ai/src/main/java/forge/ai/ability/DigAi.java index b8b13d18fd3..43eaf8a28ad 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.base.Predicate; import com.google.common.collect.Iterables; @@ -13,7 +15,6 @@ import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; -import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.TextUtil; @@ -30,6 +31,10 @@ public class DigAi extends SpellAbilityAi { final Card host = sa.getHostCard(); Player libraryOwner = ai; + if (!willPayCosts(ai, sa, sa.getPayCosts(), host)) { + return false; + } + if (sa.usesTargeting()) { sa.resetTargets(); if (!opp.canBeTargetedBy(sa)) { @@ -69,9 +74,10 @@ public class DigAi extends SpellAbilityAi { final String num = sa.getParam("DigNum"); final boolean payXLogic = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("PayX"); - if (num != null && (num.equals("X") && host.getSVar(num).equals("Count$xPaid")) || payXLogic) { + if (num != null && (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) || payXLogic) { // By default, set PayX here to maximum value. - if (!(sa instanceof AbilitySub) || host.getSVar("PayX").equals("")) { + SpellAbility root = sa.getRootAbility(); + if (root.getXManaCostPaid() == null) { int manaToSave = 0; // Special logic that asks the AI to conserve a certain amount of mana when paying X @@ -79,11 +85,11 @@ public class DigAi extends SpellAbilityAi { manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]); } - int numCards = ComputerUtilMana.determineLeftoverMana(sa, ai) - manaToSave; + int numCards = ComputerUtilCost.getMaxXValue(sa, ai) - manaToSave; if (numCards <= 0) { return false; } - host.setSVar("PayX", Integer.toString(numCards)); + root.setXManaCostPaid(numCards); } } @@ -108,6 +114,7 @@ public class DigAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + final SpellAbility root = sa.getRootAbility(); final Player opp = ai.getWeakestOpponent(); if (sa.usesTargeting()) { sa.resetTargets(); @@ -121,18 +128,18 @@ public class DigAi extends SpellAbilityAi { // Triggers that ask to pay {X} (e.g. Depala, Pilot Exemplar). if (sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("PayXButSaveMana")) { int manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]); - int numCards = ComputerUtilMana.determineLeftoverMana(sa, ai) - manaToSave; + int numCards = ComputerUtilCost.getMaxXValue(sa, ai) - manaToSave; if (numCards <= 0) { return mandatory; } - sa.getHostCard().setSVar("PayX", Integer.toString(numCards)); + root.setXManaCostPaid(numCards); } return true; } @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable valid, boolean isOptional, Player relatedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable valid, boolean isOptional, Player relatedPlayer, Map params) { if ("DigForCreature".equals(sa.getParam("AILogic"))) { Card bestChoice = ComputerUtilCard.getBestCreatureAI(valid); if (bestChoice == null) { @@ -163,7 +170,7 @@ public class DigAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List) */ @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { // an opponent choose a card from return Iterables.getFirst(options, null); } diff --git a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java index f18dc3c7784..9900061c522 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java @@ -1,6 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilMana; +import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.card.Card; import forge.game.card.CardLists; @@ -8,7 +8,6 @@ import forge.game.card.CardPredicates; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; -import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.MyRandom; @@ -76,14 +75,15 @@ public class DigUntilAi extends SpellAbilityAi { } final String num = sa.getParam("Amount"); - if ((num != null) && num.equals("X") && source.getSVar(num).equals("Count$xPaid")) { + if ((num != null) && num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) { // Set PayX here to maximum value. - if (!(sa instanceof AbilitySub) || source.getSVar("PayX").equals("")) { - int numCards = ComputerUtilMana.determineLeftoverMana(sa, ai); + SpellAbility root = sa.getRootAbility(); + if (root.getXManaCostPaid() == null) { + int numCards = ComputerUtilCost.getMaxXValue(sa, ai); if (numCards <= 0) { return false; } - source.setSVar("PayX", Integer.toString(numCards)); + root.setXManaCostPaid(numCards); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java index d7dad6b293d..3cf639917e3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -41,7 +41,7 @@ public class DiscardAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) { + if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) { return false; } @@ -85,14 +85,14 @@ public class DiscardAi extends SpellAbilityAi { } if (sa.hasParam("NumCards")) { - if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) { + if (sa.getParam("NumCards").equals("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getWeakestOpponent() + final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai), ai.getWeakestOpponent() .getCardsIn(ZoneType.Hand).size()); if (cardsToDiscard < 1) { return false; } - source.setSVar("PayX", Integer.toString(cardsToDiscard)); + sa.setXManaCostPaid(cardsToDiscard); } else { if (AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa) < 1) { return false; @@ -130,12 +130,20 @@ public class DiscardAi extends SpellAbilityAi { } } - // Don't use draw abilities before main 2 if possible + // Don't use discard abilities before main 2 if possible if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) - && !sa.hasParam("ActivationPhases")) { + && !sa.hasParam("ActivationPhases") && !aiLogic.startsWith("AnyPhase")) { return false; } + if (aiLogic.equals("AnyPhaseIfFavored")) { + if (ai.getGame().getCombat() != null) { + if (ai.getCardsIn(ZoneType.Hand).size() < ai.getGame().getCombat().getDefenderPlayerByAttacker(source).getCardsIn(ZoneType.Hand).size()) { + return false; + } + } + } + // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { return false; @@ -191,11 +199,11 @@ public class DiscardAi extends SpellAbilityAi { } } } - if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) { + if ("X".equals(sa.getParam("RevealNumber")) && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getWeakestOpponent() + final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai), ai.getWeakestOpponent() .getCardsIn(ZoneType.Hand).size()); - sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard)); + sa.setXManaCostPaid(cardsToDiscard); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java index 355361bfda7..b48bc62b42e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -23,8 +23,7 @@ import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; -import forge.game.card.CardLists; -import forge.game.card.CardPredicates; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.cost.*; import forge.game.phase.PhaseHandler; @@ -179,8 +178,7 @@ public class DrawAi extends SpellAbilityAi { int numHand = ai.getCardsIn(ZoneType.Hand).size(); if ("Jace, Vryn's Prodigy".equals(sourceName) && ai.getCardsIn(ZoneType.Graveyard).size() > 3) { - return CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANESWALKERS, - CardPredicates.isType("Jace")).size() <= 0; + return !ai.isCardInPlay("Jace, Telepath Unbound"); } if (source.isSpell() && ai.getCardsIn(ZoneType.Hand).contains(source)) { numHand--; // remember to count looter card if it is a spell in hand @@ -221,6 +219,9 @@ public class DrawAi extends SpellAbilityAi { final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size(); final int computerMaxHandSize = ai.getMaxHandSize(); + final SpellAbility root = sa.getRootAbility(); + + final SpellAbility gainLife = sa.findSubAbilityByType(ApiType.GainLife); final SpellAbility loseLife = sa.findSubAbilityByType(ApiType.LoseLife); final SpellAbility getPoison = sa.findSubAbilityByType(ApiType.Poison); @@ -237,32 +238,18 @@ public class DrawAi extends SpellAbilityAi { boolean xPaid = false; final String num = sa.getParam("NumCards"); if (num != null && num.equals("X")) { - if (source.getSVar(num).equals("Count$xPaid")) { + if (sa.getSVar(num).equals("Count$xPaid")) { // Set PayX here to maximum value. - if (drawback && !source.getSVar("PayX").equals("")) { - numCards = Integer.parseInt(source.getSVar("PayX")); + if (drawback && root.getXManaCostPaid() != null) { + numCards = root.getXManaCostPaid(); } else { - numCards = ComputerUtilMana.determineLeftoverMana(sa, ai); + numCards = ComputerUtilCost.getMaxXValue(sa, ai); // try not to overdraw int safeDraw = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3); if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent numCards = Math.min(numCards, safeDraw); - source.setSVar("PayX", Integer.toString(numCards)); - assumeSafeX = true; - } - xPaid = true; - } - if (sa.getSVar(num).equals("Count$Converge")) { - numCards = ComputerUtilMana.getConvergeCount(sa, ai); - } - } - if (num != null && num.equals("ChosenX")) { - if (sa.getSVar("X").equals("XChoice")) { - // Draw up to max hand size but leave at least 3 in library - numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3); - - if (sa.getPayCosts() != null) { + // assuming CostPayLife is the one with X if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) { // [Necrologia, Pay X Life : Draw X Cards] // Don't draw more than what's "safe" and don't risk a near death experience @@ -270,19 +257,14 @@ public class DrawAi extends SpellAbilityAi { while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) { numCards--; } - } else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { - // [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards] - // TODO: Add special logic to limit/otherwise modify the ChosenX value here - - // Skip this ability if nothing is to be chosen for sacrifice - if (numCards <= 0) { - return false; - } } - } - sa.setSVar("ChosenX", Integer.toString(numCards)); - source.setSVar("ChosenX", Integer.toString(numCards)); + root.setXManaCostPaid(numCards); + assumeSafeX = true; + } + xPaid = true; + } else if (sa.getSVar(num).equals("Count$Converge")) { + numCards = ComputerUtilMana.getConvergeCount(sa, ai); } } @@ -341,16 +323,30 @@ public class DrawAi extends SpellAbilityAi { // for drawing and losing life if (numCards >= oppA.getLife()) { if (xPaid) { - source.setSVar("PayX", Integer.toString(oppA.getLife())); + root.setXManaCostPaid(oppA.getLife()); } sa.getTargets().add(oppA); return true; } } } + + // that opponent can gain life and also lose life and that life gain is negative + if (gainLife != null && oppA.canGainLife() && oppA.canLoseLife() && ComputerUtil.lifegainNegative(oppA, source)) { + if (gainLife.hasParam("Defined") && "Targeted".equals(gainLife.getParam("Defined"))) { + if (numCards >= oppA.getLife()) { + if (xPaid) { + root.setXManaCostPaid(oppA.getLife()); + } + sa.getTargets().add(oppA); + return true; + } + } + } + // try to make opponent lose to poison // currently only Caress of Phyrexia - if (getPoison != null && oppA.canReceiveCounters(CounterType.POISON)) { + if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { if (oppA.getPoisonCounters() + numCards > 9) { sa.getTargets().add(oppA); return true; @@ -394,14 +390,14 @@ public class DrawAi extends SpellAbilityAi { } } - if (getPoison != null && ai.canReceiveCounters(CounterType.POISON)) { + if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { if (numCards + ai.getPoisonCounters() >= 8) { aiTarget = false; } } if (xPaid) { - source.setSVar("PayX", Integer.toString(numCards)); + root.setXManaCostPaid(numCards); } } @@ -412,7 +408,7 @@ public class DrawAi extends SpellAbilityAi { if (sa.getHostCard().isInZone(ZoneType.Hand)) { numCards++; // the card will be spent } - source.setSVar("PayX", Integer.toString(numCards)); + root.setXManaCostPaid(numCards); } else { // Don't draw too many cards and then risk discarding // cards at EOT @@ -453,7 +449,7 @@ public class DrawAi extends SpellAbilityAi { } // ally would lose because of poison - if (getPoison != null && ally.canReceiveCounters(CounterType.POISON)) { + if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { if (ally.getPoisonCounters() + numCards > 9) { continue; } @@ -514,6 +510,10 @@ public class DrawAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (!mandatory && !willPayCosts(ai, sa, sa.getPayCosts(), sa.getHostCard())) { + return false; + } + return targetAI(ai, sa, mandatory); } diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java index 8697b42b146..16999292f12 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -1,19 +1,14 @@ package forge.ai.ability; -import java.util.List; - import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; - import forge.ai.*; -import forge.card.mana.ManaCost; import forge.game.Game; import forge.game.ability.ApiType; import forge.game.card.*; +import forge.game.combat.Combat; import forge.game.combat.CombatUtil; -import forge.game.cost.Cost; -import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -23,6 +18,8 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import forge.util.MyRandom; +import java.util.List; + public class EffectAi extends SpellAbilityAi { @Override protected boolean canPlayAI(final Player ai,final SpellAbility sa) { @@ -104,94 +101,15 @@ public class EffectAi extends SpellAbilityAi { } randomReturn = true; } else if (logic.equals("ChainVeil")) { - if (!phase.isPlayerTurn(ai) || !phase.getPhase().equals(PhaseType.MAIN2) - || CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Planeswalker").isEmpty()) { + if (!phase.isPlayerTurn(ai) || !phase.getPhase().equals(PhaseType.MAIN2) + || CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Planeswalker").isEmpty()) { return false; } randomReturn = true; - } else if (logic.equals("SpellCopy")) { - // fetch Instant or Sorcery and AI has reason to play this turn - // does not try to get itself - final ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST; - final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() { - @Override - public boolean apply(final Card c) { - if (!(c.isInstant() || c.isSorcery()) || c.equals(sa.getHostCard())) { - return false; - } - for (SpellAbility ab : c.getSpellAbilities()) { - if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab)) - || ab.hasParam("AINoRecursiveCheck")) { - // prevent infinitely recursing mana ritual and other abilities with reentry - continue; - } else if ("SpellCopy".equals(ab.getParam("AILogic")) && ab.getApi() == ApiType.Effect) { - // don't copy another copy spell, too complex for the AI - continue; - } - if (!ab.canPlay()) { - continue; - } - AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab); - // see if we can pay both for this spell and for the Effect spell we're considering - if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) { - ManaCost costAb = ab.getPayCosts() != null ? ab.getPayCosts().getTotalMana() : ManaCost.NO_COST; - ManaCost total = ManaCost.combine(costSa, costAb); - SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false)); - // can we pay both costs? - if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) { - return true; - } - } - } - - return false; - } - }); - - if(count == 0) { - return false; - } - - randomReturn = true; - } else if (logic.equals("NarsetRebound")) { - // should be done in Main2, but it might broke for other cards - //if (phase.getPhase().isBefore(PhaseType.MAIN2)) { - // return false; - //} - - // fetch Instant or Sorcery without Rebound and AI has reason to play this turn - // only need count, not the list - final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() { - @Override - public boolean apply(final Card c) { - if (!(c.isInstant() || c.isSorcery()) || c.hasKeyword(Keyword.REBOUND)) { - return false; - } - for (SpellAbility ab : c.getSpellAbilities()) { - if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab)) - || ab.hasParam("AINoRecursiveCheck")) { - // prevent infinitely recursing mana ritual and other abilities with reentry - continue; - } - if (!ab.canPlay()) { - continue; - } - AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab); - if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) { - if (ComputerUtilMana.canPayManaCost(ab, ai, 0)) { - return true; - } - } - } - return false; - } - }); - - if(count == 0) { - return false; - } - - randomReturn = true; + } else if (logic.equals("WillCastCreature") && ai.isAI()) { + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); + SpellAbility saCreature = aic.predictSpellToCastInMain2(ApiType.PermanentCreature); + randomReturn = saCreature != null && ComputerUtilMana.canPayManaCost(saCreature, ai, 0); } else if (logic.equals("Always")) { randomReturn = true; } else if (logic.equals("Main1")) { @@ -307,6 +225,16 @@ public class EffectAi extends SpellAbilityAi { if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) { return false; } + } else if (logic.equals("Bribe")) { + Card host = sa.getHostCard(); + Combat combat = game.getCombat(); + if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host) + && game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS) + && !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) { + AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something + return true; + } + return false; } } else { //no AILogic return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java index fcfe5c957ba..53587f59cd6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java @@ -18,6 +18,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; @@ -84,7 +85,7 @@ public final class EncodeAi extends SpellAbilityAi { * forge.game.player.Player) */ @Override - public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return chooseCard(ai, options, isOptional); } diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java index 1b95b09c1ca..942579582f1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -3,6 +3,7 @@ package forge.ai.ability; import forge.ai.*; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; +import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; @@ -50,7 +51,11 @@ public class FightAi extends SpellAbilityAi { // assumes the triggered card belongs to the ai if (sa.hasParam("Defined")) { - Card fighter1 = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0); + CardCollection fighter1List = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); + if (fighter1List.isEmpty()) { + return true; + } + Card fighter1 = fighter1List.get(0); for (Card humanCreature : humCreatures) { if (ComputerUtilCombat.getDamageToKill(humanCreature) <= fighter1.getNetPower() && humanCreature.getNetPower() < ComputerUtilCombat.getDamageToKill(fighter1)) { @@ -193,7 +198,7 @@ public class FightAi extends SpellAbilityAi { for (Card humanCreature : humCreatures) { for (Card aiCreature : aiCreatures) { if (source.isSpell()) { // heroic triggers adding counters and prowess - final int bonus = getSpellBonus(aiCreature); + final int bonus = getSpellBonus(aiCreature); power += bonus; toughness += bonus; } @@ -247,28 +252,32 @@ public class FightAi extends SpellAbilityAi { return false; } - /** - * Compute the bonus from Heroic +1/+1 counters or Prowess - */ - private static int getSpellBonus(final Card aiCreature) { - for (Trigger t : aiCreature.getTriggers()) { - if (t.getMode() == TriggerType.SpellCast) { - final Map 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"))) { - return AbilityUtils.calculateAmount(aiCreature, heroic.getParam("CounterNum"), heroic); - } - break; - } - if ("ProwessPump".equals(params.get("Execute"))) { - return 1; - } - } - } - return 0; - } + /** + * Compute the bonus from Heroic +1/+1 counters or Prowess + */ + private static int getSpellBonus(final Card aiCreature) { + for (Trigger t : aiCreature.getTriggers()) { + if (t.getMode() == TriggerType.SpellCast) { + SpellAbility sa = t.ensureAbility(); + final Map params = t.getMapParams(); + if (sa == null) { + continue; + } + if (ApiType.PutCounter.equals(sa.getApi())) { + if ("Card.Self".equals(params.get("TargetsValid")) && "You".equals(params.get("ValidActivatingPlayer"))) { + SpellAbility heroic = AbilityFactory.getAbility(aiCreature.getSVar(params.get("Execute")),aiCreature); + if ("Self".equals(heroic.getParam("Defined")) && "P1P1".equals(heroic.getParam("CounterType"))) { + return AbilityUtils.calculateAmount(aiCreature, heroic.getParam("CounterNum"), heroic); + } + break; + } + } else if (ApiType.Pump.equals(sa.getApi())) { + // TODO add prowess boost + } + } + } + return 0; + } private static boolean shouldFight(Card fighter, Card opponent, int pumpAttack, int pumpDefense) { if (canKill(fighter, opponent, pumpAttack)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/FogAi.java b/forge-ai/src/main/java/forge/ai/ability/FogAi.java index 9d919b10b22..72a4a2eaf91 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FogAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FogAi.java @@ -113,6 +113,6 @@ public class FogAi extends SpellAbilityAi { chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE); } - return chance; + return chance || mandatory; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java new file mode 100644 index 00000000000..fe0830352a6 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java @@ -0,0 +1,71 @@ +package forge.ai.ability; + +import forge.ai.*; +import forge.game.player.Player; +import forge.game.spellability.AbilitySub; +import forge.game.spellability.SpellAbility; + +public class ImmediateTriggerAi extends SpellAbilityAi { + // TODO: this class is largely reused from DelayedTriggerAi, consider updating + + @Override + public boolean chkAIDrawback(SpellAbility sa, Player ai) { + String logic = sa.getParamOrDefault("AILogic", ""); + if (logic.equals("Always")) { + return true; + } + + SpellAbility trigsa = sa.getAdditionalAbility("Execute"); + if (trigsa == null) { + return false; + } + + trigsa.setActivatingPlayer(ai); + + if (trigsa instanceof AbilitySub) { + return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); + } else { + return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + } + } + + @Override + protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + String logic = sa.getParamOrDefault("AILogic", ""); + + SpellAbility trigsa = sa.getAdditionalAbility("Execute"); + if (trigsa == null) { + return false; + } + + if (logic.equals("MaxX")) { + sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai)); + } + + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); + trigsa.setActivatingPlayer(ai); + + if (!sa.hasParam("OptionalDecider")) { + return aic.doTrigger(trigsa, true); + } else { + return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You")); + } + } + + @Override + protected boolean canPlayAI(Player ai, SpellAbility sa) { + String logic = sa.getParamOrDefault("AILogic", ""); + if (logic.equals("Always")) { + return true; + } + + SpellAbility trigsa = sa.getAdditionalAbility("Execute"); + if (trigsa == null) { + return false; + } + + trigsa.setActivatingPlayer(ai); + return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + } + +} diff --git a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java new file mode 100644 index 00000000000..1ae9da74c7b --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java @@ -0,0 +1,27 @@ +package forge.ai.ability; + + +import forge.ai.SpellAbilityAi; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.PlayerActionConfirmMode; +import forge.game.spellability.SpellAbility; + +public class InvestigateAi extends SpellAbilityAi { + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) + */ + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); + + return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer; + } + + @Override + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + return true; + } +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java index 87e9d2d9464..4ad913b7a25 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java @@ -1,9 +1,11 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.collect.Iterables; import forge.ai.SpellAbilityAi; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -23,7 +25,7 @@ public class LegendaryRuleAi extends SpellAbilityAi { @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Choose a single legendary/planeswalker card to keep Card firstOption = Iterables.getFirst(options, null); boolean choosingFromPlanewalkers = firstOption.isPlaneswalker(); @@ -38,16 +40,16 @@ public class LegendaryRuleAi extends SpellAbilityAi { if (firstOption.getName().equals("Dark Depths")) { Card best = firstOption; for (Card c : options) { - if (c.getCounters(CounterType.ICE) < best.getCounters(CounterType.ICE)) { + if (c.getCounters(CounterEnumType.ICE) < best.getCounters(CounterEnumType.ICE)) { best = c; } } return best; - } else if (firstOption.getCounters(CounterType.KI) > 0) { + } else if (firstOption.getCounters(CounterEnumType.KI) > 0) { // Extra Rule for KI counter Card best = firstOption; for (Card c : options) { - if (c.getCounters(CounterType.KI) > best.getCounters(CounterType.KI)) { + if (c.getCounters(CounterEnumType.KI) > best.getCounters(CounterEnumType.KI)) { best = c; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java index 41751f85882..78044dedaec 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -49,7 +49,7 @@ public class LifeGainAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(cost, source)) { + if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) { return false; } } else { @@ -88,7 +88,6 @@ public class LifeGainAi extends SpellAbilityAi { if (lifeCritical && sa.isAbility() && sa.getHostCard() != null && sa.getHostCard().isCreature() - && sa.getPayCosts() != null && (sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class) || sa.getPayCosts().hasSpecificCostType(CostSacrifice.class))) { if (!game.getStack().isEmpty()) { SpellAbility saTop = game.getStack().peekAbility(); @@ -126,10 +125,10 @@ public class LifeGainAi extends SpellAbilityAi { final String amountStr = sa.getParam("LifeAmount"); int lifeAmount = 0; boolean activateForCost = ComputerUtil.activateForCost(sa, ai); - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); lifeAmount = xPay; } else { lifeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); @@ -147,7 +146,7 @@ public class LifeGainAi extends SpellAbilityAi { } // don't play if the conditions aren't met, unless it would trigger a // beneficial sub-condition - if (!activateForCost && !sa.getConditions().areMet(sa)) { + if (!activateForCost && !sa.metConditions()) { final AbilitySub abSub = sa.getSubAbility(); if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) { if (!abSub.getConditions().areMet(abSub)) { @@ -214,12 +213,11 @@ public class LifeGainAi extends SpellAbilityAi { } } - final Card source = sa.getHostCard(); final String amountStr = sa.getParam("LifeAmount"); - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); } return true; diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java index 23ee0a4712e..2437c97a49a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java @@ -34,14 +34,15 @@ public class LifeLoseAi extends SpellAbilityAi { final Card source = sa.getHostCard(); final String amountStr = sa.getParam("LifeAmount"); int amount = 0; - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // something already set PayX - if (source.hasSVar("PayX")) { - amount = Integer.parseInt(source.getSVar("PayX")); + SpellAbility root = sa.getRootAbility(); + if (root.getXManaCostPaid() != null) { + amount = root.getXManaCostPaid(); } else { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + root.setXManaCostPaid(xPay); amount = xPay; } } else { @@ -72,10 +73,9 @@ public class LifeLoseAi extends SpellAbilityAi { final String amountStr = sa.getParam("LifeAmount"); int amount = 0; - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. amount = ComputerUtilMana.determineLeftoverMana(sa, ai); - // source.setSVar("PayX", Integer.toString(amount)); } else { amount = AbilityUtils.calculateAmount(source, amountStr, sa); } @@ -101,10 +101,16 @@ public class LifeLoseAi extends SpellAbilityAi { final String amountStr = sa.getParam("LifeAmount"); int amount = 0; - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (sa.usesTargeting()) { + if (!doTgt(ai, sa, false)) { + return false; + } + } + + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. - amount = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(amount)); + amount = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(amount); } else { amount = AbilityUtils.calculateAmount(source, amountStr, sa); } @@ -117,11 +123,6 @@ public class LifeLoseAi extends SpellAbilityAi { return false; } - if (sa.usesTargeting()) { - if (!doTgt(ai, sa, false)) { - return false; - } - } final PlayerCollection tgtPlayers = getPlayers(ai, sa); if (ComputerUtil.playImmediately(ai, sa)) { @@ -137,7 +138,7 @@ public class LifeLoseAi extends SpellAbilityAi { // Don't use loselife before main 2 if possible if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") - && !ComputerUtil.castSpellInMain1(ai, sa)) { + && !ComputerUtil.castSpellInMain1(ai, sa) && !"AnyPhase".equals(sa.getParam("AILogic"))) { return false; } @@ -172,10 +173,10 @@ public class LifeLoseAi extends SpellAbilityAi { final Card source = sa.getHostCard(); final String amountStr = sa.getParam("LifeAmount"); int amount = 0; - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); amount = xPay; } else { amount = AbilityUtils.calculateAmount(source, amountStr, sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java index cc4d851bd9b..91e17c2da2f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java @@ -1,11 +1,11 @@ package forge.ai.ability; import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilMana; +import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -16,8 +16,6 @@ public class LifeSetAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - // Ability_Cost abCost = sa.getPayCosts(); - final Card source = sa.getHostCard(); final int myLife = ai.getLife(); final Player opponent = ai.getWeakestOpponent(); final int hlife = opponent.getLife(); @@ -42,10 +40,10 @@ public class LifeSetAi extends SpellAbilityAi { // would be paid int amount; // we shouldn't have to worry too much about PayX for SetLife - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); amount = xPay; } else { amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); @@ -114,10 +112,10 @@ public class LifeSetAi extends SpellAbilityAi { final String amountStr = sa.getParam("LifeAmount"); int amount; - if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) { + if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(xPay); amount = xPay; } else { amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); @@ -130,7 +128,7 @@ public class LifeSetAi extends SpellAbilityAi { } if (sourceName.equals("Eternity Vessel") - && (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterType.CHARGE) == 0))) { + && (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterEnumType.CHARGE) == 0))) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java index 60eaeef180f..012549c8b85 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java @@ -81,7 +81,7 @@ public class ManaEffectAi extends SpellAbilityAi { return true; // handled elsewhere, does not meet the standard requirements } - return sa.getPayCosts() != null && sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource() + return sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource() && sa.getSubAbility() == null && ComputerUtil.playImmediately(ai, sa); // return super.checkApiLogic(ai, sa); } @@ -118,9 +118,8 @@ public class ManaEffectAi extends SpellAbilityAi { int numCounters = 0; int manaSurplus = 0; - if ("XChoice".equals(host.getSVar("X")) - && sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { - CounterType ctrType = CounterType.KI; // Petalmane Baku + if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { + CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku for (CostPart part : sa.getPayCosts().getCostParts()) { if (part instanceof CostRemoveCounter) { ctrType = ((CostRemoveCounter)part).counter; @@ -206,7 +205,7 @@ public class ManaEffectAi extends SpellAbilityAi { // Don't remove more counters than would be needed to cast the more expensive thing we want to cast, // otherwise the AI grabs too many counters at once. int maxCtrs = Aggregates.max(castableSpells, CardPredicates.Accessors.fnGetCmc) - manaSurplus; - sa.setSVar("ChosenX", "Number$" + Math.min(numCounters, maxCtrs)); + sa.setXManaCostPaid(Math.min(numCounters, maxCtrs)); } // TODO: this will probably still waste the card from time to time. Somehow improve detection of castable material. diff --git a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java index b3d205c4958..7ab70c49801 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java @@ -4,7 +4,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilMana; +import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.ability.AbilityKey; @@ -51,7 +51,6 @@ public class ManifestAi extends SpellAbilityAi { */ @Override protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { - final Card source = sa.getHostCard(); // Only manifest things on your turn if sorcery speed, or would pump one of my creatures if (ph.isPlayerTurn(ai)) { if (ph.getPhase().isBefore(PhaseType.MAIN2) @@ -76,11 +75,11 @@ public class ManifestAi extends SpellAbilityAi { } } - if (source.getSVar("X").equals("Count$xPaid")) { + if (sa.getSVar("X").equals("Count$xPaid")) { // Handle either Manifest X cards, or Manifest 1 card and give it X P1P1s // Set PayX here to maximum value. - int x = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(x)); + int x = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(x); if (x <= 0) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java index 3656226ee06..02b94e24d67 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java @@ -1,15 +1,10 @@ package forge.ai.ability; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - import com.google.common.collect.Lists; import com.google.common.collect.Maps; - import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilMana; +import forge.ai.ComputerUtilCost; +import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -24,6 +19,11 @@ import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + public class MillAi extends SpellAbilityAi { @Override @@ -38,6 +38,8 @@ public class MillAi extends SpellAbilityAi { } else if (aiLogic.equals("LilianaMill")) { // Only mill if a "Raise Dead" target is available, in case of control decks with few creatures return CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES).size() >= 1; + } else if (aiLogic.equals("Rebirth")) { + return ai.getLife() <= 8; } return true; } @@ -73,7 +75,6 @@ public class MillAi extends SpellAbilityAi { * - check for Laboratory Maniac effect (needs to check for actual * effect due to possibility of "lose abilities" effect) */ - final Card source = sa.getHostCard(); if (ComputerUtil.preventRunAwayActivations(sa)) { return false; // prevents mill 0 infinite loop? } @@ -88,10 +89,10 @@ public class MillAi extends SpellAbilityAi { } if ((sa.getParam("NumCards").equals("X") || sa.getParam("NumCards").equals("Z")) - && source.getSVar("X").startsWith("Count$xPaid")) { + && sa.getSVar("X").startsWith("Count$xPaid")) { // Set PayX here to maximum value. final int cardsToDiscard = getNumToDiscard(ai, sa); - source.setSVar("PayX", Integer.toString(cardsToDiscard)); + sa.setXManaCostPaid(cardsToDiscard); return cardsToDiscard > 0; } return true; @@ -180,11 +181,9 @@ public class MillAi extends SpellAbilityAi { return false; } - final Card source = sa.getHostCard(); - if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) { + if (sa.getParam("NumCards").equals("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - final int cardsToDiscard = getNumToDiscard(aiPlayer, sa); - source.setSVar("PayX", Integer.toString(cardsToDiscard)); + sa.setXManaCostPaid(getNumToDiscard(aiPlayer, sa)); } return true; @@ -194,6 +193,10 @@ public class MillAi extends SpellAbilityAi { */ @Override public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + if ("TimmerianFiends".equals(sa.getParam("AILogic"))) { + return SpecialCardAi.TimmerianFiends.consider(player, sa); + } + return true; } @@ -225,6 +228,6 @@ public class MillAi extends SpellAbilityAi { cardsToDiscard = Math.min(ai.getCardsIn(ZoneType.Library).size() - 5, cardsToDiscard); } - return Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), cardsToDiscard); + return Math.min(ComputerUtilCost.getMaxXValue(sa, ai), cardsToDiscard); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java index 238ba76c409..fe921f05e72 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java @@ -1,7 +1,7 @@ package forge.ai.ability; import com.google.common.base.Predicate; - +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import forge.ai.*; import forge.game.Game; @@ -12,14 +12,13 @@ import forge.game.card.CardPredicates; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.keyword.Keyword; -import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class MustBlockAi extends SpellAbilityAi { @@ -28,7 +27,6 @@ public class MustBlockAi extends SpellAbilityAi { final Card source = sa.getHostCard(); final Game game = aiPlayer.getGame(); final Combat combat = game.getCombat(); - final PhaseHandler ph = game.getPhaseHandler(); final boolean onlyLethal = !"AllowNonLethal".equals(sa.getParam("AILogic")); if (combat == null || !combat.isAttacking(source)) { @@ -39,7 +37,6 @@ public class MustBlockAi extends SpellAbilityAi { return false; } - final TargetRestrictions abTgt = sa.getTargetRestrictions(); final List list = determineGoodBlockers(source, aiPlayer, combat.getDefenderPlayerByAttacker(source), sa, onlyLethal,false); if (!list.isEmpty()) { @@ -69,7 +66,6 @@ public class MustBlockAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); - final TargetRestrictions abTgt = sa.getTargetRestrictions(); // only use on creatures that can attack if (!ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { @@ -94,7 +90,7 @@ public class MustBlockAi extends SpellAbilityAi { boolean chance = false; - if (abTgt != null) { + if (sa.usesTargeting()) { final List list = determineGoodBlockers(definedAttacker, ai, ai.getWeakestOpponent(), sa, true,true); if (list.isEmpty()) { return false; @@ -119,6 +115,9 @@ public class MustBlockAi extends SpellAbilityAi { sa.getTargets().add(blocker); chance = true; + } else if (sa.hasParam("Choices")) { + // currently choice is attacked player + return true; } else { return false; } @@ -126,16 +125,9 @@ public class MustBlockAi extends SpellAbilityAi { return chance; } - private List determineGoodBlockers(final Card attacker, final Player ai, Player defender, SpellAbility sa, + private List determineBlockerFromList(final Card attacker, final Player ai, Iterable options, SpellAbility sa, final boolean onlyLethal, final boolean testTapped) { - final Card source = sa.getHostCard(); - final TargetRestrictions abTgt = sa.getTargetRestrictions(); - - List list = Lists.newArrayList(); - list = CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES); - list = CardLists.getTargetableCards(list, sa); - list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa); - list = CardLists.filter(list, new Predicate() { + List list = CardLists.filter(options, new Predicate() { @Override public boolean apply(final Card c) { boolean tapped = c.isTapped(); @@ -161,4 +153,40 @@ public class MustBlockAi extends SpellAbilityAi { return list; } + + private List determineGoodBlockers(final Card attacker, final Player ai, Player defender, SpellAbility sa, + final boolean onlyLethal, final boolean testTapped) { + + List list = Lists.newArrayList(); + list = CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES); + + if (sa.usesTargeting()) { + list = CardLists.getTargetableCards(list, sa); + } + return determineBlockerFromList(attacker, ai, list, sa, onlyLethal, testTapped); + } + + @Override + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, + Player targetedPlayer, Map params) { + final Card host = sa.getHostCard(); + + Card attacker = host; + + if (sa.hasParam("DefinedAttacker")) { + List attackers = AbilityUtils.getDefinedCards(host, sa.getParam("DefinedAttacker"), sa); + attacker = Iterables.getFirst(attackers, null); + } + if (attacker == null) { + return Iterables.getFirst(options, null); + } + + List better = determineBlockerFromList(attacker, ai, options, sa, false, false); + + if (!better.isEmpty()) { + return Iterables.getFirst(options, null); + } + + return Iterables.getFirst(options, null); + } } diff --git a/forge-ai/src/main/java/forge/ai/ability/MutateAi.java b/forge-ai/src/main/java/forge/ai/ability/MutateAi.java new file mode 100644 index 00000000000..ca17518e2f2 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/MutateAi.java @@ -0,0 +1,68 @@ +package forge.ai.ability; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import forge.ai.ComputerUtilCard; +import forge.ai.SpellAbilityAi; +import forge.game.card.Card; +import forge.game.card.CardCollectionView; +import forge.game.card.CardLists; +import forge.game.card.CardPredicates; +import forge.game.keyword.Keyword; +import forge.game.player.Player; +import forge.game.player.PlayerActionConfirmMode; +import forge.game.spellability.SpellAbility; + +import java.util.Map; + +public class MutateAi extends SpellAbilityAi { + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + CardCollectionView mutateTgts = CardLists.getTargetableCards(aiPlayer.getCreaturesInPlay(), sa); + + // Filter out some abilities that are useless + // TODO: add other stuff useless for Mutate here + mutateTgts = CardLists.filter(mutateTgts, Predicates.not(Predicates.or( + CardPredicates.hasKeyword(Keyword.DEFENDER), + CardPredicates.hasKeyword("CARDNAME can't attack."), + CardPredicates.hasKeyword("CARDNAME can't block."), + new Predicate() { + @Override + public boolean apply(final Card card) { + return ComputerUtilCard.isUselessCreature(aiPlayer, card); + } + } + ))); + + if (mutateTgts.isEmpty()) { + return false; + } + + // Choose the best target + // TODO: maybe, instead of the standard evaluator, this could inspect the abilities and decide + // which are better in context, but that's a bit complicated for the time being (not sure if necessary?). + Card mutateTgt = ComputerUtilCard.getBestCreatureAI(mutateTgts); + sa.getTargets().add(mutateTgt); + + return true; + } + + @Override + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { + // Decide which card goes on top here. Pretty rudimentary, feel free to improve. + Card choice = null; + + for (Card c : options) { + if (choice == null || c.getBasePower() > choice.getBasePower() || c.getBaseToughness() > choice.getBaseToughness()) { + choice = c; + } + } + + return choice; + } + + @Override + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + return true; + } +} diff --git a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java index bd6dedc12ec..331ee87051c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import forge.ai.SpellAbilityAi; import forge.ai.SpellApiToAi; +import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; @@ -23,10 +24,17 @@ public class PeekAndRevealAi extends SpellAbilityAi { if (sa instanceof AbilityStatic) { return false; } - if ("Main2".equals(sa.getParam("AILogic"))) { + + String logic = sa.getParamOrDefault("AILogic", ""); + if ("Main2".equals(logic)) { if (aiPlayer.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { return false; } + } else if ("EndOfOppTurn".equals(logic)) { + PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); + if (!(ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN))) { + return false; + } } // So far this only appears on Triggers, but will expand // once things get converted from Dig + NoMove diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java index 62012507864..fbc05b92472 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java @@ -102,18 +102,17 @@ public class PermanentAi extends SpellAbilityAi { ManaCost mana = sa.getPayCosts().getTotalMana(); if (mana.countX() > 0) { // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); final Card source = sa.getHostCard(); if (source.hasConverge()) { - card.setSVar("PayX", Integer.toString(0)); int nColors = ComputerUtilMana.getConvergeCount(sa, ai); for (int i = 1; i <= xPay; i++) { - card.setSVar("PayX", Integer.toString(i)); + sa.setXManaCostPaid(i); int newColors = ComputerUtilMana.getConvergeCount(sa, ai); if (newColors > nColors) { nColors = newColors; } else { - card.setSVar("PayX", Integer.toString(i - 1)); + sa.setXManaCostPaid(i - 1); break; } } @@ -122,7 +121,7 @@ public class PermanentAi extends SpellAbilityAi { if (xPay <= 0) { return false; } - card.setSVar("PayX", Integer.toString(xPay)); + sa.setXManaCostPaid(xPay); } } else if (mana.isZero()) { // if mana is zero, but card mana cost does have X, then something @@ -134,6 +133,19 @@ public class PermanentAi extends SpellAbilityAi { } } + if ("SacToReduceCost".equals(sa.getParam("AILogic"))) { + // reset X to better calculate + sa.setXManaCostPaid(0); + ManaCostBeingPaid paidCost = ComputerUtilMana.calculateManaCost(sa, true, 0); + + int generic = paidCost.getGenericManaAmount(); + // Set PayX here to maximum value. + int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + // currently cards with SacToReduceCost reduce by 2 generic + xPay = Math.min(xPay, generic / 2); + sa.setXManaCostPaid(xPay); + } + if (sa.hasParam("Announce") && sa.getParam("Announce").startsWith("Multikicker")) { // String announce = sa.getParam("Announce"); ManaCost mkCost = sa.getMultiKickerManaCost(); @@ -144,6 +156,7 @@ public class PermanentAi extends SpellAbilityAi { ManaCostBeingPaid mcbp = new ManaCostBeingPaid(mCost); if (!ComputerUtilMana.canPayManaCost(mcbp, sa, ai)) { card.setKickerMagnitude(i); + sa.setSVar("Multikicker", String.valueOf(i)); break; } card.setKickerMagnitude(i + 1); @@ -258,7 +271,7 @@ public class PermanentAi extends SpellAbilityAi { return !dontCast; } - + return true; } @@ -267,7 +280,7 @@ public class PermanentAi extends SpellAbilityAi { final Card source = sa.getHostCard(); final Cost cost = sa.getPayCosts(); - if (sa.getConditions() != null && !sa.getConditions().areMet(sa)) { + if (!sa.metConditions()) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java index fff79b466d4..63290093230 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java @@ -9,6 +9,7 @@ import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.card.CardUtil; import forge.game.combat.Combat; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -86,7 +87,7 @@ public class PermanentCreatureAi extends PermanentAi { if (ai.getController().isAI()) { advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC); } - if (card.withFlash(ai)) { + if (card.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai))) { if (advancedFlash) { return doAdvancedFlashLogic(card, ai, sa); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java index 96c22f1d519..443fb1eacfa 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -1,6 +1,9 @@ package forge.ai.ability; import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + import forge.ai.*; import forge.card.CardStateName; import forge.card.CardTypeView; @@ -14,12 +17,15 @@ import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.Spell; import forge.game.spellability.SpellAbility; +import forge.game.spellability.SpellAbilityPredicates; import forge.game.spellability.SpellPermanent; import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import forge.util.MyRandom; +import java.util.Iterator; import java.util.List; +import java.util.Map; public class PlayAi extends SpellAbilityAi { @@ -54,6 +60,21 @@ public class PlayAi extends SpellAbilityAi { } } + if (sa.hasParam("ValidSA")) { + final String valid[] = {sa.getParam("ValidSA")}; + final Iterator itr = cards.iterator(); + while (itr.hasNext()) { + final Card c = itr.next(); + final List validSA = Lists.newArrayList(Iterables.filter(AbilityUtils.getBasicSpellsFromPlayEffect(c, ai), SpellAbilityPredicates.isValid(valid, ai , c, sa))); + if (validSA.size() == 0) { + itr.remove(); + } + } + if (cards.isEmpty()) { + return false; + } + } + if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) { // Additional logic for MoJhoSto: // Do not activate Jhoira too early, usually there are few good targets @@ -84,11 +105,11 @@ public class PlayAi extends SpellAbilityAi { return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost")); } else if (logic.startsWith("NeedsChosenCard")) { int minCMC = 0; - if (sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null) { - minCMC = sa.getPayCosts().getCostMana().getMana().getCMC(); + if (sa.getPayCosts().getCostMana() != null) { + minCMC = sa.getPayCosts().getTotalMana().getCMC(); } validOpts = CardLists.filter(validOpts, CardPredicates.greaterCMC(minCMC)); - return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null) != null; + return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null, null) != null; } if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) { @@ -142,8 +163,7 @@ public class PlayAi extends SpellAbilityAi { */ @Override public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, - final boolean isOptional, - Player targetedPlayer) { + final boolean isOptional, Player targetedPlayer, Map params) { List tgtCards = CardLists.filter(options, new Predicate() { @Override public boolean apply(final Card c) { @@ -156,9 +176,7 @@ public class PlayAi extends SpellAbilityAi { if (sa.hasParam("WithoutManaCost")) { // Try to avoid casting instants and sorceries with X in their cost, since X will be assumed to be 0. if (!(spell instanceof SpellPermanent)) { - if (spell.getPayCosts() != null - && spell.getPayCosts().getCostMana() != null - && spell.getPayCosts().getCostMana().getMana().countX() > 0) { + if (spell.getPayCosts().getTotalMana().countX() > 0) { continue; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java index 0eba3082f50..5761c79832c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java @@ -5,6 +5,7 @@ import com.google.common.base.Predicate; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -59,7 +60,7 @@ public class PoisonAi extends SpellAbilityAi { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { return tgtPlayer(ai, sa, mandatory); - } else if (mandatory || !ai.canReceiveCounters(CounterType.POISON)) { + } else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { // mandatory or ai is uneffected return true; } else { @@ -92,7 +93,7 @@ public class PoisonAi extends SpellAbilityAi { public boolean apply(Player input) { if (input.cantLose()) { return false; - } else if (!input.canReceiveCounters(CounterType.POISON)) { + } else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { return false; } return true; @@ -113,7 +114,7 @@ public class PoisonAi extends SpellAbilityAi { if (tgts.isEmpty()) { if (mandatory) { // AI is uneffected - if (ai.canBeTargetedBy(sa) && ai.canReceiveCounters(CounterType.POISON)) { + if (ai.canBeTargetedBy(sa) && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { sa.getTargets().add(ai); return true; } @@ -127,7 +128,7 @@ public class PoisonAi extends SpellAbilityAi { if (input.cantLose()) { return true; } - return !input.canReceiveCounters(CounterType.POISON); + return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON)); } }); diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java index 310fa6e32af..9557f3204eb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java @@ -164,7 +164,7 @@ public class ProtectAi extends SpellAbilityAi { @Override protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { - if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { + if (!sa.usesTargeting()) { final List cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); if (cards.size() == 0) { return false; @@ -202,7 +202,7 @@ public class ProtectAi extends SpellAbilityAi { if (game.getStack().isEmpty()) { // If the cost is tapping, don't activate before declare // attack/block - if ((sa.getPayCosts() != null) && sa.getPayCosts().hasTapCost()) { + if (sa.getPayCosts().hasTapCost()) { if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && game.getPhaseHandler().isPlayerTurn(ai)) { list.remove(sa.getHostCard()); @@ -226,12 +226,12 @@ public class ProtectAi extends SpellAbilityAi { return mandatory && protectMandatoryTarget(ai, sa, mandatory); } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.getTargets().size() < 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 ((sa.getTargets().size() < tgt.getMinTargets(source, sa)) || sa.getTargets().size() == 0) { if (mandatory) { return protectMandatoryTarget(ai, sa, mandatory); } @@ -285,7 +285,7 @@ public class ProtectAi extends SpellAbilityAi { final List forced = CardLists.filterControlledBy(list, ai); final Card source = sa.getHostCard(); - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) { if (pref.isEmpty()) { break; } @@ -302,7 +302,7 @@ public class ProtectAi extends SpellAbilityAi { sa.getTargets().add(c); } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) { if (pref2.isEmpty()) { break; } @@ -319,7 +319,7 @@ public class ProtectAi extends SpellAbilityAi { sa.getTargets().add(c); } - while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMinTargets(source, sa)) { if (forced.isEmpty()) { break; } @@ -336,7 +336,7 @@ public class ProtectAi extends SpellAbilityAi { sa.getTargets().add(c); } - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { sa.resetTargets(); return false; } @@ -359,12 +359,7 @@ public class ProtectAi extends SpellAbilityAi { @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { - final Card host = sa.getHostCard(); - if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { - if (host.isCreature()) { - // TODO - } - } else { + if (sa.usesTargeting()) { return protectTgtAI(ai, sa, false); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java index 7fb827cd033..8e60b3d397c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java @@ -33,7 +33,7 @@ public class ProtectAllAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) { + if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java index ce161caee80..0939a8879ba 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -9,10 +9,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.card.CardPredicates.Presets; -import forge.game.combat.Combat; import forge.game.cost.Cost; -import forge.game.cost.CostPart; -import forge.game.cost.CostRemoveCounter; import forge.game.cost.CostTapType; import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; @@ -23,7 +20,6 @@ import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; import forge.game.staticability.StaticAbility; import forge.game.zone.ZoneType; -import forge.util.Aggregates; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; @@ -71,13 +67,19 @@ public class PumpAi extends PumpAiBase { return false; } } else if ("Aristocrat".equals(aiLogic)) { - return doAristocratLogic(sa, ai); + return SpecialAiLogic.doAristocratLogic(ai, sa); } else if (aiLogic.startsWith("AristocratCounters")) { - return doAristocratWithCountersLogic(sa, ai); + return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); } else if ("RiskFactor".equals(aiLogic)) { if (ai.getCardsIn(ZoneType.Hand).size() + 3 >= ai.getMaxHandSize()) { return false; } + } else if (aiLogic.equals("SwitchPT")) { + // Some more AI would be even better, but this is a good start to prevent spamming + if (sa.isAbility() && sa.getActivationsThisTurn() > 0 && !sa.usesTargeting()) { + // Will prevent flipping back and forth + return false; + } } return super.checkAiLogic(ai, sa, aiLogic); @@ -98,6 +100,11 @@ public class PumpAi extends PumpAiBase { if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) { return false; } + } else if (logic.equals("SwitchPT")) { + // Some more AI would be even better, but this is a good start to prevent spamming + if (ph.getPhase().isAfter(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE) || !ph.inCombat()) { + return false; + } } return super.checkPhaseRestrictions(ai, sa, ph); } @@ -127,6 +134,7 @@ public class PumpAi extends PumpAiBase { protected boolean checkApiLogic(Player ai, SpellAbility sa) { final Game game = ai.getGame(); final Card source = sa.getHostCard(); + final SpellAbility root = sa.getRootAbility(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final List keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : Lists.newArrayList(); @@ -150,7 +158,7 @@ public class PumpAi extends PumpAiBase { } final String counterType = moveSA.getParam("CounterType"); - final CounterType cType = "Any".equals(counterType) ? null : CounterType.valueOf(counterType); + final CounterType cType = "Any".equals(counterType) ? null : CounterType.getType(counterType); final PhaseHandler ph = game.getPhaseHandler(); if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) { @@ -185,7 +193,7 @@ public class PumpAi extends PumpAiBase { // cant use substract on Copy srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount); - if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { + if (cType.is(CounterEnumType.P1P1) && srcCardCpy.getNetToughness() <= 0) { return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken(); } @@ -235,7 +243,7 @@ public class PumpAi extends PumpAiBase { // cant use substract on Copy srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount); - if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { + if (cType.is(CounterEnumType.P1P1) && srcCardCpy.getNetToughness() <= 0) { return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken(); } @@ -290,19 +298,19 @@ public class PumpAi extends PumpAiBase { } } - if (source.getSVar("X").equals("Count$xPaid")) { - source.setSVar("PayX", ""); + if (sa.getSVar("X").equals("Count$xPaid")) { + root.setXManaCostPaid(null); } int defense; - if (numDefense.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (numDefense.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); + int xPay = ComputerUtilCost.getMaxXValue(sa, ai); if (sourceName.equals("Necropolis Fiend")) { xPay = Math.min(xPay, sa.getActivatingPlayer().getCardsIn(ZoneType.Graveyard).size()); sa.setSVar("X", Integer.toString(xPay)); } - source.setSVar("PayX", Integer.toString(xPay)); + sa.setXManaCostPaid(xPay); defense = xPay; if (numDefense.equals("-X")) { defense = -xPay; @@ -315,16 +323,14 @@ public class PumpAi extends PumpAiBase { } int attack; - if (numAttack.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (numAttack.contains("X") && sa.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)); + if (root.getXManaCostPaid() == null) { + final int xPay = ComputerUtilCost.getMaxXValue(root, ai); + root.setXManaCostPaid(xPay); attack = xPay; } else { - attack = Integer.parseInt(toPay); + attack = root.getXManaCostPaid(); } } else { attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa); @@ -355,7 +361,7 @@ public class PumpAi extends PumpAiBase { } //Untargeted - if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { + if (!sa.usesTargeting()) { final List cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); if (cards.isEmpty()) { @@ -388,7 +394,7 @@ public class PumpAi extends PumpAiBase { } return true; - } else if (grantsUsefulExtraBlockOpts(ai, card)) { + } else if (grantsUsefulExtraBlockOpts(ai, sa, card, keywords)) { return true; } } @@ -400,21 +406,6 @@ public class PumpAi extends PumpAiBase { return false; } - if ("DebuffForXCounters".equals(sa.getParam("AILogic")) && sa.getTargetCard() != null) { - // e.g. Skullmane Baku - CounterType ctrType = CounterType.KI; - for (CostPart part : sa.getPayCosts().getCostParts()) { - if (part instanceof CostRemoveCounter) { - ctrType = ((CostRemoveCounter)part).counter; - break; - } - } - - // Do not pay more counters than necessary to kill the targeted creature - int chosenX = Math.min(source.getCounters(ctrType), sa.getTargetCard().getNetToughness()); - sa.setSVar("ChosenX", String.valueOf(chosenX)); - } - return true; } // pumpPlayAI() @@ -515,7 +506,7 @@ public class PumpAi extends PumpAiBase { if (game.getStack().isEmpty()) { // If the cost is tapping, don't activate before declare // attack/block - if (sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.getPayCosts().hasTapCost()) { if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && game.getPhaseHandler().isPlayerTurn(ai)) { list.remove(sa.getHostCard()); @@ -575,12 +566,12 @@ public class PumpAi extends PumpAiBase { } } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.canAddMoreTarget()) { Card t = null; // boolean goodt = false; if (list.isEmpty()) { - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa) || sa.getTargets().getNumTargeted() == 0) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (mandatory || ComputerUtil.activateForCost(sa, ai)) { return pumpMandatoryTarget(ai, sa); } @@ -636,7 +627,7 @@ public class PumpAi extends PumpAiBase { forced = CardLists.filterControlledBy(list, ai.getOpponents()); } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) { if (pref.isEmpty()) { break; } @@ -653,7 +644,7 @@ public class PumpAi extends PumpAiBase { sa.getTargets().add(c); } - while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMinTargets(source, sa)) { if (forced.isEmpty()) { break; } @@ -670,7 +661,7 @@ public class PumpAi extends PumpAiBase { sa.getTargets().add(c); } - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa)) { + if (sa.getTargets().size() < tgt.getMinTargets(source, sa)) { sa.resetTargets(); return false; } @@ -680,37 +671,43 @@ public class PumpAi extends PumpAiBase { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - final Card source = sa.getHostCard(); + final SpellAbility root = sa.getRootAbility(); final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : ""; final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : ""; + if (sa.getSVar("X").equals("Count$xPaid")) { + sa.setXManaCostPaid(null); + } + int defense; - if (numDefense.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (numDefense.contains("X") && sa.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 (root.getXManaCostPaid() == null) { + final int xPay = ComputerUtilCost.getMaxXValue(root, ai); + root.setXManaCostPaid(xPay); + defense = xPay; + } else { + defense = root.getXManaCostPaid(); + } } else { defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa); } int attack; - if (numAttack.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (numAttack.contains("X") && sa.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)); + if (root.getXManaCostPaid() == null) { + final int xPay = ComputerUtilCost.getMaxXValue(root, ai); + root.setXManaCostPaid(xPay); attack = xPay; } else { - attack = Integer.parseInt(toPay); + attack = root.getXManaCostPaid(); } } else { attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa); } - if (sa.getTargetRestrictions() == null) { + if (!sa.usesTargeting()) { if (mandatory) { return true; } @@ -723,14 +720,14 @@ public class PumpAi extends PumpAiBase { @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { - + final SpellAbility root = sa.getRootAbility(); final Card source = sa.getHostCard(); final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : ""; final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : ""; if (numDefense.equals("-X") && sa.getSVar("X").equals("Count$ChosenNumber")) { - int energy = ai.getCounters(CounterType.ENERGY); + int energy = ai.getCounters(CounterEnumType.ENERGY); for (SpellAbility s : source.getSpellAbilities()) { if ("PayEnergy".equals(s.getParam("AILogic"))) { energy += AbilityUtils.calculateAmount(source, s.getParam("CounterNum"), sa); @@ -751,28 +748,35 @@ public class PumpAi extends PumpAiBase { return false; } - int defense; - if (numDefense.contains("X") && source.getSVar("X").equals("Count$xPaid")) { - defense = Integer.parseInt(source.getSVar("PayX")); - } else { - defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa); - } - int attack; - if (numAttack.contains("X") && source.getSVar("X").equals("Count$xPaid")) { - if (source.getSVar("PayX").equals("")) { + if (numAttack.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { + if (root.getXManaCostPaid() == null) { // X is not set yet - final int xPay = ComputerUtilMana.determineLeftoverMana(sa.getRootAbility(), ai); - source.setSVar("PayX", Integer.toString(xPay)); + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + root.setXManaCostPaid(xPay); attack = xPay; } else { - attack = Integer.parseInt(source.getSVar("PayX")); + attack = root.getXManaCostPaid(); } } else { attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa); } - if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { + int defense; + if (numDefense.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { + if (root.getXManaCostPaid() == null) { + // X is not set yet + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); + root.setXManaCostPaid(xPay); + defense = xPay; + } else { + defense = root.getXManaCostPaid(); + } + } else { + defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa); + } + + if (!sa.usesTargeting()) { if (source.isCreature()) { if (!source.hasKeyword(Keyword.INDESTRUCTIBLE) && source.getNetToughness() + defense <= source.getDamage()) { return false; @@ -794,256 +798,4 @@ public class PumpAi extends PumpAiBase { //and the pump isn't mandatory return true; } - - public static boolean doAristocratLogic(final SpellAbility sa, final Player ai) { - // A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT" - final Game game = ai.getGame(); - final Combat combat = game.getCombat(); - final Card source = sa.getHostCard(); - final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); - final int powerBonus = sa.hasParam("NumAtt") ? AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa) : 0; - final int toughnessBonus = sa.hasParam("NumDef") ? AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa) : 0; - final boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible"); - final int selfEval = ComputerUtilCard.evaluateCreature(source); - final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); - - if (numOtherCreats == 0) { - return false; - } - - // Try to save the card from death by pumping it if it's threatened with a damage spell - if (isThreatened && (toughnessBonus > 0 || indestructible)) { - SpellAbility saTop = game.getStack().peekAbility(); - - if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) { - int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage(); - final int numCreatsToSac = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus)); - - if (numCreatsToSac > 1) { // probably not worth sacrificing too much - return false; - } - - if (indestructible || (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg)) { - final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(), - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || card.hasSVar("SacMe") - || ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK? - } - } - ); - return sacFodder.size() >= numCreatsToSac; - } - } - - return false; - } - - if (combat == null) { - return false; - } - - if (combat.isAttacking(source)) { - if (combat.getBlockers(source).isEmpty()) { - // Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if - // the opponent is tapped out or if we're willing to risk it (will currently risk it - // in case it sacs less than half its creatures to deal lethal damage) - - // TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully - // for potential damage prevention, various effects like reducing damage to 0, etc.) - - final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); - final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; - - final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp - int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) { - lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent - } - - final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / (powerBonus != 0 ? powerBonus : 1); - - if (defTappedOut || numCreatsToSac < numOtherCreats / 2) { - return source.getNetCombatDamage() < lethalDmg - && source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg; - } else { - return false; - } - } else { - // We have already attacked. Thus, see if we have a creature to sac that is worse to lose - // than the card we attacked with. - final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(), - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || ComputerUtilCard.evaluateCreature(card) < selfEval; - } - } - ); - - if (sacTgts.isEmpty()) { - return false; - } - - final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); - final int DefP = indestructible ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower); - - // Make sure we don't over-sacrifice, only sac until we can survive and kill a creature - return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; - } - } else { - // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances - final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(), - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || card.hasSVar("SacMe") - || ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK? - } - } - ); - - return !sacFodder.isEmpty(); - } - } - - public static boolean doAristocratWithCountersLogic(final SpellAbility sa, final Player ai) { - // A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat) - final Card source = sa.getHostCard(); - final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied - final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS); - - final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); - if (numOtherCreats == 0) { - // Cut short if there's nothing to sac at all - return false; - } - - // Check if the standard Aristocrats logic applies first (if in the right conditions for it) - final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); - if (isDeclareBlockers || isThreatened) { - if (doAristocratLogic(sa, ai)) { - return true; - } - } - - // Check if anything is to be gained from the PutCounter subability - SpellAbility countersSa = null; - if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) { - if (sa.getApi() == ApiType.PutCounter) { - // called directly from CountersPutAi - countersSa = sa; - } - } else { - countersSa = sa.getSubAbility(); - } - - if (countersSa == null) { - // Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?) - System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!"); - return false; - } - - final Game game = ai.getGame(); - final Combat combat = game.getCombat(); - final int selfEval = ComputerUtilCard.evaluateCreature(source); - - String typeToGainCtr = ""; - if (logic.contains(".")) { - typeToGainCtr = logic.substring(logic.indexOf(".") + 1); - } - CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay() - : CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr)); - relevantCreats.remove(source); - if (relevantCreats.isEmpty()) { - // No relevant creatures to sac - return false; - } - - int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa); - - if (combat != null && combat.isAttacking(source) && isDeclareBlockers) { - if (combat.getBlockers(source).isEmpty()) { - // Unblocked. Check if we can deal lethal after receiving counters. - final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); - final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; - - final boolean isInfect = source.hasKeyword(Keyword.INFECT); - int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) { - lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent - } - - // Check if there's anything that will die anyway that can be eaten to gain a perma-bonus - final CardCollection forcedSacTgts = CardLists.filter(relevantCreats, - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card) - || (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat)); - } - } - ); - if (!forcedSacTgts.isEmpty()) { - return true; - } - - final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs); - - if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) { - return source.getNetCombatDamage() < lethalDmg - && source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg; - } else { - return false; - } - } else { - // We have already attacked. Thus, see if we have a creature to sac that is worse to lose - // than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing - // things that are also threatened to be destroyed anyway. - final CardCollection sacTgts = CardLists.filter(relevantCreats, - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || ComputerUtilCard.evaluateCreature(card) < selfEval - || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); - } - } - ); - - if (sacTgts.isEmpty()) { - return false; - } - - final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source); - final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); - final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower); - - // Make sure we don't over-sacrifice, only sac until we can survive and kill a creature - return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; - } - } else { - // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances - final boolean isBlocking = combat != null && combat.isBlocking(source); - final CardCollection sacFodder = CardLists.filter(relevantCreats, - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || card.hasSVar("SacMe") - || (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval) - || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); - } - } - ); - - return !sacFodder.isEmpty(); - } - } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java index eb2ab18e1d1..02bce81f94b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -9,6 +9,7 @@ import forge.ai.ComputerUtilCombat; import forge.ai.SpellAbilityAi; import forge.card.MagicColor; import forge.game.Game; +import forge.game.ability.AbilityUtils; import forge.game.card.*; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; @@ -37,22 +38,46 @@ public abstract class PumpAiBase extends SpellAbilityAi { } - public boolean grantsUsefulExtraBlockOpts(final Player ai, final Card card) { + public boolean grantsUsefulExtraBlockOpts(final Player ai, final SpellAbility sa, final Card card, List keywords) { PhaseHandler ph = ai.getGame().getPhaseHandler(); + Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords); + if (ph.isPlayerTurn(ai) || !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) { return false; } + int canBlockNum = 1 + card.canBlockAdditional(); + int canBlockNumPumped = canBlockNum; // PumpedCreature doesn't return a meaningful value of canBlockAdditional, so we'll use sa params below + + if (sa.hasParam("CanBlockAny")) { + canBlockNumPumped = Integer.MAX_VALUE; + } else if (sa.hasParam("CanBlockAmount")) { + canBlockNumPumped += AbilityUtils.calculateAmount(pumped, sa.getParam("CanBlockAmount"), sa); + } + int possibleBlockNum = 0; + int possibleBlockNumPumped = 0; + for (Card attacker : ai.getGame().getCombat().getAttackers()) { if (CombatUtil.canBlock(attacker, card)) { possibleBlockNum++; if (possibleBlockNum > canBlockNum) { + possibleBlockNum = canBlockNum; break; } } } - return possibleBlockNum > canBlockNum; + for (Card attacker : ai.getGame().getCombat().getAttackers()) { + if (CombatUtil.canBlock(attacker, pumped)) { + possibleBlockNumPumped++; + if (possibleBlockNumPumped > canBlockNumPumped) { + possibleBlockNumPumped = canBlockNumPumped; + break; + } + } + } + + return possibleBlockNumPumped > possibleBlockNum; } /** @@ -94,7 +119,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { List attackers = CardLists.filter(ai.getCreaturesInPlay(), new Predicate() { @Override public boolean apply(final Card c) { - if (c.equals(sa.getHostCard()) && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost() + if (c.equals(sa.getHostCard()) && sa.getPayCosts().hasTapCost() && (combat == null || !combat.isAttacking(c))) { return false; } @@ -112,7 +137,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { List attackers = CardLists.filter(ai.getCreaturesInPlay(), new Predicate() { @Override public boolean apply(final Card c) { - if (c.equals(sa.getHostCard()) && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost() + if (c.equals(sa.getHostCard()) && sa.getPayCosts().hasTapCost() && (combat == null || !combat.isAttacking(c))) { return false; } @@ -245,7 +270,6 @@ public abstract class PumpAiBase extends SpellAbilityAi { } else if (keyword.endsWith("Haste")) { return card.hasSickness() && !ph.isPlayerTurn(opp) && !card.isTapped() && newPower > 0 - && !card.hasKeyword("CARDNAME can attack as though it had haste.") && !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && ComputerUtilCombat.canAttackNextTurn(card); } else if (keyword.endsWith("Indestructible")) { diff --git a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java index e8092eccfa2..3e9d91f327d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java @@ -26,7 +26,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { final PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); final Card source = sa.getHostCard(); - if (source.isPermanent() && sa.getRestrictions().isInstantSpeed() && sa.getPayCosts() != null + if (source.isPermanent() && !sa.getRestrictions().isSorcerySpeed() && (sa.getPayCosts().hasTapCost() || sa.getPayCosts().hasManaCost())) { // If it has an associated cost, try to only do this before own turn if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java index 7a37733b284..077177cd0a4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java @@ -2,22 +2,18 @@ package forge.ai.ability; import forge.ai.*; -import forge.game.card.Card; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; public class RepeatAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - final Card source = sa.getHostCard(); - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Player opp = ai.getWeakestOpponent(); - if (tgt != null) { + if (sa.usesTargeting()) { if (!opp.canBeTargetedBy(sa)) { return false; } @@ -30,8 +26,8 @@ public class RepeatAi extends SpellAbilityAi { return false; } // Set PayX here to maximum value. - final int max = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(max)); + final int max = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(max); return max > 0; } return true; diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java index 8ba1494b666..c7a08d63d11 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java @@ -6,9 +6,11 @@ import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; import forge.game.card.CardUtil; +import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; @@ -16,6 +18,7 @@ import forge.game.zone.ZoneType; import forge.util.TextUtil; import java.util.List; +import java.util.Map; public class RepeatEachAi extends SpellAbilityAi { @@ -47,21 +50,6 @@ public class RepeatEachAi extends SpellAbilityAi { return false; } } - } else if ("GainControlOwns".equals(logic)) { - List list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), new Predicate() { - @Override - public boolean apply(final Card crd) { - return crd.isCreature() && !crd.getController().equals(crd.getOwner()); - } - }); - if (list.isEmpty()) { - return false; - } - for (final Card c : list) { - if (aiPlayer.equals(c.getController())) { - return false; - } - } } else if ("OpponentHasCreatures".equals(logic)) { for (Player opp : aiPlayer.getOpponents()) { if (!opp.getCreaturesInPlay().isEmpty()){ @@ -108,8 +96,21 @@ public class RepeatEachAi extends SpellAbilityAi { } } } - // would not hit oppoent, don't do that + // would not hit opponent, don't do that return hitOpp; + } else if ("EquipAll".equals(logic)) { + if (aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1, aiPlayer)) { + final CardCollection unequipped = CardLists.filter(aiPlayer.getCardsIn(ZoneType.Battlefield), new Predicate() { + @Override + public boolean apply(Card card) { + return card.isEquipment() && card.getAttachedTo() != sa.getHostCard(); + } + }); + + return !unequipped.isEmpty(); + } + + return false; } // TODO Add some normal AI variability here @@ -118,7 +119,7 @@ public class RepeatEachAi extends SpellAbilityAi { } @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return ComputerUtilCard.getBestCreatureAI(options); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java index 676d706d06b..53d8a871aa1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java @@ -69,7 +69,7 @@ public class RevealAi extends RevealAiBase { continue; // use hard coded reduce cost - spell.getMapParams().put("ReduceCost", "2"); + spell.putParam("ReduceCost", "2"); if (AiPlayDecision.WillPlay == ((PlayerControllerAi) ai.getController()).getAi() .canPlayFromEffectAI(spell, false, false)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java b/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java new file mode 100644 index 00000000000..640265431a3 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java @@ -0,0 +1,34 @@ +package forge.ai.ability; + + +import forge.ai.SpellAbilityAi; +import forge.game.cost.Cost; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.PlayerActionConfirmMode; +import forge.game.spellability.SpellAbility; + +public class RollDiceAi extends SpellAbilityAi { + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); + Cost cost = sa.getPayCosts(); + + if (cost != null && (sa.getPayCosts().hasManaCost() || sa.getPayCosts().hasTapCost())) { + return ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN); + } + + return true; + } + + @Override + protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return true; + } + + @Override + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + return true; + } +} diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java index 78081283c46..7fb243db7c0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import forge.ai.ComputerUtilCard; +import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -103,10 +104,9 @@ public class SacrificeAi extends SpellAbilityAi { return false; } - if (num.equals("X") && source.getSVar(num).equals("Count$xPaid")) { + if (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) { // Set PayX here to maximum value. - final int xPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), amount); - source.setSVar("PayX", Integer.toString(xPay)); + sa.setXManaCostPaid(Math.min(ComputerUtilCost.getMaxXValue(sa, ai), amount)); } final int half = (amount / 2) + (amount % 2); // Half of amount @@ -135,7 +135,7 @@ public class SacrificeAi extends SpellAbilityAi { final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1"; int amount = AbilityUtils.calculateAmount(source, num, sa); - if (num.equals("X") && source.getSVar(num).equals("Count$xPaid")) { + if (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) { // Set PayX here to maximum value. amount = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), amount); } diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java index 8032c127eed..84f496ef24e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java @@ -28,10 +28,9 @@ public class SacrificeAllAi extends SpellAbilityAi { valid = sa.getParam("ValidCards"); } - if (valid.contains("X") && source.getSVar("X").equals("Count$xPaid")) { + if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(xPay)); valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay)); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java index cf01c1d8026..00b86b44550 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java @@ -46,12 +46,10 @@ public class ScryAi extends SpellAbilityAi { // and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to // try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible, // even if there's no mana cost. - if (sa.getPayCosts() != null) { - if (sa.getPayCosts().hasTapCost() - && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) - && !SpellAbilityAi.isSorcerySpeed(sa)) { - return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); - } + if (sa.getPayCosts().hasTapCost() + && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) + && !SpellAbilityAi.isSorcerySpeed(sa)) { + return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); } // AI logic to scry in Main 1 if there is no better option, otherwise scry at opponent's EOT @@ -76,8 +74,7 @@ public class ScryAi extends SpellAbilityAi { boolean hasSomethingElse = false; for (Card c : CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS))) { for (SpellAbility ab : c.getAllSpellAbilities()) { - if (ab.getPayCosts() != null - && ab.getPayCosts().hasManaCost() + if (ab.getPayCosts().hasManaCost() && ComputerUtilMana.hasEnoughManaSourcesToCast(ab, ai)) { // TODO: currently looks for non-Scry cards, can most certainly be made smarter. if (ab.getApi() != ApiType.Scry) { @@ -102,7 +99,7 @@ public class ScryAi extends SpellAbilityAi { } else if ("BrainJar".equals(aiLogic)) { final Card source = sa.getHostCard(); - int counterNum = source.getCounters(CounterType.CHARGE); + int counterNum = source.getCounters(CounterEnumType.CHARGE); // no need for logic if (counterNum == 0) { return false; @@ -118,7 +115,7 @@ public class ScryAi extends SpellAbilityAi { } // has spell that can be cast if one counter is removed if (!CardLists.filter(hand, CardPredicates.hasCMC(counterNum)).isEmpty()) { - sa.setSVar("ChosenX", "Number$1"); + sa.setXManaCostPaid(1); return true; } } @@ -143,12 +140,12 @@ public class ScryAi extends SpellAbilityAi { int maxToRemove = counterNum - maxCMC + 1; // no Scry 0, even if its catched from later stuff if (maxToRemove <= 0) { - return false; + return false; } - sa.setSVar("ChosenX", "Number$" + maxToRemove); + sa.setXManaCostPaid(maxToRemove); } else { // no Instant or Sorceries anymore, just scry - sa.setSVar("ChosenX", "Number$" + Math.min(counterNum, libsize)); + sa.setXManaCostPaid(Math.min(counterNum, libsize)); } } return true; diff --git a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java index 7e03c1cf0f5..bc90961a6c5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java @@ -86,13 +86,13 @@ public class SetStateAi extends SpellAbilityAi { for (final Card c : list) { if (shouldTransformCard(c, ai, ph) || "Always".equals(logic)) { sa.getTargets().add(c); - if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) { + if (sa.getTargets().size() == tgt.getMaxTargets(source, sa)) { break; } } } - return sa.getTargets().getNumTargeted() >= tgt.getMinTargets(source, sa); + return sa.getTargets().size() >= tgt.getMinTargets(source, sa); } } else if ("TurnFace".equals(mode)) { if (!sa.usesTargeting()) { @@ -115,13 +115,13 @@ public class SetStateAi extends SpellAbilityAi { for (final Card c : list) { if (shouldTurnFace(c, ai, ph) || "Always".equals(logic)) { sa.getTargets().add(c); - if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) { + if (sa.getTargets().size() == tgt.getMaxTargets(source, sa)) { break; } } } - return sa.getTargets().getNumTargeted() >= tgt.getMinTargets(source, sa); + return sa.getTargets().size() >= tgt.getMinTargets(source, sa); } } return true; @@ -174,7 +174,7 @@ public class SetStateAi extends SpellAbilityAi { if (!card.isFaceDown()) { transformed.turnFaceDown(true); } else { - transformed.turnFaceUp(false, false); + transformed.forceTurnFaceUp(); } transformed.updateStateForView(); return compareCards(card, transformed, ai, ph); @@ -248,7 +248,7 @@ public class SetStateAi extends SpellAbilityAi { final Card othercard = aiPlayer.getCardsIn(ZoneType.Battlefield, other.getName()).getFirst(); // for legendary KI counter creatures - if (othercard.getCounters(CounterType.KI) >= source.getCounters(CounterType.KI)) { + if (othercard.getCounters(CounterEnumType.KI) >= source.getCounters(CounterEnumType.KI)) { // if the other legendary is useless try to replace it return ComputerUtilCard.isUselessCreature(aiPlayer, othercard); } diff --git a/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java b/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java index 2abada6610d..837123548b3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java @@ -32,7 +32,7 @@ public class StoreSVarAi extends SpellAbilityAi { // Set PayX here to half the remaining mana to allow for Main 2 and other combat shenanigans. final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai) / 2; if (xPay == 0) { return false; } - source.setSVar("PayX", Integer.toString(xPay)); + sa.setXManaCostPaid(xPay); } final String logic = sa.getParam("AILogic"); @@ -75,8 +75,7 @@ public class StoreSVarAi extends SpellAbilityAi { if (sa instanceof WrappedAbility) { SpellAbility origSa = ((WrappedAbility)sa).getWrappedAbility(); if (origSa.getHostCard().getName().equals("Maralen of the Mornsong Avatar")) { - origSa.setSVar("PayX", "2"); - origSa.getHostCard().setSVar("ChosenX", "2"); + origSa.setXManaCostPaid(2); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java index eb2beab480b..fb5e6efa0f7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java @@ -47,12 +47,10 @@ public class SurveilAi extends SpellAbilityAi { // and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to // try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible, // even if there's no mana cost. - if (sa.getPayCosts() != null) { - if (sa.getPayCosts().hasTapCost() - && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) - && !SpellAbilityAi.isSorcerySpeed(sa)) { - return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); - } + if (sa.getPayCosts().hasTapCost() + && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) + && !SpellAbilityAi.isSorcerySpeed(sa)) { + return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); } // in the player's turn Surveil should only be done in Main1 or in Upkeep if able diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java index a4eb933dbe4..dee2820f9e4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java @@ -3,17 +3,11 @@ package forge.ai.ability; import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; -import forge.game.card.CounterType; import forge.game.cost.Cost; -import forge.game.cost.CostPart; -import forge.game.cost.CostRemoveCounter; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; - -import java.util.List; public class TapAi extends TapAiBase { @Override @@ -47,7 +41,6 @@ public class TapAi extends TapAiBase { return false; } - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); final Cost abCost = sa.getPayCosts(); if (abCost != null) { @@ -56,32 +49,24 @@ public class TapAi extends TapAiBase { } } - if (tgt == null) { - final List defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); - + if (!sa.usesTargeting()) { boolean bFlag = false; - for (final Card c : defined) { + for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) { bFlag |= c.isUntapped(); } return bFlag; } else { - if ("TapForXCounters".equals(sa.getParam("AILogic"))) { - // e.g. Waxmane Baku - CounterType ctrType = CounterType.KI; - for (CostPart part : sa.getPayCosts().getCostParts()) { - if (part instanceof CostRemoveCounter) { - ctrType = ((CostRemoveCounter)part).counter; - break; - } - } - - int numTargetable = Math.min(sa.getHostCard().getCounters(ctrType), ai.getOpponents().getCreaturesInPlay().size()); - sa.setSVar("ChosenX", String.valueOf(numTargetable)); + // X controls the minimum targets + if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) { + // Set PayX here to maximum value. + // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? + sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai)); + // TODO since change of PayX. the shouldCastLessThanMax logic might be faulty } sa.resetTargets(); - return tapPrefTargeting(ai, source, tgt, sa, false); + return tapPrefTargeting(ai, source, sa, false); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java index 6a408821cbe..e198d6f26ee 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java @@ -18,12 +18,11 @@ import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import java.util.List; -public abstract class TapAiBase extends SpellAbilityAi { +public abstract class TapAiBase extends SpellAbilityAi { /** *

@@ -41,21 +40,18 @@ public abstract class TapAiBase extends SpellAbilityAi { */ private boolean tapTargetList(final Player ai, final SpellAbility sa, final CardCollection tapList, final boolean mandatory) { final Card source = sa.getHostCard(); - final TargetRestrictions tgt = sa.getTargetRestrictions(); - for (final Card c : sa.getTargets().getTargetCards()) { - tapList.remove(c); - } + tapList.removeAll(sa.getTargets().getTargetCards()); if (tapList.isEmpty()) { return false; } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.canAddMoreTarget()) { Card choice = null; - if (tapList.size() == 0) { - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa) || sa.getTargets().getNumTargeted() == 0) { + if (tapList.isEmpty()) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!mandatory) { sa.resetTargets(); } @@ -76,7 +72,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().getNumTargeted() == 0) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!mandatory) { sa.resetTargets(); } @@ -111,11 +107,10 @@ public abstract class TapAiBase extends SpellAbilityAi { * a boolean. * @return a boolean. */ - protected boolean tapPrefTargeting(final Player ai, final Card source, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) { + protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) { final Player opp = ai.getWeakestOpponent(); final Game game = ai.getGame(); CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); - tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa); tapList = CardLists.getTargetableCards(tapList, sa); tapList = CardLists.filter(tapList, Presets.UNTAPPED); tapList = CardLists.filter(tapList, new Predicate() { @@ -126,7 +121,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } for (final SpellAbility sa : c.getSpellAbilities()) { - if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isAbility() && sa.getPayCosts().hasTapCost()) { return true; } } @@ -136,10 +131,9 @@ public abstract class TapAiBase extends SpellAbilityAi { //use broader approach when the cost is a positive thing if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) { - tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); - tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa); + tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); tapList = CardLists.getTargetableCards(tapList, sa); - tapList = CardLists.filter(tapList, new Predicate() { + tapList = CardLists.filter(tapList, new Predicate() { @Override public boolean apply(final Card c) { if (c.isCreature()) { @@ -147,7 +141,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } for (final SpellAbility sa : c.getSpellAbilities()) { - if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isAbility() && sa.getPayCosts().hasTapCost()) { return true; } } @@ -162,15 +156,15 @@ public abstract class TapAiBase extends SpellAbilityAi { tapList.removeAll(toExclude); if (tapList.isEmpty()) { - return false; + return false; } boolean goodTargets = false; - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.canAddMoreTarget()) { Card choice = null; if (tapList.isEmpty()) { - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa) || sa.getTargets().getNumTargeted() == 0) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!mandatory) { sa.resetTargets(); } @@ -186,8 +180,8 @@ public abstract class TapAiBase extends SpellAbilityAi { PhaseHandler phase = game.getPhaseHandler(); Card primeTarget = ComputerUtil.getKilledByTargeting(sa, tapList); if (primeTarget != null) { - choice = primeTarget; - goodTargets = true; + choice = primeTarget; + goodTargets = true; } else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) { // Tap creatures possible blockers before combat during AI's turn. List attackers; @@ -231,7 +225,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().getNumTargeted() == 0) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!mandatory) { sa.resetTargets(); } @@ -249,7 +243,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } // Nothing was ever targeted, so we need to bail. - return sa.getTargets().getNumTargeted() != 0; + return sa.getTargets().size() != 0; } /** @@ -265,19 +259,17 @@ public abstract class TapAiBase extends SpellAbilityAi { */ protected boolean tapUnpreferredTargeting(final Player ai, final SpellAbility sa, final boolean mandatory) { final Card source = sa.getHostCard(); - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Game game = ai.getGame(); - CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), source.getController(), source, sa); - list = CardLists.getTargetableCards(list, sa); - + CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); + // try to tap anything controlled by the computer CardCollection tapList = CardLists.filterControlledBy(list, ai.getOpponents()); if (tapTargetList(ai, sa, tapList, mandatory)) { return true; } - if (sa.getTargets().getNumTargeted() >= tgt.getMinTargets(sa.getHostCard(), sa)) { + if (sa.isMinTargetChosen()) { return true; } @@ -296,7 +288,7 @@ public abstract class TapAiBase extends SpellAbilityAi { return true; } - if (sa.getTargets().getNumTargeted() >= tgt.getMinTargets(sa.getHostCard(), sa)) { + if (sa.isMinTargetChosen()) { return true; } @@ -308,11 +300,9 @@ public abstract class TapAiBase extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); - if (tgt == null) { + if (!sa.usesTargeting()) { if (mandatory) { return true; } @@ -322,7 +312,7 @@ public abstract class TapAiBase extends SpellAbilityAi { return true; } else { sa.resetTargets(); - if (tapPrefTargeting(ai, source, tgt, sa, mandatory)) { + if (tapPrefTargeting(ai, source, sa, mandatory)) { return true; } else if (mandatory) { // not enough preferred targets, but mandatory so keep going: @@ -335,17 +325,14 @@ public abstract class TapAiBase extends SpellAbilityAi { @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); boolean randomReturn = true; - if (tgt == null) { - // either self or defined, either way should be fine - } else { + if (sa.usesTargeting()) { // target section, maybe pull this out? sa.resetTargets(); - if (!tapPrefTargeting(ai, source, tgt, sa, false)) { + if (!tapPrefTargeting(ai, source, sa, false)) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java index b2655c16ff5..949bb217141 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java @@ -4,11 +4,8 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; import forge.util.MyRandom; -import java.util.List; - public class TapOrUntapAi extends TapAiBase { /* (non-Javadoc) @@ -16,19 +13,17 @@ public class TapOrUntapAi extends TapAiBase { */ @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - if (tgt == null) { + if (!sa.usesTargeting()) { // assume we are looking to tap human's stuff // TODO - check for things with untap abilities, and don't tap // those. - final List defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); boolean bFlag = false; - for (final Card c : defined) { + for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) { bFlag |= c.isUntapped(); } @@ -37,7 +32,7 @@ public class TapOrUntapAi extends TapAiBase { } } else { sa.resetTargets(); - if (!tapPrefTargeting(ai, source, tgt, sa, false)) { + if (!tapPrefTargeting(ai, source, sa, false)) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java index a5c3a57fad9..bc1d2256025 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -1,11 +1,12 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import forge.ai.*; import forge.game.Game; import forge.game.GameEntity; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; @@ -25,16 +26,9 @@ import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; -import forge.game.trigger.Trigger; -import forge.game.trigger.TriggerHandler; import forge.game.zone.ZoneType; -import forge.item.PaperToken; -import forge.util.MyRandom; -import forge.util.TextUtil; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import forge.util.MyRandom; /** *

@@ -45,35 +39,10 @@ import java.util.List; * @version $Id: AbilityFactoryToken.java 17656 2012-10-22 19:32:56Z Max mtg $ */ public class TokenAi extends SpellAbilityAi { - private String tokenAmount; - private String tokenPower; - private String tokenToughness; - private Card actualToken; - /** - *

- * Constructor for AbilityFactory_Token. - *

- * - * a {@link forge.game.ability.AbilityFactory} object. - */ - private void readParameters(final SpellAbility mapParams) { - this.tokenAmount = mapParams.getParamOrDefault("TokenAmount", "1"); - - this.actualToken = TokenInfo.getProtoType(mapParams.getParam("TokenScript"), mapParams); - - if (actualToken == null) { - this.tokenPower = mapParams.getParam("TokenPower"); - this.tokenToughness = mapParams.getParam("TokenToughness"); - } else { - this.tokenPower = actualToken.getBasePowerString(); - this.tokenToughness = actualToken.getBaseToughnessString(); - } - } - @Override protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { - readParameters(sa); // remember to call this somewhere! + final Card source = sa.getHostCard(); // Planeswalker-related flags boolean pwMinus = false; @@ -96,42 +65,44 @@ public class TokenAi extends SpellAbilityAi { } } } + String tokenAmount = sa.getParamOrDefault("TokenAmount", "1"); - if (actualToken == null) { - actualToken = spawnToken(ai, sa); - } + Card actualToken = spawnToken(ai, sa); - if (actualToken == null) { + if (actualToken == null || actualToken.getNetToughness() < 1) { final AbilitySub sub = sa.getSubAbility(); // useful // no token created return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub.getApi()).chkAIDrawback(sub, ai)); // planeswalker plus ability or sub-ability is } + String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString()); + String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString()); + // X-cost spells - if (this.tokenAmount.equals("X") || (this.tokenToughness != null && this.tokenToughness.equals("X"))) { - int x = AbilityUtils.calculateAmount(sa.getHostCard(), this.tokenAmount, sa); + if ("X".equals(tokenAmount) || "X".equals(tokenPower) || "X".equals(tokenToughness)) { + int x = AbilityUtils.calculateAmount(sa.getHostCard(), tokenAmount, sa); if (source.getSVar("X").equals("Count$Converge")) { x = ComputerUtilMana.getConvergeCount(sa, ai); } - if (source.getSVar("X").equals("Count$xPaid")) { + if (sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - x = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(x)); + x = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(x); } if (x <= 0) { return false; // 0 tokens or 0 toughness token(s) } } - if (canInterruptSacrifice(ai, sa, actualToken)) { + if (canInterruptSacrifice(ai, sa, actualToken, tokenAmount)) { return true; } - boolean haste = this.actualToken.hasKeyword(Keyword.HASTE); + boolean haste = actualToken.hasKeyword(Keyword.HASTE); boolean oneShot = sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.DelayedTrigger; - boolean isCreature = this.actualToken.getType().isCreature(); + boolean isCreature = actualToken.getType().isCreature(); // Don't generate tokens without haste before main 2 if possible if (ph.getPhase().isBefore(PhaseType.MAIN2) && ph.isPlayerTurn(ai) && !haste && !sa.hasParam("ActivationPhases") @@ -166,9 +137,10 @@ public class TokenAi extends SpellAbilityAi { if (ComputerUtil.preventRunAwayActivations(sa)) { return false; // prevent infinite tokens? } + Card actualToken = spawnToken(ai, sa); // Don't kill AIs Legendary tokens - if (this.actualToken.getType().isLegendary() && ai.isCardInPlay(this.actualToken.getName())) { + if (actualToken.getType().isLegendary() && ai.isCardInPlay(actualToken.getName())) { // TODO Check if Token is useless due to an aura or counters? return false; } @@ -240,7 +212,7 @@ public class TokenAi extends SpellAbilityAi { /** * Checks if the token(s) can save a creature from a sacrifice effect */ - private boolean canInterruptSacrifice(final Player ai, final SpellAbility sa, final Card token) { + private boolean canInterruptSacrifice(final Player ai, final SpellAbility sa, final Card token, final String tokenAmount) { final Game game = ai.getGame(); if (game.getStack().isEmpty()) { return false; // nothing to interrupt @@ -249,7 +221,7 @@ public class TokenAi extends SpellAbilityAi { if (topStack.getApi() != ApiType.Sacrifice) { return false; // not sacrifice effect } - final int nTokens = AbilityUtils.calculateAmount(sa.getHostCard(), this.tokenAmount, sa); + final int nTokens = AbilityUtils.calculateAmount(sa.getHostCard(), tokenAmount, sa); final String valid = topStack.getParamOrDefault("SacValid", "Card.Self"); String num = sa.getParam("Amount"); num = (num == null) ? "1" : num; @@ -271,7 +243,8 @@ public class TokenAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - readParameters(sa); + String tokenAmount = sa.getParamOrDefault("TokenAmount", "1"); + final Card source = sa.getHostCard(); final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { @@ -282,12 +255,16 @@ public class TokenAi extends SpellAbilityAi { sa.getTargets().add(ai); } } - if ("X".equals(this.tokenAmount) || "X".equals(this.tokenPower) || "X".equals(this.tokenToughness)) { - int x = AbilityUtils.calculateAmount(source, this.tokenAmount, sa); - if (source.getSVar("X").equals("Count$xPaid")) { + Card actualToken = spawnToken(ai, sa); + String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString()); + String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString()); + + if ("X".equals(tokenAmount) || "X".equals(tokenPower) || "X".equals(tokenToughness)) { + int x = AbilityUtils.calculateAmount(source, tokenAmount, sa); + if (sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. - x = ComputerUtilMana.determineLeftoverMana(sa, ai); - source.setSVar("PayX", Integer.toString(x)); + x = ComputerUtilCost.getMaxXValue(sa, ai); + sa.setXManaCostPaid(x); } if (x <= 0) { return false; @@ -321,9 +298,7 @@ public class TokenAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, Iterable options) */ @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { - // TODO: AILogic - readParameters(sa); // remember to call this somewhere! + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { Combat combat = ai.getGame().getCombat(); // TokenAttacking if (combat != null && sa.hasParam("TokenAttacking")) { @@ -341,9 +316,7 @@ public class TokenAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayerOrPlaneswalker(forge.game.player.Player, forge.card.spellability.SpellAbility, Iterable options) */ @Override - protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options) { - // TODO: AILogic - readParameters(sa); // remember to call this somewhere! + protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options, Map params) { Combat combat = ai.getGame().getCombat(); // TokenAttacking if (combat != null && sa.hasParam("TokenAttacking")) { @@ -374,154 +347,22 @@ public class TokenAi extends SpellAbilityAi { * @param sa Token SpellAbility * @return token creature created by ability */ - @Deprecated public static Card spawnToken(Player ai, SpellAbility sa) { - return spawnToken(ai, sa, false); - } - - /** - * Create the token as a Card object. - * @param ai owner of the new token - * @param sa Token SpellAbility - * @param notNull if the token would not survive, still return it - * @return token creature created by ability - */ - // TODO Is this just completely copied from TokenEffect? Let's just call that thing - @Deprecated - public static Card spawnToken(Player ai, SpellAbility sa, boolean notNull) { - final Card host = sa.getHostCard(); - + if (!sa.hasParam("TokenScript")) { + throw new RuntimeException("Spell Ability has no TokenScript: " + sa); + } Card result = TokenInfo.getProtoType(sa.getParam("TokenScript"), sa); - if (result != null) { - result.setController(ai, 0); - return result; + if (result == null) { + throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript")); } - String[] tokenKeywords = sa.hasParam("TokenKeywords") ? sa.getParam("TokenKeywords").split("<>") : new String[0]; - String tokenPower = sa.getParam("TokenPower"); - String tokenToughness = sa.getParam("TokenToughness"); - String tokenName = sa.getParam("TokenName"); - String[] tokenTypes = sa.getParam("TokenTypes").split(","); - StringBuilder cost = new StringBuilder(); - String[] tokenColors = sa.getParam("TokenColors").split(","); - String tokenImage = sa.hasParam("TokenImage") ? PaperToken.makeTokenFileName(sa.getParam("TokenImage")) : ""; - String[] tokenAbilities = sa.hasParam("TokenAbilities") ? sa.getParam("TokenAbilities").split(",") : null; - String[] tokenTriggers = sa.hasParam("TokenTriggers") ? sa.getParam("TokenTriggers").split(",") : null; - String[] tokenSVars = sa.hasParam("TokenSVars") ? sa.getParam("TokenSVars").split(",") : null; - String[] tokenStaticAbilities = sa.hasParam("TokenStaticAbilities") ? sa.getParam("TokenStaticAbilities").split(",") : null; - String[] tokenHiddenKeywords = sa.hasParam("TokenHiddenKeywords") ? sa.getParam("TokenHiddenKeywords").split("&") : null; - final String[] substitutedColors = Arrays.copyOf(tokenColors, tokenColors.length); - for (int i = 0; i < substitutedColors.length; i++) { - if (substitutedColors[i].equals("ChosenColor")) { - // this currently only supports 1 chosen color - substitutedColors[i] = host.getChosenColor(); - } - } - StringBuilder colorDesc = new StringBuilder(); - for (final String col : substitutedColors) { - if (col.equalsIgnoreCase("White")) { - colorDesc.append("W "); - } else if (col.equalsIgnoreCase("Blue")) { - colorDesc.append("U "); - } else if (col.equalsIgnoreCase("Black")) { - colorDesc.append("B "); - } else if (col.equalsIgnoreCase("Red")) { - colorDesc.append("R "); - } else if (col.equalsIgnoreCase("Green")) { - colorDesc.append("G "); - } else if (col.equalsIgnoreCase("Colorless")) { - colorDesc = new StringBuilder("C"); - } - } - - final List imageNames = new ArrayList<>(1); - if (tokenImage.equals("")) { - imageNames.add(PaperToken.makeTokenFileName(TextUtil.fastReplace(colorDesc.toString(), " ", ""), tokenPower, tokenToughness, tokenName)); - } else { - imageNames.add(0, tokenImage); - } + result.setOwner(ai); - for (final char c : colorDesc.toString().toCharArray()) { - cost.append(c + ' '); - } - - cost = new StringBuilder(colorDesc.toString().replace('C', '1').trim()); - - final int finalPower = AbilityUtils.calculateAmount(host, tokenPower, sa); - final int finalToughness = AbilityUtils.calculateAmount(host, tokenToughness, sa); - - final String[] substitutedTypes = Arrays.copyOf(tokenTypes, tokenTypes.length); - for (int i = 0; i < substitutedTypes.length; i++) { - if (substitutedTypes[i].equals("ChosenType")) { - substitutedTypes[i] = host.getChosenType(); - } - } - final String substitutedName = tokenName.equals("ChosenType") ? host.getChosenType() : tokenName; - final String imageName = imageNames.get(MyRandom.getRandom().nextInt(imageNames.size())); - final TokenInfo tokenInfo = new TokenInfo(substitutedName, imageName, - cost.toString(), substitutedTypes, tokenKeywords, finalPower, finalToughness); - Card token = tokenInfo.makeOneToken(ai); - - if (token == null) { - return null; - } - - // Grant rule changes - if (tokenHiddenKeywords != null) { - for (final String s : tokenHiddenKeywords) { - token.addHiddenExtrinsicKeyword(s); - } - } - - // Grant abilities - if (tokenAbilities != null) { - for (final String s : tokenAbilities) { - final String actualAbility = host.getSVar(s); - final SpellAbility grantedAbility = AbilityFactory.getAbility(actualAbility, token); - token.addSpellAbility(grantedAbility); - } - } - - // Grant triggers - if (tokenTriggers != null) { - for (final String s : tokenTriggers) { - final String actualTrigger = host.getSVar(s); - final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, token, true); - final String ability = host.getSVar(parsedTrigger.getParam("Execute")); - parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(ability, token)); - token.addTrigger(parsedTrigger); - } - } - - // Grant SVars - if (tokenSVars != null) { - for (final String s : tokenSVars) { - String actualSVar = host.getSVar(s); - String name = s; - if (actualSVar.startsWith("SVar")) { - actualSVar = actualSVar.split("SVar:")[1]; - name = actualSVar.split(":")[0]; - actualSVar = actualSVar.split(":")[1]; - } - token.setSVar(name, actualSVar); - } - } - - // Grant static abilities - if (tokenStaticAbilities != null) { - for (final String s : tokenStaticAbilities) { - token.addStaticAbility(host.getSVar(s)); - } - } - - // Apply static abilities and prune dead tokens + // Apply static abilities final Game game = ai.getGame(); - ComputerUtilCard.applyStaticContPT(game, token, null); - if (!notNull && token.isCreature() && token.getNetToughness() < 1) { - return null; - } else { - return token; - } + ComputerUtilCard.applyStaticContPT(game, result, null); + return result; } + } diff --git a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java index 4c877974ff2..c40f1833b64 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java @@ -1,12 +1,11 @@ package forge.ai.ability; import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilMana; +import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.GameObject; import forge.game.ability.AbilityUtils; import forge.game.card.Card; -import forge.game.cost.Cost; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -23,30 +22,23 @@ public class UnattachAllAi extends SpellAbilityAi { */ @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - final Cost abCost = sa.getPayCosts(); - final Card source = sa.getHostCard(); - - if (abCost != null) { - // No Aura spells have Additional Costs - } // prevent run-away activations - first time will always return true boolean chance = MyRandom.getRandom().nextFloat() <= .9; // Attach spells always have a target - final TargetRestrictions tgt = sa.getTargetRestrictions(); - if (tgt != null) { + if (sa.usesTargeting()) { sa.resetTargets(); } - if (abCost != null && abCost.getTotalMana().countX() > 0 && source.getSVar("X").equals("Count$xPaid")) { - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); + if (sa.getSVar("X").equals("Count$xPaid")) { + final int xPay = ComputerUtilCost.getMaxXValue(sa, ai); if (xPay == 0) { return false; } - source.setSVar("PayX", Integer.toString(xPay)); + sa.setXManaCostPaid(xPay); } if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java index 5e2659fdacc..ecb45dd2ec8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -24,6 +24,7 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class UntapAi extends SpellAbilityAi { @Override @@ -50,27 +51,24 @@ public class UntapAi extends SpellAbilityAi { @Override protected boolean checkApiLogic(Player ai, SpellAbility sa) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); if (ComputerUtil.preventRunAwayActivations(sa)) { return false; } - if (tgt == null) { + if (!sa.usesTargeting()) { final List pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai; } else { - return untapPrefTargeting(ai, tgt, sa, false); + return untapPrefTargeting(ai, sa, false); } } @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); - - if (tgt == null) { + if (!sa.usesTargeting()) { if (mandatory) { return true; } @@ -79,7 +77,7 @@ public class UntapAi extends SpellAbilityAi { final List pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai; } else { - if (untapPrefTargeting(ai, tgt, sa, mandatory)) { + if (untapPrefTargeting(ai, sa, mandatory)) { return true; } else if (mandatory) { // not enough preferred targets, but mandatory so keep going: @@ -99,7 +97,7 @@ public class UntapAi extends SpellAbilityAi { if (tgt == null) { // who cares if its already untapped, it's only a subability? } else { - if (!untapPrefTargeting(ai, tgt, sa, false)) { + if (!untapPrefTargeting(ai, sa, false)) { return false; } } @@ -112,15 +110,13 @@ public class UntapAi extends SpellAbilityAi { * untapPrefTargeting. *

* - * @param tgt - * a {@link forge.game.spellability.TargetRestrictions} object. * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @param mandatory * a boolean. * @return a boolean. */ - private static boolean untapPrefTargeting(final Player ai, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) { + private static boolean untapPrefTargeting(final Player ai, final SpellAbility sa, final boolean mandatory) { final Card source = sa.getHostCard(); Player targetController = ai; @@ -130,7 +126,6 @@ public class UntapAi extends SpellAbilityAi { } CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa); - list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); if (list.isEmpty()) { return false; @@ -153,12 +148,11 @@ public class UntapAi extends SpellAbilityAi { // Try to avoid potential infinite recursion, // e.g. Kiora's Follower untapping another Kiora's Follower and repeating infinitely - if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostTap.class)) { + if (sa.getPayCosts().hasOnlySpecificCostType(CostTap.class)) { CardCollection toRemove = new CardCollection(); for (Card c : untapList) { for (SpellAbility ab : c.getAllSpellAbilities()) { if (ab.getApi() == ApiType.Untap - && ab.getPayCosts() != null && ab.getPayCosts().hasOnlySpecificCostType(CostTap.class) && ab.canTarget(source)) { toRemove.add(c); @@ -175,7 +169,7 @@ public class UntapAi extends SpellAbilityAi { untapList.removeAll(toExclude); sa.resetTargets(); - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { Card choice = null; if (untapList.isEmpty()) { @@ -183,7 +177,7 @@ public class UntapAi extends SpellAbilityAi { if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.Animate && !list.isEmpty() && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { choice = ComputerUtilCard.getWorstPermanentAI(list, false, false, false, false); - } else if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().getNumTargeted() == 0) { + } else if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); return false; } else { @@ -204,7 +198,7 @@ public class UntapAi extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().getNumTargeted() == 0) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); return false; } else { @@ -271,11 +265,11 @@ public class UntapAi extends SpellAbilityAi { return false; } - while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { + while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) { Card choice = null; if (tapList.isEmpty()) { - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(source, sa) || sa.getTargets().getNumTargeted() == 0) { + if (sa.getTargets().size() < tgt.getMinTargets(source, sa) || sa.getTargets().size() == 0) { if (!mandatory) { sa.resetTargets(); } @@ -293,7 +287,7 @@ public class UntapAi extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().getNumTargeted() == 0) { + if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().size() == 0) { if (!mandatory) { sa.resetTargets(); } @@ -312,7 +306,7 @@ public class UntapAi extends SpellAbilityAi { } @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable list, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable list, boolean isOptional, Player targetedPlayer, Map params) { PlayerCollection pl = new PlayerCollection(); pl.add(ai); pl.addAll(ai.getAllies()); diff --git a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java index 58aab03ecbb..33d23dd3b22 100644 --- a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java @@ -46,6 +46,12 @@ public class VoteAi extends SpellAbilityAi { @Override public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map params) { + if (params.containsKey("Voter")) { + Player p = (Player)params.get("Voter"); + if (p.isOpponentOf(player)) { + return min; + } + } if (sa.getActivatingPlayer().isOpponentOf(player)) { return min; } diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java index cd99a5a6d94..8cf8c3d9d0d 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -6,7 +6,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.LobbyPlayer; import forge.ai.LobbyPlayerAi; -import forge.card.CardStateName; import forge.game.*; import forge.game.card.*; import forge.game.card.token.TokenInfo; @@ -76,7 +75,7 @@ public class GameCopier { newPlayer.addSpellCastThisTurn(); for (int j = 0; j < origPlayer.getLandsPlayedThisTurn(); j++) newPlayer.addLandPlayedThisTurn(); - newPlayer.setCounters(Maps.newEnumMap(origPlayer.getCounters())); + newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters())); newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn()); newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn()); newPlayer.setPreventNextDamage(origPlayer.getPreventNextDamage()); @@ -125,8 +124,6 @@ public class GameCopier { } } } - origGame.validateSpabCache(); - newGame.validateSpabCache(); // Undo effects first before calculating them below, to avoid them applying twice. for (StaticEffect effect : origGame.getStaticEffects().getEffects()) { @@ -167,7 +164,7 @@ public class GameCopier { if (newSa != null) { newSa.setActivatingPlayer(map.map(origSa.getActivatingPlayer())); if (origSa.usesTargeting()) { - for (GameObject o : origSa.getTargets().getTargets()) { + for (GameObject o : origSa.getTargets()) { newSa.getTargets().add(map.map(o)); } } @@ -291,16 +288,9 @@ public class GameCopier { newCard.setTapped(true); } if (c.isFaceDown()) { - boolean isCreature = newCard.isCreature(); - boolean hasManaCost = !newCard.getManaCost().isNoCost(); newCard.turnFaceDown(true); if (c.isManifested()) { newCard.setManifested(true); - // TODO: Should be able to copy other abilities... - if (isCreature && hasManaCost) { - newCard.getState(CardStateName.Original).addSpellAbility( - CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost())); - } } } if (c.isMonstrous()) { @@ -330,7 +320,7 @@ public class GameCopier { Map counters = c.getCounters(); if (!counters.isEmpty()) { - newCard.setCounters(Maps.newEnumMap(counters)); + newCard.setCounters(Maps.newHashMap(counters)); } if (c.getChosenPlayer() != null) { newCard.setChosenPlayer(playerMap.get(c.getChosenPlayer())); @@ -338,13 +328,18 @@ public class GameCopier { if (!c.getChosenType().isEmpty()) { newCard.setChosenType(c.getChosenType()); } + if (!c.getChosenType2().isEmpty()) { + newCard.setChosenType2(c.getChosenType2()); + } if (c.getChosenColors() != null) { newCard.setChosenColors(Lists.newArrayList(c.getChosenColors())); } if (!c.getNamedCard().isEmpty()) { newCard.setNamedCard(c.getNamedCard()); } - + if (!c.getNamedCard2().isEmpty()) { + newCard.setNamedCard2(c.getNamedCard()); + } newCard.setSVars(c.getSVars()); } diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java index c1d0fac1e32..927b315bf00 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java @@ -16,8 +16,6 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetChoices; -import forge.game.spellability.TargetRestrictions; -import forge.util.TextUtil; public class GameSimulator { public static boolean COPY_STACK = false; @@ -125,21 +123,15 @@ public class GameSimulator { } private SpellAbility findSaInSimGame(SpellAbility sa) { + // is already an ability from sim game + if (sa.getHostCard().getGame().equals(this.simGame)) { + return sa; + } Card origHostCard = sa.getHostCard(); Card hostCard = (Card) copier.find(origHostCard); - SpellAbility saOriginal = sa.getMayPlayOriginal(); String desc = sa.getDescription(); - if (saOriginal != null) { - // This is needed when it's an alternate cost SA and the desc string has - // been modified to add "by Foo" to it. TODO: Do we also need to do this in - // other places where we compare descriptions? - desc = saOriginal.getDescription(); - System.err.println(sa.getDescription() + "->" + desc); - } - // FIXME: This is a hack that makes testManifest pass - figure out why it's needed. - desc = TextUtil.fastReplace(desc, "Unmanifest {0}", "Unmanifest no cost"); for (SpellAbility cSa : hostCard.getSpellAbilities()) { - if (desc.equals(cSa.getDescription())) { + if (desc.startsWith(cSa.getDescription())) { return cSa; } } @@ -171,14 +163,12 @@ public class GameSimulator { SpellAbility saOrSubSa = sa; do { if (origSaOrSubSa.usesTargeting()) { - final boolean divided = origSaOrSubSa.hasParam("DividedAsYouChoose"); - final TargetRestrictions origTgtRes = origSaOrSubSa.getTargetRestrictions(); - final TargetRestrictions tgtRes = saOrSubSa.getTargetRestrictions(); - for (final GameObject o : origSaOrSubSa.getTargets().getTargets()) { + final boolean divided = origSaOrSubSa.isDividedAsYouChoose(); + for (final GameObject o : origSaOrSubSa.getTargets()) { final GameObject target = copier.find(o); saOrSubSa.getTargets().add(target); if (divided) { - tgtRes.addDividedAllocation(target, origTgtRes.getDividedValue(o)); + saOrSubSa.addDividedAllocation(target, origSaOrSubSa.getDividedValue(o)); } } } @@ -189,7 +179,7 @@ public class GameSimulator { if (debugPrint && !sa.getAllTargetChoices().isEmpty()) { debugPrint("Targets: "); for (TargetChoices target : sa.getAllTargetChoices()) { - System.out.print(target.getTargetedString()); + System.out.print(target); } System.out.println(); } diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java index f2d0f01bf85..e3dc9e8a64c 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java @@ -3,7 +3,7 @@ package forge.ai.simulation; import forge.ai.CreatureEvaluator; import forge.game.Game; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.zone.ZoneType; @@ -154,7 +154,7 @@ public class GameStateEvaluator { // e.g. a 5 CMC permanent results in 200, whereas a 5/5 creature is ~225 int value = 50 + 30 * c.getCMC(); if (c.isPlaneswalker()) { - value += 2 * c.getCounters(CounterType.LOYALTY); + value += 2 * c.getCounters(CounterEnumType.LOYALTY); } return value; } diff --git a/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java b/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java index 8d3e3f06321..18e158e9a23 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java +++ b/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java @@ -167,22 +167,21 @@ public class PossibleTargetSelector { private void selectTargetsByIndexImpl(int index) { targetingSa.resetTargets(); - while (targetingSa.getTargets().getNumTargeted() < maxTargets && index < validTargets.size()) { + while (targetingSa.getTargets().size() < maxTargets && index < validTargets.size()) { targetingSa.getTargets().add(validTargets.get(index++)); } // Divide up counters, since AI is expected to do this. For now, // divided evenly with left-overs going to the first target. - if (targetingSa.hasParam("DividedAsYouChoose")) { + if (targetingSa.isDividedAsYouChoose()) { final int targetCount = targetingSa.getTargets().getTargetCards().size(); if (targetCount > 0) { final String amountStr = targetingSa.getParam("CounterNum"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, targetingSa); final int amountPerCard = amount / targetCount; int amountLeftOver = amount - (amountPerCard * targetCount); - final TargetRestrictions tgtRes = targetingSa.getTargetRestrictions(); - for (GameObject target : targetingSa.getTargets().getTargets()) { - tgtRes.addDividedAllocation(target, amountPerCard + amountLeftOver); + for (GameObject target : targetingSa.getTargets()) { + targetingSa.addDividedAllocation(target, amountPerCard + amountLeftOver); amountLeftOver = 0; } } @@ -190,7 +189,7 @@ public class PossibleTargetSelector { } public Targets getLastSelectedTargets() { - return new Targets(targetingSaIndex, validTargets.size(), targetIndex - 1, targetingSa.getTargets().getTargetedString()); + return new Targets(targetingSaIndex, validTargets.size(), targetIndex - 1, targetingSa.getTargets().toString()); } public boolean selectTargetsByIndex(int targetIndex) { diff --git a/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java b/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java index f6588af7fe0..0bf94982686 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java @@ -177,10 +177,10 @@ public class SimulationController { saOrSubSa = saOrSubSa.getSubAbility(); } - if (saOrSubSa == null || saOrSubSa.getTargets() == null || saOrSubSa.getTargets().getTargets().size() != 1) { + if (saOrSubSa == null || saOrSubSa.getTargets() == null || saOrSubSa.getTargets().size() != 1) { return null; } - GameObject target = saOrSubSa.getTargets().getTargets().get(0); + GameObject target = saOrSubSa.getTargets().get(0); GameObject originalTarget = target; if (!(target instanceof Card)) { return null; } Card hostCard = sa.getHostCard(); diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java index e93a4b5ebfc..9d98f72b8d9 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java @@ -9,7 +9,6 @@ import forge.ai.ability.ExploreAi; import forge.ai.simulation.GameStateEvaluator.Score; import forge.game.Game; import forge.game.ability.ApiType; -import forge.game.ability.effects.CharmEffect; import forge.game.card.*; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -147,7 +146,7 @@ public class SpellAbilityPicker { return false; } if (sa.isSpell()) { - return !sa.getHostCard().isInstant() && !sa.getHostCard().withFlash(player); + return !sa.withFlash(sa.getHostCard(), player); } if (sa.isPwAbility()) { return !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."); @@ -285,7 +284,7 @@ public class SpellAbilityPicker { SpellAbility saOrSubSa = sa; do { if (saOrSubSa.usesTargeting()) { - saString.append(" (targets: ").append(saOrSubSa.getTargets().getTargetedString()).append(")"); + saString.append(" (targets: ").append(saOrSubSa.getTargets()).append(")"); } saOrSubSa = saOrSubSa.getSubAbility(); } while (saOrSubSa != null); @@ -371,14 +370,12 @@ public class SpellAbilityPicker { return bestScore; } - public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { + public List chooseModeForAbility(SpellAbility sa, List choices, int min, int num, boolean allowRepeat) { if (interceptor != null) { - List choices = CharmEffect.makePossibleOptions(sa); return interceptor.chooseModesForAbility(choices, min, num, allowRepeat); } if (plan != null && plan.getSelectedDecision() != null && plan.getSelectedDecision().modes != null) { Plan.Decision decision = plan.getSelectedDecision(); - List choices = CharmEffect.makePossibleOptions(sa); // TODO: Validate that there's no discrepancies between choices and modes? List plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes); if (plan.getSelectedDecision().targets != null) { diff --git a/forge-core/pom.xml b/forge-core/pom.xml index 9c13b0d69aa..8bae142c004 100644 --- a/forge-core/pom.xml +++ b/forge-core/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.39-SNAPSHOT forge-core diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index e07f836566d..f4f168a5c8c 100644 --- a/forge-core/src/main/java/forge/ImageKeys.java +++ b/forge-core/src/main/java/forge/ImageKeys.java @@ -6,6 +6,8 @@ import forge.util.ImageUtil; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; + + import java.io.File; import java.util.HashMap; import java.util.Map; @@ -23,6 +25,7 @@ public final class ImageKeys { public static final String HIDDEN_CARD = "hidden"; public static final String MORPH_IMAGE = "morph"; public static final String MANIFEST_IMAGE = "manifest"; + public static final String FORETELL_IMAGE = "foretell"; public static final String BACKFACE_POSTFIX = "$alt"; @@ -113,7 +116,17 @@ public final class ImageKeys { } //try fullborder... if (filename.contains(".full")) { - file = findFile(dir, TextUtil.fastReplace(filename, ".full", ".fullborder")); + String fullborderFile = TextUtil.fastReplace(filename, ".full", ".fullborder"); + file = findFile(dir, fullborderFile); + if (file != null) { return file; } + // if there's a 1st art variant try without it for .fullborder images + file = findFile(dir, TextUtil.fastReplace(fullborderFile, "1.fullborder", ".fullborder")); + if (file != null) { return file; } + // if there's an art variant try without it for .full images + file = findFile(dir, filename.replaceAll("[0-9].full",".full")); + if (file != null) { return file; } + // if there's a 1st art variant try with it for .full images + file = findFile(dir, filename.replaceAll("[0-9]*.full", "1.full")); if (file != null) { return file; } } //if an image, like phenomenon or planes is missing .full in their filenames but you have an existing images that have .full/.fullborder @@ -138,6 +151,9 @@ public final class ImageKeys { // try with upper case set file = findFile(dir, setlessFilename + "_" + setCode.toUpperCase()); if (file != null) { return file; } + // try with lower case set + file = findFile(dir, setlessFilename + "_" + setCode.toLowerCase()); + if (file != null) { return file; } // try without set name file = findFile(dir, setlessFilename); if (file != null) { return file; } @@ -152,8 +168,12 @@ public final class ImageKeys { file = findFile(dir, setlessFilename); if (file != null) { return file; } - // try lowering the art index to the minimum for regular cards if (setlessFilename.contains(".full")) { + //try fullborder + String fullborderFile = TextUtil.fastReplace(setlessFilename, ".full", ".fullborder"); + file = findFile(dir, fullborderFile); + if (file != null) { return file; } + // try lowering the art index to the minimum for regular cards file = findFile(dir, setlessFilename.replaceAll("[0-9]*[.]full", "1.full")); if (file != null) { return file; } } diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java index 6b65424daab..83885198401 100644 --- a/forge-core/src/main/java/forge/StaticData.java +++ b/forge-core/src/main/java/forge/StaticData.java @@ -54,11 +54,11 @@ public class StaticData { private static StaticData lastInstance = null; - public StaticData(CardStorageReader cardReader, String editionFolder, String blockDataFolder) { - this(cardReader, null, editionFolder, blockDataFolder); + public StaticData(CardStorageReader cardReader, String editionFolder, String blockDataFolder, boolean enableUnknownCards, boolean loadNonLegalCards) { + this(cardReader, null, editionFolder, blockDataFolder, enableUnknownCards, loadNonLegalCards); } - public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, String editionFolder, String blockDataFolder) { + public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, String editionFolder, String blockDataFolder, boolean enableUnknownCards, boolean loadNonLegalCards) { this.cardReader = cardReader; this.tokenReader = tokenReader; this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder))); @@ -84,8 +84,8 @@ public class StaticData { variantCards = new CardDb(variantsCards, editions); //must initialize after establish field values for the sake of card image logic - commonCards.initialize(false, false); - variantCards.initialize(false, false); + commonCards.initialize(false, false, enableUnknownCards, loadNonLegalCards); + variantCards.initialize(false, false, enableUnknownCards, loadNonLegalCards); } { @@ -149,6 +149,7 @@ public class StaticData { } } + // TODO Remove these in favor of them being associated to the Edition /** @return {@link forge.util.storage.IStorage}<{@link forge.item.SealedProduct.Template}> */ public IStorage getFatPacks() { if (fatPacks == null) @@ -156,12 +157,6 @@ public class StaticData { return fatPacks; } - public IStorage getBoosterBoxes() { - if (boosterBoxes == null) - boosterBoxes = new StorageBase<>("Booster boxes", new BoosterBox.Template.Reader(blockDataFolder + "boosterboxes.txt")); - return boosterBoxes; - } - /** @return {@link forge.util.storage.IStorage}<{@link forge.item.SealedProduct.Template}> */ public final IStorage getTournamentPacks() { if (tournaments == null) @@ -184,7 +179,7 @@ public class StaticData { public IStorage getPrintSheets() { if (printSheets == null) - printSheets = new StorageBase<>("Special print runs", new PrintSheet.Reader(new File(blockDataFolder, "printsheets.txt"))); + printSheets = PrintSheet.initializePrintSheets(new File(blockDataFolder, "printsheets.txt"), getEditions()); return printSheets; } @@ -215,7 +210,7 @@ public class StaticData { public Predicate getStandardPredicate() { return standardPredicate; } public Predicate getPioneerPredicate() { return pioneerPredicate; } - + public Predicate getModernPredicate() { return modernPredicate; } public Predicate getCommanderPredicate() { return commanderPredicate; } diff --git a/forge-core/src/main/java/forge/card/CardAiHints.java b/forge-core/src/main/java/forge/card/CardAiHints.java index b83e8459438..a7b2c8e7255 100644 --- a/forge-core/src/main/java/forge/card/CardAiHints.java +++ b/forge-core/src/main/java/forge/card/CardAiHints.java @@ -9,15 +9,17 @@ public class CardAiHints { private final boolean isRemovedFromAIDecks; private final boolean isRemovedFromRandomDecks; + private final boolean isRemovedFromNonCommanderDecks; private final DeckHints deckHints; private final DeckHints deckNeeds; private final DeckHints deckHas; - public CardAiHints(boolean remAi, boolean remRandom, DeckHints dh, DeckHints dn, DeckHints has) { + public CardAiHints(boolean remAi, boolean remRandom, boolean remUnlessCommander, DeckHints dh, DeckHints dn, DeckHints has) { isRemovedFromAIDecks = remAi; isRemovedFromRandomDecks = remRandom; + isRemovedFromNonCommanderDecks = remUnlessCommander; deckHints = dh; deckNeeds = dn; deckHas = has; @@ -42,8 +44,17 @@ public class CardAiHints { } /** - * @return the deckHints + * Gets the rem random decks. + * + * @return the rem random decks */ + public boolean getRemNonCommanderDecks() { + return this.isRemovedFromNonCommanderDecks; + } + + /** + * @return the deckHints + */ public DeckHints getDeckHints() { return deckHints; } diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index 2c4e54ad21a..10a8b49ae2c 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -50,8 +50,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { private final Map alternateName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); private final Map artIds = new HashMap<>(); - private final List allCards = new ArrayList<>(); - private final List roAllCards = Collections.unmodifiableList(allCards); + private final Collection roAllCards = Collections.unmodifiableCollection(allCardsByName.values()); private final CardEdition.Collection editions; public enum SetPreference { @@ -153,9 +152,11 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { public void loadCard(String cardName, CardRules cr) { rulesByName.put(cardName, cr); + // This seems very unperformant. Does this get called often? + System.out.println("Inside loading card"); for (CardEdition e : editions) { - for (CardInSet cis : e.getCards()) { + for (CardInSet cis : e.getAllCardsInSet()) { if (cis.name.equalsIgnoreCase(cardName)) { addSetCard(e, cis, cr); } @@ -165,7 +166,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { reIndex(); } - public void initialize(boolean logMissingPerEdition, boolean logMissingSummary) { + public void initialize(boolean logMissingPerEdition, boolean logMissingSummary, boolean enableUnknownCards, boolean loadNonLegalCards) { Set allMissingCards = new LinkedHashSet<>(); List missingCards = new ArrayList<>(); CardEdition upcomingSet = null; @@ -174,15 +175,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { for (CardEdition e : editions.getOrderedEditions()) { boolean coreOrExpSet = e.getType() == CardEdition.Type.CORE || e.getType() == CardEdition.Type.EXPANSION; boolean isCoreExpSet = coreOrExpSet || e.getType() == CardEdition.Type.REPRINT; + //todo sets with nonlegal cards should have tags in them so we don't need to specify the code here + boolean skip = !loadNonLegalCards && (e.getCode().equals("CMB1") || e.getBorderColor() == CardEdition.BorderColor.SILVER); if (logMissingPerEdition && isCoreExpSet) { - System.out.print(e.getName() + " (" + e.getCards().length + " cards)"); + System.out.print(e.getName() + " (" + e.getAllCardsInSet().size() + " cards)"); } - if (coreOrExpSet && e.getDate().after(today)) { - upcomingSet = e; + if (coreOrExpSet && e.getDate().after(today) && upcomingSet == null) { + if (skip) + upcomingSet = e; } - for (CardEdition.CardInSet cis : e.getCards()) { + for (CardEdition.CardInSet cis : e.getAllCardsInSet()) { CardRules cr = rulesByName.get(cis.name); + if (cr != null && !cr.getType().isBasicLand() && skip) + continue; + if (cr != null) { addSetCard(e, cis, cr); } @@ -195,7 +202,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { System.out.println(" ... 100% "); } else { - int missing = (e.getCards().length - missingCards.size()) * 10000 / e.getCards().length; + int missing = (e.getAllCardsInSet().size() - missingCards.size()) * 10000 / e.getAllCardsInSet().size(); System.out.printf(" ... %.2f%% (%s missing: %s)%n", missing * 0.01f, Lang.nounWithAmount(missingCards.size(), "card"), StringUtils.join(missingCards, " | ")); } } @@ -218,7 +225,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { if (!contains(cr.getName())) { if (upcomingSet != null) { addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown, 1)); - } else { + } else if(enableUnknownCards) { System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. "); addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special, 1)); } @@ -245,10 +252,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { private void reIndex() { uniqueCardsByName.clear(); - allCards.clear(); for (Entry> kv : allCardsByName.asMap().entrySet()) { uniqueCardsByName.put(kv.getKey(), getFirstWithImage(kv.getValue())); - allCards.addAll(kv.getValue()); } } @@ -312,17 +317,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { return tryGetCard(request); } - public int getCardCollectorNumber(String cardName, String reqEdition) { + public String getCardCollectorNumber(String cardName, String reqEdition, int artIndex) { cardName = getName(cardName); CardEdition edition = editions.get(reqEdition); if (edition == null) - return -1; - for (CardInSet card : edition.getCards()) { + return null; + int numMatches = 0; + for (CardInSet card : edition.getAllCardsInSet()) { if (card.name.equalsIgnoreCase(cardName)) { - return card.collectorNumber; + numMatches += 1; + if (numMatches == artIndex) { + return card.collectorNumber; + } } } - return -1; + return null; } private PaperCard tryGetCard(CardRequest request) { @@ -531,7 +540,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { } @Override - public List getAllCards() { + public Collection getAllCards() { return roAllCards; } @@ -558,7 +567,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { public List getAllCardsFromEdition(CardEdition edition) { List cards = Lists.newArrayList(); - for(CardInSet cis : edition.getCards()) { + for(CardInSet cis : edition.getAllCardsInSet()) { PaperCard card = this.getCard(cis.name, edition.getCode()); if (card == null) { // Just in case the card is listed in the edition file but Forge doesn't support it @@ -650,7 +659,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { // May iterate over editions and find out if there is any card named 'cardName' but not implemented with Forge script. if (StringUtils.isBlank(request.edition)) { for (CardEdition edition : editions) { - for (CardInSet cardInSet : edition.getCards()) { + for (CardInSet cardInSet : edition.getAllCardsInSet()) { if (cardInSet.name.equals(request.cardName)) { cardEdition = edition; cardRarity = cardInSet.rarity; @@ -664,7 +673,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { } else { cardEdition = editions.get(request.edition); if (cardEdition != null) { - for (CardInSet cardInSet : cardEdition.getCards()) { + for (CardInSet cardInSet : cardEdition.getAllCardsInSet()) { if (cardInSet.name.equals(request.cardName)) { cardRarity = cardInSet.rarity; break; @@ -705,9 +714,10 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { // 1. generate all paper cards from edition data we have (either explicit, or found in res/editions, or add to unknown edition) List paperCards = new ArrayList<>(); if (null == whenItWasPrinted || whenItWasPrinted.isEmpty()) { + // TODO Not performant Each time we "putCard" we loop through ALL CARDS IN ALL editions for (CardEdition e : editions.getOrderedEditions()) { int artIdx = 1; - for (CardInSet cis : e.getCards()) { + for (CardInSet cis : e.getAllCardsInSet()) { if (!cis.name.equals(cardName)) { continue; } diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index e2b786c0322..8ccc50552bf 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -19,8 +19,7 @@ package forge.card; import com.google.common.base.Function; import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; +import com.google.common.collect.*; import forge.StaticData; import forge.card.CardDb.SetPreference; import forge.deck.CardPool; @@ -38,6 +37,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** @@ -64,7 +65,17 @@ public final class CardEdition implements Comparable { // immutable FROM_THE_VAULT, OTHER, - THIRDPARTY // custom sets + THIRDPARTY; // custom sets + + public String getBoosterBoxDefault() { + switch (this) { + case CORE: + case EXPANSION: + return "36"; + default: + return "0"; + } + } } public enum FoilType { @@ -73,12 +84,51 @@ public final class CardEdition implements Comparable { // immutable MODERN // 8th Edition and newer } + public enum BorderColor { + WHITE, + BLACK, + SILVER, + GOLD + } + + // reserved names of sections inside edition files, that are not parsed as cards + private static final List reservedSectionNames = ImmutableList.of("metadata", "tokens"); + + // commonly used printsheets with collector number + public enum EditionSectionWithCollectorNumbers { + CARDS("cards"), + PRECON_PRODUCT("precon product"), + BORDERLESS("borderless"), + SHOWCASE("showcase"), + EXTENDED_ART("extended art"), + ALTERNATE_ART("alternate art"), + BUY_A_BOX("buy a box"), + PROMO("promo"); + + private final String name; + + EditionSectionWithCollectorNumbers(final String n) { this.name = n; } + + public String getName() { + return name; + } + + public static List getNames() { + List list = new ArrayList<>(); + for (EditionSectionWithCollectorNumbers s : EditionSectionWithCollectorNumbers.values()) { + String sName = s.getName(); + list.add(sName); + } + return list; + } + } + public static class CardInSet { public final CardRarity rarity; - public final int collectorNumber; + public final String collectorNumber; public final String name; - public CardInSet(final String name, final int collectorNumber, final CardRarity rarity) { + public CardInSet(final String name, final String collectorNumber, final CardRarity rarity) { this.name = name; this.collectorNumber = collectorNumber; this.rarity = rarity; @@ -86,7 +136,7 @@ public final class CardEdition implements Comparable { // immutable public String toString() { StringBuilder sb = new StringBuilder(); - if (collectorNumber != -1) { + if (collectorNumber != null) { sb.append(collectorNumber); sb.append(' '); } @@ -110,31 +160,45 @@ public final class CardEdition implements Comparable { // immutable private Type type; private String name; private String alias = null; + private BorderColor borderColor = BorderColor.BLACK; + + // SealedProduct private String prerelease = null; - private boolean whiteBorder = false; + private int boosterBoxCount = 36; + + // Booster/draft info + private boolean smallSetOverride = false; + private boolean foilAlwaysInCommonSlot = false; private FoilType foilType = FoilType.NOT_SUPPORTED; private double foilChanceInBooster = 0; - private boolean foilAlwaysInCommonSlot = false; private double chanceReplaceCommonWith = 0; private String slotReplaceCommonWith = "Common"; private String additionalSheetForFoils = ""; private String additionalUnlockSet = ""; - private boolean smallSetOverride = false; private String boosterMustContain = ""; - private final CardInSet[] cards; + private String boosterReplaceSlotFromPrintSheet = ""; + private String doublePickDuringDraft = ""; + private String[] chaosDraftThemes = new String[0]; + + private final ListMultimap cardMap; private final Map tokenNormalized; + // custom print sheets that will be loaded lazily + private final Map> customPrintSheetsToParse; private int boosterArts = 1; private SealedProduct.Template boosterTpl = null; - private CardEdition(CardInSet[] cards) { - this.cards = cards; - tokenNormalized = new HashMap<>(); + private CardEdition(ListMultimap cardMap, Map tokens, Map> customPrintSheetsToParse) { + this.cardMap = cardMap; + this.tokenNormalized = tokens; + this.customPrintSheetsToParse = customPrintSheetsToParse; } private CardEdition(CardInSet[] cards, Map tokens) { - this.cards = cards; + this.cardMap = ArrayListMultimap.create(); + this.cardMap.replaceValues("cards", Arrays.asList(cards)); this.tokenNormalized = tokens; + this.customPrintSheetsToParse = new HashMap<>(); } /** @@ -152,7 +216,7 @@ public final class CardEdition implements Comparable { // immutable * @param cards the cards in the set */ private CardEdition(String date, String code, String code2, String mciCode, Type type, String name, FoilType foil, CardInSet[] cards) { - this(cards); + this(cards, new HashMap<>()); this.code = code; this.code2 = code2; this.mciCode = mciCode; @@ -179,7 +243,10 @@ public final class CardEdition implements Comparable { // immutable public Type getType() { return type; } public String getName() { return name; } public String getAlias() { return alias; } + public String getPrerelease() { return prerelease; } + public int getBoosterBoxCount() { return boosterBoxCount; } + public FoilType getFoilType() { return foilType; } public double getFoilChanceInBooster() { return foilChanceInBooster; } public boolean getFoilAlwaysInCommonSlot() { return foilAlwaysInCommonSlot; } @@ -188,8 +255,17 @@ public final class CardEdition implements Comparable { // immutable public String getAdditionalSheetForFoils() { return additionalSheetForFoils; } public String getAdditionalUnlockSet() { return additionalUnlockSet; } public boolean getSmallSetOverride() { return smallSetOverride; } + public String getDoublePickDuringDraft() { return doublePickDuringDraft; } public String getBoosterMustContain() { return boosterMustContain; } - public CardInSet[] getCards() { return cards; } + public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; } + public String[] getChaosDraftThemes() { return chaosDraftThemes; } + + public List getCards() { return cardMap.get("cards"); } + public List getAllCardsInSet() { + return Lists.newArrayList(cardMap.values()); + } + + public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others public Map getTokens() { return tokenNormalized; } @@ -234,12 +310,12 @@ public final class CardEdition implements Comparable { // immutable return this.name + " (" + this.code + ")"; } - public boolean isWhiteBorder() { - return whiteBorder; + public BorderColor getBorderColor() { + return borderColor; } public boolean isLargeSet() { - return cards.length > 200 && !smallSetOverride; + return getAllCardsInSet().size() > 200 && !smallSetOverride; } public int getCntBoosterPictures() { @@ -254,6 +330,40 @@ public final class CardEdition implements Comparable { // immutable return boosterTpl != null; } + public List getPrintSheetsBySection() { + final CardDb cardDb = StaticData.instance().getCommonCards(); + Map cardToIndex = new HashMap<>(); + + List sheets = Lists.newArrayList(); + for(String sectionName : cardMap.keySet()) { + PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName)); + + List cards = cardMap.get(sectionName); + for(CardInSet card : cards) { + int index = 1; + if (cardToIndex.containsKey(card.name)) { + index = cardToIndex.get(card.name); + } + + cardToIndex.put(card.name, index); + + PaperCard pCard = cardDb.getCard(card.name, this.getCode(), index); + sheet.add(pCard); + } + + sheets.add(sheet); + } + + for(String sheetName : customPrintSheetsToParse.keySet()) { + List sheetToParse = customPrintSheetsToParse.get(sheetName); + CardPool sheetPool = CardPool.fromCardList(sheetToParse); + PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sheetName), sheetPool); + sheets.add(sheet); + } + + return sheets; + } + public static class Reader extends StorageReaderFolder { public Reader(File path) { super(path, CardEdition.FN_GET_CODE); @@ -263,30 +373,60 @@ public final class CardEdition implements Comparable { // immutable protected CardEdition read(File file) { final Map> contents = FileSection.parseSections(FileUtil.readFile(file)); + final Pattern pattern = Pattern.compile( + /* + The following pattern will match the WAR Japanese art entries, + it should also match the Un-set and older alternate art cards + like Merseine from FEM (should the editions files ever be updated) + */ + //"(^(?[0-9]+.?) )?((?[SCURML]) )?(?.*)$" + /* Ideally we'd use the named group above, but Android 6 and + earlier don't appear to support named groups. + So, untill support for those devices is officially dropped, + we'll have to suffice with numbered groups. + We are looking for: + * cnum - grouping #2 + * rarity - grouping #4 + * name - grouping #5 + */ + "(^([0-9]+.?) )?(([SCURML]) )?(.*)$" + ); + + ListMultimap cardMap = ArrayListMultimap.create(); Map tokenNormalized = new HashMap<>(); - List processedCards = new ArrayList<>(); - if (contents.containsKey("cards")) { - for(String line : contents.get("cards")) { - if (StringUtils.isBlank(line)) - continue; + Map> customPrintSheetsToParse = new HashMap<>(); + List editionSectionsWithCollectorNumbers = EditionSectionWithCollectorNumbers.getNames(); - // Optional collector number at the start. - String[] split = line.split(" ", 2); - int collectorNumber = -1; - if (split.length >= 2 && StringUtils.isNumeric(split[0])) { - collectorNumber = Integer.parseInt(split[0]); - line = split[1]; + for (String sectionName : contents.keySet()) { + // skip reserved section names like 'metadata' and 'tokens' that are handled separately + if (reservedSectionNames.contains(sectionName)) { + continue; + } + // parse sections of the format " " + if (editionSectionsWithCollectorNumbers.contains(sectionName)) { + for(String line : contents.get(sectionName)) { + Matcher matcher = pattern.matcher(line); + + if (!matcher.matches()) { + continue; + } + + String collectorNumber = matcher.group(2); + CardRarity r = CardRarity.smartValueOf(matcher.group(4)); + String cardName = matcher.group(5); + CardInSet cis = new CardInSet(cardName, collectorNumber, r); + + cardMap.put(sectionName, cis); } - - // You may omit rarity for early development - CardRarity r = CardRarity.smartValueOf(line.substring(0, 1)); - boolean hadRarity = r != CardRarity.Unknown && line.charAt(1) == ' '; - String cardName = hadRarity ? line.substring(2) : line; - CardInSet cis = new CardInSet(cardName, collectorNumber, r); - processedCards.add(cis); + } + // save custom print sheets of the format " ||" + // to parse later when printsheets are loaded lazily (and the cardpool is already initialized) + else { + customPrintSheetsToParse.put(sectionName, contents.get(sectionName)); } } + // parse tokens section if (contents.containsKey("tokens")) { for(String line : contents.get("tokens")) { if (StringUtils.isBlank(line)) @@ -300,11 +440,9 @@ public final class CardEdition implements Comparable { // immutable } } - CardEdition res = new CardEdition( - processedCards.toArray(new CardInSet[processedCards.size()]), - tokenNormalized - ); + CardEdition res = new CardEdition(cardMap, tokenNormalized, customPrintSheetsToParse); + // parse metadata section FileSection section = FileSection.parse(contents.get("metadata"), FileSection.EQUALS_KV_SEPARATOR); res.name = section.get("name"); res.date = parseDate(section.get("date")); @@ -323,7 +461,7 @@ public final class CardEdition implements Comparable { // immutable res.boosterTpl = boosterDesc == null ? null : new SealedProduct.Template(res.code, SealedProduct.Template.Reader.parseSlots(boosterDesc)); res.alias = section.get("alias"); - res.whiteBorder = "white".equalsIgnoreCase(section.get("border")); + res.borderColor = BorderColor.valueOf(section.get("border", "Black").toUpperCase(Locale.ENGLISH)); String type = section.get("type"); Type enumType = Type.UNKNOWN; if (null != type && !type.isEmpty()) { @@ -336,6 +474,7 @@ public final class CardEdition implements Comparable { // immutable } res.type = enumType; res.prerelease = section.get("Prerelease", null); + res.boosterBoxCount = Integer.parseInt(section.get("BoosterBox", enumType.getBoosterBoxDefault())); switch(section.get("foil", "newstyle").toLowerCase()) { case "notsupported": @@ -365,8 +504,13 @@ public final class CardEdition implements Comparable { // immutable res.additionalUnlockSet = section.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral res.smallSetOverride = section.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon) + res.doublePickDuringDraft = section.get("DoublePick", ""); // "FirstPick" or "Always" res.boosterMustContain = section.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature + res.boosterReplaceSlotFromPrintSheet = section.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card + + res.chaosDraftThemes = section.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names + return res; } @@ -554,7 +698,7 @@ public final class CardEdition implements Comparable { // immutable private static class CanMakeBoosterBox implements Predicate { @Override public boolean apply(final CardEdition subject) { - return StaticData.instance().getBoosterBoxes().contains(subject.getCode()); + return subject.getBoosterBoxCount() > 0; } } diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index 322c003b6f2..1023b6a13d4 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -222,7 +222,12 @@ public final class CardRules implements ICardCharacteristics { public boolean canBeBrawlCommander() { CardType type = mainPart.getType(); - return (type.isLegendary() && type.isCreature()) || type.isPlaneswalker(); + return type.isLegendary() && (type.isCreature() || type.isPlaneswalker()); + } + + public boolean canBeTinyLeadersCommander() { + CardType type = mainPart.getType(); + return type.isLegendary() && (type.isCreature() || type.isPlaneswalker()); } public String getMeldWith() { @@ -286,6 +291,7 @@ public final class CardRules implements ICardCharacteristics { // fields to build CardAiHints private boolean removedFromAIDecks = false; private boolean removedFromRandomDecks = false; + private boolean removedFromNonCommanderDecks = false; private DeckHints hints = null; private DeckHints needs = null; private DeckHints has = null; @@ -305,6 +311,7 @@ public final class CardRules implements ICardCharacteristics { this.removedFromAIDecks = false; this.removedFromRandomDecks = false; + this.removedFromNonCommanderDecks = false; this.needs = null; this.hints = null; this.has = null; @@ -319,7 +326,7 @@ public final class CardRules implements ICardCharacteristics { * @return the card */ public final CardRules getCard() { - CardAiHints cah = new CardAiHints(removedFromAIDecks, removedFromRandomDecks, hints, needs, has); + CardAiHints cah = new CardAiHints(removedFromAIDecks, removedFromRandomDecks, removedFromNonCommanderDecks, hints, needs, has); faces[0].assignMissingFields(); if (null != faces[1]) faces[1].assignMissingFields(); final CardRules result = new CardRules(faces, altMode, cah); @@ -372,6 +379,7 @@ public final class CardRules implements ICardCharacteristics { if ( "RemoveDeck".equals(variable) ) { this.removedFromAIDecks = "All".equalsIgnoreCase(value); this.removedFromRandomDecks = "Random".equalsIgnoreCase(value); + this.removedFromNonCommanderDecks = "NonCommander".equalsIgnoreCase(value); } } else if ("AlternateMode".equals(key)) { //System.out.println(faces[curFace].getName()); @@ -479,7 +487,7 @@ public final class CardRules implements ICardCharacteristics { if ("T".equals(key)) { this.faces[this.curFace].addTrigger(value); } else if ("Types".equals(key)) { - this.faces[this.curFace].setType(CardType.parse(value)); + this.faces[this.curFace].setType(CardType.parse(value, false)); } else if ("Text".equals(key) && !"no text".equals(value) && StringUtils.isNotBlank(value)) { this.faces[this.curFace].setNonAbilityText(value); } @@ -526,12 +534,10 @@ public final class CardRules implements ICardCharacteristics { public final ManaCostShard next() { final String unparsed = st.nextToken(); // System.out.println(unparsed); - try { - int iVal = Integer.parseInt(unparsed); - this.genericCost += iVal; + if (StringUtils.isNumeric(unparsed)) { + this.genericCost += Integer.parseInt(unparsed); return null; } - catch (NumberFormatException nex) { } return ManaCostShard.parseNonGeneric(unparsed); } @@ -548,10 +554,10 @@ public final class CardRules implements ICardCharacteristics { } public static CardRules getUnsupportedCardNamed(String name) { - CardAiHints cah = new CardAiHints(true, true, null, null, null); + CardAiHints cah = new CardAiHints(true, true, true, null, null, null); CardFace[] faces = { new CardFace(name), null}; faces[0].setColor(ColorSet.fromMask(0)); - faces[0].setType(CardType.parse("")); + faces[0].setType(CardType.parse("", false)); faces[0].setOracleText("This card is not supported by Forge. Whenever you start a game with this card, it will be bugged."); faces[0].setNonAbilityText("This card is not supported by Forge.\nWhenever you start a game with this card, it will be bugged."); faces[0].assignMissingFields(); diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java index 6679b3ad673..e32e87b7070 100644 --- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java +++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java @@ -8,6 +8,7 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; +import forge.util.CardTranslation; import forge.util.ComparableOp; import forge.util.PredicateString; @@ -350,14 +351,18 @@ public final class CardRulesPredicates { boolean shouldContain; switch (this.field) { case NAME: - return op(card.getName(), this.operand); + boolean otherName = false; + if (card.getOtherPart() != null) { + otherName = (op(CardTranslation.getTranslatedName(card.getOtherPart().getName()), this.operand) || op(card.getOtherPart().getName(), this.operand)); + } + return otherName || (op(CardTranslation.getTranslatedName(card.getName()), this.operand) || op(card.getName(), this.operand)); case SUBTYPE: shouldContain = (this.getOperator() == StringOp.CONTAINS) || (this.getOperator() == StringOp.EQUALS); return shouldContain == card.getType().hasSubtype(this.operand); case ORACLE_TEXT: - return op(card.getOracleText(), operand); + return (op(CardTranslation.getTranslatedOracle(card.getName()), operand) || op(card.getOracleText(), this.operand)); case JOINED_TYPE: - return op(card.getType().toString(), operand); + return (op(CardTranslation.getTranslatedType(card.getName(), card.getType().toString()), operand) || op(card.getType().toString(), operand)); case COST: final String cost = card.getManaCost().toString(); return op(cost, operand); @@ -594,8 +599,10 @@ public final class CardRulesPredicates { public static final Predicate IS_VANGUARD = CardRulesPredicates.coreType(true, CardType.CoreType.Vanguard); public static final Predicate IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy); public static final Predicate IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land); - public static final Predicate CAN_BE_BRAWL_COMMANDER = Predicates.or(Presets.IS_PLANESWALKER, - Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY)); + public static final Predicate CAN_BE_BRAWL_COMMANDER = Predicates.and(Presets.IS_LEGENDARY, + Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER)); + public static final Predicate CAN_BE_TINY_LEADERS_COMMANDER = Predicates.and(Presets.IS_LEGENDARY, + Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER)); /** The Constant IS_NON_CREATURE_SPELL. **/ public static final Predicate IS_NON_CREATURE_SPELL = com.google.common.base.Predicates diff --git a/forge-core/src/main/java/forge/card/CardSplitType.java b/forge-core/src/main/java/forge/card/CardSplitType.java index f1e53ed0898..43670c7bafe 100644 --- a/forge-core/src/main/java/forge/card/CardSplitType.java +++ b/forge-core/src/main/java/forge/card/CardSplitType.java @@ -9,7 +9,8 @@ public enum CardSplitType Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld), Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit), Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped), - Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure); + Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure), + Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal); CardSplitType(FaceSelectionMethod calcMode, CardStateName stateName) { method = calcMode; diff --git a/forge-core/src/main/java/forge/card/CardStateName.java b/forge-core/src/main/java/forge/card/CardStateName.java index 70b857dc5c8..90149dde340 100644 --- a/forge-core/src/main/java/forge/card/CardStateName.java +++ b/forge-core/src/main/java/forge/card/CardStateName.java @@ -10,6 +10,7 @@ public enum CardStateName { LeftSplit, RightSplit, Adventure, + Modal ; diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java index fbc16fe6ad3..55b9590ea32 100644 --- a/forge-core/src/main/java/forge/card/CardType.java +++ b/forge-core/src/main/java/forge/card/CardType.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; @@ -35,10 +36,8 @@ import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import forge.util.EnumUtil; import forge.util.Settable; /** @@ -47,34 +46,46 @@ import forge.util.Settable; *

* * @author Forge - * @version $Id: java 9708 2011-08-09 19:34:12Z jendave $ */ public final class CardType implements Comparable, CardTypeView { private static final long serialVersionUID = 4629853583167022151L; - public static final CardTypeView EMPTY = new CardType(); + public static final CardTypeView EMPTY = new CardType(false); + + public static final String AllCreatureTypes = "AllCreatureTypes"; public enum CoreType { - Artifact(true), - Conspiracy(false), - Creature(true), - Emblem(false), - Enchantment(true), - Instant(false), - Land(true), - Phenomenon(false), - Plane(false), - Planeswalker(true), - Scheme(false), - Sorcery(false), - Tribal(false), - Vanguard(false); + Artifact(true, "artifacts"), + Conspiracy(false, "conspiracies"), + Creature(true, "creatures"), + Emblem(false, "emblems"), + Enchantment(true, "enchantments"), + Instant(false, "instants"), + Land(true, "lands"), + Phenomenon(false, "phenomenons"), + Plane(false, "planes"), + Planeswalker(true, "planeswalkers"), + Scheme(false, "schemes"), + Sorcery(false, "sorceries"), + Tribal(false, "tribals"), + Vanguard(false, "vanguards"); public final boolean isPermanent; - private static final ImmutableList allCoreTypeNames = EnumUtil.getNames(CoreType.class); + public final String pluralName; + private static Map stringToCoreType = EnumUtils.getEnumMap(CoreType.class); + private static final Set allCoreTypeNames = stringToCoreType.keySet(); - CoreType(final boolean permanent) { + public static CoreType getEnum(String name) { + return stringToCoreType.get(name); + } + + public static boolean isValidEnum(String name) { + return stringToCoreType.containsKey(name); + } + + CoreType(final boolean permanent, final String plural) { isPermanent = permanent; + pluralName = plural; } } @@ -86,29 +97,30 @@ public final class CardType implements Comparable, CardTypeView { Ongoing, World; - private static final ImmutableList allSuperTypeNames = EnumUtil.getNames(Supertype.class); - } + private static Map stringToSupertype = EnumUtils.getEnumMap(Supertype.class); + private static final Set allSuperTypeNames = stringToSupertype.keySet(); - // This will be useful for faster parses - private static Map stringToCoreType = Maps.newHashMap(); - private static Map stringToSupertype = Maps.newHashMap(); - static { - for (final Supertype st : Supertype.values()) { - stringToSupertype.put(st.name(), st); + public static Supertype getEnum(String name) { + return stringToSupertype.get(name); } - for (final CoreType ct : CoreType.values()) { - stringToCoreType.put(ct.name(), ct); + + public static boolean isValidEnum(String name) { + return stringToSupertype.containsKey(name); } + } private final Set coreTypes = EnumSet.noneOf(CoreType.class); private final Set supertypes = EnumSet.noneOf(Supertype.class); private final Set subtypes = Sets.newLinkedHashSet(); + private boolean incomplete = false; private transient String calculatedType = null; - public CardType() { + public CardType(boolean incomplete) { + this.incomplete = incomplete; } - public CardType(final Iterable from0) { + public CardType(final Iterable from0, boolean incomplete) { + this.incomplete = incomplete; addAll(from0); } public CardType(final CardType from0) { @@ -120,12 +132,12 @@ public final class CardType implements Comparable, CardTypeView { public boolean add(final String t) { boolean changed; - final CoreType ct = stringToCoreType.get(t); + final CoreType ct = CoreType.getEnum(t); if (ct != null) { changed = coreTypes.add(ct); } else { - final Supertype st = stringToSupertype.get(t); + final Supertype st = Supertype.getEnum(t); if (st != null) { changed = supertypes.add(st); } @@ -147,6 +159,7 @@ public final class CardType implements Comparable, CardTypeView { changed = true; } } + sanisfySubtypes(); return changed; } public boolean addAll(final CardType type) { @@ -154,6 +167,7 @@ public final class CardType implements Comparable, CardTypeView { if (coreTypes.addAll(type.coreTypes)) { changed = true; } if (supertypes.addAll(type.supertypes)) { changed = true; } if (subtypes.addAll(type.subtypes)) { changed = true; } + sanisfySubtypes(); return changed; } public boolean addAll(final CardTypeView type) { @@ -161,6 +175,7 @@ public final class CardType implements Comparable, CardTypeView { if (Iterables.addAll(coreTypes, type.getCoreTypes())) { changed = true; } if (Iterables.addAll(supertypes, type.getSupertypes())) { changed = true; } if (Iterables.addAll(subtypes, type.getSubtypes())) { changed = true; } + sanisfySubtypes(); return changed; } @@ -170,6 +185,7 @@ public final class CardType implements Comparable, CardTypeView { if (supertypes.removeAll(type.supertypes)) { changed = true; } if (subtypes.removeAll(type.subtypes)) { changed = true; } if (changed) { + sanisfySubtypes(); calculatedType = null; return true; } @@ -183,21 +199,30 @@ public final class CardType implements Comparable, CardTypeView { subtypes.clear(); calculatedType = null; } - + public boolean remove(final Supertype st) { return supertypes.remove(st); } public boolean remove(final String str) { boolean changed = false; - if (CardType.isASupertype(str) && supertypes.remove(stringToSupertype.get(str))) { - changed = true; - } else if (CardType.isACardType(str) && coreTypes.remove(stringToCoreType.get(str))) { - changed = true; - } else if (subtypes.remove(str)) { + + // try to remove sub type first if able + if (subtypes.remove(str)) { changed = true; + } else { + Supertype st = Supertype.getEnum(str); + if (st != null && supertypes.remove(st)) { + changed = true; + } + CoreType ct = CoreType.getEnum(str); + if (ct != null && coreTypes.remove(ct)) { + changed = true; + } } + if (changed) { + sanisfySubtypes(); calculatedType = null; } return changed; @@ -210,7 +235,7 @@ public final class CardType implements Comparable, CardTypeView { } boolean changed = Iterables.removeIf(subtypes, Predicates.IS_CREATURE_TYPE); // need to remove AllCreatureTypes too when setting Creature Type - if (subtypes.remove("AllCreatureTypes")) { + if (subtypes.remove(AllCreatureTypes)) { changed = true; } subtypes.addAll(ctypes); @@ -239,7 +264,7 @@ public final class CardType implements Comparable, CardTypeView { final Set creatureTypes = Sets.newHashSet(); if (isCreature() || isTribal()) { for (final String t : subtypes) { - if (isACreatureType(t) || t.equals("AllCreatureTypes")) { + if (isACreatureType(t) || t.equals(AllCreatureTypes)) { creatureTypes.add(t); } } @@ -267,15 +292,13 @@ public final class CardType implements Comparable, CardTypeView { if (hasSubtype(t)) { return true; } - final char firstChar = t.charAt(0); - if (Character.isLowerCase(firstChar)) { - t = Character.toUpperCase(firstChar) + t.substring(1); //ensure string is proper case for enum types - } - final CoreType type = stringToCoreType.get(t); + + t = StringUtils.capitalize(t); + final CoreType type = CoreType.getEnum(t); if (type != null) { return hasType(type); } - final Supertype supertype = stringToSupertype.get(t); + final Supertype supertype = Supertype.getEnum(t); if (supertype != null) { return hasSupertype(supertype); } @@ -291,7 +314,7 @@ public final class CardType implements Comparable, CardTypeView { } @Override public boolean hasSubtype(final String subtype) { - if (isACreatureType(subtype) && subtypes.contains("AllCreatureTypes")) { + if (isACreatureType(subtype) && subtypes.contains(AllCreatureTypes)) { return true; } return subtypes.contains(subtype); @@ -304,21 +327,21 @@ public final class CardType implements Comparable, CardTypeView { creatureType = toMixedCase(creatureType); if (!isACreatureType(creatureType)) { return false; } - return subtypes.contains(creatureType) || subtypes.contains("AllCreatureTypes"); + return subtypes.contains(creatureType) || subtypes.contains(AllCreatureTypes); } private static String toMixedCase(final String s) { - if (s.equals("")) { + if (s.isEmpty()) { return s; } final StringBuilder sb = new StringBuilder(); // to handle hyphenated Types + // TODO checkout WordUtils for this final String[] types = s.split("-"); for (int i = 0; i < types.length; i++) { if (i != 0) { sb.append("-"); } - sb.append(types[i].substring(0, 1).toUpperCase()); - sb.append(types[i].substring(1).toLowerCase()); + sb.append(StringUtils.capitalize(types[i])); } return sb.toString(); } @@ -474,7 +497,7 @@ public final class CardType implements Comparable, CardTypeView { if (ct.isRemoveCreatureTypes()) { Iterables.removeIf(newType.subtypes, Predicates.IS_CREATURE_TYPE); // need to remove AllCreatureTypes too when removing creature Types - newType.subtypes.remove("AllCreatureTypes"); + newType.subtypes.remove(AllCreatureTypes); } if (ct.isRemoveArtifactTypes()) { Iterables.removeIf(newType.subtypes, Predicates.IS_ARTIFACT_TYPE); @@ -492,29 +515,37 @@ public final class CardType implements Comparable, CardTypeView { } // sanisfy subtypes if (newType != null && !newType.subtypes.isEmpty()) { - if (!newType.isCreature() && !newType.isTribal()) { - Iterables.removeIf(newType.subtypes, Predicates.IS_CREATURE_TYPE); - newType.subtypes.remove("AllCreatureTypes"); - } - if (!newType.isLand()) { - Iterables.removeIf(newType.subtypes, Predicates.IS_LAND_TYPE); - } - if (!newType.isArtifact()) { - Iterables.removeIf(newType.subtypes, Predicates.IS_ARTIFACT_TYPE); - } - if (!newType.isEnchantment()) { - Iterables.removeIf(newType.subtypes, Predicates.IS_ENCHANTMENT_TYPE); - } - if (!newType.isInstant() && !newType.isSorcery()) { - Iterables.removeIf(newType.subtypes, Predicates.IS_SPELL_TYPE); - } - if (!newType.isPlaneswalker() && !newType.isEmblem()) { - Iterables.removeIf(newType.subtypes, Predicates.IS_WALKER_TYPE); - } + newType.sanisfySubtypes(); } return newType == null ? this : newType; } + public void sanisfySubtypes() { + // incomplete types are used for changing effects + if (this.incomplete) { + return; + } + if (!isCreature() && !isTribal()) { + Iterables.removeIf(subtypes, Predicates.IS_CREATURE_TYPE); + subtypes.remove(AllCreatureTypes); + } + if (!isLand()) { + Iterables.removeIf(subtypes, Predicates.IS_LAND_TYPE); + } + if (!isArtifact()) { + Iterables.removeIf(subtypes, Predicates.IS_ARTIFACT_TYPE); + } + if (!isEnchantment()) { + Iterables.removeIf(subtypes, Predicates.IS_ENCHANTMENT_TYPE); + } + if (!isInstant() && !isSorcery()) { + Iterables.removeIf(subtypes, Predicates.IS_SPELL_TYPE); + } + if (!isPlaneswalker() && !isEmblem()) { + Iterables.removeIf(subtypes, Predicates.IS_WALKER_TYPE); + } + } + @Override public Iterator iterator() { final Iterator coreTypeIterator = coreTypes.iterator(); @@ -549,7 +580,69 @@ public final class CardType implements Comparable, CardTypeView { return toString().compareTo(o.toString()); } - public boolean sharesSubtypeWith(final CardType ctOther) { + public boolean sharesCreaturetypeWith(final CardTypeView ctOther) { + if (ctOther == null) { + return false; + } + if (this.subtypes.contains(AllCreatureTypes) && ctOther.hasSubtype(AllCreatureTypes)) { + return true; + } + for (final String type : getCreatureTypes()) { + if (ctOther.hasCreatureType(type)) { + return true; + } + } + for (final String type : ctOther.getCreatureTypes()) { + if (this.hasCreatureType(type)) { + return true; + } + } + return false; + } + + public boolean sharesLandTypeWith(final CardTypeView ctOther) { + if (ctOther == null) { + return false; + } + + for (final String type : getLandTypes()) { + if (ctOther.hasSubtype(type)) { + return true; + } + } + return false; + } + + public boolean sharesPermanentTypeWith(final CardTypeView ctOther) { + if (ctOther == null) { + return false; + } + + for (final CoreType type : getCoreTypes()) { + if (type.isPermanent && ctOther.hasType(type)) { + return true; + } + } + return false; + } + + public boolean sharesCardTypeWith(final CardTypeView ctOther) { + if (ctOther == null) { + return false; + } + + for (final CoreType type : getCoreTypes()) { + if (ctOther.hasType(type)) { + return true; + } + } + return false; + } + + public boolean sharesSubtypeWith(final CardTypeView ctOther) { + if (ctOther == null) { + return false; + } for (final String t : ctOther.getSubtypes()) { if (hasSubtype(t)) { return true; @@ -558,11 +651,11 @@ public final class CardType implements Comparable, CardTypeView { return false; } - public static CardType parse(final String typeText) { + public static CardType parse(final String typeText, boolean incomplete) { // Most types and subtypes, except "Serra's Realm" and // "Bolas's Meditation Realm" consist of only one word final char space = ' '; - final CardType result = new CardType(); + final CardType result = new CardType(incomplete); int iTypeStart = 0; int iSpace = typeText.indexOf(space); @@ -582,7 +675,7 @@ public final class CardType implements Comparable, CardTypeView { } public static CardType combine(final CardType a, final CardType b) { - final CardType result = new CardType(); + final CardType result = new CardType(false); result.supertypes.addAll(a.supertypes); result.supertypes.addAll(b.supertypes); result.coreTypes.addAll(a.coreTypes); @@ -613,11 +706,17 @@ public final class CardType implements Comparable, CardTypeView { public static final Set ENCHANTMENT_TYPES = Sets.newHashSet(); public static final Set ARTIFACT_TYPES = Sets.newHashSet(); public static final Set WALKER_TYPES = Sets.newHashSet(); - + // singular -> plural public static final BiMap pluralTypes = HashBiMap.create(); // plural -> singular public static final BiMap singularTypes = pluralTypes.inverse(); + + static { + for (CoreType c : CoreType.values()) { + pluralTypes.put(c.name(), c.pluralName); + } + } } public static class Predicates { public static Predicate IS_LAND_TYPE = new Predicate() { @@ -662,14 +761,14 @@ public final class CardType implements Comparable, CardTypeView { } }; } - + ///////// Utility methods public static boolean isACardType(final String cardType) { - return getAllCardTypes().contains(cardType); + return CoreType.isValidEnum(cardType); } - public static ImmutableList getAllCardTypes() { + public static Set getAllCardTypes() { return CoreType.allCoreTypeNames; } @@ -714,7 +813,7 @@ public final class CardType implements Comparable, CardTypeView { } public static boolean isASupertype(final String cardType) { - return (Supertype.allSuperTypeNames.contains(cardType)); + return Supertype.isValidEnum(cardType); } public static boolean isASubType(final String cardType) { @@ -740,7 +839,7 @@ public final class CardType implements Comparable, CardTypeView { public static boolean isABasicLandType(final String cardType) { return (Constant.BASIC_TYPES.contains(cardType)); } - + public static boolean isAnEnchantmentType(final String cardType) { return (Constant.ENCHANTMENT_TYPES.contains(cardType)); } diff --git a/forge-core/src/main/java/forge/card/CardTypeView.java b/forge-core/src/main/java/forge/card/CardTypeView.java index b1c01369b2b..e9b65aeac81 100644 --- a/forge-core/src/main/java/forge/card/CardTypeView.java +++ b/forge-core/src/main/java/forge/card/CardTypeView.java @@ -19,6 +19,12 @@ public interface CardTypeView extends Iterable, Serializable { boolean hasSupertype(Supertype supertype); boolean hasSubtype(String subtype); boolean hasCreatureType(String creatureType); + + public boolean sharesCreaturetypeWith(final CardTypeView ctOther); + public boolean sharesLandTypeWith(final CardTypeView ctOther); + public boolean sharesPermanentTypeWith(final CardTypeView ctOther); + public boolean sharesCardTypeWith(final CardTypeView ctOther); + boolean isPermanent(); boolean isCreature(); boolean isPlaneswalker(); diff --git a/forge-core/src/main/java/forge/card/ColorSet.java b/forge-core/src/main/java/forge/card/ColorSet.java index 0384ff5a481..8065f0a4e48 100644 --- a/forge-core/src/main/java/forge/card/ColorSet.java +++ b/forge-core/src/main/java/forge/card/ColorSet.java @@ -41,7 +41,6 @@ public final class ColorSet implements Comparable, Iterable, Ser private static final long serialVersionUID = 794691267379929080L; private final byte myColor; - public byte getMyColor() { return myColor; } private final float orderWeight; private static final ColorSet[] cache = new ColorSet[32]; diff --git a/forge-core/src/main/java/forge/card/ICardDatabase.java b/forge-core/src/main/java/forge/card/ICardDatabase.java index a6256207fc7..a83faea5b4d 100644 --- a/forge-core/src/main/java/forge/card/ICardDatabase.java +++ b/forge-core/src/main/java/forge/card/ICardDatabase.java @@ -24,9 +24,9 @@ public interface ICardDatabase extends Iterable { int getArtCount(String cardName, String edition); Collection getUniqueCards(); - List getAllCards(); - List getAllCards(String cardName); - List getAllCards(Predicate predicate); + Collection getAllCards(); + Collection getAllCards(String cardName); + Collection getAllCards(Predicate predicate); List getAllCardsFromEdition(CardEdition edition); diff --git a/forge-core/src/main/java/forge/card/MagicColor.java b/forge-core/src/main/java/forge/card/MagicColor.java index c21dedeac97..408b00d8080 100644 --- a/forge-core/src/main/java/forge/card/MagicColor.java +++ b/forge-core/src/main/java/forge/card/MagicColor.java @@ -139,16 +139,22 @@ public final class MagicColor { public static final ImmutableList SNOW_LANDS = ImmutableList.of("Snow-Covered Plains", "Snow-Covered Island", "Snow-Covered Swamp", "Snow-Covered Mountain", "Snow-Covered Forest"); public static final ImmutableMap ANY_COLOR_CONVERSION = new ImmutableMap.Builder() .put("ManaColorConversion", "Additive") - .put("WhiteConversion", "All") - .put("BlueConversion", "All") - .put("BlackConversion", "All") - .put("RedConversion", "All") - .put("GreenConversion", "All") + .put("WhiteConversion", "Color") + .put("BlueConversion", "Color") + .put("BlackConversion", "Color") + .put("RedConversion", "Color") + .put("GreenConversion", "Color") + .put("ColorlessConversion", "Color") .build(); public static final ImmutableMap ANY_TYPE_CONVERSION = new ImmutableMap.Builder() - .putAll(ANY_COLOR_CONVERSION) - .put("ColorlessConversion", "All") + .put("ManaColorConversion", "Additive") + .put("WhiteConversion", "Type") + .put("BlueConversion", "Type") + .put("BlackConversion", "Type") + .put("RedConversion", "Type") + .put("GreenConversion", "Type") + .put("ColorlessConversion", "Type") .build(); /** * Private constructor to prevent instantiation. diff --git a/forge-core/src/main/java/forge/card/PrintSheet.java b/forge-core/src/main/java/forge/card/PrintSheet.java index 7c5c22461fa..969bd8ab7c4 100644 --- a/forge-core/src/main/java/forge/card/PrintSheet.java +++ b/forge-core/src/main/java/forge/card/PrintSheet.java @@ -5,6 +5,8 @@ import forge.deck.CardPool; import forge.item.PaperCard; import forge.util.ItemPool; import forge.util.MyRandom; +import forge.util.storage.IStorage; +import forge.util.storage.StorageExtendable; import forge.util.storage.StorageReaderFileSections; import java.io.File; @@ -23,6 +25,18 @@ public class PrintSheet { @Override public final String apply(PrintSheet sheet) { return sheet.name; } }; + public static final IStorage initializePrintSheets(File sheetsFile, CardEdition.Collection editions) { + IStorage sheets = new StorageExtendable<>("Special print runs", new PrintSheet.Reader(sheetsFile)); + + for(CardEdition edition : editions) { + for(PrintSheet ps : edition.getPrintSheetsBySection()) { + System.out.println(ps.name); + sheets.add(ps.name, ps); + } + } + + return sheets; + } private final ItemPool cardsWithWeights; @@ -78,6 +92,16 @@ public class PrintSheet { return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip } + public List all() { + List result = new ArrayList<>(); + for(Entry kv : cardsWithWeights) { + for(int i = 0; i < kv.getValue(); i++) { + result.add(kv.getKey()); + } + } + return result; + } + public List random(int number, boolean wantUnique) { List result = new ArrayList<>(); diff --git a/forge-core/src/main/java/forge/card/mana/ManaCost.java b/forge-core/src/main/java/forge/card/mana/ManaCost.java index 51611a48d18..34a0387b61b 100644 --- a/forge-core/src/main/java/forge/card/mana/ManaCost.java +++ b/forge-core/src/main/java/forge/card/mana/ManaCost.java @@ -345,13 +345,7 @@ public final class ManaCost implements Comparable, Iterable 0; + } public boolean canBePaidWithManaOfColor(byte colorCode) { return this.isOr2Generic() || ((COLORS_SUPERPOSITION | ManaAtom.COLORLESS) & this.shard) == 0 || - (colorCode & this.shard) > 0; + this.isColor(colorCode); } public boolean isOfKind(int atom) { diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index f9cd57dc0ef..96e4634daaa 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -73,7 +73,7 @@ public enum DeckFormat { private final Set bannedCards = ImmutableSet.of( "Ancestral Recall", "Balance", "Black Lotus", "Black Vise", "Channel", "Chaos Orb", "Contract From Below", "Counterbalance", "Darkpact", "Demonic Attorney", "Demonic Tutor", "Earthcraft", "Edric, Spymaster of Trest", "Falling Star", "Fastbond", "Flash", "Goblin Recruiter", "Grindstone", "Hermit Druid", "Imperial Seal", "Jeweled Bird", "Karakas", "Library of Alexandria", "Mana Crypt", "Mana Drain", "Mana Vault", "Metalworker", "Mind Twist", "Mishra's Workshop", - "Mox Emerald", "Mox Jet", "Mox Pearl", "Mox Ruby", "Mox Sapphire", "Necropotence", "Shahrazad", "Skullclamp", "Sol Ring", "Strip Mine", "Survival of the Fittest", "Sword of Body and Mind", "Time Vault", "Time Walk", "Timetwister", + "Mox Emerald", "Mox Jet", "Mox Pearl", "Mox Ruby", "Mox Sapphire", "Najeela, the Blade Blossom", "Necropotence", "Shahrazad", "Skullclamp", "Sol Ring", "Strip Mine", "Survival of the Fittest", "Sword of Body and Mind", "Time Vault", "Time Walk", "Timetwister", "Timmerian Fiends", "Tolarian Academy", "Umezawa's Jitte", "Vampiric Tutor", "Wheel of Fortune", "Yawgmoth's Will"); @Override @@ -375,9 +375,10 @@ public enum DeckFormat { public static Integer canHaveSpecificNumberInDeck(final IPaperCard card) { // Ideally, this would be parsed during card parsing and set this value - if (Iterables.contains(card.getRules().getMainPart().getKeywords(), - "A deck can have up to seven cards named CARDNAME.")) { + if (card.getRules().hasKeyword("A deck can have up to seven cards named CARDNAME.")) { return 7; + } else if (card.getRules().hasKeyword("Megalegendary")) { + return 1; } return null; @@ -463,6 +464,9 @@ public enum DeckFormat { if (this.equals(DeckFormat.Brawl)) { return rules.canBeBrawlCommander(); } + if (this.equals(DeckFormat.TinyLeaders)) { + return rules.canBeTinyLeadersCommander(); + } return rules.canBeCommander(); } diff --git a/forge-core/src/main/java/forge/deck/generation/DeckGenerator5Color.java b/forge-core/src/main/java/forge/deck/generation/DeckGenerator5Color.java index b17814ffa09..4341c1d7531 100644 --- a/forge-core/src/main/java/forge/deck/generation/DeckGenerator5Color.java +++ b/forge-core/src/main/java/forge/deck/generation/DeckGenerator5Color.java @@ -69,7 +69,8 @@ public class DeckGenerator5Color extends DeckGeneratorBase { */ public DeckGenerator5Color(IDeckGenPool pool0, DeckFormat format0, Predicate formatFilter0) { super(pool0, format0, formatFilter0); - + format0.adjustCMCLevels(cmcLevels); + colors = ColorSet.fromMask(0).inverse(); } public DeckGenerator5Color(IDeckGenPool pool0, DeckFormat format0) { diff --git a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java index ec0b0c270ba..4af48f8458b 100644 --- a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java +++ b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java @@ -290,10 +290,18 @@ public abstract class DeckGeneratorBase { // remove cards that generated decks don't like Predicate canPlay = forAi ? AI_CAN_PLAY : HUMAN_CAN_PLAY; Predicate hasColor = new MatchColorIdentity(colors); + Predicate canUseInFormat = new Predicate() { + @Override + public boolean apply(CardRules c) { + // FIXME: should this be limited to AI only (!forAi) or should it be generally applied to all random generated decks? + return !c.getAiHints().getRemNonCommanderDecks() || format.hasCommander(); + } + }; + if (useArtifacts) { hasColor = Predicates.or(hasColor, COLORLESS_CARDS); } - return Iterables.filter(pool.getAllCards(),Predicates.compose(Predicates.and(canPlay, hasColor), PaperCard.FN_GET_RULES)); + return Iterables.filter(pool.getAllCards(),Predicates.compose(Predicates.and(canPlay, hasColor, canUseInFormat), PaperCard.FN_GET_RULES)); } protected static Map countLands(ItemPool outList) { diff --git a/forge-core/src/main/java/forge/item/BoosterBox.java b/forge-core/src/main/java/forge/item/BoosterBox.java index 8b3c5e93f51..a17d1d44bac 100644 --- a/forge-core/src/main/java/forge/item/BoosterBox.java +++ b/forge-core/src/main/java/forge/item/BoosterBox.java @@ -18,25 +18,22 @@ package forge.item; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; - import com.google.common.base.Function; - import forge.ImageKeys; import forge.StaticData; import forge.card.CardEdition; -import forge.util.TextUtil; -import forge.util.storage.StorageReaderFile; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.ArrayList; public class BoosterBox extends BoxedProduct { public static final Function FN_FROM_SET = new Function() { @Override public BoosterBox apply(final CardEdition arg1) { - BoosterBox.Template d = StaticData.instance().getBoosterBoxes().get(arg1.getCode()); + if (arg1.getBoosterBoxCount() <= 0) { + return null; + } + BoosterBox.Template d = new Template(arg1); if (d == null) { return null; } return new BoosterBox(arg1.getName(), d, d.cntBoosters); } @@ -72,40 +69,13 @@ public class BoosterBox extends BoxedProduct { public static class Template extends SealedProduct.Template { private final int cntBoosters; - public int getCntBoosters() { return cntBoosters; } - private Template(String edition, int boosters, Iterable> itrSlots) - { - super(edition, itrSlots); - cntBoosters = boosters; + private Template(CardEdition edition) { + super(edition.getCode(), new ArrayList<>()); + cntBoosters = edition.getBoosterBoxCount(); } - public static final class Reader extends StorageReaderFile