diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DayNightTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DayNightTest.java index 3ce078d3f4d..3a58e0dad27 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DayNightTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DayNightTest.java @@ -1,8 +1,10 @@ package org.mage.test.cards.abilities.keywords; +import mage.cards.Card; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.game.permanent.Permanent; +import mage.util.CardUtil; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -59,6 +61,41 @@ public class DayNightTest extends CardTestPlayerBase { assertRuffianSmasher(true); } + @Test + public void testCopy() { + // possible bug: stack overflow on copy + addCard(Zone.HAND, playerA, ruffian); + + runCode("copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + Card card = currentGame.getCards().stream().filter(c -> c.getName().equals(ruffian)).findFirst().orElse(null); + Assert.assertNotNull(card); + Assert.assertNotNull(card.getSecondCardFace()); + + // original + Assert.assertNotNull(card.getSecondCardFace()); + // copy + Card copy = card.copy(); + Assert.assertNotNull(copy.getSecondCardFace()); + // deep copy + copy = CardUtil.deepCopyObject(card); + Assert.assertNotNull(copy.getSecondCardFace()); + + // copied + Card copied = game.copyCard(card, null, playerA.getId()); + Assert.assertNotNull(copied.getSecondCardFace()); + // copy + copy = copied.copy(); + Assert.assertNotNull(copy.getSecondCardFace()); + // deep copy + copy = CardUtil.deepCopyObject(copied); + Assert.assertNotNull(copy.getSecondCardFace()); + }); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + @Test public void testNightbound() { currentGame.setDaytime(false); diff --git a/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java b/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java index eb8f3fcd19f..f5f81daebd0 100644 --- a/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java +++ b/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java @@ -62,10 +62,14 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH public ModalDoubleFacedCard(ModalDoubleFacedCard card) { super(card); - this.leftHalfCard = card.getLeftHalfCard().copy(); - ((ModalDoubleFacedCardHalf) leftHalfCard).setParentCard(this); - this.rightHalfCard = card.rightHalfCard.copy(); - ((ModalDoubleFacedCardHalf) rightHalfCard).setParentCard(this); + if (card.leftHalfCard != null) { + this.leftHalfCard = card.leftHalfCard; + ((ModalDoubleFacedCardHalf) this.leftHalfCard).setParentCard(this); + } + if (card.rightHalfCard != null) { + this.rightHalfCard = card.rightHalfCard; + ((ModalDoubleFacedCardHalf) this.rightHalfCard).setParentCard(this); + } } public ModalDoubleFacedCardHalf getLeftHalfCard() { diff --git a/Mage/src/main/java/mage/cards/SplitCard.java b/Mage/src/main/java/mage/cards/SplitCard.java index aafccf957b2..db87315ebb3 100644 --- a/Mage/src/main/java/mage/cards/SplitCard.java +++ b/Mage/src/main/java/mage/cards/SplitCard.java @@ -38,10 +38,14 @@ public abstract class SplitCard extends CardImpl implements CardWithHalves { protected SplitCard(SplitCard card) { super(card); - this.leftHalfCard = card.getLeftHalfCard().copy(); - ((SplitCardHalf) leftHalfCard).setParentCard(this); - this.rightHalfCard = card.rightHalfCard.copy(); - ((SplitCardHalf) rightHalfCard).setParentCard(this); + if (card.leftHalfCard != null) { + this.leftHalfCard = card.leftHalfCard; + ((SplitCardHalf) this.leftHalfCard).setParentCard(this); + } + if (card.rightHalfCard != null) { + this.rightHalfCard = card.rightHalfCard; + ((SplitCardHalf) this.rightHalfCard).setParentCard(this); + } } public void setParts(SplitCardHalf leftHalfCard, SplitCardHalf rightHalfCard) { diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index 65d336cdf1a..809fc545a32 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -16,6 +16,7 @@ import mage.game.permanent.PermanentToken; import mage.game.stack.Spell; import mage.players.Player; import mage.target.TargetCard; +import mage.util.FuzzyTestsUtil; import java.util.*; @@ -416,6 +417,9 @@ public final class ZonesHandler { && card.removeFromZone(game, fromZone, source)) { success = true; event.setTarget(permanent); + + // tests only: inject fuzzy data with random phased out permanents + FuzzyTestsUtil.addRandomPhasedOutPermanent(permanent, source, game); } else { // revert controller to owner if permanent does not enter game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId()); diff --git a/Mage/src/main/java/mage/util/FuzzyTestsUtil.java b/Mage/src/main/java/mage/util/FuzzyTestsUtil.java new file mode 100644 index 00000000000..1523c9040a5 --- /dev/null +++ b/Mage/src/main/java/mage/util/FuzzyTestsUtil.java @@ -0,0 +1,90 @@ +package mage.util; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.EntersBattlefieldEffect; +import mage.abilities.keyword.PhasingAbility; +import mage.cards.Card; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.Map; +import java.util.UUID; + +/** + * Helper class for fuzzy testing + *

+ * Support: + * - [x] enable by command line; + * - [x] phased out permanents must be hidden + * - [ ] TODO: out of range players and permanents must be hidden + * - [ ] TODO: leave/disconnected players must be hidden + *

+ * How-to use: + * - enable here or by command line + * - run unit tests and research the fails + * + * @author JayDi85 + */ +public class FuzzyTestsUtil { + + /** + * Phased out permanents must be hidden + * Make sure other cards and effects do not see phased out permanents and ignore it + *

+ * Use case: + * - each permanent on battlefield will have copied phased out version with all abilities and effects + *

+ * How-to use: + * - set true or run with -Dxmage.tests.addRandomPhasedOutPermanents=true + */ + public static boolean ADD_RANDOM_PHASED_OUT_PERMANENTS = false; + + static { + String val = System.getProperty("xmage.tests.addRandomPhasedOutPermanents"); + if (val != null) { + ADD_RANDOM_PHASED_OUT_PERMANENTS = Boolean.parseBoolean(val); + } + } + + /** + * Create duplicated phased out permanent + */ + public static void addRandomPhasedOutPermanent(Permanent originalPermanent, Ability source, Game game) { + if (!ADD_RANDOM_PHASED_OUT_PERMANENTS) { + return; + } + Player samplePlayer = game.getPlayers().values().stream().findFirst().orElse(null); + if (samplePlayer == null || !samplePlayer.isTestsMode()) { + return; + } + + // copy permanent and put it to battlefield as phased out (diff sides also supported here) + // TODO: add phased out tests support (must not fail on it) + Card originalCardSide = game.getCard(originalPermanent.getId()); + Card originalCardMain = originalCardSide.getMainCard(); + if (!originalCardMain.hasAbility(PhasingAbility.getInstance(), game)) { + boolean canCreate = true; + Card doppelgangerCardMain = game.copyCard(originalCardMain, source, originalPermanent.getControllerId()); + Map mapOldToNew = CardUtil.getOriginalToCopiedPartsMap(originalCardMain, doppelgangerCardMain); + Card doppelgangerCardSide = (Card) mapOldToNew.getOrDefault(originalCardSide.getId(), null); + doppelgangerCardSide.addAbility(PhasingAbility.getInstance()); + + // compatibility workaround: remove all etb abilities to skip any etb choices (e.g. copy effect) + doppelgangerCardSide.getAbilities().removeIf(a -> a.getEffects().stream().anyMatch(e -> e instanceof EntersBattlefieldEffect)); + // compatibility workaround: ignore aura to skip any etb choices (e.g. select new target) + if (doppelgangerCardSide.hasSubtype(SubType.AURA, game)) { + canCreate = false; + } + + if (canCreate) { + doppelgangerCardSide.putOntoBattlefield(game, Zone.BATTLEFIELD, source, originalPermanent.getControllerId()); + Permanent doppelgangerPerm = CardUtil.getPermanentFromCardPutToBattlefield(doppelgangerCardSide, game); + doppelgangerPerm.phaseOut(game, true); // use indirect, so no phase in on untap + } + } + } +}