> weightedOptions = new ArrayList<>();
for (final Mana thisMana : manapool) {
if (!manapool.canPayForShardWithColor(shard, thisMana.getColor())) {
continue;
}
- if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsManaRestrictions(saBeingPaidFor)) {
+ if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) {
continue;
}
@@ -756,11 +1011,11 @@ public class ComputerUtilMana {
int weight = 0;
if (colorsPaid == -1) {
- // prefer colorless mana to spend
- weight += thisMana.isColorless() ? 5 : 0;
+ // prefer colorless mana to spend
+ weight += thisMana.isColorless() ? 5 : 0;
} else {
- // get more colors for converge
- weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0;
+ // get more colors for converge
+ weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0;
}
// prefer restricted mana to spend
@@ -777,7 +1032,7 @@ public class ComputerUtilMana {
}
return weightedOptions;
}
-
+
private static void setExpressColorChoice(final SpellAbility sa, final Player ai, ManaCostBeingPaid cost,
ManaCostShard toPay, SpellAbility saPayment) {
@@ -818,7 +1073,7 @@ public class ComputerUtilMana {
if (isManaSourceReserved(ai, sourceCard, sa)) {
return false;
}
-
+
if (toPay.isSnow() && !sourceCard.isSnow()) {
return false;
}
@@ -837,10 +1092,9 @@ public class ComputerUtilMana {
if (checkCosts) {
// Check if AI can still play this mana ability
ma.setActivatingPlayer(ai);
- if (ma.getPayCosts() != null) { // if the AI can't pay the additional costs skip the mana ability
- if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) {
- return false;
- }
+ // if the AI can't pay the additional costs skip the mana ability
+ if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) {
+ return false;
}
else if (sourceCard.isTapped()) {
return false;
@@ -965,7 +1219,7 @@ public class ComputerUtilMana {
*
* getComboManaChoice.
*
- *
+ *
* @param manaAb
* a {@link forge.game.spellability.SpellAbility} object.
* @param saRoot
@@ -989,7 +1243,7 @@ public class ComputerUtilMana {
choice = abMana.getExpressChoice();
abMana.clearExpressChoice();
byte colorMask = ManaAtom.fromName(choice);
- if (abMana.canProduce(choice, manaAb) && testCost.isAnyPartPayableWith(colorMask, ai.getManaPool())) {
+ if (manaAb.canProduce(choice) && satisfiesColorChoice(abMana, choiceString, choice) && testCost.isAnyPartPayableWith(colorMask, ai.getManaPool())) {
choiceString.append(choice);
payMultipleMana(testCost, choice, ai);
continue;
@@ -999,7 +1253,7 @@ public class ComputerUtilMana {
if (!testCost.isPaid()) {
// Loop over combo colors
for (String color : comboColors) {
- if (testCost.isAnyPartPayableWith(ManaAtom.fromName(color), ai.getManaPool())) {
+ if (satisfiesColorChoice(abMana, choiceString, choice) && testCost.isAnyPartPayableWith(ManaAtom.fromName(color), ai.getManaPool())) {
payMultipleMana(testCost, color, ai);
if (nMana != 1) {
choiceString.append(" ");
@@ -1014,14 +1268,18 @@ public class ComputerUtilMana {
}
}
// check if combo mana can produce most common color in hand
- String commonColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(
- ZoneType.Hand));
- if (!commonColor.isEmpty() && abMana.getComboColors().contains(MagicColor.toShortString(commonColor))) {
+ String commonColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Hand));
+ if (!commonColor.isEmpty() && satisfiesColorChoice(abMana, choiceString, MagicColor.toShortString(commonColor)) && abMana.getComboColors().contains(MagicColor.toShortString(commonColor))) {
choice = MagicColor.toShortString(commonColor);
}
else {
- // default to first color
- choice = comboColors[0];
+ // default to first available color
+ for (String c : comboColors) {
+ if (satisfiesColorChoice(abMana, choiceString, c)) {
+ choice = c;
+ break;
+ }
+ }
}
if (nMana != 1) {
choiceString.append(" ");
@@ -1036,6 +1294,10 @@ public class ComputerUtilMana {
abMana.setExpressChoice(choiceString.toString());
}
+ private static boolean satisfiesColorChoice(AbilityManaPart abMana, StringBuilder choices, String choice) {
+ return !abMana.getOrigProduced().contains("Different") || !choices.toString().contains(choice);
+ }
+
/**
*
* payMultipleMana.
@@ -1066,7 +1328,7 @@ public class ComputerUtilMana {
}
return unused.isEmpty() ? null : StringUtils.join(unused, ' ');
}
-
+
/**
* Find all mana sources.
* @param manaAbilityMap The map of SpellAbilities that produce mana.
@@ -1098,7 +1360,7 @@ public class ComputerUtilMana {
res.putAll(shard, manaAbilityMap.get(ManaAtom.GENERIC));
continue;
}
-
+
if (shard == ManaCostShard.GENERIC) {
continue;
}
@@ -1125,8 +1387,8 @@ public class ComputerUtilMana {
* @param extraMana extraMana
* @return ManaCost
*/
- static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final boolean test, final int extraMana) {
- Card card = sa.getHostCard();
+ public static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final boolean test, final int extraMana) {
+ Card card = sa.getHostCard();
ZoneType castFromBackup = null;
if (test && sa.isSpell()) {
castFromBackup = card.getCastFrom();
@@ -1144,42 +1406,29 @@ public class ComputerUtilMana {
ManaCostBeingPaid cost = new ManaCostBeingPaid(mana, restriction);
// Tack xMana Payments into mana here if X is a set value
- if (sa.getPayCosts() != null && (cost.getXcounter() > 0 || extraMana > 0)) {
+ if (cost.getXcounter() > 0 || extraMana > 0) {
int manaToAdd = 0;
if (test && extraMana > 0) {
final int multiplicator = Math.max(cost.getXcounter(), 1);
manaToAdd = extraMana * multiplicator;
} else {
- // For Count$xPaid set PayX in the AFs then use that here
- // Else calculate it as appropriate.
- final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
- if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar)) {
- if (xSvar.equals("PayX") && (card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar))) {
- // X SVar may end up being an empty string when copying a spell with no cost (e.g. Jhoira Avatar)
- String xValue = card.hasSVar(xSvar) ? card.getSVar(xSvar) : card.getState(CardStateName.Original).getSVar(xSvar);
- manaToAdd = xValue.isEmpty() ? 0 : Integer.parseInt(xValue) * cost.getXcounter(); // X
- } else {
- manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter();
- }
- }
+ manaToAdd = AbilityUtils.calculateAmount(card, "X", sa) * cost.getXcounter();
}
- String manaXColor = sa.getParam("XColor");
- ManaCostShard shardToGrow = ManaCostShard.parseNonGeneric(manaXColor == null ? "1" : manaXColor);
- cost.increaseShard(shardToGrow, manaToAdd);
+ cost.increaseShard(ManaCostShard.parseNonGeneric(sa.getParamOrDefault("XColor", "1")), manaToAdd);
if (!test) {
- card.setXManaCostPaid(manaToAdd / cost.getXcounter());
+ sa.setXManaCostPaid(manaToAdd / cost.getXcounter());
}
}
-
+
CostAdjustment.adjust(cost, sa, null, test);
int timesMultikicked = card.getKickerMagnitude();
if (timesMultikicked > 0 && sa.hasParam("Announce") && sa.getParam("Announce").startsWith("Multikicker")) {
ManaCost mkCost = sa.getMultiKickerManaCost();
for (int i = 0; i < timesMultikicked; i++) {
- cost.addManaCost(mkCost);
+ cost.addManaCost(mkCost);
}
sa.setSVar("Multikicker", String.valueOf(timesMultikicked));
}
@@ -1218,7 +1467,7 @@ public class ComputerUtilMana {
for (SpellAbility ma : src.getManaAbilities()) {
ma.setActivatingPlayer(p);
if (!checkPlayable || ma.canPlay()) {
- int costsToActivate = ma.getPayCosts() != null && ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
+ int costsToActivate = ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length;
int producedAmount = AbilityUtils.calculateAmount(src, ma.getParamOrDefault("Amount", "1"), ma);
@@ -1281,6 +1530,16 @@ public class ComputerUtilMana {
// 3. Use lands that produce any color many
// 4. all other sources (creature, costs, drawback, etc.)
for (Card card : manaSources) {
+ // exclude creature sources that will tap as a part of an attack declaration
+ if (card.isCreature()) {
+ if (card.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)) {
+ Combat combat = card.getGame().getCombat();
+ if (combat.getAttackers().indexOf(card) != -1 && !card.hasKeyword(Keyword.VIGILANCE)) {
+ continue;
+ }
+ }
+ }
+
if (card.isCreature() || card.isEnchanted()) {
otherManaSources.add(card);
continue; // don't use creatures before other permanents
@@ -1363,20 +1622,6 @@ public class ComputerUtilMana {
final ListMultimap manaMap = ArrayListMultimap.create();
final Game game = ai.getGame();
- List replacementEffects = new ArrayList<>();
- for (final Player p : game.getPlayers()) {
- for (final Card crd : p.getAllCards()) {
- for (final ReplacementEffect replacementEffect : crd.getReplacementEffects()) {
- if (replacementEffect.requirementsCheck(game)
- && replacementEffect.getMode() == ReplacementType.ProduceMana
- && replacementEffect.hasParam("ManaReplacement")
- && replacementEffect.zonesCheck(game.getZoneOf(crd))) {
- replacementEffects.add(replacementEffect);
- }
- }
- }
- }
-
// Loop over all current available mana sources
for (final Card sourceCard : getAvailableManaSources(ai, checkPlayable)) {
if (DEBUG_MANA_PAYMENT) {
@@ -1406,48 +1651,80 @@ public class ComputerUtilMana {
}
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
- AbilityManaPart mp = m.getManaPart();
- // setup produce mana replacement effects
- final Map repParams = AbilityKey.newMap();
- repParams.put(AbilityKey.Mana, mp.getOrigProduced());
- repParams.put(AbilityKey.Affected, sourceCard);
- repParams.put(AbilityKey.Player, ai);
- repParams.put(AbilityKey.AbilityMana, m);
+ SpellAbility tail = m;
+ while (tail != null) {
+ AbilityManaPart mp = m.getManaPart();
+ if (mp != null && tail.metConditions()) {
+ // TODO Replacement Check currently doesn't work for reflected colors
- for (final ReplacementEffect replacementEffect : replacementEffects) {
- if (replacementEffect.canReplace(repParams)) {
- Card crd = replacementEffect.getHostCard();
- String repType = crd.getSVar(replacementEffect.getParam("ManaReplacement"));
- if (repType.contains("Chosen")) {
- repType = TextUtil.fastReplace(repType, "Chosen", MagicColor.toShortString(crd.getChosenColor()));
+ // setup produce mana replacement effects
+ String origin = mp.getOrigProduced();
+ final Map repParams = AbilityKey.newMap();
+ repParams.put(AbilityKey.Mana, origin);
+ repParams.put(AbilityKey.Affected, sourceCard);
+ repParams.put(AbilityKey.Player, ai);
+ repParams.put(AbilityKey.AbilityMana, m); // RootAbility
+
+ List reList = game.getReplacementHandler().getReplacementList(ReplacementType.ProduceMana, repParams, ReplacementLayer.Other);
+
+ if (reList.isEmpty()) {
+ Set reflectedColors = CardUtil.getReflectableManaColors(m);
+ // find possible colors
+ for (byte color : MagicColor.WUBRG) {
+ if (tail.canThisProduce(MagicColor.toShortString(color)) || reflectedColors.contains(MagicColor.toLongString(color))) {
+ manaMap.put((int)color, m);
+ }
+ }
+ if (m.canThisProduce("C") || reflectedColors.contains(MagicColor.Constant.COLORLESS)) {
+ manaMap.put(ManaAtom.COLORLESS, m);
+ }
+ } else {
+ // try to guess the color the mana gets replaced to
+ for (ReplacementEffect re : reList) {
+ SpellAbility o = re.getOverridingAbility();
+ String replaced = origin;
+ if (o == null || o.getApi() != ApiType.ReplaceMana) {
+ continue;
+ }
+ if (o.hasParam("ReplaceMana")) {
+ replaced = o.getParam("ReplaceMana");
+ } else if (o.hasParam("ReplaceType")) {
+ String color = o.getParam("ReplaceType");
+ for (byte c : MagicColor.WUBRGC) {
+ String s = MagicColor.toShortString(c);
+ replaced = replaced.replace(s, color);
+ }
+ } else if (o.hasParam("ReplaceColor")) {
+ String color = o.getParam("ReplaceColor");
+ if (o.hasParam("ReplaceOnly")) {
+ replaced = replaced.replace(o.getParam("ReplaceOnly"), color);
+ } else {
+ for (byte c : MagicColor.WUBRG) {
+ String s = MagicColor.toShortString(c);
+ replaced = replaced.replace(s, color);
+ }
+ }
+ }
+
+ for (byte color : MagicColor.WUBRG) {
+ if ("Any".equals(replaced) || replaced.contains(MagicColor.toShortString(color))) {
+ manaMap.put((int)color, m);
+ }
+ }
+
+ if (replaced.contains("C")) {
+ manaMap.put(ManaAtom.COLORLESS, m);
+ }
+
+ }
}
- mp.setManaReplaceType(repType);
}
+ tail = tail.getSubAbility();
}
- Set reflectedColors = CardUtil.getReflectableManaColors(m);
- // find possible colors
- if (mp.canProduce("W", m) || reflectedColors.contains(MagicColor.Constant.WHITE)) {
- manaMap.get(ManaAtom.WHITE).add(m);
- }
- if (mp.canProduce("U", m) || reflectedColors.contains(MagicColor.Constant.BLUE)) {
- manaMap.get(ManaAtom.BLUE).add(m);
- }
- if (mp.canProduce("B", m) || reflectedColors.contains(MagicColor.Constant.BLACK)) {
- manaMap.get(ManaAtom.BLACK).add(m);
- }
- if (mp.canProduce("R", m) || reflectedColors.contains(MagicColor.Constant.RED)) {
- manaMap.get(ManaAtom.RED).add(m);
- }
- if (mp.canProduce("G", m) || reflectedColors.contains(MagicColor.Constant.GREEN)) {
- manaMap.get(ManaAtom.GREEN).add(m);
- }
- if (mp.canProduce("C", m) || reflectedColors.contains(MagicColor.Constant.COLORLESS)) {
- manaMap.get(ManaAtom.COLORLESS).add(m);
- }
- if (mp.isSnow()) {
- manaMap.get(ManaAtom.IS_SNOW).add(m);
+ if (m.getHostCard().isSnow()) {
+ manaMap.put(ManaAtom.IS_SNOW, m);
}
if (DEBUG_MANA_PAYMENT) {
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor manaMap = " + manaMap);
@@ -1462,7 +1739,7 @@ public class ComputerUtilMana {
*
* determineLeftoverMana.
*
- *
+ *
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @param player
@@ -1483,7 +1760,7 @@ public class ComputerUtilMana {
*
* determineLeftoverMana.
*
- *
+ *
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @param player
@@ -1512,7 +1789,7 @@ public class ComputerUtilMana {
*
* getAIPlayableMana.
*
- *
+ *
* @return a {@link java.util.List} object.
*/
public static List getAIPlayableMana(Card c) {
@@ -1559,8 +1836,8 @@ public class ComputerUtilMana {
sa.resetSacrificedAsEmerge();
}
}
-
-
+
+
/**
* Matches list of creatures to shards in mana cost for convoking.
* @param cost cost of convoked ability
@@ -1592,31 +1869,4 @@ public class ComputerUtilMana {
}
return convoke;
}
-
- public static int determineMaxAffordableX(Player ai, SpellAbility sa) {
- if (sa.getPayCosts() == null || sa.getPayCosts().getCostMana() == null) {
- return -1;
- }
-
- int numTgts = 0;
- int numX = sa.getPayCosts().getCostMana().getAmountOfX();
-
- if (numX == 0) {
- return -1;
- }
-
- int testX = 1;
- while (testX <= 100) {
- if (ComputerUtilMana.canPayManaCost(sa, ai, testX)) {
- numTgts++;
- } else {
- break;
- }
- testX++;
- }
-
- numTgts /= numX;
-
- return numTgts;
- }
}
diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
index 3b7a9c7890a..917012b236d 100644
--- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
+++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
@@ -5,7 +5,7 @@ import com.google.common.base.Function;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
-import forge.game.card.CounterType;
+import forge.game.card.CounterEnumType;
import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
@@ -242,11 +242,11 @@ public class CreatureEvaluator implements Function {
&& "+X".equals(sa.getParam("NumDef"))
&& !sa.usesTargeting()
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
- if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
+ if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
// Electrostatic Pummeler, can be expanded for similar cards
int initPower = getEffectivePower(sa.getHostCard());
int pumpedPower = initPower;
- int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY);
+ int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY);
if (energy > 0) {
int numActivations = energy / 3;
for (int i = 0; i < numActivations; i++) {
diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java
index 2156d3f8910..768cb8743a6 100644
--- a/forge-ai/src/main/java/forge/ai/GameState.java
+++ b/forge-ai/src/main/java/forge/ai/GameState.java
@@ -9,7 +9,9 @@ import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
+import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
+import forge.game.ability.AbilityKey;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
@@ -23,7 +25,6 @@ import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
-import forge.game.ability.AbilityKey;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
@@ -75,9 +76,12 @@ public abstract class GameState {
private final Map> cardToChosenClrs = new HashMap<>();
private final Map cardToChosenCards = new HashMap<>();
private final Map cardToChosenType = new HashMap<>();
+ private final Map cardToChosenType2 = new HashMap<>();
private final Map> cardToRememberedId = new HashMap<>();
private final Map> cardToImprintedId = new HashMap<>();
+ private final Map> cardToMergedCards = new HashMap<>();
private final Map cardToNamedCard = new HashMap<>();
+ private final Map cardToNamedCard2 = new HashMap<>();
private final Map cardToExiledWithId = new HashMap<>();
private final Map cardAttackMap = new HashMap<>();
@@ -258,7 +262,13 @@ public abstract class GameState {
if (c.getPaperCard() == null) {
return;
}
- newText.append(c.getPaperCard().getName());
+
+ if (!c.getMergedCards().isEmpty()) {
+ // we have to go by the current top card name here
+ newText.append(c.getTopMergedCard().getPaperCard().getName());
+ } else {
+ newText.append(c.getPaperCard().getName());
+ }
}
if (c.isCommander()) {
newText.append("|IsCommander");
@@ -300,6 +310,8 @@ public abstract class GameState {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
+ } else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
+ newText.append("|Modal");
}
if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
@@ -321,9 +333,15 @@ public abstract class GameState {
if (!c.getChosenType().isEmpty()) {
newText.append("|ChosenType:").append(c.getChosenType());
}
+ if (!c.getChosenType2().isEmpty()) {
+ newText.append("|ChosenType2:").append(c.getChosenType2());
+ }
if (!c.getNamedCard().isEmpty()) {
newText.append("|NamedCard:").append(c.getNamedCard());
}
+ if (!c.getNamedCard2().isEmpty()) {
+ newText.append("|NamedCard2:").append(c.getNamedCard2());
+ }
List chosenCardIds = Lists.newArrayList();
for (Object obj : c.getChosenCards()) {
@@ -355,6 +373,17 @@ public abstract class GameState {
if (!imprintedCardIds.isEmpty()) {
newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ","));
}
+
+ if (!c.getMergedCards().isEmpty()) {
+ List mergedCardNames = new ArrayList<>();
+ for (Card merged : c.getMergedCards()) {
+ if (c.getTopMergedCard() == merged) {
+ continue;
+ }
+ mergedCardNames.add(merged.getPaperCard().getName().replace(",", "^"));
+ }
+ newText.append("|MergedCards:").append(TextUtil.join(mergedCardNames, ","));
+ }
}
if (zoneType == ZoneType.Exile) {
@@ -591,10 +620,13 @@ public abstract class GameState {
cardToEnchantPlayerId.clear();
cardToRememberedId.clear();
cardToExiledWithId.clear();
+ cardToImprintedId.clear();
markedDamage.clear();
cardToChosenClrs.clear();
cardToChosenCards.clear();
cardToChosenType.clear();
+ cardToChosenType2.clear();
+ cardToMergedCards.clear();
cardToScript.clear();
cardAttackMap.clear();
@@ -627,6 +659,7 @@ public abstract class GameState {
handleCardAttachments();
handleChosenEntities();
handleRememberedEntities();
+ handleMergedCards();
handleScriptExecution(game);
handlePrecastSpells(game);
handleMarkedDamage();
@@ -781,6 +814,7 @@ public abstract class GameState {
Card exiledWith = idToCard.get(Integer.parseInt(id));
c.setExiledWith(exiledWith);
+ c.setExiledBy(exiledWith.getController());
}
}
@@ -815,6 +849,12 @@ public abstract class GameState {
break;
}
}
+
+ if (sa.hasParam("RememberTargets")) {
+ for (final GameObject o : sa.getTargets()) {
+ sa.getHostCard().addRemembered(o);
+ }
+ }
}
private void handleScriptExecution(final Game game) {
@@ -974,14 +1014,27 @@ public abstract class GameState {
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
}
- PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
+ Card c = null;
- if (pc == null) {
- System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!");
- return;
+ if (StringUtils.isNumeric(spellDef)) {
+ // Precast from a specific host
+ c = idToCard.get(Integer.parseInt(spellDef));
+ if (c == null) {
+ System.err.println("ERROR: Could not find a card with ID " + spellDef + " to precast!");
+ return;
+ }
+ } else {
+ // Precast from a card by name
+ PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
+
+ if (pc == null) {
+ System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!");
+ return;
+ }
+
+ c = Card.fromPaperCard(pc, activator);
}
- Card c = Card.fromPaperCard(pc, activator);
SpellAbility sa = null;
if (!scriptID.isEmpty()) {
@@ -1029,12 +1082,24 @@ public abstract class GameState {
c.setChosenType(entry.getValue());
}
+ // Chosen type 2
+ for (Entry entry : cardToChosenType2.entrySet()) {
+ Card c = entry.getKey();
+ c.setChosenType2(entry.getValue());
+ }
+
// Named card
for (Entry entry : cardToNamedCard.entrySet()) {
Card c = entry.getKey();
c.setNamedCard(entry.getValue());
}
+ // Named card 2
+ for (Entry entry : cardToNamedCard2.entrySet()) {
+ Card c = entry.getKey();
+ c.setNamedCard2(entry.getValue());
+ }
+
// Chosen cards
for (Entry entry : cardToChosenCards.entrySet()) {
Card c = entry.getKey();
@@ -1069,12 +1134,61 @@ public abstract class GameState {
}
}
+ private void handleMergedCards() {
+ for(Entry> entry : cardToMergedCards.entrySet()) {
+ Card mergedTo = entry.getKey();
+ for(String mergedCardName : entry.getValue()) {
+ Card c;
+ PaperCard pc = StaticData.instance().getCommonCards().getCard(mergedCardName. replace("^", ","));
+ if (pc == null) {
+ System.err.println("ERROR: Tried to create a non-existent card named " + mergedCardName + " (as a merged card) when loading game state!");
+ continue;
+ }
+
+ c = Card.fromPaperCard(pc, mergedTo.getOwner());
+ emulateMergeViaMutate(mergedTo, c);
+ }
+ }
+ }
+
+ private void emulateMergeViaMutate(Card top, Card bottom) {
+ if (top == null || bottom == null) {
+ System.err.println("ERROR: Tried to call emulateMergeViaMutate with a null card!");
+ return;
+ }
+
+ Game game = top.getGame();
+
+ bottom.setMergedToCard(top);
+ if (!top.hasMergedCard()) {
+ top.addMergedCard(top);
+ }
+ top.addMergedCard(bottom);
+
+ if (top.getMutatedTimestamp() != -1) {
+ top.removeCloneState(top.getMutatedTimestamp());
+ }
+
+ final Long ts = game.getNextTimestamp();
+ top.setMutatedTimestamp(ts);
+ if (top.getCurrentStateName() != CardStateName.FaceDown) {
+ final CardCloneStates mutatedStates = CardFactory.getMutatedCloneStates(top, null/*FIXME*/);
+ top.addCloneState(mutatedStates, ts);
+ }
+ bottom.setTapped(top.isTapped());
+ bottom.setFlipped(top.isFlipped());
+ top.setTimesMutated(top.getTimesMutated() + 1);
+ top.updateTokenView();
+
+ // TODO: Merged commanders aren't supported yet
+ }
+
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
- entity.setCounters(Maps.newEnumMap(CounterType.class));
+ entity.setCounters(Maps.newHashMap());
String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2);
- entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
+ entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
}
}
@@ -1116,7 +1230,7 @@ public abstract class GameState {
Map counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters
// var as-is.
- c.setCounters(Maps.newEnumMap(CounterType.class));
+ c.setCounters(Maps.newHashMap());
if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter)
@@ -1140,6 +1254,9 @@ public abstract class GameState {
zone.setCards(kv.getValue());
}
}
+ for (Card cmd : p.getCommanders()) {
+ p.getZone(ZoneType.Command).add(Player.createCommanderEffect(p.getGame(), cmd));
+ }
}
/**
@@ -1210,7 +1327,10 @@ public abstract class GameState {
c.setState(CardStateName.Flipped, true);
} else if (info.startsWith("Meld")) {
c.setState(CardStateName.Meld, true);
- } else if (info.startsWith("OnAdventure")) {
+ } else if (info.startsWith("Modal")) {
+ c.setState(CardStateName.Modal, true);
+ }
+ else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell";
AbilitySub saAdventure = (AbilitySub)AbilityFactory.getAbility(abAdventure, c);
StringBuilder sbPlay = new StringBuilder();
@@ -1220,11 +1340,10 @@ public abstract class GameState {
saAdventure.setActivatingPlayer(c.getOwner());
saAdventure.resolve();
c.setExiledWith(c); // This seems to be the way it's set up internally. Potentially not needed here?
+ c.setExiledBy(c.getController());
} else if (info.startsWith("IsCommander")) {
- // TODO: This doesn't seem to properly restore the ability to play the commander. Why?
c.setCommander(true);
player.setCommanders(Lists.newArrayList(c));
- player.getZone(ZoneType.Command).add(Player.createCommanderEffect(player.getGame(), c));
} else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3));
idToCard.put(id, c);
@@ -1253,6 +1372,8 @@ public abstract class GameState {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) {
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
+ } else if (info.startsWith("ChosenType2:")) {
+ cardToChosenType2.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ChosenCards:")) {
CardCollection chosen = new CardCollection();
String[] idlist = info.substring(info.indexOf(':') + 1).split(",");
@@ -1260,8 +1381,13 @@ public abstract class GameState {
chosen.add(idToCard.get(Integer.parseInt(id)));
}
cardToChosenCards.put(c, chosen);
+ } else if (info.startsWith("MergedCards:")) {
+ List cardNames = Arrays.asList(info.substring(info.indexOf(':') + 1).split(","));
+ cardToMergedCards.put(c, cardNames);
} else if (info.startsWith("NamedCard:")) {
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
+ } else if (info.startsWith("NamedCard2:")) {
+ cardToNamedCard2.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ExecuteScript:")) {
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("RememberedCards:")) {
diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
index 04a9f303832..ab50c1a806f 100644
--- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
+++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
@@ -103,7 +103,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public Integer announceRequirements(SpellAbility ability, String announce, boolean allowZero) {
+ public Integer announceRequirements(SpellAbility ability, String announce) {
// For now, these "announcements" are made within the AI classes of the appropriate SA effects
if (ability.getApi() != null) {
switch (ability.getApi()) {
@@ -145,12 +145,12 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional) {
- return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional);
+ public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional, Map params) {
+ return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional, params);
}
@Override
- public T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer) {
+ public T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer, Map params) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
@@ -158,13 +158,13 @@ public class PlayerControllerAi extends PlayerController {
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
- return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer);
+ return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer, params);
}
@Override
public List chooseEntitiesForEffect(
FCollectionView optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
- Player targetedPlayer) {
+ Player targetedPlayer, Map params) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
@@ -172,7 +172,7 @@ public class PlayerControllerAi extends PlayerController {
List selecteds = new ArrayList<>();
T selected;
do {
- selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
+ selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer, params);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
@@ -182,7 +182,23 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public SpellAbility chooseSingleSpellForEffect(java.util.List spells, SpellAbility sa, String title,
+ public List chooseSpellAbilitiesForEffect(List spells, SpellAbility sa, String title,
+ int num, Map params) {
+ List remaining = Lists.newArrayList(spells);
+ List selecteds = Lists.newArrayList();
+ SpellAbility selected;
+ do {
+ selected = chooseSingleSpellForEffect(remaining, sa, title, params);
+ if ( selected != null ) {
+ remaining.remove(selected);
+ selecteds.add(selected);
+ }
+ } while ( (selected != null ) && (selecteds.size() < num) );
+ return selecteds;
+ }
+
+ @Override
+ public SpellAbility chooseSingleSpellForEffect(List spells, SpellAbility sa, String title,
Map params) {
ApiType api = sa.getApi();
if (null == api) {
@@ -208,15 +224,13 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public boolean confirmTrigger(WrappedAbility wrapper, Map triggerParams, boolean isMandatory) {
+ public boolean confirmTrigger(WrappedAbility wrapper) {
final SpellAbility sa = wrapper.getWrappedAbility();
//final Trigger regtrig = wrapper.getTrigger();
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true;
}
- if (triggerParams.containsKey("DelayedTrigger") || isMandatory) {
- //TODO: The only card with an optional delayed trigger is Shirei, Shizo's Caretaker,
- // needs to be expanded when a more difficult cards comes up
+ if (wrapper.isMandatory()) {
return true;
}
// Store/replace target choices more properly to get this SA cleared.
@@ -253,7 +267,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public Player chooseStartingPlayer(boolean isFirstGame) {
+ public Player chooseStartingPlayer(boolean isFirstgame) {
return this.player; // AI is brave :)
}
@@ -350,7 +364,7 @@ public class PlayerControllerAi extends PlayerController {
if (destinationZone == ZoneType.Graveyard) {
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
- if (!CardLists.filter(game.getCardsInGame(), new Predicate() {
+ if (!CardLists.filter(getGame().getCardsInGame(), new Predicate() {
@Override
public boolean apply(Card card) {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
@@ -471,7 +485,7 @@ public class PlayerControllerAi extends PlayerController {
public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) {
if (canSetupTargets)
brains.doTrigger(effectSA, true); // first parameter does not matter, since return value won't be used
- ComputerUtil.playNoStack(player, effectSA, game);
+ ComputerUtil.playNoStack(player, effectSA, getGame());
}
@Override
@@ -480,7 +494,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public TargetChoices chooseNewTargetsFor(SpellAbility ability) {
+ public TargetChoices chooseNewTargetsFor(SpellAbility ability, Predicate filter, boolean optional) {
// AI currently can't do this. But when it can it will need to be based on Ability API
return null;
}
@@ -503,18 +517,18 @@ public class PlayerControllerAi extends PlayerController {
@Override
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection validTypes, List invalidTypes, boolean isOptional) {
- String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), invalidTypes);
+ String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), validTypes, invalidTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) {
chosen = validTypes.iterator().next();
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: chosen");
}
- game.getAction().nofityOfValue(sa, player, chosen, player);
+ getGame().getAction().nofityOfValue(sa, player, chosen, player);
return chosen;
}
@Override
- public Object vote(SpellAbility sa, String prompt, List