From 926f5f06218eac8ac1cb3f52e456c02ade885372 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Thu, 17 Dec 2020 06:37:17 +0400 Subject: [PATCH] * Copy spell for each other permanents that it could target - fixed that AI can freeze the game, fixed wrong highlighting; --- .../test/cards/single/tor/RadiateTest.java | 80 +++++++++++++++++++ .../CopySpellForEachItCouldTargetEffect.java | 21 +++-- .../filter/common/FilterCreatureOrPlayer.java | 25 +++--- .../FilterCreaturePlayerOrPlaneswalker.java | 28 ++++--- .../common/FilterPermanentOrPlayer.java | 24 +++--- .../FilterPermanentOrPlayerWithCounter.java | 2 +- 6 files changed, 138 insertions(+), 42 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java new file mode 100644 index 0000000000..b733059c78 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java @@ -0,0 +1,80 @@ +package org.mage.test.cards.single.tor; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class RadiateTest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_Play_Manual() { + // Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell + // for each other permanent or player the spell could target. Each copy targets a different one of those + // permanents and players. + addCard(Zone.HAND, playerA, "Radiate", 6); // {3}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + // + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 2); + + // cast bolt and copy spell for each another target + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Radiate", "Lightning Bolt", "Lightning Bolt"); + checkStackSize("before radiate", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + // must have: 2x for corsairs, 2x for bears, 1x for A + checkStackSize("after radiate", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1 + 5); + addTarget(playerA, TestPlayer.TARGET_SKIP); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, 6); // 6 lands + assertPermanentCount(playerB, 0); + assertLife(playerA, 20 - 3); + assertLife(playerB, 20 - 3); + } + + @Test + public void test_Play_AI() { + // possible bug: game freeze or Target wasn't handled... TargetWithAdditionalFilter + + // Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell + // for each other permanent or player the spell could target. Each copy targets a different one of those + // permanents and players. + addCard(Zone.HAND, playerA, "Radiate", 6); // {3}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + // + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 2); + + // cast bolt and copy spell for each another target + // must call commands manually cause it's a bad scenario and AI don't cast it itself + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Radiate", "Lightning Bolt", "Lightning Bolt"); + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); // but AI can choose targets + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, 6); // 6 lands + assertPermanentCount(playerB, 0); + assertLife(playerA, 20 - 3); + assertLife(playerB, 20 - 3); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java index 3145e222af..37921cb89f 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java @@ -80,7 +80,7 @@ public abstract class CopySpellForEachItCouldTargetEffect ex return false; } - // collect objects that can be targeted + // generate copies for each possible target, but do not put it to stack (use must choose targets in custom order later) Spell copy = spell.copySpell(source.getControllerId(), game); modifyCopy(copy, game, source); Target sampleTarget = targetsToBeChanged.iterator().next().getTarget(copy); @@ -135,28 +135,33 @@ public abstract class CopySpellForEachItCouldTargetEffect ex } } - // allow the copies' controller to choose the order that they go on the stack + // allows controller of the copies to choose spells order on stack (by using targeting GUI) for (Player player : game.getPlayers().values()) { if (playerTargetCopyMap.containsKey(player.getId())) { Map targetCopyMap = playerTargetCopyMap.get(player.getId()); if (targetCopyMap != null) { while (!targetCopyMap.isEmpty()) { + // all checks must be make for new copied spell, not original (controller can be changed) + Spell spellSample = targetCopyMap.values().stream().findFirst().get(); FilterInPlay setFilter = filter.copy(); - setFilter.add(new FromSetPredicate(targetCopyMap.keySet())); + setFilter.add(new FromSetPredicate(targetCopyMap.keySet())); // allows only unselected targets Target target = new TargetWithAdditionalFilter(sampleTarget, setFilter); target.setNotTarget(false); // it is targeted, not chosen - target.setMinNumberOfTargets(0); + target.setMinNumberOfTargets(0); // if not selected then it uses first target (see below), same for AI target.setMaxNumberOfTargets(1); - target.setTargetName(filter.getMessage() + " that " + spell.getLogName() + target.setTargetName(filter.getMessage() + " that " + spellSample.getLogName() + " could target (" + targetCopyMap.size() + " remaining)"); - // shortcut if there's only one possible target remaining + if (targetCopyMap.size() > 1 - && target.canChoose(spell.getId(), player.getId(), game)) { + && target.canChoose(spellSample.getId(), player.getId(), game)) { // The original "source" is not applicable here due to the spell being a copy. ie: Zada, Hedron Grinder - player.chooseTarget(Outcome.Neutral, target, spell.getSpellAbility(), game); // not source, but the spell that is copied + Outcome outcome = spellSample.getSpellAbility().getAllEffects().getOutcome(spellSample.getSpellAbility()); + player.chooseTarget(outcome, target, spellSample.getSpellAbility(), game); // not source, but the spell that is copied } + Collection chosenIds = target.getTargets(); if (chosenIds.isEmpty()) { + // uses first target on cancel/non-selected chosenIds = targetCopyMap.keySet(); } List toDelete = new ArrayList<>(); diff --git a/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java b/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java index d44c37311c..c89ad32d6b 100644 --- a/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java +++ b/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java @@ -1,7 +1,5 @@ - package mage.filter.common; -import java.util.UUID; import mage.MageItem; import mage.filter.FilterImpl; import mage.filter.FilterInPlay; @@ -10,8 +8,9 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** - * * @author BetaSteward_at_googlemail.com */ public class FilterCreatureOrPlayer extends FilterImpl implements FilterInPlay { @@ -42,20 +41,24 @@ public class FilterCreatureOrPlayer extends FilterImpl implements Filt @Override public boolean match(MageItem o, Game game) { - if (o instanceof Player) { - return playerFilter.match((Player) o, game); - } else if (o instanceof Permanent) { - return creatureFilter.match((Permanent) o, game); + if (super.match(o, game)) { + if (o instanceof Player) { + return playerFilter.match((Player) o, game); + } else if (o instanceof Permanent) { + return creatureFilter.match((Permanent) o, game); + } } return false; } @Override public boolean match(MageItem o, UUID sourceId, UUID playerId, Game game) { - if (o instanceof Player) { - return playerFilter.match((Player) o, sourceId, playerId, game); - } else if (o instanceof Permanent) { - return creatureFilter.match((Permanent) o, sourceId, playerId, game); + if (super.match(o, game)) { // process predicates + if (o instanceof Player) { + return playerFilter.match((Player) o, sourceId, playerId, game); + } else if (o instanceof Permanent) { + return creatureFilter.match((Permanent) o, sourceId, playerId, game); + } } return false; } diff --git a/Mage/src/main/java/mage/filter/common/FilterCreaturePlayerOrPlaneswalker.java b/Mage/src/main/java/mage/filter/common/FilterCreaturePlayerOrPlaneswalker.java index 34b45ae64a..999d4b98e6 100644 --- a/Mage/src/main/java/mage/filter/common/FilterCreaturePlayerOrPlaneswalker.java +++ b/Mage/src/main/java/mage/filter/common/FilterCreaturePlayerOrPlaneswalker.java @@ -1,12 +1,12 @@ package mage.filter.common; -import java.util.UUID; - import mage.MageItem; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** * @author JRHerlehy Created on 4/8/18. */ @@ -33,22 +33,26 @@ public class FilterCreaturePlayerOrPlaneswalker extends FilterPermanentOrPlayer @Override public boolean match(MageItem o, Game game) { - if (o instanceof Player) { - return playerFilter.match((Player) o, game); - } else if (o instanceof Permanent) { - return creatureFilter.match((Permanent) o, game) - || planeswalkerFilter.match((Permanent) o, game); + if (super.match(o, game)) { + if (o instanceof Player) { + return playerFilter.match((Player) o, game); + } else if (o instanceof Permanent) { + return creatureFilter.match((Permanent) o, game) + || planeswalkerFilter.match((Permanent) o, game); + } } return false; } @Override public boolean match(MageItem o, UUID sourceId, UUID playerId, Game game) { - if (o instanceof Player) { - return playerFilter.match((Player) o, sourceId, playerId, game); - } else if (o instanceof Permanent) { - return creatureFilter.match((Permanent) o, sourceId, playerId, game) - || planeswalkerFilter.match((Permanent) o, sourceId, playerId, game); + if (super.match(o, game)) { // process predicates + if (o instanceof Player) { + return playerFilter.match((Player) o, sourceId, playerId, game); + } else if (o instanceof Permanent) { + return creatureFilter.match((Permanent) o, sourceId, playerId, game) + || planeswalkerFilter.match((Permanent) o, sourceId, playerId, game); + } } return false; } diff --git a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java index f52643b37f..327404dfd1 100644 --- a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java +++ b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayer.java @@ -1,7 +1,5 @@ - package mage.filter.common; -import java.util.UUID; import mage.MageItem; import mage.filter.FilterImpl; import mage.filter.FilterInPlay; @@ -11,6 +9,8 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** * @author nantuko */ @@ -46,20 +46,24 @@ public class FilterPermanentOrPlayer extends FilterImpl implements Fil @Override public boolean match(MageItem o, Game game) { - if (o instanceof Player) { - return playerFilter.match((Player) o, game); - } else if (o instanceof Permanent) { - return permanentFilter.match((Permanent) o, game); + if (super.match(o, game)) { + if (o instanceof Player) { + return playerFilter.match((Player) o, game); + } else if (o instanceof Permanent) { + return permanentFilter.match((Permanent) o, game); + } } return false; } @Override public boolean match(MageItem o, UUID sourceId, UUID playerId, Game game) { - if (o instanceof Player) { - return playerFilter.match((Player) o, sourceId, playerId, game); - } else if (o instanceof Permanent) { - return permanentFilter.match((Permanent) o, sourceId, playerId, game); + if (super.match(o, game)) { // process predicates + if (o instanceof Player) { + return playerFilter.match((Player) o, sourceId, playerId, game); + } else if (o instanceof Permanent) { + return permanentFilter.match((Permanent) o, sourceId, playerId, game); + } } return false; } diff --git a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayerWithCounter.java b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayerWithCounter.java index 16c0eb3623..e2ffcfa07f 100644 --- a/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayerWithCounter.java +++ b/Mage/src/main/java/mage/filter/common/FilterPermanentOrPlayerWithCounter.java @@ -38,7 +38,7 @@ public class FilterPermanentOrPlayerWithCounter extends FilterPermanentOrPlayer @Override public boolean match(MageItem o, UUID sourceId, UUID playerId, Game game) { - if (super.match(o, sourceId, playerId, game)) { + if (super.match(o, sourceId, playerId, game)) { // same as parent class, so can call with full params if (o instanceof Player) { return !((Player) o).getCounters().isEmpty(); } else if (o instanceof Permanent) {