diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index be0361f369c..0b8bf14c558 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -41,7 +41,6 @@ import forge.game.zone.ZoneType; import forge.util.MyRandom; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import java.util.*; @@ -67,16 +66,6 @@ public class ComputerUtilMana { return payManaCost(cost, sa, ai, test, checkPlayable, effect); } - private static void refundMana(List manaSpent, Player ai, SpellAbility sa) { - if (sa.getHostCard() != null) { - sa.getHostCard().setCanCounter(true); - } - for (final Mana m : manaSpent) { - ai.getManaPool().addMana(m); - } - manaSpent.clear(); - } - /** * Return the number of colors used for payment for Converge */ @@ -598,7 +587,7 @@ public class ComputerUtilMana { } // get a mana of this type from floating, bail if none available - final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), (byte) -1, cost.getXManaCostPaidByColor()); + final Mana mana = CostPayment.getMana(ai, part, sa, cost.getSourceRestriction(), (byte) -1, cost.getXManaCostPaidByColor()); if (mana != null) { if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, false)) { manaSpentToPay.add(0, mana); @@ -609,18 +598,16 @@ public class ComputerUtilMana { if (cost.isPaid()) { // refund any mana taken from mana pool when test - refundMana(manaSpentToPay, ai, sa); - - handleOfferingsAI(sa, true, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, true, cost.isPaid()); return manaSources; } // arrange all mana abilities by color produced. final ListMultimap manaAbilityMap = groupSourcesByManaColor(ai, true); if (manaAbilityMap.isEmpty()) { - refundMana(manaSpentToPay, ai, sa); - - handleOfferingsAI(sa, true, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, true, cost.isPaid()); return manaSources; } @@ -666,9 +653,8 @@ public class ComputerUtilMana { Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard())); } - handleOfferingsAI(sa, true, cost.isPaid()); - - refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, true, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); return manaSources; } // getManaSourcesToPayCost() @@ -681,7 +667,7 @@ public class ComputerUtilMana { List manaSpentToPay = test ? new ArrayList<>() : sa.getPayingMana(); List paymentList = Lists.newArrayList(); - if (payManaCostFromPool(cost, sa, ai, test, manaSpentToPay)) { + if (ManaPool.payManaCostFromPool(cost, sa, ai, test, manaSpentToPay)) { return true; // paid all from floating mana } @@ -870,7 +856,7 @@ public class ComputerUtilMana { } } - handleOfferingsAI(sa, test, cost.isPaid()); + CostPayment.handleOfferings(sa, test, cost.isPaid()); // if (DEBUG_MANA_PAYMENT) { // System.err.printf("%s > [%s] payment has %s (%s +%d) for (%s) %s:%n\t%s%n%n", @@ -880,7 +866,7 @@ public class ComputerUtilMana { // The cost is still unpaid, so refund the mana and report if (!cost.isPaid()) { - refundMana(manaSpentToPay, ai, sa); + ManaPool.refundMana(manaSpentToPay, ai, sa); if (test) { resetPayment(paymentList); return false; @@ -891,7 +877,7 @@ public class ComputerUtilMana { } if (test) { - refundMana(manaSpentToPay, ai, sa); + ManaPool.refundMana(manaSpentToPay, ai, sa); resetPayment(paymentList); } @@ -929,8 +915,8 @@ public class ComputerUtilMana { final ListMultimap manaAbilityMap = groupSourcesByManaColor(ai, checkPlayable); if (manaAbilityMap.isEmpty()) { // no mana abilities, bailing out - refundMana(manaSpentToPay, ai, sa); - handleOfferingsAI(sa, test, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, test, cost.isPaid()); return null; } if (DEBUG_MANA_PAYMENT) { @@ -968,157 +954,6 @@ public class ComputerUtilMana { return sourcesForShards; } - /** - * Checks if the given mana cost can be paid from floating mana. - * @param cost mana cost to pay for - * @param sa ability to pay for - * @param ai activating player - * @param test actual payment is made if this is false - * @param manaSpentToPay list of mana spent - * @return whether the floating mana is sufficient to pay the cost fully - */ - public static boolean payManaCostFromPool(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, - final boolean test, List manaSpentToPay) { - final boolean hasConverge = sa.getHostCard().hasConverge(); - List unpaidShards = cost.getUnpaidShards(); - Collections.sort(unpaidShards); // most difficult shards must come first - for (ManaCostShard part : unpaidShards) { - if (part != ManaCostShard.X) { - if (cost.isPaid()) { - continue; - } - - // get a mana of this type from floating, bail if none available - final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), hasConverge ? cost.getColorsPaid() : -1, cost.getXManaCostPaidByColor()); - if (mana != null) { - if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, test)) { - manaSpentToPay.add(0, mana); - } - } - } - } - - // refund any mana taken from mana pool when test - if (test) { - refundMana(manaSpentToPay, ai, sa); - } - - if (cost.isPaid()) { - handleOfferingsAI(sa, test, cost.isPaid()); - return true; - } - return false; - } - - /** - *

