diff --git a/Mage.Common/src/main/java/mage/view/PermanentView.java b/Mage.Common/src/main/java/mage/view/PermanentView.java index b1a6ef27b02..34ca1e0af5a 100644 --- a/Mage.Common/src/main/java/mage/view/PermanentView.java +++ b/Mage.Common/src/main/java/mage/view/PermanentView.java @@ -26,7 +26,7 @@ public class PermanentView extends CardView { private final boolean summoningSickness; private final int damage; private List attachments; - private final CardView original; // original card before transforms and modifications + private final CardView original; // original card before transforms and modifications (null for opponents face down cards) private final boolean copy; private final String nameOwner; // only filled if != controller private final boolean controlled; @@ -51,13 +51,17 @@ public class PermanentView extends CardView { attachments.addAll(permanent.getAttachments()); } this.attachedTo = permanent.getAttachedTo(); + + // show face down cards to all players at the game end + boolean showFaceDownInfo = controlled || game.hasEnded(); + if (isToken()) { original = new CardView(((PermanentToken) permanent).getToken().copy(), (Game) null); original.expansionSetCode = permanent.getExpansionSetCode(); expansionSetCode = permanent.getExpansionSetCode(); } else { - if (card != null) { - // original may not be face down + if (card != null && showFaceDownInfo) { + // face down card must be hidden from opponent, but shown on game end for all original = new CardView(card.copy(), (Game) null); } else { original = null; @@ -71,10 +75,7 @@ public class PermanentView extends CardView { if (permanent.isCopy() && permanent.isFlipCard()) { this.alternateName = permanent.getFlipCardName(); } else { - if (controlled // controller may always know - || (!morphed && !manifested)) { // others don't know for morph or transformed cards - this.alternateName = original.getName(); - } + this.alternateName = original.getName(); } } if (permanent.getOwnerId() != null && !permanent.getOwnerId().equals(permanent.getControllerId())) { @@ -88,8 +89,9 @@ public class PermanentView extends CardView { this.nameOwner = ""; } + // add info for face down permanents if (permanent.isFaceDown(game) && card != null) { - if (controlled) { + if (showFaceDownInfo) { // must be a morphed or manifested card for (Ability permanentAbility : permanent.getAbilities(game)) { if (permanentAbility.getWorksFaceDown()) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java index 261a38076f5..14bef916591 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java @@ -4,13 +4,19 @@ import mage.cards.Card; import mage.constants.EmptyNames; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.game.Game; import mage.game.permanent.Permanent; +import mage.view.CardView; +import mage.view.GameView; +import mage.view.PermanentView; +import mage.view.PlayerView; import org.junit.Assert; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * @author LevelX2 + * @author LevelX2, JayDi85 */ public class ManifestTest extends CardTestPlayerBase { @@ -391,7 +397,7 @@ public class ManifestTest extends CardTestPlayerBase { @Test public void testWhisperwoodElemental() { setStrictChooseMode(true); - + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); // Seismic Rupture deals 2 damage to each creature without flying. addCard(Zone.HAND, playerA, "Seismic Rupture", 1); @@ -474,7 +480,7 @@ public class ManifestTest extends CardTestPlayerBase { } - @Test + @Test public void test_ManifestSorceryAndBlinkIt() { addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); @@ -483,11 +489,11 @@ public class ManifestTest extends CardTestPlayerBase { // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - + // Exile target creature you control, then return that card to the battlefield under your control. addCard(Zone.HAND, playerB, "Cloudshift", 1); //Instant {W} - + // Devoid // Flying // At the beginning of your upkeep, sacrifice a creature @@ -502,14 +508,14 @@ public class ManifestTest extends CardTestPlayerBase { setChoice(playerB, "Silvercoat Lion"); waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN, playerB); - + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cloudshift", EmptyNames.FACE_DOWN_CREATURE.toString()); setStrictChooseMode(true); setStopAt(2, PhaseStep.END_TURN); execute(); - // no life gain + // no life gain assertLife(playerA, 20); assertLife(playerB, 20); @@ -517,11 +523,91 @@ public class ManifestTest extends CardTestPlayerBase { assertGraveyardCount(playerB, "Silvercoat Lion", 1); assertGraveyardCount(playerB, "Cloudshift", 1); - - assertPermanentCount(playerB, "Lightning Bolt", 0); - assertExileCount(playerB, "Lightning Bolt", 1); + + assertPermanentCount(playerB, "Lightning Bolt", 0); + assertExileCount(playerB, "Lightning Bolt", 1); assertHandCount(playerB, "Mountain", 1); - } + } + + private PermanentView findFaceDownPermanent(Game game, TestPlayer viewFromPlayer, TestPlayer searchInPlayer) { + Permanent perm = game.getBattlefield().getAllPermanents() + .stream() + .filter(permanent -> permanent.isFaceDown(game)) + .findFirst() + .orElse(null); + Assert.assertNotNull(perm); + GameView gameView = new GameView(game.getState(), game, viewFromPlayer.getId(), null); + PlayerView playerView = gameView.getPlayers() + .stream() + .filter(view -> view.getPlayerId().equals(searchInPlayer.getId())) + .findFirst() + .orElse(null); + Assert.assertNotNull(playerView); + PermanentView permanentView = playerView.getBattlefield().values() + .stream() + .filter(CardView::isFaceDown) + .findFirst() + .orElse(null); + Assert.assertNotNull(permanentView); + return permanentView; + } + + private void assertFaceDown(String info, PermanentView faceDownPermanent, String realPermanentName, boolean realInfoMustBeVisible) { + if (realInfoMustBeVisible) { + // show all info + Assert.assertEquals(realPermanentName, faceDownPermanent.getName()); // show real name + Assert.assertEquals("2", faceDownPermanent.getPower()); + Assert.assertEquals("2", faceDownPermanent.getToughness()); + // + Assert.assertNotNull(faceDownPermanent.getOriginal()); + Assert.assertEquals(realPermanentName, faceDownPermanent.getOriginal().getName()); + } else { + // hide original info + Assert.assertEquals(info, "", faceDownPermanent.getName()); + Assert.assertEquals(info, "2", faceDownPermanent.getPower()); + Assert.assertEquals(info, "2", faceDownPermanent.getToughness()); + Assert.assertNull(info, faceDownPermanent.getOriginal()); + } + + } + + @Test + public void test_FaceDownCardsMustBeVisibleOnGameEnd() { + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerA, "Reality Shift"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + // + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reality Shift", "Silvercoat Lion"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + runCode("on active game", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + // hide from opponent + PermanentView permanent = findFaceDownPermanent(game, playerA, playerB); + assertFaceDown("in game: must hide from opponent", permanent, "Mountain", false); + + // show for yourself + permanent = findFaceDownPermanent(game, playerB, playerB); + assertFaceDown("in game: must show for yourself", permanent, "Mountain", true); + }); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + + // workaround to force end game (can't use other test commands after that) + playerA.won(currentGame); + Assert.assertTrue(currentGame.hasEnded()); + + // show all after game end + PermanentView permanent = findFaceDownPermanent(currentGame, playerA, playerB); + assertFaceDown("end game: must show for opponent", permanent, "Mountain", true); + // + permanent = findFaceDownPermanent(currentGame, playerB, playerB); + assertFaceDown("end game: must show for yourself", permanent, "Mountain", true); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java index a9c7bcff059..e608a8449f5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/AuratouchedMageTest.java @@ -30,7 +30,7 @@ public class AuratouchedMageTest extends CardTestPlayerBase { * made the Mage an artifact, for example, you could search for an Aura with * “enchant artifact.” (2005-10-01) */ - @Test + @Test // TODO: fix very rare random fails (16 of 1000) public void testAuratouchedMageEffectHasMadeIntoTypeArtifact() { //Expected result: An effect has made Auratouched Mage into an artifact upon entering the battlefield. An aura that only works on artifacts should work. setStrictChooseMode(true); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index ca347cb3f27..13df2764d21 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -675,8 +675,8 @@ public abstract class GameImpl implements Game { spell = (Spell) obj; } else if (obj != null) { logger.error(String.format( - "getSpellOrLKIStack got non-spell id %s correlating to non-spell object %s.", - obj.getClass().getName(), obj.getName()), + "getSpellOrLKIStack got non-spell id %s correlating to non-spell object %s.", + obj.getClass().getName(), obj.getName()), new Throwable() ); } @@ -1398,6 +1398,27 @@ public abstract class GameImpl implements Game { logger.debug("END of gameId: " + this.getId()); endTime = new Date(); state.endGame(); + + // inform players about face down cards + state.getBattlefield().getAllPermanents() + .stream() + .filter(permanent -> permanent.isFaceDown(this)) + .map(permanent -> { + Player player = this.getPlayer(permanent.getControllerId()); + Card card = permanent.getMainCard(); + if (card != null) { + return String.format("Face down card reveal: %s had %s", + (player == null ? "Unknown" : player.getLogName()), + permanent.getLogName()); + } else { + return null; + } + }) + .filter(Objects::nonNull) + .sorted() + .forEach(this::informPlayers); + + // cancel all player dialogs/feedbacks for (Player player : state.getPlayers().values()) { player.abort(); }