Refactor: Significant speed-up for ManaOptions (#9233)

This commit is contained in:
Alex Vasile 2022-10-04 00:08:20 -04:00 committed by GitHub
parent 23a4d2640b
commit 55a6acba22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 782 additions and 408 deletions

View file

@ -12,9 +12,12 @@ import mage.game.Game;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
* If subclassing and adding extra field, you must be sure to override equals() and hashCode to include the new fields.
*
* @author nantuko
*/
public class ConditionalMana extends Mana implements Serializable, Emptiable {
@ -188,11 +191,17 @@ public class ConditionalMana extends Mana implements Serializable, Emptiable {
}
public String getConditionString() {
String condStr = "[";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (Condition condition : conditions) {
condStr += condition.getManaText();
sb.append('{');
sb.append(condition.getManaText());
sb.append('}');
}
return condStr + "]";
sb.append(']');
return sb.toString();
}
public void add(ManaType manaType, int amount) {
@ -220,4 +229,42 @@ public class ConditionalMana extends Mana implements Serializable, Emptiable {
break;
}
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), conditions, staticText, scope, manaProducerId, manaProducerOriginalId);
}
@Override
public boolean equals(Object o) {
// Check Mana.equals(). If that's isn't equal no need to check further.
if (!super.equals(o)) {
return false;
}
ConditionalMana that = (ConditionalMana) o;
if (!Objects.equals(this.staticText, that.staticText)) {
return false;
}
if (!Objects.equals(this.manaProducerId, that.manaProducerId)) {
return false;
}
if (!Objects.equals(this.manaProducerOriginalId, that.manaProducerOriginalId)) {
return false;
}
if (!Objects.equals(this.scope, that.scope)) {
return false;
}
if (this.conditions == null || that.conditions == null
|| this.conditions.size() != that.conditions.size()) {
return false;
}
for (int i = 0; i < this.conditions.size(); i++) {
if (!(Objects.equals(this.conditions.get(i), that.conditions.get(i)))) {
return false;
}
}
return true;
}
}

View file

