mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30: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
|
|
@ -90,17 +90,17 @@ public final class RateCard {
|
|||
}
|
||||
|
||||
String name = card.getName();
|
||||
if (useCache && allowedColors == null && ratedCard.containsKey(name)) {
|
||||
int rate = ratedCard.get(name);
|
||||
return rate;
|
||||
if (useCache && allowedColors.isEmpty() && ratedCard.containsKey(name)) {
|
||||
return ratedCard.get(name);
|
||||
}
|
||||
|
||||
int typeMultiplier = typeMultiplier(card);
|
||||
int score = getBaseCardScore(card) + 2 * typeMultiplier + getManaCostScore(card, allowedColors)
|
||||
+ 40 * isRemoval(card);
|
||||
|
||||
if (useCache && allowedColors == null)
|
||||
if (useCache && allowedColors.isEmpty()) {
|
||||
ratedCard.put(name, score);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
|
@ -111,17 +111,17 @@ public final class RateCard {
|
|||
}
|
||||
|
||||
String name = cardview.getName();
|
||||
if (useCache && allowedColors == null && ratedCardView.containsKey(name)) {
|
||||
int rate = ratedCardView.get(name);
|
||||
return rate;
|
||||
if (useCache && allowedColors.isEmpty() && ratedCardView.containsKey(name)) {
|
||||
return ratedCardView.get(name);
|
||||
}
|
||||
|
||||
int typeMultiplier = typeMultiplier(cardview);
|
||||
int score = getBaseCardScore(cardview) + 2 * typeMultiplier + getManaCostScore(cardview, allowedColors);
|
||||
// Cardview does not have enough info to know the card is a removal.
|
||||
|
||||
if (useCache && allowedColors == null)
|
||||
if (useCache && allowedColors.isEmpty()) {
|
||||
ratedCardView.put(name, score);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
|
@ -402,7 +402,7 @@ public final class RateCard {
|
|||
}
|
||||
|
||||
private static int getManaCostScore(String name, int manaValue, List<String> manaCostSymbols, List<ColoredManaSymbol> allowedColors) {
|
||||
if (allowedColors == null) {
|
||||
if (allowedColors.isEmpty()) {
|
||||
int colorPenalty = 0;
|
||||
for (String symbol : manaCostSymbols) {
|
||||
if (isColoredMana(symbol)) {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ import mage.target.common.*;
|
|||
import mage.util.*;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
|
@ -75,14 +74,17 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
|
||||
final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 1; // TODO: rework simulations logic to use multiple calcs instead one by one
|
||||
|
||||
private transient Map<Mana, Card> unplayable = new TreeMap<>();
|
||||
private transient List<Card> playableNonInstant = new ArrayList<>();
|
||||
private transient List<Card> playableInstant = new ArrayList<>();
|
||||
private transient List<ActivatedAbility> playableAbilities = new ArrayList<>();
|
||||
private transient List<PickedCard> pickedCards;
|
||||
private transient List<ColoredManaSymbol> chosenColors;
|
||||
private final transient Map<Mana, Card> unplayable = new TreeMap<>();
|
||||
private final transient List<Card> playableNonInstant = new ArrayList<>();
|
||||
private final transient List<Card> playableInstant = new ArrayList<>();
|
||||
private final transient List<ActivatedAbility> playableAbilities = new ArrayList<>();
|
||||
private final transient List<PickedCard> pickedCards = new ArrayList<>();
|
||||
private final transient List<ColoredManaSymbol> chosenColors = new ArrayList<>();
|
||||
|
||||
private transient ManaCost currentUnpaidMana;
|
||||
// keep current paying cost info for choose dialogs
|
||||
// mana abilities must ask payment too, so keep full chain
|
||||
// TODO: make sure it thread safe for AI simulations (all transient fields above and bottom)
|
||||
private final transient Map<UUID, ManaCost> lastUnpaidMana = new LinkedHashMap<>();
|
||||
|
||||
// For stopping infinite loops when trying to pay Phyrexian mana when the player can't spend life and no other sources are available
|
||||
private transient boolean alreadyTryingToPayPhyrexian;
|
||||
|
|
@ -94,7 +96,6 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
userData.setAvatarId(64);
|
||||
userData.setGroupId(UserGroup.COMPUTER.getGroupId());
|
||||
userData.setFlagName("computer.png");
|
||||
pickedCards = new ArrayList<>();
|
||||
}
|
||||
|
||||
protected ComputerPlayer(UUID id) {
|
||||
|
|
@ -104,7 +105,6 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
userData.setAvatarId(64);
|
||||
userData.setGroupId(UserGroup.COMPUTER.getGroupId());
|
||||
userData.setFlagName("computer.png");
|
||||
pickedCards = new ArrayList<>();
|
||||
}
|
||||
|
||||
public ComputerPlayer(final ComputerPlayer player) {
|
||||
|
|
@ -653,7 +653,7 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
while (!target.isChosen(game)
|
||||
&& !cardsInHand.isEmpty()
|
||||
&& target.getMaxNumberOfTargets() > target.getTargets().size()) {
|
||||
Card card = pickBestCard(cardsInHand, null, target, source, game);
|
||||
Card card = pickBestCard(cardsInHand, Collections.emptyList(), target, source, game);
|
||||
if (card != null) {
|
||||
if (target.canTarget(abilityControllerId, card.getId(), source, game)) {
|
||||
target.addTarget(card.getId(), source, game);
|
||||
|
|
@ -1139,9 +1139,9 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
while (!cards.isEmpty()) {
|
||||
|
||||
if (outcome.isGood()) {
|
||||
card = pickBestCard(cards, null, target, source, game);
|
||||
card = pickBestCard(cards, Collections.emptyList(), target, source, game);
|
||||
} else {
|
||||
card = pickWorstCard(cards, null, target, source, game);
|
||||
card = pickWorstCard(cards, Collections.emptyList(), target, source, game);
|
||||
}
|
||||
if (!target.getTargets().contains(card.getId())) {
|
||||
if (source != null) {
|
||||
|
|
@ -1152,7 +1152,7 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
return card;
|
||||
}
|
||||
}
|
||||
cards.remove(card);
|
||||
cards.remove(card); // TODO: research parent code - is it depends on original list? Can be bugged
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1550,11 +1550,12 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
@Override
|
||||
public boolean playMana(Ability ability, ManaCost unpaid, String promptText, Game game) {
|
||||
payManaMode = true;
|
||||
currentUnpaidMana = unpaid;
|
||||
lastUnpaidMana.put(ability.getId(), unpaid.copy());
|
||||
try {
|
||||
return playManaHandling(ability, unpaid, game);
|
||||
} finally {
|
||||
currentUnpaidMana = null;
|
||||
|
||||
lastUnpaidMana.remove(ability.getId());
|
||||
payManaMode = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1575,19 +1576,27 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
producers = this.getAvailableManaProducers(game);
|
||||
producers.addAll(this.getAvailableManaProducersWithCost(game));
|
||||
}
|
||||
|
||||
// use fully compatible colored mana producers first
|
||||
for (MageObject mageObject : producers) {
|
||||
// use color producing mana abilities with costs first that produce all color manas that are needed to pay
|
||||
// otherwise the computer may not be able to pay the cost for that source
|
||||
ManaAbility:
|
||||
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
|
||||
int colored = 0;
|
||||
boolean canPayColoredMana = false;
|
||||
for (Mana mana : manaAbility.getNetMana(game)) {
|
||||
// if mana ability can produce non-useful mana then ignore whole ability here (example: {R} or {G})
|
||||
// (AI can't choose a good mana option, so make sure any selection option will be compatible with cost)
|
||||
// AI support {Any} choice by lastUnpaidMana, so it can safly used in includesMana
|
||||
if (!unpaid.getMana().includesMana(mana)) {
|
||||
continue ManaAbility;
|
||||
} else if (mana.getAny() > 0) {
|
||||
throw new IllegalArgumentException("Wrong mana calculation: AI do not support color choosing from {Any}");
|
||||
}
|
||||
if (mana.countColored() > 0) {
|
||||
canPayColoredMana = true;
|
||||
}
|
||||
colored = CardUtil.overflowInc(colored, mana.countColored());
|
||||
}
|
||||
if (colored > 1 && (cost instanceof ColoredManaCost)) {
|
||||
// found compatible source - try to pay
|
||||
if (canPayColoredMana && (cost instanceof ColoredManaCost)) {
|
||||
for (Mana netMana : manaAbility.getNetMana(game)) {
|
||||
if (cost.testPay(netMana)) {
|
||||
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
|
||||
|
|
@ -1605,6 +1614,7 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
}
|
||||
|
||||
// use any other mana produces
|
||||
for (MageObject mageObject : producers) {
|
||||
// pay all colored costs first
|
||||
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
|
||||
|
|
@ -1723,6 +1733,7 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
// pay phyrexian life costs
|
||||
if (cost.isPhyrexian()) {
|
||||
alreadyTryingToPayPhyrexian = true;
|
||||
// TODO: make sure it's thread safe and protected from modifications (cost/unpaid can be shared between AI simulation threads?)
|
||||
boolean paidPhyrexian = cost.pay(ability, game, ability, playerId, false, null) || hasApprovingObject;
|
||||
alreadyTryingToPayPhyrexian = false;
|
||||
return paidPhyrexian;
|
||||
|
|
@ -1956,29 +1967,33 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
chooseCreatureType(outcome, choice, game);
|
||||
}
|
||||
|
||||
// choose the correct color to pay a spell
|
||||
if (outcome == Outcome.PutManaInPool && choice.isManaColorChoice() && currentUnpaidMana != null) {
|
||||
if (currentUnpaidMana.containsColor(ColoredManaSymbol.W) && choice.getChoices().contains("White")) {
|
||||
// choose the correct color to pay a spell (use last unpaid ability for color hint)
|
||||
ManaCost unpaid = null;
|
||||
if (!lastUnpaidMana.isEmpty()) {
|
||||
unpaid = new ArrayList<>(lastUnpaidMana.values()).get(lastUnpaidMana.size() - 1);
|
||||
}
|
||||
if (outcome == Outcome.PutManaInPool && unpaid != null && choice.isManaColorChoice()) {
|
||||
if (unpaid.containsColor(ColoredManaSymbol.W) && choice.getChoices().contains("White")) {
|
||||
choice.setChoice("White");
|
||||
return true;
|
||||
}
|
||||
if (currentUnpaidMana.containsColor(ColoredManaSymbol.R) && choice.getChoices().contains("Red")) {
|
||||
if (unpaid.containsColor(ColoredManaSymbol.R) && choice.getChoices().contains("Red")) {
|
||||
choice.setChoice("Red");
|
||||
return true;
|
||||
}
|
||||
if (currentUnpaidMana.containsColor(ColoredManaSymbol.G) && choice.getChoices().contains("Green")) {
|
||||
if (unpaid.containsColor(ColoredManaSymbol.G) && choice.getChoices().contains("Green")) {
|
||||
choice.setChoice("Green");
|
||||
return true;
|
||||
}
|
||||
if (currentUnpaidMana.containsColor(ColoredManaSymbol.U) && choice.getChoices().contains("Blue")) {
|
||||
if (unpaid.containsColor(ColoredManaSymbol.U) && choice.getChoices().contains("Blue")) {
|
||||
choice.setChoice("Blue");
|
||||
return true;
|
||||
}
|
||||
if (currentUnpaidMana.containsColor(ColoredManaSymbol.B) && choice.getChoices().contains("Black")) {
|
||||
if (unpaid.containsColor(ColoredManaSymbol.B) && choice.getChoices().contains("Black")) {
|
||||
choice.setChoice("Black");
|
||||
return true;
|
||||
}
|
||||
if (currentUnpaidMana.getMana().getColorless() > 0 && choice.getChoices().contains("Colorless")) {
|
||||
if (unpaid.getMana().getColorless() > 0 && choice.getChoices().contains("Colorless")) {
|
||||
choice.setChoice("Colorless");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -2411,11 +2426,14 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
int deckMinSize = deckValidator != null ? deckValidator.getDeckMinSize() : 0;
|
||||
|
||||
if (deck != null && deck.getMaindeckCards().size() < deckMinSize && !deck.getSideboard().isEmpty()) {
|
||||
if (chosenColors == null) {
|
||||
if (chosenColors.isEmpty()) {
|
||||
for (Card card : deck.getSideboard()) {
|
||||
rememberPick(card, RateCard.rateCard(card, null));
|
||||
rememberPick(card, RateCard.rateCard(card, Collections.emptyList()));
|
||||
}
|
||||
List<ColoredManaSymbol> deckColors = chooseDeckColorsIfPossible();
|
||||
if (deckColors != null) {
|
||||
chosenColors.addAll(deckColors);
|
||||
}
|
||||
chosenColors = chooseDeckColorsIfPossible();
|
||||
}
|
||||
deck = buildDeck(deckMinSize, new ArrayList<>(deck.getSideboard()), chosenColors);
|
||||
}
|
||||
|
|
@ -2525,7 +2543,7 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
if (pickedCardRate <= 30) {
|
||||
// if card is bad
|
||||
// try to counter pick without any color restriction
|
||||
Card counterPick = pickBestCard(cards, null);
|
||||
Card counterPick = pickBestCard(cards, Collections.emptyList());
|
||||
int counterPickScore = RateCard.getBaseCardScore(counterPick);
|
||||
// card is really good
|
||||
// take it!
|
||||
|
|
@ -2537,11 +2555,14 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
|
||||
String colors = "not chosen yet";
|
||||
// remember card if colors are not chosen yet
|
||||
if (chosenColors == null) {
|
||||
if (chosenColors.isEmpty()) {
|
||||
rememberPick(bestCard, maxScore);
|
||||
chosenColors = chooseDeckColorsIfPossible();
|
||||
List<ColoredManaSymbol> chosen = chooseDeckColorsIfPossible();
|
||||
if (chosen != null) {
|
||||
chosenColors.addAll(chosen);
|
||||
}
|
||||
}
|
||||
if (chosenColors != null) {
|
||||
if (!chosenColors.isEmpty()) {
|
||||
colors = "";
|
||||
for (ColoredManaSymbol symbol : chosenColors) {
|
||||
colors += symbol.toString();
|
||||
|
|
@ -2612,7 +2633,7 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
if (colorsChosen.size() > 1) {
|
||||
// no need to remember picks anymore
|
||||
pickedCards = null;
|
||||
pickedCards.clear();
|
||||
return colorsChosen;
|
||||
}
|
||||
}
|
||||
|
|
@ -2915,14 +2936,6 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
}
|
||||
|
||||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
in.defaultReadObject();
|
||||
unplayable = new TreeMap<>();
|
||||
playableNonInstant = new ArrayList<>();
|
||||
playableInstant = new ArrayList<>();
|
||||
playableAbilities = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanUpOnMatchEnd() {
|
||||
super.cleanUpOnMatchEnd();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
|
||||
|
||||
package mage.cards.g;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.common.continuous.BoostTargetEffect;
|
||||
import mage.cards.CardImpl;
|
||||
|
|
@ -12,20 +9,21 @@ import mage.constants.Duration;
|
|||
import mage.constants.Outcome;
|
||||
import mage.target.common.TargetCreaturePermanent;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public final class GiantGrowth extends CardImpl {
|
||||
|
||||
public GiantGrowth(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{G}");
|
||||
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{G}");
|
||||
|
||||
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
|
||||
// Target creature gets +3/+3 until end of turn.
|
||||
Effect effect = new BoostTargetEffect(3, 3, Duration.EndOfTurn);
|
||||
effect.setOutcome(Outcome.Benefit);
|
||||
this.getSpellAbility().addEffect(effect);
|
||||
|
||||
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
|
||||
}
|
||||
|
||||
private GiantGrowth(final GiantGrowth card) {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,12 @@ public class SummonersEggTest extends CardTestPlayerBase {
|
|||
/**
|
||||
* Summoner's Egg
|
||||
* Artifact Creature — Construct 0/4, 4 (4)
|
||||
* Imprint — When Summoner's Egg enters the battlefield, you may exile a
|
||||
* Imprint — When Summoner's Egg enters the battlefield, you may exile a
|
||||
* card from your hand face down.
|
||||
* When Summoner's Egg dies, turn the exiled card face up. If it's a creature
|
||||
* When Summoner's Egg dies, turn the exiled card face up. If it's a creature
|
||||
* card, put it onto the battlefield under your control.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
// test that cards imprinted using Summoner's Egg are face down
|
||||
@Test
|
||||
public void testSummonersEggImprint() {
|
||||
|
|
@ -32,6 +31,10 @@ public class SummonersEggTest extends CardTestPlayerBase {
|
|||
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Summoner's Egg");
|
||||
setChoice(playerA, true); // use imprint
|
||||
setChoice(playerA, "Goblin Roughrider");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
|
|
@ -40,7 +43,7 @@ public class SummonersEggTest extends CardTestPlayerBase {
|
|||
assertHandCount(playerA, "Goblin Roughrider", 0);
|
||||
|
||||
assertExileCount("Goblin Roughrider", 1);
|
||||
for (Card card :currentGame.getExile().getAllCards(currentGame)){
|
||||
for (Card card : currentGame.getExile().getAllCards(currentGame)) {
|
||||
if (card.getName().equals("Goblin Roughrider")) {
|
||||
Assert.assertTrue("Exiled card is not face down", card.isFaceDown(currentGame));
|
||||
}
|
||||
|
|
@ -57,23 +60,26 @@ public class SummonersEggTest extends CardTestPlayerBase {
|
|||
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
|
||||
addCard(Zone.HAND, playerB, "Char");
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3);
|
||||
|
||||
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Summoner's Egg");
|
||||
setChoice(playerA, true); // use imprint
|
||||
setChoice(playerA, "Goblin Roughrider");
|
||||
|
||||
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Char", "Summoner's Egg");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
|
||||
assertHandCount(playerA, 1);
|
||||
assertHandCount(playerA, "Maritime Guard", 1);
|
||||
assertHandCount(playerA, "Goblin Roughrider", 0);
|
||||
|
||||
|
||||
assertGraveyardCount(playerA, "Summoner's Egg", 1);
|
||||
|
||||
assertExileCount("Goblin Roughrider", 0);
|
||||
assertPermanentCount(playerA, "Goblin Roughrider", 1);
|
||||
for (Permanent p :currentGame.getBattlefield().getAllActivePermanents()){
|
||||
for (Permanent p : currentGame.getBattlefield().getAllActivePermanents()) {
|
||||
if (p.getName().equals("Goblin Roughrider")) {
|
||||
Assert.assertTrue("Permanent is not face up", !p.isFaceDown(currentGame));
|
||||
}
|
||||
|
|
@ -89,26 +95,29 @@ public class SummonersEggTest extends CardTestPlayerBase {
|
|||
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
|
||||
addCard(Zone.HAND, playerB, "Char");
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3);
|
||||
|
||||
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Summoner's Egg");
|
||||
setChoice(playerA, true); // use imprint
|
||||
setChoice(playerA, "Forest");
|
||||
|
||||
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Char", "Summoner's Egg");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
|
||||
assertHandCount(playerA, 1);
|
||||
assertHandCount(playerA, "Forest", 1);
|
||||
|
||||
|
||||
assertGraveyardCount(playerA, "Summoner's Egg", 1);
|
||||
|
||||
assertExileCount("Forest", 1);
|
||||
for (Card card :currentGame.getExile().getAllCards(currentGame)){
|
||||
for (Card card : currentGame.getExile().getAllCards(currentGame)) {
|
||||
if (card.getName().equals("Forest")) {
|
||||
Assert.assertTrue("Exiled card is not face up", !card.isFaceDown(currentGame));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
package org.mage.test.cards.mana;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class EnergyRefractorManaCalculationsTest extends CardTestPlayerBase {
|
||||
|
||||
@Test
|
||||
public void test_Single() {
|
||||
// {2}: Add one mana of any color.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Energy Refractor", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
|
||||
|
||||
// make sure it works
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: ");
|
||||
setChoice(playerA, "Red");
|
||||
checkManaPool("mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // TODO: must fix infinite mana calculation
|
||||
public void test_Multiple() {
|
||||
int cardsAmount = 2; // after fix make it 10+ for testing
|
||||
|
||||
// {2}: Add one mana of any color.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Energy Refractor", cardsAmount);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 * cardsAmount);
|
||||
|
||||
runCode("simple way to cause freeze", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
|
||||
player.getManaAvailable(game);
|
||||
});
|
||||
|
||||
// make sure it works
|
||||
//activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: ");
|
||||
//setChoice(playerA, "Red");
|
||||
//checkManaPool("mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package org.mage.test.cards.mana;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class InfiniteManaUsagesTest extends CardTestPlayerBaseWithAIHelps {
|
||||
|
||||
@Test
|
||||
public void test_EnergyRefractor_Single_AddToPool() {
|
||||
// {2}: Add one mana of any color.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Energy Refractor", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
|
||||
|
||||
// make sure it works
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: ");
|
||||
setChoice(playerA, "Red");
|
||||
checkManaPool("mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_EnergyRefractor_Multiple_AddToPool() {
|
||||
int cardsAmount = 20;
|
||||
|
||||
// {2}: Add one mana of any color.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Energy Refractor", cardsAmount);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 * cardsAmount);
|
||||
|
||||
// make sure it works
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: ");
|
||||
setChoice(playerA, "Red");
|
||||
checkManaPool("mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_EnergyRefractor_CastBears_Manual() {
|
||||
// {2}: Add one mana of any color.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Energy Refractor", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 + 1);
|
||||
//
|
||||
addCard(Zone.HAND, playerA, "Grizzly Bears", 1); // {1}{G}
|
||||
|
||||
// make sure it works
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears");
|
||||
setChoice(playerA, "Green");
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "Grizzly Bears", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_EnergyRefractor_CastBears_AI() {
|
||||
// possible bug: StackOverflowError on mana usage
|
||||
|
||||
// {2}: Add one mana of any color.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Energy Refractor", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 + 1);
|
||||
//
|
||||
addCard(Zone.HAND, playerA, "Grizzly Bears", 1); // {1}{G}
|
||||
|
||||
// ai must see playable card and cast it
|
||||
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "Grizzly Bears", 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
package org.mage.test.cards.triggers;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
|
|
@ -6,37 +5,40 @@ import mage.constants.Zone;
|
|||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jeffwadsworth
|
||||
*/
|
||||
/**
|
||||
* Selvala, Heart of the Wilds {1}{G}{G} Whenever another creature enters the
|
||||
* battlefield, its controller may draw a card if its power is greater than each
|
||||
* other creature's power Add X mana in any combination of colors to your mana
|
||||
* pool, where X is the greatest power among creatures you control
|
||||
*
|
||||
* @author jeffwadsworth
|
||||
*/
|
||||
public class SelvalaHeartOfTheWildsTest extends CardTestPlayerBase {
|
||||
|
||||
@Test
|
||||
public void testTrigger() {
|
||||
// No card will be drawn due to the Memnite having a lower power than any other permanent on the battlefield
|
||||
addCard(Zone.LIBRARY, playerA, "Island", 2);
|
||||
public void test_NoTriggerOnLowerPower() {
|
||||
skipInitShuffling();
|
||||
addCard(Zone.LIBRARY, playerA, "Island", 1);
|
||||
|
||||
// Whenever another creature enters the battlefield, its controller may draw a card if its
|
||||
// power is greater than each other creature's power.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Selvala, Heart of the Wilds", 1); // 2/3
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Shivan Dragon", 1); // 5/5
|
||||
//
|
||||
addCard(Zone.HAND, playerA, "Memnite"); // 1/1
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Shivan Dragon", 1); // 5/5
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Blinking Spirit", 1); // 2/2
|
||||
// Nightmare's power and toughness are each equal to the number of Swamps you control.
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Nightmare", 1); // 4/4
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4);
|
||||
|
||||
// cast low power memnite - no draw trigger
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
|
||||
execute();
|
||||
|
||||
assertHandCount(playerA, 0); // no cards drawn
|
||||
|
||||
assertHandCount(playerA, 0); // no draw
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,40 +46,45 @@ public class SelvalaHeartOfTheWildsTest extends CardTestPlayerBase {
|
|||
* pumps its power to the highest on the battlefield allowing the controller to draw a card.
|
||||
*/
|
||||
@Test
|
||||
public void testTriggerWithGiantGrowth() {
|
||||
addCard(Zone.LIBRARY, playerA, "Island", 2);
|
||||
// Whenever another creature enters the battlefield, its controller may draw a card if its power is greater than each other creature's power.
|
||||
// {G}, {T}: Add X mana in any combination of colors, where X is the greatest power among creatures you control.
|
||||
public void test_TriggerOnBigBoosted() {
|
||||
skipInitShuffling();
|
||||
addCard(Zone.LIBRARY, playerA, "Island", 1);
|
||||
|
||||
// Whenever another creature enters the battlefield, its controller may draw a card if its
|
||||
// power is greater than each other creature's power.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Selvala, Heart of the Wilds", 1); // 2/3
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Shivan Dragon", 1); // 5/5
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
//
|
||||
addCard(Zone.HAND, playerA, "Memnite"); // 1/1
|
||||
addCard(Zone.HAND, playerA, "Giant Growth", 2);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Shivan Dragon", 1); // 5/5
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Blinking Spirit", 1); // 2/2
|
||||
// Flying
|
||||
// Nightmare's power and toughness are each equal to the number of Swamps you control.
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Nightmare", 1); // 4/4
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4);
|
||||
//
|
||||
// Target creature gets +3/+3 until end of turn.
|
||||
addCard(Zone.HAND, playerA, "Giant Growth", 2); // {G}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
|
||||
// prepare etb trigger
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite");
|
||||
setChoice(playerA, "X=0");
|
||||
setChoice(playerA, "X=0");
|
||||
setChoice(playerA, "X=0");
|
||||
setChoice(playerA, "X=0");
|
||||
setChoice(playerA, "X=5");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN ,1);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Giant Growth", "Memnite");
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Giant Growth", "Memnite"); // a whopping 7/7
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
|
||||
checkStackSize("must have etb", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1);
|
||||
checkStackObject("must have etb", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever another creature", 1);
|
||||
|
||||
setChoice(playerA, true);
|
||||
// boost before trigger resolve (make memnite 7/7)
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Giant Growth", "Memnite", "Whenever another creature");
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Giant Growth", "Memnite", "Whenever another creature");
|
||||
checkStackSize("must have etb + boost spells", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 3);
|
||||
|
||||
// trigger on grater power
|
||||
setChoice(playerA, true); // draw card
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
|
||||
execute();
|
||||
|
||||
assertPowerToughness(playerA, "Memnite", 7, 7);
|
||||
assertGraveyardCount(playerA, "Giant Growth", 2);
|
||||
assertHandCount(playerA, 1); // 2 cards drawn
|
||||
|
||||
assertHandCount(playerA, "Island", 1); // after draw
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package org.mage.test.utils;
|
||||
|
||||
import mage.Mana;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class ManaIncludesTest extends CardTestPlayerBase {
|
||||
|
||||
private void assertFullyIncludes(boolean canPay, String cost, String pool) {
|
||||
// workaround to add {Any} mana by string param
|
||||
String strictMain = pool.replace("{Any}", "");
|
||||
String stringPart = cost.replace("{Any}", "");
|
||||
|
||||
Mana manaMain = new ManaCostsImpl<>(strictMain).getMana();
|
||||
Mana manaPart = new ManaCostsImpl<>(stringPart).getMana();
|
||||
|
||||
manaMain.add(new Mana(0, 0, 0, 0, 0, 0, (pool.length() - strictMain.length()) / 5, 0));
|
||||
manaPart.add(new Mana(0, 0, 0, 0, 0, 0, (cost.length() - stringPart.length()) / 5, 0));
|
||||
|
||||
Assert.assertEquals(canPay, manaMain.includesMana(manaPart));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_ManaIncludes() {
|
||||
assertFullyIncludes(true, "", "");
|
||||
assertFullyIncludes(true, "", "{R}");
|
||||
assertFullyIncludes(true, "", "{C}");
|
||||
assertFullyIncludes(true, "", "{1}");
|
||||
assertFullyIncludes(true, "", "{Any}");
|
||||
assertFullyIncludes(true, "", "{R}{G}");
|
||||
assertFullyIncludes(true, "", "{C}{G}");
|
||||
assertFullyIncludes(true, "", "{1}{G}");
|
||||
assertFullyIncludes(true, "", "{Any}{G}");
|
||||
|
||||
assertFullyIncludes(false, "{B}", "");
|
||||
assertFullyIncludes(false, "{B}", "{R}");
|
||||
assertFullyIncludes(false, "{B}", "{C}");
|
||||
assertFullyIncludes(false, "{B}", "{1}");
|
||||
assertFullyIncludes(true, "{B}", "{Any}");
|
||||
assertFullyIncludes(false, "{B}", "{R}{G}");
|
||||
assertFullyIncludes(false, "{B}", "{C}{G}");
|
||||
assertFullyIncludes(false, "{B}", "{1}{G}");
|
||||
assertFullyIncludes(true, "{B}", "{Any}{G}");
|
||||
|
||||
assertFullyIncludes(false, "{G}", "");
|
||||
assertFullyIncludes(false, "{G}", "{R}");
|
||||
assertFullyIncludes(false, "{G}", "{C}");
|
||||
assertFullyIncludes(false, "{G}", "{1}");
|
||||
assertFullyIncludes(true, "{G}", "{Any}");
|
||||
assertFullyIncludes(true, "{G}", "{R}{G}");
|
||||
assertFullyIncludes(true, "{G}", "{C}{G}");
|
||||
assertFullyIncludes(true, "{G}", "{1}{G}");
|
||||
assertFullyIncludes(true, "{G}", "{Any}{G}");
|
||||
|
||||
assertFullyIncludes(false, "{C}", "");
|
||||
assertFullyIncludes(false, "{C}", "{R}");
|
||||
assertFullyIncludes(true, "{C}", "{C}");
|
||||
assertFullyIncludes(false, "{C}", "{1}");
|
||||
assertFullyIncludes(false, "{C}", "{Any}");
|
||||
assertFullyIncludes(false, "{C}", "{R}{G}");
|
||||
assertFullyIncludes(true, "{C}", "{C}{G}");
|
||||
assertFullyIncludes(false, "{C}", "{1}{G}");
|
||||
assertFullyIncludes(false, "{C}", "{Any}{G}");
|
||||
|
||||
assertFullyIncludes(false, "{1}", "");
|
||||
assertFullyIncludes(true, "{1}", "{R}");
|
||||
assertFullyIncludes(true, "{1}", "{C}");
|
||||
assertFullyIncludes(true, "{1}", "{1}");
|
||||
assertFullyIncludes(true, "{1}", "{Any}");
|
||||
assertFullyIncludes(true, "{1}", "{R}{G}");
|
||||
assertFullyIncludes(true, "{1}", "{C}{G}");
|
||||
assertFullyIncludes(true, "{1}", "{1}{G}");
|
||||
assertFullyIncludes(true, "{1}", "{Any}{G}");
|
||||
|
||||
assertFullyIncludes(false, "{Any}", "");
|
||||
assertFullyIncludes(false, "{Any}", "{R}");
|
||||
assertFullyIncludes(false, "{Any}", "{C}");
|
||||
assertFullyIncludes(false, "{Any}", "{1}");
|
||||
assertFullyIncludes(true, "{Any}", "{Any}");
|
||||
assertFullyIncludes(false, "{Any}", "{R}{G}");
|
||||
assertFullyIncludes(false, "{Any}", "{C}{G}");
|
||||
assertFullyIncludes(false, "{Any}", "{1}{G}");
|
||||
assertFullyIncludes(true, "{Any}", "{Any}{G}");
|
||||
|
||||
// possible integer overflow problems
|
||||
String maxGeneric = "{" + Integer.MAX_VALUE + "}";
|
||||
assertFullyIncludes(false, maxGeneric, "");
|
||||
assertFullyIncludes(false, maxGeneric, "{1}");
|
||||
assertFullyIncludes(false, maxGeneric, "{R}");
|
||||
assertFullyIncludes(true, "", maxGeneric);
|
||||
assertFullyIncludes(true, "{1}", maxGeneric);
|
||||
assertFullyIncludes(false, "{R}", maxGeneric);
|
||||
assertFullyIncludes(true, maxGeneric, maxGeneric);
|
||||
|
||||
// data from infinite mana calcs bug
|
||||
assertFullyIncludes(false, "{R}{R}", "{R}");
|
||||
assertFullyIncludes(true, "{R}{R}", "{R}{R}");
|
||||
assertFullyIncludes(true, "{R}{R}", "{R}{R}{R}");
|
||||
assertFullyIncludes(true, "{R}{R}", "{R}{R}{R}{R}");
|
||||
assertFullyIncludes(true, "{R}{R}", "{R}{R}{R}{R}{R}");
|
||||
assertFullyIncludes(false, "{Any}{Any}", "{Any}");
|
||||
assertFullyIncludes(true, "{Any}{Any}", "{Any}{Any}");
|
||||
assertFullyIncludes(true, "{Any}{Any}", "{Any}{Any}{Any}");
|
||||
assertFullyIncludes(true, "{Any}{Any}", "{Any}{Any}{Any}{Any}");
|
||||
assertFullyIncludes(true, "{Any}{Any}", "{Any}{Any}{Any}{Any}{Any}");
|
||||
|
||||
// additional use cases
|
||||
assertFullyIncludes(false, "{W}{W}", "{W}{B}");
|
||||
}
|
||||
}
|
||||
|
|
@ -2765,7 +2765,7 @@ public class VerifyCardDataTest {
|
|||
List<Card> cardsList = new ArrayList<>(CardScanner.getAllCards());
|
||||
Map<String, Integer> cardRates = new HashMap<>();
|
||||
for (Card card : cardsList) {
|
||||
int curRate = RateCard.rateCard(card, null, false);
|
||||
int curRate = RateCard.rateCard(card, Collections.emptyList(), false);
|
||||
int prevRate = cardRates.getOrDefault(card.getName(), 0);
|
||||
if (prevRate == 0) {
|
||||
cardRates.putIfAbsent(card.getName(), curRate);
|
||||
|
|
|
|||
|
|
@ -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