package mage.abilities.mana; import mage.ConditionalMana; import mage.Mana; import mage.abilities.Ability; import mage.constants.ManaType; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ManaEvent; import mage.game.events.TappedForManaEvent; import mage.players.Player; import org.apache.log4j.Logger; import java.util.*; /** *

* this class is used to build a list of all possible mana combinations it can * be used to find all the ways to pay a mana cost or all the different mana * combinations available to a player *

* TODO: Conditional Mana is not supported yet. * The mana adding removes the condition of conditional mana *

* A LinkedHashSet is used to get the performance benefits of automatic de-duplication of the Mana * to avoid performance issues related with manual de-duplication (see https://github.com/magefree/mage/issues/7710). * * @author BetaSteward_at_googlemail.com, JayDi85 */ public class ManaOptions extends LinkedHashSet { private static final Logger logger = Logger.getLogger(ManaOptions.class); public ManaOptions() { } protected ManaOptions(final ManaOptions options) { for (Mana mana : options) { this.add(mana.copy()); } } public void addMana(List abilities, Game game) { if (isEmpty()) { this.add(new Mana()); } if (abilities.isEmpty()) { return; // Do nothing } if (abilities.size() == 1) { //if there is only one mana option available add it to all the existing options List netManas = abilities.get(0).getNetMana(game); if (netManas.size() == 1) { checkManaReplacementAndTriggeredMana(abilities.get(0), game, netManas.get(0)); addMana(netManas.get(0)); addTriggeredMana(game, abilities.get(0)); } else if (netManas.size() > 1) { addManaVariation(netManas, abilities.get(0), game); } } else { // mana source has more than 1 ability //perform a union of all existing options and the new options List copy = new ArrayList<>(this); this.clear(); for (ActivatedManaAbilityImpl ability : abilities) { for (Mana netMana : ability.getNetMana(game)) { checkManaReplacementAndTriggeredMana(ability, game, netMana); for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) { SkipAddMana: for (Mana mana : copy) { Mana newMana = new Mana(); newMana.add(mana); newMana.add(triggeredManaVariation); for (Mana existingMana : this) { if (existingMana.equalManaValue(newMana)) { continue SkipAddMana; } Mana moreValuable = Mana.getMoreValuableMana(newMana, existingMana); if (moreValuable != null) { // only keep the more valuable mana existingMana.setToMana(moreValuable); continue SkipAddMana; } } this.add(newMana); } } } } } } private void addManaVariation(List netManas, ActivatedManaAbilityImpl ability, Game game) { Mana newMana; List copy = new ArrayList<>(this); this.clear(); for (Mana netMana : netManas) { for (Mana mana : copy) { if (!ability.hasTapCost() || checkManaReplacementAndTriggeredMana(ability, game, netMana)) { newMana = mana.copy(); newMana.add(netMana); this.add(newMana); } } } } private static List> getSimulatedTriggeredManaFromPlayer(Game game, Ability ability) { Player player = game.getPlayer(ability.getControllerId()); List> newList = new ArrayList<>(); if (player != null) { newList.addAll(player.getAvailableTriggeredMana()); player.getAvailableTriggeredMana().clear(); } return newList; } /** * Generates triggered mana and checks replacement of Tapped_For_Mana event. * Also generates triggered mana for MANA_ADDED event. * * @param ability * @param game * @param mana * @return false if mana production was completely replaced */ private boolean checkManaReplacementAndTriggeredMana(Ability ability, Game game, Mana mana) { if (ability.hasTapCost()) { ManaEvent event = new TappedForManaEvent(ability.getSourceId(), ability, ability.getControllerId(), mana, game); if (game.replaceEvent(event)) { return false; } game.fireEvent(event); } ManaEvent manaEvent = new ManaEvent(GameEvent.EventType.MANA_ADDED, ability.getSourceId(), ability, ability.getControllerId(), mana); manaEvent.setData(mana.toString()); game.fireEvent(manaEvent); return true; } /** * This adds the mana the abilities can produce to the possible mana variation. * * @param abilities * @param game * @return false if the costs could not be paid */ public boolean addManaWithCost(List abilities, Game game) { boolean wasUsable = false; if (isEmpty()) { this.add(new Mana()); // needed if this is the first available mana, otherwise looping over existing options woold not loop } if (!abilities.isEmpty()) { if (abilities.size() == 1) { List netManas = abilities.get(0).getNetMana(game); if (!netManas.isEmpty()) { // ability can produce mana ActivatedManaAbilityImpl ability = abilities.get(0); // The ability has no mana costs if (ability.getManaCosts().isEmpty()) { // No mana costs, so no mana to subtract from available if (netManas.size() == 1) { checkManaReplacementAndTriggeredMana(ability, game, netManas.get(0)); addMana(netManas.get(0)); addTriggeredMana(game, ability); } else { List copy = new ArrayList<>(this); this.clear(); for (Mana netMana : netManas) { checkManaReplacementAndTriggeredMana(ability, game, netMana); for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) { for (Mana mana : copy) { Mana newMana = mana.copy(); newMana.add(triggeredManaVariation); this.add(newMana); wasUsable = true; } } } } } else {// The ability has mana costs List copy = new ArrayList<>(this); this.clear(); Mana manaCosts = ability.getManaCosts().getMana(); for (Mana netMana : netManas) { checkManaReplacementAndTriggeredMana(ability, game, netMana); for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) { for (Mana startingMana : copy) { if (startingMana.includesMana(manaCosts)) { // can pay the mana costs to use the ability if (!subtractCostAddMana(manaCosts, triggeredManaVariation, ability.getCosts().isEmpty(), startingMana, ability, game)) { // the starting mana includes mana parts that the increased mana does not include, so add starting mana also as an option add(startingMana); } wasUsable = true; } else { // mana costs can't be paid so keep starting mana add(startingMana); } } } } } } } else { //perform a union of all existing options and the new options List copy = new ArrayList<>(this); this.clear(); for (ActivatedManaAbilityImpl ability : abilities) { List netManas = ability.getNetMana(game); if (ability.getManaCosts().isEmpty()) { for (Mana netMana : netManas) { checkManaReplacementAndTriggeredMana(ability, game, netMana); for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) { for (Mana mana : copy) { Mana newMana = mana.copy(); newMana.add(triggeredManaVariation); this.add(newMana); wasUsable = true; } } } } else { for (Mana netMana : netManas) { checkManaReplacementAndTriggeredMana(ability, game, netMana); for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) { for (Mana previousMana : copy) { for (Mana manaOption : ability.getManaCosts().getManaOptions()) { if (previousMana.includesMana(manaOption)) { // costs can be paid // subtractCostAddMana has side effects on {this}. Do not add wasUsable to the if-statement above wasUsable |= subtractCostAddMana(manaOption, triggeredManaVariation, ability.getCosts().isEmpty(), previousMana, ability, game); } } } } } } } } } if (logger.isTraceEnabled() && this.size() > 30) { logger.trace("ManaOptionsCosts " + this.size()); logger.trace("Abilities: " + abilities.toString()); } return wasUsable; } public boolean addManaPoolDependant(List abilities, Game game) { if (abilities.isEmpty() || abilities.size() != 1) { return false; } boolean wasUsable = false; ActivatedManaAbilityImpl ability = (ActivatedManaAbilityImpl) abilities.get(0); Mana manaCosts = ability.getManaCosts().getMana(); Set copy = copy(); this.clear(); for (Mana previousMana : copy) { if (previousMana.includesMana(manaCosts)) { // can pay the mana costs to use the ability for (Mana manaOption : ability.getManaCosts().getManaOptions()) { if (!subtractCostAddMana(manaOption, null, ability.getCosts().isEmpty(), previousMana, ability, game)) { // the starting mana includes mana parts that the increased mana does not include, so add starting mana also as an option add(previousMana); } } wasUsable = true; } else { // mana costs can't be paid so keep starting mana add(previousMana); } } return wasUsable; } public static List getTriggeredManaVariations(Game game, Ability ability, Mana baseMana) { List baseManaPlusTriggeredMana = new ArrayList<>(); baseManaPlusTriggeredMana.add(baseMana); List> availableTriggeredManaList = ManaOptions.getSimulatedTriggeredManaFromPlayer(game, ability); for (List availableTriggeredMana : availableTriggeredManaList) { if (availableTriggeredMana.size() == 1) { for (Mana prevMana : baseManaPlusTriggeredMana) { prevMana.add(availableTriggeredMana.get(0)); } } else if (availableTriggeredMana.size() > 1) { List copy = new ArrayList<>(baseManaPlusTriggeredMana); baseManaPlusTriggeredMana.clear(); for (Mana triggeredMana : availableTriggeredMana) { for (Mana prevMana : copy) { Mana newMana = new Mana(); newMana.add(prevMana); newMana.add(triggeredMana); baseManaPlusTriggeredMana.add(newMana); } } } } return baseManaPlusTriggeredMana; } private void addTriggeredMana(Game game, Ability ability) { List> netManaList = getSimulatedTriggeredManaFromPlayer(game, ability); for (List triggeredNetMana : netManaList) { if (triggeredNetMana.size() == 1) { addMana(triggeredNetMana.get(0)); } else if (triggeredNetMana.size() > 1) { // Add variations List copy = new ArrayList<>(this); this.clear(); for (Mana triggeredMana : triggeredNetMana) { for (Mana mana : copy) { Mana newMana = mana.copy(); newMana.add(triggeredMana); this.add(newMana); } } } } } /** * Adds the given mana value to all existing options * * @param addMana Mana to add to the existing options */ public void addMana(Mana addMana) { if (isEmpty()) { this.add(new Mana()); } if (addMana instanceof ConditionalMana) { List copy = new ArrayList<>(this); this.clear(); for (Mana mana : copy) { ConditionalMana condMana = ((ConditionalMana) addMana).copy(); condMana.add(mana); add(condMana); // Add mana as option with condition add(mana); // Add old mana without the condition } } else { for (Mana mana : this) { mana.add(addMana); } } } public void addMana(ManaOptions options) { if (isEmpty()) { this.add(new Mana()); } if (!options.isEmpty()) { if (options.size() == 1) { //if there is only one mana option available add it to all the existing options addMana(options.getAtIndex(0)); } else { //perform a union of all existing options and the new options List copy = new ArrayList<>(this); this.clear(); for (Mana addMana : options) { for (Mana mana : copy) { Mana newMana = mana.copy(); newMana.add(addMana); this.add(newMana); } } } } } public ManaOptions copy() { return new ManaOptions(this); } /** * Performs the simulation of a mana ability with costs * * @param cost cost to use the ability * @param manaToAdd one mana variation that can be added by using * this ability * @param onlyManaCosts flag to know if the costs are mana costs only (will try to use ability multiple times) * @param startingMana the mana available before the usage of the * ability * @return true if the new complete mana does replace the current mana completely */ private boolean subtractCostAddMana(Mana cost, Mana manaToAdd, boolean onlyManaCosts, final Mana startingMana, ManaAbility manaAbility, Game game) { boolean oldManaWasReplaced = false; // True if the newly created mana includes all mana possibilities of the old boolean repeatable = manaToAdd != null // TODO: re-write "only replace to any with mana costs only will be repeated if able" && onlyManaCosts && manaToAdd.countColored() > 0; boolean canHaveBetterValues; int maxRepeat = manaAbility.getMaxMoreActivationsThisTurn(game); Mana possibleMana = new Mana(); Mana improvedMana = new Mana(); // simulate multiple calls of mana abilities and replace mana pool by better values // example: {G}: Add one mana of any color for (Mana possiblePay : ManaOptions.getPossiblePayCombinations(cost, startingMana)) { improvedMana.setToMana(startingMana); int currentAttempt = 0; do { // loop until all mana replaced by better values canHaveBetterValues = false; currentAttempt++; // it's impossible to analyse all payment order (pay {R} for {1}, {Any} for {G}, etc) // so use simple cost simulation by subtract possibleMana.setToMana(improvedMana); possibleMana.subtract(possiblePay); if (!possibleMana.isValid()) { //if (possibleMana.canPayMana(possiblePay)) { // TODO: canPayMana/includesMana uses better pay logic, so subtract can be improved somehow //logger.warn("found un-supported payment combination: pool " + possibleMana + ", cost " + possiblePay); //} continue; } // find resulting mana (it can have multiple options) List addingManaOptions = (manaToAdd != null) ? Collections.singletonList(manaToAdd) : manaAbility.getNetMana(game, possibleMana); for (Mana addingMana : addingManaOptions) { // TODO: is it bugged on addingManaOptions.size() > 1 (adding multiple times)? possibleMana.add(addingMana); // already found that combination before if (this.contains(possibleMana)) { continue; } // found new combination - add it to final options this.add(possibleMana.copy()); canHaveBetterValues = true; // remove old worse options if (possibleMana.isMoreValuableThan(improvedMana)) { oldManaWasReplaced = true; if (!startingMana.equalManaValue(improvedMana)) { this.removeEqualMana(improvedMana); } } improvedMana.setToMana(possibleMana); } } while (repeatable && (currentAttempt < maxRepeat) && canHaveBetterValues && improvedMana.includesMana(possiblePay)); } return oldManaWasReplaced; } /** * @param manaCost * @param manaAvailable * @return */ public static Set getPossiblePayCombinations(Mana manaCost, Mana manaAvailable) { Set payCombinations = new HashSet<>(); Mana fixedMana = manaCost.copy(); // If there are no generic costs, then there is only one combination of colors available to pay for it. // That combination is itself (fixedMana) if (manaCost.getGeneric() == 0) { payCombinations.add(fixedMana); return payCombinations; } // Get the available mana left to pay for the cost after subtracting the non-generic parts of the cost from it fixedMana.setGeneric(0); Mana manaAfterFixedPayment = manaAvailable.copy(); manaAfterFixedPayment.subtract(fixedMana); // handle generic mana costs if (manaAvailable.countColored() == 0) { payCombinations.add(Mana.ColorlessMana(manaCost.getGeneric())); } else { ManaType[] manaTypes = ManaType.values(); // Do not move, here for optimization reasons. Mana manaToPayFrom = new Mana(); for (int i = 0; i < manaCost.getGeneric(); i++) { List existingManas = new ArrayList<>(payCombinations.size()); if (i > 0) { existingManas.addAll(payCombinations); payCombinations.clear(); } else { existingManas.add(new Mana()); } for (Mana existingMana : existingManas) { manaToPayFrom.setToMana(manaAfterFixedPayment); manaToPayFrom.subtract(existingMana); for (ManaType manaType : manaTypes) { existingMana.increase(manaType); if (manaToPayFrom.get(manaType) > 0 && !payCombinations.contains(existingMana)) { payCombinations.add(existingMana.copy()); manaToPayFrom.decrease(manaType); } existingMana.decrease(manaType); } // Pay with any only needed if colored payment was not possible // NOTE: This isn't in the for loop since ManaType doesn't include ANY. existingMana.increaseAny(); if (payCombinations.isEmpty() && manaToPayFrom.getAny() > 0) { payCombinations.add(existingMana.copy()); manaToPayFrom.decreaseAny(); } existingMana.decreaseAny(); } } } for (Mana mana : payCombinations) { mana.add(fixedMana); } // All mana values in here are of length 5 return payCombinations; } public boolean removeEqualMana(Mana manaToRemove) { boolean result = false; for (Iterator iterator = this.iterator(); iterator.hasNext(); ) { Mana next = iterator.next(); if (next.equalManaValue(manaToRemove)) { iterator.remove(); result = true; } } return result; } /** * Remove fully included variations. * E.g. If both {R} and {R}{W} are in this, then {R} will be removed. */ public void removeFullyIncludedVariations() { List that = new ArrayList<>(this); Set list = new HashSet<>(); for (int i = this.size() - 1; i >= 0; i--) { String s; if (that.get(i) instanceof ConditionalMana) { s = that.get(i).toString() + ((ConditionalMana) that.get(i)).getConditionString(); } else { s = that.get(i).toString(); } if (s.isEmpty()) { this.remove(i); } else if (list.contains(s)) { // remove duplicated this.remove(i); } else { list.add(s); } } // Remove fully included variations for (int i = this.size() - 1; i >= 0; i--) { for (int ii = 0; ii < i; ii++) { Mana moreValuable = Mana.getMoreValuableMana(that.get(i), that.get(ii)); if (moreValuable != null) { that.get(ii).setToMana(moreValuable); that.remove(i); break; } } } this.clear(); this.addAll(that); } /** * Checks if the given mana (cost) is already included in one available mana * option * * @param mana * @return */ public boolean enough(Mana mana) { // 117.5. Some costs are represented by {0}, or are reduced to {0}. The action necessary for a player to pay // such a cost is the player’s acknowledgment that he or she is paying it. Even though such a cost requires // no resources, it’s not automatically paid. if (mana.count() == 0) { return true; } for (Mana avail : this) { if (mana.enough(avail)) { return true; } } return false; } @Override public String toString() { Iterator it = this.iterator(); if (!it.hasNext()) { return "[]"; } StringBuilder sb = new StringBuilder(); sb.append('['); for (; ; ) { Mana mana = it.next(); sb.append(mana.toString()); if (mana instanceof ConditionalMana) { sb.append(((ConditionalMana) mana).getConditionString()); } if (!it.hasNext()) { return sb.append(']').toString(); } sb.append(',').append(' '); } } /** * Utility function to get a Mana from ManaOptions at the specified position. * Since the implementation uses a LinkedHashSet the ordering of the items is preserved. *

* NOTE: Do not use in tight loops as performance of the lookup is much worse than * for ArrayList (the previous superclass of ManaOptions). */ public Mana getAtIndex(int i) { if (i < 0 || i >= this.size()) { throw new IndexOutOfBoundsException(); } Iterator itr = this.iterator(); while (itr.hasNext()) { if (i == 0) { return itr.next(); } else { itr.next(); // Ignore the value i--; } } return null; // Not sure how we'd ever get here, but leave just in case since IDE complains. } } /** * from: https://stackoverflow.com/a/35000727/7983747 * * @author Gili Tzabari */ final class Comparators { /** * Verify that a comparator is transitive. * * @param the type being compared * @param comparator the comparator to test * @param elements the elements to test against * @throws AssertionError if the comparator is not transitive */ public static void verifyTransitivity(Comparator comparator, Collection elements) { for (T first : elements) { for (T second : elements) { int result1 = comparator.compare(first, second); int result2 = comparator.compare(second, first); if (result1 != -result2 && !(result1 == 0 && result1 == result2)) { // Uncomment the following line to step through the failed case comparator.compare(first, second); comparator.compare(second, first); throw new AssertionError("compare(" + first + ", " + second + ") == " + result1 + " but swapping the parameters returns " + result2); } } } for (T first : elements) { for (T second : elements) { int firstGreaterThanSecond = comparator.compare(first, second); if (firstGreaterThanSecond <= 0) { continue; } for (T third : elements) { int secondGreaterThanThird = comparator.compare(second, third); if (secondGreaterThanThird <= 0) { continue; } int firstGreaterThanThird = comparator.compare(first, third); if (firstGreaterThanThird <= 0) { // Uncomment the following line to step through the failed case comparator.compare(first, second); comparator.compare(second, third); comparator.compare(first, third); throw new AssertionError("compare(" + first + ", " + second + ") > 0, " + "compare(" + second + ", " + third + ") > 0, but compare(" + first + ", " + third + ") == " + firstGreaterThanThird); } } } } } private Comparators() { } }