diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java index 7f2802ac69..9337b72f3b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java @@ -964,9 +964,8 @@ public class MorphTest extends CardTestPlayerBase { setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); - // 1 action must be here ("no" option is restores on failed morph call in playLand) - //assertAllCommandsUsed(); - assertChoicesCount(playerA, 1); + + assertAllCommandsUsed(); assertPermanentCount(playerA, "Zoetic Cavern", 1); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/damage/DeflectingPalmTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/damage/DeflectingPalmTest.java deleted file mode 100644 index 172daea20d..0000000000 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/damage/DeflectingPalmTest.java +++ /dev/null @@ -1,54 +0,0 @@ - -package org.mage.test.cards.abilities.oneshot.damage; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class DeflectingPalmTest extends CardTestPlayerBase { - - /** - * Test that prevented damage will be created with the correct source and - * will trigger the ability of Satyr Firedance - * https://github.com/magefree/mage/issues/804 - */ - - @Test - public void testDamageInPlayer() { - addCard(Zone.BATTLEFIELD, playerA, "Mountain"); - addCard(Zone.BATTLEFIELD, playerA, "Plains"); - // The next time a source of your choice would deal damage to you this turn, prevent that damage. - // If damage is prevented this way, Deflecting Palm deals that much damage to that source's controller. - addCard(Zone.HAND, playerA, "Deflecting Palm"); - // Whenever an instant or sorcery spell you control deals damage to an opponent, Satyr Firedancer deals - // that much damage to target creature that player controls. - addCard(Zone.BATTLEFIELD, playerA, "Satyr Firedancer"); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerB, "Mountain"); - addCard(Zone.HAND, playerB, "Lightning Bolt"); - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB ,"Lightning Bolt", playerA); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA ,"Deflecting Palm", null, "Lightning Bolt"); - setChoice(playerA, "Lightning Bolt"); - addTarget(playerA, "Silvercoat Lion"); // target for Satyr Firedancer - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertGraveyardCount(playerA, "Deflecting Palm", 1); - assertGraveyardCount(playerB, "Lightning Bolt", 1); - - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - assertLife(playerA, 20); - assertLife(playerB, 17); - } -} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java index 874026b6ed..397ba5e480 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java @@ -95,20 +95,20 @@ public class JaceTest extends CardTestPlayerBase { activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Draw a card"); setChoice(playerA, "Pillarfield Ox"); - - setStopAt(3, PhaseStep.BEGIN_COMBAT); + + rollbackTurns(3, PhaseStep.BEGIN_COMBAT, playerA, 0); // Start of turn 3 + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); execute(); - - currentGame.rollbackTurns(0); // Start of turn 3 assertGraveyardCount(playerA, "Pillarfield Ox", 0); // Goes back to hand assertHandCount(playerA, "Pillarfield Ox", 1); - + assertExileCount("Jace, Vryn's Prodigy", 0); - + assertPermanentCount(playerA, "Jace, Telepath Unbound", 0); assertPermanentCount(playerA, "Jace, Vryn's Prodigy", 1); - + Assert.assertFalse("Jace, Vryn's Prodigy may not be flipped", getPermanent("Jace, Vryn's Prodigy").isFlipped()); } @@ -119,7 +119,7 @@ public class JaceTest extends CardTestPlayerBase { // exile Jace, Vryn's Prodigy, then return him to the battefield transformed under his owner's control. String jVryn = "Jace, Vryn's Prodigy"; // {U}{1} 0/2 - //−3: You may cast target instant or sorcery card from your graveyard this turn. If that card would be put into your graveyard this turn, exile it instead. + //−3: You may cast target instant or sorcery card from your graveyard this turn. If that card would be put into your graveyard this turn, exile it instead. String jTelepath = "Jace, Telepath Unbound"; // 5 loyalty // Sorcery, Suspend 4 {U}. Target player draws three cards. diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/DeflectingPalmTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/DeflectingPalmTest.java index ff6fe1e86a..8f58df1b51 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/DeflectingPalmTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/DeflectingPalmTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.replacement.prevent; import mage.constants.PhaseStep; @@ -49,7 +48,7 @@ public class DeflectingPalmTest extends CardTestPlayerBase { */ @Test public void testPreventDamageWithDromokasCommand() { - + setStrictChooseMode(true); // Choose two - // - Prevent all damage target instant or sorcery spell would deal this turn; // - or Target player sacrifices an enchantment; @@ -68,15 +67,21 @@ public class DeflectingPalmTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Deflecting Palm"); setChoice(playerB, "Silvercoat Lion"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dromoka's Command", "Deflecting Palm"); - addTarget(playerA, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dromoka's Command", null, "Deflecting Palm"); + setModeChoice(playerA, "1"); + addTarget(playerA, "Deflecting Palm"); setModeChoice(playerA, "3"); + addTarget(playerA, "Silvercoat Lion"); attack(1, playerA, "Silvercoat Lion"); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); + + assertAllCommandsUsed(); + assertGraveyardCount(playerB, "Deflecting Palm", 1); assertGraveyardCount(playerA, "Dromoka's Command", 1); @@ -87,4 +92,40 @@ public class DeflectingPalmTest extends CardTestPlayerBase { } + /** + * Test that prevented damage will be created with the correct source and + * will trigger the ability of Satyr Firedance + * https://github.com/magefree/mage/issues/804 + */ + @Test + public void testDamageInPlayer() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + // The next time a source of your choice would deal damage to you this turn, prevent that damage. + // If damage is prevented this way, Deflecting Palm deals that much damage to that source's controller. + addCard(Zone.HAND, playerA, "Deflecting Palm"); + // Whenever an instant or sorcery spell you control deals damage to an opponent, Satyr Firedancer deals + // that much damage to target creature that player controls. + addCard(Zone.BATTLEFIELD, playerA, "Satyr Firedancer"); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Deflecting Palm", null, "Lightning Bolt"); + setChoice(playerA, "Lightning Bolt"); + addTarget(playerA, "Silvercoat Lion"); // target for Satyr Firedancer + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Deflecting Palm", 1); + assertGraveyardCount(playerB, "Lightning Bolt", 1); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertLife(playerA, 20); + assertLife(playerB, 17); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java b/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java index 62ffe0fa4c..a3509cd70b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java @@ -37,7 +37,11 @@ public class PlayerAction { } /** - * Calls after action removed from commands queue later (for multi steps action, e.g. AI related) + * Calls after action removed from commands queue later (for multi steps + * action, e.g.AI related) + * + * @param game + * @param player */ public void onActionRemovedLater(Game game, TestPlayer player) { // diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 558a00f866..6538937e25 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -1,8 +1,14 @@ package org.mage.test.player; +import java.io.Serializable; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import mage.MageItem; import mage.MageObject; import mage.MageObjectReference; +import mage.Mana; import mage.ObjectColor; import mage.abilities.*; import mage.abilities.costs.AlternativeSourceCosts; @@ -57,14 +63,6 @@ import mage.util.CardUtil; import org.apache.log4j.Logger; import org.junit.Assert; import org.junit.Ignore; - -import java.io.Serializable; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import mage.Mana; - import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; /** @@ -87,7 +85,8 @@ public class TestPlayer implements Player { private boolean AIPlayer; // full playable AI private boolean AICanChooseInStrictMode = false; // AI can choose in custom aiXXX commands (e.g. on one priority or step) private final List actions = new ArrayList<>(); - private final Map actionsToRemovesLater = new HashMap<>(); // remove actions later, on next step (e.g. for AI commands) + private final Map actionsToRemoveLater = new HashMap<>(); // remove actions later, on next step (e.g. for AI commands) + private final Map>> rollbackActions = new HashMap<>(); // actions to add after a executed rollback private final List choices = new ArrayList<>(); // choices stack for choice private final List targets = new ArrayList<>(); // targets stack for choose (it's uses on empty direct target by cast command) private final Map aliases = new HashMap<>(); // aliases for game objects/players (use it for cards with same name to save and use) @@ -552,16 +551,16 @@ public class TestPlayer implements Player { @Override public boolean priority(Game game) { // later remove actions (ai commands related) - if (actionsToRemovesLater.size() > 0) { + if (actionsToRemoveLater.size() > 0) { List removed = new ArrayList<>(); - actionsToRemovesLater.forEach((action, step) -> { + actionsToRemoveLater.forEach((action, step) -> { if (game.getStep().getType() != step) { action.onActionRemovedLater(game, this); actions.remove(action); removed.add(action); } }); - removed.forEach(actionsToRemovesLater::remove); + removed.forEach(actionsToRemoveLater::remove); } int numberOfActions = actions.size(); @@ -681,11 +680,15 @@ public class TestPlayer implements Player { String[] groups = command.split("\\$"); if (groups.length > 0) { if (groups[0].equals("Rollback")) { - if (groups.length > 1 && groups[1].startsWith("turns=")) { + if (groups.length > 2 && groups[1].startsWith("turns=") && groups[2].startsWith("rollbackBlock=")) { int turns = Integer.parseInt(groups[1].substring(6)); + int rollbackBlockNumber = Integer.parseInt(groups[2].substring(14)); game.rollbackTurns(turns); actions.remove(action); + addActionsAfterRollback(game, rollbackBlockNumber); return true; + } else { + Assert.fail("Rollback command misses parameter: " + command); } } if (groups[0].equals("Concede")) { @@ -710,7 +713,7 @@ public class TestPlayer implements Player { // play step if (command.equals(AI_COMMAND_PLAY_STEP)) { AICanChooseInStrictMode = true; // disable on action's remove - actionsToRemovesLater.put(action, game.getStep().getType()); + actionsToRemoveLater.put(action, game.getStep().getType()); computerPlayer.priority(game); return true; } @@ -1008,6 +1011,32 @@ public class TestPlayer implements Player { return false; } + /** + * Adds actions to the player actions after an executed rollback Actions + * have to be added after the rollback becauuse otherwise the actions are + * not valid because otehr ot the same actions are already taken before the + * rollback. + * + * @param game + * @param rollbackBlock rollback block to add the actions for + */ + private void addActionsAfterRollback(Game game, int rollbackBlockNumber) { + Map> rollbackBlock = rollbackActions.get(rollbackBlockNumber); + if (rollbackBlock != null && !rollbackBlock.isEmpty()) { + for (Map.Entry> entry : rollbackBlock.entrySet()) { + TestPlayer testPlayer = (TestPlayer) game.getPlayer(entry.getKey()); + if (testPlayer != null) { + // Add the actions at the start of the action list + int pos = 0; + for (PlayerAction playerAction : entry.getValue()) { + testPlayer.getActions().add(pos, playerAction); + pos++; + } + } + } + } + } + private void tryToPlayPriority(Game game) { if (AIPlayer) { computerPlayer.priority(game); @@ -1081,13 +1110,13 @@ public class TestPlayer implements Player { List data = cards.stream() .map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ") - + c.getIdName() - + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") - + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() - + (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") - + ", " + (c.isTapped() ? "Tapped" : "Untapped") - + getPrintableAliases(", [", c.getId(), "]") - + (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName()))) + + c.getIdName() + + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") + + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() + + (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") + + ", " + (c.isTapped() ? "Tapped" : "Untapped") + + getPrintableAliases(", [", c.getId(), "]") + + (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName()))) .sorted() .collect(Collectors.toList()); @@ -1111,12 +1140,12 @@ public class TestPlayer implements Player { List data = abilities.stream() .map(a -> (a.getZone() + " -> " - + a.getSourceObject(game).getIdName() + " -> " - + (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified - + (a.toString().length() > 0 - ? a.toString().substring(0, Math.min(20, a.toString().length())) - : a.getClass().getSimpleName()) - + "...")) + + a.getSourceObject(game).getIdName() + " -> " + + (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified + + (a.toString().length() > 0 + ? a.toString().substring(0, Math.min(20, a.toString().length())) + : a.getClass().getSimpleName()) + + "...")) .sorted() .collect(Collectors.toList()); @@ -1531,7 +1560,7 @@ public class TestPlayer implements Player { UUID defenderId = null; boolean mustAttackByAction = false; boolean madeAttackByAction = false; - for (Iterator it = actions.iterator(); it.hasNext(); ) { + for (Iterator it = actions.iterator(); it.hasNext();) { PlayerAction action = it.next(); // aiXXX commands @@ -2112,7 +2141,7 @@ public class TestPlayer implements Player { // skip targets if (targets.get(0).equals(TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " - + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", + + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); targets.remove(0); return true; @@ -2423,7 +2452,7 @@ public class TestPlayer implements Player { this.chooseStrictModeFailed("choice", game, "Triggered list (total " + abilities.size() + "):\n" - + abilities.stream().map(a -> getInfo(a, game)).collect(Collectors.joining("\n"))); + + abilities.stream().map(a -> getInfo(a, game)).collect(Collectors.joining("\n"))); return computerPlayer.chooseTriggeredAbility(abilities, game); } @@ -2538,16 +2567,7 @@ public class TestPlayer implements Player { @Override public void restore(Player player) { - this.modesSet.clear(); - this.modesSet.addAll(((TestPlayer) player).modesSet); - this.actions.clear(); - this.actions.addAll(((TestPlayer) player).actions); - this.choices.clear(); - this.choices.addAll(((TestPlayer) player).choices); - this.targets.clear(); - this.targets.addAll(((TestPlayer) player).targets); - this.aliases.clear(); - this.aliases.putAll(((TestPlayer) player).aliases); + // no rollback for test player meta data (modesSet, actions, choices, targets, aliases) computerPlayer.restore(player); } @@ -3275,17 +3295,17 @@ public class TestPlayer implements Player { public ManaOptions getManaAvailable(Game game) { return computerPlayer.getManaAvailable(game); } - + @Override public void addAvailableTriggeredMana(List availableTriggeredMana) { computerPlayer.addAvailableTriggeredMana(availableTriggeredMana); - } + } @Override public List> getAvailableTriggeredMana() { return computerPlayer.getAvailableTriggeredMana(); } - + @Override public List getPlayable(Game game, boolean hidden) { return computerPlayer.getPlayable(game, hidden); @@ -3681,7 +3701,7 @@ public class TestPlayer implements Player { // skip targets if (targets.get(0).equals(TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " - + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", + + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); targets.remove(0); return false; // false in chooseTargetAmount = stop to choose @@ -4031,4 +4051,9 @@ public class TestPlayer implements Player { public void setAICanChooseInStrictMode(boolean AICanChooseInStrictMode) { this.AICanChooseInStrictMode = AICanChooseInStrictMode; } + + public Map>> getRollbackActions() { + return rollbackActions; + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/rollback/DemonicPactTest.java b/Mage.Tests/src/test/java/org/mage/test/rollback/DemonicPactTest.java index b48a60b38b..9124fac54a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/rollback/DemonicPactTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/rollback/DemonicPactTest.java @@ -87,13 +87,13 @@ public class DemonicPactTest extends CardTestPlayerBase { * the game. The log says I'm the winner and the opponent lost and that is * immediately after rollback request. */ - @Test public void testPactOfNegationRollback() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, "Silvercoat Lion", 1); addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - addCard(Zone.BATTLEFIELD, playerB, "Island", 5); // Counter target spell. // At the beginning of your next upkeep, pay {3}{U}{U}. If you don't, you lose the game. @@ -106,22 +106,21 @@ public class DemonicPactTest extends CardTestPlayerBase { rollbackTurns(2, PhaseStep.PRECOMBAT_MAIN, playerB, 0); - setStrictChooseMode(true); + setChoice(playerB, "Yes"); + setStopAt(2, PhaseStep.BEGIN_COMBAT); execute(); - - assertAllCommandsUsed(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Silvercoat Lion", 1); assertGraveyardCount(playerB, "Pact of Negation", 1); - + Assert.assertTrue("Player A is still in game", playerA.isInGame()); Assert.assertTrue("Player B is still in game", playerB.isInGame()); - - assertTappedCount("Island", true, 5); + assertTappedCount("Island", true, 5); } - + } diff --git a/Mage.Tests/src/test/java/org/mage/test/rollback/NewCreaturesAreRemovedTest.java b/Mage.Tests/src/test/java/org/mage/test/rollback/NewCreaturesAreRemovedTest.java index a61042996f..f2cd3eecaf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/rollback/NewCreaturesAreRemovedTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/rollback/NewCreaturesAreRemovedTest.java @@ -1,4 +1,3 @@ - package org.mage.test.rollback; import mage.constants.PhaseStep; @@ -70,28 +69,38 @@ public class NewCreaturesAreRemovedTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Port Town"); // Land addCard(Zone.HAND, playerA, "Island"); // Land - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 3); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // TODO: Check why the test fails (related to rollback?) if the number is set to 3 addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 3); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tamiyo's Journal"); - attack(2, playerB, "Pillarfield Ox"); + attack(2, playerB, "Pillarfield Ox"); // A = 18 + + attack(3, playerA, "Silvercoat Lion"); // B = 18 - attack(3, playerA, "Silvercoat Lion"); rollbackTurns(3, PhaseStep.END_TURN, playerA, 0); + rollbackAfterActionsStart(); + attack(3, playerA, "Silvercoat Lion"); // B = 18 + rollbackAfterActionsEnd(); - attack(4, playerB, "Pillarfield Ox"); - - attack(5, playerA, "Silvercoat Lion"); + attack(4, playerB, "Pillarfield Ox"); // A =16 + attack(5, playerA, "Silvercoat Lion"); // B = 16 rollbackTurns(5, PhaseStep.END_TURN, playerA, 0); + rollbackAfterActionsStart(); + attack(5, playerA, "Silvercoat Lion"); // B = 16 + rollbackAfterActionsEnd(); - attack(6, playerB, "Pillarfield Ox"); + attack(6, playerB, "Pillarfield Ox"); // A = 14 playLand(7, PhaseStep.PRECOMBAT_MAIN, playerA, "Port Town"); - attack(7, playerA, "Silvercoat Lion"); + attack(7, playerA, "Silvercoat Lion"); // B = 14 rollbackTurns(7, PhaseStep.POSTCOMBAT_MAIN, playerA, 0); + rollbackAfterActionsStart(); + playLand(7, PhaseStep.PRECOMBAT_MAIN, playerA, "Port Town"); + attack(7, playerA, "Silvercoat Lion"); // B = 14 + rollbackAfterActionsEnd(); setStopAt(7, PhaseStep.END_TURN); execute(); @@ -100,8 +109,8 @@ public class NewCreaturesAreRemovedTest extends CardTestPlayerBase { assertTapped("Port Town", false); assertPermanentCount(playerA, "Clue", 3); - assertLife(playerA, 14); assertLife(playerB, 14); + assertLife(playerA, 14); } diff --git a/Mage.Tests/src/test/java/org/mage/test/rollback/StateValuesTest.java b/Mage.Tests/src/test/java/org/mage/test/rollback/StateValuesTest.java index c751d8799e..c8c309da1b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/rollback/StateValuesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/rollback/StateValuesTest.java @@ -30,20 +30,29 @@ public class StateValuesTest extends CardTestPlayerBase { attack(3, playerA, "Dragon Whelp"); rollbackTurns(3, PhaseStep.BEGIN_COMBAT, playerA, 0); + rollbackAfterActionsStart(); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: "); + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: "); + + attack(3, playerA, "Dragon Whelp"); + rollbackAfterActionsEnd(); setStopAt(4, PhaseStep.UPKEEP); execute(); - assertLife(playerA, 20); - assertLife(playerB, 12); - assertPermanentCount(playerA, "Dragon Whelp", 1); assertGraveyardCount(playerA, "Dragon Whelp", 0); + assertLife(playerA, 20); + assertLife(playerB, 12); + } @Test public void testBriarbridgePatrol() { + setStrictChooseMode(true); + // Whenever Briarbridge Patrol deals damage to one or more creatures, investigate (Create a colorless Clue artifact token onto the battlefield with "{2}, Sacrifice this artifact: Draw a card."). // At the beginning of each end step, if you sacrificed three or more Clues this turn, you may put a creature card from your hand onto the battlefield. addCard(Zone.BATTLEFIELD, playerA, "Briarbridge Patrol", 1); // 3/3 @@ -55,11 +64,19 @@ public class StateValuesTest extends CardTestPlayerBase { attack(3, playerA, "Briarbridge Patrol"); block(3, playerB, "Pillarfield Ox", "Briarbridge Patrol"); + rollbackTurns(3, PhaseStep.POSTCOMBAT_MAIN, playerA, 0); + rollbackAfterActionsStart(); + attack(3, playerA, "Briarbridge Patrol"); + block(3, playerB, "Pillarfield Ox", "Briarbridge Patrol"); + rollbackAfterActionsEnd(); + setStopAt(3, PhaseStep.END_TURN); execute(); + assertAllCommandsUsed(); + assertLife(playerA, 20); assertLife(playerB, 20); diff --git a/Mage.Tests/src/test/java/org/mage/test/rollback/TransformTest.java b/Mage.Tests/src/test/java/org/mage/test/rollback/TransformTest.java index 05fcab6428..878db38a62 100644 --- a/Mage.Tests/src/test/java/org/mage/test/rollback/TransformTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/rollback/TransformTest.java @@ -1,4 +1,3 @@ - package org.mage.test.rollback; import mage.constants.PhaseStep; @@ -50,6 +49,7 @@ public class TransformTest extends CardTestPlayerBase { // BACK: It That Rides as One // Creature 4/4 First strike, lifelink addCard(Zone.HAND, playerA, "Lone Rider"); // Creature {1}{W} 1/1 + // When Venerable Monk enters the battlefield, you gain 2 life. addCard(Zone.HAND, playerA, "Venerable Monk"); // Creature {2}{W} 2/2 @@ -59,6 +59,11 @@ public class TransformTest extends CardTestPlayerBase { attack(3, playerA, "Lone Rider"); rollbackTurns(3, PhaseStep.END_TURN, playerA, 0); + + castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Venerable Monk"); + + attack(3, playerA, "Lone Rider"); + setStopAt(4, PhaseStep.PRECOMBAT_MAIN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index abb4c38c4b..8133445f79 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1,5 +1,14 @@ package org.mage.test.serverside.base.impl; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import mage.MageObject; import mage.Mana; import mage.ObjectColor; @@ -35,15 +44,6 @@ import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestAPI; import org.mage.test.serverside.base.MageTestPlayerBase; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - /** * API for test initialization and asserting the test results. * @@ -130,6 +130,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement protected String deckNameC; protected String deckNameD; + private int rollbackBlock = 0; // used to handle actions that have to be added aufter a rollback + private boolean rollbackBlockActive = false; + private TestPlayer rollbackPlayer = null; + protected enum ExpectedType { TURN_NUMBER, RESULT, @@ -197,6 +201,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement addCard(Zone.LIBRARY, playerB, "Plains", 10); } + /** + * + * @throws GameException + * @throws FileNotFoundException + */ @Before public void reset() throws GameException, FileNotFoundException { if (currentGame != null) { @@ -223,6 +232,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement gameOptions = new GameOptions(); + rollbackBlock = 0; + rollbackBlockActive = false; + } abstract protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException; @@ -279,7 +291,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } } Assert.assertFalse("Wrong stop command on " + this.stopOnTurn + " / " + this.stopAtStep + " (" + this.stopAtStep.getIndex() + ")" - + " (found actions after stop on " + maxTurn + " / " + maxPhase + ")", + + " (found actions after stop on " + maxTurn + " / " + maxPhase + ")", (maxTurn > this.stopOnTurn) || (maxTurn == this.stopOnTurn && maxPhase > this.stopAtStep.getIndex())); for (Player player : currentGame.getPlayers().values()) { @@ -296,6 +308,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement gameOptions.stopAtStep = stopAtStep; currentGame.setGameOptions(gameOptions); currentGame.start(activePlayer.getId()); + currentGame.setGameStopped(true); // used for rollback handling long t2 = System.nanoTime(); logger.debug("Winner: " + currentGame.getWinner()); logger.info(Thread.currentThread().getStackTrace()[2].getMethodName() + " has been executed. Execution time: " + (t2 - t1) / 1000000 + " ms"); @@ -329,13 +342,34 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement return player; } + private void addPlayerAction(TestPlayer player, int turnNum, PhaseStep step, String action) { + PlayerAction playerAction = new PlayerAction("", turnNum, step, action); + addPlayerAction(player, playerAction); + } + + private void addPlayerAction(TestPlayer player, String actionName, int turnNum, PhaseStep step, String action) { + PlayerAction playerAction = new PlayerAction(actionName, turnNum, step, action); + addPlayerAction(player, playerAction); + } + + private void addPlayerAction(TestPlayer player, PlayerAction playerAction) { + if (rollbackBlockActive) { + rollbackPlayer.getRollbackActions() + .computeIfAbsent(rollbackBlock, block -> new HashMap<>()) + .computeIfAbsent(player.getId(), playerId -> new ArrayList<>()) + .add(playerAction); + } else { + player.addAction(playerAction); + } + } + // check commands private void check(String checkName, int turnNum, PhaseStep step, TestPlayer player, String command, String... params) { String res = CHECK_PREFIX + command; for (String param : params) { res += CHECK_PARAM_DELIMETER + param; } - player.addAction(checkName, turnNum, step, res); + addPlayerAction(player, checkName, turnNum, step, res); } public void checkPT(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer power, Integer toughness) { @@ -452,7 +486,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement for (String param : params) { res += CHECK_PARAM_DELIMETER + param; } - player.addAction(showName, turnNum, step, res); + addPlayerAction(player, showName, turnNum, step, res); } public void showLibrary(String showName, int turnNum, PhaseStep step, TestPlayer player) { @@ -564,8 +598,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param cardName Card name in string format. * @param count Amount of cards to be added. * @param tapped In case gameZone is Battlefield, determines whether - * permanent should be tapped. In case gameZone is other than Battlefield, - * {@link IllegalArgumentException} is thrown + * permanent should be tapped. In case gameZone is other + * than Battlefield, {@link IllegalArgumentException} is + * thrown */ @Override public void addCard(Zone gameZone, TestPlayer player, String cardName, int count, boolean tapped) { @@ -744,10 +779,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param cardName Card name to compare with. * @param power Expected power to compare with. * @param toughness Expected toughness to compare with. - * @param scope {@link mage.filter.Filter.ComparisonScope} Use ANY, if you - * want "at least one creature with given name should have specified p\t" - * Use ALL, if you want "all creature with gived name should have specified - * p\t" + * @param scope {@link mage.filter.Filter.ComparisonScope} Use ANY, if + * you want "at least one creature with given name should + * have specified p\t" Use ALL, if you want "all creature + * with gived name should have specified p\t" */ @Override public void assertPowerToughness(Player player, String cardName, int power, int toughness, Filter.ComparisonScope scope) @@ -1432,36 +1467,40 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void playLand(int turnNum, PhaseStep step, TestPlayer player, String cardName) { //Assert.assertNotEquals("", cardName); assertAliaseSupportInActivateCommand(cardName, false); - player.addAction(turnNum, step, ACTIVATE_PLAY + cardName); + addPlayerAction(player, turnNum, step, ACTIVATE_PLAY + cardName); } public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName) { //Assert.assertNotEquals("", cardName); assertAliaseSupportInActivateCommand(cardName, false); - player.addAction(turnNum, step, ACTIVATE_CAST + cardName); + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName); } public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, Player target) { //Assert.assertNotEquals("", cardName); // warning, target in spell cast command setups without choose target call assertAliaseSupportInActivateCommand(cardName, false); - player.addAction(turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName()); + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName()); } public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, Player target, int manaInPool) { //Assert.assertNotEquals("", cardName); assertAliaseSupportInActivateCommand(cardName, false); - player.addAction(turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName() + "$manaInPool=" + manaInPool); + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName() + "$manaInPool=" + manaInPool); } /** * AI play one PRIORITY with multi game simulations (calcs and play ONE best - * action, can be called with stack) All choices must be made by AI (e.g. - * strict mode possible) + * action, can be called with stack) All choices must be made by AI + * (e.g.strict mode possible) + * + * @param turnNum + * @param step + * @param player */ public void aiPlayPriority(int turnNum, PhaseStep step, TestPlayer player) { assertAiPlayAndGameCompatible(player); - player.addAction(createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_PRIORITY)); + addPlayerAction(player, createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_PRIORITY)); } /** @@ -1471,7 +1510,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement */ public void aiPlayStep(int turnNum, PhaseStep step, TestPlayer player) { assertAiPlayAndGameCompatible(player); - player.addAction(createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_STEP)); + addPlayerAction(player, createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_STEP)); } public PlayerAction createAIPlayerAction(int turnNum, PhaseStep step, String aiCommand) { @@ -1497,12 +1536,14 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void waitStackResolved(int turnNum, PhaseStep step, TestPlayer player, boolean skipOneStackObjectOnly) { String command = "waitStackResolved" + (skipOneStackObjectOnly ? ":1" : ""); - player.addAction(turnNum, step, command); + addPlayerAction(player, turnNum, step, command); } /** * Rollback the number of given turns: 0 = rollback to the start of the - * current turn + * current turn. Use the commands rollbackAfterActionsStart() and + * rollbackAfterActionsEnd() to define a block of actions, that will be + * added and executed after the rollback. * * @param turnNum * @param step @@ -1510,7 +1551,33 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param turns */ public void rollbackTurns(int turnNum, PhaseStep step, TestPlayer player, int turns) { - player.addAction(turnNum, step, "playerAction:Rollback" + "$turns=" + turns); + rollbackBlock++; + addPlayerAction(player, turnNum, step, "playerAction:Rollback" + "$turns=" + turns + "$rollbackBlock=" + rollbackBlock); + rollbackPlayer = player; + } + + /** + * Adds a number of actions that will be added to the to the start of the + * list of actions of the players but only after the rollback is executed + * because otherwis the actions are executed to early and would lead to + * invalid actions (e.g. casting the same spell twice). + */ + public void rollbackAfterActionsStart() throws IllegalStateException { + if (rollbackPlayer == null || rollbackBlock < 1) { + throw new IllegalStateException("There was no rollback action defined before. You can use this command only after a rollback action."); + } + rollbackBlockActive = true; + } + + /** + * Ends a block of actions to be added after an rollback action + */ + public void rollbackAfterActionsEnd() throws IllegalStateException { + if (rollbackBlockActive = false || rollbackPlayer == null) { + throw new IllegalStateException("There was no rollback action defined before or no rollback block started. You can use this command only after a rollback action."); + } + rollbackBlockActive = false; + rollbackPlayer = null; } /** @@ -1521,7 +1588,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param player */ public void concede(int turnNum, PhaseStep step, TestPlayer player) { - player.addAction(turnNum, step, "playerAction:Concede"); + addPlayerAction(player, turnNum, step, "playerAction:Concede"); } /** @@ -1530,14 +1597,14 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param player * @param cardName * @param targetName for modes you can add "mode=3" before target name, - * multiple targets can be seperated by ^, not target marks as - * TestPlayer.NO_TARGET + * multiple targets can be seperated by ^, not target + * marks as TestPlayer.NO_TARGET */ public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, String targetName) { //Assert.assertNotEquals("", cardName); assertAliaseSupportInActivateCommand(cardName, true); assertAliaseSupportInActivateCommand(targetName, true); - player.addAction(turnNum, step, ACTIVATE_CAST + cardName + "$target=" + targetName); + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + "$target=" + targetName); } public enum StackClause { @@ -1580,11 +1647,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement assertAliaseSupportInActivateCommand(targetName, true); assertAliaseSupportInActivateCommand(spellOnStack, false); if (StackClause.WHILE_ON_STACK == clause) { - player.addAction(turnNum, step, ACTIVATE_CAST + cardName + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName) + "$spellOnStack=" + spellOnStack); } else { - player.addAction(turnNum, step, ACTIVATE_CAST + cardName + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName) + "$!spellOnStack=" + spellOnStack); } @@ -1603,7 +1670,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement if (spellOnTopOfStack != null && !spellOnTopOfStack.isEmpty()) { action += "$spellOnTopOfStack=" + spellOnTopOfStack; } - player.addAction(turnNum, step, action); + addPlayerAction(player, turnNum, step, action); } public void activateManaAbility(int turnNum, PhaseStep step, TestPlayer player, String ability) { @@ -1612,20 +1679,20 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void activateManaAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, int timesToActivate) { for (int i = 0; i < timesToActivate; i++) { - player.addAction(turnNum, step, ACTIVATE_MANA + ability); + addPlayerAction(player, turnNum, step, ACTIVATE_MANA + ability); } } public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability) { // TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't assertAliaseSupportInActivateCommand(ability, false); - player.addAction(turnNum, step, ACTIVATE_ABILITY + ability); + addPlayerAction(player, turnNum, step, ACTIVATE_ABILITY + ability); } public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, Player target) { // TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't assertAliaseSupportInActivateCommand(ability, false); - player.addAction(turnNum, step, ACTIVATE_ABILITY + ability + "$targetPlayer=" + target.getName()); + addPlayerAction(player, turnNum, step, ACTIVATE_ABILITY + ability + "$targetPlayer=" + target.getName()); } public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String... targetNames) { @@ -1634,7 +1701,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement Arrays.stream(targetNames).forEach(n -> { assertAliaseSupportInActivateCommand(n, true); }); - player.addAction(turnNum, step, ACTIVATE_ABILITY + ability + "$target=" + String.join("^", targetNames)); + addPlayerAction(player, turnNum, step, ACTIVATE_ABILITY + ability + "$target=" + String.join("^", targetNames)); } /** @@ -1682,31 +1749,31 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } sb.append(spellOnStack); } - player.addAction(turnNum, step, sb.toString()); + addPlayerAction(player, turnNum, step, sb.toString()); } public void addCounters(int turnNum, PhaseStep step, TestPlayer player, String cardName, CounterType type, int count) { //Assert.assertNotEquals("", cardName); - player.addAction(turnNum, step, "addCounters:" + cardName + '$' + type.getName() + '$' + count); + addPlayerAction(player, turnNum, step, "addCounters:" + cardName + '$' + type.getName() + '$' + count); } public void attack(int turnNum, TestPlayer player, String attacker) { //Assert.assertNotEquals("", attacker); assertAliaseSupportInActivateCommand(attacker, false); // it uses old special notation like card_name:index - player.addAction(turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker); + addPlayerAction(player, turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker); } public void attack(int turnNum, TestPlayer player, String attacker, TestPlayer defendingPlayer) { //Assert.assertNotEquals("", attacker); assertAliaseSupportInActivateCommand(attacker, false); // it uses old special notation like card_name:index - player.addAction(turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker + "$defendingPlayer=" + defendingPlayer.getName()); + addPlayerAction(player, turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker + "$defendingPlayer=" + defendingPlayer.getName()); } public void attack(int turnNum, TestPlayer player, String attacker, String planeswalker) { //Assert.assertNotEquals("", attacker); assertAliaseSupportInActivateCommand(attacker, false); // it uses old special notation like card_name:index assertAliaseSupportInActivateCommand(planeswalker, false); - player.addAction(turnNum, PhaseStep.DECLARE_ATTACKERS, new StringBuilder("attack:").append(attacker).append("$planeswalker=").append(planeswalker).toString()); + addPlayerAction(player, turnNum, PhaseStep.DECLARE_ATTACKERS, new StringBuilder("attack:").append(attacker).append("$planeswalker=").append(planeswalker).toString()); } public void attackSkip(int turnNum, TestPlayer player) { @@ -1718,7 +1785,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement //Assert.assertNotEquals("", attacker); assertAliaseSupportInActivateCommand(blocker, false); // it uses old special notation like card_name:index assertAliaseSupportInActivateCommand(attacker, false); - player.addAction(turnNum, PhaseStep.DECLARE_BLOCKERS, "block:" + blocker + '$' + attacker); + addPlayerAction(player, turnNum, PhaseStep.DECLARE_BLOCKERS, "block:" + blocker + '$' + attacker); } public void blockSkip(int turnNum, TestPlayer player) { @@ -1751,10 +1818,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * * @param player * @param choice starting with "1" for mode 1, "2" for mode 2 and so on (to - * set multiple modes call the command multiple times). If a spell mode can - * be used only once like Demonic Pact, the value has to be set to the - * number of the remaining modes (e.g. if only 2 are left the number need to - * be 1 or 2). + * set multiple modes call the command multiple times). If a + * spell mode can be used only once like Demonic Pact, the + * value has to be set to the number of the remaining modes + * (e.g. if only 2 are left the number need to be 1 or 2). */ public void setModeChoice(TestPlayer player, String choice) { player.addModeChoice(choice); @@ -1765,12 +1832,13 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * * @param player * @param target you can add multiple targets by separating them by the "^" - * character e.g. "creatureName1^creatureName2" you can qualify the target - * additional by setcode e.g. "creatureName-M15" you can add [no copy] to - * the end of the target name to prohibit targets that are copied you can - * add [only copy] to the end of the target name to allow only targets that - * are copies. For modal spells use a prefix with the mode number: - * mode=1Lightning Bolt^mode=2Silvercoat Lion + * character e.g. "creatureName1^creatureName2" you can + * qualify the target additional by setcode e.g. + * "creatureName-M15" you can add [no copy] to the end of the + * target name to prohibit targets that are copied you can add + * [only copy] to the end of the target name to allow only + * targets that are copies. For modal spells use a prefix with + * the mode number: mode=1Lightning Bolt^mode=2Silvercoat Lion */ // TODO: mode options doesn't work here (see BrutalExpulsionTest) public void addTarget(TestPlayer player, String target) {