From 5ae041f39a80edd76550ba57924a8ef210e7ec10 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 23 Jun 2020 09:29:38 +0400 Subject: [PATCH] Additional tests for morph and #6680 --- Mage.Sets/src/mage/cards/a/Abolish.java | 5 +- .../cards/abilities/keywords/MorphTest.java | 46 +++++++ .../UseAlternateSourceCostsTest.java | 84 +++++++++++- .../CostReduceWithConditionTest.java | 53 ++++++++ .../main/java/mage/players/PlayerImpl.java | 124 ++++++++---------- 5 files changed, 235 insertions(+), 77 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceWithConditionTest.java diff --git a/Mage.Sets/src/mage/cards/a/Abolish.java b/Mage.Sets/src/mage/cards/a/Abolish.java index 47d2170ba01..e619b5c4aed 100644 --- a/Mage.Sets/src/mage/cards/a/Abolish.java +++ b/Mage.Sets/src/mage/cards/a/Abolish.java @@ -1,4 +1,3 @@ - package mage.cards.a; import mage.abilities.costs.AlternativeCostSourceAbility; @@ -16,7 +15,6 @@ import mage.target.common.TargetCardInHand; import java.util.UUID; /** - * * @author Backfir3 */ public final class Abolish extends CardImpl { @@ -28,8 +26,7 @@ public final class Abolish extends CardImpl { } public Abolish(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{1}{W}{W}"); - + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}{W}"); // You may discard a Plains card rather than pay Abolish's mana cost. this.addAbility(new AlternativeCostSourceAbility(new DiscardTargetCost(new TargetCardInHand(filterCost)))); 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 7e3333567aa..7f2802ac699 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 @@ -1065,4 +1065,50 @@ public class MorphTest extends CardTestPlayerBase { execute(); assertAllCommandsUsed(); } + + @Test + public void test_MorphWithCostReductionMustBePlayable_NormalCondition() { + // {1}{U} creature + // Morph {1}{U} (You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost.) + // When Willbender is turned face up, change the target of target spell or ability with a single target. + addCard(Zone.HAND, playerA, "Willbender"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + // + // Creature spells you cast cost {1} less to cast. + addCard(Zone.BATTLEFIELD, playerA, "Nylea, Keen-Eyed"); + + checkPlayableAbility("can", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Willbender", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Willbender"); + setChoice(playerA, "Yes"); // morph + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } + + @Test + public void test_MorphWithCostReductionMustBePlayable_MorphCondition() { + // {1}{U} creature + // Morph {1}{U} (You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost.) + // When Willbender is turned face up, change the target of target spell or ability with a single target. + addCard(Zone.HAND, playerA, "Willbender"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + // + // Face-down creature spells you cast cost {1} less to cast. + addCard(Zone.BATTLEFIELD, playerA, "Dream Chisel"); + + checkPlayableAbility("can", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Willbender", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Willbender"); + setChoice(playerA, "Yes"); // morph + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java index ae8ea580ce2..1ecc0ebc70d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java @@ -1,13 +1,12 @@ - package org.mage.test.cards.cost.alternate; import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { @@ -75,4 +74,85 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Gray Ogre", 1); assertGraveyardCount(playerA, "Lightning Bolt", 1); } + + + @Test + public void test_Playable_WithMana() { + // {1}{W}{W} instant + // You may discard a Plains card rather than pay Abolish's mana cost. + // Destroy target artifact or enchantment. + addCard(Zone.HAND, playerA, "Abolish"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.HAND, playerA, "Plains", 1); // discard cost + // + addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr"); + + checkPlayableAbility("can", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", "Alpha Myr"); + setChoice(playerA, "Yes"); // use alternative cost + setChoice(playerA, "Plains"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Alpha Myr", 1); + assertTappedCount("Plains", false, 3); // must discard 1 instead tap + } + + @Test + public void test_Playable_WithoutMana() { + // {1}{W}{W} instant + // You may discard a Plains card rather than pay Abolish's mana cost. + // Destroy target artifact or enchantment. + addCard(Zone.HAND, playerA, "Abolish"); + //addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.HAND, playerA, "Plains", 1); // discard cost + // + addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr"); + + checkPlayableAbility("can", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", "Alpha Myr"); + setChoice(playerA, "Yes"); // use alternative cost + setChoice(playerA, "Plains"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Alpha Myr", 1); + } + + @Test + public void test_Playable_WithoutManaAndCost() { + // {1}{W}{W} instant + // You may discard a Plains card rather than pay Abolish's mana cost. + // Destroy target artifact or enchantment. + addCard(Zone.HAND, playerA, "Abolish"); + //addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + //addCard(Zone.HAND, playerA, "Plains", 1); // discard cost + // + addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr"); + + // can't see as playable (no mana for normal, no discard for alternative) + checkPlayableAbility("can't", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + @Ignore // TODO: make test to check combo of alternative cost and cost reduction effects + public void test_Playable_WithCostReduction() { + addCard(Zone.HAND, playerA, "xxx"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceWithConditionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceWithConditionTest.java new file mode 100644 index 00000000000..cd733ed0f85 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceWithConditionTest.java @@ -0,0 +1,53 @@ +package org.mage.test.cards.cost.modification; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class CostReduceWithConditionTest extends CardTestPlayerBase { + + @Test + public void test_PriceOfFame_Normal() { + // {3}{B} + // This spell costs {2} less to cast if it targets a legendary creature. + // Destroy target creature. + addCard(Zone.HAND, playerA, "Price of Fame", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Price of Fame", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Balduvian Bears", 1); + } + + @Test + @Ignore + // TODO: implement workaround like putToStackAsNonPlayable for abilities, see https://github.com/magefree/mage/issues/6685 + public void test_PriceOfFame_Reduce() { + // {3}{B} + // This spell costs {2} less to cast if it targets a legendary creature. + // Destroy target creature. + addCard(Zone.HAND, playerA, "Price of Fame", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4 - 2); + addCard(Zone.BATTLEFIELD, playerB, "Anje Falkenrath", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Price of Fame", "Anje Falkenrath"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Anje Falkenrath", 1); + } +} diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index d2066015eef..55125a4602d 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2852,7 +2852,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public ManaOptions getManaAvailable(Game game) { - ManaOptions available = new ManaOptions(); + ManaOptions availableMana = new ManaOptions(); List> sourceWithoutManaCosts = new ArrayList<>(); List> sourceWithCosts = new ArrayList<>(); @@ -2884,17 +2884,17 @@ public abstract class PlayerImpl implements Player, Serializable { } for (Abilities manaAbilities : sourceWithoutManaCosts) { - available.addMana(manaAbilities, game); + availableMana.addMana(manaAbilities, game); } for (Abilities manaAbilities : sourceWithCosts) { - available.removeDuplicated(); - available.addManaWithCost(manaAbilities, game); + availableMana.removeDuplicated(); + availableMana.addManaWithCost(manaAbilities, game); } // remove duplicated variants (see ManaOptionsTest for info - when that rises) - available.removeDuplicated(); + availableMana.removeDuplicated(); - return available; + return availableMana; } // returns only mana producers that don't require mana payment @@ -2958,18 +2958,18 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param ability - * @param available if null, it won't be checked if enough mana is available + * @param availableMana if null, it won't be checked if enough mana is available * @param sourceObject * @param game * @return */ - protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) { + protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { if (!(ability instanceof ActivatedManaAbilityImpl)) { ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability if (!copy.canActivate(playerId, game).canActivate()) { return false; } - if (available != null) { + if (availableMana != null) { game.getContinuousEffects().costModification(copy, game); } boolean canBeCastRegularly = true; @@ -2980,31 +2980,8 @@ public abstract class PlayerImpl implements Player, Serializable { canBeCastRegularly = false; } if (canBeCastRegularly) { - ManaOptions abilityOptions = copy.getMinimumCostToActivate(playerId, game); - if (abilityOptions.isEmpty()) { + if (canPayMinimumManaCost(copy, availableMana, game)) { return true; - } else { - if (available == null) { - return true; - } - MageObjectReference permittingObject = game.getContinuousEffects().asThough(copy.getSourceId(), - AsThoughEffectType.SPEND_OTHER_MANA, copy, copy.getControllerId(), game); - for (Mana mana : abilityOptions) { - for (Mana avail : available) { - // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, - // but that code processing it as any color, need to test and fix another use cases - // (example: Sunglasses of Urza - may spend white mana as though it were red mana) - - // - // add tests for non any color like Sunglasses of Urza - if (permittingObject != null && mana.count() <= avail.count()) { - return true; - } - if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost - return true; - } - } - } } } @@ -3036,12 +3013,42 @@ public abstract class PlayerImpl implements Player, Serializable { } // ALTERNATIVE COST from source card (any AlternativeSourceCosts) - return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), available, copy, game); + return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); } return false; } - protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions available, Ability ability, Game game) { + protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { + ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); + if (abilityOptions.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + MageObjectReference permittingObject = game.getContinuousEffects().asThough(ability.getSourceId(), + AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); + for (Mana mana : abilityOptions) { + for (Mana avail : availableMana) { + // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, + // but that code processing it as any color, need to test and fix another use cases + // (example: Sunglasses of Urza - may spend white mana as though it were red mana) + + // + // add tests for non any color like Sunglasses of Urza + if (permittingObject != null && mana.count() <= avail.count()) { + return true; + } + if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost + return true; + } + } + } + } + return false; + } + + protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { if (sourceObject != null && !(sourceObject instanceof Permanent)) { for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { // if cast for noMana no Alternative costs are allowed @@ -3058,11 +3065,11 @@ public abstract class PlayerImpl implements Player, Serializable { if (manaCosts.isEmpty()) { return true; } else { - if (available == null) { + if (availableMana == null) { return true; } for (Mana mana : manaCosts.getOptions()) { - for (Mana avail : available) { + for (Mana avail : availableMana) { if (mana.enough(avail)) { return true; } @@ -3090,7 +3097,7 @@ public abstract class PlayerImpl implements Player, Serializable { return true; } else { for (Mana mana : manaCosts.getOptions()) { - for (Mana avail : available) { + for (Mana avail : availableMana) { if (mana.enough(avail)) { return true; } @@ -3105,10 +3112,10 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } - protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions manaAvailable, Ability ability, Game game) { + protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { // special mana to pay spell cost - ManaOptions manaFull = manaAvailable.copy(); + ManaOptions manaFull = availableMana.copy(); if (ability instanceof SpellAbility) { for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() .filter(a -> a instanceof AlternateManaPaymentAbility) @@ -3139,7 +3146,7 @@ public abstract class PlayerImpl implements Player, Serializable { return null; } - protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions manaAvailable, Ability ability, Game game) { + protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { // return play ability that can activate AlternativeSourceCosts if (ability instanceof AlternativeSourceCosts && !(object instanceof Permanent)) { ActivatedAbility playAbility = null; @@ -3153,13 +3160,14 @@ public abstract class PlayerImpl implements Player, Serializable { } // 707.4.Objects that are cast face down are turned face down before they are put onto the stack - // (e.g. no lands per turn limit, no cast restrictions, another cost, etc) - // so morph must checks only mana payment here + // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc + // Even mana cost can't be checked here without lookahead + // So make it available all the time boolean canUse; if (ability instanceof MorphAbility) { - canUse = game.canPlaySorcery(playerId) && canPayAlternateSourceCostsAbility(object, playAbility, manaAvailable, ability, game); + canUse = game.canPlaySorcery(playerId) && ((MorphAbility) ability).isAvailable(playAbility, game); } else { - canUse = canPlay(playAbility, manaAvailable, object, game); // canPlay already checks alternative source costs and all conditions + canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions } if (canUse) { @@ -3169,32 +3177,6 @@ public abstract class PlayerImpl implements Player, Serializable { return null; } - protected boolean canPayAlternateSourceCostsAbility(MageObject sourceObject, Ability sourceAbility, ManaOptions available, Ability alternativeAbility, Game game) { - if (sourceAbility != null && ((AlternativeSourceCosts) alternativeAbility).isAvailable(sourceAbility, game)) { - if (alternativeAbility.getCosts().canPay(alternativeAbility, sourceObject.getId(), this.getId(), game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : alternativeAbility.getCosts()) { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - for (Mana mana : manaCosts.getOptions()) { - for (Mana avail : available) { - if (mana.enough(avail)) { - return true; - } - } - } - } - } - } - return false; - } - private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { if (fromZone == null || object == null) { return;