From 86f6d39f5a42436a1e192fe4f78c5b2372871542 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 9 Mar 2020 13:49:07 +0400 Subject: [PATCH] * AI: fixed rollback errors on play cards with target stack (Diplomatic Escort, Not of This World, etc); --- .../java/mage/player/ai/ComputerPlayer.java | 7 +- .../src/mage/cards/n/NotOfThisWorld.java | 105 ++---------------- .../AI/basic/TargetStackObjectByAITest.java | 101 +++++++++++++++++ .../other/TargetsPermanentPredicate.java | 8 +- .../java/mage/target/TargetStackObject.java | 6 +- 5 files changed, 126 insertions(+), 101 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetStackObjectByAITest.java diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 58e18c17a4b..296fe78496b 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -819,10 +819,13 @@ public class ComputerPlayer extends PlayerImpl implements Player { return target.isChosen(); } - if (target.getOriginalTarget() instanceof TargetSpell) { + if (target.getOriginalTarget() instanceof TargetSpell + || target.getOriginalTarget() instanceof TargetStackObject) { if (!game.getStack().isEmpty()) { for (StackObject o : game.getStack()) { - if (o instanceof Spell && !source.getId().equals(o.getStackAbility().getId())) { + if (o instanceof Spell + && !source.getId().equals(o.getStackAbility().getId()) + && target.canTarget(abilityControllerId, o.getStackAbility().getId(), source, game)) { return tryAddTarget(target, o.getId(), source, game); } } diff --git a/Mage.Sets/src/mage/cards/n/NotOfThisWorld.java b/Mage.Sets/src/mage/cards/n/NotOfThisWorld.java index a253cb7073d..651ff4a65d7 100644 --- a/Mage.Sets/src/mage/cards/n/NotOfThisWorld.java +++ b/Mage.Sets/src/mage/cards/n/NotOfThisWorld.java @@ -2,7 +2,6 @@ package mage.cards.n; import mage.abilities.Ability; import mage.abilities.Mode; -import mage.abilities.Modes; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.Condition; import mage.abilities.effects.common.CounterTargetEffect; @@ -10,19 +9,17 @@ import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; -import mage.filter.Filter; +import mage.filter.FilterStackObject; +import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.PowerPredicate; +import mage.filter.predicate.other.TargetsPermanentPredicate; import mage.game.Game; -import mage.game.stack.Spell; -import mage.game.stack.StackAbility; import mage.game.stack.StackObject; import mage.target.Target; -import mage.target.TargetObject; +import mage.target.TargetStackObject; import java.util.Collection; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** @@ -30,13 +27,19 @@ import java.util.UUID; */ public final class NotOfThisWorld extends CardImpl { + private static final FilterStackObject filter = new FilterStackObject("spell or ability that targets a permanent you control"); + + static { + filter.add(new TargetsPermanentPredicate(new FilterControlledPermanent())); + } + public NotOfThisWorld(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.TRIBAL, CardType.INSTANT}, "{7}"); this.subtype.add(SubType.ELDRAZI); // Counter target spell or ability that targets a permanent you control. - this.getSpellAbility().addTarget(new TargetStackObjectTargetingControlledPermanent()); this.getSpellAbility().addEffect(new CounterTargetEffect()); + this.getSpellAbility().addTarget(new TargetStackObject(filter)); // Not of This World costs {7} less to cast if it targets a spell or ability that targets a creature you control with power 7 or greater. this.addAbility(new SimpleStaticAbility(Zone.STACK, new SpellCostReductionSourceEffect(7, NotOfThisWorldCondition.instance))); @@ -52,92 +55,6 @@ public final class NotOfThisWorld extends CardImpl { } } -class TargetStackObjectTargetingControlledPermanent extends TargetObject { - - TargetStackObjectTargetingControlledPermanent() { - this.minNumberOfTargets = 1; - this.maxNumberOfTargets = 1; - this.zone = Zone.STACK; - this.targetName = "spell or ability that targets a permanent you control"; - } - - private TargetStackObjectTargetingControlledPermanent(final TargetStackObjectTargetingControlledPermanent target) { - super(target); - } - - @Override - public Filter getFilter() { - throw new UnsupportedOperationException("Not supported."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - StackObject stackObject = game.getStack().getStackObject(id); - return (stackObject instanceof Spell) || (stackObject instanceof StackAbility); - } - - @Override - public boolean canChoose(UUID sourceId, UUID sourceControllerId, Game game) { - return canChoose(sourceControllerId, game); - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return game.getStack() - .stream() - .filter(stackObject -> stackObject instanceof Spell || stackObject instanceof StackAbility) - .map(StackObject::getStackAbility) - .map(Ability::getModes) - .map(Modes::values) - .flatMap(Collection::stream) - .map(Mode::getTargets) - .flatMap(Collection::stream) - .filter(target -> !target.isNotTarget()) - .map(Target::getTargets) - .flatMap(Collection::stream) - .map(game::getPermanentOrLKIBattlefield) - .anyMatch(permanent -> permanent != null && permanent.isControlledBy(sourceControllerId)); - } - - @Override - public Set possibleTargets(UUID sourceId, UUID sourceControllerId, - Game game) { - return possibleTargets(sourceControllerId, game); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - game.getStack().stream().forEach(stackObject -> { - if (!(stackObject instanceof Spell || stackObject instanceof StackAbility)) { - return; - } - boolean flag = stackObject - .getStackAbility() - .getModes() - .values() - .stream() - .map(Mode::getTargets) - .flatMap(Collection::stream) - .filter(target -> !target.isNotTarget()) - .map(Target::getTargets) - .flatMap(Collection::stream) - .map(game::getPermanentOrLKIBattlefield) - .anyMatch(permanent -> permanent != null && permanent.isControlledBy(sourceControllerId)); - if (flag) { - possibleTargets.add(stackObject.getId()); - } - }); - return possibleTargets; - } - - @Override - public TargetStackObjectTargetingControlledPermanent copy() { - return new TargetStackObjectTargetingControlledPermanent(this); - } - -} - enum NotOfThisWorldCondition implements Condition { instance; diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetStackObjectByAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetStackObjectByAITest.java new file mode 100644 index 00000000000..af84540392e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetStackObjectByAITest.java @@ -0,0 +1,101 @@ +package org.mage.test.AI.basic; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class TargetStackObjectByAITest extends CardTestPlayerBaseWithAIHelps { + + // only PlayerA is AI controlled + // use case: java.lang.IllegalStateException: Target wasn't handled. class:class mage.target.TargetStackObject + + @Test + public void test_TargetStack_Manual() { + // {U}, {T}, Discard a card: Counter target spell or ability that targets a creature. + addCard(Zone.BATTLEFIELD, playerB, "Diplomatic Escort", 1); + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + addCard(Zone.HAND, playerB, "Swamp", 1); + // + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + // A attack and B response + + // attack creature + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears"); + // counter it + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{U}, {T}, Discard a card", "Lightning Bolt", "Lightning Bolt"); + setChoice(playerB, "Swamp"); // discard + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Grizzly Bears", 0); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertTapped("Diplomatic Escort", true); + } + + @Test + public void test_TargetStack_ChooseByAI() { + // {U}, {T}, Discard a card: Counter target spell or ability that targets a creature. + addCard(Zone.BATTLEFIELD, playerB, "Diplomatic Escort", 1); + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + addCard(Zone.HAND, playerB, "Swamp", 1); + // + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + // A attack and B response + + // attack creature + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears"); + // counter it + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{U}, {T}, Discard a card"); // AI choose target + //setChoice(playerB, "Swamp"); // discard + + setStopAt(1, PhaseStep.END_TURN); + //setStrictChooseMode(true); // AI must choose + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Grizzly Bears", 0); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertTapped("Diplomatic Escort", true); + } + + @Test + public void test_TargetStack_PlayByAI() { + // {U}, {T}, Discard a card: Counter target spell or ability that targets a creature. + addCard(Zone.BATTLEFIELD, playerB, "Diplomatic Escort", 1); + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + addCard(Zone.HAND, playerB, "Swamp", 1); + // + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + // A attack and B response + + // attack creature + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears"); + // AI must counter it + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); // AI play with full simulation + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Grizzly Bears", 0); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertTapped("Diplomatic Escort", true); + } +} diff --git a/Mage/src/main/java/mage/filter/predicate/other/TargetsPermanentPredicate.java b/Mage/src/main/java/mage/filter/predicate/other/TargetsPermanentPredicate.java index 6cf6f6e71b8..a78546f22e0 100644 --- a/Mage/src/main/java/mage/filter/predicate/other/TargetsPermanentPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/other/TargetsPermanentPredicate.java @@ -1,7 +1,5 @@ - package mage.filter.predicate.other; -import java.util.UUID; import mage.MageObject; import mage.abilities.Mode; import mage.filter.FilterPermanent; @@ -12,8 +10,9 @@ import mage.game.permanent.Permanent; import mage.game.stack.StackObject; import mage.target.Target; +import java.util.UUID; + /** - * * @author LoneFox */ public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate> { @@ -31,6 +30,9 @@ public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate= this.minNumberOfTargets) { return true; @@ -77,7 +78,8 @@ public class TargetStackObject extends TargetObject { public Set possibleTargets(UUID sourceId, UUID sourceControllerId, Game game) { Set possibleTargets = new HashSet<>(); for (StackObject stackObject : game.getStack()) { - if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) && filter.match(stackObject, sourceId, sourceControllerId, game)) { + if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) + && filter.match(stackObject, sourceId, sourceControllerId, game)) { possibleTargets.add(stackObject.getId()); } }