diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index f775e378ff6..054fca62154 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -635,7 +635,7 @@ public class HumanPlayer extends PlayerImpl { } // choose one or multiple cards - if (cards == null) { + if (cards == null || cards.isEmpty()) { return false; } @@ -666,6 +666,11 @@ public class HumanPlayer extends PlayerImpl { options.put("choosable", (Serializable) choosable); } + // if nothing to choose then show window (user must see non selectable items and click on any of them) + if (required && choosable.isEmpty()) { + required = false; + } + updateGameStatePriority("choose(4)", game); prepareForResponse(game); if (!isExecutingMacro()) { @@ -704,7 +709,7 @@ public class HumanPlayer extends PlayerImpl { return true; } - if (cards == null) { + if (cards == null || cards.isEmpty()) { return false; } @@ -735,6 +740,11 @@ public class HumanPlayer extends PlayerImpl { options.put("choosable", (Serializable) choosable); } + // if nothing to choose then show window (user must see non selectable items and click on any of them) + if (required && choosable.isEmpty()) { + required = false; + } + updateGameStatePriority("chooseTarget(5)", game); prepareForResponse(game); if (!isExecutingMacro()) { diff --git a/Mage.Sets/src/mage/cards/a/AgonizingRemorse.java b/Mage.Sets/src/mage/cards/a/AgonizingRemorse.java index c3277ebc2bc..2b4c26c059c 100644 --- a/Mage.Sets/src/mage/cards/a/AgonizingRemorse.java +++ b/Mage.Sets/src/mage/cards/a/AgonizingRemorse.java @@ -26,7 +26,8 @@ public final class AgonizingRemorse extends CardImpl { public AgonizingRemorse(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}"); - // Target opponent reveals their hand. You choose a nonland card from it or a card from their graveyard. Exile that card. You lose 1 life. + // Target opponent reveals their hand. You choose a nonland card from + // it or a card from their graveyard. Exile that card. You lose 1 life. this.getSpellAbility().addEffect(new AgonizingRemorseEffect()); this.getSpellAbility().addTarget(new TargetOpponent()); } @@ -45,8 +46,8 @@ class AgonizingRemorseEffect extends OneShotEffect { AgonizingRemorseEffect() { super(Outcome.Benefit); - staticText = "Target opponent reveals their hand. You choose a nonland card from it " + - "or a card from their graveyard. Exile that card. You lose 1 life."; + staticText = "Target opponent reveals their hand. You choose a nonland card from it " + + "or a card from their graveyard. Exile that card. You lose 1 life."; } private AgonizingRemorseEffect(final AgonizingRemorseEffect effect) { @@ -68,12 +69,15 @@ class AgonizingRemorseEffect extends OneShotEffect { opponent.revealCards(source, opponent.getHand(), game); TargetCard target; Cards cards; - if (controller.chooseUse(outcome, "Exile a card from hand or graveyard?", null, "Hand", "Graveyard", source, game)) { - target = new TargetCard(Zone.HAND, new FilterNonlandCard("nonland card in " + opponent.getName() + "'s hand")); + if (controller.chooseUse(outcome, "Exile a card from hand or graveyard?", + null, "Hand", "Graveyard", source, game)) { + target = new TargetCard(Zone.HAND, new FilterNonlandCard("nonland card in " + + opponent.getName() + "'s hand")); target.setNotTarget(true); cards = opponent.getHand(); } else { - target = new TargetCard(Zone.GRAVEYARD, new FilterCard("card in " + opponent.getName() + "'s graveyard")); + target = new TargetCard(Zone.GRAVEYARD, new FilterCard("card in " + + opponent.getName() + "'s graveyard")); target.setNotTarget(true); cards = opponent.getGraveyard(); } @@ -85,6 +89,7 @@ class AgonizingRemorseEffect extends OneShotEffect { return true; } controller.moveCards(card, Zone.EXILED, source, game); + controller.loseLife(1, game, false); return true; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/d/DrivenDespair.java b/Mage.Sets/src/mage/cards/d/DrivenDespair.java index 09d29f92ce9..b6edb290559 100644 --- a/Mage.Sets/src/mage/cards/d/DrivenDespair.java +++ b/Mage.Sets/src/mage/cards/d/DrivenDespair.java @@ -28,20 +28,26 @@ public final class DrivenDespair extends SplitCard { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, new CardType[]{CardType.SORCERY}, "{1}{G}", "{1}{B}", SpellAbilityType.SPLIT_AFTERMATH); // Until end of turn, creatures you control gain trample and "Whenever this creature deals combat damage to a player, draw a card." - getLeftHalfCard().getSpellAbility().addEffect(new GainAbilityControlledEffect(TrampleAbility.getInstance(), Duration.EndOfTurn)); + getLeftHalfCard().getSpellAbility().addEffect(new GainAbilityControlledEffect( + TrampleAbility.getInstance(), Duration.EndOfTurn, StaticFilters.FILTER_PERMANENT_CREATURES) + .setText("Until end of turn, creatures you control gain trample")); TriggeredAbility ability = new DealsCombatDamageToAPlayerTriggeredAbility(new DrawCardSourceControllerEffect(1), false); getLeftHalfCard().getSpellAbility().addEffect(new GainAbilityControlledEffect(ability, Duration.EndOfTurn) - .setText("and \"Whenever this creature deals combat damage to a player, draw a card.\"")); + .setText("\"Whenever this creature deals combat damage to a player, draw a card.\"") + .concatBy("and")); // Despair {1}{B} // Sorcery // Aftermath getRightHalfCard().addAbility(new AftermathAbility().setRuleAtTheTop(true)); // Until end of turn, creatures you control gain menace and "Whenever this creature deals combat damage to a player, that player discards a card." - getRightHalfCard().getSpellAbility().addEffect(new GainAbilityControlledEffect(new MenaceAbility(), Duration.EndOfTurn, StaticFilters.FILTER_CONTROLLED_CREATURES)); + getRightHalfCard().getSpellAbility().addEffect(new GainAbilityControlledEffect( + new MenaceAbility(), Duration.EndOfTurn, StaticFilters.FILTER_PERMANENT_CREATURES) + .setText("Until end of turn, creatures you control gain menace")); ability = new DealsCombatDamageToAPlayerTriggeredAbility(new DiscardTargetEffect(1), false, true); getRightHalfCard().getSpellAbility().addEffect(new GainAbilityControlledEffect(ability, Duration.EndOfTurn, StaticFilters.FILTER_CONTROLLED_CREATURES) - .setText("and \"Whenever this creature deals combat damage to a player, that player discards a card.\"")); + .setText("\"Whenever this creature deals combat damage to a player, that player discards a card.\"") + .concatBy("and")); } diff --git a/Mage.Sets/src/mage/cards/h/HatefulEidolon.java b/Mage.Sets/src/mage/cards/h/HatefulEidolon.java index e039ed7c684..01318989a8f 100644 --- a/Mage.Sets/src/mage/cards/h/HatefulEidolon.java +++ b/Mage.Sets/src/mage/cards/h/HatefulEidolon.java @@ -2,23 +2,18 @@ package mage.cards.h; import mage.MageInt; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.keyword.LifelinkAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.permanent.EnchantedPredicate; -import mage.game.Controllable; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; - -import java.util.Objects; import java.util.UUID; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.game.permanent.Permanent; /** * @author TheElk801 @@ -35,7 +30,8 @@ public final class HatefulEidolon extends CardImpl { // Lifelink this.addAbility(LifelinkAbility.getInstance()); - // Whenever an enchanted creature dies, draw a card for each Aura you controlled that was attached to it. + // Whenever an enchanted creature dies, draw a card for each + // Aura you controlled that was attached to it. this.addAbility(new HatefulEidolonTriggeredAbility()); } @@ -51,12 +47,6 @@ public final class HatefulEidolon extends CardImpl { class HatefulEidolonTriggeredAbility extends TriggeredAbilityImpl { - private static final FilterPermanent filter = new FilterCreaturePermanent(); - - static { - filter.add(EnchantedPredicate.instance); - } - HatefulEidolonTriggeredAbility() { super(Zone.BATTLEFIELD, null, false); } @@ -77,21 +67,21 @@ class HatefulEidolonTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { + int auraCount = 0; ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (!zEvent.isDiesEvent() || !filter.match(zEvent.getTarget(), game)) { + if (!zEvent.isDiesEvent()) { return false; } - int auraCount = zEvent - .getTarget() - .getAttachments() - .stream() - .map(game::getPermanentOrLKIBattlefield) - .filter(Objects::nonNull) - .filter(permanent -> permanent.hasSubtype(SubType.AURA, game)) - .map(Controllable::getControllerId) - .filter(this.getControllerId()::equals) - .mapToInt(x -> 1) - .sum(); + Permanent deadCreature = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (deadCreature.getAttachments().isEmpty()) { + return false; + } + for (UUID auraId : deadCreature.getAttachments()) { + Permanent aura = game.getPermanentOrLKIBattlefield(auraId); + if (aura.getControllerId().equals(controllerId)) { + auraCount += 1; + } + } this.getEffects().clear(); this.addEffect(new DrawCardSourceControllerEffect(auraCount)); return true; @@ -99,6 +89,7 @@ class HatefulEidolonTriggeredAbility extends TriggeredAbilityImpl { @Override public String getRule() { - return "Whenever an enchanted creature dies, draw a card for each Aura you controlled that was attached to it."; + return "Whenever an enchanted creature dies, draw a card for each " + + "Aura you controlled that was attached to it."; } } diff --git a/Mage.Sets/src/mage/cards/k/KarnLiberated.java b/Mage.Sets/src/mage/cards/k/KarnLiberated.java index ecc83a1a607..662cc84fa1e 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.Sets/src/mage/cards/m/MagusOfTheMoon.java b/Mage.Sets/src/mage/cards/m/MagusOfTheMoon.java index 08e9130be37..d68dba2bd48 100644 --- a/Mage.Sets/src/mage/cards/m/MagusOfTheMoon.java +++ b/Mage.Sets/src/mage/cards/m/MagusOfTheMoon.java @@ -79,8 +79,7 @@ public final class MagusOfTheMoon extends CardImpl { land.removeAllAbilities(source.getSourceId(), game); land.getSubtype(game).removeAll(SubType.getLandTypes()); land.getSubtype(game).add(SubType.MOUNTAIN); - break; - case AbilityAddingRemovingEffects_6: + // Mountains have the red mana ability intrinsically so the ability must be added in this layer land.addAbility(new RedManaAbility(), source.getSourceId(), game); break; } @@ -90,7 +89,7 @@ public final class MagusOfTheMoon extends CardImpl { @Override public boolean hasLayer(Layer layer) { - return layer == Layer.AbilityAddingRemovingEffects_6 || layer == Layer.TypeChangingEffects_4; + return layer == Layer.TypeChangingEffects_4; } } diff --git a/Mage.Sets/src/mage/cards/s/SpectersShriek.java b/Mage.Sets/src/mage/cards/s/SpectersShriek.java index 0a5a41119e7..bf86c08f404 100644 --- a/Mage.Sets/src/mage/cards/s/SpectersShriek.java +++ b/Mage.Sets/src/mage/cards/s/SpectersShriek.java @@ -12,11 +12,11 @@ import mage.filter.FilterCard; import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; -import mage.target.TargetCard; import mage.target.common.TargetCardInHand; import mage.target.common.TargetOpponent; - import java.util.UUID; +import mage.filter.common.FilterNonlandCard; +import mage.target.TargetCard; /** * @author TheElk801 @@ -26,7 +26,9 @@ public final class SpectersShriek extends CardImpl { public SpectersShriek(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}"); - // Target opponent reveals their hand. You may choose a nonland card from it. If you do, that player exiles that card. If a nonblack card is exiled this way, exile a card from your hand. + // Target opponent reveals their hand. You may choose a nonland card + // from it. If you do, that player exiles that card. If a nonblack + // card is exiled this way, exile a card from your hand. this.getSpellAbility().addEffect(new SpectersShriekEffect()); this.getSpellAbility().addTarget(new TargetOpponent()); } @@ -47,8 +49,8 @@ class SpectersShriekEffect extends OneShotEffect { SpectersShriekEffect() { super(Outcome.Benefit); - staticText = "Target opponent reveals their hand. You may choose a nonland card from it. If you do, " + - "that player exiles that card. If a nonblack card is exiled this way, exile a card from your hand."; + staticText = "Target opponent reveals their hand. You may choose a nonland card from it. If you do, " + + "that player exiles that card. If a nonblack card is exiled this way, exile a card from your hand."; } private SpectersShriekEffect(final SpectersShriekEffect effect) { @@ -64,17 +66,20 @@ class SpectersShriekEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getFirstTarget()); - if (controller == null || player == null) { + if (controller == null + || player == null) { return false; } player.revealCards(source, player.getHand(), game); if (player.getHand().count(StaticFilters.FILTER_CARD_NON_LAND, game) == 0 - || !controller.chooseUse(outcome, "Exile a card from " + player.getName() + "'s hand?", source, game)) { - return true; + || !controller.chooseUse(Outcome.Benefit, "Exile a card from " + + player.getName() + "'s hand?", source, game)) { + return false; } - TargetCard target = new TargetCardInHand(StaticFilters.FILTER_CARD_NON_LAND); + TargetCard target = new TargetCard(Zone.HAND, new FilterNonlandCard()); target.setNotTarget(true); - if (!controller.choose(outcome, player.getHand(), target, game)) { + target.setRequired(false); + if (!controller.chooseTarget(Outcome.Benefit, player.getHand(), target, source, game)) { return false; } Card card = game.getCard(target.getFirstTarget()); @@ -83,12 +88,13 @@ class SpectersShriekEffect extends OneShotEffect { } boolean isBlack = card.getColor(game).isBlack(); player.moveCards(card, Zone.EXILED, source, game); - if (isBlack || controller.getHand().isEmpty()) { + if (isBlack + || controller.getHand().isEmpty()) { return true; } target = new TargetCardInHand(filter); target.setNotTarget(true); - return controller.choose(outcome, controller.getHand(), target, game) + return controller.choose(Outcome.Detriment, controller.getHand(), target, game) && controller.moveCards(game.getCard(target.getFirstTarget()), Zone.EXILED, source, game); } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/w/WhirlwindDenial.java b/Mage.Sets/src/mage/cards/w/WhirlwindDenial.java index 0ac1faf73a5..0a4c1ff20d8 100644 --- a/Mage.Sets/src/mage/cards/w/WhirlwindDenial.java +++ b/Mage.Sets/src/mage/cards/w/WhirlwindDenial.java @@ -10,7 +10,6 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; - import java.util.Objects; import java.util.UUID; @@ -40,8 +39,8 @@ class WhirlwindDenialEffect extends OneShotEffect { WhirlwindDenialEffect() { super(Outcome.Benefit); - staticText = "For each spell and ability your opponents control, " + - "counter it unless its controller pays {4}."; + staticText = "For each spell and ability your opponents control, " + + "counter it unless its controller pays {4}."; } private WhirlwindDenialEffect(final WhirlwindDenialEffect effect) { @@ -59,7 +58,7 @@ class WhirlwindDenialEffect extends OneShotEffect { .stream() .filter(Objects::nonNull) .forEachOrdered(stackObject -> { - if (!game.getOpponents(stackObject.getControllerId()).contains(source.getControllerId())) { + if (!game.getOpponents(source.getControllerId()).contains(stackObject.getControllerId())) { return; } Player player = game.getPlayer(stackObject.getControllerId()); @@ -67,13 +66,14 @@ class WhirlwindDenialEffect extends OneShotEffect { return; } Cost cost = new GenericManaCost(4); - if (cost.canPay(source, source.getSourceId(), source.getControllerId(), game) - && player.chooseUse(outcome, "Pay {4} to prevent " + stackObject.getIdName() + " from being countered?", source, game) - && cost.pay(source, game, source.getSourceId(), source.getControllerId(), false)) { + if (cost.canPay(source, source.getSourceId(), stackObject.getControllerId(), game) + && player.chooseUse(outcome, "Pay {4} to prevent " + + stackObject.getIdName() + " from being countered?", source, game) + && cost.pay(source, game, source.getSourceId(), stackObject.getControllerId(), false)) { return; } stackObject.counter(source.getSourceId(), game); }); return true; } -} \ No newline at end of file +} 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 2c49ea509fa..9abe4909fac 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 4eb9e30dd99..e040b056322 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/abilities/keyword/HauntAbility.java b/Mage/src/main/java/mage/abilities/keyword/HauntAbility.java index 71297bc8930..cc7eb68a479 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HauntAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HauntAbility.java @@ -1,5 +1,3 @@ - - package mage.abilities.keyword; import mage.MageObject; @@ -21,27 +19,29 @@ import mage.target.targetpointer.FixedTarget; /** * 702.53. Haunt * - * 702.53a Haunt is a triggered ability. "Haunt" on a permanent means "When this permanent is put - * into a graveyard from the battlefield, exile it haunting target creature." - * "Haunt" on an instant or sorcery spell means "When this spell is put into a graveyard during - * its resolution, exile it haunting target creature." + * 702.53a Haunt is a triggered ability. "Haunt" on a permanent means "When this + * permanent is put into a graveyard from the battlefield, exile it haunting + * target creature." "Haunt" on an instant or sorcery spell means "When this + * spell is put into a graveyard during its resolution, exile it haunting target + * creature." * - * 702.53b Cards that are in the exile zone as the result of a haunt ability "haunt" the creature - * targeted by that ability. The phrase "creature it haunts" refers to the object targeted by the - * haunt ability, regardless of whether or not that object is still a creature. + * 702.53b Cards that are in the exile zone as the result of a haunt ability + * "haunt" the creature targeted by that ability. The phrase "creature it + * haunts" refers to the object targeted by the haunt ability, regardless of + * whether or not that object is still a creature. * - * 702.53c Triggered abilities of cards with haunt that refer to the haunted creature can trigger in the exile zone. + * 702.53c Triggered abilities of cards with haunt that refer to the haunted + * creature can trigger in the exile zone. * * @author LevelX2 */ - public class HauntAbility extends TriggeredAbilityImpl { private boolean usedFromExile = false; - private boolean creatureHaunt; - + private boolean creatureHaunt; + public HauntAbility(Card card, Effect effect) { - super(Zone.ALL, effect , false); + super(Zone.ALL, effect, false); creatureHaunt = card.isCreature(); addSubAbility(new HauntExileAbility(creatureHaunt)); } @@ -66,7 +66,7 @@ public class HauntAbility extends TriggeredAbilityImpl { } return false; } - + @Override public boolean checkTrigger(GameEvent event, Game game) { switch (event.getType()) { @@ -76,16 +76,24 @@ public class HauntAbility extends TriggeredAbilityImpl { } break; case ZONE_CHANGE: - if (!usedFromExile && game.getState().getZone(getSourceId()) == Zone.EXILED) { + if (!usedFromExile + && game.getState().getZone(getSourceId()) == Zone.EXILED) { ZoneChangeEvent zEvent = (ZoneChangeEvent) event; if (zEvent.isDiesEvent()) { Card card = game.getCard(getSourceId()); - if (card != null) { - String key = new StringBuilder("Haunting_").append(getSourceId().toString()).append('_').append(card.getZoneChangeCounter(game)).toString(); + if (card != null + && game.getCard(event.getTargetId()) != null) { + String key = new StringBuilder("Haunting_") + .append(getSourceId().toString()) + .append(card.getZoneChangeCounter(game) + + (game.getPermanentOrLKIBattlefield(event.getTargetId())) + .getZoneChangeCounter(game)).toString(); Object object = game.getState().getValue(key); if (object instanceof FixedTarget) { FixedTarget target = (FixedTarget) object; - if (target.getTarget() != null && target.getTarget().equals(event.getTargetId())) { + if (target.getTarget() != null + && target.getTarget() + .equals(event.getTargetId())) { usedFromExile = true; return true; } @@ -102,23 +110,24 @@ public class HauntAbility extends TriggeredAbilityImpl { @Override public String getRule() { - return (creatureHaunt ? "When {this} enters the battlefield or the creature it haunts dies, " : - "When the creature {this} haunts dies, ") + return (creatureHaunt ? "When {this} enters the battlefield or the creature it haunts dies, " + : "When the creature {this} haunts dies, ") + super.getRule(); } } class HauntExileAbility extends ZoneChangeTriggeredAbility { - private static final String RULE_TEXT_CREATURE = "Haunt (When this creature dies, exile it haunting target creature.)"; - private static final String RULE_TEXT_SPELL = "Haunt (When this spell card is put into a graveyard after resolving, exile it haunting target creature.)"; - + private static final String RULE_TEXT_CREATURE = "Haunt (When this creature dies, " + + "exile it haunting target creature.)"; + private static final String RULE_TEXT_SPELL = "Haunt (When this spell card is put " + + "into a graveyard after resolving, exile it haunting target creature.)"; + private boolean creatureHaunt; - + // TODO: It's not checked yet, if the Haunt spell has resolved (and was not countered or removed from stack). - public HauntExileAbility(boolean creatureHaunt) { - super(creatureHaunt ? Zone.BATTLEFIELD: Zone.STACK, Zone.GRAVEYARD, new HauntEffect(), null, false); + super(creatureHaunt ? Zone.BATTLEFIELD : Zone.STACK, Zone.GRAVEYARD, new HauntEffect(), null, false); this.creatureHaunt = creatureHaunt; this.setRuleAtTheTop(creatureHaunt); this.addTarget(new TargetCreaturePermanent()); @@ -131,19 +140,22 @@ class HauntExileAbility extends ZoneChangeTriggeredAbility { } @Override - public boolean isInUseableZone(Game game, MageObject source, GameEvent event) { + public boolean isInUseableZone(Game game, MageObject source, GameEvent event) { boolean fromOK = true; Permanent sourcePermanent = (Permanent) game.getLastKnownInformation(sourceId, Zone.BATTLEFIELD); - if (creatureHaunt && sourcePermanent == null) { + if (creatureHaunt + && sourcePermanent == null) { // check it was previously on battlefield fromOK = false; - } + } if (!this.hasSourceObjectAbility(game, sourcePermanent, event)) { return false; - } + } // check now it is in graveyard Zone after = game.getState().getZone(sourceId); - return fromOK && after != null && Zone.GRAVEYARD.match(after); + return fromOK + && after != null + && Zone.GRAVEYARD.match(after); } @Override @@ -182,7 +194,10 @@ class HauntEffect extends OneShotEffect { if (hauntedCreature != null) { if (card.moveToExile(source.getSourceId(), "Haunting", source.getSourceId(), game)) { // remember the haunted creature - String key = new StringBuilder("Haunting_").append(source.getSourceId().toString()).append('_').append(card.getZoneChangeCounter(game)).toString(); + String key = new StringBuilder("Haunting_") + .append(source.getSourceId().toString()) + .append(card.getZoneChangeCounter(game) + + hauntedCreature.getZoneChangeCounter(game)).toString(); // in case it is blinked game.getState().setValue(key, new FixedTarget(targetPointer.getFirst(game, source))); card.addInfo("hauntinfo", new StringBuilder("Haunting ").append(hauntedCreature.getLogName()).toString(), game); hauntedCreature.addInfo("hauntinfo", new StringBuilder("Haunted by ").append(card.getLogName()).toString(), game); diff --git a/Mage/src/main/java/mage/game/GameCommanderImpl.java b/Mage/src/main/java/mage/game/GameCommanderImpl.java index fe9a4aa5d87..6731f4f39f3 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 9547ff10525..f2794367cc2 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); }