package mage.abilities.mana; import mage.Mana; import mage.abilities.Ability; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ManaEvent; import mage.players.Player; import org.apache.log4j.Logger; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import mage.ConditionalMana; /** * @author BetaSteward_at_googlemail.com *

* 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 */ public class ManaOptions extends ArrayList { private static final Logger logger = Logger.getLogger(ManaOptions.class); public ManaOptions() { } public 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()) { 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 = copy(); 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); } } } } } } forceManaDeduplication(); } private void addManaVariation(List netManas, ActivatedManaAbilityImpl ability, Game game) { List copy = copy(); this.clear(); for (Mana netMana : netManas) { for (Mana mana : copy) { if (!ability.hasTapCost() || checkManaReplacementAndTriggeredMana(ability, game, netMana)) { Mana newMana = new Mana(); newMana.add(mana); newMana.add(netMana); this.add(newMana); } } } forceManaDeduplication(); } 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 ManaEvent(GameEvent.EventType.TAPPED_FOR_MANA, ability.getSourceId(), ability, ability.getControllerId(), mana); 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 * variabtion. * * @param abilities * @param game * @return false if the costs could not be paid */ public boolean addManaWithCost(List abilities, Game game) { boolean wasUsable = false; int replaces = 0; 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.size() > 0) { // 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 = copy(); this.clear(); for (Mana netMana : netManas) { checkManaReplacementAndTriggeredMana(ability, game, netMana); for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) { for (Mana mana : copy) { Mana newMana = new Mana(); newMana.add(mana); newMana.add(triggeredManaVariation); this.add(newMana); wasUsable = true; } } } } } else {// The ability has mana costs List copy = copy(); this.clear(); for (Mana netMana : netManas) { checkManaReplacementAndTriggeredMana(ability, game, netMana); for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) { for (Mana prevMana : copy) { Mana startingMana = prevMana.copy(); Mana manaCosts = ability.getManaCosts().getMana(); 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(prevMana); } wasUsable = true; } else { // mana costs can't be paid so keep starting mana add(prevMana); } } } } } } } else { //perform a union of all existing options and the new options List copy = copy(); 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 = new Mana(); newMana.add(mana); 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) { CombineWithExisting: for (Mana manaOption : ability.getManaCosts().getManaOptions()) { if (previousMana.includesMana(manaOption)) { // costs can be paid wasUsable |= subtractCostAddMana(manaOption, triggeredManaVariation, ability.getCosts().isEmpty(), previousMana, ability, game); } } } } } } } } } if (this.size() > 30 || replaces > 30) { logger.trace("ManaOptionsCosts " + this.size() + " Ign:" + replaces + " => " + this.toString()); logger.trace("Abilities: " + abilities.toString()); } forceManaDeduplication(); return wasUsable; } public boolean addManaPoolDependant(List abilities, Game game) { boolean wasUsable = false; if (!abilities.isEmpty()) { if (abilities.size() == 1) { ActivatedManaAbilityImpl ability = (ActivatedManaAbilityImpl) abilities.get(0); List copy = copy(); this.clear(); for (Mana previousMana : copy) { Mana startingMana = previousMana.copy(); Mana manaCosts = ability.getManaCosts().getMana(); if (startingMana.includesMana(manaCosts)) { // can pay the mana costs to use the ability for (Mana manaOption : ability.getManaCosts().getManaOptions()) { if (!subtractCostAddMana(manaOption, null, 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(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 = copy(); this.clear(); for (Mana triggeredMana : triggeredNetMana) { for (Mana mana : copy) { Mana newMana = new Mana(); newMana.add(mana); newMana.add(triggeredMana); this.add(newMana); } } } } forceManaDeduplication(); } /** * 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) { ManaOptions copy = this.copy(); 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); } } forceManaDeduplication(); } 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.get(0)); } else { //perform a union of all existing options and the new options List copy = copy(); this.clear(); for (Mana addMana : options) { for (Mana mana : copy) { Mana newMana = new Mana(); newMana.add(mana); newMana.add(addMana); this.add(newMana); } } } } forceManaDeduplication(); } private void forceManaDeduplication() { // memory overflow protection - force de-duplication on too much mana sources // bug example: https://github.com/magefree/mage/issues/6938 // use it after new mana adding if (this.size() > 1000) { this.removeDuplicated(); } } 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 * @param currentMana the mana available before the usage of the * ability * @param oldManaWasReplaced returns the info if the new complete mana does * replace the current mana completely */ private boolean subtractCostAddMana(Mana cost, Mana manaToAdd, boolean onlyManaCosts, Mana currentMana, ManaAbility manaAbility, Game game) { boolean oldManaWasReplaced = false; // true if the newly created mana includes all mana possibilities of the old boolean repeatable = false; if (manaToAdd != null && (manaToAdd.countColored() > 0 || manaToAdd.getAny() > 0) && manaToAdd.count() > 0 && onlyManaCosts) { repeatable = true; // only replace to any with mana costs only will be repeated if able } for (Mana payCombination : ManaOptions.getPossiblePayCombinations(cost, currentMana)) { Mana currentManaCopy = currentMana.copy(); // copy start mana because in iteration it will be updated while (currentManaCopy.includesMana(payCombination)) { // loop for multiple usage if possible boolean newCombinations = false; if (manaToAdd == null) { Mana newMana = currentManaCopy.copy(); newMana.subtract(payCombination); for (Mana mana : manaAbility.getNetMana(game, newMana)) { // get the mana to add from the ability related to the currently generated possible mana pool newMana.add(mana); 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 Mana moreValuable = Mana.getMoreValuableMana(currentManaCopy, newMana); if (newMana.equals(moreValuable)) { oldManaWasReplaced = true; // the new mana includes all possibilities of the old one, so no need to add it after return if (!currentMana.equalManaValue(currentManaCopy)) { this.removeEqualMana(currentManaCopy); } } currentManaCopy = newMana.copy(); } } } else { Mana newMana = currentManaCopy.copy(); newMana.subtract(payCombination); newMana.add(manaToAdd); 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 Mana moreValuable = Mana.getMoreValuableMana(currentManaCopy, newMana); if (newMana.equals(moreValuable)) { oldManaWasReplaced = true; // the new mana includes all possible mana of the old one, so no need to add it after return if (!currentMana.equalManaValue(currentManaCopy)) { this.removeEqualMana(currentManaCopy); } } currentManaCopy = newMana.copy(); } } if (!newCombinations || !repeatable) { break; } } } forceManaDeduplication(); return oldManaWasReplaced; } /** * @param number of generic mana * @param manaAvailable * @return */ public static List getPossiblePayCombinations(Mana manaCost, Mana manaAvailable) { List payCombinations = new ArrayList<>(); List payCombinationsStrings = new ArrayList<>(); // handle fixed mana costs Mana fixedMana = manaCost.copy(); if (manaCost.getGeneric() == 0) { payCombinations.add(fixedMana); return payCombinations; } fixedMana.setGeneric(0); Mana manaAfterFixedPayment = manaAvailable.copy(); manaAfterFixedPayment.subtract(fixedMana); // handle generic mana costs if (manaAvailable.countColored() > 0) { for (int i = 0; i < manaCost.getGeneric(); i++) { List existingManas = new ArrayList<>(); if (i > 0) { existingManas.addAll(payCombinations); payCombinations.clear(); payCombinationsStrings.clear(); } else { existingManas.add(new Mana()); } for (Mana existingMana : existingManas) { Mana manaToPayFrom = manaAfterFixedPayment.copy(); manaToPayFrom.subtract(existingMana); if (manaToPayFrom.getBlack() > 0 && !payCombinationsStrings.contains(existingMana.toString() + Mana.BlackMana(1).toString())) { manaToPayFrom.subtract(Mana.BlackMana(1)); ManaOptions.addManaCombination(Mana.BlackMana(1), existingMana, payCombinations, payCombinationsStrings); } if (manaToPayFrom.getBlue() > 0 && !payCombinationsStrings.contains(existingMana.toString() + Mana.BlueMana(1).toString())) { manaToPayFrom.subtract(Mana.BlueMana(1)); ManaOptions.addManaCombination(Mana.BlueMana(1), existingMana, payCombinations, payCombinationsStrings); } if (manaToPayFrom.getGreen() > 0 && !payCombinationsStrings.contains(existingMana.toString() + Mana.GreenMana(1).toString())) { manaToPayFrom.subtract(Mana.GreenMana(1)); ManaOptions.addManaCombination(Mana.GreenMana(1), existingMana, payCombinations, payCombinationsStrings); } if (manaToPayFrom.getRed() > 0 && !payCombinationsStrings.contains(existingMana.toString() + Mana.RedMana(1).toString())) { manaToPayFrom.subtract(Mana.RedMana(1)); ManaOptions.addManaCombination(Mana.RedMana(1), existingMana, payCombinations, payCombinationsStrings); } if (manaToPayFrom.getWhite() > 0 && !payCombinationsStrings.contains(existingMana.toString() + Mana.WhiteMana(1).toString())) { manaToPayFrom.subtract(Mana.WhiteMana(1)); ManaOptions.addManaCombination(Mana.WhiteMana(1), existingMana, payCombinations, payCombinationsStrings); } if (manaToPayFrom.getColorless() > 0 && !payCombinationsStrings.contains(existingMana.toString() + Mana.ColorlessMana(1).toString())) { manaToPayFrom.subtract(Mana.ColorlessMana(1)); ManaOptions.addManaCombination(Mana.ColorlessMana(1), existingMana, payCombinations, payCombinationsStrings); } // Pay with any only needed if colored payment was not possible if (payCombinations.isEmpty() && manaToPayFrom.getAny() > 0 && !payCombinationsStrings.contains(existingMana.toString() + Mana.AnyMana(1).toString())) { manaToPayFrom.subtract(Mana.AnyMana(1)); ManaOptions.addManaCombination(Mana.AnyMana(1), existingMana, payCombinations, payCombinationsStrings); } } } } else { payCombinations.add(Mana.ColorlessMana(manaCost.getGeneric())); } for (Mana mana : payCombinations) { mana.add(fixedMana); } 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; } 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; } public static void addManaCombination(Mana mana, Mana existingMana, List payCombinations, List payCombinationsStrings) { Mana newMana = existingMana.copy(); newMana.add(mana); payCombinations.add(newMana); payCombinationsStrings.add(newMana.toString()); } public void removeDuplicated() { Set list = new HashSet<>(); for (int i = this.size() - 1; i >= 0; i--) { String s; if (this.get(i) instanceof ConditionalMana) { s = this.get(i).toString() + ((ConditionalMana) this.get(i)).getConditionString(); } else { s = this.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 // TODO: research too many manas and freeze (put 1 card to slow down, put 3 cards to freeze here) // battlefield:Human:Cascading Cataracts:1 for (int i = this.size() - 1; i >= 0; i--) { for (int ii = 0; ii < i; ii++) { Mana moreValuable = Mana.getMoreValuableMana(this.get(i), this.get(ii)); if (moreValuable != null) { this.get(ii).setToMana(moreValuable); this.remove(i); break; } } } } /** * 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; } @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(' '); } } }