From fce6807a3b8faf84fbc946ea5351c927c61c245e Mon Sep 17 00:00:00 2001 From: Agetian Date: Mon, 25 Sep 2017 08:02:08 +0000 Subject: [PATCH] - Simple AI support for Explore (feel free to expand). --- .gitattributes | 1 + .../src/main/java/forge/ai/AiController.java | 7 ++- forge-ai/src/main/java/forge/ai/AiProps.java | 6 ++- .../src/main/java/forge/ai/SpellApiToAi.java | 7 ++- .../main/java/forge/ai/ability/ExploreAi.java | 52 +++++++++++++++++++ .../ai/simulation/SpellAbilityPicker.java | 22 ++++---- forge-gui/res/ai/Cautious.ai | 7 +++ forge-gui/res/ai/Default.ai | 7 +++ forge-gui/res/ai/Experimental.ai | 7 +++ forge-gui/res/ai/Reckless.ai | 7 +++ 10 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 forge-ai/src/main/java/forge/ai/ability/ExploreAi.java diff --git a/.gitattributes b/.gitattributes index 6d907f4a3a3..a8008d1bd35 100644 --- a/.gitattributes +++ b/.gitattributes @@ -95,6 +95,7 @@ forge-ai/src/main/java/forge/ai/ability/DrawAi.java svneol=native#text/plain forge-ai/src/main/java/forge/ai/ability/EffectAi.java -text forge-ai/src/main/java/forge/ai/ability/EncodeAi.java -text forge-ai/src/main/java/forge/ai/ability/EndTurnAi.java -text +forge-ai/src/main/java/forge/ai/ability/ExploreAi.java -text forge-ai/src/main/java/forge/ai/ability/FightAi.java -text forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java -text forge-ai/src/main/java/forge/ai/ability/FogAi.java -text diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 79cf3567594..e350cb0a74a 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -25,6 +25,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.ai.ability.ChangeZoneAi; +import forge.ai.ability.ExploreAi; import forge.ai.simulation.SpellAbilityPicker; import forge.card.MagicColor; import forge.card.mana.ManaCost; @@ -1749,7 +1750,11 @@ public class AiController { if (useSimulation) { return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); } - return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); + if (sa.getApi() == ApiType.Explore) { + return ExploreAi.shouldPutInGraveyard(fetchList, decider); + } else { + return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); + } } public List orderPlaySa(List activePlayerSAs) { diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index 1d560a3023f..b1caabbc854 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -94,8 +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"), - INTUITION_ALTERNATIVE_LOGIC ("false"); /** */ + BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF ("3"), /** */ + INTUITION_ALTERNATIVE_LOGIC ("false"), /** */ + EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"), + EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE ("2"); /** */ // Experimental features, must be removed after extensive testing and, ideally, defaulting // <-- There are no experimental options here --> diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 93447e407c2..f492ba0fc94 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -1,14 +1,13 @@ package forge.ai; -import java.util.Map; - import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; - import forge.ai.ability.*; import forge.game.ability.ApiType; import forge.util.ReflectionUtil; +import java.util.Map; + public enum SpellApiToAi { Converter; @@ -72,7 +71,7 @@ public enum SpellApiToAi { .put(ApiType.ExchangeControlVariant, CannotPlayAi.class) .put(ApiType.ExchangePower, PowerExchangeAi.class) .put(ApiType.ExchangeZone, ZoneExchangeAi.class) - .put(ApiType.Explore, AlwaysPlayAi.class) + .put(ApiType.Explore, ExploreAi.class) .put(ApiType.Fight, FightAi.class) .put(ApiType.FlipACoin, FlipACoinAi.class) .put(ApiType.Fog, FogAi.class) diff --git a/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java b/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java new file mode 100644 index 00000000000..f56157eb4c7 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java @@ -0,0 +1,52 @@ +package forge.ai.ability; + + +import forge.ai.*; +import forge.game.card.*; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; + +public class ExploreAi 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) { + return true; + } + + public static Card shouldPutInGraveyard(CardCollection top, Player ai) { + int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size(); + CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield); + CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand); + CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA); + CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA); + + int maxCMCDiff = 1; + int numLandsToStillNeedMore = 2; + + if (ai.getController().isAI()) { + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); + maxCMCDiff = aic.getIntProperty(AiProps.EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD); + numLandsToStillNeedMore = aic.getIntProperty(AiProps.EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE); + } + + if (!top.isEmpty()) { + Card topCard = top.getFirst(); + if (landsInHand.isEmpty() && landsOTB.size() <= numLandsToStillNeedMore) { + // We need more lands to improve our mana base, explore away the non-lands + return topCard; + } + if (topCard.getCMC() - maxCMCDiff >= predictedMana) { + // We're not casting this in foreseeable future, put it in the graveyard + return topCard; + } + } + + // Put on top of the library (do not mark the card for placement in the graveyard) + return null; + } + +} + 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 8719b21fb9f..8166889a913 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java @@ -1,22 +1,16 @@ package forge.ai.simulation; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCost; import forge.ai.ability.ChangeZoneAi; +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.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.*; import forge.game.cost.Cost; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -27,6 +21,10 @@ import forge.game.spellability.SpellAbilityCondition; import forge.game.zone.ZoneType; import forge.util.TextUtil; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + public class SpellAbilityPicker { private Game game; private Player player; @@ -432,7 +430,11 @@ public class SpellAbilityPicker { return card; } } - return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); + if (sa.getApi() == ApiType.Explore) { + return ExploreAi.shouldPutInGraveyard(fetchList, decider); + } else { + return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); + } } public CardCollectionView chooseSacrificeType(String type, SpellAbility ability, int amount) { diff --git a/forge-gui/res/ai/Cautious.ai b/forge-gui/res/ai/Cautious.ai index 836344b6299..d5564a18cb8 100644 --- a/forge-gui/res/ai/Cautious.ai +++ b/forge-gui/res/ai/Cautious.ai @@ -170,3 +170,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3 # combo deck more appropriately. In Reanimator decks, this logic will make the AI pick the fattest threats in the # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true + +# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the +# card on top of the library +EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2 +# The number of lands on the battlefield when the AI would use Explore to put non-land cards in graveyard if it +# doesn't have a land in hand +EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE=2 diff --git a/forge-gui/res/ai/Default.ai b/forge-gui/res/ai/Default.ai index 6c221362c56..e4746f315a2 100644 --- a/forge-gui/res/ai/Default.ai +++ b/forge-gui/res/ai/Default.ai @@ -170,3 +170,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3 # combo deck more appropriately. In Reanimator decks, this logic will make the AI pick the fattest threats in the # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true + +# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the +# card on top of the library +EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2 +# The number of lands on the battlefield when the AI would use Explore to put non-land cards in graveyard if it +# doesn't have a land in hand +EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE=2 diff --git a/forge-gui/res/ai/Experimental.ai b/forge-gui/res/ai/Experimental.ai index 0b70a4da6f0..d1514f298d4 100644 --- a/forge-gui/res/ai/Experimental.ai +++ b/forge-gui/res/ai/Experimental.ai @@ -171,6 +171,13 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=5 # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true +# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the +# card on top of the library +EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2 +# The number of lands on the battlefield when the AI would use Explore to put non-land cards in graveyard if it +# doesn't have a land in hand +EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE=3 + # -- 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 -- diff --git a/forge-gui/res/ai/Reckless.ai b/forge-gui/res/ai/Reckless.ai index d7dba4f9336..bb9d78336fe 100644 --- a/forge-gui/res/ai/Reckless.ai +++ b/forge-gui/res/ai/Reckless.ai @@ -170,3 +170,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3 # combo deck more appropriately. In Reanimator decks, this logic will make the AI pick the fattest threats in the # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true + +# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the +# card on top of the library +EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=1 +# The number of lands on the battlefield when the AI would use Explore to put non-land cards in graveyard if it +# doesn't have a land in hand +EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE=2