From 653a2dd7b2edc876fbd6cc5506add60f1e65d84a Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Fri, 24 Jul 2020 12:06:14 +0200 Subject: [PATCH] * Available mana calculation - Fixed a problem with repeatable mana converting abilities (e.g. Farrelite Priest) that were only considered once (#6698). --- Mage.Sets/src/mage/cards/e/EyeOfRamos.java | 7 +-- .../cards/mana/TappedForManaRelatedTest.java | 38 ++++++++++++ .../main/java/mage/abilities/AbilityImpl.java | 26 ++++---- .../java/mage/abilities/mana/ManaOptions.java | 60 +++++++++++++++---- .../main/java/mage/players/PlayerImpl.java | 18 +++--- 5 files changed, 109 insertions(+), 40 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/EyeOfRamos.java b/Mage.Sets/src/mage/cards/e/EyeOfRamos.java index 54a2e54f42..e0f56cada8 100644 --- a/Mage.Sets/src/mage/cards/e/EyeOfRamos.java +++ b/Mage.Sets/src/mage/cards/e/EyeOfRamos.java @@ -1,4 +1,3 @@ - package mage.cards.e; import java.util.UUID; @@ -18,11 +17,11 @@ import mage.constants.Zone; public final class EyeOfRamos extends CardImpl { public EyeOfRamos(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{3}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); - // {tap}: Add {U}. + // {T}: Add {U}. this.addAbility(new BlueManaAbility()); - + // Sacrifice Eye of Ramos: Add {U}. this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD, Mana.BlueMana(1), new SacrificeSourceCost())); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/TappedForManaRelatedTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/TappedForManaRelatedTest.java index 9767172efc..4bc52790e0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/TappedForManaRelatedTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/TappedForManaRelatedTest.java @@ -240,4 +240,42 @@ public class TappedForManaRelatedTest extends CardTestPlayerBase { Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); assertManaOptions("{Any}", manaOptions); } + + @Test + public void TestEyeOfRamos() { + setStrictChooseMode(true); + // {T}: Add {U}. + // Sacrifice Eye of Ramos: Add {U}. + addCard(Zone.BATTLEFIELD, playerA, "Eye of Ramos", 2); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + assertManaOptions("{U}{U}{U}{U}", manaOptions); + } + + @Test + public void TestFarrelitePriest() { + setStrictChooseMode(true); + // {1}: Add {W}. If this ability has been activated four or more times this turn, sacrifice Farrelite Priest at the beginning of the next end step. + addCard(Zone.BATTLEFIELD, playerA, "Farrelite Priest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 5, manaOptions.size()); + assertManaOptions("{W}{W}{W}{W}", manaOptions); + assertManaOptions("{W}{W}{W}{B}", manaOptions); + assertManaOptions("{W}{W}{B}{B}", manaOptions); + assertManaOptions("{W}{B}{B}{B}", manaOptions); + assertManaOptions("{B}{B}{B}{B}", manaOptions); + } } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index e58033ec89..66a2bf596f 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1,5 +1,9 @@ package mage.abilities; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; import mage.MageObject; import mage.abilities.costs.*; import mage.abilities.costs.common.PayLifeCost; @@ -8,6 +12,7 @@ import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ManaEffect; import mage.abilities.hint.Hint; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.cards.Card; @@ -30,12 +35,6 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import org.apache.log4j.Logger; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; -import mage.abilities.effects.common.ManaEffect; - /** * @author BetaSteward_at_googlemail.com */ @@ -54,7 +53,7 @@ public abstract class AbilityImpl implements Ability { protected ManaCosts manaCostsToPay; protected Costs costs; protected Costs optionalCosts; - protected Modes modes; // access to it by GetModes only (it's can be override by some abilities) + protected Modes modes; // access to it by GetModes only (it can be overridden by some abilities) protected Zone zone; protected String name; protected AbilityWord abilityWord; @@ -65,7 +64,7 @@ public abstract class AbilityImpl implements Ability { protected boolean activated = false; protected boolean worksFaceDown = false; protected int sourceObjectZoneChangeCounter; - protected List watchers = new ArrayList<>(); // access to it by GetWatchers only (it's can be override by some abilities) + protected List watchers = new ArrayList<>(); // access to it by GetWatchers only (it can be overridden by some abilities) protected List subAbilities = null; protected boolean canFizzle = true; protected TargetAdjuster targetAdjuster = null; @@ -1243,10 +1242,13 @@ public abstract class AbilityImpl implements Ability { } /** - * Dynamic cost modification for ability. - * Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState): - * 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes) - * 2. In real cast state it must check current use case (e.g. real selected targets and modes) + * Dynamic cost modification for ability.
+ * Example: if it need stack related info (like real targets) then must + * check two states (game.inCheckPlayableState):
+ * 1. In playable state it must check all possible use cases (e.g. allow to + * reduce on any available target and modes)
+ * 2. In real cast state it must check current use case (e.g. real selected + * targets and modes) * * @param costAdjuster */ diff --git a/Mage/src/main/java/mage/abilities/mana/ManaOptions.java b/Mage/src/main/java/mage/abilities/mana/ManaOptions.java index faa011b4a4..501471adcd 100644 --- a/Mage/src/main/java/mage/abilities/mana/ManaOptions.java +++ b/Mage/src/main/java/mage/abilities/mana/ManaOptions.java @@ -348,26 +348,34 @@ public class ManaOptions extends ArrayList { private boolean subtractCostAddMana(Mana cost, Mana manaToAdd, boolean onlyManaCosts, Mana currentMana) { boolean oldManaWasReplaced = false; // true if the newly created mana includes all mana possibilities of the old boolean repeatable = false; - if (manaToAdd.getAny() == 1 && manaToAdd.count() == 1 && onlyManaCosts) { + if ((manaToAdd.countColored() > 0 || manaToAdd.getAny() > 0) && manaToAdd.count() > 0 && onlyManaCosts) { // deactivated because it does cause loops TODO: Find reason repeatable = true; // only replace to any with mana costs only will be repeated if able } Mana prevMana = currentMana.copy(); - if (currentMana.includesMana(cost)) { // it can be paid + if (currentMana.includesMana(cost)) { // cost can be paid // generic mana costs can be paid with different colored mana, can lead to different color combinations if (cost.getGeneric() > 0 && cost.getGeneric() > (currentMana.getGeneric() + currentMana.getColorless())) { - Mana coloredCost = cost.copy(); - coloredCost.setGeneric(0); - currentMana.subtract(coloredCost); for (Mana payCombination : getPossiblePayCombinations(cost.getGeneric(), currentMana)) { - Mana newMana = currentMana.copy(); - newMana.subtract(payCombination); - newMana.add(manaToAdd); - Mana moreValuable = Mana.getMoreValuableMana(prevMana, newMana); - if (!prevMana.equals(moreValuable)) { - this.add(newMana); - if (moreValuable != null) { - oldManaWasReplaced = true; // the new mana includes all possibilities of the old one + Mana currentManaCopy = currentMana.copy(); + while (currentManaCopy.includesMana(payCombination)) { // loop for multiple usage if possible + boolean newCombinations = false; + + Mana newMana = currentManaCopy.copy(); + newMana.subtract(payCombination); + newMana.add(manaToAdd); + // Mana moreValuable = Mana.getMoreValuableMana(currentMana, newMana); + if (!isExistingManaCombination(newMana)) { + this.add(newMana); // add the new combination + newCombinations = true; // repeat the while as long there are new combinations and usage is repeatable + currentManaCopy = newMana.copy(); + Mana moreValuable = Mana.getMoreValuableMana(currentMana, newMana); + if (!oldManaWasReplaced && newMana.equals(moreValuable)) { + oldManaWasReplaced = true; // the new mana includes all possibilities of the old one, so no need to add it after return + } + } + if (!newCombinations || !repeatable) { + break; } } @@ -447,6 +455,16 @@ public class ManaOptions extends ArrayList { return payCombinations; } + private boolean isExistingManaCombination(Mana newMana) { + for (Mana mana : this) { + Mana moreValuable = Mana.getMoreValuableMana(mana, newMana); + if (mana.equals(moreValuable)) { + return true; + } + } + return false; + } + private void addManaCombination(Mana mana, Mana existingMana, List payCombinations, List payCombinationsStrings) { Mana newMana = existingMana.copy(); newMana.add(mana); @@ -478,4 +496,20 @@ public class ManaOptions extends ArrayList { } } } + + /** + * Checks if the given mana (cost) is already included in one available mana + * option + * + * @param mana + * @return + */ + public boolean enough(Mana mana) { + for (Mana avail : this) { + if (mana.enough(avail)) { + return true; + } + } + return false; + } } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 99d5a6d21c..f5bfff2e08 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3157,10 +3157,8 @@ public abstract class PlayerImpl implements Player, Serializable { game.getContinuousEffects().costModification(copyAbility, game); for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - for (Mana avail : availableMana) { - if (mana.enough(avail)) { - return true; - } + if (availableMana.enough(mana)) { + return true; } } } @@ -3196,10 +3194,8 @@ public abstract class PlayerImpl implements Player, Serializable { game.getContinuousEffects().costModification(copyAbility, game); for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - for (Mana avail : availableMana) { - if (mana.enough(avail)) { - return true; - } + if (availableMana.enough(mana)) { + return true; } } } @@ -3247,7 +3243,7 @@ public abstract class PlayerImpl implements Player, Serializable { 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)) { + if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { ActivatedAbility playAbility = null; if (object.isLand()) { playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); @@ -3263,8 +3259,8 @@ public abstract class PlayerImpl implements Player, Serializable { // 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) && ((MorphAbility) ability).isAvailable(playAbility, game); + if (ability instanceof MorphAbility && object instanceof Card && game.canPlaySorcery(getId())) { + canUse = canPlayCardByAlternateCost((Card) object, availableMana, ability, game); } else { canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions }