diff --git a/Mage.Sets/src/mage/cards/k/KarnLiberated.java b/Mage.Sets/src/mage/cards/k/KarnLiberated.java index ecc83a1a60..662cc84fa1 100644 --- a/Mage.Sets/src/mage/cards/k/KarnLiberated.java +++ b/Mage.Sets/src/mage/cards/k/KarnLiberated.java @@ -1,5 +1,8 @@ package mage.cards.k; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; @@ -23,10 +26,6 @@ import mage.target.TargetPlayer; import mage.target.common.TargetCardInHand; import mage.util.CardUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - /** * @author bunchOfDevs */ @@ -111,7 +110,7 @@ class KarnLiberatedEffect extends OneShotEffect { && !cards.contains(card)) { // not the exiled cards if (game.getCommandersIds(player).contains(card.getId())) { game.addCommander(new Commander(card)); // TODO: check restart and init - // no needs in initCommander call -- it's uses on game startup (init) + // no needs in initCommander call -- it's used on game startup (init) game.setZone(card.getId(), Zone.COMMAND); } else { player.getLibrary().putOnTop(card, game); @@ -124,10 +123,7 @@ class KarnLiberatedEffect extends OneShotEffect { } for (Card card : cards) { game.getState().setZone(card.getId(), Zone.EXILED); - if (card.isPermanent() - && !card.hasSubtype(SubType.AURA, game)) { - game.getExile().add(exileId, sourceObject.getIdName(), card); - } + game.getExile().add(exileId, sourceObject.getIdName(), card); } game.addDelayedTriggeredAbility(new KarnLiberatedDelayedTriggeredAbility(exileId), source); game.setStartingPlayerId(source.getControllerId()); diff --git a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java index 2c49ea509f..9abe4909fa 100644 --- a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java @@ -1,4 +1,3 @@ - package org.mage.test.commander.duel; import java.io.FileNotFoundException; @@ -6,6 +5,8 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import mage.game.Game; import mage.game.GameException; +import mage.watchers.common.CommanderInfoWatcher; +import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestCommanderDuelBase; @@ -21,7 +22,7 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { // When you cast Prossh, Skyraider of Kher, put X 0/1 red Kobold creature tokens named Kobolds of Kher Keep onto the battlefield, where X is the amount of mana spent to cast Prossh. // Sacrifice another creature: Prossh gets +1/+0 until end of turn. setDecknamePlayerA("Power Hungry.dck"); // Commander = Prosssh, Skyrider of Kher {3}{B}{R}{G} - setDecknamePlayerB("CommanderDuel_UW.dck"); // Daxos of Meletis {1}{W}{U} + setDecknamePlayerB("CommanderDuel_UW.dck"); // Commander = Daxos of Meletis {1}{W}{U} return super.createNewGameAndPlayers(); } @@ -38,7 +39,10 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Savage Summoning"); castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Prossh, Skyraider of Kher"); // 5/5 setStopAt(1, PhaseStep.END_COMBAT); + + setStrictChooseMode(true); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Savage Summoning", 1); assertPermanentCount(playerA, "Prossh, Skyraider of Kher", 1); @@ -66,7 +70,10 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "-14: Restart"); setStopAt(5, PhaseStep.BEGIN_COMBAT); + + setStrictChooseMode(true); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Karn Liberated", 0); assertPermanentCount(playerA, "Silvercoat Lion", 2); @@ -75,6 +82,55 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { } + /** + * If the commander is exiled by Karn (and not returned to the command + * zone), it needs to restart the game in play and not the command zone. + */ + @Test + public void testCommanderRestoredToBattlefieldAfterKarnUltimate() { + // +4: Target player exiles a card from their hand. + // -3: Exile target permanent. + // -14: Restart the game, leaving in exile all non-Aura permanent cards exiled with Karn Liberated. Then put those cards onto the battlefield under your control. + addCard(Zone.BATTLEFIELD, playerA, "Karn Liberated", 1); // Planeswalker (6) + addCard(Zone.HAND, playerA, "Silvercoat Lion", 3); + + addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA); + addTarget(playerA, "Silvercoat Lion"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Daxos of Meletis"); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA); + addTarget(playerA, "Silvercoat Lion"); + + attack(4, playerB, "Daxos of Meletis"); + + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "-3: Exile target permanent", "Daxos of Meletis"); + setChoice(playerB, "No"); // Move commander NOT to command zone + + activateAbility(7, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA); + addTarget(playerA, "Silvercoat Lion"); + activateAbility(9, PhaseStep.PRECOMBAT_MAIN, playerA, "-14: Restart"); + + setStopAt(9, PhaseStep.BEGIN_COMBAT); + + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Karn Liberated", 0); + assertPermanentCount(playerA, "Silvercoat Lion", 3); + assertCommandZoneCount(playerA, "Prossh, Skyraider of Kher", 1); + assertCommandZoneCount(playerB, "Daxos of Meletis", 0); + assertPermanentCount(playerA, "Daxos of Meletis", 1); // Karn brings back the cards under the control of Karn's controller + + CommanderInfoWatcher watcher = currentGame.getState().getWatcher(CommanderInfoWatcher.class, playerB.getCommandersIds().iterator().next()); + Assert.assertEquals("Watcher is reset to 0 commander damage", 0, (int) watcher.getDamageToPlayer().size()); + + } + /** * Mogg infestation creates tokens "for each creature that died this way". * When a commander is moved to a command zone, it doesn't "die", and thus @@ -92,9 +148,14 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Daxos of Meletis"); castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Mogg Infestation"); + addTarget(playerA, playerB); + setChoice(playerB, "Yes"); // Move commander to command zone setStopAt(3, PhaseStep.BEGIN_COMBAT); + + setStrictChooseMode(true); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Mogg Infestation", 1); assertCommandZoneCount(playerB, "Daxos of Meletis", 1); 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 4eb9e30dd9..e040b05632 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,5 +1,10 @@ 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; @@ -56,13 +61,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 static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; /** @@ -80,7 +78,7 @@ public class TestPlayer implements Player { public static final String ATTACK_SKIP = "[attack_skip]"; public static final String NO_TARGET = "NO_TARGET"; // cast spell or activate ability without target defines - private int maxCallsWithoutAction = 100; + private int maxCallsWithoutAction = 400; private int foundNoAction = 0; private boolean AIPlayer; private final List actions = new ArrayList<>(); @@ -179,7 +177,7 @@ public class TestPlayer implements Player { /** * @param maxCallsWithoutAction max number of priority passes a player may - * have for this test (default = 100) + * have for this test (default = 100) */ public void setMaxCallsWithoutAction(int maxCallsWithoutAction) { this.maxCallsWithoutAction = maxCallsWithoutAction; @@ -518,6 +516,7 @@ public class TestPlayer implements Player { if (computerPlayer.activateAbility((ActivatedAbility) newAbility, game)) { actions.remove(action); groupsForTargetHandling = null; + foundNoAction = 0; // Reset enless loop check because of no action return true; } else { game.restoreState(bookmark, ability.getRule()); @@ -854,7 +853,8 @@ public class TestPlayer implements Player { if (numberOfActions == actions.size()) { foundNoAction++; if (foundNoAction > maxCallsWithoutAction) { - throw new AssertionError("More priority calls to " + getName() + " and doing no action than allowed (" + maxCallsWithoutAction + ')'); + throw new AssertionError("More priority calls to " + getName() + + " without taking any action than allowed (" + maxCallsWithoutAction + ") on turn " + game.getTurnNum()); } } else { foundNoAction = 0; @@ -903,12 +903,12 @@ 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") - + (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") + + (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName()))) .sorted() .collect(Collectors.toList()); @@ -932,11 +932,11 @@ public class TestPlayer implements Player { List data = abilities.stream() .map(a -> (a.getZone() + " -> " - + a.getSourceObject(game).getIdName() + " -> " - + (a.toString().length() > 0 - ? a.toString().substring(0, Math.min(20, a.toString().length()) - 1) - : a.getClass().getSimpleName()) - + "...")) + + a.getSourceObject(game).getIdName() + " -> " + + (a.toString().length() > 0 + ? a.toString().substring(0, Math.min(20, a.toString().length()) - 1) + : a.getClass().getSimpleName()) + + "...")) .sorted() .collect(Collectors.toList()); @@ -1290,7 +1290,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(); if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("attack:")) { mustAttackByAction = true; @@ -1779,7 +1779,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; @@ -2082,7 +2082,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); } @@ -3258,7 +3258,7 @@ public class TestPlayer implements Player { @Override public boolean choose(Outcome outcome, Target target, - UUID sourceId, Game game + UUID sourceId, Game game ) { // needed to call here the TestPlayer because it's overwitten return choose(outcome, target, sourceId, game, null); @@ -3266,7 +3266,7 @@ public class TestPlayer implements Player { @Override public boolean choose(Outcome outcome, Cards cards, - TargetCard target, Game game + TargetCard target, Game game ) { if (!choices.isEmpty()) { for (String choose2 : choices) { @@ -3302,7 +3302,7 @@ public class TestPlayer implements Player { @Override public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, - Ability source, Game game + Ability source, Game game ) { // chooseTargetAmount calls for EACH target cycle (e.g. one target per click, see TargetAmount) // if use want to stop choosing then chooseTargetAmount must return false (example: up to xxx) @@ -3314,7 +3314,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 @@ -3367,15 +3367,15 @@ public class TestPlayer implements Player { @Override public boolean choosePile(Outcome outcome, String message, - List pile1, List pile2, - Game game + List pile1, List pile2, + Game game ) { return computerPlayer.choosePile(outcome, message, pile1, pile2, game); } @Override public boolean playMana(Ability ability, ManaCost unpaid, - String promptText, Game game + String promptText, Game game ) { groupsForTargetHandling = null; return computerPlayer.playMana(ability, unpaid, promptText, game); @@ -3389,15 +3389,15 @@ public class TestPlayer implements Player { @Override public UUID chooseBlockerOrder(List blockers, CombatGroup combatGroup, - List blockerOrder, Game game + List blockerOrder, Game game ) { return computerPlayer.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game); } @Override public void assignDamage(int damage, List targets, - String singleTargetName, UUID sourceId, - Game game + String singleTargetName, UUID sourceId, + Game game ) { computerPlayer.assignDamage(damage, targets, singleTargetName, sourceId, game); } @@ -3416,14 +3416,14 @@ public class TestPlayer implements Player { @Override public void pickCard(List cards, Deck deck, - Draft draft + Draft draft ) { computerPlayer.pickCard(cards, deck, draft); } @Override public boolean scry(int value, Ability source, - Game game + Game game ) { // Don't scry at the start of the game. if (game.getTurnNum() == 1 && game.getStep() == null) { @@ -3434,44 +3434,44 @@ public class TestPlayer implements Player { @Override public boolean surveil(int value, Ability source, - Game game + Game game ) { return computerPlayer.surveil(value, source, game); } @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return computerPlayer.moveCards(card, toZone, source, game); } @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { return computerPlayer.moveCards(card, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); } @Override public boolean moveCards(Cards cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return computerPlayer.moveCards(cards, toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return computerPlayer.moveCards(cards, toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { return computerPlayer.moveCards(cards, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); } diff --git a/Mage/src/main/java/mage/game/GameCommanderImpl.java b/Mage/src/main/java/mage/game/GameCommanderImpl.java index fe9a4aa5d8..6731f4f39f 100644 --- a/Mage/src/main/java/mage/game/GameCommanderImpl.java +++ b/Mage/src/main/java/mage/game/GameCommanderImpl.java @@ -1,5 +1,7 @@ package mage.game; +import java.util.Map; +import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.InfoEffect; @@ -16,9 +18,6 @@ import mage.players.Player; import mage.watchers.common.CommanderInfoWatcher; import mage.watchers.common.CommanderPlaysCountWatcher; -import java.util.Map; -import java.util.UUID; - public abstract class GameCommanderImpl extends GameImpl { // private final Map mulliganedCards = new HashMap<>(); @@ -78,7 +77,9 @@ public abstract class GameCommanderImpl extends GameImpl { } public void initCommander(Card commander, Player player) { - commander.moveToZone(Zone.COMMAND, null, this, true); + if (!Zone.EXILED.equals(getState().getZone(commander.getId()))) { // Exile check needed for Karn Liberated restart + commander.moveToZone(Zone.COMMAND, null, this, true); + } commander.getAbilities().setControllerId(player.getId()); Ability ability = new SimpleStaticAbility(Zone.COMMAND, new InfoEffect("Commander effects")); diff --git a/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java b/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java index 9547ff1052..f2794367cc 100644 --- a/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java @@ -1,5 +1,8 @@ package mage.watchers.common; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import mage.MageObject; import mage.cards.Card; import mage.constants.WatcherScope; @@ -11,10 +14,6 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - /* 20130711 *903.14a A player that's been dealt 21 or more combat damage by the same commander * over the course of the game loses the game. (This is a state-based action. See rule 704.) @@ -69,7 +68,7 @@ public class CommanderInfoWatcher extends Watcher { } if (object != null) { StringBuilder sb = new StringBuilder(); - sb.append("" + commanderTypeName + ""); + sb.append("").append(commanderTypeName).append(""); CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); int playsCount = watcher.getPlaysCount(sourceId); if (playsCount > 0) { @@ -80,7 +79,7 @@ public class CommanderInfoWatcher extends Watcher { if (checkCommanderDamage) { for (Map.Entry entry : damageToPlayer.entrySet()) { Player damagedPlayer = game.getPlayer(entry.getKey()); - sb.append("" + commanderTypeName + " did ").append(entry.getValue()).append(" combat damage to player ").append(damagedPlayer.getLogName()).append('.'); + sb.append("").append(commanderTypeName).append(" did ").append(entry.getValue()).append(" combat damage to player ").append(damagedPlayer.getLogName()).append('.'); this.addInfo(object, "Commander" + entry.getKey(), "" + commanderTypeName + " did " + entry.getValue() + " combat damage to player " + damagedPlayer.getLogName() + '.', game); }