diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index ff2cee6f5a5..cc6a9657c9a 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -94,9 +94,10 @@ public enum AiProps { /** */ BOUNCE_ALL_TO_HAND_CREAT_EVAL_DIFF ("200"), /** */ BOUNCE_ALL_ELSEWHERE_CREAT_EVAL_DIFF ("200"), /** */ BOUNCE_ALL_TO_HAND_NONCREAT_EVAL_DIFF ("3"), /** */ - BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF ("3"); /** */ + BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF ("3"), /** */ // Experimental features, must be removed after extensive testing and, ideally, defaulting // <-- there are currently no experimental options here --> + INTUITION_SPECIAL_LOGIC ("false"); /** */ private final String strDefaultVal; diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index 2dbf3c944fc..f8dc7563699 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -25,6 +25,7 @@ import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaCost; import forge.game.Game; +import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; @@ -38,8 +39,12 @@ import forge.game.player.Player; import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; import forge.game.staticability.StaticAbility; +import forge.game.trigger.Trigger; import forge.game.zone.ZoneType; import forge.util.Aggregates; +import forge.util.TextUtil; +import forge.util.maps.LinkedHashMapToAmount; +import forge.util.maps.MapToAmount; import org.apache.commons.lang3.tuple.Pair; import java.util.Arrays; @@ -520,7 +525,134 @@ public class SpecialCardAi { return chosen; } } - + + // Intuition (and any other card that might potentially let you pick N cards from the library, + // one of which will then be picked for you by the opponent) + public static class Intuition { + public static CardCollection considerMultiple(final Player ai, final SpellAbility sa) { + if (ai.getController().isAI()) { + if (!((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.INTUITION_SPECIAL_LOGIC)) { + return new CardCollection(); // fall back to standard ChangeZoneAi considerations + } + } + + int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("ChangeNum"), sa); + CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library), + Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName()))); + Collections.sort(lib, CardLists.CmcComparatorInv); + + // Additional cards which are difficult to auto-classify but which are generally good to Intuition for + List highPriorityNamedCards = Lists.newArrayList("Accumulated Knowledge"); + + // figure out how many of each card we have in deck + MapToAmount cardAmount = new LinkedHashMapToAmount<>(); + for (Card c : lib) { + cardAmount.add(c.getName()); + } + + // Trix: see if we can complete the combo + int numIllusionsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals("Illusions of Grandeur")).size(); + int numDonateInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals("Donate")).size(); + int numIllusionsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.nameEquals("Illusions of Grandeur")).size(); + int numIllusionsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Illusions of Grandeur")).size(); + int numDonateInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.nameEquals("Donate")).size(); + CardCollection comboList = new CardCollection(); + if ((numIllusionsInHand > 0 || numIllusionsOTB > 0) && numDonateInHand == 0 && numDonateInLib >= 3) { + for (Card c : lib) { + if (c.getName().equals("Donate")) { + comboList.add(c); + } + } + return comboList; + } else if (numDonateInHand > 0 && numIllusionsInHand == 0 && numIllusionsInLib >= 3) { + for (Card c : lib) { + if (c.getName().equals("Illusions of Grandeur")) { + comboList.add(c); + } + } + return comboList; + } + + // Create a priority list for cards that we have no more than 4 of and that are not lands + CardCollection libPriorityList = new CardCollection(); + CardCollection libHighPriorityList = new CardCollection(); + CardCollection libLowPriorityList = new CardCollection(); + List processed = Lists.newArrayList(); + for (int i = 4; i > 0; i--) { + for (Card c : lib) { + if (cardAmount.get(c.getName()) == i && !c.isLand() && !processed.contains(c.getName())) { + // if it's a card that is generally good to place in the graveyard, also add it + // to the mix + 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); + } + if (ab == null) { continue; } + + if (ab.getApi() == ApiType.ChangeZone + && "Self".equals(ab.getParam("Defined")) + && "Graveyard".equals(ab.getParam("Origin")) + && "Battlefield".equals(ab.getParam("Destination"))) { + canRetFromGrave = true; + } + if (ab.getApi() == ApiType.ChangeZoneAll + && TextUtil.concatNoSpace("Creature.named", name).equals(ab.getParam("ChangeType")) + && "Graveyard".equals(ab.getParam("Origin")) + && "Battlefield".equals(ab.getParam("Destination"))) { + canRetFromGrave = true; + } + } + boolean isGoodToPutInGrave = c.hasSVar("DiscardMe") || canRetFromGrave + || (ComputerUtil.isPlayingReanimator(ai) && c.isCreature()); + + for (Card c1 : lib) { + if (c1.getName().equals(c.getName())) { + if (CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName())).isEmpty() + && ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) { + // Try not to search for things we already have in hand or that we can't cast + libPriorityList.add(c1); + } else { + libLowPriorityList.add(c1); + } + if (isGoodToPutInGrave || highPriorityNamedCards.contains(c.getName())) { + libHighPriorityList.add(c1); + } + } + } + processed.add(c.getName()); + } + } + } + + // If we're playing Reanimator, we're really interested just in the highest CMC spells, not the + // ones we necessarily have multiples of + if (ComputerUtil.isPlayingReanimator(ai)) { + Collections.sort(libHighPriorityList, CardLists.CmcComparatorInv); + } + + // Otherwise, try to grab something that is hopefully decent to grab, in priority order + CardCollection chosen = new CardCollection(); + if (libHighPriorityList.size() >= changeNum) { + for (int i = 0; i < changeNum; i++) { + chosen.add(libHighPriorityList.get(i)); + } + } else if (libPriorityList.size() >= changeNum) { + for (int i = 0; i < changeNum; i++) { + chosen.add(libPriorityList.get(i)); + } + } else if (libLowPriorityList.size() >= changeNum) { + for (int i = 0; i < changeNum; i++) { + chosen.add(libLowPriorityList.get(i)); + } + } + + return chosen; + } + } + // Living Death (and possibly other similar cards using AILogic LivingDeath) public static class LivingDeath { public static boolean consider(final Player ai, final SpellAbility sa) { 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 ec248d2c9eb..30f65abe495 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -37,7 +37,11 @@ public class ChangeZoneAi extends SpellAbilityAi { * idea to re-factor ChangeZoneAi into more specific effects since it is really doing * too much: blink/bounce/exile/tutor/Raise Dead/Surgical Extraction/...... */ - + + // multipleCardsToChoose is used by Intuition and can be adapted to be used by other + // cards where multiple cards are fetched at once and they need to be coordinated + private static CardCollection multipleCardsToChoose = new CardCollection(); + @Override protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) { @@ -60,6 +64,8 @@ public class ChangeZoneAi extends SpellAbilityAi { @Override protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { // Checks for "return true" unlike checkAiLogic() + + multipleCardsToChoose.clear(); String aiLogic = sa.getParam("AILogic"); if (aiLogic != null) { if (aiLogic.equals("Always")) { @@ -70,8 +76,12 @@ public class ChangeZoneAi extends SpellAbilityAi { return this.doSacAndReturnFromGraveLogic(aiPlayer, sa); } else if (aiLogic.equals("Necropotence")) { return SpecialCardAi.Necropotence.consider(aiPlayer, sa); - } else if (aiLogic.equals("SameName")) { // Declaration in Stone + } else if (aiLogic.equals("SameName")) { // Declaration in Stone return this.doSameNameLogic(aiPlayer, sa); + } else if (aiLogic.equals("Intuition")) { + // This logic only fills the multiple cards array, the decision to play is made + // separately in hiddenOriginCanPlayAI later. + multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa); } } if (isHidden(sa)) { @@ -1343,6 +1353,12 @@ public class ChangeZoneAi extends SpellAbilityAi { return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList); } else if ("SurvivalOfTheFittest".equals(logic)) { return SpecialCardAi.SurvivalOfTheFittest.considerCardToGet(decider, sa); + } else if ("Intuition".equals(logic)) { + if (!multipleCardsToChoose.isEmpty()) { + Card choice = multipleCardsToChoose.get(0); + multipleCardsToChoose.remove(0); + return choice; + } } } if (fetchList.isEmpty()) { diff --git a/forge-gui/res/ai/Experimental.ai b/forge-gui/res/ai/Experimental.ai index 6206a42672a..5de775da20a 100644 --- a/forge-gui/res/ai/Experimental.ai +++ b/forge-gui/res/ai/Experimental.ai @@ -168,4 +168,8 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=5 # -- Experimental feature toggles which only exist until the testing procedure for the relevant -- # -- features is over. These toggles will be removed later, or may be reintroduced under a -- # -- different name if necessary -- -# <-- There are currently no experimental options here --> + +# Experimental logic for Intuition that makes it work better in reanimator decks, combo decks like Trix, +# and generally tries to either grab cards that are also good to put in the graveyard or that are available +# in multiples in the deck. +INTUITION_SPECIAL_LOGIC=true diff --git a/forge-gui/res/cardsfolder/i/intuition.txt b/forge-gui/res/cardsfolder/i/intuition.txt index d22cf481489..454a5d31c08 100644 --- a/forge-gui/res/cardsfolder/i/intuition.txt +++ b/forge-gui/res/cardsfolder/i/intuition.txt @@ -1,7 +1,7 @@ Name:Intuition ManaCost:2 U Types:Instant -A:SP$ ChangeZone | Cost$ 2 U | Origin$ Library | Destination$ Library | ChangeType$ Card | ChangeNum$ 3 | RememberChanged$ True | Reveal$ True | Shuffle$ False | SubAbility$ DBChangeZone1 | StackDescription$ Search your library for three cards and reveal them. Target opponent chooses one. Put that card into your hand and the rest into your graveyard. Then shuffle your library. | SpellDescription$ Search your library for three cards and reveal them. Target opponent chooses one. Put that card into your hand and the rest into your graveyard. Then shuffle your library. +A:SP$ ChangeZone | Cost$ 2 U | Origin$ Library | Destination$ Library | ChangeType$ Card | ChangeNum$ 3 | RememberChanged$ True | Reveal$ True | Shuffle$ False | AILogic$ Intuition | SubAbility$ DBChangeZone1 | StackDescription$ Search your library for three cards and reveal them. Target opponent chooses one. Put that card into your hand and the rest into your graveyard. Then shuffle your library. | SpellDescription$ Search your library for three cards and reveal them. Target opponent chooses one. Put that card into your hand and the rest into your graveyard. Then shuffle your library. SVar:DBChangeZone1:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Card.IsRemembered | Chooser$ Opponent | ChangeNum$ 1 | Mandatory$ True | NoLooking$ True | SelectPrompt$ Select a card for the hand | Shuffle$ False | SubAbility$ DBChangeZone2 | StackDescription$ None SVar:DBChangeZone2:DB$ ChangeZoneAll | Origin$ Library | Destination$ Graveyard | ChangeType$ Card.IsRemembered | Shuffle$ True | StackDescription$ None | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True