mirror of
https://github.com/magefree/mage.git
synced 2026-01-10 21:02:08 -08:00
Playable mana calculation improved:
* server: fixed server crashes on usage of multiple permanents with {Any} mana abilities (example: Energy Refractor, related to #11285);
* AI: fixed game freezes and errors on computer's {Any} mana usage (closes #9467, closes #6419);
This commit is contained in:
parent
19f7ba8937
commit
2298ebc5f5
11 changed files with 504 additions and 221 deletions
|
|
@ -9,17 +9,20 @@ import mage.util.Copyable;
|
|||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* WARNING, all mana operations must use overflow check, see usage of CardUtil.addWithOverflowCheck and same methods
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
|
||||
|
||||
private static final transient Logger logger = Logger.getLogger(Mana.class);
|
||||
private static final Logger logger = Logger.getLogger(Mana.class);
|
||||
|
||||
protected int white;
|
||||
protected int blue;
|
||||
|
|
@ -442,6 +445,20 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
|
|||
any = CardUtil.overflowDec(any, mana.any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mana must contains only positive values
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return white >= 0
|
||||
&& blue >= 0
|
||||
&& black >= 0
|
||||
&& red >= 0
|
||||
&& green >= 0
|
||||
&& generic >= 0
|
||||
&& colorless >= 0
|
||||
&& any >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts the passed in mana values from this instance. The difference
|
||||
* between this and {@code subtract()} is that if we do not have the
|
||||
|
|
@ -1211,24 +1228,99 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns if this {@link Mana} object has more than or equal values of mana
|
||||
* as the passed in {@link Mana} object. Ignores {Any} mana to prevent
|
||||
* endless iterations.
|
||||
*
|
||||
* @param mana the mana to compare with
|
||||
* @return if this object has more than or equal mana to the passed in
|
||||
* {@link Mana}.
|
||||
* Compare two mana - is one part includes into another part. Support any mana types and uses a payment logic.
|
||||
* <p>
|
||||
* Used for AI and mana optimizations to remove duplicated mana options.
|
||||
*/
|
||||
public boolean includesMana(Mana mana) {
|
||||
return this.white >= mana.white
|
||||
&& this.blue >= mana.blue
|
||||
&& this.black >= mana.black
|
||||
&& this.red >= mana.red
|
||||
&& this.green >= mana.green
|
||||
&& this.colorless >= mana.colorless
|
||||
&& (this.generic >= mana.generic
|
||||
|| CardUtil.overflowInc(this.countColored(), this.colorless) >= mana.count());
|
||||
public boolean includesMana(Mana manaPart) {
|
||||
if (!this.isValid() || !manaPart.isValid()) {
|
||||
// how-to fix: make sure mana calculations do not add or subtract values without result checks or isValid call
|
||||
throw new IllegalArgumentException("Wrong code usage: found negative values in mana calculations: main " + this + ", part " + manaPart);
|
||||
}
|
||||
|
||||
if (this.count() < manaPart.count()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (manaPart.count() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// it's uses pay logic with additional {any} mana support:
|
||||
// - {any} in cost - can be paid by {any} mana only
|
||||
// - {any} in pay - can be used to pay {any}, {1} and colored mana (but not {C})
|
||||
Mana pool = this.copy();
|
||||
Mana cost = manaPart.copy();
|
||||
|
||||
// first pay type by type (it's important to pay {any} first)
|
||||
// 10 - 3 = 7 in pool, 0 in cost
|
||||
// 5 - 7 = 0 in pool, 2 in cost
|
||||
pool.subtract(cost);
|
||||
cost.white = Math.max(0, -1 * pool.white);
|
||||
pool.white = Math.max(0, pool.white);
|
||||
cost.blue = Math.max(0, -1 * pool.blue);
|
||||
pool.blue = Math.max(0, pool.blue);
|
||||
cost.black = Math.max(0, -1 * pool.black);
|
||||
pool.black = Math.max(0, pool.black);
|
||||
cost.red = Math.max(0, -1 * pool.red);
|
||||
pool.red = Math.max(0, pool.red);
|
||||
cost.green = Math.max(0, -1 * pool.green);
|
||||
pool.green = Math.max(0, pool.green);
|
||||
cost.generic = Math.max(0, -1 * pool.generic);
|
||||
pool.generic = Math.max(0, pool.generic);
|
||||
cost.colorless = Math.max(0, -1 * pool.colorless);
|
||||
pool.colorless = Math.max(0, pool.colorless);
|
||||
cost.any = Math.max(0, -1 * pool.any);
|
||||
pool.any = Math.max(0, pool.any);
|
||||
if (cost.count() > pool.count()) {
|
||||
throw new IllegalArgumentException("Wrong mana calculation: " + cost + " - " + pool);
|
||||
}
|
||||
|
||||
// can't pay {any} or {C}
|
||||
if (cost.any > 0 || cost.colorless > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// then pay colored by {any}
|
||||
if (pool.any > 0 && cost.white > 0) {
|
||||
int diff = Math.min(pool.any, cost.white);
|
||||
pool.any -= diff;
|
||||
cost.white -= diff;
|
||||
}
|
||||
if (pool.any > 0 && cost.blue > 0) {
|
||||
int diff = Math.min(pool.any, cost.blue);
|
||||
pool.any -= diff;
|
||||
cost.blue -= diff;
|
||||
}
|
||||
if (pool.any > 0 && cost.black > 0) {
|
||||
int diff = Math.min(pool.any, cost.black);
|
||||
pool.any -= diff;
|
||||
cost.black -= diff;
|
||||
}
|
||||
if (pool.any > 0 && cost.red > 0) {
|
||||
int diff = Math.min(pool.any, cost.red);
|
||||
pool.any -= diff;
|
||||
cost.red -= diff;
|
||||
}
|
||||
if (pool.any > 0 && cost.green > 0) {
|
||||
int diff = Math.min(pool.any, cost.green);
|
||||
pool.any -= diff;
|
||||
cost.green -= diff;
|
||||
}
|
||||
|
||||
// can't pay colored
|
||||
if (cost.countColored() > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// then pay generic by {any}, colored or {C}
|
||||
int leftPool = pool.count();
|
||||
if (leftPool > 0 && cost.generic > 0) {
|
||||
int diff = Math.min(leftPool, cost.generic);
|
||||
cost.generic -= diff;
|
||||
}
|
||||
|
||||
return cost.count() == 0;
|
||||
}
|
||||
|
||||
public boolean isMoreValuableThan(Mana that) {
|
||||
|
|
@ -1350,19 +1442,19 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
|
|||
public int getDifferentColors() {
|
||||
int count = 0;
|
||||
if (white > 0) {
|
||||
count = CardUtil.overflowInc(count, 1);
|
||||
count++;
|
||||
}
|
||||
if (blue > 0) {
|
||||
count = CardUtil.overflowInc(count, 1);
|
||||
count++;
|
||||
}
|
||||
if (black > 0) {
|
||||
count = CardUtil.overflowInc(count, 1);
|
||||
count++;
|
||||
}
|
||||
if (red > 0) {
|
||||
count = CardUtil.overflowInc(count, 1);
|
||||
count++;
|
||||
}
|
||||
if (green > 0) {
|
||||
count = CardUtil.overflowInc(count, 1);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import org.apache.log4j.Logger;
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
* <p>
|
||||
* 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
|
||||
|
|
@ -25,6 +24,8 @@ import java.util.*;
|
|||
* <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).
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public class ManaOptions extends LinkedHashSet<Mana> {
|
||||
|
||||
|
|
@ -378,54 +379,69 @@ public class ManaOptions extends LinkedHashSet<Mana> {
|
|||
/**
|
||||
* 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
|
||||
* @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 currentMana, ManaAbility manaAbility, Game game) {
|
||||
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.getAny() > 0 || manaToAdd.countColored() > 0)
|
||||
&& manaToAdd.count() > 0;
|
||||
boolean newCombinations;
|
||||
&& manaToAdd.countColored() > 0;
|
||||
boolean canHaveBetterValues;
|
||||
|
||||
Mana newMana = new Mana();
|
||||
Mana currentManaCopy = new Mana();
|
||||
Mana possibleMana = new Mana();
|
||||
Mana improvedMana = new Mana();
|
||||
|
||||
for (Mana payCombination : ManaOptions.getPossiblePayCombinations(cost, currentMana)) {
|
||||
currentManaCopy.setToMana(currentMana); // copy start mana because in iteration it will be updated
|
||||
do { // loop for multiple usage if possible
|
||||
newCombinations = false;
|
||||
// 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);
|
||||
do {
|
||||
// loop until all mana replaced by better values
|
||||
canHaveBetterValues = false;
|
||||
|
||||
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)) {
|
||||
// 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<Mana> 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;
|
||||
}
|
||||
|
||||
this.add(newMana.copy()); // add the new combination
|
||||
newCombinations = true; // repeat the while as long there are new combinations and usage is repeatable
|
||||
// found new combination - add it to final options
|
||||
this.add(possibleMana.copy());
|
||||
canHaveBetterValues = true;
|
||||
|
||||
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);
|
||||
// remove old worse options
|
||||
if (possibleMana.isMoreValuableThan(improvedMana)) {
|
||||
oldManaWasReplaced = true;
|
||||
if (!startingMana.equalManaValue(improvedMana)) {
|
||||
this.removeEqualMana(improvedMana);
|
||||
}
|
||||
}
|
||||
currentManaCopy.setToMana(newMana);
|
||||
improvedMana.setToMana(possibleMana);
|
||||
}
|
||||
} while (repeatable && newCombinations && currentManaCopy.includesMana(payCombination));
|
||||
} while (repeatable && canHaveBetterValues && improvedMana.includesMana(possiblePay));
|
||||
}
|
||||
return oldManaWasReplaced;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue