From 0e0fa119f57767fbe5b187ba32ebb40c3aef35b0 Mon Sep 17 00:00:00 2001 From: xenohedron Date: Wed, 21 Aug 2024 21:49:09 -0400 Subject: [PATCH 01/12] remove unused scoring system code --- .../java/mage/actions/MageDrawAction.java | 36 ++------------ .../java/mage/actions/impl/MageAction.java | 49 ------------------- .../score/ArtificialScoringSystem.java | 42 ---------------- .../mage/actions/score/ScoringConstants.java | 13 ----- .../mage/actions/score/ScoringSystem.java | 13 ----- 5 files changed, 5 insertions(+), 148 deletions(-) delete mode 100644 Mage/src/main/java/mage/actions/score/ArtificialScoringSystem.java delete mode 100644 Mage/src/main/java/mage/actions/score/ScoringConstants.java delete mode 100644 Mage/src/main/java/mage/actions/score/ScoringSystem.java diff --git a/Mage/src/main/java/mage/actions/MageDrawAction.java b/Mage/src/main/java/mage/actions/MageDrawAction.java index eaef9d77c0ed..c9763e5013ae 100644 --- a/Mage/src/main/java/mage/actions/MageDrawAction.java +++ b/Mage/src/main/java/mage/actions/MageDrawAction.java @@ -2,7 +2,6 @@ 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; @@ -13,10 +12,6 @@ import mage.players.Player; import mage.util.CardUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - /** * Action for drawing cards. * @@ -24,10 +19,7 @@ */ 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; @@ -35,7 +27,6 @@ public class MageDrawAction extends MageAction { public MageDrawAction(Player player, int amount, GameEvent originalDrawEvent) { this.player = player; this.amount = amount; - this.drawnCards = new ArrayList<>(); this.originalDrawEvent = originalDrawEvent; } @@ -49,23 +40,20 @@ public MageDrawAction(Player player, int amount, GameEvent originalDrawEvent) { @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) { + boolean value = drawCard(source, this.originalDrawEvent, game); + if (!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; } @@ -79,35 +67,21 @@ public int doAction(Ability source, Game game) { * @param game * @return */ - protected int drawCard(Ability source, GameEvent originalDrawEvent, Game game) { + protected boolean 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 true; } } - return NEGATIVE_VALUE; + return false; } - /** - * 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 index 13b8463a8076..208754e75975 100644 --- a/Mage/src/main/java/mage/actions/impl/MageAction.java +++ b/Mage/src/main/java/mage/actions/impl/MageAction.java @@ -2,9 +2,6 @@ import mage.abilities.Ability; import mage.game.Game; -import mage.players.Player; - -import java.util.UUID; /** * Base class for mage actions. @@ -13,45 +10,6 @@ */ 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. * @@ -62,13 +20,6 @@ public int getScore(final Player player) { */ 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); -} From a55891042d376eb4f3798d42d8a8d0e242b46334 Mon Sep 17 00:00:00 2001 From: xenohedron Date: Wed, 21 Aug 2024 22:21:14 -0400 Subject: [PATCH 02/12] add test for Alms Collector replacement effect --- .../cards/replacement/DrawEffectsTest.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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..854fb05e4097 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; @@ -104,4 +103,25 @@ 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); + } + } From e363cd33f9ba4790066c2e02eb1d699e1c8639cb Mon Sep 17 00:00:00 2001 From: xenohedron Date: Wed, 21 Aug 2024 22:35:19 -0400 Subject: [PATCH 03/12] flatten draw cards into single method in PlayerImpl remove outdated MageAction framework clarify game event for drawing two or more cards --- Mage.Sets/src/mage/cards/a/AlmsCollector.java | 2 +- .../java/mage/actions/MageDrawAction.java | 87 ------------------- .../java/mage/actions/impl/MageAction.java | 27 ------ Mage/src/main/java/mage/game/Game.java | 3 - Mage/src/main/java/mage/game/GameImpl.java | 6 -- ...vent.java => DrawTwoOrMoreCardsEvent.java} | 6 +- .../main/java/mage/game/events/GameEvent.java | 2 +- .../main/java/mage/players/PlayerImpl.java | 37 ++++++-- 8 files changed, 36 insertions(+), 134 deletions(-) delete mode 100644 Mage/src/main/java/mage/actions/MageDrawAction.java delete mode 100644 Mage/src/main/java/mage/actions/impl/MageAction.java rename Mage/src/main/java/mage/game/events/{DrawCardsEvent.java => DrawTwoOrMoreCardsEvent.java} (69%) 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/src/main/java/mage/actions/MageDrawAction.java b/Mage/src/main/java/mage/actions/MageDrawAction.java deleted file mode 100644 index c9763e5013ae..000000000000 --- a/Mage/src/main/java/mage/actions/MageDrawAction.java +++ /dev/null @@ -1,87 +0,0 @@ -package mage.actions; - -import mage.abilities.Ability; -import mage.actions.impl.MageAction; -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; - -/** - * Action for drawing cards. - * - * @author ayrat - */ -public class MageDrawAction extends MageAction { - - private final Player player; - 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.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; - 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++) { - boolean value = drawCard(source, this.originalDrawEvent, game); - if (!value) { - continue; - } - numDrawn++; - } - if (!player.isTopCardRevealed() && numDrawn > 0) { - game.fireInformEvent(player.getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : "")); - } - } - 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 boolean 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) { - 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 true; - } - } - return false; - } - -} 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 208754e75975..000000000000 --- a/Mage/src/main/java/mage/actions/impl/MageAction.java +++ /dev/null @@ -1,27 +0,0 @@ -package mage.actions.impl; - -import mage.abilities.Ability; -import mage.game.Game; - -/** - * Base class for mage actions. - * - * @author ayratn - */ -public abstract class MageAction { - - /** - * Execute action. - * - * - * @param source - * @param game Game context. - * @return - */ - public abstract int doAction(Ability source, final Game game); - - @Override - public String toString() { - return ""; - } -} 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/DrawCardsEvent.java b/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java similarity index 69% rename from Mage/src/main/java/mage/game/events/DrawCardsEvent.java rename to Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java index d6e7288a35b3..48df1f1b8e59 100644 --- a/Mage/src/main/java/mage/game/events/DrawCardsEvent.java +++ b/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java @@ -7,10 +7,10 @@ /** * @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); + 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 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/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 682cb5ce3d7c..1b43ef9e526d 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; @@ -737,15 +736,41 @@ 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); } @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 + GameEvent multiDrawEvent = new DrawTwoOrMoreCardsEvent(getId(), source, event, num); + if (game.replaceEvent(multiDrawEvent)) { + return 0; + } + num = multiDrawEvent.getAmount(); + } + int numDrawn = 0; + for (int i = 0; i < num; i++) { + if (game.replaceEvent(new DrawCardEvent(getId(), source, event))) { + continue; + } + Card card = getLibrary().removeFromTop(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" : "")); + } + return numDrawn; } @Override From b79fc462c8562fe45529ff0f4aa743c941f353c3 Mon Sep 17 00:00:00 2001 From: xenohedron Date: Wed, 21 Aug 2024 23:42:23 -0400 Subject: [PATCH 04/12] clarify methods for getting cards from library --- .../mage/player/ai/ComputerPlayerMCTS.java | 2 +- .../src/mage/player/ai/MCTSNode.java | 3 +- Mage.Sets/src/mage/cards/c/CellarDoor.java | 2 +- .../mage/cards/i/InzervaMasterOfInsights.java | 2 +- Mage.Sets/src/mage/cards/l/LidlessGaze.java | 4 +- .../src/mage/cards/w/WriteIntoBeing.java | 4 +- .../effects/keyword/FatesealEffect.java | 2 +- Mage/src/main/java/mage/players/Library.java | 44 ++++++------------- .../main/java/mage/players/PlayerImpl.java | 3 +- 9 files changed, 23 insertions(+), 43 deletions(-) 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/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/i/InzervaMasterOfInsights.java b/Mage.Sets/src/mage/cards/i/InzervaMasterOfInsights.java index 7a02e65e87ed..8ee6ab43cc1a 100644 --- a/Mage.Sets/src/mage/cards/i/InzervaMasterOfInsights.java +++ b/Mage.Sets/src/mage/cards/i/InzervaMasterOfInsights.java @@ -80,7 +80,7 @@ public boolean apply(Game game, Ability source) { continue; } for (int i = 0; i < count; i++) { - Card card = opponent.getLibrary().removeFromTop(game); + Card card = opponent.getLibrary().getFromTop(game); cards.add(card); } TargetCard targets = new TargetCard(0, cards.size(), Zone.LIBRARY, new FilterCard("cards to PUT on the BOTTOM of " + opponent.getName() + "'s library")); diff --git a/Mage.Sets/src/mage/cards/l/LidlessGaze.java b/Mage.Sets/src/mage/cards/l/LidlessGaze.java index ce0f9979d247..05a43178c921 100644 --- a/Mage.Sets/src/mage/cards/l/LidlessGaze.java +++ b/Mage.Sets/src/mage/cards/l/LidlessGaze.java @@ -73,7 +73,7 @@ public boolean apply(Game game, Ability source) { for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { Player player = game.getPlayer(playerId); if (player != null) { - Card card = player.getLibrary().removeFromTop(game); + Card card = player.getLibrary().getFromTop(game); if (card != null) { controller.moveCardsToExile(card, source, game, true, exileId, exileName); cards.add(card); @@ -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/w/WriteIntoBeing.java b/Mage.Sets/src/mage/cards/w/WriteIntoBeing.java index 4b6ff09b2d46..1e661e38680c 100644 --- a/Mage.Sets/src/mage/cards/w/WriteIntoBeing.java +++ b/Mage.Sets/src/mage/cards/w/WriteIntoBeing.java @@ -75,8 +75,8 @@ public boolean apply(Game game, Ability source) { cardToManifest = cards.getRandom(game); } if (!controller.getLibrary().getFromTop(game).equals(cardToManifest)) { - Card cardToPutBack = controller.getLibrary().removeFromTop(game); - cardToManifest = controller.getLibrary().removeFromTop(game); + Card cardToPutBack = controller.getLibrary().getFromTop(game); + cardToManifest = controller.getLibrary().getFromTop(game); controller.getLibrary().putOnTop(cardToPutBack, game); controller.getLibrary().putOnTop(cardToManifest, game); } diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java index a8fffd829a5f..9dade52fbea2 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java @@ -54,7 +54,7 @@ public boolean apply(Game game, Ability source) { return true; } for (int i = 0; i < count; i++) { - Card card = opponent.getLibrary().removeFromTop(game); + Card card = opponent.getLibrary().getFromTop(game); cards.add(card); } TargetCard target1 = new TargetCard(Zone.LIBRARY, filter1); diff --git a/Mage/src/main/java/mage/players/Library.java b/Mage/src/main/java/mage/players/Library.java index 1ce50eaf77c0..0edc933493ea 100644 --- a/Mage/src/main/java/mage/players/Library.java +++ b/Mage/src/main/java/mage/players/Library.java @@ -47,13 +47,10 @@ 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) { + public Card drawFromTop(Game game) { UUID cardId = library.pollFirst(); Card card = game.getCard(cardId); if (card == null) { @@ -63,13 +60,10 @@ 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) { + public Card drawFromBottom(Game game) { UUID cardId = library.pollLast(); Card card = game.getCard(cardId); if (card == null) { @@ -79,25 +73,19 @@ public Card removeFromBottom(Game game) { } /** - * Returns the top card of the Library without removing it - * - * @param game - * @return Card - * @see Card + * 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()); // does not remove the card from its position in the library } public void putOnTop(Card card, Game game) { @@ -116,7 +104,7 @@ public void putCardToTopXPos(Card card, int pos, Game game) { int idx = 1; while (hasCards() && idx < pos) { idx++; - save.add(removeFromTop(game)); + save.add(drawFromTop(game)); // TODO: rework to manipulate directly rather than removing via draw method } putOnTop(card, game); while (!save.isEmpty()) { @@ -152,10 +140,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 +220,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/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 1b43ef9e526d..b42eb610e106 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -757,7 +757,7 @@ public int drawCards(int num, Ability source, Game game, GameEvent event) { if (game.replaceEvent(new DrawCardEvent(getId(), source, event))) { continue; } - Card card = getLibrary().removeFromTop(game); + Card card = 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()) { @@ -1075,7 +1075,6 @@ public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int && !(card instanceof PermanentToken) && !card.isCopy()) { Card cardInLib = getLibrary().getFromTop(game); if (cardInLib != null && cardInLib.getId().equals(card.getMainCard().getId())) { // check needed because e.g. commander can go to command zone - cardInLib = getLibrary().removeFromTop(game); getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); game.informPlayers((withName ? cardInLib.getLogName() : "A card") + " is put into " From fd00c1c2ecfc16a4e046f832223057d769a34258 Mon Sep 17 00:00:00 2001 From: xenohedron Date: Thu, 22 Aug 2024 00:04:13 -0400 Subject: [PATCH 05/12] implement [WHO] River Song --- Mage.Sets/src/mage/cards/r/RiverSong.java | 134 ++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 1 + .../java/mage/game/events/DrawCardEvent.java | 11 ++ .../main/java/mage/players/PlayerImpl.java | 7 +- 4 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/r/RiverSong.java 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/src/main/java/mage/game/events/DrawCardEvent.java b/Mage/src/main/java/mage/game/events/DrawCardEvent.java index ff42083931cf..abbc09bfe009 100644 --- a/Mage/src/main/java/mage/game/events/DrawCardEvent.java +++ b/Mage/src/main/java/mage/game/events/DrawCardEvent.java @@ -9,6 +9,8 @@ */ public class DrawCardEvent extends GameEvent { + private boolean fromBottom = false; // for replacement effects that draw from bottom of library instead + public DrawCardEvent(UUID playerId, Ability source, GameEvent originalDrawEvent) { super(GameEvent.EventType.DRAW_CARD, playerId, null, playerId, 0, false); @@ -22,4 +24,13 @@ 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; + } + } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index b42eb610e106..be85b7b8b2bc 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -81,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(); /** @@ -754,10 +754,11 @@ public int drawCards(int num, Ability source, Game game, GameEvent event) { } int numDrawn = 0; for (int i = 0; i < num; i++) { - if (game.replaceEvent(new DrawCardEvent(getId(), source, event))) { + DrawCardEvent drawCardEvent = new DrawCardEvent(getId(), source, event); + if (game.replaceEvent(drawCardEvent)) { continue; } - Card card = getLibrary().drawFromTop(game); + 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()) { From 7815aeda29c3d2e15b90141ace8b3b7af2815b6b Mon Sep 17 00:00:00 2001 From: xenohedron Date: Thu, 22 Aug 2024 00:34:24 -0400 Subject: [PATCH 06/12] fix error --- Mage/src/main/java/mage/players/PlayerImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index be85b7b8b2bc..ad5fb239b2ec 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1076,6 +1076,7 @@ public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int && !(card instanceof PermanentToken) && !card.isCopy()) { Card cardInLib = getLibrary().getFromTop(game); if (cardInLib != null && cardInLib.getId().equals(card.getMainCard().getId())) { // check needed because e.g. commander can go to command zone + cardInLib = getLibrary().drawFromTop(game); // TODO: refactor so this separate step isn't needed getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); game.informPlayers((withName ? cardInLib.getLogName() : "A card") + " is put into " From 8c47fe14dda98fceafc0cb1b93de123b430143a5 Mon Sep 17 00:00:00 2001 From: xenohedron Date: Thu, 22 Aug 2024 23:16:19 -0400 Subject: [PATCH 07/12] adjust library methods --- .../mage/cards/i/InzervaMasterOfInsights.java | 2 +- Mage.Sets/src/mage/cards/l/LidlessGaze.java | 2 +- Mage.Sets/src/mage/cards/w/WriteIntoBeing.java | 4 ++-- .../effects/keyword/FatesealEffect.java | 2 +- Mage/src/main/java/mage/players/Library.java | 16 +++++++++++----- Mage/src/main/java/mage/players/PlayerImpl.java | 2 +- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Mage.Sets/src/mage/cards/i/InzervaMasterOfInsights.java b/Mage.Sets/src/mage/cards/i/InzervaMasterOfInsights.java index 8ee6ab43cc1a..7a02e65e87ed 100644 --- a/Mage.Sets/src/mage/cards/i/InzervaMasterOfInsights.java +++ b/Mage.Sets/src/mage/cards/i/InzervaMasterOfInsights.java @@ -80,7 +80,7 @@ public boolean apply(Game game, Ability source) { continue; } for (int i = 0; i < count; i++) { - Card card = opponent.getLibrary().getFromTop(game); + Card card = opponent.getLibrary().removeFromTop(game); cards.add(card); } TargetCard targets = new TargetCard(0, cards.size(), Zone.LIBRARY, new FilterCard("cards to PUT on the BOTTOM of " + opponent.getName() + "'s library")); diff --git a/Mage.Sets/src/mage/cards/l/LidlessGaze.java b/Mage.Sets/src/mage/cards/l/LidlessGaze.java index 05a43178c921..2f12678b403b 100644 --- a/Mage.Sets/src/mage/cards/l/LidlessGaze.java +++ b/Mage.Sets/src/mage/cards/l/LidlessGaze.java @@ -73,7 +73,7 @@ public boolean apply(Game game, Ability source) { for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { Player player = game.getPlayer(playerId); if (player != null) { - Card card = player.getLibrary().getFromTop(game); + Card card = player.getLibrary().removeFromTop(game); if (card != null) { controller.moveCardsToExile(card, source, game, true, exileId, exileName); cards.add(card); diff --git a/Mage.Sets/src/mage/cards/w/WriteIntoBeing.java b/Mage.Sets/src/mage/cards/w/WriteIntoBeing.java index 1e661e38680c..4b6ff09b2d46 100644 --- a/Mage.Sets/src/mage/cards/w/WriteIntoBeing.java +++ b/Mage.Sets/src/mage/cards/w/WriteIntoBeing.java @@ -75,8 +75,8 @@ public boolean apply(Game game, Ability source) { cardToManifest = cards.getRandom(game); } if (!controller.getLibrary().getFromTop(game).equals(cardToManifest)) { - Card cardToPutBack = controller.getLibrary().getFromTop(game); - cardToManifest = controller.getLibrary().getFromTop(game); + Card cardToPutBack = controller.getLibrary().removeFromTop(game); + cardToManifest = controller.getLibrary().removeFromTop(game); controller.getLibrary().putOnTop(cardToPutBack, game); controller.getLibrary().putOnTop(cardToManifest, game); } diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java index 9dade52fbea2..a8fffd829a5f 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/FatesealEffect.java @@ -54,7 +54,7 @@ public boolean apply(Game game, Ability source) { return true; } for (int i = 0; i < count; i++) { - Card card = opponent.getLibrary().getFromTop(game); + Card card = opponent.getLibrary().removeFromTop(game); cards.add(card); } TargetCard target1 = new TargetCard(Zone.LIBRARY, filter1); diff --git a/Mage/src/main/java/mage/players/Library.java b/Mage/src/main/java/mage/players/Library.java index 0edc933493ea..b12206443983 100644 --- a/Mage/src/main/java/mage/players/Library.java +++ b/Mage/src/main/java/mage/players/Library.java @@ -51,8 +51,7 @@ public void shuffle() { * If library is empty, returns null and sets flag for drawing from an empty library. */ public Card drawFromTop(Game game) { - UUID cardId = library.pollFirst(); - Card card = game.getCard(cardId); + Card card = game.getCard(library.pollFirst()); if (card == null) { emptyDraw = true; } @@ -64,14 +63,21 @@ public Card drawFromTop(Game game) { * If library is empty, returns null and sets flag for drawing from an empty library. */ public Card drawFromBottom(Game game) { - UUID cardId = library.pollLast(); - Card card = game.getCard(cardId); + Card card = game.getCard(library.pollLast()); if (card == null) { emptyDraw = true; } return 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 @@ -104,7 +110,7 @@ public void putCardToTopXPos(Card card, int pos, Game game) { int idx = 1; while (hasCards() && idx < pos) { idx++; - save.add(drawFromTop(game)); // TODO: rework to manipulate directly rather than removing via draw method + save.add(removeFromTop(game)); } putOnTop(card, game); while (!save.isEmpty()) { diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index ad5fb239b2ec..9535c05c5bcd 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1076,7 +1076,7 @@ public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int && !(card instanceof PermanentToken) && !card.isCopy()) { Card cardInLib = getLibrary().getFromTop(game); if (cardInLib != null && cardInLib.getId().equals(card.getMainCard().getId())) { // check needed because e.g. commander can go to command zone - cardInLib = getLibrary().drawFromTop(game); // TODO: refactor so this separate step isn't needed + cardInLib = getLibrary().removeFromTop(game); getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); game.informPlayers((withName ? cardInLib.getLogName() : "A card") + " is put into " From 83b98d9c051d4ce9b8a4baa8afc28590e1548f8d Mon Sep 17 00:00:00 2001 From: xenohedron Date: Fri, 23 Aug 2024 00:41:58 -0400 Subject: [PATCH 08/12] add lots of test cases for draw replacement effects --- .../src/mage/cards/b/BloodScrivener.java | 6 +- .../cards/replacement/DrawEffectsTest.java | 244 +++++++++++++++++- .../main/java/mage/players/PlayerImpl.java | 9 + 3 files changed, 252 insertions(+), 7 deletions(-) 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.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 854fb05e4097..96aba4d69837 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 @@ -7,11 +7,251 @@ 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 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); + } + + /** * The effects of multiple Thought Reflections are cumulative. For example, * if you have three Thought Reflections on the battlefield, you'll draw diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 9535c05c5bcd..e65915d69edf 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -739,6 +739,15 @@ public int drawCards(int num, Ability source, Game game) { 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) { if (num == 0) { From 24f78eb3b84e0c13fad6bb41f9c1982640d1d5ab Mon Sep 17 00:00:00 2001 From: xenohedron Date: Fri, 23 Aug 2024 01:20:06 -0400 Subject: [PATCH 09/12] fix #12616 --- .../cards/replacement/DrawEffectsTest.java | 28 +++++++++++++++++++ .../java/mage/game/events/DrawCardEvent.java | 10 +++++++ Mage/src/main/java/mage/players/Player.java | 14 ++++------ .../main/java/mage/players/PlayerImpl.java | 4 +++ 4 files changed, 47 insertions(+), 9 deletions(-) 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 96aba4d69837..1157076884e8 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 @@ -14,6 +14,8 @@ 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. @@ -251,6 +253,32 @@ public void testReflectionNotionThief1() { 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); + } + /** * The effects of multiple Thought Reflections are cumulative. For example, diff --git a/Mage/src/main/java/mage/game/events/DrawCardEvent.java b/Mage/src/main/java/mage/game/events/DrawCardEvent.java index abbc09bfe009..f5c62db46210 100644 --- a/Mage/src/main/java/mage/game/events/DrawCardEvent.java +++ b/Mage/src/main/java/mage/game/events/DrawCardEvent.java @@ -11,6 +11,8 @@ 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); @@ -33,4 +35,12 @@ 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/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 e65915d69edf..959b30d05fbf 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -765,6 +765,7 @@ public int drawCards(int num, Ability source, Game game, GameEvent event) { 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); @@ -780,6 +781,9 @@ public int drawCards(int num, Ability source, Game game, GameEvent event) { if (!isTopCardRevealed() && numDrawn > 0) { game.fireInformEvent(getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : "")); } + if (event instanceof DrawCardEvent) { + ((DrawCardEvent) event).incrementCardsDrawn(numDrawn); + } return numDrawn; } From 9764265f6c02f446eedab49154c5a1957f075dfd Mon Sep 17 00:00:00 2001 From: xenohedron Date: Fri, 23 Aug 2024 21:16:48 -0400 Subject: [PATCH 10/12] track cards drawn this way through multi draw replacement as well --- .../cards/replacement/DrawEffectsTest.java | 64 +++++++++++++++++++ .../game/events/DrawTwoOrMoreCardsEvent.java | 11 ++++ .../main/java/mage/players/PlayerImpl.java | 11 +++- 3 files changed, 83 insertions(+), 3 deletions(-) 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 1157076884e8..0e705304c23e 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 @@ -279,6 +279,70 @@ public void testAncientExcavation() { 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, diff --git a/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java b/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java index 48df1f1b8e59..8394a6b4cc1f 100644 --- a/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java +++ b/Mage/src/main/java/mage/game/events/DrawTwoOrMoreCardsEvent.java @@ -9,6 +9,8 @@ */ public class DrawTwoOrMoreCardsEvent extends GameEvent { + 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); @@ -22,4 +24,13 @@ public DrawTwoOrMoreCardsEvent(UUID playerId, Ability source, GameEvent original 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/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 959b30d05fbf..bc9a622517ec 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -755,9 +755,9 @@ public int drawCards(int num, Ability source, Game game, GameEvent event) { } if (num >= 2) { // Event for replacement effects that only apply when two or more cards are drawn - GameEvent multiDrawEvent = new DrawTwoOrMoreCardsEvent(getId(), source, event, num); + DrawTwoOrMoreCardsEvent multiDrawEvent = new DrawTwoOrMoreCardsEvent(getId(), source, event, num); if (game.replaceEvent(multiDrawEvent)) { - return 0; + return multiDrawEvent.getCardsDrawn(); } num = multiDrawEvent.getAmount(); } @@ -781,9 +781,14 @@ public int drawCards(int num, Ability source, Game game, GameEvent event) { if (!isTopCardRevealed() && numDrawn > 0) { game.fireInformEvent(getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : "")); } - if (event instanceof DrawCardEvent) { + // 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; } From a5202c343f3510be52fc8f1c8653b343f8e1b88a Mon Sep 17 00:00:00 2001 From: xenohedron Date: Fri, 23 Aug 2024 23:42:32 -0400 Subject: [PATCH 11/12] add test for River Song --- .../cards/replacement/DrawEffectsTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 0e705304c23e..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 @@ -456,4 +456,28 @@ public void testAlmsCollector() { 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); + } + } From 56c717d5428c2c40bbde62b61e3dc2c4f65781a4 Mon Sep 17 00:00:00 2001 From: xenohedron Date: Fri, 23 Aug 2024 23:51:03 -0400 Subject: [PATCH 12/12] remove redundant comment --- Mage/src/main/java/mage/players/Library.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage/src/main/java/mage/players/Library.java b/Mage/src/main/java/mage/players/Library.java index b12206443983..fcc84fca2bbf 100644 --- a/Mage/src/main/java/mage/players/Library.java +++ b/Mage/src/main/java/mage/players/Library.java @@ -91,7 +91,7 @@ public Card getFromTop(Game game) { * The card is still in the library, until/unless some zone-handling code moves it */ public Card getFromBottom(Game game) { - return game.getCard(library.peekLast()); // does not remove the card from its position in the library + return game.getCard(library.peekLast()); } public void putOnTop(Card card, Game game) {