From b94344341bf47968fcfa2018aa418547da25917e Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 3 Jun 2020 12:44:58 +0400 Subject: [PATCH] Improved playable abilities and split cards: * Now human player uses same code for playable abilities search as test framework (old version used different code, so it could not work in one of the modes); * Split cards - improves playable highlights; * Split cards - fixed that it doesn't work with dynamic added abilities like flashback (#6327, #6470, #6549); --- .../src/mage/player/human/HumanPlayer.java | 11 +- .../cards/abilities/keywords/MorphTest.java | 93 ++++++ .../test/cards/mana/DoublingCubeTest.java | 3 +- .../CastSplitCardsWithAsThoughManaTest.java | 108 +++++++ .../CastSplitCardsWithFlashbackTest.java | 70 +++++ .../java/org/mage/test/player/TestPlayer.java | 4 +- Mage/src/main/java/mage/cards/SplitCard.java | 8 +- .../main/java/mage/players/PlayerImpl.java | 288 +++++++----------- 8 files changed, 399 insertions(+), 186 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithAsThoughManaTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithFlashbackTest.java diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 18ca9e30a42..7455d093361 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -1047,8 +1047,9 @@ public class HumanPlayer extends PlayerImpl { Zone zone = game.getState().getZone(object.getId()); if (zone != null) { // look at card or try to cast/activate abilities + LinkedHashMap useableAbilities = new LinkedHashMap<>(); + Player actingPlayer = null; - LinkedHashMap useableAbilities = null; if (playerId.equals(game.getPriorityPlayerId())) { actingPlayer = this; } else if (getPlayersUnderYourControl().contains(game.getPriorityPlayerId())) { @@ -1060,11 +1061,10 @@ public class HumanPlayer extends PlayerImpl { if (object instanceof Card && ((Card) object).isFaceDown(game) - && lookAtFaceDownCard((Card) object, game, useableAbilities == null ? 0 : useableAbilities.size())) { + && lookAtFaceDownCard((Card) object, game, useableAbilities.size())) { result = true; } else { - if (useableAbilities != null - && !useableAbilities.isEmpty()) { + if (!useableAbilities.isEmpty()) { activateAbility(useableAbilities, object, game); result = true; } @@ -1347,8 +1347,7 @@ public class HumanPlayer extends PlayerImpl { Zone zone = game.getState().getZone(object.getId()); if (zone != null) { LinkedHashMap useableAbilities = getUseableManaAbilities(object, zone, game); - if (useableAbilities != null - && !useableAbilities.isEmpty()) { + if (!useableAbilities.isEmpty()) { useableAbilities = ManaUtil.tryToAutoPay(unpaid, useableAbilities); // eliminates other abilities if one fits perfectly currentlyUnpaidMana = unpaid; activateAbility(useableAbilities, object, game); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java index dacc8d11e2c..253339652ef 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java @@ -914,4 +914,97 @@ public class MorphTest extends CardTestPlayerBase { Permanent akroma = getPermanent("Akroma, Angel of Fury"); Assert.assertTrue("Akroma has to be red", akroma.getColor(currentGame).isRed()); } + + @Test + public void test_LandWithMorph_PlayLand() { + // Morph {2} + addCard(Zone.HAND, playerA, "Zoetic Cavern"); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern"); + setChoice(playerA, "No"); // no morph (canPay for generic/colored mana returns true all the time, so xmage ask about face down cast) + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + // 1 action must be here ("no" option is restores on failed morph call in playLand) + //assertAllCommandsUsed(); + assertChoicesCount(playerA, 1); + + assertPermanentCount(playerA, "Zoetic Cavern", 1); + } + + @Test + public void test_LandWithMorph_Morph() { + // Morph {2} + addCard(Zone.HAND, playerA, "Zoetic Cavern"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true); + checkPlayableAbility("morph must be replaced by play ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Morph", false); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern"); + setChoice(playerA, "Yes"); // morph + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Zoetic Cavern", 0); + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } + + @Test + public void test_LandWithMorph_MorphAfterLand() { + removeAllCardsFromHand(playerA); + + // Morph {2} + addCard(Zone.HAND, playerA, "Zoetic Cavern"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // + addCard(Zone.HAND, playerA, "Island", 1); + + // play land first + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Island"); + + // morph ability (play as face down) calls from playLand method, so it visible for play land command + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true); + checkPlayableAbility("morph must be replaced by play ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Morph", false); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern"); + setChoice(playerA, "Yes"); // morph + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Island", 1); + assertPermanentCount(playerA, "Zoetic Cavern", 0); + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } + + @Test + public void test_LandWithMorph_MorphFromLibrary() { + removeAllCardsFromLibrary(playerA); + + // You may play lands and cast spells from the top of your library. + addCard(Zone.BATTLEFIELD, playerA, "Future Sight"); + // + // Morph {2} + addCard(Zone.LIBRARY, playerA, "Zoetic Cavern"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true); + checkPlayableAbility("morph must be replaced by play ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Morph", false); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern"); + setChoice(playerA, "Yes"); // morph + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Zoetic Cavern", 0); + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/DoublingCubeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/DoublingCubeTest.java index 7e2bb98b157..69744bf9437 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/DoublingCubeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/DoublingCubeTest.java @@ -31,9 +31,10 @@ public class DoublingCubeTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{3}, {T}:"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); assertManaPool(playerA, ManaType.COLORLESS, 4); - + assertAllCommandsUsed(); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithAsThoughManaTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithAsThoughManaTest.java new file mode 100644 index 00000000000..4330d9dd40b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithAsThoughManaTest.java @@ -0,0 +1,108 @@ +package org.mage.test.cards.split; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class CastSplitCardsWithAsThoughManaTest extends CardTestPlayerBase { + + @Test + public void test_AsThoughMana_Simple() { + // {1}{R} + // When Dire Fleet Daredevil enters the battlefield, exile target instant or sorcery card from an opponent’s graveyard. + // You may cast that card this turn, and you may spend mana as though it were mana of any type to cast that spell. + // If that card would be put into a graveyard this turn, exile it instead. + addCard(Zone.HAND, playerA, "Dire Fleet Daredevil", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.GRAVEYARD, playerB, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + // cast fleet + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 2); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dire Fleet Daredevil"); + addTarget(playerA, "Lightning Bolt"); + + // cast bolt with blue mana + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertLife(playerB, 20 - 3); + } + + @Test + public void test_AsThoughMana_Split_WearTear() { + // {1}{R} + // When Dire Fleet Daredevil enters the battlefield, exile target instant or sorcery card from an opponent’s graveyard. + // You may cast that card this turn, and you may spend mana as though it were mana of any type to cast that spell. + // If that card would be put into a graveyard this turn, exile it instead. + addCard(Zone.HAND, playerA, "Dire Fleet Daredevil", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + // Wear {1}{R} Destroy target artifact. + // Tear {W} Destroy target enchantment. + addCard(Zone.GRAVEYARD, playerB, "Wear // Tear", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, "Bident of Thassa", 1); // Legendary Enchantment Artifact + addCard(Zone.BATTLEFIELD, playerB, "Bow of Nylea", 1); // Legendary Enchantment Artifact + + // cast fleet + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 2); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dire Fleet Daredevil"); + addTarget(playerA, "Wear // Tear"); + + // cast Wear with black mana + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear", "Bident of Thassa"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Bident of Thassa", 1); + assertPermanentCount(playerB, "Bow of Nylea", 1); + } + + @Test + public void test_AsThoughMana_Split_CatchRelease() { + // {1}{R} + // When Dire Fleet Daredevil enters the battlefield, exile target instant or sorcery card from an opponent’s graveyard. + // You may cast that card this turn, and you may spend mana as though it were mana of any type to cast that spell. + // If that card would be put into a graveyard this turn, exile it instead. + addCard(Zone.HAND, playerA, "Dire Fleet Daredevil", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + // Catch {1}{U}{R} Gain control of target permanent until end of turn. Untap it. It gains haste until end of turn. + // Release {4}{R}{W} Each player sacrifices an artifact, a creature, an enchantment, a land, and a planeswalker. + addCard(Zone.GRAVEYARD, playerB, "Catch // Release", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + + // cast fleet + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 2); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dire Fleet Daredevil"); + addTarget(playerA, "Catch // Release"); + + // cast Catch with black mana + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Catch", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Balduvian Bears", 1); + assertPermanentCount(playerB, "Balduvian Bears", 0); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithFlashbackTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithFlashbackTest.java new file mode 100644 index 00000000000..d81189f6e06 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithFlashbackTest.java @@ -0,0 +1,70 @@ +package org.mage.test.cards.split; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class CastSplitCardsWithFlashbackTest extends CardTestPlayerBase { + + @Test + public void test_Flashback_Simple() { + // {1}{U} + // When Snapcaster Mage enters the battlefield, target instant or sorcery card in your graveyard gains flashback until end of turn. + addCard(Zone.HAND, playerA, "Snapcaster Mage", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + // + addCard(Zone.GRAVEYARD, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + // add flashback + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snapcaster Mage"); + addTarget(playerA, "Lightning Bolt"); + + // cast as flashback + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback", playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertLife(playerB, 20 - 3); + } + + @Test + public void test_Flashback_Split() { + // {1}{U} + // When Snapcaster Mage enters the battlefield, target instant or sorcery card in your graveyard gains flashback until end of turn. + addCard(Zone.HAND, playerA, "Snapcaster Mage", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + // + // Wear {1}{R} Destroy target artifact. + // Tear {W} Destroy target enchantment. + addCard(Zone.GRAVEYARD, playerA, "Wear // Tear", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerB, "Bident of Thassa", 1); // Legendary Enchantment Artifact + addCard(Zone.BATTLEFIELD, playerB, "Bow of Nylea", 1); // Legendary Enchantment Artifact + + // add flashback + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snapcaster Mage"); + addTarget(playerA, "Wear // Tear"); + + // cast as flashback + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback {1}{R}", "Bident of Thassa"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Bident of Thassa", 1); + assertPermanentCount(playerB, "Bow of Nylea", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index fdb665dd5ed..b0c26158742 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -963,7 +963,7 @@ public class TestPlayer implements Player { // you don't need to use stack command all the time, so some cast commands can be skiped to next check if (game.getStack().isEmpty()) { this.chooseStrictModeFailed("cast/activate", game, - "Can't find available command - " + action.getAction() + " (use checkPlayableAbility for non castable checks)", true); + "Can't find available command - " + action.getAction() + " (use checkPlayableAbility for \"non available\" checks)", true); } } // turn/step } @@ -1088,7 +1088,7 @@ public class TestPlayer implements Player { .map(a -> (a.getZone() + " -> " + a.getSourceObject(game).getIdName() + " -> " + (a.toString().length() > 0 - ? a.toString().substring(0, Math.min(20, a.toString().length()) - 1) + ? a.toString().substring(0, Math.min(20, a.toString().length())) : a.getClass().getSimpleName()) + "...")) .sorted() diff --git a/Mage/src/main/java/mage/cards/SplitCard.java b/Mage/src/main/java/mage/cards/SplitCard.java index 6845daa0401..4205f051584 100644 --- a/Mage/src/main/java/mage/cards/SplitCard.java +++ b/Mage/src/main/java/mage/cards/SplitCard.java @@ -110,11 +110,13 @@ public abstract class SplitCard extends CardImpl { public Abilities getAbilities() { Abilities allAbilites = new AbilitiesImpl<>(); for (Ability ability : super.getAbilities()) { + // ignore split abilities TODO: why it here, for GUI's cleanup in card texts? Maybe it can be removed if (ability instanceof SpellAbility - && ((SpellAbility) ability).getSpellAbilityType() != SpellAbilityType.SPLIT - && ((SpellAbility) ability).getSpellAbilityType() != SpellAbilityType.SPLIT_AFTERMATH) { - allAbilites.add(ability); + && (((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLIT + || ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLIT_AFTERMATH)) { + continue; } + allAbilites.add(ability); } allAbilites.addAll(leftHalfCard.getAbilities()); allAbilites.addAll(rightHalfCard.getAbilities()); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 5c8dd00ea54..9fa114ba307 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -246,7 +246,6 @@ public abstract class PlayerImpl implements Player, Serializable { this.storedBookmark = player.storedBookmark; this.topCardRevealed = player.topCardRevealed; - this.playersUnderYourControl.clear(); this.playersUnderYourControl.addAll(player.playersUnderYourControl); this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); @@ -254,7 +253,6 @@ public abstract class PlayerImpl implements Player, Serializable { this.isGameUnderControl = player.isGameUnderControl; this.turnController = player.turnController; - this.turnControllers.clear(); this.turnControllers.addAll(player.turnControllers); this.passed = player.passed; @@ -1190,18 +1188,20 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } ActivatedAbility playLandAbility = null; - boolean found = false; + boolean foundAlternative = false; for (Ability ability : card.getAbilities()) { // if cast for noMana no Alternative costs are allowed if ((ability instanceof AlternativeSourceCosts) || (ability instanceof OptionalAdditionalSourceCosts)) { - found = true; + foundAlternative = true; } if (ability instanceof PlayLandAbility) { playLandAbility = (ActivatedAbility) ability; } } - if (found) { + + // try alternative cast (face down) + if (foundAlternative) { SpellAbility spellAbility = new SpellAbility(null, "", game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); spellAbility.setControllerId(this.getId()); @@ -1210,9 +1210,11 @@ public abstract class PlayerImpl implements Player, Serializable { return true; } } + if (playLandAbility == null) { return false; } + //20091005 - 114.2a ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); if (ignoreTiming) { @@ -1496,110 +1498,29 @@ public abstract class PlayerImpl implements Player, Serializable { return useable; } - // Get the usable activated abilities for a *single card object*, that is, either a card or half of a split card. - // Also called on the whole split card but only passing the fuse ability and other whole-split-card shared abilities - // as candidates. - private void getUseableActivatedAbilitiesHalfImpl(MageObject object, Zone zone, Game game, Abilities candidateAbilites, - LinkedHashMap output) { - boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); - ManaOptions availableMana = null; - // ManaOptions availableMana = getManaAvailable(game); // can only be activated if mana calculation works flawless otherwise player can't play spells they could play if calculation would work correctly - // availableMana.addMana(manaPool.getMana()); - for (Ability ability : candidateAbilites) { - if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - if (ability.getZone().match(zone)) { - if (ability instanceof ActivatedAbility) { - if (ability instanceof ActivatedManaAbilityImpl) { - if (((ActivatedAbility) ability).canActivate(playerId, game).canActivate()) { - output.put(ability.getId(), (ActivatedAbility) ability); - } - } else if (canPlay(((ActivatedAbility) ability), availableMana, object, game)) { - output.put(ability.getId(), (ActivatedAbility) ability); - } - } else if (ability instanceof AlternativeSourceCosts) { - if (object.isLand()) { - for (Ability ability2 : object.getAbilities().copy()) { - if (ability2 instanceof PlayLandAbility) { - output.put(ability2.getId(), (ActivatedAbility) ability2); - } - } - } - } - } - } - } - if (zone != Zone.HAND) { - if (Zone.GRAVEYARD == zone && canPlayCardsFromGraveyard()) { - for (ActivatedAbility ability : candidateAbilites.getPlayableAbilities(Zone.HAND)) { - if (canUse - || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - if (ability.getZone().equals(zone) || ability.getZone().equals(Zone.HAND)) { - if (ability.canActivate(playerId, game).canActivate()) { - output.put(ability.getId(), ability); - } - } - } - } - } - if (zone != Zone.BATTLEFIELD) { - for (Ability ability : candidateAbilites) { - if (ability.getZone().equals(zone) || ability.getZone().equals(Zone.HAND)) { - if (game.getContinuousEffects().asThough(object.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, - null, - this.getId(), - game) - != null - // if anyone sees an issue with this code, please report it. Worked in my testing. ! - || game.getContinuousEffects().asThough(object.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, - ability, - this.getId(), - game) - != null) { - if (canUse - || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - ability.setControllerId(this.getId()); - if (ability instanceof ActivatedAbility - && ability.getZone().match(Zone.HAND) - && ((ActivatedAbility) ability).canActivate(playerId, game).canActivate()) { - output.put(ability.getId(), (ActivatedAbility) ability); - } - } - } - } - } - } - } - } - @Override public LinkedHashMap getUseableActivatedAbilities(MageObject object, Zone zone, Game game) { - // TODO: replace with getPlayableFromNonHandCardAll (uses for all tests) + LinkedHashMap useable = new LinkedHashMap<>(); boolean previousState = game.inCheckPlayableState(); game.setCheckPlayableState(true); - LinkedHashMap useable = new LinkedHashMap<>(); - if (object instanceof StackAbility) { // It may not be possible to activate abilities of stack abilities - return useable; - } - if (object instanceof SplitCard) { // TODO: use of getAbilities(game) - SplitCard splitCard = (SplitCard) object; - getUseableActivatedAbilitiesHalfImpl(splitCard.getLeftHalfCard(), - zone, game, splitCard.getLeftHalfCard().getAbilities(game), useable); - getUseableActivatedAbilitiesHalfImpl(splitCard.getRightHalfCard(), - zone, game, splitCard.getRightHalfCard().getAbilities(game), useable); - getUseableActivatedAbilitiesHalfImpl(splitCard, - zone, game, splitCard.getSharedAbilities(game), useable); - } else if (object instanceof Card) { - getUseableActivatedAbilitiesHalfImpl(object, - zone, game, ((Card) object).getAbilities(game), useable); - } else if (object != null) { - getUseableActivatedAbilitiesHalfImpl(object, - zone, game, object.getAbilities(), useable); - getOtherUseableActivatedAbilities(object, zone, game, useable); - } + try { + // It may not be possible to activate abilities of stack abilities + if (object instanceof StackAbility) { + return useable; + } - game.setCheckPlayableState(previousState); + // collect and filter playable activated abilities + List allPlayable = getPlayable(game, true, zone, false); + for (Ability ability : allPlayable) { + if (ability instanceof ActivatedAbility) { + if (object.hasAbility(ability, game)) { + useable.putIfAbsent(ability.getId(), (ActivatedAbility) ability); + } + } + } + } finally { + game.setCheckPlayableState(previousState); + } return useable; } @@ -3184,6 +3105,51 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } + protected ActivatedAbility findActivatedAbilityFromPlayable(Card card, ManaOptions manaAvailable, Ability ability, Game game) { + // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) + + if (ability instanceof ActivatedManaAbilityImpl) { + // mana ability + if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { + return (ActivatedManaAbilityImpl) ability; + } + } else if (ability instanceof AlternativeSourceCosts) { + // alternative cost must be replaced by real play ability + return findActivatedAbilityFromAlternativeSourceCost(card, manaAvailable, ability, game); + } else if (ability instanceof ActivatedAbility) { + // activated ability + if (canPlay((ActivatedAbility) ability, manaAvailable, card, game)) { + return (ActivatedAbility) ability; + } + } + + // non playable abilities like static + return null; + } + + protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(Card card, ManaOptions manaAvailable, Ability ability, Game game) { + // alternative cost must be replaced by real play ability + if (ability instanceof AlternativeSourceCosts) { + AlternativeSourceCosts altAbility = (AlternativeSourceCosts) ability; + if (card.isLand()) { + // land + // morph ability is static, so it must be replaced with play land ability (playLand search and try to use face down first) + if (canLandPlayAlternateSourceCostsAbility(card, manaAvailable, ability, game)) { // e.g. Land with Morph + Optional landAbility = card.getAbilities(game).stream().filter(a -> a instanceof PlayLandAbility).findFirst(); + if (landAbility.isPresent()) { + return (ActivatedAbility) landAbility.get(); + } + } + } else { + // creature and other + if (altAbility.isAvailable(card.getSpellAbility(), game)) { + return card.getSpellAbility(); + } + } + } + return null; + } + protected boolean canLandPlayAlternateSourceCostsAbility(Card sourceObject, ManaOptions available, Ability ability, Game game) { if (!(sourceObject instanceof Permanent)) { Ability sourceAbility = sourceObject.getAbilities().stream() @@ -3216,27 +3182,7 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } - private void getPlayableFromGraveyardCard(Game game, Card card, Abilities candidateAbilities, ManaOptions availableMana, List output) { - MageObjectReference permittingObject = game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, card.getSpellAbility(), this.getId(), game); - for (ActivatedAbility ability : candidateAbilities.getActivatedAbilities(Zone.ALL)) { - boolean possible = false; - if (ability.getZone().match(Zone.GRAVEYARD)) { - possible = true; - } else if (ability.getZone().match(Zone.HAND) - && (ability instanceof SpellAbility - || ability instanceof PlayLandAbility)) { - if (permittingObject != null || canPlayCardsFromGraveyard()) { - possible = true; - } - } - if (possible && canPlay(ability, availableMana, card, game)) { - output.add(ability); - } - } - } - - private void getPlayableFromNonHandCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List output) { + private void getPlayableFromCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List output) { if (fromZone == null) { return; } @@ -3244,16 +3190,16 @@ public abstract class PlayerImpl implements Player, Serializable { // BASIC abilities if (card instanceof SplitCard) { SplitCard splitCard = (SplitCard) card; - getPlayableFromNonHandCardSingle(game, fromZone, splitCard.getLeftHalfCard(), splitCard.getLeftHalfCard().getAbilities(), availableMana, output); - getPlayableFromNonHandCardSingle(game, fromZone, splitCard.getRightHalfCard(), splitCard.getRightHalfCard().getAbilities(), availableMana, output); - getPlayableFromNonHandCardSingle(game, fromZone, splitCard, splitCard.getSharedAbilities(game), availableMana, output); + getPlayableFromCardSingle(game, fromZone, splitCard.getLeftHalfCard(), splitCard.getLeftHalfCard().getAbilities(), availableMana, output); + getPlayableFromCardSingle(game, fromZone, splitCard.getRightHalfCard(), splitCard.getRightHalfCard().getAbilities(), availableMana, output); + getPlayableFromCardSingle(game, fromZone, splitCard, splitCard.getSharedAbilities(game), availableMana, output); } else if (card instanceof AdventureCard) { // adventure must use different card characteristics for different spells (main or adventure) AdventureCard adventureCard = (AdventureCard) card; - getPlayableFromNonHandCardSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(), availableMana, output); - getPlayableFromNonHandCardSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); + getPlayableFromCardSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(), availableMana, output); + getPlayableFromCardSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); } else { - getPlayableFromNonHandCardSingle(game, fromZone, card, card.getAbilities(), availableMana, output); + getPlayableFromCardSingle(game, fromZone, card, card.getAbilities(game), availableMana, output); } // DYNAMIC ADDED abilities @@ -3278,7 +3224,7 @@ public abstract class PlayerImpl implements Player, Serializable { } } - private void getPlayableFromNonHandCardSingle(Game game, Zone fromZone, Card card, Abilities candidateAbilities, ManaOptions availableMana, List output) { + private void getPlayableFromCardSingle(Game game, Zone fromZone, Card card, Abilities candidateAbilities, ManaOptions availableMana, List output) { // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) for (ActivatedAbility ability : candidateAbilities.getActivatedAbilities(Zone.ALL)) { boolean isPlaySpell = (ability instanceof SpellAbility); @@ -3339,8 +3285,13 @@ public abstract class PlayerImpl implements Player, Serializable { possibleToPlay = true; } - if (possibleToPlay && canPlay(ability, availableMana, card, game)) { - output.add(ability); + if (!possibleToPlay) { + continue; + } + + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); } } finally { ability.setControllerId(savedControllerId); @@ -3355,8 +3306,13 @@ public abstract class PlayerImpl implements Player, Serializable { public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { List playable = new ArrayList<>(); + if (shouldSkipGettingPlayable(game)) { + return playable; + } + + boolean previousState = game.inCheckPlayableState(); game.setCheckPlayableState(true); - if (!shouldSkipGettingPlayable(game)) { + try { ManaOptions availableMana = getManaAvailable(game); availableMana.addMana(manaPool.getMana()); for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { @@ -3390,26 +3346,9 @@ public abstract class PlayerImpl implements Player, Serializable { continue; } - // if have alternative cost - if (ability instanceof ActivatedAbility) { - // normal ability - if (canPlay((ActivatedAbility) ability, availableMana, card, game)) { - playable.add(ability); - } - } else if (ability instanceof AlternativeSourceCosts) { - if (card.isLand()) { - if (canLandPlayAlternateSourceCostsAbility(card, availableMana, ability, game)) { // e.g. Land with Morph - playable.add(ability); - } - } else if (card.isCreature()) { // e.g. makes a card available for play by Morph if the card may not be cast normally - if (!playable.contains(card.getSpellAbility())) { - if (((AlternativeSourceCosts) ability).isAvailable(card.getSpellAbility(), game)) { - playable.add(card.getSpellAbility()); - } - } - } - } else { - // unknown type + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); + if (playAbility != null && !playable.contains(playAbility)) { + playable.add(playAbility); } } } @@ -3418,14 +3357,14 @@ public abstract class PlayerImpl implements Player, Serializable { if (fromAll || fromZone == Zone.GRAVEYARD) { for (Card card : graveyard.getCards(game)) { - getPlayableFromNonHandCardAll(game, Zone.GRAVEYARD, card, availableMana, playable); + getPlayableFromCardAll(game, Zone.GRAVEYARD, card, availableMana, playable); } } if (fromAll || fromZone == Zone.EXILED) { for (ExileZone exile : game.getExile().getExileZones()) { for (Card card : exile.getCards(game)) { - getPlayableFromNonHandCardAll(game, Zone.EXILED, card, availableMana, playable); + getPlayableFromCardAll(game, Zone.EXILED, card, availableMana, playable); } } } @@ -3435,7 +3374,7 @@ public abstract class PlayerImpl implements Player, Serializable { for (Cards revealedCards : game.getState().getRevealed().values()) { for (Card card : revealedCards.getCards(game)) { // revealed cards can be from any zones - getPlayableFromNonHandCardAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); + getPlayableFromCardAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); } } } @@ -3444,7 +3383,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (fromAll || fromZone == Zone.OUTSIDE) { for (Cards companionCards : game.getState().getCompanion().values()) { for (Card card : companionCards.getCards(game)) { - getPlayableFromNonHandCardAll(game, Zone.OUTSIDE, card, availableMana, playable); + getPlayableFromCardAll(game, Zone.OUTSIDE, card, availableMana, playable); } } } @@ -3453,12 +3392,10 @@ public abstract class PlayerImpl implements Player, Serializable { if (fromAll || fromZone == Zone.LIBRARY) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { Player player = game.getPlayer(playerInRangeId); - if (player != null) { - if (/*player.isTopCardRevealed() &&*/player.getLibrary().hasCards()) { - Card card = player.getLibrary().getFromTop(game); - if (card != null) { - getPlayableFromNonHandCardAll(game, Zone.LIBRARY, card, availableMana, playable); - } + if (player != null && player.getLibrary().hasCards()) { + Card card = player.getLibrary().getFromTop(game); + if (card != null) { + getPlayableFromCardAll(game, Zone.LIBRARY, card, availableMana, playable); } } } @@ -3471,10 +3408,13 @@ public abstract class PlayerImpl implements Player, Serializable { // activated abilities from battlefield objects if (fromAll || fromZone == Zone.BATTLEFIELD) { for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - LinkedHashMap useableAbilities = getUseableActivatedAbilities(permanent, Zone.BATTLEFIELD, game); - for (ActivatedAbility ability : useableAbilities.values()) { - activatedUnique.putIfAbsent(ability.toString(), ability); - activatedAll.add(ability); + List battlePlayable = new ArrayList<>(); + getPlayableFromCardAll(game, Zone.BATTLEFIELD, permanent, availableMana, battlePlayable); + for (Ability ability : battlePlayable) { + if (ability instanceof ActivatedAbility) { + activatedUnique.putIfAbsent(ability.toString(), ability); + activatedAll.add(ability); + } } } } @@ -3483,8 +3423,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (fromAll || fromZone == Zone.STACK) { for (StackObject stackObject : game.getState().getStack()) { for (ActivatedAbility ability : stackObject.getAbilities().getActivatedAbilities(Zone.STACK)) { - if (ability != null - && canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) { + if (canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) { activatedUnique.put(ability.toString(), ability); activatedAll.add(ability); } @@ -3496,8 +3435,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (fromAll || fromZone == Zone.COMMAND) { for (CommandObject commandObject : game.getState().getCommand()) { for (ActivatedAbility ability : commandObject.getAbilities().getActivatedAbilities(Zone.COMMAND)) { - if (ability.isControlledBy(getId()) - && canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) { + if (canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) { activatedUnique.put(ability.toString(), ability); activatedAll.add(ability); } @@ -3510,8 +3448,10 @@ public abstract class PlayerImpl implements Player, Serializable { } else { playable.addAll(activatedAll); } + } finally { + game.setCheckPlayableState(previousState); } - game.setCheckPlayableState(false); + return playable; }