Merge remote-tracking branch 'origin/master'

This commit is contained in:
Hythonia
2021-03-26 16:34:34 +01:00
61 changed files with 539 additions and 368 deletions

View File

@@ -91,7 +91,7 @@ public class AiAttackController {
public AiAttackController(final Player ai, boolean nextTurn) { public AiAttackController(final Player ai, boolean nextTurn) {
this.ai = ai; this.ai = ai;
this.defendingOpponent = choosePreferredDefenderPlayer(); this.defendingOpponent = choosePreferredDefenderPlayer(ai);
this.oppList = getOpponentCreatures(this.defendingOpponent); this.oppList = getOpponentCreatures(this.defendingOpponent);
this.myList = ai.getCreaturesInPlay(); this.myList = ai.getCreaturesInPlay();
this.attackers = new ArrayList<>(); this.attackers = new ArrayList<>();
@@ -107,7 +107,7 @@ public class AiAttackController {
public AiAttackController(final Player ai, Card attacker) { public AiAttackController(final Player ai, Card attacker) {
this.ai = ai; this.ai = ai;
this.defendingOpponent = choosePreferredDefenderPlayer(); this.defendingOpponent = choosePreferredDefenderPlayer(ai);
this.oppList = getOpponentCreatures(this.defendingOpponent); this.oppList = getOpponentCreatures(this.defendingOpponent);
this.myList = ai.getCreaturesInPlay(); this.myList = ai.getCreaturesInPlay();
this.attackers = new ArrayList<>(); this.attackers = new ArrayList<>();
@@ -156,13 +156,12 @@ public class AiAttackController {
} }
/** Choose opponent for AI to attack here. Expand as necessary. */ /** Choose opponent for AI to attack here. Expand as necessary. */
private Player choosePreferredDefenderPlayer() { public static Player choosePreferredDefenderPlayer(Player ai) {
Player defender = ai.getWeakestOpponent(); //Gets opponent with the least life Player defender = ai.getWeakestOpponent(); //Concentrate on opponent within easy kill range
if (defender.getLife() < 8) { //Concentrate on opponent within easy kill range if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players
return defender; // TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked
} else { //Otherwise choose a random opponent to ensure no ganging up on players return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size()));
defender = ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size()));
} }
return defender; return defender;
} }
@@ -624,7 +623,7 @@ public class AiAttackController {
int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage; int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage;
int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp); int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp);
if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai) >= opp.getLife() if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, opp) >= opp.getLife()
&& !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) { && !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) {
return true; return true;
} }
@@ -919,7 +918,7 @@ public class AiAttackController {
// find the potential damage ratio the AI can cause // find the potential damage ratio the AI can cause
double humanLifeToDamageRatio = 1000000; double humanLifeToDamageRatio = 1000000;
if (candidateUnblockedDamage > 0) { if (candidateUnblockedDamage > 0) {
humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai)) / candidateUnblockedDamage; humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai, opp)) / candidateUnblockedDamage;
} }
// determine if the ai outnumbers the player // determine if the ai outnumbers the player

View File