- * getManaFrom. - *

- * - * @param saBeingPaidFor - * a {@link forge.game.spellability.SpellAbility} object. - * @return a {@link forge.game.mana.Mana} object. - */ - private static Mana getMana(final Player ai, final ManaCostShard shard, final SpellAbility saBeingPaidFor, - String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { - final List> weightedOptions = selectManaToPayFor(ai.getManaPool(), shard, - saBeingPaidFor, restriction, colorsPaid, xManaCostPaidByColor); - - // Exclude border case - if (weightedOptions.isEmpty()) { - return null; // There is no matching mana in the pool - } - - // select equal weight possibilities - List manaChoices = new ArrayList<>(); - int bestWeight = Integer.MIN_VALUE; - for (Pair option : weightedOptions) { - int thisWeight = option.getRight(); - Mana thisMana = option.getLeft(); - - if (thisWeight > bestWeight) { - manaChoices.clear(); - bestWeight = thisWeight; - } - - if (thisWeight == bestWeight) { - // add only distinct Mana-s - boolean haveDuplicate = false; - for (Mana m : manaChoices) { - if (m.equals(thisMana)) { - haveDuplicate = true; - break; - } - } - if (!haveDuplicate) { - manaChoices.add(thisMana); - } - } - } - - // got an only one best option? - if (manaChoices.size() == 1) { - return manaChoices.get(0); - } - - // if we are simulating mana payment for the human controller, use the first mana available (and avoid prompting the human player) - if (!(ai.getController() instanceof PlayerControllerAi)) { - return manaChoices.get(0); - } - - // Let them choose then - return ai.getController().chooseManaFromPool(manaChoices); - } - - private static List> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard, - final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { - final List> weightedOptions = new ArrayList<>(); - for (final Mana thisMana : manapool) { - if (shard == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(thisMana.getColor()), xManaCostPaidByColor)) { - continue; - } - - if (!manapool.canPayForShardWithColor(shard, thisMana.getColor())) { - continue; - } - - if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) { - continue; - } - - boolean canPay = manapool.canPayForShardWithColor(shard, thisMana.getColor()); - if (!canPay || (shard.isSnow() && !thisMana.isSnow())) { - continue; - } - - if (StringUtils.isNotBlank(restriction) && !thisMana.getSourceCard().getType().hasStringType(restriction)) { - continue; - } - - int weight = 0; - if (colorsPaid == -1) { - // prefer colorless mana to spend - weight += thisMana.isColorless() ? 5 : 0; - } else { - // get more colors for converge - weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0; - } - - // prefer restricted mana to spend - if (thisMana.isRestricted()) { - weight += 2; - } - - // Spend non-snow mana first - if (!thisMana.isSnow()) { - weight += 1; - } - - weightedOptions.add(Pair.of(thisMana, weight)); - } - return weightedOptions; - } - private static void setExpressColorChoice(final SpellAbility sa, final Player ai, ManaCostBeingPaid cost, ManaCostShard toPay, SpellAbility saPayment) { @@ -1936,25 +1771,6 @@ public class ComputerUtilMana { return res; } - private static void handleOfferingsAI(final SpellAbility sa, boolean test, boolean costIsPaid) { - if (sa.isOffering() && sa.getSacrificedAsOffering() != null) { - final Card offering = sa.getSacrificedAsOffering(); - offering.setUsedToPay(false); - if (costIsPaid && !test) { - sa.getHostCard().getGame().getAction().sacrifice(offering, sa, false, null, null); - } - sa.resetSacrificedAsOffering(); - } - if (sa.isEmerge() && sa.getSacrificedAsEmerge() != null) { - final Card emerge = sa.getSacrificedAsEmerge(); - emerge.setUsedToPay(false); - if (costIsPaid && !test) { - sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, false, null, null); - } - sa.resetSacrificedAsEmerge(); - } - } - /** * Matches list of creatures to shards in mana cost for convoking. * @param cost cost of convoked ability diff --git a/forge-game/src/main/java/forge/game/cost/CostPayment.java b/forge-game/src/main/java/forge/game/cost/CostPayment.java index 74e1836d044..bce4340ec24 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPayment.java +++ b/forge-game/src/main/java/forge/game/cost/CostPayment.java @@ -17,15 +17,25 @@ */ package forge.game.cost; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import forge.card.MagicColor; +import forge.card.mana.ManaCostShard; import forge.game.Game; import forge.game.card.Card; +import forge.game.mana.Mana; import forge.game.mana.ManaConversionMatrix; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.mana.ManaPool; +import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -221,4 +231,132 @@ public class CostPayment extends ManaConversionMatrix { } return true; } + + /** + *

+ * getManaFrom. + *

+ * + * @param saBeingPaidFor + * a {@link forge.game.spellability.SpellAbility} object. + * @return a {@link forge.game.mana.Mana} object. + */ + public static Mana getMana(final Player ai, final ManaCostShard shard, final SpellAbility saBeingPaidFor, + String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { + final List> weightedOptions = selectManaToPayFor(ai.getManaPool(), shard, + saBeingPaidFor, restriction, colorsPaid, xManaCostPaidByColor); + + // Exclude border case + if (weightedOptions.isEmpty()) { + return null; // There is no matching mana in the pool + } + + // select equal weight possibilities + List manaChoices = new ArrayList<>(); + int bestWeight = Integer.MIN_VALUE; + for (Pair option : weightedOptions) { + int thisWeight = option.getRight(); + Mana thisMana = option.getLeft(); + + if (thisWeight > bestWeight) { + manaChoices.clear(); + bestWeight = thisWeight; + } + + if (thisWeight == bestWeight) { + // add only distinct Mana-s + boolean haveDuplicate = false; + for (Mana m : manaChoices) { + if (m.equals(thisMana)) { + haveDuplicate = true; + break; + } + } + if (!haveDuplicate) { + manaChoices.add(thisMana); + } + } + } + + // got an only one best option? + if (manaChoices.size() == 1) { + return manaChoices.get(0); + } + + // if we are simulating mana payment for the human controller, use the first mana available (and avoid prompting the human player) + if (!ai.getController().isAI()) { + return manaChoices.get(0); + } + + // Let them choose then + return ai.getController().chooseManaFromPool(manaChoices); + } + + private static List> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard, + final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { + final List> weightedOptions = new ArrayList<>(); + for (final Mana thisMana : manapool) { + if (shard == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(thisMana.getColor()), xManaCostPaidByColor)) { + continue; + } + + if (!manapool.canPayForShardWithColor(shard, thisMana.getColor())) { + continue; + } + + if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) { + continue; + } + + boolean canPay = manapool.canPayForShardWithColor(shard, thisMana.getColor()); + if (!canPay || (shard.isSnow() && !thisMana.isSnow())) { + continue; + } + + if (StringUtils.isNotBlank(restriction) && !thisMana.getSourceCard().getType().hasStringType(restriction)) { + continue; + } + + int weight = 0; + if (colorsPaid == -1) { + // prefer colorless mana to spend + weight += thisMana.isColorless() ? 5 : 0; + } else { + // get more colors for converge + weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0; + } + + // prefer restricted mana to spend + if (thisMana.isRestricted()) { + weight += 2; + } + + // Spend non-snow mana first + if (!thisMana.isSnow()) { + weight += 1; + } + + weightedOptions.add(Pair.of(thisMana, weight)); + } + return weightedOptions; + } + + public static void handleOfferings(final SpellAbility sa, boolean test, boolean costIsPaid) { + if (sa.isOffering() && sa.getSacrificedAsOffering() != null) { + final Card offering = sa.getSacrificedAsOffering(); + offering.setUsedToPay(false); + if (costIsPaid && !test) { + sa.getHostCard().getGame().getAction().sacrifice(offering, sa, false, null, null); + } + sa.resetSacrificedAsOffering(); + } + if (sa.isEmerge() && sa.getSacrificedAsEmerge() != null) { + final Card emerge = sa.getSacrificedAsEmerge(); + emerge.setUsedToPay(false); + if (costIsPaid && !test) { + sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, false, null, null); + } + sa.resetSacrificedAsEmerge(); + } + } } diff --git a/forge-game/src/main/java/forge/game/mana/ManaPool.java b/forge-game/src/main/java/forge/game/mana/ManaPool.java index 3fe18c50722..682833edbf9 100644 --- a/forge-game/src/main/java/forge/game/mana/ManaPool.java +++ b/forge-game/src/main/java/forge/game/mana/ManaPool.java @@ -18,6 +18,7 @@ package forge.game.mana; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -33,6 +34,7 @@ import forge.card.mana.ManaCostShard; import forge.game.Game; import forge.game.GlobalRuleChange; import forge.game.ability.AbilityKey; +import forge.game.cost.CostPayment; import forge.game.event.EventValueChangeType; import forge.game.event.GameEventManaPool; import forge.game.event.GameEventZone; @@ -289,6 +291,16 @@ public class ManaPool extends ManaConversionMatrix implements Iterable { return true; } + public static void refundMana(List manaSpent, Player ai, SpellAbility sa) { + if (sa.getHostCard() != null) { + sa.getHostCard().setCanCounter(true); + } + for (final Mana m : manaSpent) { + ai.getManaPool().addMana(m); + } + manaSpent.clear(); + } + public final void refundManaPaid(final SpellAbility sa) { // Send all mana back to your mana pool, before accounting for it. final List manaPaid = sa.getPayingMana(); @@ -342,6 +354,47 @@ public class ManaPool extends ManaConversionMatrix implements Iterable { return shard.canBePaidWithManaOfColor((byte)0); } + /** + * Checks if the given mana cost can be paid from floating mana. + * @param cost mana cost to pay for + * @param sa ability to pay for + * @param ai activating player + * @param test actual payment is made if this is false + * @param manaSpentToPay list of mana spent + * @return whether the floating mana is sufficient to pay the cost fully + */ + public static boolean payManaCostFromPool(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, + final boolean test, List manaSpentToPay) { + final boolean hasConverge = sa.getHostCard().hasConverge(); + List unpaidShards = cost.getUnpaidShards(); + Collections.sort(unpaidShards); // most difficult shards must come first + for (ManaCostShard part : unpaidShards) { + if (part != ManaCostShard.X) { + if (cost.isPaid()) { + continue; + } + + // get a mana of this type from floating, bail if none available + final Mana mana = CostPayment.getMana(ai, part, sa, cost.getSourceRestriction(), hasConverge ? cost.getColorsPaid() : -1, cost.getXManaCostPaidByColor()); + if (mana != null) { + if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, test)) { + manaSpentToPay.add(0, mana); + } + } + } + } + + if (cost.isPaid()) { + // refund any mana taken from mana pool when test + if (test) { + refundMana(manaSpentToPay, ai, sa); + } + CostPayment.handleOfferings(sa, test, cost.isPaid()); + return true; + } + return false; + } + @Override public Iterator iterator() { return floatingMana.values().iterator(); diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java index ef3a63a148b..a9160c0f85c 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java @@ -1,14 +1,14 @@ package forge.gamemodes.match.input; import java.util.ArrayList; +import java.util.List; -import forge.ai.ComputerUtilMana; import forge.card.mana.ManaAtom; import forge.card.mana.ManaCostShard; -import forge.game.cost.CostPayment; import forge.game.mana.Mana; import forge.game.mana.ManaConversionMatrix; import forge.game.mana.ManaCostBeingPaid; +import forge.game.mana.ManaPool; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.localinstance.properties.ForgePreferences; @@ -28,7 +28,9 @@ public class InputPayManaOfCostPayment extends InputPayMana { // CR 118.3c forced cast must use pool mana // TODO this introduces a small risk to lock up the GUI if the human "wastes" enough mana for abilities like Doubling Cube if (spellAbility.getPayCosts().isMandatory()) { - mandatory = ComputerUtilMana.payManaCostFromPool(new ManaCostBeingPaid(cost), spellAbility, payer, true, new ArrayList()); + List refund = new ArrayList<>(); + mandatory = ManaPool.payManaCostFromPool(new ManaCostBeingPaid(cost), spellAbility, payer, true, refund); + ManaPool.refundMana(refund, payer, spellAbility); } // Set Mana cost being paid for SA to be able to reference it later