From 985599a3b217822bb2eb82de476f2853f3d943a7 Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Tue, 6 Nov 2018 11:59:49 +0000 Subject: [PATCH] Various AI improvements. --- forge-ai/src/main/java/forge/ai/AiProps.java | 2 + .../src/main/java/forge/ai/ComputerUtil.java | 67 ++++++++++++++++++- .../java/forge/ai/ability/DamageDealAi.java | 7 +- .../main/java/forge/ai/ability/DestroyAi.java | 5 ++ .../main/java/forge/ai/ability/PlayAi.java | 7 ++ forge-gui/res/ai/Cautious.ai | 8 +++ forge-gui/res/ai/Default.ai | 8 +++ forge-gui/res/ai/Experimental.ai | 10 ++- forge-gui/res/ai/Reckless.ai | 8 +++ forge-gui/res/cardsfolder/c/charnel_troll.txt | 2 + .../res/cardsfolder/g/gruesome_menagerie.txt | 3 +- 11 files changed, 122 insertions(+), 5 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index cb3f8d99007..69a6fbddf10 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -74,6 +74,8 @@ public enum AiProps { /** */ DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */ DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */ DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */ + AVOID_TARGETING_CREATS_THAT_WILL_DIE ("true"), /** */ + DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION ("true"), /** */ PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */ USE_BERSERK_AGGRESSIVELY ("false"), /** */ MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */ diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 113e0c2bab3..35656ae808e 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -1770,7 +1770,68 @@ public class ComputerUtil { Iterables.addAll(threatened, ComputerUtil.predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility())); return threatened; } - + + /** + * Returns true if the specified creature will die this turn either from lethal damage in combat + * or from a killing spell on stack. + * TODO: This currently does not account for the fact that spells on stack can be countered, can be improved. + * + * @param creature + * A creature to check + * @return true if the creature dies according to current board position. + */ + public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature) { + final Game game = creature.getGame(); + + // a creature will die as a result of combat + boolean willDieInCombat = game.getPhaseHandler().inCombat() + && ComputerUtilCombat.combatantWouldBeDestroyed(creature.getController(), creature, game.getCombat()); + + // a creature will [hopefully] die from a spell on stack + boolean willDieFromSpell = false; + boolean noStackCheck = false; + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); + if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) { + // See if permission is on stack and ignore this check if there is and the relevant AI flag is set + // TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells. + for (SpellAbilityStackInstance si : game.getStack()) { + if (si.getSpellAbility(false).getApi() == ApiType.Counter) { + noStackCheck = true; + break; + } + } + } + willDieFromSpell = !noStackCheck && ComputerUtil.predictThreatenedObjects(creature.getController(), null).contains(creature); + + return willDieInCombat || willDieFromSpell; + } + + /** + * Returns a list of cards excluding any creatures that will die in active combat or from a spell on stack. + * Works only on AI profiles which have AVOID_TARGETING_CREATS_THAT_WILL_DIE enabled, otherwise returns + * the original list. + * + * @param ai + * The AI player performing this evaluation + * @param list + * The list of cards to work with + * @return a filtered list with no dying creatures in it + */ + public static CardCollection filterCreaturesThatWillDieThisTurn(final Player ai, final CardCollection list) { + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); + if (aic.getBooleanProperty(AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) { + // Try to avoid targeting creatures that are dead on board + List willBeKilled = CardLists.filter(list, new Predicate() { + @Override + public boolean apply(Card card) { + return card.isCreature() && ComputerUtil.predictCreatureWillDieThisTurn(ai, card); + } + }); + list.removeAll(willBeKilled); + } + return list; + } + public static boolean playImmediately(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final Zone zone = source.getZone(); @@ -2779,6 +2840,10 @@ public class ComputerUtil { if (ab.getApi() == null) { // only API-based SAs are supported, other things may lead to a NPE (e.g. Ancestral Vision Suspend SA) continue; + } else if (ab.getApi() == ApiType.Mana && "ManaRitual".equals(ab.getParam("AILogic"))) { + // Mana Ritual cards are too complex for the AI to consider casting through a spell effect and will + // lead to a stack overflow. Consider improving. + continue; } SpellAbility abTest = withoutPayingManaCost ? ab.copyWithNoManaCost() : ab.copy(); // at this point, we're assuming that card will be castable from whichever zone it's in by the AI player. 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 473ff354f99..a1502c80c20 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -275,7 +275,7 @@ public class DamageDealAi extends DamageAiBase { final Game game = source.getGame(); List hPlay = getTargetableCards(ai, sa, pl, tgt, activator, source, game); - List killables = CardLists.filter(hPlay, new Predicate() { + CardCollection killables = CardLists.filter(hPlay, new Predicate() { @Override public boolean apply(final Card c) { return c.getSVar("Targeting").equals("Dies") @@ -286,7 +286,10 @@ public class DamageDealAi extends DamageAiBase { }); // Filter AI-specific targets if provided - killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true); + killables = ComputerUtil.filterAITgts(sa, ai, killables, true); + + // Try not to target anything which will already be dead by the time the spell resolves + killables = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, killables); Card targetCard = null; if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) { 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 742692e5c1d..40d3aaf4138 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -178,6 +178,8 @@ public class DestroyAi extends SpellAbilityAi { }); } + // Try to avoid targeting creatures that are dead on board + list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list); if (list.isEmpty()) { return false; } @@ -313,6 +315,9 @@ public class DestroyAi extends SpellAbilityAi { CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); + // Try to avoid targeting creatures that are dead on board + list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list); + if (list.isEmpty() || list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) { return false; } 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 49fd600f9a4..f023a718fb1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -175,6 +175,13 @@ public class PlayAi extends SpellAbilityAi { spell = (Spell) spell.copyWithDefinedCost(abCost); } if( AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) { + // Before accepting, see if the spell has a valid number of targets (it should at this point). + // Proceeding past this point if the spell is not correctly targeted will result + // in "Failed to add to stack" error and the card disappearing from the game completely. + if (!spell.isTargetNumberValid()) { + return false; + } + return true; } } diff --git a/forge-gui/res/ai/Cautious.ai b/forge-gui/res/ai/Cautious.ai index c65e16a652f..b2112e06b01 100644 --- a/forge-gui/res/ai/Cautious.ai +++ b/forge-gui/res/ai/Cautious.ai @@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135 # If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=true +# Options that allow the AI to attempt to optimize targeting for removal and damaging spells. +# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case +# this creature will die in current combat or to a spell which is currently on stack targeting it. +AVOID_TARGETING_CREATS_THAT_WILL_DIE=true +# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it, +# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not. +DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=true + # Only works when AI cheating is enabled in preferences, otherwise does nothing CHEAT_WITH_MANA_ON_SHUFFLE=true diff --git a/forge-gui/res/ai/Default.ai b/forge-gui/res/ai/Default.ai index 9029ae5c99b..2f6b99c304e 100644 --- a/forge-gui/res/ai/Default.ai +++ b/forge-gui/res/ai/Default.ai @@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135 # If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=true +# Options that allow the AI to attempt to optimize targeting for removal and damaging spells. +# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case +# this creature will die in current combat or to a spell which is currently on stack targeting it. +AVOID_TARGETING_CREATS_THAT_WILL_DIE=true +# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it, +# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not. +DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=true + # Only works when AI cheating is enabled in preferences, otherwise does nothing CHEAT_WITH_MANA_ON_SHUFFLE=true diff --git a/forge-gui/res/ai/Experimental.ai b/forge-gui/res/ai/Experimental.ai index 1829c14890b..d844e5c2925 100644 --- a/forge-gui/res/ai/Experimental.ai +++ b/forge-gui/res/ai/Experimental.ai @@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135 # If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=false +# Options that allow the AI to attempt to optimize targeting for removal and damaging spells. +# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case +# this creature will die in current combat or to a spell which is currently on stack targeting it. +AVOID_TARGETING_CREATS_THAT_WILL_DIE=true +# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it, +# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not. +DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=false + # Only works when AI cheating is enabled in preferences, otherwise does nothing CHEAT_WITH_MANA_ON_SHUFFLE=true @@ -219,4 +227,4 @@ AI_IN_DANGER_THRESHOLD=3 # for each evaluation, introducing some unpredictability. AI_IN_DANGER_MAX_THRESHOLD=12 -# <-- there are no options here at the moment --> +# <-- there are no other experimental options here at the moment --> diff --git a/forge-gui/res/ai/Reckless.ai b/forge-gui/res/ai/Reckless.ai index f96968c5056..da788df7ce1 100644 --- a/forge-gui/res/ai/Reckless.ai +++ b/forge-gui/res/ai/Reckless.ai @@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135 # If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=true +# Options that allow the AI to attempt to optimize targeting for removal and damaging spells. +# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case +# this creature will die in current combat or to a spell which is currently on stack targeting it. +AVOID_TARGETING_CREATS_THAT_WILL_DIE=true +# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it, +# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not. +DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=true + # Only works when AI cheating is enabled in preferences, otherwise does nothing CHEAT_WITH_MANA_ON_SHUFFLE=true diff --git a/forge-gui/res/cardsfolder/c/charnel_troll.txt b/forge-gui/res/cardsfolder/c/charnel_troll.txt index 260ef6b81fd..cfcdff7459f 100644 --- a/forge-gui/res/cardsfolder/c/charnel_troll.txt +++ b/forge-gui/res/cardsfolder/c/charnel_troll.txt @@ -9,6 +9,8 @@ SVar:DBPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | Condition SVar:DBSac:DB$ Sacrifice | SacValid# Self | ConditionDefined$ Remembered | ConditionPresent$ Card.Creature | ConditionCompare$ EQ0 | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True A:AB$ PutCounter | Cost$ B G Discard<1/Creature> | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on CARDNAME. +SVar:NeedsToPlayVar:Z GE1 +SVar:Z:Count$ValidGraveyard Creature.YouCtrl AI:RemoveDeck:Random DeckNeeds:Ability$Graveyard DeckHas:Ability$Counters diff --git a/forge-gui/res/cardsfolder/g/gruesome_menagerie.txt b/forge-gui/res/cardsfolder/g/gruesome_menagerie.txt index 8a28cd52744..38137dc5fa1 100644 --- a/forge-gui/res/cardsfolder/g/gruesome_menagerie.txt +++ b/forge-gui/res/cardsfolder/g/gruesome_menagerie.txt @@ -6,5 +6,6 @@ SVar:DBChoose2:DB$ ChooseCard | Defined$ You | Choices$ Creature.YouOwn+cmcEQ2 | SVar:DBChoose3:DB$ ChooseCard | Defined$ You | Choices$ Creature.YouOwn+cmcEQ3 | ChoiceZone$ Graveyard | Amount$ 1 | SubAbility$ DBReturn | RememberChosen$ True | SpellDescription$ Choose a creature card with converted mana cost 3 in your graveyard. SVar:DBReturn:DB$ ChangeZoneAll | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Card.IsRemembered | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -SVar:NeedsToPlay:Creature.YouCtrl+inZoneGraveyard+cmcLE3 +SVar:NeedsToPlayVar:Z GE1 +SVar:Z:Count$ValidGraveyard Creature.YouCtrl+cmcLE3 Oracle:Choose a creature card with converted mana cost 1 in your graveyard, then do the same for creature cards with converted mana costs 2 and 3. Return those cards to the battlefield.