@@ -1102,6 +1102,8 @@ public class AiController {
} }
public CardCollection getCardsToDiscard(int min, final int max, final CardCollection validCards, final SpellAbility sa) { public CardCollection getCardsToDiscard(int min, final int max, final CardCollection validCards, final SpellAbility sa) {
String logic = sa.getParamOrDefault("AILogic", "");
if (validCards.size() < min) { if (validCards.size() < min) {
return null; return null;
} }
@@ -1111,11 +1113,16 @@ public class AiController {
int count = 0; int count = 0;
if (sa != null) { if (sa != null) {
sourceCard = sa.getHostCard(); sourceCard = sa.getHostCard();
if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) { if ("Always".equals(logic) && !validCards.isEmpty()) {
min = 1; min = 1;
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) { } else if (logic.startsWith("UnlessAtLife.")) {
int threshold = AbilityUtils.calculateAmount(sourceCard, logic.substring(logic.indexOf(".") + 1), sa);
if (player.getLife() <= threshold) {
min = 1;
}
} else if ("VolrathsShapeshifter".equals(logic)) {
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa); return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
} else if ("DiscardCMCX".equals(sa.getParam("AILogic"))) { } else if ("DiscardCMCX".equals(logic)) {
final int cmc = sa.getXManaCostPaid(); final int cmc = sa.getXManaCostPaid();
CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc)); CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc));
if (discards.isEmpty()) { if (discards.isEmpty()) {

View File

@@ -188,7 +188,6 @@ public class ComputerUtil {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
// Play higher costing spells first? // Play higher costing spells first?
final Cost cost = sa.getPayCosts(); final Cost cost = sa.getPayCosts();
@@ -213,7 +212,8 @@ public class ComputerUtil {
if (unless != null && !unless.endsWith(">")) { if (unless != null && !unless.endsWith(">")) {
final int amount = AbilityUtils.calculateAmount(source, unless, sa); final int amount = AbilityUtils.calculateAmount(source, unless, sa);
final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtil.getOpponentFor(ai), true).size(); // this is enough as long as the AI is only smart enough to target top of stack
final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtilAbility.getTopSpellAbilityOnStack(ai.getGame(), sa).getActivatingPlayer(), true).size();
// If the Unless isn't enough, this should be less likely to be used // If the Unless isn't enough, this should be less likely to be used
if (amount > usableManaSources) { if (amount > usableManaSources) {
@@ -1068,9 +1068,6 @@ public class ComputerUtil {
return true; return true;
} }
} }
if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ComputerUtil.getOpponentFor(ai))) {
return true;
}
if (card.isCreature()) { if (card.isCreature()) {
if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) { if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) {
return true; return true;
@@ -1093,8 +1090,8 @@ public class ComputerUtil {
} // BuffedBy } // BuffedBy
// get all cards the human controls with AntiBuffedBy // there's a good chance AI will attack weak target
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield); final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield);
for (Card buffedcard : antibuffed) { for (Card buffedcard : antibuffed) {
if (buffedcard.hasSVar("AntiBuffedBy")) { if (buffedcard.hasSVar("AntiBuffedBy")) {
final String buffedby = buffedcard.getSVar("AntiBuffedBy"); final String buffedby = buffedcard.getSVar("AntiBuffedBy");
@@ -1142,27 +1139,16 @@ public class ComputerUtil {
* @return true if it's OK to cast this Card for less than the max targets * @return true if it's OK to cast this Card for less than the max targets
*/ */
public static boolean shouldCastLessThanMax(final Player ai, final Card source) { public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
boolean ret = true; if (source.getXManaCostPaid() > 0) {
if (source.getManaCost().countX() > 0) { // If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by mana available. return true;
return ret;
} else {
// Otherwise, if life is possibly in danger, then this is fine.
Combat combat = new Combat(ComputerUtil.getOpponentFor(ai));
CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
for (Card att : attackers) {
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController()));
}
}
AiBlockController aiBlock = new AiBlockController(ai);
aiBlock.assignBlockersForCombat(combat);
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
// Otherwise, return false. Do not play now.
ret = false;
}
} }
return ret; if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
} }
/** /**
@@ -1266,8 +1252,8 @@ public class ComputerUtil {
} }
} }
// get all cards the human controls with AntiBuffedBy // there's a good chance AI will attack weak target
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield); final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield);
for (Card buffedcard : antibuffed) { for (Card buffedcard : antibuffed) {
if (buffedcard.hasSVar("AntiBuffedBy")) { if (buffedcard.hasSVar("AntiBuffedBy")) {
final String buffedby = buffedcard.getSVar("AntiBuffedBy"); final String buffedby = buffedcard.getSVar("AntiBuffedBy");
@@ -1463,7 +1449,7 @@ public class ComputerUtil {
return false; return false;
} }
public static int possibleNonCombatDamage(Player ai) { public static int possibleNonCombatDamage(Player ai, Player enemy) {
int damage = 0; int damage = 0;
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
all.addAll(ai.getCardsActivableInExternalZones(true)); all.addAll(ai.getCardsActivableInExternalZones(true));
@@ -1483,7 +1469,6 @@ public class ComputerUtil {
if (tgt == null) { if (tgt == null) {
continue; continue;
} }
final Player enemy = ComputerUtil.getOpponentFor(ai);
if (!sa.canTarget(enemy)) { if (!sa.canTarget(enemy)) {
continue; continue;
} }
@@ -2346,7 +2331,7 @@ public class ComputerUtil {
} }
} }
else if (logic.equals("ChosenLandwalk")) { else if (logic.equals("ChosenLandwalk")) {
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) { for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
for (String t : c.getType()) { for (String t : c.getType()) {
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) { if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
chosen = t; chosen = t;
@@ -2364,7 +2349,7 @@ public class ComputerUtil {
else if (kindOfType.equals("Land")) { else if (kindOfType.equals("Land")) {
if (logic != null) { if (logic != null) {
if (logic.equals("ChosenLandwalk")) { if (logic.equals("ChosenLandwalk")) {
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) { for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
for (String t : c.getType().getLandTypes()) { for (String t : c.getType().getLandTypes()) {
if (!invalidTypes.contains(t)) { if (!invalidTypes.contains(t)) {
chosen = t; chosen = t;
@@ -2399,23 +2384,26 @@ public class ComputerUtil {
case "Torture": case "Torture":
return "Torture"; return "Torture";
case "GraceOrCondemnation": case "GraceOrCondemnation":
return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace" List<ZoneType> graceZones = new ArrayList<ZoneType>();
: "Condemnation"; graceZones.add(ZoneType.Battlefield);
graceZones.add(ZoneType.Graveyard);
CardCollection graceCreatures = CardLists.getType(sa.getHostCard().getGame().getCardsIn(graceZones), "Creature");
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
return aiGrace > humanGrace ? "Grace" : "Condemnation";
case "CarnageOrHomage": case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists CardCollection cardsInPlay = CardLists.getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land");
.getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land");
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents()); CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
CardCollection computerlist = CardLists.filterControlledBy(cardsInPlay, ai); CardCollection computerlist = ai.getCreaturesInPlay();
return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
case "Judgment": case "Judgment":
if (votes.isEmpty()) { if (votes.isEmpty()) {
CardCollection list = new CardCollection(); CardCollection list = new CardCollection();
for (Object o : options) { for (Object o : options) {
if (o instanceof Card) { if (o instanceof Card) {
list.add((Card) o); list.add((Card) o);
}
} }
}
return ComputerUtilCard.getBestAI(list); return ComputerUtilCard.getBestAI(list);
} else { } else {
return Iterables.getFirst(votes.keySet(), null); return Iterables.getFirst(votes.keySet(), null);
@@ -2934,23 +2922,6 @@ public class ComputerUtil {
return true; return true;
} }
@Deprecated
public static final Player getOpponentFor(final Player player) {
// This method is deprecated and currently functions as a synonym for player.getWeakestOpponent
// until it can be replaced everywhere in the code.
// Consider replacing calls to this method either with a multiplayer-friendly determination of
// opponent that contextually makes the most sense, or with a direct call to player.getWeakestOpponent
// where that is applicable and makes sense from the point of view of multiplayer AI logic.
Player opponent = player.getWeakestOpponent();
if (opponent != null) {
return opponent;
}
throw new IllegalStateException("No opponents left ingame for " + player);
}
public static int countUsefulCreatures(Player p) { public static int countUsefulCreatures(Player p) {
CardCollection creats = p.getCreaturesInPlay(); CardCollection creats = p.getCreaturesInPlay();
int count = 0; int count = 0;
@@ -3033,31 +3004,32 @@ public class ComputerUtil {
// call this to determine if it's safe to use a life payment spell // call this to determine if it's safe to use a life payment spell
// or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell. // or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell.
public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) { public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) {
Player opponent = ComputerUtil.getOpponentFor(ai); for (Player opponent: ai.getOpponents()) {
// test whether the human can kill the ai next turn // test whether the human can kill the ai next turn
Combat combat = new Combat(opponent); Combat combat = new Combat(opponent);
boolean containsAttacker = false; boolean containsAttacker = false;
for (Card att : opponent.getCreaturesInPlay()) { for (Card att : opponent.getCreaturesInPlay()) {
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
combat.addAttacker(att, ai); combat.addAttacker(att, ai);
containsAttacker = true; containsAttacker = true;
}
} }
} if (!containsAttacker) {
if (!containsAttacker) { return false;
return false; }
} AiBlockController block = new AiBlockController(ai);
AiBlockController block = new AiBlockController(ai); block.assignBlockersForCombat(combat);
block.assignBlockersForCombat(combat);
// TODO predict other, noncombat sources of damage and add them to the "payment" variable. // TODO predict other, noncombat sources of damage and add them to the "payment" variable.
// examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc // examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc
// If added, might need a parameter to define whether we want to check all threats or combat threats. // If added, might need a parameter to define whether we want to check all threats or combat threats.
if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) { if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) {
return true; return true;
} }
if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) { if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) {
return true; return true;
}
} }
return false; return false;

View File

@@ -550,7 +550,7 @@ public class ComputerUtilCard {
*/ */
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) { public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
AiBlockController aiBlk = new AiBlockController(ai); AiBlockController aiBlk = new AiBlockController(ai);
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
Combat combat = new Combat(opp); Combat combat = new Combat(opp);
//Use actual attackers if available, else consider all possible attackers //Use actual attackers if available, else consider all possible attackers
Combat currentCombat = ai.getGame().getCombat(); Combat currentCombat = ai.getGame().getCombat();

View File

@@ -97,34 +97,39 @@ public class ComputerUtilCombat {
* canAttackNextTurn. * canAttackNextTurn.
* </p> * </p>
* *
* @param atacker * @param attacker
* a {@link forge.game.card.Card} object. * a {@link forge.game.card.Card} object.
* @param defender * @param defender
* the defending {@link GameEntity}. * the defending {@link GameEntity}.
* @return a boolean. * @return a boolean.
*/ */
public static boolean canAttackNextTurn(final Card atacker, final GameEntity defender) { public static boolean canAttackNextTurn(final Card attacker, final GameEntity defender) {
if (!atacker.isCreature()) { if (!attacker.isCreature()) {
return false; return false;
} }
if (!CombatUtil.canAttackNextTurn(atacker, defender)) { if (!CombatUtil.canAttackNextTurn(attacker, defender)) {
return false; return false;
} }
for (final KeywordInterface inst : atacker.getKeywords()) { for (final KeywordInterface inst : attacker.getKeywords()) {
final String keyword = inst.getOriginal(); final String keyword = inst.getOriginal();
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) { if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
final String defined = keyword.split(":")[1]; final String defined = keyword.split(":")[1];
final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0); final Player player = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0);
if (!defender.equals(player)) { if (!defender.equals(player)) {
return false; return false;
} }
} }
} }
// TODO this should be a factor but needs some alignment with AttachAi
//boolean leavesPlay = !ComputerUtilCard.hasActiveUndyingOrPersist(attacker)
// && ((attacker.hasKeyword(Keyword.VANISHING) && attacker.getCounters(CounterEnumType.TIME) == 1)
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay"));
// The creature won't untap next turn // The creature won't untap next turn
return !atacker.isTapped() || Untap.canUntap(atacker); return !attacker.isTapped() || Untap.canUntap(attacker);
} // canAttackNextTurn(Card, GameEntity) }
/** /**
* <p> * <p>

View File

@@ -34,6 +34,7 @@ public enum SpellApiToAi {
.put(ApiType.BidLife, BidLifeAi.class) .put(ApiType.BidLife, BidLifeAi.class)
.put(ApiType.Bond, BondAi.class) .put(ApiType.Bond, BondAi.class)
.put(ApiType.Branch, AlwaysPlayAi.class) .put(ApiType.Branch, AlwaysPlayAi.class)
.put(ApiType.Camouflage, ChooseCardAi.class)
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class) .put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
.put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class)
.put(ApiType.ChangeX, AlwaysPlayAi.class) .put(ApiType.ChangeX, AlwaysPlayAi.class)

View File

@@ -22,7 +22,6 @@ public class ActivateAbilityAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Player opp = ai.getWeakestOpponent(); final Player opp = ai.getWeakestOpponent();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card")); List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
if (list.isEmpty()) { if (list.isEmpty()) {
@@ -40,6 +39,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} }
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return randomReturn; return randomReturn;
} }
@@ -56,7 +56,8 @@ public class ActivateAbilityAi extends SpellAbilityAi {
} else { } else {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
return defined.contains(opp); // if at least two players are returned we can affect another opponent
return defined.size() > 1 || defined.contains(opp);
} }
} else { } else {

View File

@@ -13,18 +13,21 @@ public class BalanceAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String logic = sa.getParam("AILogic"); String logic = sa.getParam("AILogic");
int diff = 0; int diff = 0;
// TODO Add support for multiplayer logic Player opp = aiPlayer.getWeakestOpponent();
final Player opp = aiPlayer.getWeakestOpponent();
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield); final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
for (Player min : aiPlayer.getOpponents()) {
if (min.getCardsIn(ZoneType.Battlefield).size() < opp.getCardsIn(ZoneType.Battlefield).size()) {
opp = min;
}
}
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
if ("BalanceCreaturesAndLands".equals(logic)) { if ("BalanceCreaturesAndLands".equals(logic)) {
// Copied over from hardcoded Balance. We should be checking value of the lands/creatures not just counting // TODO Copied over from hardcoded Balance. We should be checking value of the lands/creatures for each opponent, not just counting
diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() - diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() -
CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size(); CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size();
diff += 1.5 * ( CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() - diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() -
CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size()); CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size());
} }
else if ("BalancePermanents".equals(logic)) { else if ("BalancePermanents".equals(logic)) {

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
@@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canTgtCreature()) { if (tgt.canTgtCreature()) {
List<Card> list = CardLists.getTargetableCards(aiPlayer.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), sa); List<Card> list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;

View File

@@ -15,6 +15,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.AiAttackController;
import forge.ai.AiBlockController; import forge.ai.AiBlockController;
import forge.ai.AiCardMemory; import forge.ai.AiCardMemory;
import forge.ai.AiController; import forge.ai.AiController;
@@ -263,7 +264,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
ZoneType origin = null; ZoneType origin = null;
final Player opponent = ai.getWeakestOpponent(); final Player opponent = AiAttackController.choosePreferredDefenderPlayer(ai);
boolean activateForCost = ComputerUtil.activateForCost(sa, ai); boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) { if (sa.hasParam("Origin")) {
@@ -471,7 +472,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// if putting cards from hand to library and parent is drawing cards // if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something: // make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = aiPlayer.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
if (tgt != null && tgt.canTgtPlayer()) { if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse(); boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opp)) { if (isCurse && sa.canTarget(opp)) {
@@ -530,7 +531,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
Iterable<Player> pDefined; Iterable<Player> pDefined;
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if ((tgt != null) && tgt.canTgtPlayer()) { if ((tgt != null) && tgt.canTgtPlayer()) {
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.isCurse()) { if (sa.isCurse()) {
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
@@ -892,7 +893,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
// TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore?
sa.setXManaCostPaid(xPay); sa.setXManaCostPaid(xPay);
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
} }
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
@@ -913,9 +913,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.isSpell()) { if (sa.isSpell()) {
list.remove(source); // spells can't target their own source, because it's actually in the stack zone list.remove(source); // spells can't target their own source, because it's actually in the stack zone
} }
//System.out.println("isPreferredTarget " + list);
if (sa.hasParam("AttachedTo")) { if (sa.hasParam("AttachedTo")) {
//System.out.println("isPreferredTarget att " + list);
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
@@ -927,7 +925,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
}); });
//System.out.println("isPreferredTarget ok " + list);
} }
if (list.size() < sa.getMinTargets()) { if (list.size() < sa.getMinTargets()) {
@@ -1482,9 +1479,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
if ("DeathgorgeScavenger".equals(logic)) { if ("DeathgorgeScavenger".equals(logic)) {
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa); return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
} else if ("ExtraplanarLens".equals(logic)) { }
if ("ExtraplanarLens".equals(logic)) {
return SpecialCardAi.ExtraplanarLens.consider(ai, sa); return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
} else if ("ExileCombatThreat".equals(logic)) { }
if ("ExileCombatThreat".equals(logic)) {
return doExileCombatThreatLogic(ai, sa); return doExileCombatThreatLogic(ai, sa);
} }
@@ -1984,11 +1983,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
} }
if (toPay == 0) { if (toPay == 0 || toPay <= usableManaSources) {
canBeSaved.add(potentialTgt);
}
if (toPay <= usableManaSources) {
canBeSaved.add(potentialTgt); canBeSaved.add(potentialTgt);
} }

View File

@@ -9,6 +9,7 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
@@ -114,7 +115,7 @@ public class ChooseCardAi extends SpellAbilityAi {
return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty(); return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty();
} else if (aiLogic.equals("Duneblast")) { } else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay(); CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay(); CardCollection oppCreatures = AiAttackController.choosePreferredDefenderPlayer(ai).getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE); aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE); oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);

View File

@@ -7,6 +7,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.StaticData; import forge.StaticData;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi; import forge.ai.SpecialCardAi;
@@ -44,7 +45,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) { if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(ai.getWeakestOpponent()); sa.getTargets().add(AiAttackController.choosePreferredDefenderPlayer(ai));
} else { } else {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} }

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -16,7 +17,7 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
TargetRestrictions tgt = sa.getTargetRestrictions(); TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
Player opp = aiPlayer.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -16,7 +17,7 @@ public class ChooseNumberAi extends SpellAbilityAi {
TargetRestrictions tgt = sa.getTargetRestrictions(); TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
Player opp = aiPlayer.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {

View File

@@ -8,6 +8,7 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -54,7 +55,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
Player opp = ai.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
CardCollection list = CardCollection list =
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); CardLists.getValidCards(AiAttackController.choosePreferredDefenderPlayer(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on // AI won't try to grab cards that are filtered out of AI decks on
// purpose // purpose
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {

View File

@@ -8,6 +8,7 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -26,9 +27,6 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
public class DebuffAi extends SpellAbilityAi { public class DebuffAi extends SpellAbilityAi {
// *************************************************************************
// ***************************** Debuff ************************************
// *************************************************************************
@Override @Override
protected boolean canPlayAI(final Player ai, final SpellAbility sa) { protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
@@ -140,7 +138,6 @@ public class DebuffAi extends SpellAbilityAi {
while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) { while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
Card t = null; Card t = null;
// boolean goodt = false;
if (list.isEmpty()) { if (list.isEmpty()) {
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) { if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
@@ -176,19 +173,18 @@ public class DebuffAi extends SpellAbilityAi {
* @return a CardCollection. * @return a CardCollection.
*/ */
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) { private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa); CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
if (!list.isEmpty()) { if (!list.isEmpty()) {
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
return c.hasAnyKeyword(kws); // don't add duplicate negative return c.hasAnyKeyword(kws); // don't add duplicate negative keywords
// keywords
} }
}); });
} }
return list; return list;
} // getCurseCreatures() }
/** /**
* <p> * <p>
@@ -216,7 +212,7 @@ public class DebuffAi extends SpellAbilityAi {
list.remove(c); list.remove(c);
} }
final CardCollection pref = CardLists.filterControlledBy(list, ai.getWeakestOpponent()); final CardCollection pref = CardLists.filterControlledBy(list, ai.getOpponents());
final CardCollection forced = CardLists.filterControlledBy(list, ai); final CardCollection forced = CardLists.filterControlledBy(list, ai);
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -242,8 +238,7 @@ public class DebuffAi extends SpellAbilityAi {
break; break;
} }
// TODO - if forced targeting, just pick something without the given // TODO - if forced targeting, just pick something without the given keyword
// keyword
Card c; Card c;
if (CardLists.getNotType(forced, "Creature").size() == 0) { if (CardLists.getNotType(forced, "Creature").size() == 0) {
c = ComputerUtilCard.getWorstCreatureAI(forced); c = ComputerUtilCard.getWorstCreatureAI(forced);

View File

@@ -70,12 +70,12 @@ public class DestroyAllAi extends SpellAbilityAi {
return doMassRemovalLogic(ai, sa); return doMassRemovalLogic(ai, sa);
} }
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) { public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", ""); final String logic = sa.getParamOrDefault("AILogic", "");
Player opponent = ai.getWeakestOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()?
final int CREATURE_EVAL_THRESHOLD = 200; // if we hit the whole board, the other opponents who are not the reason to cast this probably still suffer a bit too
final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1);
if (logic.equals("Always")) { if (logic.equals("Always")) {
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
@@ -93,99 +93,101 @@ public class DestroyAllAi extends SpellAbilityAi {
valid = valid.replace("X", Integer.toString(xPay)); valid = valid.replace("X", Integer.toString(xPay));
} }
CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), // TODO should probably sort results when targeted to use on biggest threat instead of first match
valid.split(","), source.getController(), source, sa); for (Player opponent: ai.getOpponents()) {
CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
source.getController(), source, sa); CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
opplist = CardLists.filter(opplist, predicate);
ailist = CardLists.filter(ailist, predicate);
if (opplist.isEmpty()) {
return false;
}
if (sa.usesTargeting()) { opplist = CardLists.filter(opplist, predicate);
sa.resetTargets(); ailist = CardLists.filter(ailist, predicate);
if (sa.canTarget(opponent)) { if (opplist.isEmpty()) {
sa.getTargets().add(opponent);
ailist.clear();
} else {
return false; return false;
} }
}
// Special handling for Raiding Party if (sa.usesTargeting()) {
if (logic.equals("RaidingParty")) { sa.resetTargets();
int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size()); if (sa.canTarget(opponent)) {
int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size()); sa.getTargets().add(opponent);
ailist.clear();
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave); } else {
}
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
return true;
}
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
return true;
}
// if only creatures are affected evaluate both lists and pass only if
// human creatures are more valuable
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
return true;
}
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
return false;
}
// test whether the human can kill the ai next turn
Combat combat = new Combat(opponent);
boolean containsAttacker = false;
for (Card att : opponent.getCreaturesInPlay()) {
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
combat.addAttacker(att, ai);
containsAttacker = containsAttacker | opplist.contains(att);
}
}
if (!containsAttacker) {
return false;
}
AiBlockController block = new AiBlockController(ai);
block.assignBlockersForCombat(combat);
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
return true;
}
return false;
} // only lands involved
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds") && !opplist.isEmpty()) {
return true;
}
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = opponent.getCreaturesInPlay();
if (!oppCreatures.isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
return false; return false;
} }
} }
// check if the AI would lose more lands than the opponent would
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) { // Special handling for Raiding Party
if (logic.equals("RaidingParty")) {
int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size());
int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size());
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
}
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
return true;
}
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
return true;
}
// if only creatures are affected evaluate both lists and pass only if human creatures are more valuable
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
return true;
}
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
return false;
}
// test whether the human can kill the ai next turn
Combat combat = new Combat(opponent);
boolean containsAttacker = false;
for (Card att : opponent.getCreaturesInPlay()) {
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
combat.addAttacker(att, ai);
containsAttacker = containsAttacker | opplist.contains(att);
}
}
if (!containsAttacker) {
return false;
}
AiBlockController block = new AiBlockController(ai);
block.assignBlockersForCombat(combat);
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
return true;
}
return false;
} // only lands involved
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) {
return true;
}
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = opponent.getCreaturesInPlay();
if (!oppCreatures.isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
return false;
}
}
// check if the AI would lose more lands than the opponent would
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) {
return false;
}
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
return false; return false;
} }
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable return true;
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
return false;
} }
return true; return false;
} }
}
}

View File

@@ -5,6 +5,7 @@ import java.util.Map;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -32,7 +33,7 @@ public class DigAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame(); final Game game = ai.getGame();
Player opp = ai.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
Player libraryOwner = ai; Player libraryOwner = ai;
@@ -120,7 +121,7 @@ public class DigAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility(); final SpellAbility root = sa.getRootAbility();
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (mandatory && sa.canTarget(opp)) { if (mandatory && sa.canTarget(opp)) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
@@ -19,7 +20,7 @@ public class DigMultipleAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame(); final Game game = ai.getGame();
Player opp = ai.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
Player libraryOwner = ai; Player libraryOwner = ai;
@@ -77,7 +78,7 @@ public class DigMultipleAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (mandatory && sa.canTarget(opp)) { if (mandatory && sa.canTarget(opp)) {

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
@@ -31,10 +32,8 @@ public class DigUntilAi extends SpellAbilityAi {
chance = 1; chance = 1;
} }
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
Player libraryOwner = ai; Player libraryOwner = ai;
Player opp = ai.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if ("DontMillSelf".equals(logic)) { if ("DontMillSelf".equals(logic)) {
// A card that digs for specific things and puts everything revealed before it into graveyard // A card that digs for specific things and puts everything revealed before it into graveyard
@@ -92,12 +91,12 @@ public class DigUntilAi extends SpellAbilityAi {
return false; return false;
} }
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
return randomReturn; return randomReturn;
} }
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (sa.isCurse()) { if (sa.isCurse()) {

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
@@ -60,11 +61,9 @@ public class DiscardAi extends SpellAbilityAi {
if (players.get(0) == ai) { if (players.get(0) == ai) {
// the ai should only be using something like this if he has // the ai should only be using something like this if he has
// few cards in hand, // few cards in hand,
// cards like this better have a good drawback to be in the // cards like this better have a good drawback to be in the AIs deck
// AIs deck
} else { } else {
// defined to the human, so that's fine as long the human // defined to the human, so that's fine as long the human has cards
// has cards
if (!humanHasHand) { if (!humanHasHand) {
return false; return false;
} }
@@ -170,7 +169,7 @@ public class DiscardAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
Player opp = ai.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (!discardTargetAI(ai, sa)) { if (!discardTargetAI(ai, sa)) {
if (mandatory && sa.canTarget(opp)) { if (mandatory && sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);

View File

@@ -23,8 +23,7 @@ public class DrainManaAi extends SpellAbilityAi {
if (tgt == null) { if (tgt == null) {
// assume we are looking to tap human's stuff // assume we are looking to tap human's stuff
// TODO - check for things with untap abilities, and don't tap // TODO - check for things with untap abilities, and don't tap those.
// those.
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (!defined.contains(opp)) { if (!defined.contains(opp)) {

View File

@@ -29,11 +29,10 @@ public class GameLossAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
// Phage the Untouchable // Phage the Untouchable
// (Final Fortune would need to attach it's delayed trigger to a // (Final Fortune would need to attach it's delayed trigger to a
// specific turn, which can't be done yet) // specific turn, which can't be done yet)
Player opp = ai.getWeakestOpponent(); Player opp = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard());
if (!mandatory && opp.cantLose()) { if (!mandatory && opp.cantLose()) {
return false; return false;

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -19,7 +20,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final int myLife = aiPlayer.getLife(); final int myLife = aiPlayer.getLife();
Player opponent = aiPlayer.getWeakestOpponent(); Player opponent = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
final int hLife = opponent.getLife(); final int hLife = opponent.getLife();
if (!aiPlayer.canGainLife()) { if (!aiPlayer.canGainLife()) {
@@ -75,7 +76,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
final boolean mandatory) { final boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = ai.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.AiProps; import forge.ai.AiProps;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
@@ -154,7 +155,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
final boolean mandatory) { final boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = ai.getWeakestOpponent(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {

View File

@@ -17,7 +17,7 @@ public class LifeSetAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final int myLife = ai.getLife(); final int myLife = ai.getLife();
final Player opponent = ai.getWeakestOpponent(); final Player opponent = ai.getStrongestOpponent();
final int hlife = opponent.getLife(); final int hlife = opponent.getLife();
final String amountStr = sa.getParam("LifeAmount"); final String amountStr = sa.getParam("LifeAmount");
@@ -36,8 +36,7 @@ public class LifeSetAi extends SpellAbilityAi {
return false; return false;
} }
// TODO handle proper calculation of X values based on Cost and what // TODO handle proper calculation of X values based on Cost and what would be paid
// would be paid
int amount; int amount;
// we shouldn't have to worry too much about PayX for SetLife // we shouldn't have to worry too much about PayX for SetLife
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
@@ -58,11 +57,9 @@ public class LifeSetAi extends SpellAbilityAi {
if (tgt.canOnlyTgtOpponent()) { if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(opponent); sa.getTargets().add(opponent);
// if we can only target the human, and the Human's life // if we can only target the human, and the Human's life
// would // would go up, don't play it.
// go up, don't play it.
// possibly add a combo here for Magister Sphinx and // possibly add a combo here for Magister Sphinx and
// Higedetsu's // Higedetsu's (sp?) Second Rite
// (sp?) Second Rite
if ((amount > hlife) || !opponent.canLoseLife()) { if ((amount > hlife) || !opponent.canLoseLife()) {
return false; return false;
} }
@@ -81,8 +78,7 @@ public class LifeSetAi extends SpellAbilityAi {
if (sa.getParam("Defined").equals("Player")) { if (sa.getParam("Defined").equals("Player")) {
if (amount == 0) { if (amount == 0) {
return false; return false;
} else if (myLife > amount) { // will decrease computer's } else if (myLife > amount) { // will decrease computer's life
// life
if ((myLife < 5) || ((myLife - amount) > (hlife - amount))) { if ((myLife < 5) || ((myLife - amount) > (hlife - amount))) {
return false; return false;
} }
@@ -104,7 +100,7 @@ public class LifeSetAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final int myLife = ai.getLife(); final int myLife = ai.getLife();
final Player opponent = ai.getWeakestOpponent(); final Player opponent = ai.getStrongestOpponent();
final int hlife = opponent.getLife(); final int hlife = opponent.getLife();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
@@ -133,8 +129,7 @@ public class LifeSetAi extends SpellAbilityAi {
} }
// If the Target is gaining life, target self. // If the Target is gaining life, target self.
// if the Target is modifying how much life is gained, this needs to // if the Target is modifying how much life is gained, this needs to be handled better
// be handled better
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();

View File

@@ -30,13 +30,12 @@ public class PowerExchangeAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
List<Card> list = List<Card> list =
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); CardLists.getValidCards(ai.getGame().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on // AI won't try to grab cards that are filtered out of AI decks on purpose
// purpose
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa); return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa) && c.getController() != ai;
} }
}); });
CardLists.sortByPowerAsc(list); CardLists.sortByPowerAsc(list);

View File

@@ -6,6 +6,7 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
@@ -201,7 +202,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
final Game game = ai.getGame(); final Game game = ai.getGame();
final Combat combat = game.getCombat(); final Combat combat = game.getCombat();
final PhaseHandler ph = game.getPhaseHandler(); final PhaseHandler ph = game.getPhaseHandler();
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final int newPower = card.getNetCombatDamage() + attack; final int newPower = card.getNetCombatDamage() + attack;
//int defense = getNumDefense(sa); //int defense = getNumDefense(sa);
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) { if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {

View File

@@ -48,12 +48,6 @@ public class PumpAllAi extends PumpAiBase {
} }
} }
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>();
final PhaseType phase = game.getPhaseHandler().getPhase();
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return false;
} }
@@ -63,16 +57,10 @@ public class PumpAllAi extends PumpAiBase {
return false; return false;
} }
} }
if (sa.hasParam("ValidCards")) {
valid = sa.getParam("ValidCards");
}
final Player opp = ai.getWeakestOpponent();
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = ai.getStrongestOpponent();
if (tgt != null && sa.canTarget(opp) && sa.hasParam("IsCurse")) { if (tgt != null && sa.canTarget(opp) && sa.hasParam("IsCurse")) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(opp); sa.getTargets().add(opp);
@@ -85,6 +73,18 @@ public class PumpAllAi extends PumpAiBase {
return true; return true;
} }
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>();
final PhaseType phase = game.getPhaseHandler().getPhase();
if (sa.hasParam("ValidCards")) {
valid = sa.getParam("ValidCards");
}
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
if (!game.getStack().isEmpty() && !sa.isCurse()) { if (!game.getStack().isEmpty() && !sa.isCurse()) {
return pumpAgainstRemoval(ai, sa, comp); return pumpAgainstRemoval(ai, sa, comp);
} }
@@ -139,8 +139,7 @@ public class PumpAllAi extends PumpAiBase {
return true; return true;
} }
// evaluate both lists and pass only if human creatures are more // evaluate both lists and pass only if human creatures are more valuable
// valuable
return (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human); return (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human);
} // end Curse } // end Curse

View File

@@ -1,6 +1,7 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.AiController; import forge.ai.AiController;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi; import forge.ai.PlayerControllerAi;
@@ -14,7 +15,7 @@ public class RepeatAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (!opp.canBeTargetedBy(sa)) { if (!opp.canBeTargetedBy(sa)) {
@@ -44,9 +45,8 @@ public class RepeatAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(opp); sa.getTargets().add(opp);

View File

@@ -20,9 +20,6 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
public class SacrificeAi extends SpellAbilityAi { public class SacrificeAi extends SpellAbilityAi {
// **************************************************************
// *************************** Sacrifice ***********************
// **************************************************************
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -48,8 +45,7 @@ public class SacrificeAi extends SpellAbilityAi {
} }
// Improve AI for triggers. If source is a creature with: // Improve AI for triggers. If source is a creature with:
// When ETB, sacrifice a creature. Check to see if the AI has something // When ETB, sacrifice a creature. Check to see if the AI has something to sacrifice
// to sacrifice
// Eventually, we can call the trigger of ETB abilities with not // Eventually, we can call the trigger of ETB abilities with not
// mandatory as part of the checks to cast something // mandatory as part of the checks to cast something
@@ -58,12 +54,11 @@ public class SacrificeAi extends SpellAbilityAi {
} }
private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa) { private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final boolean destroy = sa.hasParam("Destroy"); final boolean destroy = sa.hasParam("Destroy");
Player opp = ai.getWeakestOpponent(); Player opp = ai.getStrongestOpponent();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
@@ -109,8 +104,7 @@ public class SacrificeAi extends SpellAbilityAi {
sa.setXManaCostPaid(Math.min(ComputerUtilCost.getMaxXValue(sa, ai), amount)); sa.setXManaCostPaid(Math.min(ComputerUtilCost.getMaxXValue(sa, ai), amount));
} }
final int half = (amount / 2) + (amount % 2); // Half of amount final int half = (amount / 2) + (amount % 2); // Half of amount rounded up
// rounded up
// If the Human has at least half rounded up of the amount to be // If the Human has at least half rounded up of the amount to be
// sacrificed, cast the spell // sacrificed, cast the spell
@@ -130,8 +124,7 @@ public class SacrificeAi extends SpellAbilityAi {
// If Sacrifice hits both players: // If Sacrifice hits both players:
// Only cast it if Human has the full amount of valid // Only cast it if Human has the full amount of valid
// Only cast it if AI doesn't have the full amount of Valid // Only cast it if AI doesn't have the full amount of Valid
// TODO: Cast if the type is favorable: my "worst" valid is // TODO: Cast if the type is favorable: my "worst" valid is worse than his "worst" valid
// worse than his "worst" valid
final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1"; final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1";
int amount = AbilityUtils.calculateAmount(source, num, sa); int amount = AbilityUtils.calculateAmount(source, num, sa);

View File

@@ -2,17 +2,12 @@ package forge.ai.ability;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil;
public class SacrificeAllAi extends SpellAbilityAi { public class SacrificeAllAi extends SpellAbilityAi {
@@ -22,22 +17,7 @@ public class SacrificeAllAi extends SpellAbilityAi {
// based on what the expected targets could be // based on what the expected targets could be
final Cost abCost = sa.getPayCosts(); final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
String valid = ""; final String logic = sa.getParamOrDefault("AILogic", "");
if (sa.hasParam("ValidCards")) {
valid = sa.getParam("ValidCards");
}
if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay));
}
CardCollection humanlist =
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
CardCollection computerlist =
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
if (abCost != null) { if (abCost != null) {
// AI currently disabled for some costs // AI currently disabled for some costs
@@ -45,30 +25,20 @@ public class SacrificeAllAi extends SpellAbilityAi {
return false; return false;
} }
} }
if (logic.equals("HellionEruption")) {
if (ai.getCreaturesInPlay().size() < 5 || ai.getCreaturesInPlay().size() * 150 < ComputerUtilCard.evaluateCreatureList(ai.getCreaturesInPlay())) {
return false;
}
}
if (!DestroyAllAi.doMassRemovalLogic(ai, sa)) {
return false;
}
// prevent run-away activations - first time will always return true // prevent run-away activations - first time will always return true
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
// if only creatures are affected evaluate both lists and pass only if
// human creatures are more valuable
if ((CardLists.getNotType(humanlist, "Creature").size() == 0) && (CardLists.getNotType(computerlist, "Creature").size() == 0)) {
if ((ComputerUtilCard.evaluateCreatureList(computerlist) + 200) >= ComputerUtilCard
.evaluateCreatureList(humanlist)) {
return false;
}
} // only lands involved
else if ((CardLists.getNotType(humanlist, "Land").size() == 0) && (CardLists.getNotType(computerlist, "Land").size() == 0)) {
if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 1) >= ComputerUtilCard
.evaluatePermanentList(humanlist)) {
return false;
}
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are more valuable
else if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 3) >= ComputerUtilCard
.evaluatePermanentList(humanlist)) {
return false;
}
return ((MyRandom.getRandom().nextFloat() < .9667) && chance); return ((MyRandom.getRandom().nextFloat() < .9667) && chance);
} }

View File

@@ -66,7 +66,6 @@ public class TapAi extends TapAiBase {
// Set PayX here to maximum value. // Set PayX here to maximum value.
// TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore?
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai)); sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai));
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
} }
sa.resetTargets(); sa.resetTargets();

View File

@@ -5,6 +5,7 @@ import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -109,7 +110,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
* @return a boolean. * @return a boolean.
*/ */
protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) { protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Game game = ai.getGame(); final Game game = ai.getGame();
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tapList = CardLists.getTargetableCards(tapList, sa); tapList = CardLists.getTargetableCards(tapList, sa);

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi {
valid = sa.getParam("ValidCards"); valid = sa.getParam("ValidCards");
} }
final Player opp = ai.getWeakestOpponent(); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {

View File

@@ -56,7 +56,6 @@ public class UnattachAllAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card card = sa.getHostCard(); final Card card = sa.getHostCard();
final Player opp = ai.getWeakestOpponent();
// Check if there are any valid targets // Check if there are any valid targets
List<GameObject> targets = new ArrayList<>(); List<GameObject> targets = new ArrayList<>();
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -66,8 +65,8 @@ public class UnattachAllAi extends SpellAbilityAi {
if (!mandatory && card.isEquipment() && !targets.isEmpty()) { if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
Card newTarget = (Card) targets.get(0); Card newTarget = (Card) targets.get(0);
//don't equip human creatures //don't equip opponent creatures
if (newTarget.getController().equals(opp)) { if (!newTarget.getController().equals(ai)) {
return false; return false;
} }

View File

@@ -6,6 +6,7 @@ import java.util.Map;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -130,7 +131,8 @@ public class UntapAi extends SpellAbilityAi {
Player targetController = ai; Player targetController = ai;
if (sa.isCurse()) { if (sa.isCurse()) {
targetController = ai.getWeakestOpponent(); // TODO search through all opponents, may need to check if different controllers allowed
targetController = AiAttackController.choosePreferredDefenderPlayer(ai);
} }
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa); CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
@@ -149,8 +151,7 @@ public class UntapAi extends SpellAbilityAi {
} }
CardCollection untapList = targetUntapped ? list : CardLists.filter(list, Presets.TAPPED); CardCollection untapList = targetUntapped ? list : CardLists.filter(list, Presets.TAPPED);
// filter out enchantments and planeswalkers, their tapped state doesn't // filter out enchantments and planeswalkers, their tapped state doesn't matter.
// matter.
final String[] tappablePermanents = {"Creature", "Land", "Artifact"}; final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa); untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa);

