diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java index f50503d8d2ac..8619ef2aeb80 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java @@ -309,7 +309,7 @@ protected Game createMCTSGame(Game game) { newPlayer.getHand().clear(); newPlayer.getLibrary().shuffle(); for (int i = 0; i < handSize; i++) { - Card card = newPlayer.getLibrary().removeFromTop(mcts); + Card card = newPlayer.getLibrary().drawFromTop(mcts); card.setZone(Zone.HAND, mcts); newPlayer.getHand().add(card); } diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java index bfb5669af1e0..a42e6102068d 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java @@ -11,7 +11,6 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import mage.abilities.Ability; -import mage.abilities.ActivatedAbility; import mage.abilities.PlayLandAbility; import mage.abilities.common.PassAbility; import mage.cards.Card; @@ -270,7 +269,7 @@ protected void randomizePlayers(Game game, UUID playerId) { player.getHand().clear(); player.getLibrary().shuffle(); for (int i = 0; i < handSize; i++) { - Card card = player.getLibrary().removeFromTop(game); + Card card = player.getLibrary().drawFromTop(game); card.setZone(Zone.HAND, game); player.getHand().add(card); } diff --git a/Mage.Sets/src/mage/cards/a/AlmsCollector.java b/Mage.Sets/src/mage/cards/a/AlmsCollector.java index 23be86e935c3..f1a5e2c95e7c 100644 --- a/Mage.Sets/src/mage/cards/a/AlmsCollector.java +++ b/Mage.Sets/src/mage/cards/a/AlmsCollector.java @@ -78,7 +78,7 @@ public boolean replaceEvent(GameEvent event, Ability source, Game game) { @Override public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DRAW_CARDS; + return event.getType() == GameEvent.EventType.DRAW_TWO_OR_MORE_CARDS; } @Override diff --git a/Mage.Sets/src/mage/cards/b/BloodScrivener.java b/Mage.Sets/src/mage/cards/b/BloodScrivener.java index ae1df072cb87..d90ffecad168 100644 --- a/Mage.Sets/src/mage/cards/b/BloodScrivener.java +++ b/Mage.Sets/src/mage/cards/b/BloodScrivener.java @@ -80,11 +80,7 @@ public boolean checksEventType(GameEvent event, Game game) { public boolean applies(GameEvent event, Ability source, Game game) { if (event.getPlayerId().equals(source.getControllerId())) { Player player = game.getPlayer(event.getPlayerId()); - if(player != null) { - if (player.getHand().isEmpty()) { - return true; - } - } + return player != null && player.getHand().isEmpty(); } return false; } diff --git a/Mage.Sets/src/mage/cards/c/CellarDoor.java b/Mage.Sets/src/mage/cards/c/CellarDoor.java index 36da9f634146..26f7c3683e3a 100644 --- a/Mage.Sets/src/mage/cards/c/CellarDoor.java +++ b/Mage.Sets/src/mage/cards/c/CellarDoor.java @@ -64,7 +64,7 @@ public CellarDoorEffect copy() { public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getFirstTarget()); if (player != null && player.getLibrary().hasCards()) { - Card card = player.getLibrary().removeFromBottom(game); + Card card = player.getLibrary().getFromBottom(game); if (card != null) { player.moveCards(card, Zone.GRAVEYARD, source, game); if (card.isCreature(game)) { diff --git a/Mage.Sets/src/mage/cards/l/LidlessGaze.java b/Mage.Sets/src/mage/cards/l/LidlessGaze.java index ce0f9979d247..2f12678b403b 100644 --- a/Mage.Sets/src/mage/cards/l/LidlessGaze.java +++ b/Mage.Sets/src/mage/cards/l/LidlessGaze.java @@ -89,4 +89,4 @@ public boolean apply(Game game, Ability source) { return true; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/r/RiverSong.java b/Mage.Sets/src/mage/cards/r/RiverSong.java new file mode 100644 index 000000000000..6829d15dd416 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiverSong.java @@ -0,0 +1,134 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.SourcePermanentPowerCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.DrawCardEvent; +import mage.game.events.GameEvent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author xenohedron + */ +public final class RiverSong extends CardImpl { + + public RiverSong(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.TIME_LORD); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Meet in Reverse -- You draw cards from the bottom of your library rather than the top. + this.addAbility(new SimpleStaticAbility(new RiverSongDrawFromBottomReplacementEffect()) + .withFlavorWord("Meet in Reverse")); + + // Spoilers -- Whenever an opponent scries, surveils, or searches their library, put a +1/+1 counter on River Song. + // Then River Song deals damage to that player equal to its power. + TriggeredAbility trigger = new RiverSongTriggeredAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance())); + trigger.addEffect(new DamageTargetEffect(new SourcePermanentPowerCount(false)) + .setText("Then {this} deals damage to that player equal to its power")); + this.addAbility(trigger.withFlavorWord("Spoilers")); + } + + private RiverSong(final RiverSong card) { + super(card); + } + + @Override + public RiverSong copy() { + return new RiverSong(this); + } +} + +class RiverSongDrawFromBottomReplacementEffect extends ReplacementEffectImpl { + + RiverSongDrawFromBottomReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Neutral); + staticText = "You draw cards from the bottom of your library rather than the top"; + } + + private RiverSongDrawFromBottomReplacementEffect(final RiverSongDrawFromBottomReplacementEffect effect) { + super(effect); + } + + @Override + public RiverSongDrawFromBottomReplacementEffect copy() { + return new RiverSongDrawFromBottomReplacementEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + ((DrawCardEvent) event).setFromBottom(true); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DRAW_CARD; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return source.getControllerId().equals(event.getPlayerId()); + } +} + +class RiverSongTriggeredAbility extends TriggeredAbilityImpl { + + RiverSongTriggeredAbility(Effect effect) { + super(Zone.BATTLEFIELD, effect); + setTriggerPhrase("Whenever an opponent scries, surveils, or searches their library, "); + } + + private RiverSongTriggeredAbility(final RiverSongTriggeredAbility ability) { + super(ability); + } + + @Override + public RiverSongTriggeredAbility copy() { + return new RiverSongTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + switch (event.getType()) { + case SCRIED: + case SURVEILED: + case LIBRARY_SEARCHED: + return true; + default: + return false; + } + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Player controller = game.getPlayer(getControllerId()); + if (controller != null + && controller.hasOpponent(event.getPlayerId(), game) + && event.getPlayerId().equals(event.getTargetId())) { // searches own library + getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index 4e98ac7a5d06..79650578e0ad 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -178,6 +178,7 @@ private DoctorWho() { cards.add(new SetCardInfo("Return the Past", 92, Rarity.RARE, mage.cards.r.ReturnThePast.class)); cards.add(new SetCardInfo("Return to Dust", 211, Rarity.UNCOMMON, mage.cards.r.ReturnToDust.class)); cards.add(new SetCardInfo("Reverse the Polarity", 54, Rarity.RARE, mage.cards.r.ReverseThePolarity.class)); + cards.add(new SetCardInfo("River Song", 152, Rarity.RARE, mage.cards.r.RiverSong.class)); cards.add(new SetCardInfo("River of Tears", 297, Rarity.RARE, mage.cards.r.RiverOfTears.class)); cards.add(new SetCardInfo("Rockfall Vale", 298, Rarity.RARE, mage.cards.r.RockfallVale.class)); cards.add(new SetCardInfo("Rogue's Passage", 299, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DrawEffectsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DrawEffectsTest.java index 0f1966e36b6e..7df5061fd1d6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DrawEffectsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DrawEffectsTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.replacement; import mage.constants.PhaseStep; @@ -8,11 +7,343 @@ import org.mage.test.serverside.base.CardTestPlayerBase; /** - * - * @author LevelX2 + * @author LevelX2, xenohedron */ public class DrawEffectsTest extends CardTestPlayerBase { + private static final String drawOne = "Radical Idea"; // 1U instant + private static final String drawTwo = "Quick Study"; // 2U instant + private static final String drawThree = "Jace's Ingenuity"; // 3UU instant + private static final String excavation = "Ancient Excavation"; // 2UB instant + // Draw cards equal to the number of cards in your hand, then discard a card for each card drawn this way. + + private static final String reflection = "Thought Reflection"; + // If you would draw a card, draw two cards instead. + private static final String scrivener = "Blood Scrivener"; + // If you would draw a card while you have no cards in hand, instead you draw two cards and you lose 1 life. + private static final String notionThief = "Notion Thief"; + // If an opponent would draw a card except the first one they draw in each of their draw steps, + // instead that player skips that draw and you draw a card. + private static final String asmodeus = "Asmodeus the Archfiend"; + // If you would draw a card, exile the top card of your library face down instead. + private static final String almsCollector = "Alms Collector"; + // If an opponent would draw two or more cards, instead you and that player each draw a card. + + private void testBase(String cardDraw, int handPlayerA, int handPlayerB) { + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.BATTLEFIELD, playerB, "Island", 5); + addCard(Zone.HAND, playerA, cardDraw); + addCard(Zone.HAND, playerB, cardDraw); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, cardDraw); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, cardDraw); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, cardDraw, 1); + assertGraveyardCount(playerB, cardDraw, 1); + assertHandCount(playerA, handPlayerA); + assertHandCount(playerB, handPlayerB); + } + + private void testSingle(String cardDraw, int handPlayerA, int handPlayerB, String... choices) { + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.HAND, playerA, cardDraw); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, cardDraw); + for (String choice : choices) { + setChoice(playerA, choice); + } + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, cardDraw, 1); + assertHandCount(playerA, handPlayerA); + assertHandCount(playerB, handPlayerB); + } + + @Test + public void testReflection1() { + addCard(Zone.BATTLEFIELD, playerA, reflection); + testBase(drawOne, 2, 1); + } + + @Test + public void testReflection2() { + addCard(Zone.BATTLEFIELD, playerA, reflection); + testBase(drawTwo, 4, 2); + } + + @Test + public void testReflection3() { + addCard(Zone.BATTLEFIELD, playerA, reflection); + testBase(drawThree, 6, 3); + } + + @Test + public void testScrivener1() { + addCard(Zone.BATTLEFIELD, playerA, scrivener); + testBase(drawOne, 2, 1); + assertLife(playerA, 19); + } + + @Test + public void testScrivener2() { + addCard(Zone.BATTLEFIELD, playerA, scrivener); + testBase(drawTwo, 3, 2); + assertLife(playerA, 19); + } + + @Test + public void testScrivener3() { + addCard(Zone.BATTLEFIELD, playerA, scrivener); + testBase(drawThree, 4, 3); + assertLife(playerA, 19); + } + + /* + * Each additional Blood Scrivener you control will effectively add one card and 1 life lost. + * Say you control two Blood Scriveners and would draw a card while you have no cards in hand. + * The effect of one Blood Scrivener will replace the event “draw a card” with “draw two cards and lose 1 life.” + * The effect of the other Blood Scrivener will replace the drawing of the first of those two cards with + * “draw two cards and lose 1 life.” You’ll draw two cards and lose 1 life, + * then draw another card and lose another 1 life. (2013-04-15) + */ + + @Test + public void testDoubleScrivener1() { + addCard(Zone.BATTLEFIELD, playerA, scrivener, 2); + testSingle(drawOne, 3, 0, scrivener); + assertLife(playerA, 18); + } + + @Test + public void testDoubleScrivener2() { + addCard(Zone.BATTLEFIELD, playerA, scrivener, 2); + testSingle(drawTwo, 4, 0, scrivener); + assertLife(playerA, 18); + } + + @Test + public void testDoubleScrivener3() { + addCard(Zone.BATTLEFIELD, playerA, scrivener, 2); + testSingle(drawThree, 5, 0, scrivener); + assertLife(playerA, 18); + } + + @Test + public void testAsmodeus1() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + testBase(drawOne, 0, 1); + assertExileCount(playerA, 1); + } + + @Test + public void testAsmodeus2() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + testBase(drawTwo, 0, 2); + assertExileCount(playerA, 2); + } + + @Test + public void testAsmodeus3() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + testBase(drawThree, 0, 3); + assertExileCount(playerA, 3); + } + + @Test + public void testReflectionAsmodeus1() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawOne, 0, 0, reflection); + assertExileCount(playerA, 2); + } + + @Test + public void testReflectionAsmodeus2() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawTwo, 0, 0, reflection, reflection); + assertExileCount(playerA, 4); + } + + @Test + public void testReflectionAsmodeus3() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawThree, 0, 0, reflection, reflection, reflection); + assertExileCount(playerA, 6); + } + + @Test + public void testAsmodeusReflection1() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawOne, 0, 0, asmodeus); + assertExileCount(playerA, 1); + } + + @Test + public void testAsmodeusReflection2() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawTwo, 0, 0, asmodeus, asmodeus); + assertExileCount(playerA, 2); + } + + @Test + public void testAsmodeusReflection3() { + addCard(Zone.BATTLEFIELD, playerA, asmodeus); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawThree, 0, 0, asmodeus, asmodeus, asmodeus); + assertExileCount(playerA, 3); + } + + @Test + public void testAlmsCollectorReflection1() { + addCard(Zone.BATTLEFIELD, playerB, almsCollector); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawOne, 1, 1); + } + + @Test + public void testAlmsCollectorReflection2() { + addCard(Zone.BATTLEFIELD, playerB, almsCollector); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawTwo, 2, 1); + } + + @Test + public void testAlmsCollectorReflection3() { + addCard(Zone.BATTLEFIELD, playerB, almsCollector); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawThree, 2, 1); + } + + @Test + public void testNotionThiefReflection1() { + addCard(Zone.BATTLEFIELD, playerB, notionThief); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawOne, 0, 1, notionThief); + } + + @Test + public void testNotionThiefReflection2() { + addCard(Zone.BATTLEFIELD, playerB, notionThief); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawTwo, 0, 2, notionThief, notionThief); + } + + @Test + public void testNotionThiefReflection3() { + addCard(Zone.BATTLEFIELD, playerB, notionThief); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawThree, 0, 3, notionThief, notionThief, notionThief); + } + + @Test + public void testReflectionNotionThief1() { + addCard(Zone.BATTLEFIELD, playerB, notionThief); + addCard(Zone.BATTLEFIELD, playerA, reflection); + testSingle(drawOne, 0, 2, reflection); + } + + @Test + public void testAncientExcavation() { + addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 4); + addCard(Zone.HAND, playerA, excavation); + addCard(Zone.HAND, playerA, "Shock"); + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Healing Salve"); + addCard(Zone.LIBRARY, playerA, "Giant Growth"); + addCard(Zone.BATTLEFIELD, playerA, reflection); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, excavation); + // 1 card in hand, thought reflection -> draw 2, then discard two + setChoice(playerA, "Shock"); // to discard + setChoice(playerA, "Healing Salve"); // to discard + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, excavation, 1); + assertGraveyardCount(playerA, "Shock", 1); + assertGraveyardCount(playerA, "Healing Salve", 1); + assertHandCount(playerA, "Giant Growth", 1); + assertHandCount(playerA, 1); + } + + @Test + public void testAncientExcavationNotionThief() { + addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 4); + addCard(Zone.HAND, playerA, excavation); + addCard(Zone.HAND, playerA, "Shock"); + addCard(Zone.HAND, playerA, "Dark Ritual"); + addCard(Zone.HAND, playerA, "Ornithopter"); + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Healing Salve"); + addCard(Zone.LIBRARY, playerA, "Giant Growth"); + addCard(Zone.BATTLEFIELD, playerB, notionThief); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, excavation); + // 3 cards in hand, notion thief -> instead opponent draws three + // but cards were still drawn this way, so discard all three (no choice to make) + // if this turns out to be incorrect, modify the test accordingly + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, excavation, 1); + assertGraveyardCount(playerA, "Shock", 1); + assertGraveyardCount(playerA, "Dark Ritual", 1); + assertGraveyardCount(playerA, "Ornithopter", 1); + assertHandCount(playerA, 0); + assertLibraryCount(playerA, "Healing Salve", 1); + assertLibraryCount(playerA, "Giant Growth", 1); + assertHandCount(playerB, 3); + } + + @Test + public void testAncientExcavationAlmsCollector() { + addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 4); + addCard(Zone.HAND, playerA, excavation); + addCard(Zone.HAND, playerA, "Shock"); + addCard(Zone.HAND, playerA, "Dark Ritual"); + addCard(Zone.HAND, playerA, "Ornithopter"); + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Healing Salve"); + addCard(Zone.LIBRARY, playerA, "Giant Growth"); + addCard(Zone.BATTLEFIELD, playerB, almsCollector); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, excavation); + // 3 cards in hand, alms collector -> instead each player draws one + // interpret as two cards were drawn this way in total + // if this turns out to be incorrect, modify the test accordingly + setChoice(playerA, "Shock"); // to discard + setChoice(playerA, "Giant Growth"); // to discard + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, excavation, 1); + assertGraveyardCount(playerA, "Shock", 1); + assertGraveyardCount(playerA, "Giant Growth", 1); + assertHandCount(playerA, "Dark Ritual", 1); + assertHandCount(playerA, "Ornithopter", 1); + assertHandCount(playerA, 2); + assertLibraryCount(playerA, "Healing Salve", 1); + assertHandCount(playerB, 1); + } + + /** * The effects of multiple Thought Reflections are cumulative. For example, * if you have three Thought Reflections on the battlefield, you'll draw @@ -104,4 +435,49 @@ public void WordsOfWilding() { assertPermanentCount(playerA, "Bear Token", 1); assertHandCount(playerA, 1); } + + @Test + public void testAlmsCollector() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerB, "Alms Collector"); + // If an opponent would draw two or more cards, instead you and that player each draw a card. + + // Draw two cards. + addCard(Zone.HAND, playerA, "Counsel of the Soratami", 1); // Sorcery {2}{U} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Counsel of the Soratami"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Counsel of the Soratami", 1); + assertHandCount(playerA, 1); + assertHandCount(playerB, 1); + } + + @Test + public void testRiverSong() { + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.LIBRARY, playerA, "Healing Salve"); // bottom + addCard(Zone.LIBRARY, playerA, "Giant Growth"); + addCard(Zone.LIBRARY, playerA, "Shock"); // top + addCard(Zone.BATTLEFIELD, playerA, "River Song"); + // You draw cards from the bottom of your library rather than the top. + addCard(Zone.HAND, playerA, drawOne, 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, drawOne); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, drawOne, 1); + assertHandCount(playerA, 1); + assertHandCount(playerA, "Healing Salve", 1); + assertLibraryCount(playerA, "Shock", 1); + } + } diff --git a/Mage/src/main/java/mage/actions/MageDrawAction.java b/Mage/src/main/java/mage/actions/MageDrawAction.java deleted file mode 100644 index eaef9d77c0ed..000000000000 --- a/Mage/src/main/java/mage/actions/MageDrawAction.java +++ /dev/null @@ -1,113 +0,0 @@ -package mage.actions; - -import mage.abilities.Ability; -import mage.actions.impl.MageAction; -import mage.actions.score.ArtificialScoringSystem; -import mage.cards.Card; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.DrawCardEvent; -import mage.game.events.DrawCardsEvent; -import mage.game.events.DrewCardEvent; -import mage.game.events.GameEvent; -import mage.players.Player; -import mage.util.CardUtil; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Action for drawing cards. - * - * @author ayrat - */ -public class MageDrawAction extends MageAction { - - private static final int NEGATIVE_VALUE = -1000000; - - private final Player player; - private final List drawnCards; - private final GameEvent originalDrawEvent; // for replace effects - - private int amount; - - public MageDrawAction(Player player, int amount, GameEvent originalDrawEvent) { - this.player = player; - this.amount = amount; - this.drawnCards = new ArrayList<>(); - this.originalDrawEvent = originalDrawEvent; - } - - /** - * Draw and set action score. - * - * @param source - * @param game Game context. - * @return Number of cards drawn - */ - @Override - public int doAction(Ability source, Game game) { - int numDrawn = 0; - int score = 0; - GameEvent event = new DrawCardsEvent(this.player.getId(), source, this.originalDrawEvent, this.amount); - // TODO: This needs a better description of how it works. Why "amount < 2"? - if (amount < 2 || !game.replaceEvent(event)) { - amount = event.getAmount(); - for (int i = 0; i < amount; i++) { - int value = drawCard(source, this.originalDrawEvent, game); - if (value == NEGATIVE_VALUE) { - continue; - } - numDrawn++; - score += value; - } - if (!player.isTopCardRevealed() && numDrawn > 0) { - game.fireInformEvent(player.getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : "")); - } - setScore(player, score); - } - return numDrawn; - } - - /** - * Draw a card if possible (there is no replacement effect that prevent us - * from drawing). Fire event about card drawn. - * - * @param source - * @param originalDrawEvent original draw event for replacement effects, can be null for normal calls - * @param game - * @return - */ - protected int drawCard(Ability source, GameEvent originalDrawEvent, Game game) { - GameEvent event = new DrawCardEvent(this.player.getId(), source, originalDrawEvent); - if (!game.replaceEvent(event)) { - Card card = player.getLibrary().removeFromTop(game); - if (card != null) { - drawnCards.add(card); - card.moveToZone(Zone.HAND, source, game, false); // if you want to use event.getSourceId() here then thinks x10 times - if (player.isTopCardRevealed()) { - game.fireInformEvent(player.getLogName() + " draws a revealed card (" + card.getLogName() + ')'); - } - - game.fireEvent(new DrewCardEvent(card.getId(), player.getId(), source, originalDrawEvent)); - return ArtificialScoringSystem.inst.getCardScore(card); - } - } - return NEGATIVE_VALUE; - } - - /** - * Return a card back to top. - * - * @param game Game context - */ - @Override - public void undoAction(Game game) { - for (int index = drawnCards.size() - 1; index >= 0; index--) { - Card card = drawnCards.get(index); - player.getHand().remove(card); - player.getLibrary().putOnTop(card, game); - } - } -} diff --git a/Mage/src/main/java/mage/actions/impl/MageAction.java b/Mage/src/main/java/mage/actions/impl/MageAction.java deleted file mode 100644 index 13b8463a8076..000000000000 --- a/Mage/src/main/java/mage/actions/impl/MageAction.java +++ /dev/null @@ -1,76 +0,0 @@ -package mage.actions.impl; - -import mage.abilities.Ability; -import mage.game.Game; -import mage.players.Player; - -import java.util.UUID; - -/** - * Base class for mage actions. - * - * @author ayratn - */ -public abstract class MageAction { - - /** - * {@link Player} we count score for. - */ - private Player scorePlayer; - - /** - * Current game score for the player. - */ - private int score = 0; - - /** - * Set or change action score. - * - * @param scorePlayer Set player. - * @param score Set score value. - */ - protected void setScore(Player scorePlayer, int score) { - this.scorePlayer = scorePlayer; - this.score = score; - } - - /** - * Get game score for the {@link Player}. Value depends on the owner of this - * action. In case player and owner differ, negative value is returned. - * - * @param player - * @return - */ - public int getScore(final Player player) { - if (player == null || scorePlayer == null) { - return 0; - } - if (player.getId().equals(scorePlayer.getId())) { - return score; - } else { - return -score; - } - } - - /** - * Execute action. - * - * - * @param source - * @param game Game context. - * @return - */ - public abstract int doAction(Ability source, final Game game); - - /** - * Undo action. - * - * @param game Game context - */ - public abstract void undoAction(final Game game); - - @Override - public String toString() { - return ""; - } -} diff --git a/Mage/src/main/java/mage/actions/score/ArtificialScoringSystem.java b/Mage/src/main/java/mage/actions/score/ArtificialScoringSystem.java deleted file mode 100644 index 67fc047c4f64..000000000000 --- a/Mage/src/main/java/mage/actions/score/ArtificialScoringSystem.java +++ /dev/null @@ -1,42 +0,0 @@ -package mage.actions.score; - -import mage.cards.Card; -import mage.game.Game; -import org.apache.log4j.Logger; - -/** - * @author ayratn - */ -public class ArtificialScoringSystem implements ScoringSystem { - - public static ArtificialScoringSystem inst; - - private static final Logger log = Logger.getLogger(ArtificialScoringSystem.class); - - static { - inst = new ArtificialScoringSystem(); - log.debug("ArtificialScoringSystem has been instantiated."); - } - - /** - * Lose score is lowered in function of the turn and phase when it occurs. - * Encourages AI to win as fast as possible. - * - * @param game - * @return - */ - @Override - public int getLoseGameScore(final Game game) { - if (game.getStep() == null) { - return 0; - } - return ScoringConstants.LOSE_GAME_SCORE + game.getTurnNum() * 2500 + game.getTurnStepType().getIndex() * 200; - } - - @Override - public int getCardScore(Card card) { - //TODO: implement - return ScoringConstants.UNKNOWN_CARD_SCORE; - } - -} \ No newline at end of file diff --git a/Mage/src/main/java/mage/actions/score/ScoringConstants.java b/Mage/src/main/java/mage/actions/score/ScoringConstants.java deleted file mode 100644 index 4de34a404a6f..000000000000 --- a/Mage/src/main/java/mage/actions/score/ScoringConstants.java +++ /dev/null @@ -1,13 +0,0 @@ -package mage.actions.score; - -/** - * Constants for scoring system. - * - * @author ayratn - */ -public final class ScoringConstants { - public static final int WIN_GAME_SCORE = 100000000; - public static final int LOSE_GAME_SCORE = -WIN_GAME_SCORE; - - public static final int UNKNOWN_CARD_SCORE = 300; -} diff --git a/Mage/src/main/java/mage/actions/score/ScoringSystem.java b/Mage/src/main/java/mage/actions/score/ScoringSystem.java deleted file mode 100644 index b99c3c2580bc..000000000000 --- a/Mage/src/main/java/mage/actions/score/ScoringSystem.java +++ /dev/null @@ -1,13 +0,0 @@ -package mage.actions.score; - -import mage.cards.Card; -import mage.game.Game; - -/** - * @author ayratn - */ -public interface ScoringSystem { - - int getLoseGameScore(final Game game); - int getCardScore(final Card card); -} diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 900f8f6f48ba..5e5f5b58ca58 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -11,7 +11,6 @@ import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffects; import mage.abilities.effects.PreventionEffectData; -import mage.actions.impl.MageAction; import mage.cards.Card; import mage.cards.Cards; import mage.cards.MeldCard; @@ -552,8 +551,6 @@ default boolean isOpponent(Player player, UUID playerToCheckId) { boolean endTurn(Ability source); - int doAction(Ability source, MageAction action); - //game transaction methods void saveState(boolean bookmark); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 4402a485a42b..ba5d909dcf11 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -22,7 +22,6 @@ import mage.abilities.keyword.*; import mage.abilities.mana.DelayedTriggeredManaAbility; import mage.abilities.mana.TriggeredManaAbility; -import mage.actions.impl.MageAction; import mage.cards.*; import mage.cards.decks.Deck; import mage.cards.decks.DeckCardInfo; @@ -3772,11 +3771,6 @@ public boolean endTurn(Ability source) { return true; } - @Override - public int doAction(Ability source, MageAction action) { - return action.doAction(source, this); - } - @Override public Date getStartTime() { if (startTime == null) { diff --git a/Mage/src/main/java/mage/game/events/DrawCardEvent.java b/Mage/src/main/java/mage/game/events/DrawCardEvent.java index ff42083931cf..f5c62db46210 100644 --- a/Mage/src/main/java/mage/game/events/DrawCardEvent.java +++ b/Mage/src/main/java/mage/game/events/DrawCardEvent.java @@ -9,6 +9,10 @@ */ public class DrawCardEvent extends GameEvent { + private boolean fromBottom = false; // for replacement effects that draw from bottom of library instead + + private int cardsDrawn = 0; // for replacement effects to keep track for "cards drawn this way" + public DrawCardEvent(UUID playerId, Ability source, GameEvent originalDrawEvent) { super(GameEvent.EventType.DRAW_CARD, playerId, null, playerId, 0, false); @@ -22,4 +26,21 @@ public DrawCardEvent(UUID playerId, Ability source, GameEvent originalDrawEvent) this.addAppliedEffects(originalDrawEvent.getAppliedEffects()); } } + + public void setFromBottom(boolean fromBottom) { + this.fromBottom = fromBottom; + } + + public boolean isFromBottom() { + return fromBottom; + } + + public void incrementCardsDrawn(int cardsDrawn) { + this.cardsDrawn += cardsDrawn; + } + + public int getCardsDrawn() { + return cardsDrawn; + } + } diff --git a/Mage/src/main/java/mage/game/events/DrawCardsEvent.java b/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java similarity index 53% rename from Mage/src/main/java/mage/game/events/DrawCardsEvent.java rename to Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java index d6e7288a35b3..8394a6b4cc1f 100644 --- a/Mage/src/main/java/mage/game/events/DrawCardsEvent.java +++ b/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java @@ -7,10 +7,12 @@ /** * @author JayDi85 */ -public class DrawCardsEvent extends GameEvent { +public class DrawTwoOrMoreCardsEvent extends GameEvent { - public DrawCardsEvent(UUID playerId, Ability source, GameEvent originalDrawEvent, int amount) { - super(GameEvent.EventType.DRAW_CARDS, playerId, null, playerId, amount, false); + private int cardsDrawn = 0; // for replacement effects to keep track for "cards drawn this way" + + public DrawTwoOrMoreCardsEvent(UUID playerId, Ability source, GameEvent originalDrawEvent, int amount) { + super(GameEvent.EventType.DRAW_TWO_OR_MORE_CARDS, playerId, null, playerId, amount, false); // source of draw events must be kept between replacements, example: UnpredictableCycloneTest this.setSourceId(originalDrawEvent == null @@ -22,4 +24,13 @@ public DrawCardsEvent(UUID playerId, Ability source, GameEvent originalDrawEvent this.addAppliedEffects(originalDrawEvent.getAppliedEffects()); } } + + public void incrementCardsDrawn(int cardsDrawn) { + this.cardsDrawn += cardsDrawn; + } + + public int getCardsDrawn() { + return cardsDrawn; + } + } diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index ee4a1a973451..45ede5935661 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -75,7 +75,7 @@ public enum EventType { ZONE_CHANGE, ZONE_CHANGE_GROUP, // between two specific zones only; TODO: rework all usages to ZONE_CHANGE_BATCH instead, see #11895 ZONE_CHANGE_BATCH, // all zone changes that occurred from a single effect - DRAW_CARDS, // event calls for multi draws only (if player draws 2+ cards at once) + DRAW_TWO_OR_MORE_CARDS, // event calls for multi draws only (if player draws 2+ cards at once) DRAW_CARD, DREW_CARD, EXPLORE, EXPLORED, // targetId is exploring permanent, playerId is its controller ECHO_PAID, diff --git a/Mage/src/main/java/mage/players/Library.java b/Mage/src/main/java/mage/players/Library.java index 1ce50eaf77c0..fcc84fca2bbf 100644 --- a/Mage/src/main/java/mage/players/Library.java +++ b/Mage/src/main/java/mage/players/Library.java @@ -47,15 +47,11 @@ public void shuffle() { } /** - * Removes the top card of the Library and returns it - * - * @param game - * @return Card - * @see Card + * Draws a card from the top of the library, removing it from the library. + * If library is empty, returns null and sets flag for drawing from an empty library. */ - public Card removeFromTop(Game game) { - UUID cardId = library.pollFirst(); - Card card = game.getCard(cardId); + public Card drawFromTop(Game game) { + Card card = game.getCard(library.pollFirst()); if (card == null) { emptyDraw = true; } @@ -63,15 +59,11 @@ public Card removeFromTop(Game game) { } /** - * Removes the bottom card of the Library and returns it - * - * @param game - * @return Card - * @see Card + * Draws a card from the bottom of the library, removing it from the library. + * If library is empty, returns null and sets flag for drawing from an empty library. */ - public Card removeFromBottom(Game game) { - UUID cardId = library.pollLast(); - Card card = game.getCard(cardId); + public Card drawFromBottom(Game game) { + Card card = game.getCard(library.pollLast()); if (card == null) { emptyDraw = true; } @@ -79,25 +71,27 @@ public Card removeFromBottom(Game game) { } /** - * Returns the top card of the Library without removing it - * - * @param game - * @return Card - * @see Card + * Removes the top card from the Library and returns it (can be null if library is empty). + */ + @Deprecated // recommend refactoring methods that re-order library to not require this explicit removal + public Card removeFromTop(Game game) { + return game.getCard(library.pollFirst()); + } + + /** + * Returns the top card of the Library (can be null if library is empty). + * The card is still in the library, until/unless some zone-handling code moves it */ public Card getFromTop(Game game) { return game.getCard(library.peekFirst()); } /** - * Returns the bottommost card of the Library without removing it - * - * @param game - * @return Card - * @see Card + * Returns the bottom card of the library (can be null if library is empty) + * The card is still in the library, until/unless some zone-handling code moves it */ public Card getFromBottom(Game game) { - return game.getCard(library.pollLast()); + return game.getCard(library.peekLast()); } public void putOnTop(Card card, Game game) { @@ -152,10 +146,7 @@ public List getCardList() { } /** - * Returns the cards of the library in a list ordered from top to buttom - * - * @param game - * @return + * Returns the cards of the library in a list ordered from top to bottom */ public List getCards(Game game) { return library.stream().map(game::getCard).filter(Objects::nonNull).collect(Collectors.toList()); @@ -235,9 +226,6 @@ public void reset() { /** * Tests only -- find card position in library - * - * @param cardId - * @return */ public int getCardPosition(UUID cardId) { UUID[] list = library.toArray(new UUID[0]); diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index d0bd663c0259..1f9cae6b8a7b 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -407,25 +407,21 @@ default boolean isComputer() { void shuffleLibrary(Ability source, Game game); /** - * Draw cards. If you call it in replace events then use method with event.appliedEffects param instead. - * Returns 0 if replacement effect triggers on card draw. + * Draw cards. If you call it in replace events then use method with event param instead (for appliedEffects) * - * @param num + * @param num cards to draw * @param source can be null for game default draws (non effects, example: start of the turn) - * @param game - * @return + * @return number of cards drawn, including as a result of replacement effects */ int drawCards(int num, Ability source, Game game); /** * Draw cards with applied effects, for replaceEvent - * Returns 0 if replacement effect triggers on card draw. * - * @param num + * @param num cards to draw * @param source can be null for game default draws (non effects, example: start of the turn) - * @param game * @param event original draw event in replacement code - * @return + * @return number of cards drawn, including as a result of replacement effects */ int drawCards(int num, Ability source, Game game, GameEvent event); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 682cb5ce3d7c..bc9a622517ec 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -19,7 +19,6 @@ import mage.abilities.keyword.*; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.abilities.mana.ManaOptions; -import mage.actions.MageDrawAction; import mage.cards.*; import mage.cards.decks.Deck; import mage.choices.Choice; @@ -82,7 +81,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * During some steps we can't play anything */ - final static Map SILENT_PHASES_STEPS = ImmutableMap.builder(). + static final Map SILENT_PHASES_STEPS = ImmutableMap.builder(). put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); /** @@ -737,15 +736,60 @@ public boolean hasProtectionFrom(MageObject source, Game game) { @Override public int drawCards(int num, Ability source, Game game) { - if (num > 0) { - return game.doAction(source, new MageDrawAction(this, num, null)); - } - return 0; + return drawCards(num, source, game, null); } + /* + * 614.11. Some effects replace card draws. These effects are applied even if no cards could be drawn because + * there are no cards in the affected player's library. + * 614.11a. If an effect replaces a draw within a sequence of card draws, all actions required by the replacement + * are completed, if possible, before resuming the sequence. + * 614.11b. If an effect would have a player both draw a card and perform an additional action on that card, and + * the draw is replaced, the additional action is not performed on any cards that are drawn as a result of that + * replacement effect. + */ @Override public int drawCards(int num, Ability source, Game game, GameEvent event) { - return game.doAction(source, new MageDrawAction(this, num, event)); + if (num == 0) { + return 0; + } + if (num >= 2) { + // Event for replacement effects that only apply when two or more cards are drawn + DrawTwoOrMoreCardsEvent multiDrawEvent = new DrawTwoOrMoreCardsEvent(getId(), source, event, num); + if (game.replaceEvent(multiDrawEvent)) { + return multiDrawEvent.getCardsDrawn(); + } + num = multiDrawEvent.getAmount(); + } + int numDrawn = 0; + for (int i = 0; i < num; i++) { + DrawCardEvent drawCardEvent = new DrawCardEvent(getId(), source, event); + if (game.replaceEvent(drawCardEvent)) { + numDrawn += drawCardEvent.getCardsDrawn(); + continue; + } + Card card = drawCardEvent.isFromBottom() ? getLibrary().drawFromBottom(game) : getLibrary().drawFromTop(game); + if (card != null) { + card.moveToZone(Zone.HAND, source, game, false); // if you want to use event.getSourceId() here then thinks x10 times + if (isTopCardRevealed()) { + game.fireInformEvent(getLogName() + " draws a revealed card (" + card.getLogName() + ')'); + } + game.fireEvent(new DrewCardEvent(card.getId(), getId(), source, event)); + numDrawn++; + } + } + if (!isTopCardRevealed() && numDrawn > 0) { + game.fireInformEvent(getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : "")); + } + // if this method was called from a replacement event, pass the number of cards back through + // (uncomment conditions if correct ruling is to only count cards drawn by the same player) + if (event instanceof DrawCardEvent /* && event.getPlayerId().equals(getId()) */ ) { + ((DrawCardEvent) event).incrementCardsDrawn(numDrawn); + } + if (event instanceof DrawTwoOrMoreCardsEvent /* && event.getPlayerId().equals(getId()) */ ) { + ((DrawTwoOrMoreCardsEvent) event).incrementCardsDrawn(numDrawn); + } + return numDrawn; } @Override