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:
Oleg Agafonov 2024-05-27 22:24:58 +04:00
parent 19f7ba8937
commit 2298ebc5f5
11 changed files with 504 additions and 221 deletions

View file

@ -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;
}