@ -1,5 +1,6 @@
package mage;
import mage.abilities.condition.Condition;
import mage.constants.ColoredManaSymbol;
import mage.constants.ManaType;
import mage.filter.FilterMana;
@ -8,10 +9,8 @@ import mage.util.Copyable;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.function.BiFunction;
/**
* WARNING, all mana operations must use overflow check, see usage of CardUtil.addWithOverflowCheck and same methods
@ -320,32 +319,51 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
}
/**
* Increases the given mana by one.
* Increases this mana by one of the passed in ManaType.
*
* @param manaType the type of mana to increase by one.
*/
public void increase(ManaType manaType) {
increaseOrDecrease(manaType, true);
}
/**
* Decreases this mana by onw of the passed in ManaType.
*
* @param manaType the type of mana to increase by one.
*/
public void decrease(ManaType manaType) {
increaseOrDecrease(manaType, false);
}
/**
* Helper function for increase and decrease to not have the code duplicated.
* @param manaType
* @param increase
*/
private void increaseOrDecrease(ManaType manaType, boolean increase) {
BiFunction<Integer, Integer, Integer> overflowIncOrDec = increase ? CardUtil::overflowInc : CardUtil::overflowDec;
switch (manaType) {
case WHITE:
white = CardUtil.overflowInc(white, 1);
white = overflowIncOrDec.apply(white, 1);
break;
case BLUE:
blue = CardUtil.overflowInc(blue, 1);
blue = overflowIncOrDec.apply(blue, 1);
break;
case BLACK:
black = CardUtil.overflowInc(black, 1);
black = overflowIncOrDec.apply(black, 1);
break;
case RED:
red = CardUtil.overflowInc(red, 1);
red = overflowIncOrDec.apply(red, 1);
break;
case GREEN:
green = CardUtil.overflowInc(green, 1);
green = overflowIncOrDec.apply(green, 1);
break;
case COLORLESS:
colorless = CardUtil.overflowInc(colorless, 1);
colorless = overflowIncOrDec.apply(colorless, 1);
break;
case GENERIC:
generic = CardUtil.overflowInc(generic, 1);
generic = overflowIncOrDec.apply(generic, 1);
break;
}
}
@ -399,6 +417,13 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
colorless = CardUtil.overflowInc(colorless, 1);
}
public void increaseAny() {
any = CardUtil.overflowInc(any, 1);
}
public void decreaseAny() {
any = CardUtil.overflowDec(any, 1);
}
/**
* Subtracts the passed in mana values from this instance.
*
@ -477,14 +502,11 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
* @return the total count of all combined mana.
*/
public int count() {
return white
+ blue
+ black
+ red
+ green
+ generic
+ colorless
+ any;
int sum = countColored();
sum = CardUtil.overflowInc(sum, generic);
sum = CardUtil.overflowInc(sum, colorless);
return sum;
}
/**
@ -493,12 +515,13 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
* @return the total count of all colored mana.
*/
public int countColored() {
return white
+ blue
+ black
+ red
+ green
+ any;
int sum = CardUtil.overflowInc(white, blue);
sum = CardUtil.overflowInc(sum, black);
sum = CardUtil.overflowInc(sum, red);
sum = CardUtil.overflowInc(sum, green);
sum = CardUtil.overflowInc(sum, any);
return sum;
}
/**
@ -1103,7 +1126,7 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
case GREEN:
return green;
case COLORLESS:
return CardUtil.overflowInc(generic, colorless);
return CardUtil.overflowInc(generic, colorless); // TODO: This seems like a mistake
}
return 0;
}
@ -1206,77 +1229,114 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
}
public boolean isMoreValuableThan(Mana that) {
// Use of == is intentional since getMoreValuableMana returns one of its inputs.
return this == Mana.getMoreValuableMana(this, that);
}
/**
* Returns the mana that is more colored or has a greater amount but does
* not contain one less mana in any color but generic.
* not contain one less mana in any type but generic.
* <p>
* See tests ManaTest.moreValuableManaTest for several examples
*
* Examples:
* {1} and {R} -> {R}
* {2} and {1}{W} -> {1}{W}
* {3} and {1}{W} -> {1}{W}
* {1}{W}{R} and {G}{W}{R} -> {G}{W}{R}
* {G}{W}{R} and {G}{W}{R} -> {G}{W}{R}
* {G}{W}{R} and {G}{W}{R} -> null
* {G}{W}{B} and {G}{W}{R} -> null
* {C} and {ANY} -> null
*
* @param mana1 The 1st mana to compare.
* @param mana2 The 2nd mana to compare.
* @return The greater of the two manas, or null if they're the same
* @return The greater of the two manas, or null if they're the same OR they cannot be compared
*/
public static Mana getMoreValuableMana(final Mana mana1, final Mana mana2) {
String conditionString1 = "";
String conditionString2 = "";
if (mana1 instanceof ConditionalMana) {
conditionString1 = ((ConditionalMana) mana1).getConditionString();
}
if (mana2 instanceof ConditionalMana) {
conditionString2 = ((ConditionalMana) mana2).getConditionString();
}
if (!conditionString1.equals(conditionString2)) {
if (mana1.equals(mana2)) {
return null;
}
boolean mana1IsConditional = mana1 instanceof ConditionalMana;
boolean mana2IsConditional = mana2 instanceof ConditionalMana;
if (mana1IsConditional != mana2IsConditional) {
return null;
}
if (mana1IsConditional) {
List<Condition> conditions1 = ((ConditionalMana) mana1).getConditions();
List<Condition> conditions2 = ((ConditionalMana) mana2).getConditions();
if (!Objects.equals(conditions1, conditions2)) {
return null;
}
}
// Set one mana as moreMana and one as lessMana.
Mana moreMana;
Mana lessMana;
if (mana2.countColored() > mana1.countColored() || mana2.getAny() > mana1.getAny() || mana2.count() > mana1.count()) {
if (mana2.any > mana1.any
|| mana2.colorless > mana1.colorless
|| mana2.countColored() > mana1.countColored()
|| (mana2.countColored() == mana1.countColored()
&& mana2.colorless == mana1.colorless
&& mana2.count() > mana1.count())) {
moreMana = mana2;
lessMana = mana1;
} else {
moreMana = mana1;
lessMana = mana2;
}
int anyDiff = CardUtil.overflowDec(mana2.getAny(), mana1.getAny());
if (lessMana.getWhite() > moreMana.getWhite()) {
anyDiff = CardUtil.overflowDec(anyDiff, CardUtil.overflowDec(lessMana.getWhite(), moreMana.getWhite()));
if (anyDiff < 0) {
return null;
}
}
if (lessMana.getRed() > moreMana.getRed()) {
anyDiff = CardUtil.overflowDec(anyDiff, CardUtil.overflowDec(lessMana.getRed(), moreMana.getRed()));
if (anyDiff < 0) {
return null;
}
}
if (lessMana.getGreen() > moreMana.getGreen()) {
anyDiff = CardUtil.overflowDec(anyDiff, CardUtil.overflowDec(lessMana.getGreen(), moreMana.getGreen()));
if (anyDiff < 0) {
return null;
}
}
if (lessMana.getBlue() > moreMana.getBlue()) {
anyDiff = CardUtil.overflowDec(anyDiff, CardUtil.overflowDec(lessMana.getBlue(), moreMana.getBlue()));
if (anyDiff < 0) {
return null;
}
}
if (lessMana.getBlack() > moreMana.getBlack()) {
anyDiff = CardUtil.overflowDec(anyDiff, CardUtil.overflowDec(lessMana.getBlack(), moreMana.getBlack()));
if (anyDiff < 0) {
return null;
}
}
if (lessMana.getColorless() > moreMana.getColorless()) {
return null; // Any (color) can't produce colorless mana
}
if (lessMana.getAny() > moreMana.getAny()) {
if (lessMana.any > moreMana.any) {
return null;
}
if (lessMana.colorless > moreMana.colorless) {
return null; // Any (color) can't produce colorless mana
}
int anyDiff = CardUtil.overflowDec(moreMana.any, lessMana.any);
int whiteDiff = CardUtil.overflowDec(lessMana.white, moreMana.white);
if (whiteDiff > 0) {
anyDiff = CardUtil.overflowDec(anyDiff, whiteDiff);
if (anyDiff < 0) {
return null;
}
}
int redDiff = CardUtil.overflowDec(lessMana.red, moreMana.red);
if (redDiff > 0) {
anyDiff = CardUtil.overflowDec(anyDiff, redDiff);
if (anyDiff < 0) {
return null;
}
}
int greenDiff = CardUtil.overflowDec(lessMana.green, moreMana.green);
if (greenDiff > 0) {
anyDiff = CardUtil.overflowDec(anyDiff, greenDiff);
if (anyDiff < 0) {
return null;
}
}
int blueDiff = CardUtil.overflowDec(lessMana.blue, moreMana.blue);
if (blueDiff > 0) {
anyDiff = CardUtil.overflowDec(anyDiff, blueDiff);
if (anyDiff < 0) {
return null;
}
}
int blackDiff = CardUtil.overflowDec(lessMana.black, moreMana.black);
if (blackDiff > 0) {
anyDiff = CardUtil.overflowDec(anyDiff, blackDiff);
if (anyDiff < 0) {
return null;
}
}
return moreMana;
}
@ -1323,9 +1383,27 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
&& any == mana.any;
}
/**
* Hardcoding here versus using Objects.hash in order to increase performance since this is
* called thousands of times by {@link mage.abilities.mana.ManaOptions#addManaWithCost(List, Game)}
*
* @return
*/
@Override
public int hashCode() {
return Objects.hash(white, blue, black, red, green, generic, colorless, any, flag);
long result = 1;
result = 31 * result + white;
result = 31 * result + blue;
result = 31 * result + black;
result = 31 * result + red;
result = 31 * result + green;
result = 31 * result + generic;
result = 31 * result + colorless;
result = 31 * result + any;
result = 31 * result + (flag ? 1 : 0);
return Long.hashCode(result);
}
/**

View file

@ -23,7 +23,7 @@ public interface Condition extends Serializable {
boolean apply(Game game, Ability source);
default String getManaText() {
return "{" + this.getClass().getSimpleName() + "}";
return this.getClass().getSimpleName();
}
default boolean caresAboutManaColor() {

View file

@ -104,7 +104,7 @@ public class AddManaInAnyCombinationEffect extends ManaEffect {
allPossibleMana.addMana(currentPossibleMana);
}
allPossibleMana.removeDuplicated();
allPossibleMana.removeFullyIncludedVariations();
return new ArrayList<>(allPossibleMana);
} else {

View file

@ -164,7 +164,7 @@ public class ConvokeAbility extends SimpleStaticAbility implements AlternateMana
options.addMana(permMana);
});
options.removeDuplicated();
options.removeFullyIncludedVariations();
return options;
}
}

View file

@ -115,7 +115,7 @@ public class OfferingAbility extends StaticAbility implements AlternateManaPayme
}
);
additionalManaOptionsForThisAbility.removeDuplicated();
additionalManaOptionsForThisAbility.removeFullyIncludedVariations();
return additionalManaOptionsForThisAbility;
}
}

View file

@ -3,11 +3,13 @@ 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 mage.util.TreeNode;
import org.apache.log4j.Logger;
import java.util.*;
@ -19,10 +21,14 @@ import java.util.*;
* be used to find all the ways to pay a mana cost or all the different mana
* combinations available to a player
* <p>
* TODO: Conditional Mana is not supported yet. The mana adding removes the
* condition of conditional mana
* TODO: Conditional Mana is not supported yet.
* The mana adding removes the condition of conditional mana
* <p>
* 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).
*
*/
public class ManaOptions extends ArrayList<Mana> {
public class ManaOptions extends LinkedHashSet<Mana> {
private static final Logger logger = Logger.getLogger(ManaOptions.class);
@ -39,69 +45,68 @@ public class ManaOptions extends ArrayList<Mana> {
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<Mana> 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);
}
if (abilities.isEmpty()) {
return; // Do nothing
}
} else { // mana source has more than 1 ability
//perform a union of all existing options and the new options
List<Mana> 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;
}
if (abilities.size() == 1) {
//if there is only one mana option available add it to all the existing options
List<Mana> 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<Mana> 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);
}
this.add(newMana);
}
}
}
}
}
forceManaDeduplication();
}
private void addManaVariation(List<Mana> netManas, ActivatedManaAbilityImpl ability, Game game) {
List<Mana> copy = copy();
Mana newMana;
List<Mana> copy = new ArrayList<>(this);
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 = mana.copy();
newMana.add(netMana);
this.add(newMana);
}
}
}
forceManaDeduplication();
}
private static List<List<Mana>> getSimulatedTriggeredManaFromPlayer(Game game, Ability ability) {
@ -138,8 +143,7 @@ public class ManaOptions extends ArrayList<Mana> {
}
/**
* This adds the mana the abilities can produce to the possible mana
* variabtion.
* This adds the mana the abilities can produce to the possible mana variation.
*
* @param abilities
* @param game
@ -147,14 +151,13 @@ public class ManaOptions extends ArrayList<Mana> {
*/
public boolean addManaWithCost(List<ActivatedManaAbilityImpl> 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<Mana> netManas = abilities.get(0).getNetMana(game);
if (netManas.size() > 0) { // ability can produce mana
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
@ -163,14 +166,13 @@ public class ManaOptions extends ArrayList<Mana> {
addMana(netManas.get(0));
addTriggeredMana(game, ability);
} else {
List<Mana> copy = copy();
List<Mana> 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 = new Mana();
newMana.add(mana);
Mana newMana = new Mana(mana);
newMana.add(triggeredManaVariation);
this.add(newMana);
wasUsable = true;
@ -179,23 +181,22 @@ public class ManaOptions extends ArrayList<Mana> {
}
}
} else {// The ability has mana costs
List<Mana> copy = copy();
List<Mana> 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 prevMana : copy) {
Mana startingMana = prevMana.copy();
Mana manaCosts = ability.getManaCosts().getMana();
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(prevMana);
add(startingMana);
}
wasUsable = true;
} else {
// mana costs can't be paid so keep starting mana
add(prevMana);
add(startingMana);
}
}
}
@ -204,7 +205,7 @@ public class ManaOptions extends ArrayList<Mana> {
}
} else {
//perform a union of all existing options and the new options
List<Mana> copy = copy();
List<Mana> copy = new ArrayList<>(this);
this.clear();
for (ActivatedManaAbilityImpl ability : abilities) {
List<Mana> netManas = ability.getNetMana(game);
@ -213,8 +214,7 @@ public class ManaOptions extends ArrayList<Mana> {
checkManaReplacementAndTriggeredMana(ability, game, netMana);
for (Mana triggeredManaVariation : getTriggeredManaVariations(game, ability, netMana)) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
Mana newMana = new Mana(mana);
newMana.add(triggeredManaVariation);
this.add(newMana);
wasUsable = true;
@ -226,9 +226,9 @@ public class ManaOptions extends ArrayList<Mana> {
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
// 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);
}
}
@ -241,41 +241,41 @@ public class ManaOptions extends ArrayList<Mana> {
}
}
if (this.size() > 30 || replaces > 30) {
logger.trace("ManaOptionsCosts " + this.size() + " Ign:" + replaces + " => " + this.toString());
if (logger.isTraceEnabled() && this.size() > 30) {
logger.trace("ManaOptionsCosts " + this.size());
logger.trace("Abilities: " + abilities.toString());
}
forceManaDeduplication();
return wasUsable;
}
public boolean addManaPoolDependant(List<ActivatedManaAbilityImpl> abilities, Game game) {
if (abilities.isEmpty() || abilities.size() != 1) {
return false;
}
boolean wasUsable = false;
if (!abilities.isEmpty()) {
if (abilities.size() == 1) {
ActivatedManaAbilityImpl ability = (ActivatedManaAbilityImpl) abilities.get(0);
List<Mana> 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
ActivatedManaAbilityImpl ability = (ActivatedManaAbilityImpl) abilities.get(0);
Mana manaCosts = ability.getManaCosts().getMana();
Set<Mana> 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;
}
@ -311,20 +311,17 @@ public class ManaOptions extends ArrayList<Mana> {
addMana(triggeredNetMana.get(0));
} else if (triggeredNetMana.size() > 1) {
// Add variations
List<Mana> copy = copy();
List<Mana> copy = new ArrayList<>(this);
this.clear();
for (Mana triggeredMana : triggeredNetMana) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
Mana newMana = new Mana(mana);
newMana.add(triggeredMana);
this.add(newMana);
}
}
}
}
forceManaDeduplication();
}
/**
@ -337,7 +334,7 @@ public class ManaOptions extends ArrayList<Mana> {
this.add(new Mana());
}
if (addMana instanceof ConditionalMana) {
ManaOptions copy = this.copy();
List<Mana> copy = new ArrayList<>(this);
this.clear();
for (Mana mana : copy) {
ConditionalMana condMana = ((ConditionalMana) addMana).copy();
@ -351,8 +348,6 @@ public class ManaOptions extends ArrayList<Mana> {
mana.add(addMana);
}
}
forceManaDeduplication();
}
public void addMana(ManaOptions options) {
@ -362,32 +357,20 @@ public class ManaOptions extends ArrayList<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));
addMana(options.getAtIndex(0));
} else {
//perform a union of all existing options and the new options
List<Mana> copy = copy();
List<Mana> copy = new ArrayList<>(this);
this.clear();
for (Mana addMana : options) {
for (Mana mana : copy) {
Mana newMana = new Mana();
newMana.add(mana);
Mana newMana = new Mana(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() {
@ -406,62 +389,46 @@ public class ManaOptions extends ArrayList<Mana> {
* @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
}
private boolean subtractCostAddMana(Mana cost, Mana manaToAdd, boolean onlyManaCosts, final Mana currentMana, 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.getAny() > 0 || manaToAdd.countColored() > 0)
&& manaToAdd.count() > 0;
boolean newCombinations;
Mana newMana = new Mana();
Mana currentManaCopy = new Mana();
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;
currentManaCopy.setToMana(currentMana); // copy start mana because in iteration it will be updated
do { // loop for multiple usage if possible
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
newMana.setToMana(currentManaCopy);
newMana.subtract(payCombination);
// Get the mana to iterate over.
// If manaToAdd is specified add it, otherwise add the mana produced by the mana ability
List<Mana> manasToAdd = (manaToAdd != null) ? Collections.singletonList(manaToAdd) : manaAbility.getNetMana(game, newMana);
for (Mana mana : manasToAdd) {
newMana.add(mana);
if (this.contains(newMana)) {
continue;
}
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();
this.add(newMana.copy()); // add the new combination
newCombinations = true; // repeat the while as long there are new combinations and usage is repeatable
if (newMana.isMoreValuableThan(currentManaCopy)) {
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);
}
}
} 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();
}
currentManaCopy.setToMana(newMana);
}
if (!newCombinations || !repeatable) {
break;
}
}
} while (repeatable && newCombinations && currentManaCopy.includesMana(payCombination));
}
forceManaDeduplication();
return oldManaWasReplaced;
}
@ -470,86 +437,72 @@ public class ManaOptions extends ArrayList<Mana> {
* @param manaAvailable
* @return
*/
public static List<Mana> getPossiblePayCombinations(Mana manaCost, Mana manaAvailable) {
List<Mana> payCombinations = new ArrayList<>();
List<String> payCombinationsStrings = new ArrayList<>();
// handle fixed mana costs
public static Set<Mana> getPossiblePayCombinations(Mana manaCost, Mana manaAvailable) {
Set<Mana> 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) {
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<Mana> existingManas = new ArrayList<>();
List<Mana> existingManas = new ArrayList<>(payCombinations.size());
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);
String manaString = existingMana.toString();
if (manaToPayFrom.getBlack() > 0 && !payCombinationsStrings.contains(manaString + Mana.BlackMana(1))) {
manaToPayFrom.subtract(Mana.BlackMana(1));
ManaOptions.addManaCombination(Mana.BlackMana(1), existingMana, payCombinations, payCombinationsStrings);
}
if (manaToPayFrom.getBlue() > 0 && !payCombinationsStrings.contains(manaString + Mana.BlueMana(1).toString())) {
manaToPayFrom.subtract(Mana.BlueMana(1));
ManaOptions.addManaCombination(Mana.BlueMana(1), existingMana, payCombinations, payCombinationsStrings);
}
if (manaToPayFrom.getGreen() > 0 && !payCombinationsStrings.contains(manaString + Mana.GreenMana(1).toString())) {
manaToPayFrom.subtract(Mana.GreenMana(1));
ManaOptions.addManaCombination(Mana.GreenMana(1), existingMana, payCombinations, payCombinationsStrings);
}
if (manaToPayFrom.getRed() > 0 && !payCombinationsStrings.contains(manaString + Mana.RedMana(1).toString())) {
manaToPayFrom.subtract(Mana.RedMana(1));
ManaOptions.addManaCombination(Mana.RedMana(1), existingMana, payCombinations, payCombinationsStrings);
}
if (manaToPayFrom.getWhite() > 0 && !payCombinationsStrings.contains(manaString + Mana.WhiteMana(1).toString())) {
manaToPayFrom.subtract(Mana.WhiteMana(1));
ManaOptions.addManaCombination(Mana.WhiteMana(1), existingMana, payCombinations, payCombinationsStrings);
}
if (manaToPayFrom.getColorless() > 0 && !payCombinationsStrings.contains(manaString + Mana.ColorlessMana(1).toString())) {
manaToPayFrom.subtract(Mana.ColorlessMana(1));
ManaOptions.addManaCombination(Mana.ColorlessMana(1), existingMana, payCombinations, payCombinationsStrings);
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
if (payCombinations.isEmpty() && manaToPayFrom.getAny() > 0 && !payCombinationsStrings.contains(manaString + Mana.AnyMana(1).toString())) {
manaToPayFrom.subtract(Mana.AnyMana(1));
ManaOptions.addManaCombination(Mana.AnyMana(1), existingMana, payCombinations, payCombinationsStrings);
// 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();
}
}
} else {
payCombinations.add(Mana.ColorlessMana(manaCost.getGeneric()));
}
for (Mana mana : payCombinations) {
mana.add(fixedMana);
}
// All mana values in here are of length 5
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<Mana> iterator = this.iterator(); iterator.hasNext(); ) {
@ -562,22 +515,20 @@ public class ManaOptions extends ArrayList<Mana> {
return result;
}
public static void addManaCombination(Mana mana, Mana existingMana, List<Mana> payCombinations, List<String> payCombinationsStrings) {
Mana newMana = existingMana.copy();
newMana.add(mana);
payCombinations.add(newMana);
payCombinationsStrings.add(newMana.toString());
}
public void removeDuplicated() {
/**
* Remove fully included variations.
* E.g. If both {R} and {R}{W} are in this, then {R} will be removed.
*/
public void removeFullyIncludedVariations() {
List<Mana> that = new ArrayList<>(this);
Set<String> 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();
if (that.get(i) instanceof ConditionalMana) {
s = that.get(i).toString() + ((ConditionalMana) that.get(i)).getConditionString();
} else {
s = this.get(i).toString();
s = that.get(i).toString();
}
if (s.isEmpty()) {
this.remove(i);
@ -590,18 +541,19 @@ public class ManaOptions extends ArrayList<Mana> {
}
// 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));
Mana moreValuable = Mana.getMoreValuableMana(that.get(i), that.get(ii));
if (moreValuable != null) {
this.get(ii).setToMana(moreValuable);
this.remove(i);
that.get(ii).setToMana(moreValuable);
that.remove(i);
break;
}
}
}
this.clear();
this.addAll(that);
}
/**
@ -648,4 +600,81 @@ public class ManaOptions extends ArrayList<Mana> {
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<Mana> 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 <T> 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 <T> void verifyTransitivity(Comparator<T> comparator, Collection<T> 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() {}
}

View file

@ -6,6 +6,8 @@ import mage.Mana;
import mage.abilities.Ability;
import mage.game.Game;
import java.util.Objects;
/**
* @author noxx
*/
@ -19,4 +21,22 @@ public abstract class ConditionalManaBuilder implements Builder<ConditionalMana>
}
public abstract String getRule();
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return Objects.equals(this.mana, ((ConditionalManaBuilder) obj).mana);
}
@Override
public int hashCode() {
return Objects.hash(mana);
}
}

View file

@ -14,7 +14,9 @@ import mage.game.Game;
import mage.game.command.Commander;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import sun.font.CreatedFontTracker;
import java.util.Objects;
import java.util.UUID;
/**
@ -38,6 +40,20 @@ public class ConditionalSpellManaBuilder extends ConditionalManaBuilder {
public String getRule() {
return "Spend this mana only to cast " + filter.getMessage() + '.';
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), this.filter);
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) {
return false;
}
return Objects.equals(this.filter, ((ConditionalSpellManaBuilder) obj).filter);
}
}
class SpellCastConditionalMana extends ConditionalMana {

View file

@ -3352,7 +3352,6 @@ public abstract class PlayerImpl implements Player, Serializable {
}
if (used) {
iterator.remove();
availableMana.removeDuplicated();
anAbilityWasUsed = true;
}
}
@ -3363,9 +3362,8 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
// remove duplicated variants (see ManaOptionsTest for info - when that rises)
availableMana.removeDuplicated();
availableMana.removeFullyIncludedVariations();
availableMana.remove(new Mana()); // Remove any empty mana that was left over from the way the code is written
game.setCheckPlayableState(oldState);
return availableMana;
}

View file

@ -176,7 +176,7 @@ public final class CardUtil {
}
// ignore unknown mana
if (manaCost.getOptions().size() == 0) {
if (manaCost.getOptions().isEmpty()) {
continue;
}
@ -186,7 +186,7 @@ public final class CardUtil {
}
// generic mana reduce
Mana mana = manaCost.getOptions().get(0);
Mana mana = manaCost.getOptions().getAtIndex(0);
int colorless = mana != null ? mana.getGeneric() : 0;
if (restToReduce != 0 && colorless > 0) {
if ((colorless - restToReduce) > 0) {
@ -219,7 +219,7 @@ public final class CardUtil {
if (manaCost instanceof MonoHybridManaCost) {
// current implemention supports reduce from left to right hybrid cost without cost parts announce
MonoHybridManaCost mono = (MonoHybridManaCost) manaCost;
int colorless = mono.getOptions().get(1).getGeneric();
int colorless = mono.getOptions().getAtIndex(1).getGeneric();
if (restToReduce != 0 && colorless > 0) {
if ((colorless - restToReduce) > 0) {
// partly reduce
@ -254,7 +254,7 @@ public final class CardUtil {
// add to existing cost
if (reduceCount != 0 && manaCost instanceof GenericManaCost) {
GenericManaCost gen = (GenericManaCost) manaCost;
changedCost.put(manaCost, new GenericManaCost(gen.getOptions().get(0).getGeneric() + -reduceCount));
changedCost.put(manaCost, new GenericManaCost(gen.getOptions().getAtIndex(0).getGeneric() + -reduceCount));
reduceCount = 0;
added = true;
} else {
@ -708,9 +708,9 @@ public final class CardUtil {
}
private static int overflowResult(long value) {
if (value > Integer.MAX_VALUE) {
if (value >= Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
} else if (value < Integer.MIN_VALUE) {
} else if (value <= Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
} else {
return (int) value;