diff --git a/src/main/java/forge/Card.java b/src/main/java/forge/Card.java index e61cb118bfb..07dc16404ee 100644 --- a/src/main/java/forge/Card.java +++ b/src/main/java/forge/Card.java @@ -8897,6 +8897,10 @@ public class Card extends GameEntity implements Comparable { return Singletons.getModel().getGame().isCardInZone(this, zone); } + public final boolean canBeDestroyed() { + return isInPlay() && (!hasKeyword("Indestructible") || (isCreature() && getNetDefense() <= 0)); + } + /** * Can target. * @@ -9210,4 +9214,17 @@ public class Card extends GameEntity implements Comparable { return getManaCost().getCMC() + xPaid; } + public final boolean canBeSacrificedBy(final SpellAbility source) + { + if (isImmutable()) { + System.out.println("Trying to sacrifice immutables: " + this); + return false; + } + if (source != null && !getController().isOpponentOf(source.getActivatingPlayer()) + && getController().hasKeyword("Spells and abilities your opponents control can't cause you to sacrifice permanents.")) { + return false; + } + return true; + } + } // end Card class diff --git a/src/main/java/forge/CardPredicates.java b/src/main/java/forge/CardPredicates.java index 6b693e3838d..15cc1bc707f 100644 --- a/src/main/java/forge/CardPredicates.java +++ b/src/main/java/forge/CardPredicates.java @@ -147,6 +147,16 @@ public final class CardPredicates { }; } + public static final Predicate canBeSacrificedBy(final SpellAbility sa) { + return new Predicate() { + @Override + public boolean apply(final Card c) { + return c.canBeSacrificedBy(sa); + } + }; + }; + + public static class Presets { /** @@ -347,6 +357,12 @@ public final class CardPredicates { return c.isPlaneswalker(); } }; + public static final Predicate CAN_BE_DESTROYED = new Predicate() { + @Override + public boolean apply(final Card c) { + return c.canBeDestroyed(); + } + }; } public static class Accessors { diff --git a/src/main/java/forge/card/ability/ai/AttachAi.java b/src/main/java/forge/card/ability/ai/AttachAi.java index 16de90d79e4..60df27fffdf 100644 --- a/src/main/java/forge/card/ability/ai/AttachAi.java +++ b/src/main/java/forge/card/ability/ai/AttachAi.java @@ -400,7 +400,7 @@ public class AttachAi extends SpellAiLogic { @Override public boolean apply(final Card c) { // Don't enchant creatures that can survive - if (c.hasKeyword("Indestructible") || c.getNetCombatDamage() < c.getNetDefense()) { + if (!c.canBeDestroyed() || c.getNetCombatDamage() < c.getNetDefense()) { return false; } return true; diff --git a/src/main/java/forge/card/ability/effects/SacrificeEffect.java b/src/main/java/forge/card/ability/effects/SacrificeEffect.java index 469f8122a89..6f1bf7dc1f0 100644 --- a/src/main/java/forge/card/ability/effects/SacrificeEffect.java +++ b/src/main/java/forge/card/ability/effects/SacrificeEffect.java @@ -1,6 +1,5 @@ package forge.card.ability.effects; -import java.util.ArrayList; import java.util.List; import forge.Card; @@ -8,10 +7,9 @@ import forge.Singletons; import forge.card.ability.AbilityUtils; import forge.card.ability.SpellEffect; import forge.card.spellability.SpellAbility; -import forge.game.ai.ComputerUtil; +import forge.game.GameState; import forge.game.player.Player; import forge.game.zone.ZoneType; -import forge.gui.GuiChoose; import forge.util.Aggregates; public class SacrificeEffect extends SpellEffect { @@ -19,6 +17,7 @@ public class SacrificeEffect extends SpellEffect { @Override public void resolve(SpellAbility sa) { final Card card = sa.getSourceCard(); + final GameState game = Singletons.getModel().getGame(); // Expand Sacrifice keyword here depending on what we need out of it. final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1"; @@ -41,29 +40,31 @@ public class SacrificeEffect extends SpellEffect { final boolean remSacrificed = sa.hasParam("RememberSacrificed"); if (valid.equals("Self")) { - if (Singletons.getModel().getGame().getZoneOf(card).is(ZoneType.Battlefield)) { - if (Singletons.getModel().getGame().getAction().sacrifice(card, sa) && remSacrificed) { + if (game.getZoneOf(card).is(ZoneType.Battlefield)) { + if (game.getAction().sacrifice(card, sa) && remSacrificed) { card.addRemembered(card); } } } else { - List sacList = null; + List choosenToSacrifice = null; for (final Player p : tgts) { + List battlefield = p.getCardsIn(ZoneType.Battlefield); + List validTargets = AbilityUtils.filterListByType(battlefield, valid, sa); + if (sa.hasParam("Random")) { - sacList = sacrificeRandom(p, amount, valid, sa, destroy); - } else if (p.isComputer()) { - if (sa.hasParam("Optional") && sa.getActivatingPlayer().isOpponentOf(p)) { - continue; - } - sacList = sacrificeAI(p, amount, valid, sa, destroy); + choosenToSacrifice = Aggregates.random(validTargets, Math.min(amount, validTargets.size())); } else { - sacList = sacrificeHuman(p, amount, valid, sa, destroy, - sa.hasParam("Optional")); + boolean isOptional = sa.hasParam("Optional"); + choosenToSacrifice = p.getController().choosePermanentsToSacrifice(validTargets, amount, sa, destroy, isOptional); } - if (remSacrificed) { - for (int i = 0; i < sacList.size(); i++) { - card.addRemembered(sacList.get(i)); + + for(Card sac : choosenToSacrifice) { + boolean wasSacrificed = !destroy && game.getAction().sacrifice(sac, sa); + boolean wasDestroyed = destroy && game.getAction().destroy(sac); + + if ( remSacrificed && (wasDestroyed || wasSacrificed) ) { + card.addRemembered(sac); } } } @@ -111,115 +112,4 @@ public class SacrificeEffect extends SpellEffect { return sb.toString(); } - - /** - *

- * sacrificeAI. - *

- * - * @param p - * a {@link forge.game.player.Player} object. - * @param amount - * a int. - * @param valid - * a {@link java.lang.String} object. - * @param sa - * a {@link forge.card.spellability.SpellAbility} object. - */ - private List sacrificeAI(final Player p, final int amount, final String valid, final SpellAbility sa, - final boolean destroy) { - List battlefield = p.getCardsIn(ZoneType.Battlefield); - List sacList = AbilityUtils.filterListByType(battlefield, valid, sa); - sacList = ComputerUtil.sacrificePermanents(p, amount, sacList, destroy, sa); - - return sacList; - } - - /** - *

- * sacrificeHuman. - *

- * - * @param p - * a {@link forge.game.player.Player} object. - * @param amount - * a int. - * @param valid - * a {@link java.lang.String} object. - * @param sa - * a {@link forge.card.spellability.SpellAbility} object. - * @param message - * a {@link java.lang.String} object. - */ - public static List sacrificeHuman(final Player p, final int amount, final String valid, final SpellAbility sa, - final boolean destroy, final boolean optional) { - List list = AbilityUtils.filterListByType(p.getCardsIn(ZoneType.Battlefield), valid, sa); - List sacList = new ArrayList(); - - for (int i = 0; i < amount; i++) { - if (list.isEmpty()) { - break; - } - Card c; - if (optional) { - c = GuiChoose.oneOrNone("Select a card to sacrifice", list); - } else { - c = GuiChoose.one("Select a card to sacrifice", list); - } - if (c != null) { - if (destroy) { - if (Singletons.getModel().getGame().getAction().destroy(c)) { - sacList.add(c); - } - } else { - if (Singletons.getModel().getGame().getAction().sacrifice(c, sa)) { - sacList.add(c); - } - } - - list.remove(c); - - } else { - return sacList; - } - } - return sacList; - } - - /** - *

- * sacrificeRandom. - *

- * - * @param p - * a {@link forge.game.player.Player} object. - * @param amount - * a int. - * @param valid - * a {@link java.lang.String} object. - * @param sa - * a {@link forge.card.spellability.SpellAbility} object. - */ - private List sacrificeRandom(final Player p, final int amount, final String valid, final SpellAbility sa, - final boolean destroy) { - List sacList = new ArrayList(); - for (int i = 0; i < amount; i++) { - List battlefield = p.getCardsIn(ZoneType.Battlefield); - List list = AbilityUtils.filterListByType(battlefield, valid, sa); - if (list.size() != 0) { - final Card sac = Aggregates.random(list); - if (destroy) { - if (Singletons.getModel().getGame().getAction().destroy(sac)) { - sacList.add(sac); - } - } else { - if (Singletons.getModel().getGame().getAction().sacrifice(sac, sa)) { - sacList.add(sac); - } - } - } - } - return sacList; - } - } diff --git a/src/main/java/forge/game/GameAction.java b/src/main/java/forge/game/GameAction.java index 50316298acf..57ce89971ff 100644 --- a/src/main/java/forge/game/GameAction.java +++ b/src/main/java/forge/game/GameAction.java @@ -1166,27 +1166,11 @@ public class GameAction { } } // destroyLegendaryCreatures() - /** - *

