Fixes for Mana.enough and Mana.needed. For #8153 (#8663)

Fixes to Mana.enough:

mana of any color (ManaType.Any) was being used to pay for colourless mana.
Fixes for Mana.needed:

mana of any color (ManaType.Any) was being used to pay for colourless mana.
calculation for generic mana remaining was using min(0, available) instead of max(0, available) meaning that leftover mana of other types was never used to pay for any leftover generic costs.
Other:

Added tests for both .needed and .enough.
Moved tests some tests from ManaUtilTest to ManaTest
Simplified Mana.subtractCosts by calling Mana.substract first to make use of common functionality.
Added more documentation
Added tests for both .needed
Added more tests for .enough to cover the changes with colourless mana.
This commit is contained in:
Alex Vasile 2022-06-01 08:13:37 -06:00 committed by GitHub
parent 16914632c4
commit e679574a15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 293 additions and 196 deletions

View file

@ -3,9 +3,7 @@ package org.mage.test.utils;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.mana.BasicManaAbility; import mage.abilities.mana.BasicManaAbility;
@ -15,7 +13,6 @@ import mage.abilities.mana.RedManaAbility;
import mage.abilities.mana.WhiteManaAbility; import mage.abilities.mana.WhiteManaAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.repository.CardRepository; import mage.cards.repository.CardRepository;
import mage.util.CardUtil;
import mage.util.ManaUtil; import mage.util.ManaUtil;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -27,42 +24,42 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
public class ManaUtilTest extends CardTestPlayerBase { public class ManaUtilTest extends CardTestPlayerBase {
@Test @Test
public void test() { public void testAutoPay() {
testManaToPayVsLand("{R}", "Blood Crypt", 2, 1); // should use {R} testManaToPayVsLand("{R}", "Blood Crypt", 2, 1); // should use {R}
testManaToPayVsLand("{1}{R}", "Blood Crypt", 2, RedManaAbility.class); // should use {R} testManaToPayVsLand("{1}{R}", "Blood Crypt", 2, RedManaAbility.class); // should use {R}
testManaToPayVsLand("{R}{B}", "Blood Crypt", 2, 2); // can't auto choose to pay testManaToPayVsLand("{R}{B}", "Blood Crypt", 2, 2); // can't auto choose to pay
testManaToPayVsLand("{2}{R}{B}", "Blood Crypt", 2, 2); // can't auto choose to pay testManaToPayVsLand("{2}{R}{B}", "Blood Crypt", 2, 2); // can't auto choose to pay
testManaToPayVsLand("{R}{R}{B}{B}", "Blood Crypt", 2, 2); // can't auto choose to pay testManaToPayVsLand("{R}{R}{B}{B}", "Blood Crypt", 2, 2); // can't auto choose to pay
testManaToPayVsLand("{R}{G}{W}{W}{U}", "Blood Crypt", 2, RedManaAbility.class); // should use {R} testManaToPayVsLand("{R}{G}{W}{W}{U}", "Blood Crypt", 2, RedManaAbility.class); // should use {R}
testManaToPayVsLand("{R}{R}{G}{W}{W}{U}", "Blood Crypt", 2, RedManaAbility.class); // should use {R} testManaToPayVsLand("{R}{R}{G}{W}{W}{U}", "Blood Crypt", 2, RedManaAbility.class); // should use {R}
testManaToPayVsLand("{R}{R}", "Blood Crypt", 2, RedManaAbility.class); // should use {R} testManaToPayVsLand("{R}{R}", "Blood Crypt", 2, RedManaAbility.class); // should use {R}
testManaToPayVsLand("{G}{W}", "Blood Crypt", 2, 2); // can't auto choose to pay testManaToPayVsLand("{G}{W}", "Blood Crypt", 2, 2); // can't auto choose to pay
testManaToPayVsLand("{1}{G}{W}", "Blood Crypt", 2, 1); // should use any but auto choose it testManaToPayVsLand("{1}{G}{W}", "Blood Crypt", 2, 1); // should use any but auto choose it
testManaToPayVsLand("{2}{G}{W}{U}", "Blood Crypt", 2, 1); // should use any but auto choose it testManaToPayVsLand("{2}{G}{W}{U}", "Blood Crypt", 2, 1); // should use any but auto choose it
testManaToPayVsLand("{3}", "Blood Crypt", 2, 1); // should use any but auto choose it testManaToPayVsLand("{3}", "Blood Crypt", 2, 1); // should use any but auto choose it
testManaToPayVsLand("{R}{R}{G}{W}{W}{U}", "Watery Grave", 2, 1); // should use {U} testManaToPayVsLand("{R}{R}{G}{W}{W}{U}", "Watery Grave", 2, 1); // should use {U}
testManaToPayVsLand("{R}{R}{G}{W}{W}", "Steam Vents", 2, 1); // should use {R} testManaToPayVsLand("{R}{R}{G}{W}{W}", "Steam Vents", 2, 1); // should use {R}
testManaToPayVsLand("{R}{R}{G}{B}{U}", "Temple Garden", 2, 1); // should use {G} testManaToPayVsLand("{R}{R}{G}{B}{U}", "Temple Garden", 2, 1); // should use {G}
testManaToPayVsLand("{W}{W}{G}{B}{U}", "Sacred Foundry", 2, 1); // should use {W} testManaToPayVsLand("{W}{W}{G}{B}{U}", "Sacred Foundry", 2, 1); // should use {W}
testManaToPayVsLand("{W}{W}{R}{B}{U}", "Overgrown Tomb", 2, BlackManaAbility.class); // should use {B} testManaToPayVsLand("{W}{W}{R}{B}{U}", "Overgrown Tomb", 2, BlackManaAbility.class); // should use {B}
testManaToPayVsLand("{W}{W}{R}{B}{U}", "Swamp", 1, BlackManaAbility.class); testManaToPayVsLand("{W}{W}{R}{B}{U}", "Swamp", 1, BlackManaAbility.class);
testManaToPayVsLand("{W}{W}{R}{B}{U}", "Plains", 1, WhiteManaAbility.class); testManaToPayVsLand("{W}{W}{R}{B}{U}", "Plains", 1, WhiteManaAbility.class);
testManaToPayVsLand("{1}{R}", "Cavern of Souls", 2, 2); // can't auto choose to pay testManaToPayVsLand("{1}{R}", "Cavern of Souls", 2, 2); // can't auto choose to pay
testManaToPayVsLand("{2}", "Cavern of Souls", 2, 2); // can't auto choose to pay testManaToPayVsLand("{2}", "Cavern of Souls", 2, 2); // can't auto choose to pay
testManaToPayVsLand("{2}", "Eldrazi Temple", 2, 2); // can't auto choose to pay testManaToPayVsLand("{2}", "Eldrazi Temple", 2, 2); // can't auto choose to pay
// hybrid mana // hybrid mana
testManaToPayVsLand("{W/R}{W/R}{W/R}", "Sacred Foundry", 2, 1); // auto choose for hybrid mana: choose any testManaToPayVsLand("{W/R}{W/R}{W/R}", "Sacred Foundry", 2, 1); // auto choose for hybrid mana: choose any
testManaToPayVsLand("{R}{W/R}", "Sacred Foundry", 2, RedManaAbility.class); // auto choose for hybrid mana: we should choose {R} testManaToPayVsLand("{R}{W/R}", "Sacred Foundry", 2, RedManaAbility.class); // auto choose for hybrid mana: we should choose {R}
testManaToPayVsLand("{G}{W/R}", "Sacred Foundry", 2, 1); // auto choose for hybrid mana: choose any testManaToPayVsLand("{G}{W/R}", "Sacred Foundry", 2, 1); // auto choose for hybrid mana: choose any
testManaToPayVsLand("{G}{W/R}{W}", "Sacred Foundry", 2, WhiteManaAbility.class); // auto choose for hybrid mana: choose {W} testManaToPayVsLand("{G}{W/R}{W}", "Sacred Foundry", 2, WhiteManaAbility.class); // auto choose for hybrid mana: choose {W}
testManaToPayVsLand("{W/B}{W/B}", "Swamp", 1, BlackManaAbility.class); testManaToPayVsLand("{W/B}{W/B}", "Swamp", 1, BlackManaAbility.class);
testManaToPayVsLand("{R}", "Glimmervoid", 1, 1); testManaToPayVsLand("{R}", "Glimmervoid", 1, 1);
testManaToPayVsLand("{R}{1}", "Glimmervoid", 1, 1); testManaToPayVsLand("{R}{1}", "Glimmervoid", 1, 1);
// we can't auto choose here: // we can't auto choose here:
// let say we auto choose {R}, then we have to use it to pay for {R} not {W/R} (as {W/R} is more generic cost) // let say we auto choose {R}, then we have to use it to pay for {R} not {W/R} (as {W/R} is more generic cost)
@ -75,100 +72,24 @@ public class ManaUtilTest extends CardTestPlayerBase {
testManaToPayVsLand("{W/R}{R/G}", "Sacred Foundry", 2, 2); // can't auto choose to pay testManaToPayVsLand("{W/R}{R/G}", "Sacred Foundry", 2, 2); // can't auto choose to pay
} }
/**
* Mana.condenseManaCostString is used to simplify the String representation of a mana cost to make it more readable.
*/
@Test @Test
public void testManaCondensing() { public void testManaCondensing() {
Assert.assertEquals("{5}{W}", ManaUtil.condenseManaCostString(("{1}{1}{1}{2}{W}"))); Assert.assertEquals("{5}{W}", ManaUtil.condenseManaCostString("{1}{1}{1}{2}{W}"));
Assert.assertEquals("{4}{B}{B}", ManaUtil.condenseManaCostString("{2}{B}{2}{B}")); Assert.assertEquals("{4}{B}{B}", ManaUtil.condenseManaCostString("{2}{B}{2}{B}"));
Assert.assertEquals("{6}{R}{R}{R}{U}", ManaUtil.condenseManaCostString("{R}{1}{R}{2}{R}{3}{U}")); Assert.assertEquals("{6}{R}{R}{R}{U}", ManaUtil.condenseManaCostString("{R}{1}{R}{2}{R}{3}{U}"));
Assert.assertEquals("{5}{B}{U}{W}", ManaUtil.condenseManaCostString("{1}{B}{W}{4}{U}")); Assert.assertEquals("{5}{B}{U}{W}", ManaUtil.condenseManaCostString("{1}{B}{W}{4}{U}"));
Assert.assertEquals("{8}{B}{G}{G}{U}", ManaUtil.condenseManaCostString("{1}{G}{1}{2}{3}{G}{B}{U}{1}")); Assert.assertEquals("{8}{B}{G}{G}{U}", ManaUtil.condenseManaCostString("{1}{G}{1}{2}{3}{G}{B}{U}{1}"));
Assert.assertEquals("{3}{R}{U}", ManaUtil.condenseManaCostString("{3}{R}{U}")); Assert.assertEquals("{3}{R}{U}", ManaUtil.condenseManaCostString("{3}{R}{U}"));
Assert.assertEquals("{10}", ManaUtil.condenseManaCostString("{1}{2}{3}{4}")); Assert.assertEquals("{10}", ManaUtil.condenseManaCostString("{1}{2}{3}{4}"));
Assert.assertEquals("{B}{G}{R}{U}{W}", ManaUtil.condenseManaCostString("{B}{G}{R}{U}{W}")); Assert.assertEquals("{B}{G}{R}{U}{W}", ManaUtil.condenseManaCostString("{B}{G}{R}{U}{W}"));
Assert.assertEquals("{R}{R}", ManaUtil.condenseManaCostString("{R}{R}")); Assert.assertEquals("{R}{R}", ManaUtil.condenseManaCostString("{R}{R}"));
Assert.assertEquals("{U}", ManaUtil.condenseManaCostString("{U}")); Assert.assertEquals("{U}", ManaUtil.condenseManaCostString("{U}"));
Assert.assertEquals("{2}", ManaUtil.condenseManaCostString("{2}")); Assert.assertEquals("{2}", ManaUtil.condenseManaCostString("{2}"));
Assert.assertEquals("", ManaUtil.condenseManaCostString("{}")); Assert.assertEquals("", ManaUtil.condenseManaCostString("{}"));
Assert.assertEquals("{5}{C}{R}{R}{R}{U}", ManaUtil.condenseManaCostString("{R}{C}{R}{2}{R}{3}{U}")); Assert.assertEquals("{5}{C}{R}{R}{R}{U}", ManaUtil.condenseManaCostString("{R}{C}{R}{2}{R}{3}{U}"));
}
/**
* Mana.enough is used to check if a spell can be cast with an given amount
* of avalable mana
*/
@Test
public void testManaEnough() {
testManaAvailEnough("{G}", 1, "", true);
testManaAvailEnough("{G}", 0, "{G}", true);
testManaAvailEnough("{R}", 0, "{G}", false);
testManaAvailEnough("{B}", 0, "{G}", false);
testManaAvailEnough("{U}", 0, "{G}", false);
testManaAvailEnough("{W}", 0, "{G}", false);
testManaAvailEnough("{R}", 1, "", true);
testManaAvailEnough("{R}", 0, "{R}", true);
testManaAvailEnough("{G}", 0, "{R}", false);
testManaAvailEnough("{B}", 0, "{R}", false);
testManaAvailEnough("{U}", 0, "{R}", false);
testManaAvailEnough("{W}", 0, "{R}", false);
testManaAvailEnough("{U}{B}{W}{G}{R}", 4, "{R}", true);
testManaAvailEnough("{U}{B}{W}{G}{R}", 3, "{R}{B}", true);
testManaAvailEnough("{U}{U}{U}{G}{G}{2}", 2, "{U}{U}{G}{R}{B}", true);
testManaAvailEnough("{2}{U}{U}", 0, "{U}{U}{U}{U}", true);
testManaAvailEnough("{2}{U}{U}", 0, "{4}", false);
testManaAvailEnough("{2}{U}{U}", 0, "{B}{B}{4}", false);
testManaAvailEnough("{G}", 0, "{G/W}", true);
testManaAvailEnough("{G}{W}", 0, "{G/W}{G/W}", true);
testManaAvailEnough("{W}{W}", 0, "{G/W}{G/W}", true);
testManaAvailEnough("{G}{G}", 0, "{G/W}{G/W}", true);
}
/**
* Mana.enough is used to check if a spell can be cast with an given amount
* of avalable mana
*/
@Test
public void testManaIncrease() {
// cost - reduction - rest
testManaReduction("{G}{G}", "{G}", "{G}");
testManaReduction("{1}{G}{G}", "{G}", "{1}{G}");
testManaReduction("{B}{B}", "{B}", "{B}");
testManaReduction("{1}{B}{B}", "{B}", "{1}{B}");
testManaReduction("{W}{W}", "{W}", "{W}");
testManaReduction("{1}{W}{W}", "{W}", "{1}{W}");
testManaReduction("{U}{U}", "{U}", "{U}");
testManaReduction("{1}{U}{U}", "{U}", "{1}{U}");
testManaReduction("{R}{R}", "{R}", "{R}");
testManaReduction("{1}{R}{R}", "{R}", "{1}{R}");
testManaReduction("{R}{G}{B}{U}{W}", "{R}{G}{B}{U}{W}", "{0}");
// Hybrid Mana
testManaReduction("{2/B}{2/B}{2/B}", "{B}{B}", "{2/B}");
testManaReduction("{2/B}{2/B}{2/B}", "{B}{B}{B}", "{0}");
testManaReduction("{2/W}{2/W}{2/W}", "{W}{W}", "{2/W}");
testManaReduction("{2/W}{2/W}{2/W}", "{W}{W}{W}", "{0}");
testManaReduction("{G/B}{G/B}{G/B}", "{B}{G}{B}", "{0}");
}
/**
* Checks if a given mana reduction left the expected amount of mana costs
*
* @param manaCostsToPay
* @param availablyAny
* @param available
* @param expected
*/
private void testManaReduction(String manaCostsToPay, String manaToReduce, String restMana) {
SpellAbility spellAbility = new SpellAbility(new ManaCostsImpl(manaCostsToPay), "Test");
CardUtil.adjustCost(spellAbility, new ManaCostsImpl(manaToReduce), true);
Assert.assertTrue("The mana cost to pay " + manaCostsToPay + " reduced by " + manaToReduce + " should left " + restMana + " but the rest was " + spellAbility.getManaCostsToPay().getText(), spellAbility.getManaCostsToPay().getText().equals(restMana));
} }
/** /**
@ -184,7 +105,7 @@ public class ManaUtilTest extends CardTestPlayerBase {
* should be returned after optimization. * should be returned after optimization.
*/ */
private void testManaToPayVsLand(String manaToPay, String landName, int expected1, int expected2) { private void testManaToPayVsLand(String manaToPay, String landName, int expected1, int expected2) {
ManaCost unpaid = new ManaCostsImpl(manaToPay); ManaCost unpaid = new ManaCostsImpl<>(manaToPay);
Card card = CardRepository.instance.findCard(landName).getCard(); Card card = CardRepository.instance.findCard(landName).getCard();
Assert.assertNotNull(card); Assert.assertNotNull(card);
@ -206,13 +127,13 @@ public class ManaUtilTest extends CardTestPlayerBase {
* We get all mana abilities, then try to auto pay and compare to expected1 * We get all mana abilities, then try to auto pay and compare to expected1
* and expected2 params. * and expected2 params.
* *
* @param manaToPay Mana that should be paid using land. * @param manaToPay Mana that should be paid using land.
* @param landName Land to use as mana producer. * @param landName Land to use as mana producer.
* @param expected1 The amount of mana abilities the land should have. * @param expected1 The amount of mana abilities the land should have.
* @param expectedChosen * @param expectedChosen The mana ability expected to be chosen.
*/ */
private void testManaToPayVsLand(String manaToPay, String landName, int expected1, Class<? extends BasicManaAbility> expectedChosen) { private void testManaToPayVsLand(String manaToPay, String landName, int expected1, Class<? extends BasicManaAbility> expectedChosen) {
ManaCost unpaid = new ManaCostsImpl(manaToPay); ManaCost unpaid = new ManaCostsImpl<>(manaToPay);
Card card = CardRepository.instance.findCard(landName).getCard(); Card card = CardRepository.instance.findCard(landName).getCard();
Assert.assertNotNull(card); Assert.assertNotNull(card);
@ -225,31 +146,11 @@ public class ManaUtilTest extends CardTestPlayerBase {
Assert.assertTrue("Wrong mana ability has been chosen", expectedChosen.isInstance(ability)); Assert.assertTrue("Wrong mana ability has been chosen", expectedChosen.isInstance(ability));
} }
/**
* Checks if the given available Mana is enough to pay a given mana cost
*
* @param manaCostsToPay
* @param availablyAny
* @param available
* @param expected
*/
private void testManaAvailEnough(String manaCostsToPay, int availablyAny, String available, boolean expected) {
ManaCost unpaid = new ManaCostsImpl(manaCostsToPay);
ManaCost costAvailable = new ManaCostsImpl(available);
Mana manaAvailable = costAvailable.getMana();
manaAvailable.setAny(availablyAny);
if (expected) {
Assert.assertTrue("The available Mana " + costAvailable.getText() + " should be enough to pay the costs " + unpaid.getText(), unpaid.getMana().enough(manaAvailable));
} else {
Assert.assertFalse("The available Mana " + costAvailable.getText() + " shouldn't be enough to pay the costs " + unpaid.getText(), unpaid.getMana().enough(manaAvailable));
}
}
/** /**
* Extracts mana abilities from the card. * Extracts mana abilities from the card.
* *
* @param card Card to extract mana abilities from. * @param card Card to extract mana abilities from.
* @return * @return Map between the UUID of each ability on the card and the ability.
*/ */
private Map<UUID, ActivatedManaAbilityImpl> getManaAbilities(Card card) { private Map<UUID, ActivatedManaAbilityImpl> getManaAbilities(Card card) {
Map<UUID, ActivatedManaAbilityImpl> useableAbilities = new LinkedHashMap<>(); Map<UUID, ActivatedManaAbilityImpl> useableAbilities = new LinkedHashMap<>();

View file

@ -76,7 +76,7 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
* Copy constructor. Creates a {@link Mana} object from existing * Copy constructor. Creates a {@link Mana} object from existing
* {@link Mana} * {@link Mana}
* *
* @param mana object to create copy from * @param mana object to create copy from.
*/ */
public Mana(final Mana mana) { public Mana(final Mana mana) {
Objects.requireNonNull(mana, "The passed in mana can not be null"); Objects.requireNonNull(mana, "The passed in mana can not be null");
@ -129,6 +129,11 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
} }
} }
/**
* Creates a {@link Mana} object of one mana of the passed {@link ManaType}.
*
* @param manaType The type of mana to set to one.
*/
public Mana(final ManaType manaType) { public Mana(final ManaType manaType) {
this(); this();
Objects.requireNonNull(manaType, "The passed in ManaType can not be null"); Objects.requireNonNull(manaType, "The passed in ManaType can not be null");
@ -159,6 +164,12 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
} }
} }
/**
* Creates a {@link Mana} object of #num mana of the passed {@link ManaType}.
*
* @param manaType The type of mana to set.
* @param num The number of mana available of the passed ManaType.
**/
public Mana(final ManaType manaType, int num) { public Mana(final ManaType manaType, int num) {
this(); this();
Objects.requireNonNull(manaType, "The passed in ManaType can not be null"); Objects.requireNonNull(manaType, "The passed in ManaType can not be null");
@ -311,7 +322,7 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
/** /**
* Increases the given mana by one. * Increases the given mana by one.
* *
* @param manaType * @param manaType the type of mana to increase by one.
*/ */
public void increase(ManaType manaType) { public void increase(ManaType manaType) {
switch (manaType) { switch (manaType) {
@ -411,18 +422,12 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
* *
* @param mana mana values to subtract * @param mana mana values to subtract
* @throws ArithmeticException thrown if there is not enough available * @throws ArithmeticException thrown if there is not enough available
* colored mana to pay the generic cost * mana to pay the generic cost
*/ */
public void subtractCost(final Mana mana) throws ArithmeticException { public void subtractCost(final Mana mana) throws ArithmeticException {
white = CardUtil.overflowDec(white, mana.white); this.subtract(mana);
blue = CardUtil.overflowDec(blue, mana.blue);
black = CardUtil.overflowDec(black, mana.black);
red = CardUtil.overflowDec(red, mana.red);
green = CardUtil.overflowDec(green, mana.green);
generic = CardUtil.overflowDec(generic, mana.generic);
colorless = CardUtil.overflowDec(colorless, mana.colorless);
any = CardUtil.overflowDec(any, mana.any);
// Handle any unpaid generic mana costs
while (generic < 0) { while (generic < 0) {
int oldColorless = generic; int oldColorless = generic;
if (white > 0) { if (white > 0) {
@ -666,17 +671,32 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
} }
/** /**
* Returns if there is enough available mana to pay the mana provided by the * Returns if the cost (this) can be paid by the mana provided by the passed in {@link Mana} object.
* passed in {@link Mana} object.
* *
* @param cost the cost to compare too. * @param avail The mana to compare too.
* @return if there is enough available mana to pay. * @return boolean indicating if there is enough available mana to pay.
*/ */
public boolean enough(final Mana cost) { public boolean enough(final Mana avail) {
Mana compare = cost.copy(); Mana compare = avail.copy();
// Subtract the mana cost (this) from the mana available (compare).
// This will only subtract like mana types from one another (e.g. green from green, coloreless from colorless).
compare.subtract(this); compare.subtract(this);
// A negative value for compare.X means that mana of type X from the cost could not be paid by mana
// of the same kind from the available mana.
// Check each of the types, and see if there is enough mana of any color left to pay for the colors.
if (compare.colorless < 0) { // Put first to shortcut the calculations
// Colorless mana can only be paid by colorless mana.
// If there's a negative value, then there's nothing else that can be used to pay for it.
return false;
}
if (compare.white < 0) { if (compare.white < 0) {
compare.any = CardUtil.overflowInc(compare.any, compare.white); compare.any = CardUtil.overflowInc(compare.any, compare.white);
// A negatice value means that there was more mana of the given type required than there was mana of any
// color to pay for it.
// So, there is not enough mana to pay the avail.
if (compare.any < 0) { if (compare.any < 0) {
return false; return false;
} }
@ -710,13 +730,6 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
} }
compare.green = 0; compare.green = 0;
} }
if (compare.colorless < 0) {
compare.any = CardUtil.overflowInc(compare.any, compare.colorless);
if (compare.any < 0) {
return false;
}
compare.colorless = 0;
}
if (compare.generic < 0) { if (compare.generic < 0) {
compare.generic = CardUtil.overflowInc(compare.generic, compare.white); compare.generic = CardUtil.overflowInc(compare.generic, compare.white);
compare.generic = CardUtil.overflowInc(compare.generic, compare.blue); compare.generic = CardUtil.overflowInc(compare.generic, compare.blue);
@ -731,16 +744,27 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
} }
/** /**
* Returns the total mana needed to meet the passed in {@link Mana} object. * Returns the total mana needed to meet the cost of this given the available mana passed in
* as a {@link Mana} object.
* *
* @param cost the mana cost * Used by the AI to calculate what mana it needs to obtain for a spell to become playable.
* @return the total mana needed to meet the passes in {@link Mana} object. *
* @param avail the mana available to pay the cost
* @return the total mana needed to pay this given the available mana passed in as a {@link Mana} object.
*/ */
public Mana needed(final Mana cost) { public Mana needed(final Mana avail) {
Mana compare = cost.copy(); Mana compare = avail.copy();
// Subtract the mana cost (this) from the mana available (compare).
// This will only subtract like mana types from one another (e.g. green from green, coloreless from colorless).
compare.subtract(this); compare.subtract(this);
// A negative value for compare.X means that mana of type X from the cost could not be paid by mana
// of the same kind from the available mana (this).
if (compare.white < 0 && compare.any > 0) { if (compare.white < 0 && compare.any > 0) {
// Calculate how much of the unpaid colored mana can be covered by mana of any color
int diff = Math.min(compare.any, Math.abs(compare.white)); int diff = Math.min(compare.any, Math.abs(compare.white));
// Make the payment
compare.any = CardUtil.overflowDec(compare.any, diff); compare.any = CardUtil.overflowDec(compare.any, diff);
compare.white = CardUtil.overflowInc(compare.white, diff); compare.white = CardUtil.overflowInc(compare.white, diff);
} }
@ -764,25 +788,30 @@ public class Mana implements Comparable<Mana>, Serializable, Copyable<Mana> {
compare.any = CardUtil.overflowDec(compare.any, diff); compare.any = CardUtil.overflowDec(compare.any, diff);
compare.green = CardUtil.overflowInc(compare.green, diff); compare.green = CardUtil.overflowInc(compare.green, diff);
} }
if (compare.colorless < 0 && compare.any > 0) {
int diff = Math.min(compare.any, Math.abs(compare.colorless)); // Colorless mana can only be paid by colorless sources, so a check for it is not performed.
compare.any = CardUtil.overflowDec(compare.any, diff);
compare.colorless = CardUtil.overflowInc(compare.colorless, diff);
}
if (compare.generic < 0) { if (compare.generic < 0) {
// Calculate total leftover mana available to pay for generic costs
int remaining = 0; int remaining = 0;
remaining = CardUtil.overflowInc(remaining, Math.min(0, compare.white)); remaining = CardUtil.overflowInc(remaining, Math.max(0, compare.white));
remaining = CardUtil.overflowInc(remaining, Math.min(0, compare.blue)); remaining = CardUtil.overflowInc(remaining, Math.max(0, compare.blue));
remaining = CardUtil.overflowInc(remaining, Math.min(0, compare.black)); remaining = CardUtil.overflowInc(remaining, Math.max(0, compare.black));
remaining = CardUtil.overflowInc(remaining, Math.min(0, compare.red)); remaining = CardUtil.overflowInc(remaining, Math.max(0, compare.red));
remaining = CardUtil.overflowInc(remaining, Math.min(0, compare.green)); remaining = CardUtil.overflowInc(remaining, Math.max(0, compare.green));
remaining = CardUtil.overflowInc(remaining, Math.min(0, compare.colorless)); remaining = CardUtil.overflowInc(remaining, Math.max(0, compare.colorless));
remaining = CardUtil.overflowInc(remaining, Math.min(0, compare.any)); remaining = CardUtil.overflowInc(remaining, Math.max(0, compare.any));
if (remaining > 0) { if (remaining > 0) {
// Calculate how much of the unpaid generic cost can be paid by the leftover mana
int diff = Math.min(remaining, Math.abs(compare.generic)); int diff = Math.min(remaining, Math.abs(compare.generic));
// Make the payment
compare.generic = CardUtil.overflowInc(compare.generic, diff); compare.generic = CardUtil.overflowInc(compare.generic, diff);
} }
} }
// Create the mana object holding the mana needed to pay the cost
// If the value in compare is positive it means that there's excess mana of that type, and no more is needed.
Mana needed = new Mana(); Mana needed = new Mana();
if (compare.white < 0) { if (compare.white < 0) {
needed.white = CardUtil.overflowDec(needed.white, compare.white); needed.white = CardUtil.overflowDec(needed.white, compare.white);

View file

@ -449,9 +449,14 @@ public final class ManaUtil {
} }
/** /**
* Converts a collection of mana symbols into a single condensed string e.g. * Converts a collection of mana symbols into a single condensed string e.g:
* {1}{1}{1}{1}{1}{W} = {5}{W} {2}{B}{2}{B}{2}{B} = {6}{B}{B}{B} * {1}{1}{1}{1}{1}{W} = {5}{W}
* {1}{2}{R}{U}{1}{1} = {5}{R}{U} {B}{G}{R} = {B}{G}{R} * {2}{B}{2}{B}{2}{B} = {6}{B}{B}{B}
* {1}{2}{R}{U}{1}{1} = {5}{R}{U}
* {B}{G}{R} = {B}{G}{R}
*
* @param rawCost the uncondensed version of the mana String.
* @return the condensed version of the mana String.
*/ */
public static String condenseManaCostString(String rawCost) { public static String condenseManaCostString(String rawCost) {
int total = 0; int total = 0;

View file

@ -1,8 +1,13 @@
package mage; package mage;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.constants.ColoredManaSymbol; import mage.constants.ColoredManaSymbol;
import mage.constants.ManaType; import mage.constants.ManaType;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.util.CardUtil;
import org.junit.Assert;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
@ -11,7 +16,7 @@ import static org.junit.Assert.*;
/** /**
* Custom unit tests for {link Mana}. * Custom unit tests for {@link Mana}.
* *
* @author githubpoixen@github.com * @author githubpoixen@github.com
*/ */
@ -686,4 +691,161 @@ public class ManaTest {
assertEquals(Integer.MAX_VALUE, mana.getGeneric()); assertEquals(Integer.MAX_VALUE, mana.getGeneric());
assertEquals(Integer.MAX_VALUE, mana.getAny()); assertEquals(Integer.MAX_VALUE, mana.getAny());
} }
/**
* Mana.enough is used to check if a spell can be cast with an given amount
* of avalable mana
*/
@Test
public void testManaEnough() {
assertAvailableManaEnough("{G}", 1, "", true);
assertAvailableManaEnough("{G}", 0, "{G}", true);
assertAvailableManaEnough("{R}", 0, "{G}", false);
assertAvailableManaEnough("{B}", 0, "{G}", false);
assertAvailableManaEnough("{U}", 0, "{G}", false);
assertAvailableManaEnough("{W}", 0, "{G}", false);
assertAvailableManaEnough("{W}", 0, "{C}", false);
assertAvailableManaEnough("{R}", 1, "", true);
assertAvailableManaEnough("{R}", 0, "{R}", true);
assertAvailableManaEnough("{G}", 0, "{R}", false);
assertAvailableManaEnough("{B}", 0, "{R}", false);
assertAvailableManaEnough("{U}", 0, "{R}", false);
assertAvailableManaEnough("{W}", 0, "{R}", false);
assertAvailableManaEnough("{U}{B}{W}{G}{R}", 4, "{R}", true);
assertAvailableManaEnough("{U}{B}{W}{G}{R}", 3, "{R}{B}", true);
assertAvailableManaEnough("{U}{U}{U}{G}{G}{2}", 2, "{U}{U}{G}{R}{B}", true);
assertAvailableManaEnough("{2}{U}{U}", 0, "{U}{U}{U}{U}", true);
assertAvailableManaEnough("{2}{U}{U}", 0, "{4}", false);
assertAvailableManaEnough("{2}{U}{U}", 0, "{B}{B}{4}", false);
assertAvailableManaEnough("{G}", 0, "{G/W}", true);
assertAvailableManaEnough("{G}{W}", 0, "{G/W}{G/W}", true);
assertAvailableManaEnough("{W}{W}", 0, "{G/W}{G/W}", true);
assertAvailableManaEnough("{G}{G}", 0, "{G/W}{G/W}", true);
assertAvailableManaEnough("{C}", 1, "", false);
assertAvailableManaEnough("{C}", 0, "{C}", true);
assertAvailableManaEnough("{C}", 0, "{G}", false);
assertAvailableManaEnough("{C}", 0, "{R}", false);
assertAvailableManaEnough("{C}", 0, "{B}", false);
assertAvailableManaEnough("{C}", 0, "{W}", false);
assertAvailableManaEnough("{C}", 0, "{U}", false);
}
/**
* Mana.enough is used to check if a spell can be cast with an given amount
* of avalable mana
*/
@Test
public void testManaReduction() {
// cost - reduction - rest
assertManaReduction("{G}{G}", "{G}", "{G}");
assertManaReduction("{1}{G}{G}", "{G}", "{1}{G}");
assertManaReduction("{B}{B}", "{B}", "{B}");
assertManaReduction("{1}{B}{B}", "{B}", "{1}{B}");
assertManaReduction("{W}{W}", "{W}", "{W}");
assertManaReduction("{1}{W}{W}", "{W}", "{1}{W}");
assertManaReduction("{U}{U}", "{U}", "{U}");
assertManaReduction("{1}{U}{U}", "{U}", "{1}{U}");
assertManaReduction("{R}{R}", "{R}", "{R}");
assertManaReduction("{1}{R}{R}", "{R}", "{1}{R}");
assertManaReduction("{R}{G}{B}{U}{W}", "{R}{G}{B}{U}{W}", "{0}");
// Hybrid Mana
assertManaReduction("{2/B}{2/B}{2/B}", "{B}{B}", "{2/B}");
assertManaReduction("{2/B}{2/B}{2/B}", "{B}{B}{B}", "{0}");
assertManaReduction("{2/W}{2/W}{2/W}", "{W}{W}", "{2/W}");
assertManaReduction("{2/W}{2/W}{2/W}", "{W}{W}{W}", "{0}");
assertManaReduction("{G/B}{G/B}{G/B}", "{B}{G}{B}", "{0}");
}
/**
* Mana.needed is used by the AI to know how much mana it needs in order to be able to play a card.
*/
@Test
public void should() {
// TODO: How does it handle generic and any.
// How *should* it handle them?
testManaNeeded(
new Mana(ManaType.COLORLESS, 1), // Available
new Mana(ManaType.COLORLESS, 2), // Cost
new Mana(ManaType.COLORLESS, 1) // Needed
);
testManaNeeded(
new Mana(ManaType.RED, 1), // Avaiable
new Mana(ManaType.GENERIC, 1), // Cost
new Mana() // Needed
);
testManaNeeded(
new Mana(ManaType.COLORLESS, 1), // Avaiable
new Mana(ManaType.GENERIC, 1), // Cost
new Mana() // Needed
);
testManaNeeded(
new Mana(), // Available
new Mana(2, 0, 0, 0, 0, 2, 0, 0), // Cost
new Mana(2, 0, 0, 0, 0, 2, 0, 0) // Needed
);
}
/**
* Checks if the mana needed calculations produces the expected needed mana amount.
*
* @param available The mana currently available.
* @param cost The mana needed for a cost.
* @param neededExpected The mana expected to be required to pay the cost.
*/
private void testManaNeeded(Mana available, Mana cost, Mana neededExpected) {
Mana neededActual = cost.needed(available);
Assert.assertTrue(
"The mana needed to pay " + cost + " given " + available
+ " should have been " + neededExpected + " but was calculate to be " + neededActual,
neededActual.equalManaValue(neededExpected)
);
}
/**
* Checks if the given available Mana is enough to pay a given mana cost
*
* @param manaCostsToPay The mana cost that needs to be paid.
* @param availablyAny The amount of generic mana available.
* @param available The colored and colorless mana available.
* @param expected boolean indicating if the available mana is expected to cover the mana cost.
*/
private void assertAvailableManaEnough(String manaCostsToPay, int availablyAny, String available, boolean expected) {
ManaCost unpaid = new ManaCostsImpl<>(manaCostsToPay);
ManaCost costAvailable = new ManaCostsImpl<>(available);
Mana manaAvailable = costAvailable.getMana();
manaAvailable.setAny(availablyAny);
if (expected) {
Assert.assertTrue("The available Mana " + costAvailable.getText() + " should be enough to pay the costs " + unpaid.getText(), unpaid.getMana().enough(manaAvailable));
} else {
Assert.assertFalse("The available Mana " + costAvailable.getText() + " shouldn't be enough to pay the costs " + unpaid.getText(), unpaid.getMana().enough(manaAvailable));
}
}
/**
* Checks if a given mana reduction left the expected amount of mana costs
*
* @param manaCostsToPay The mana cost before reductions are applied.
* @param manaToReduce The amount and types of many to reduced the cost by.
* @param restMana The expected amount of mana left
*/
private void assertManaReduction(String manaCostsToPay, String manaToReduce, String restMana) {
SpellAbility spellAbility = new SpellAbility(new ManaCostsImpl<>(manaCostsToPay), "Test");
CardUtil.adjustCost(spellAbility, new ManaCostsImpl<>(manaToReduce), true);
Assert.assertEquals(
"The mana cost to pay " + manaCostsToPay + " reduced by " + manaToReduce +
" should left " + restMana + " but the rest was " + spellAbility.getManaCostsToPay(),
spellAbility.getManaCostsToPay().getText(),
restMana
);
}
} }