diff --git a/Mage.Sets/src/mage/cards/b/BrokenAmbitions.java b/Mage.Sets/src/mage/cards/b/BrokenAmbitions.java index ad7c0ef751e..d503e6e8530 100644 --- a/Mage.Sets/src/mage/cards/b/BrokenAmbitions.java +++ b/Mage.Sets/src/mage/cards/b/BrokenAmbitions.java @@ -66,7 +66,7 @@ class BrokenAmbitionsEffect extends OneShotEffect { if (player == null) { return false; } - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { player.millCards(4, source, game); } return true; diff --git a/Mage.Sets/src/mage/cards/c/CaptivatingGlance.java b/Mage.Sets/src/mage/cards/c/CaptivatingGlance.java index c447f311960..e9bd8abface 100644 --- a/Mage.Sets/src/mage/cards/c/CaptivatingGlance.java +++ b/Mage.Sets/src/mage/cards/c/CaptivatingGlance.java @@ -80,7 +80,7 @@ class CaptivatingGlanceEffect extends OneShotEffect { if (controller != null && captivatingGlance != null) { Permanent enchantedCreature = game.getPermanent(captivatingGlance.getAttachedTo()); - clashResult = ClashEffect.getInstance().apply(game, source); + clashResult = new ClashEffect().apply(game, source); if (enchantedCreature != null) { if (clashResult) { ContinuousEffect effect = new GainControlTargetEffect(Duration.Custom, controller.getId()); diff --git a/Mage.Sets/src/mage/cards/f/FistfulOfForce.java b/Mage.Sets/src/mage/cards/f/FistfulOfForce.java index 9cb872cedb9..1c7da9593e9 100644 --- a/Mage.Sets/src/mage/cards/f/FistfulOfForce.java +++ b/Mage.Sets/src/mage/cards/f/FistfulOfForce.java @@ -69,7 +69,7 @@ class FistfulOfForceEffect extends OneShotEffect { ContinuousEffect effect = new BoostTargetEffect(2,2,Duration.EndOfTurn); effect.setTargetPointer(new FixedTarget(creature.getId(), game)); game.addEffect(effect, source); - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { game.addEffect(effect.copy(), source); effect = new GainAbilityTargetEffect(TrampleAbility.getInstance(), Duration.EndOfTurn); effect.setTargetPointer(new FixedTarget(creature.getId(), game)); diff --git a/Mage.Sets/src/mage/cards/g/GiltLeafAmbush.java b/Mage.Sets/src/mage/cards/g/GiltLeafAmbush.java index a8dfa2fd140..7ea31dc7c05 100644 --- a/Mage.Sets/src/mage/cards/g/GiltLeafAmbush.java +++ b/Mage.Sets/src/mage/cards/g/GiltLeafAmbush.java @@ -66,7 +66,7 @@ class GiltLeafAmbushCreateTokenEffect extends OneShotEffect { if (controller != null) { CreateTokenEffect effect = new CreateTokenEffect(new ElfWarriorToken(), 2); effect.apply(game, source); - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { for (UUID tokenId : effect.getLastAddedTokenIds()) { Permanent token = game.getPermanent(tokenId); if (token != null) { diff --git a/Mage.Sets/src/mage/cards/h/HoardersGreed.java b/Mage.Sets/src/mage/cards/h/HoardersGreed.java index 16433d20d1c..c4473f3b74c 100644 --- a/Mage.Sets/src/mage/cards/h/HoardersGreed.java +++ b/Mage.Sets/src/mage/cards/h/HoardersGreed.java @@ -59,7 +59,7 @@ class HoardersGreedEffect extends OneShotEffect { do { controller.loseLife(2, game, source, false); controller.drawCards(2, source, game); - } while (controller.canRespond() && ClashEffect.getInstance().apply(game, source)); + } while (controller.canRespond() && new ClashEffect().apply(game, source)); return true; } return false; diff --git a/Mage.Sets/src/mage/cards/l/LashOut.java b/Mage.Sets/src/mage/cards/l/LashOut.java index 1c1f9ec0185..40cd5b1dda8 100644 --- a/Mage.Sets/src/mage/cards/l/LashOut.java +++ b/Mage.Sets/src/mage/cards/l/LashOut.java @@ -60,7 +60,7 @@ class LashOutEffect extends OneShotEffect { Permanent creature = game.getPermanent(getTargetPointer().getFirst(game, source)); if (controller != null && creature != null) { creature.damage(3, source.getSourceId(), source, game, false, true); - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { Player creaturesController = game.getPlayer(creature.getControllerId()); if (creaturesController != null) { creaturesController.damage(3, source.getSourceId(), source, game); diff --git a/Mage.Sets/src/mage/cards/p/PullingTeeth.java b/Mage.Sets/src/mage/cards/p/PullingTeeth.java index e0248713045..0d997f588c0 100644 --- a/Mage.Sets/src/mage/cards/p/PullingTeeth.java +++ b/Mage.Sets/src/mage/cards/p/PullingTeeth.java @@ -59,7 +59,7 @@ class PullingTeethEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { int cardsToDiscard; - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { cardsToDiscard = 2; } else { cardsToDiscard = 1; diff --git a/Mage.Sets/src/mage/cards/s/ScatteringStroke.java b/Mage.Sets/src/mage/cards/s/ScatteringStroke.java index 2c3bb9f6eb8..0897fa2ea82 100644 --- a/Mage.Sets/src/mage/cards/s/ScatteringStroke.java +++ b/Mage.Sets/src/mage/cards/s/ScatteringStroke.java @@ -65,7 +65,7 @@ class ScatteringStrokeEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null && spell != null) { game.getStack().counter(spell.getId(), source, game); - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { Effect effect = new AddManaToManaPoolSourceControllerEffect(new Mana(0, 0, 0, 0, 0, 0, 0, spell.getManaValue())); AtTheBeginOfMainPhaseDelayedTriggeredAbility delayedAbility = new AtTheBeginOfMainPhaseDelayedTriggeredAbility(effect, true, TargetController.YOU, AtTheBeginOfMainPhaseDelayedTriggeredAbility.PhaseSelection.NEXT_MAIN); diff --git a/Mage.Sets/src/mage/cards/w/WeedStrangle.java b/Mage.Sets/src/mage/cards/w/WeedStrangle.java index d319fa64ab5..3d2e7c43ce8 100644 --- a/Mage.Sets/src/mage/cards/w/WeedStrangle.java +++ b/Mage.Sets/src/mage/cards/w/WeedStrangle.java @@ -62,7 +62,7 @@ class WeedStrangleEffect extends OneShotEffect { Permanent creature = game.getPermanent(getTargetPointer().getFirst(game, source)); if (controller != null && creature != null) { creature.destroy(source, game, false); - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { controller.gainLife(creature.getToughness().getValue(), game, source); } return true; diff --git a/Mage.Sets/src/mage/cards/w/WhirlpoolWhelm.java b/Mage.Sets/src/mage/cards/w/WhirlpoolWhelm.java index 705671224e7..e2afa56aed5 100644 --- a/Mage.Sets/src/mage/cards/w/WhirlpoolWhelm.java +++ b/Mage.Sets/src/mage/cards/w/WhirlpoolWhelm.java @@ -61,7 +61,7 @@ class WhirlpoolWhelmEffect extends OneShotEffect { Permanent creature = game.getPermanent(getTargetPointer().getFirst(game, source)); if (controller != null) { boolean topOfLibrary = false; - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { topOfLibrary = controller.chooseUse(outcome, "Put " + creature.getLogName() + " to top of libraray instead?", source, game); } if (topOfLibrary) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ClashEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ClashEffectTest.java new file mode 100644 index 00000000000..05be86ce921 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ClashEffectTest.java @@ -0,0 +1,135 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class ClashEffectTest extends CardTestPlayerBase { + + /** + * 701.23. Clash + * 701.23a. To clash, a player reveals the top card of their library. + * That player may then put that card on the bottom of their library. + * 701.23b. "Clash with an opponent" means "Choose an opponent. You and that opponent each clash." + * 701.23c. Each clashing player reveals the top card of their library at the same time. + * Then those players decide in APNAP order (see rule 101.4) where to put those cards, then those cards move at the same time. + * 701.23d. A player wins a clash if that player revealed a card with a higher mana value than all other cards revealed in that clash. + */ + + private static final String rascal = "Paperfin Rascal"; // 2/2 for 2U, ETB clash, +1/+1 counter if won (so 3/3) + + private void prepareClashing() { + + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, rascal); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerB, "Sylvan Echoes" , 1); // Whenever you clash and win, you may draw a card. + + // Default: both players have only lands with mana value 0 + // Add card with greater mana value to one player's library so that they win the clash + removeAllCardsFromLibrary(playerA); + removeAllCardsFromLibrary(playerB); + addCard(Zone.LIBRARY, playerA, "Island", 4); + addCard(Zone.LIBRARY, playerB, "Island", 4); + skipInitShuffling(); + + } + + private void performClashing() { + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rascal); + setChoice(playerA, "PlayerB"); // choose an opponent to clash with + setChoice(playerA, false); // put revealed card to bottom of library + setChoice(playerB, true); // put revealed card to top of library + + } + + @Test + public void testClashYouWin() { + prepareClashing(); + addCard(Zone.LIBRARY, playerA, "Divination", 1); + performClashing(); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPowerToughness(playerA, rascal, 3, 3); + } + + @Test + public void testClashOppWin() { + prepareClashing(); + addCard(Zone.LIBRARY, playerB, "Divination", 1); + performClashing(); + setChoice(playerB, true); // Draw a card from Sylvan Echoes + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPowerToughness(playerA, rascal, 2, 2); + assertHandCount(playerB, "Divination", 1); + } + + @Test + public void testClashNoWin() { + prepareClashing(); + performClashing(); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPowerToughness(playerA, rascal, 2, 2); + } + + @Test + public void testHoardersGreed() { + + setStrictChooseMode(true); + + removeAllCardsFromLibrary(playerA); + removeAllCardsFromLibrary(playerB); + skipInitShuffling(); + addCard(Zone.LIBRARY, playerB, "Island", 5); + addCard(Zone.LIBRARY, playerA, "Island", 5); + addCard(Zone.LIBRARY, playerA, "Wastes", 1); + addCard(Zone.LIBRARY, playerA, "Razorfield Thresher", 1); + addCard(Zone.LIBRARY, playerA, "Phyrexian Hulk", 1); + addCard(Zone.LIBRARY, playerA, "Stone Golem", 1); + addCard(Zone.LIBRARY, playerA, "Gilded Sentinel", 1); + addCard(Zone.LIBRARY, playerA, "Stonework Puma", 1); + addCard(Zone.LIBRARY, playerA, "Field Creeper", 1); + addCard(Zone.LIBRARY, playerA, "Metallic Sliver", 1); + addCard(Zone.LIBRARY, playerA, "Memnite", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.HAND, playerA, "Hoarder's Greed", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hoarder's Greed"); + // Draw two cards (Memnite and Metallic Sliver), lose 2 life, clash: + setChoice(playerA, "PlayerB"); + setChoice(playerA, false); // Field Creeper to bottom of library + setChoice(playerB, true); + // Clash won. Draw two cards (Stonework Puma and Gilded Sentinel), lose 2 life, clash: + setChoice(playerA, "PlayerB"); + setChoice(playerA, false); // Stone Golem to bottom of library + setChoice(playerB, true); + // Clash won. Draw two cards (Phyrexian Hulk and Razorfield Thresher), lose 2 life, clash: + setChoice(playerA, "PlayerB"); + setChoice(playerA, true); // Wastes to top of library + setChoice(playerB, true); + // No winner in this clash. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - (2 * 3)); + assertHandCount(playerA, "Memnite", 1); + assertHandCount(playerA, "Metallic Sliver", 1); + assertHandCount(playerA, "Field Creeper", 0); + assertHandCount(playerA, "Stonework Puma", 1); + assertHandCount(playerA, "Gilded Sentinel", 1); + assertHandCount(playerA, "Stone Golem", 0); + assertHandCount(playerA, "Phyrexian Hulk", 1); + assertHandCount(playerA, "Razorfield Thresher", 1); + + } + +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ClashEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ClashEffect.java index afd3b9ccc77..da9556cda7d 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ClashEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ClashEffect.java @@ -2,8 +2,6 @@ package mage.abilities.effects.common; import mage.MageObject; import mage.abilities.Ability; -import mage.abilities.MageSingleton; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.Cards; @@ -13,11 +11,10 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.players.Player; -import mage.players.PlayerList; import mage.target.Target; import mage.target.common.TargetOpponent; -import java.io.ObjectStreamException; +import java.util.UUID; /** * 1. The controller of the spell or ability chooses an opponent. (This doesn't @@ -54,24 +51,14 @@ import java.io.ObjectStreamException; * * @author LevelX2 */ -public class ClashEffect extends OneShotEffect implements MageSingleton { +public class ClashEffect extends OneShotEffect { - private static final ClashEffect instance = new ClashEffect(); - - private Object readResolve() throws ObjectStreamException { - return instance; - } - - private ClashEffect() { + public ClashEffect() { super(Outcome.Benefit); this.staticText = "Clash with an opponent"; } - public static ClashEffect getInstance() { - return instance; - } - - public ClashEffect(final ClashEffect effect) { + protected ClashEffect(final ClashEffect effect) { super(effect); } @@ -94,7 +81,6 @@ public class ClashEffect extends OneShotEffect implements MageSingleton { // choose opponent Target target = new TargetOpponent(true); target.setTargetName("an opponent to clash with"); - target.setNotTarget(true); if (!controller.choose(Outcome.Benefit, target, source, game)) { return false; } @@ -133,32 +119,26 @@ public class ClashEffect extends OneShotEffect implements MageSingleton { message.append(" no card"); } message.append(" - "); - if (!game.isSimulation()) { - if (cmcController > cmcOpponent) { - message.append(controller.getLogName()).append(" won the clash"); - game.informPlayer(controller, "You won the clash!"); - } else if (cmcController < cmcOpponent) { - message.append(opponent.getLogName()).append(" won the clash"); - game.informPlayer(controller, opponent.getName() + " won the clash!"); - } else { - message.append(" no winner "); - } - game.informPlayers(message.toString()); + if (cmcController > cmcOpponent) { + message.append(controller.getLogName()).append(" won the clash"); + } else if (cmcController < cmcOpponent) { + message.append(opponent.getLogName()).append(" won the clash"); + } else { + message.append(" no winner "); } - // decide to put the cards on top or on the buttom of library in turn order beginning with the active player in turn order - PlayerList playerList = game.getPlayerList().copy(); - playerList.setCurrent(game.getActivePlayerId()); - Player nextPlayer; - do { - Player current = playerList.getCurrent(game); - if (cardController != null && current.getId().equals(controller.getId())) { - topController = current.chooseUse(Outcome.Detriment, "Put " + cardController.getLogName() + " back on top of your library? (otherwise it goes to bottom)", source, game); + game.informPlayers(message.toString()); + // decide to put the cards on top or on the bottom of library in turn order beginning with the active player in turn order + for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; } - if (cardOpponent != null && current.getId().equals(opponent.getId())) { - topOpponent = current.chooseUse(Outcome.Detriment, "Put " + cardOpponent.getLogName() + " back on top of your library? (otherwise it goes to bottom)", source, game); + if (cardController != null && player.getId().equals(controller.getId())) { + topController = player.chooseUse(Outcome.Detriment, "Put " + cardController.getLogName() + " back on top of your library? (otherwise it goes to bottom)", source, game); + } else if (cardOpponent != null && player.getId().equals(opponent.getId())) { + topOpponent = player.chooseUse(Outcome.Detriment, "Put " + cardOpponent.getLogName() + " back on top of your library? (otherwise it goes to bottom)", source, game); } - nextPlayer = playerList.getNext(game, false); - } while (nextPlayer != null && !nextPlayer.getId().equals(game.getActivePlayerId())); + } // put the cards back to library if (cardController != null) { controller.moveCardToLibraryWithInfo(cardController, source, game, Zone.LIBRARY, topController, true); @@ -166,14 +146,14 @@ public class ClashEffect extends OneShotEffect implements MageSingleton { if (cardOpponent != null) { opponent.moveCardToLibraryWithInfo(cardOpponent, source, game, Zone.LIBRARY, topOpponent, true); } - // fire CLASHED event with info about who won - game.fireEvent(new GameEvent( - GameEvent.EventType.CLASHED, controller.getId(), source, - opponent.getId(), 0, cmcController > cmcOpponent - )); + // fire CLASHED events with info about winner (flag is true if playerId won; other player is targetId) game.fireEvent(new GameEvent( GameEvent.EventType.CLASHED, opponent.getId(), source, - controller.getId(), 0, cmcOpponent > cmcController + controller.getId(), 0, cmcController > cmcOpponent + )); + game.fireEvent(new GameEvent( + GameEvent.EventType.CLASHED, controller.getId(), source, + opponent.getId(), 0, cmcOpponent > cmcController )); // set opponent to DoIfClashWonEffect diff --git a/Mage/src/main/java/mage/abilities/effects/common/DoIfClashWonEffect.java b/Mage/src/main/java/mage/abilities/effects/common/DoIfClashWonEffect.java index 4d2aa61f08b..aad0175d8df 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/DoIfClashWonEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/DoIfClashWonEffect.java @@ -54,7 +54,7 @@ public class DoIfClashWonEffect extends OneShotEffect { } if (chooseUseText == null || player.chooseUse(executingEffect.getOutcome(), message, source, game)) { - if (ClashEffect.getInstance().apply(game, source)) { + if (new ClashEffect().apply(game, source)) { if (setTargetPointerToClashedOpponent) { Object opponent = getValue("clashOpponent"); if (opponent instanceof Player) { diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index d1bb13d8a44..9da63ad5552 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -99,6 +99,11 @@ public class GameEvent implements Serializable { DISCARDED_CARD, DISCARDED_CARDS, CYCLE_CARD, CYCLED_CARD, CYCLE_DRAW, + /* CLASHED (one event fired for each player involved) + playerId the id of the clashing player + flag true = playerId won the clash + targetId the id of the other player in the clash + */ CLASH, CLASHED, DAMAGE_PLAYER, MILL_CARDS, @@ -107,9 +112,9 @@ public class GameEvent implements Serializable { /* DAMAGED_PLAYER targetId the id of the damaged player sourceId sourceId of the ability which caused the damage - playerId the id of the damged player + playerId the id of the damaged player amount amount of damage - flag true = comabat damage - other damage = false + flag true = combat damage - other damage = false */ DAMAGED_PLAYER, @@ -121,7 +126,7 @@ public class GameEvent implements Serializable { /* DAMAGE_CAUSES_LIFE_LOSS, targetId the id of the damaged player sourceId sourceId of the ability which caused the damage, can be null for default events like combat - playerId the id of the damged player + playerId the id of the damaged player amount amount of damage flag is it combat damage */ @@ -134,7 +139,7 @@ public class GameEvent implements Serializable { sourceId sourceId of the ability which caused the lose playerId the id of the player loosing life amount amount of life loss - flag true = from comabat damage - other from non combat damage + flag true = from combat damage - other from non combat damage */ PLAY_LAND, LAND_PLAYED, CREATURE_CHAMPIONED, @@ -333,7 +338,7 @@ public class GameEvent implements Serializable { TAP, /* TAPPED, targetId tapped permanent - sourceId id of the abilitity's source (can be null for standard tap actions like combat) + sourceId id of the ability's source (can be null for standard tap actions like combat) playerId controller of the tapped permanent amount not used for this event flag is it tapped for combat @@ -441,7 +446,7 @@ public class GameEvent implements Serializable { /* LOST_CONTROL targetId id of the creature that lost control sourceId null - playerId player that controlles the creature before + playerId player that controls the creature before amount not used for this event flag not used for this event */ diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index d8005c14ede..af86d2f0de4 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -567,11 +567,11 @@ public interface Player extends MageItem, Copyable { void revealCards(Ability source, Cards cards, Game game); - void revealCards(String titelSuffix, Cards cards, Game game); + void revealCards(String titleSuffix, Cards cards, Game game); - void revealCards(Ability source, String titelSuffix, Cards cards, Game game); + void revealCards(Ability source, String titleSuffix, Cards cards, Game game); - void revealCards(String titelSuffix, Cards cards, Game game, boolean postToLog); + void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog); /** * Adds the cards to the reveal window and adds the source object's id name