- * sacrifice. - *

- * - * @param c - * a {@link forge.Card} object. - * @param source - * a SpellAbility object. - * @return a boolean. - */ + public final boolean sacrifice(final Card c, final SpellAbility source) { - if (c.isImmutable()) { - System.out.println("Trying to sacrifice immutables: " + c); + if(!c.canBeSacrificedBy(source)) return false; - } - if (source != null && !c.getController().equals(source.getActivatingPlayer()) - && c.getController().hasKeyword("Spells and abilities your opponents control can't cause" - + " you to sacrifice permanents.")) { - return false; - } + this.sacrificeDestroy(c); // Play the Sacrifice sound @@ -1196,7 +1180,6 @@ public class GameAction { final HashMap runParams = new HashMap(); runParams.put("Card", c); game.getTriggerHandler().runTrigger(TriggerType.Sacrificed, runParams, false); - return true; } @@ -1210,8 +1193,7 @@ public class GameAction { * @return a boolean. */ public final boolean destroy(final Card c) { - if (!c.isInPlay() - || (c.hasKeyword("Indestructible") && (!c.isCreature() || c.getNetDefense() > 0))) { + if (!c.canBeDestroyed()) { return false; } @@ -1242,10 +1224,8 @@ public class GameAction { * @return a boolean. */ public final boolean destroyNoRegeneration(final Card c) { - if (!c.isInPlay() - || (c.hasKeyword("Indestructible") && (!c.isCreature() || c.getNetDefense() > 0))) { + if ( !c.canBeDestroyed() ) return false; - } if (c.isEnchanted()) { List list = new ArrayList(c.getEnchantedBy()); diff --git a/src/main/java/forge/game/ai/ComputerUtil.java b/src/main/java/forge/game/ai/ComputerUtil.java index 39a7a76351f..218553ec369 100644 --- a/src/main/java/forge/game/ai/ComputerUtil.java +++ b/src/main/java/forge/game/ai/ComputerUtil.java @@ -625,84 +625,78 @@ public class ComputerUtil { *

* sacrificePermanents. *

- * * @param amount * a int. - * @param list - * a {@link forge.CardList} object. - * @param destroy - * the destroy * @param source * the source SpellAbility + * @param destroy + * the destroy + * @param list + * a {@link forge.CardList} object. + * * @return the card list */ - public static List sacrificePermanents(final Player ai, final int amount, final List cardlist, final boolean destroy, - SpellAbility source) { - final List list = new ArrayList(cardlist); - final List sacList = new ArrayList(); - // used in Annihilator and AF_Sacrifice - int max = list.size(); - if (max > amount) { - max = amount; - } + public static List choosePermanentsToSacrifice(final Player ai, final List cardlist, final int amount, SpellAbility source, + final boolean destroy, final boolean isOptional) { + final List remaining = new ArrayList(cardlist); + final List sacrificed = new ArrayList(); - CardLists.sortCMC(list); - Collections.reverse(list); + if (isOptional && source.getActivatingPlayer().isOpponentOf(ai)) { + return sacrificed; // sacrifice none + } + + CardLists.sortCMC(remaining); + Collections.reverse(remaining); + + final int max = Math.min(remaining.size(), amount); for (int i = 0; i < max; i++) { - Card c = null; - - if (destroy) { - final List indestructibles = CardLists.getKeyword(list, "Indestructible"); - if (!indestructibles.isEmpty()) { - c = indestructibles.get(0); - } - } - for (int ip = 0; ip < 6; ip++) { // priority 0 is the lowest, priority 5 the highest - final int priority = 6 - ip; - for (Card card : list) { - if (!card.getSVar("SacMe").equals("") && Integer.parseInt(card.getSVar("SacMe")) == priority) { - c = card; - break; - } - } - } - - if (c == null) { - if (CardLists.getNotType(list, "Creature").size() == 0) { - c = CardFactoryUtil.getWorstCreatureAI(list); - } else if (CardLists.getNotType(list, "Land").size() == 0) { - c = CardFactoryUtil.getWorstLand(ai); - } else { - c = CardFactoryUtil.getWorstPermanentAI(list, false, false, false, false); - } - - final ArrayList auras = c.getEnchantedBy(); - - if (auras.size() > 0) { - // TODO: choose "worst" controlled enchanting Aura - for (int j = 0; j < auras.size(); j++) { - final Card aura = auras.get(j); - if (aura.getController().equals(c.getController()) && list.contains(aura)) { - c = aura; - break; - } - } - } - } - if (destroy) { - if (!Singletons.getModel().getGame().getAction().destroy(c)) { - continue; - } - } else { - if (!Singletons.getModel().getGame().getAction().sacrifice(c, source)) { - continue; - } - } - list.remove(c); - sacList.add(c); + Card c = chooseCardToSacrifice(remaining, ai, destroy); + remaining.remove(c); + sacrificed.add(c); } - return sacList; + return sacrificed; + } + + // Precondition it wants: remaining are reverse-sorted by CMC + private static Card chooseCardToSacrifice(final List remaining, final Player ai, final boolean destroy) { + if (destroy) { + final List indestructibles = CardLists.getKeyword(remaining, "Indestructible"); + if (!indestructibles.isEmpty()) { + return indestructibles.get(0); + } + } + for (int ip = 0; ip < 6; ip++) { // priority 0 is the lowest, priority 5 the highest + final int priority = 6 - ip; + for (Card card : remaining) { + if (!card.getSVar("SacMe").equals("") && Integer.parseInt(card.getSVar("SacMe")) == priority) { + return card; + } + } + } + + Card c; + + if (CardLists.getNotType(remaining, "Creature").size() == 0) { + c = CardFactoryUtil.getWorstCreatureAI(remaining); + } else if (CardLists.getNotType(remaining, "Land").size() == 0) { + c = CardFactoryUtil.getWorstLand(CardLists.filter(remaining, CardPredicates.Presets.LANDS)); + } else { + c = CardFactoryUtil.getWorstPermanentAI(remaining, false, false, false, false); + } + + final ArrayList auras = c.getEnchantedBy(); + + if (auras.size() > 0) { + // TODO: choose "worst" controlled enchanting Aura + for (int j = 0; j < auras.size(); j++) { + final Card aura = auras.get(j); + if (aura.getController().equals(c.getController()) && remaining.contains(aura)) { + return aura; + } + } + } + return c; } /** diff --git a/src/main/java/forge/game/phase/CombatUtil.java b/src/main/java/forge/game/phase/CombatUtil.java index dd196fdb880..9b2843c78f1 100644 --- a/src/main/java/forge/game/phase/CombatUtil.java +++ b/src/main/java/forge/game/phase/CombatUtil.java @@ -37,7 +37,6 @@ import forge.Constant; import forge.GameEntity; import forge.Singletons; import forge.card.SpellManaCost; -import forge.card.ability.effects.SacrificeEffect; import forge.card.cardfactory.CardFactoryUtil; import forge.card.cost.Cost; import forge.card.cost.CostUtil; @@ -1247,37 +1246,34 @@ public class CombatUtil { // Annihilator: if (!c.getDamageHistory().getCreatureAttackedThisCombat()) { final ArrayList kws = c.getKeyword(); - final Pattern p = Pattern.compile("Annihilator [0-9]+"); - Matcher m; for (final String key : kws) { - m = p.matcher(key); - if (m.find()) { - final String[] k = key.split(" "); - final int a = Integer.valueOf(k[1]); - final Card crd = c; + if( !key.startsWith("Annihilator ") ) continue; + final String[] k = key.split(" ", 2); + final int a = Integer.valueOf(k[1]); - final Ability ability = new Ability(c, SpellManaCost.ZERO) { - @Override - public void resolve() { - final Player cp = crd.getController(); - final Player opponent = Singletons.getModel().getGame().getCombat().getDefendingPlayerRelatedTo(c); - if (cp.isHuman()) { - final List list = opponent.getCardsIn(ZoneType.Battlefield); - ComputerUtil.sacrificePermanents(opponent, a, list, false, this); - } else { - SacrificeEffect.sacrificeHuman(opponent, a, "Permanent", this, false, false); - } + final Ability ability = new Ability(c, SpellManaCost.ZERO) { + @Override + public void resolve() { + final Player opponent = Singletons.getModel().getGame().getCombat().getDefendingPlayerRelatedTo(c); + //List list = AbilityUtils.filterListByType(opponent.getCardsIn(ZoneType.Battlefield), "Permanent", this); + final List list = opponent.getCardsIn(ZoneType.Battlefield); + List toSac = opponent.getController().choosePermanentsToSacrifice(list, a, this, false, false); + + for(Card sacd : toSac) { + final GameState game = Singletons.getModel().getGame(); + game.getAction().sacrifice(sacd, this); } - }; - final StringBuilder sb = new StringBuilder(); - sb.append("Annihilator - Defending player sacrifices ").append(a).append(" permanents."); - ability.setStackDescription(sb.toString()); - ability.setDescription(sb.toString()); - ability.setActivatingPlayer(c.getController()); - ability.setTrigger(true); + + } + }; + String sb = String.format("Annihilator - Defending player sacrifices %d permanents.", a); + ability.setStackDescription(sb); + ability.setDescription(sb); + ability.setActivatingPlayer(c.getController()); + ability.setTrigger(true); + + Singletons.getModel().getGame().getStack().add(ability); - Singletons.getModel().getGame().getStack().add(ability); - } // find } // for } // creatureAttacked // Annihilator diff --git a/src/main/java/forge/game/player/PlayerController.java b/src/main/java/forge/game/player/PlayerController.java index a9325a845bb..1e0bc74ba9b 100644 --- a/src/main/java/forge/game/player/PlayerController.java +++ b/src/main/java/forge/game/player/PlayerController.java @@ -93,4 +93,6 @@ public abstract class PlayerController { public abstract Map assignCombatDamage(Card attacker, List blockers, int damageDealt, GameEntity defender); public abstract String announceRequirements(SpellAbility ability, String announce); + + public abstract List choosePermanentsToSacrifice(List validTargets, int amount, SpellAbility sa, boolean destroy, boolean isOptional); } diff --git a/src/main/java/forge/game/player/PlayerControllerAi.java b/src/main/java/forge/game/player/PlayerControllerAi.java index 27d088e90c9..1fc0a48cfb8 100644 --- a/src/main/java/forge/game/player/PlayerControllerAi.java +++ b/src/main/java/forge/game/player/PlayerControllerAi.java @@ -180,5 +180,13 @@ public class PlayerControllerAi extends PlayerController { return null; } + /* (non-Javadoc) + * @see forge.game.player.PlayerController#choosePermanentsToSacrifice(java.util.List, int, forge.card.spellability.SpellAbility, boolean, boolean) + */ + @Override + public List choosePermanentsToSacrifice(List validTargets, int amount, SpellAbility sa, boolean destroy, boolean isOptional) { + return ComputerUtil.choosePermanentsToSacrifice(player, validTargets, amount, sa, destroy, isOptional); + } + } diff --git a/src/main/java/forge/game/player/PlayerControllerHuman.java b/src/main/java/forge/game/player/PlayerControllerHuman.java index 91052097950..b8534c6aa7b 100644 --- a/src/main/java/forge/game/player/PlayerControllerHuman.java +++ b/src/main/java/forge/game/player/PlayerControllerHuman.java @@ -1,5 +1,6 @@ package forge.game.player; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -210,5 +211,32 @@ public class PlayerControllerHuman extends PlayerController { return JOptionPane.showInputDialog(sb.toString()); } + /* (non-Javadoc) + * @see forge.game.player.PlayerController#choosePermanentsToSacrifice(java.util.List, int, forge.card.spellability.SpellAbility, boolean, boolean) + */ + @Override + public List choosePermanentsToSacrifice(List validTargets, int amount, SpellAbility sa, boolean destroy, boolean isOptional) { + List result = new ArrayList(); + + for (int i = 0; i < amount; i++) { + if (validTargets.isEmpty()) { + break; + } + Card c; + if (isOptional) { + c = GuiChoose.oneOrNone("Select a card to sacrifice", validTargets); + } else { + c = GuiChoose.one("Select a card to sacrifice", validTargets); + } + if (c != null) { + result.add(c); + validTargets.remove(c); + } else { + return result; + } + } + return result; + } + }