View File

@@ -30,6 +30,7 @@ public enum ApiType {
Block (BlockEffect.class), Block (BlockEffect.class),
Bond (BondEffect.class), Bond (BondEffect.class),
Branch (BranchEffect.class), Branch (BranchEffect.class),
Camouflage (CamouflageEffect.class),
ChangeCombatants (ChangeCombatantsEffect.class), ChangeCombatants (ChangeCombatantsEffect.class),
ChangeTargets (ChangeTargetsEffect.class), ChangeTargets (ChangeTargetsEffect.class),
ChangeText (ChangeTextEffect.class), ChangeText (ChangeTextEffect.class),

View File

@@ -0,0 +1,108 @@
package forge.game.ability.effects;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Localizer;
public class CamouflageEffect extends SpellAbilityEffect {
private void randomizeBlockers(SpellAbility sa, Combat combat, Player declarer, Player defender, List<Card> attackers, List<CardCollection> blockerPiles) {
CardLists.shuffle(attackers);
for (int i = 0; i < attackers.size(); i++) {
final Card attacker = attackers.get(i);
CardCollection blockers = blockerPiles.get(i);
// Remove all illegal blockers first
for (int j = blockers.size() - 1; j >= 0; j--) {
final Card blocker = blockers.get(j);
if (!CombatUtil.canBlock(attacker, blocker, combat)) {
blockers.remove(j);
}
}
if (attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") &&
blockers.size() < defender.getCreaturesInPlay().size() ||
blockers.size() < CombatUtil.needsBlockers(attacker)) {
// If not enough remaining creatures to block, don't add them as blocker
continue;
}
if (attacker.hasKeyword("CantBeBlockedByAmount GT1") && blockers.size() > 1) {
// If no more than one creature can block, order the player to choose one to block
Card chosen = declarer.getController().chooseCardsForEffect(blockers, sa,
Localizer.getInstance().getMessage("lblChooseBlockerForAttacker", attacker.toString()), 1, 1, false, null).get(0);
combat.addBlocker(attacker, chosen);
continue;
}
// Add all remaning blockers normally
for (final Card blocker : blockers) {
combat.addBlocker(attacker, blocker);
}
}
}
@Override
public void resolve(SpellAbility sa) {
Card hostCard = sa.getHostCard();
Player declarer = getDefinedPlayersOrTargeted(sa).get(0);
Player defender = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("Defender"), sa).get(0);
Combat combat = hostCard.getGame().getCombat();
List<Card> attackers = combat.getAttackers();
List<CardCollection> blockerPiles = new ArrayList<>();
if (declarer.isAI()) {
// For AI player, just let it declare blockers normally, then randomize it later.
declarer.getController().declareBlockers(defender, combat);
// Remove all blockers first
for (final Card attacker : attackers) {
CardCollection blockers = combat.getBlockers(attacker);
blockerPiles.add(blockers);
for (final Card blocker : blockers) {
combat.removeFromCombat(blocker);
}
}
} else { // Human player
CardCollection pool = new CardCollection(defender.getCreaturesInPlay());
// remove all blockers that can't block
for (final Card blocker : pool) {
if (!CombatUtil.canBlock(blocker)) {
pool.remove(blocker);
}
}
List<Integer> blockedSoFar = new ArrayList<>(Collections.nCopies(pool.size(), 0));
for (int i = 0; i < attackers.size(); i++) {
int size = pool.size();
CardCollection blockers = new CardCollection(declarer.getController().chooseCardsForEffect(
pool, sa, Localizer.getInstance().getMessage("lblChooseBlockersForPile", String.valueOf(i + 1)), 0, size, false, null));
blockerPiles.add(blockers);
// Remove chosen creatures, unless it can block additional attackers
for (final Card blocker : blockers) {
int index = pool.indexOf(blocker);
Integer blockedCount = blockedSoFar.get(index) + 1;
if (!blocker.canBlockAny() && blocker.canBlockAdditional() < blockedCount) {
pool.remove(index);
blockedSoFar.remove(index);
} else {
blockedSoFar.set(index, blockedCount);
}
}
}
}
randomizeBlockers(sa, combat, declarer, defender, attackers, blockerPiles);
}
}

View File

@@ -349,6 +349,9 @@ public class DiscardEffect extends SpellAbilityEffect {
runParams.put(AbilityKey.Cause, sa); runParams.put(AbilityKey.Cause, sa);
runParams.put(AbilityKey.FirstTime, firstDiscard); runParams.put(AbilityKey.FirstTime, firstDiscard);
game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false);
if (sa.hasParam("RememberDiscardingPlayers")) {
source.addRemembered(p);
}
} }
} }

View File

@@ -1247,17 +1247,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
currentState.addTrigger(t); currentState.addTrigger(t);
return t; return t;
} }
@Deprecated
public final void removeTrigger(final Trigger t) {
currentState.removeTrigger(t);
}
@Deprecated
public final void removeTrigger(final Trigger t, final CardStateName state) {
getState(state).removeTrigger(t);
}
public final void clearTriggersNew() {
currentState.clearTriggers();
}
public final boolean hasTrigger(final Trigger t) { public final boolean hasTrigger(final Trigger t) {
return currentState.hasTrigger(t); return currentState.hasTrigger(t);

View File

@@ -653,7 +653,14 @@ public class PhaseHandler implements java.io.Serializable {
} }
if (combat.isPlayerAttacked(p)) { if (combat.isPlayerAttacked(p)) {
if (CombatUtil.canBlock(p, combat)) { if (CombatUtil.canBlock(p, combat)) {
whoDeclaresBlockers.getController().declareBlockers(p, combat); // Replacement effects (for Camouflage)
final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(p);
repRunParams.put(AbilityKey.Player, whoDeclaresBlockers);
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.DeclareBlocker, repRunParams);
if (repres == ReplacementResult.NotReplaced) {
// If not replaced, run normal declare blockers
whoDeclaresBlockers.getController().declareBlockers(p, combat);
}
} }
} }
else { continue; } else { continue; }

View File

@@ -421,6 +421,9 @@ public class Player extends GameEntity implements Comparable<Player> {
public final Player getWeakestOpponent() { public final Player getWeakestOpponent() {
return getOpponents().min(PlayerPredicates.compareByLife()); return getOpponents().min(PlayerPredicates.compareByLife());
} }
public final Player getStrongestOpponent() {
return getOpponents().max(PlayerPredicates.compareByLife());
}
public boolean isOpponentOf(Player other) { public boolean isOpponentOf(Player other) {
return other != this && other != null && (other.teamNumber < 0 || other.teamNumber != teamNumber); return other != this && other != null && (other.teamNumber < 0 || other.teamNumber != teamNumber);

View File

@@ -0,0 +1,29 @@
package forge.game.replacement;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
public class ReplaceDeclareBlocker extends ReplacementEffect {
public ReplaceDeclareBlocker(final Map<String, String> mapParams, final Card host, final boolean intrinsic) {
super(mapParams, host, intrinsic);
}
@Override
public boolean canReplace(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) {
return false;
}
return true;
}
@Override
public void setReplacingObjects(Map<AbilityKey, Object> runParams, SpellAbility sa) {
sa.setReplacingObject(AbilityKey.DefendingPlayer, runParams.get(AbilityKey.Affected));
// Here the Player is the one who would declare blockers (may be changed by some Card's effect)
sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Player));
}
}

View File

@@ -21,6 +21,7 @@ public enum ReplacementType {
CreateToken(ReplaceToken.class), CreateToken(ReplaceToken.class),
DamageDone(ReplaceDamage.class), DamageDone(ReplaceDamage.class),
DealtDamage(ReplaceDealtDamage.class), DealtDamage(ReplaceDealtDamage.class),
DeclareBlocker(ReplaceDeclareBlocker.class),
Destroy(ReplaceDestroy.class), Destroy(ReplaceDestroy.class),
Discard(ReplaceDiscard.class), Discard(ReplaceDiscard.class),
Draw(ReplaceDraw.class), Draw(ReplaceDraw.class),

View File

@@ -0,0 +1,7 @@
Name:Camouflage
ManaCost:G
Types:Instant
A:SP$ Effect | Cost$ G | ReplacementEffects$ RDeclareBlocker | ActivationPhases$ Declare Attackers | PlayerTurn$ True | AILogic$ Evasion | SpellDescription$ Cast this spell only during your declare attackers step. This turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.)
SVar:RDeclareBlocker:Event$ DeclareBlocker | ValidPlayer$ Opponent | ReplaceWith$ DBCamouflage | Description$ This turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.)
SVar:DBCamouflage:DB$ Camouflage | Defined$ ReplacedPlayer | Defender$ ReplacedDefendingPlayer | AILogic$ BestBlocker
Oracle:Cast this spell only during your declare attackers step.\nThis turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.)

View File

@@ -10,4 +10,5 @@ SVar:DBCleanup:DB$ Cleanup | ForgetDefined$ TriggeredCard
A:AB$ FlipACoin | Cost$ T | TgtZone$ Stack | TargetType$ Spell | ValidTgts$ Artifact.YouCtrl+IsRemembered | WinSubAbility$ DBDraw | LoseSubAbility$ DBCounter | TgtPrompt$ Select target Artifact spell | SpellDescription$ Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans. A:AB$ FlipACoin | Cost$ T | TgtZone$ Stack | TargetType$ Spell | ValidTgts$ Artifact.YouCtrl+IsRemembered | WinSubAbility$ DBDraw | LoseSubAbility$ DBCounter | TgtPrompt$ Select target Artifact spell | SpellDescription$ Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans.
SVar:DBDraw:DB$ Draw | NumCards$ 1 | Defined$ You SVar:DBDraw:DB$ Draw | NumCards$ 1 | Defined$ You
SVar:DBCounter:DB$ Counter | Defined$ Targeted SVar:DBCounter:DB$ Counter | Defined$ Targeted
AI:RemoveDeck:All
Oracle:{T}: Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans. Oracle:{T}: Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans.

View File

@@ -1,9 +1,8 @@
Name:Hellion Eruption Name:Hellion Eruption
ManaCost:5 R ManaCost:5 R
Types:Sorcery Types:Sorcery
A:SP$ SacrificeAll | Cost$ 5 R | ValidCards$ Creature.YouCtrl | RememberSacrificed$ True | SubAbility$ DBToken | SpellDescription$ Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens. A:SP$ SacrificeAll | Cost$ 5 R | ValidCards$ Creature.YouCtrl | RememberSacrificed$ True | SubAbility$ DBToken | AILogic$ HellionEruption | SpellDescription$ Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens.
SVar:DBToken:DB$Token | TokenAmount$ X | TokenScript$ r_4_4_hellion | TokenOwner$ You | LegacyImage$ r 4 4 hellion roe SVar:DBToken:DB$Token | TokenAmount$ X | TokenScript$ r_4_4_hellion | TokenOwner$ You | LegacyImage$ r 4 4 hellion roe
SVar:X:Remembered$Amount SVar:X:Remembered$Amount
AI:RemoveDeck:All
SVar:Picture:http://www.wizards.com/global/images/magic/general/hellion_eruption.jpg SVar:Picture:http://www.wizards.com/global/images/magic/general/hellion_eruption.jpg
Oracle:Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens. Oracle:Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens.

View File

@@ -0,0 +1,8 @@
Name:Frostboil Snarl
ManaCost:no cost
Types:Land
K:ETBReplacement:Other:DBTap
SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Island,Mountain/Island or Mountain> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Island or Mountain card from your hand. If you don't, CARDNAME enters the battlefield tapped.
A:AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}.
A:AB$ Mana | Cost$ T | Produced$ R | SpellDescription$ Add {R}.
Oracle:As Frostboil Snarl enters the battlefield, you may reveal a Island or Mountain card from your hand. If you don't, Frostboil Snarl enters the battlefield tapped.\n{T}: Add {U} or {R}.

View File

@@ -0,0 +1,8 @@
Name:Furycalm Snarl
ManaCost:no cost
Types:Land
K:ETBReplacement:Other:DBTap
SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Mountain,Plains/Mountain or Plains> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Mountain or Plains card from your hand. If you don't, CARDNAME enters the battlefield tapped.
A:AB$ Mana | Cost$ T | Produced$ R | SpellDescription$ Add {R}.
A:AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}.
Oracle:As Furycalm Snarl enters the battlefield, you may reveal a Mountain or Plains card from your hand. If you don't, Furycalm Snarl enters the battlefield tapped.\n{T}: Add {R} or {W}.

View File

@@ -0,0 +1,8 @@
Name:Necroblossom Snarl
ManaCost:no cost
Types:Land
K:ETBReplacement:Other:DBTap
SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Swamp,Forest/Swamp or Forest> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Swamp or Forest card from your hand. If you don't, CARDNAME enters the battlefield tapped.
A:AB$ Mana | Cost$ T | Produced$ B | SpellDescription$ Add {B}.
A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}.
Oracle:As Necroblossom Snarl enters the battlefield, you may reveal a Swamp or Forest card from your hand. If you don't, Necroblossom Snarl enters the battlefield tapped.\n{T}: Add {B} or {G}.

View File

@@ -0,0 +1,20 @@
Name:Professor Onyx
ManaCost:4 B B
Types:Legendary Planeswalker Liliana
Loyalty:5
T:Mode$ SpellCast | ValidCard$ Instant,Sorcery | ValidActivatingPlayer$ You | NoResolvingCheck$ True | Execute$ TrigDrain | TriggerZones$ Battlefield | TriggerDescription$ Magecraft — Whenever you cast or copy an instant or sorcery spell, each opponent loses 2 life and you gain 2 life.
T:Mode$ SpellCopy | ValidCard$ Instant,Sorcery | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigDrain | Secondary$ True | TriggerDescription$ Magecraft — Whenever you cast or copy an instant or sorcery spell, each opponent loses 2 life and you gain 2 life.
SVar:TrigDrain:DB$ LoseLife | Defined$ Player.Opponent | LifeAmount$ 2 | SubAbility$ DBGainLife
SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 2
A:AB$ LoseLife | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | Defined$ You | LifeAmount$ 1 | SubAbility$ DBDig | SpellDescription$ You lose 1 life. Look at the top three cards of your library. Put one of them into your hand and the rest into your graveyard.
SVar:DBDig:DB$ Dig | DigNum$ 3 | ChangeNum$ 1 | DestinationZone2$ Graveyard | StackDescription$ {p:You} looks at the top three cards of their library. {p:You} puts one of them into their hand and the rest into their graveyard.
A:AB$ RepeatEach | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | RepeatPlayers$ Opponent | RepeatSubAbility$ DBChooseCard | SubAbility$ DBSac | SpellDescription$ Each opponent sacrifices a creature with the greatest power among creatures that player controls.
SVar:DBChooseCard:DB$ ChooseCard | Defined$ Player.IsRemembered | Choices$ Creature.greatestPowerControlledByRemembered | ChoiceTitle$ Choose a creature you control with the greatest power | Mandatory$ True | RememberChosen$ True
SVar:DBSac:DB$ SacrificeAll | ValidCards$ Card.IsRemembered | SubAbility$ DBCleanup | StackDescription$ Each opponent sacrifices a creature with the greatest power among creatures they control.
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearChosen$ True
A:AB$ Repeat | Cost$ SubCounter<8/LOYALTY> | Planeswalker$ True | Ultimate$ True | RepeatSubAbility$ TrigDiscard | MaxRepeat$ 7 | StackDescription$ SpellDescription | SpellDescription$ Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times.
SVar:TrigDiscard:DB$ Discard | Defined$ Player.Opponent | Mode$ TgtChoose | Optional$ True | RememberDiscardingPlayers$ True | AILogic$ UnlessAtLife.6 | SubAbility$ DBLoseLife
SVar:DBLoseLife:DB$ LoseLife | Defined$ Opponent.IsNotRemembered | LifeAmount$ 3 | SubAbility$ DBCleanup
DeckHints:Type$Instant|Sorcery
DeckHas:Ability$Graveyard & Ability$LifeGain
Oracle:Magecraft — Whenever you cast or copy an instant or sorcery spell, each opponent loses 2 life and you gain 2 life.\n[+1]: You lose 1 life. Look at the top three cards of your library. Put one of them into your hand and the rest into your graveyard.\n[3]: Each opponent sacrifices a creature with the greatest power among creatures that player controls.\n[8]: Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times.

View File

@@ -0,0 +1,8 @@
Name:Shineshadow Snarl
ManaCost:no cost
Types:Land
K:ETBReplacement:Other:DBTap
SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Plains,Swamp/Plains or Swamp> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Plains or Swamp card from your hand. If you don't, CARDNAME enters the battlefield tapped.
A:AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}.
A:AB$ Mana | Cost$ T | Produced$ B | SpellDescription$ Add {B}.
Oracle:As Shineshadow Snarl enters the battlefield, you may reveal a Plains or Swamp card from your hand. If you don't, Shineshadow Snarl enters the battlefield tapped.\n{T}: Add {W} or {B}.

View File

@@ -0,0 +1,8 @@
Name:Vineglimmer Snarl
ManaCost:no cost
Types:Land
K:ETBReplacement:Other:DBTap
SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Forest,Island/Forest or Island> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Forest or Island card from your hand. If you don't, CARDNAME enters the battlefield tapped.
A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}.
A:AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}.
Oracle:As Vineglimmer Snarl enters the battlefield, you may reveal a Forest or Island card from your hand. If you don't, Vineglimmer Snarl enters the battlefield tapped.\n{T}: Add {G} or {U}.

View File

@@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Möchtest du überbieten? Aktuelles Gebot:
lblTopBidWithValueLife=hat mit {0} Leben überboten lblTopBidWithValueLife=hat mit {0} Leben überboten
#BondEffect.java #BondEffect.java
lblSelectACardPair=Wähle Karte zum Verbinden lblSelectACardPair=Wähle Karte zum Verbinden
#CamouflageEffect.java
lblChooseBlockerForAttacker=Choose a creature to block {0}
lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty)
#ChangeCombatantsEffect.java #ChangeCombatantsEffect.java
lblChooseDefenderToAttackWithCard=Welchen Verteidiger mit {0} angreifen? lblChooseDefenderToAttackWithCard=Welchen Verteidiger mit {0} angreifen?
#ChangeTargetsEffect.java #ChangeTargetsEffect.java

View File

@@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Do you want to top bid? Current Bid \=
lblTopBidWithValueLife=topped bid with {0} life lblTopBidWithValueLife=topped bid with {0} life
#BondEffect.java #BondEffect.java
lblSelectACardPair=Select a card to pair with lblSelectACardPair=Select a card to pair with
#CamouflageEffect.java
lblChooseBlockerForAttacker=Choose a creature to block {0}
lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty)
#ChangeCombatantsEffect.java #ChangeCombatantsEffect.java
lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0} lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0}
#ChangeTargetsEffect.java #ChangeTargetsEffect.java

View File

@@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=¿Quieres hacer una puja máxima? Puja actual \=
lblTopBidWithValueLife=puja más alta con {0} de vida lblTopBidWithValueLife=puja más alta con {0} de vida
#BondEffect.java #BondEffect.java
lblSelectACardPair=Selecciona una carta para emparejarla con lblSelectACardPair=Selecciona una carta para emparejarla con
#CamouflageEffect.java
lblChooseBlockerForAttacker=Choose a creature to block {0}
lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty)
#ChangeCombatantsEffect.java #ChangeCombatantsEffect.java
lblChooseDefenderToAttackWithCard=Elige con qué defensor atacar con {0} lblChooseDefenderToAttackWithCard=Elige con qué defensor atacar con {0}
#ChangeTargetsEffect.java #ChangeTargetsEffect.java

View File

@@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Do you want to top bid? Current Bid \=
lblTopBidWithValueLife=topped bid with {0} life lblTopBidWithValueLife=topped bid with {0} life
#BondEffect.java #BondEffect.java
lblSelectACardPair=Select a card to pair with lblSelectACardPair=Select a card to pair with
#CamouflageEffect.java
lblChooseBlockerForAttacker=Choose a creature to block {0}
lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty)
#ChangeCombatantsEffect.java #ChangeCombatantsEffect.java
lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0} lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0}
#ChangeTargetsEffect.java #ChangeTargetsEffect.java

View File

@@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=競りの点数を上げますか? 現在の点数 \=
lblTopBidWithValueLife={0}点のライフで競りの点数をつけた lblTopBidWithValueLife={0}点のライフで競りの点数をつけた
#BondEffect.java #BondEffect.java
lblSelectACardPair=組にしたいカードを選ぶ lblSelectACardPair=組にしたいカードを選ぶ
#CamouflageEffect.java
lblChooseBlockerForAttacker={0}をブロックするクリーチャーを選ぶ
lblChooseBlockersForPile={0}番の束に入れるクリーチャーを選ぶ(空にできる)
#ChangeCombatantsEffect.java #ChangeCombatantsEffect.java
lblChooseDefenderToAttackWithCard={0}が攻撃する対象を選ぶ lblChooseDefenderToAttackWithCard={0}が攻撃する対象を選ぶ
#ChangeTargetsEffect.java #ChangeTargetsEffect.java

View File

@@ -117,8 +117,8 @@ cbpCounterDisplayLocation=计数器显示区域
cbpGraveyardOrdering=允许指衍生物进入墓地 cbpGraveyardOrdering=允许指衍生物进入墓地
lblAltLifeDisplay=备用牌手布局(横向模式) lblAltLifeDisplay=备用牌手布局(横向模式)
nlAltLifeDisplay=启用备用牌手布局以显示玩家的生命以及中毒,能量和经验指示物。 nlAltLifeDisplay=启用备用牌手布局以显示玩家的生命以及中毒,能量和经验指示物。
lblPreferredArt=Preferred Art lblPreferredArt=首选卡图
nlPreferredArt=Sets the preferred art for cards. nlPreferredArt=设置牌张的首选卡图。
Troubleshooting=故障排除 Troubleshooting=故障排除
GeneralConfiguration=常规配置 GeneralConfiguration=常规配置
lblPlayerName=玩家名称 lblPlayerName=玩家名称
@@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=你想要喊更高的价? 现在价钱 \=
lblTopBidWithValueLife=最高喊价为{0}生命 lblTopBidWithValueLife=最高喊价为{0}生命
#BondEffect.java #BondEffect.java
lblSelectACardPair=选择要组成搭档的牌 lblSelectACardPair=选择要组成搭档的牌
#CamouflageEffect.java
lblChooseBlockerForAttacker=Choose a creature to block {0}
lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty)
#ChangeCombatantsEffect.java #ChangeCombatantsEffect.java
lblChooseDefenderToAttackWith=选择守军进行进攻 lblChooseDefenderToAttackWith=选择守军进行进攻
#ChangeTargetsEffect.java #ChangeTargetsEffect.java
@@ -1933,9 +1936,9 @@ lblTapOrUntapTarget=横置还是重置{0}
#TwoPilesEffect.java #TwoPilesEffect.java
lblSelectCardForFaceDownPile=选择一个面朝下的堆 lblSelectCardForFaceDownPile=选择一个面朝下的堆
lblDivideCardIntoTwoPiles=将牌分为两堆 lblDivideCardIntoTwoPiles=将牌分为两堆
lblSelectCardForLeftPile=Select cards for the left pile lblSelectCardForLeftPile=选择左堆中的牌张
lblLeftPile=Left pile lblLeftPile=左堆
lblRightPile=Right pile lblRightPile=右堆
lblChoosesPile=选择堆 lblChoosesPile=选择堆
lblEmptyPile=空堆 lblEmptyPile=空堆
#UntapEffect.java #UntapEffect.java
@@ -1953,7 +1956,7 @@ lblViewAll=查看所有牌
lblSetupGame=设定游戏状态 lblSetupGame=设定游戏状态
lblDumpGame=转储游戏状态 lblDumpGame=转储游戏状态
lblTutor=导师牌 lblTutor=导师牌
lblRollbackPhase=Rollback Phase lblRollbackPhase=回滚阶段
lblAddCounterPermanent=向牌添加指示物 lblAddCounterPermanent=向牌添加指示物
lblSubCounterPermanent=从牌减少指示物 lblSubCounterPermanent=从牌减少指示物
lblTapPermanent=横置永久物 lblTapPermanent=横置永久物