From 2ffa719278069205a0cf3e1e8a6b41543d5b30c6 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 23 Feb 2021 08:41:54 -0500 Subject: [PATCH] fixed issue with Kruphix, God of Horizons and Horizon Stone causing endless replacement effect loop --- Mage.Sets/src/mage/cards/h/HorizonStone.java | 33 +++------- .../mage/cards/k/KruphixGodOfHorizons.java | 31 +++------ .../single/jou/KruphixGodOfHorizonsTest.java | 62 +++++++++++++++++ Mage/src/main/java/mage/ConditionalMana.java | 2 +- Mage/src/main/java/mage/Emptiable.java | 15 +++++ Mage/src/main/java/mage/players/ManaPool.java | 66 +++++++++++-------- .../main/java/mage/players/ManaPoolItem.java | 3 +- 7 files changed, 137 insertions(+), 75 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/jou/KruphixGodOfHorizonsTest.java create mode 100644 Mage/src/main/java/mage/Emptiable.java diff --git a/Mage.Sets/src/mage/cards/h/HorizonStone.java b/Mage.Sets/src/mage/cards/h/HorizonStone.java index c2c032d355..07f6e2efda 100644 --- a/Mage.Sets/src/mage/cards/h/HorizonStone.java +++ b/Mage.Sets/src/mage/cards/h/HorizonStone.java @@ -2,14 +2,12 @@ package mage.cards.h; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.ContinuousEffectImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; +import mage.constants.*; import mage.game.Game; -import mage.game.events.GameEvent; +import mage.players.Player; import java.util.UUID; @@ -35,11 +33,11 @@ public final class HorizonStone extends CardImpl { } } -class HorizonStoneEffect extends ReplacementEffectImpl { +class HorizonStoneEffect extends ContinuousEffectImpl { HorizonStoneEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "If you would lose unspent mana, that mana becomes colorless instead."; + super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit); + staticText = "if you would lose unspent mana, that mana becomes colorless instead"; } private HorizonStoneEffect(final HorizonStoneEffect effect) { @@ -53,21 +51,10 @@ class HorizonStoneEffect extends ReplacementEffectImpl { @Override public boolean apply(Game game, Ability source) { - return false; - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + player.getManaPool().setManaBecomesColorless(true); + } return true; } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.EMPTY_MANA_POOL; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - return event.getPlayerId().equals(source.getControllerId()); - } } diff --git a/Mage.Sets/src/mage/cards/k/KruphixGodOfHorizons.java b/Mage.Sets/src/mage/cards/k/KruphixGodOfHorizons.java index c5bbad1ce6..e1523b10d9 100644 --- a/Mage.Sets/src/mage/cards/k/KruphixGodOfHorizons.java +++ b/Mage.Sets/src/mage/cards/k/KruphixGodOfHorizons.java @@ -4,7 +4,7 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.dynamicvalue.common.DevotionCount; -import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.continuous.LoseCreatureTypeSourceEffect; import mage.abilities.effects.common.continuous.MaximumHandSizeControllerEffect; import mage.abilities.keyword.IndestructibleAbility; @@ -12,7 +12,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; -import mage.game.events.GameEvent; +import mage.players.Player; import java.util.UUID; @@ -41,9 +41,9 @@ public final class KruphixGodOfHorizons extends CardImpl { Integer.MAX_VALUE, Duration.WhileOnBattlefield, MaximumHandSizeControllerEffect.HandSizeModification.SET ))); + // If unused mana would empty from your mana pool, that mana becomes colorless instead. this.addAbility(new SimpleStaticAbility(new KruphixGodOfHorizonsEffect())); - } private KruphixGodOfHorizons(final KruphixGodOfHorizons card) { @@ -56,11 +56,11 @@ public final class KruphixGodOfHorizons extends CardImpl { } } -class KruphixGodOfHorizonsEffect extends ReplacementEffectImpl { +class KruphixGodOfHorizonsEffect extends ContinuousEffectImpl { KruphixGodOfHorizonsEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "If you would lose unspent mana, that mana becomes colorless instead."; + super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit); + staticText = "if you would lose unspent mana, that mana becomes colorless instead"; } private KruphixGodOfHorizonsEffect(final KruphixGodOfHorizonsEffect effect) { @@ -74,21 +74,10 @@ class KruphixGodOfHorizonsEffect extends ReplacementEffectImpl { @Override public boolean apply(Game game, Ability source) { - return false; - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + player.getManaPool().setManaBecomesColorless(true); + } return true; } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.EMPTY_MANA_POOL; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - return event.getPlayerId().equals(source.getControllerId()); - } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/jou/KruphixGodOfHorizonsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/jou/KruphixGodOfHorizonsTest.java new file mode 100644 index 0000000000..73359b77e9 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/jou/KruphixGodOfHorizonsTest.java @@ -0,0 +1,62 @@ +package org.mage.test.cards.single.jou; + +import mage.constants.ManaType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class KruphixGodOfHorizonsTest extends CardTestPlayerBase { + + private static final String kruphix = "Kruphix, God of Horizons"; + private static final String sliver = "Metallic Sliver"; + private static final String repeal = "Mystic Repeal"; + + @Test + public void testKruphixNormal() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.BATTLEFIELD, playerA, kruphix); + + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}"); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertManaPool(playerA, ManaType.COLORLESS, 3); + assertManaPool(playerA, ManaType.GREEN, 0); + assertPermanentCount(playerA, kruphix, 1); + } + + @Test + public void testKruphixRemoved() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.BATTLEFIELD, playerA, kruphix); + addCard(Zone.HAND, playerA, sliver); + addCard(Zone.HAND, playerA, repeal); + + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}"); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, sliver); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, repeal, kruphix); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertManaPool(playerA, ManaType.COLORLESS, 0); + assertManaPool(playerA, ManaType.GREEN, 0); + assertPermanentCount(playerA, sliver, 1); + assertPermanentCount(playerA, kruphix, 0); + assertTappedCount("Forest", true, 1); + } +} diff --git a/Mage/src/main/java/mage/ConditionalMana.java b/Mage/src/main/java/mage/ConditionalMana.java index 4c51a6cbcb..1c2cb20991 100644 --- a/Mage/src/main/java/mage/ConditionalMana.java +++ b/Mage/src/main/java/mage/ConditionalMana.java @@ -17,7 +17,7 @@ import java.util.UUID; /** * @author nantuko */ -public class ConditionalMana extends Mana implements Serializable { +public class ConditionalMana extends Mana implements Serializable, Emptiable { /** * Conditions that should be met (all or any depending on comparison scope) diff --git a/Mage/src/main/java/mage/Emptiable.java b/Mage/src/main/java/mage/Emptiable.java new file mode 100644 index 0000000000..8578452fe3 --- /dev/null +++ b/Mage/src/main/java/mage/Emptiable.java @@ -0,0 +1,15 @@ +package mage; + +import mage.constants.ManaType; + +/** + * @author TheElk801 + */ +public interface Emptiable { + + public void add(ManaType manaType, int amount); + + public void clear(ManaType manaType); + + public int get(final ManaType manaType); +} diff --git a/Mage/src/main/java/mage/players/ManaPool.java b/Mage/src/main/java/mage/players/ManaPool.java index 0d46942b27..67b87e73ed 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -1,6 +1,7 @@ package mage.players; import mage.ConditionalMana; +import mage.Emptiable; import mage.MageObject; import mage.Mana; import mage.abilities.Ability; @@ -37,6 +38,7 @@ public class ManaPool implements Serializable { private final List poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes private final Set doNotEmptyManaTypes = new HashSet<>(); + private boolean manaBecomesColorless = false; private static final class ConditionalManaInfo { private final ManaType manaType; @@ -73,6 +75,7 @@ public class ManaPool implements Serializable { } this.doNotEmptyManaTypes.addAll(pool.doNotEmptyManaTypes); this.lastPaymentWasSnow = pool.lastPaymentWasSnow; + this.manaBecomesColorless = pool.manaBecomesColorless; } public int getRed() { @@ -229,12 +232,21 @@ public class ManaPool implements Serializable { public void clearEmptyManaPoolRules() { doNotEmptyManaTypes.clear(); + this.manaBecomesColorless = false; } public void addDoNotEmptyManaType(ManaType manaType) { doNotEmptyManaTypes.add(manaType); } + public void setManaBecomesColorless(boolean manaBecomesColorless) { + this.manaBecomesColorless = manaBecomesColorless; + } + + public boolean isManaBecomesColorless() { + return manaBecomesColorless; + } + public void init() { manaItems.clear(); } @@ -246,35 +258,15 @@ public class ManaPool implements Serializable { ManaPoolItem item = it.next(); ConditionalMana conditionalItem = item.getConditionalMana(); for (ManaType manaType : ManaType.values()) { - if (!doNotEmptyManaTypes.contains(manaType)) { - if (item.get(manaType) > 0) { - if (item.getDuration() != Duration.EndOfTurn - || game.getPhase().getType() == TurnPhase.END) { - if (game.replaceEvent(new GameEvent(GameEvent.EventType.EMPTY_MANA_POOL, playerId, null, playerId))) { - int amount = item.get(manaType); - item.clear(manaType); - item.add(ManaType.COLORLESS, amount); - } else { - total += item.get(manaType); - item.clear(manaType); - } - } - } - if (conditionalItem != null) { - if (conditionalItem.get(manaType) > 0) { - if (item.getDuration() != Duration.EndOfTurn - || game.getPhase().getType() == TurnPhase.END) { - if (game.replaceEvent(new GameEvent(GameEvent.EventType.EMPTY_MANA_POOL, playerId, null, playerId))) { - int amount = conditionalItem.get(manaType); - conditionalItem.clear(manaType); - conditionalItem.add(ManaType.COLORLESS, amount); - } else { - total += conditionalItem.get(manaType); - conditionalItem.clear(manaType); - } - } - } - } + if (doNotEmptyManaTypes.contains(manaType)) { + continue; + } + if (item.get(manaType) > 0) { + total += emptyItem(item, item, game, manaType); + } + if (conditionalItem != null + && conditionalItem.get(manaType) > 0) { + total += emptyItem(item, conditionalItem, game, manaType); } } if (item.count() == 0) { @@ -284,6 +276,22 @@ public class ManaPool implements Serializable { return total; } + private int emptyItem(ManaPoolItem item, Emptiable toEmpty, Game game, ManaType manaType) { + if (item.getDuration() == Duration.EndOfTurn + && game.getPhase().getType() != TurnPhase.END) { + return 0; + } + if (!manaBecomesColorless) { + int amount = toEmpty.get(manaType); + toEmpty.clear(manaType); + return amount; + } + int amount = toEmpty.get(manaType); + toEmpty.clear(manaType); + toEmpty.add(ManaType.COLORLESS, amount); + return 0; + } + public Mana getMana() { Mana m = new Mana(); for (ManaPoolItem item : manaItems) { diff --git a/Mage/src/main/java/mage/players/ManaPoolItem.java b/Mage/src/main/java/mage/players/ManaPoolItem.java index ba75b8798e..5b18b4d53a 100644 --- a/Mage/src/main/java/mage/players/ManaPoolItem.java +++ b/Mage/src/main/java/mage/players/ManaPoolItem.java @@ -5,6 +5,7 @@ import java.util.UUID; import mage.ConditionalMana; import mage.MageObject; import mage.Mana; +import mage.Emptiable; import mage.constants.Duration; import mage.constants.ManaType; @@ -12,7 +13,7 @@ import mage.constants.ManaType; * * @author BetaSteward_at_googlemail.com */ -public class ManaPoolItem implements Serializable { +public class ManaPoolItem implements Serializable, Emptiable { private int red = 0; private int green = 0;