From 67a5848b265d165c5a11bf5e40b7b5c91e74eb62 Mon Sep 17 00:00:00 2001 From: excessum Date: Mon, 7 Apr 2014 13:46:10 +0000 Subject: [PATCH] - Update for ProtectAI. AI can now use Protect effects to 1) counter certain targeted spells 2) protect against lethal damage 3) create an unblockable attacker depending on expected damage --- .../java/forge/ai/AiAttackController.java | 78 +++++++++-- .../src/main/java/forge/ai/ComputerUtil.java | 29 ++++- .../java/forge/ai/PlayerControllerAi.java | 38 ++++++ .../main/java/forge/ai/ability/ProtectAi.java | 123 ++++++++++++++---- .../res/cardsfolder/a/apostles_blessing.txt | 2 - forge-gui/res/cardsfolder/g/gods_willing.txt | 1 - 6 files changed, 233 insertions(+), 38 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 0e678e39294..c703d958c9e 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -23,6 +23,7 @@ import com.google.common.collect.Lists; import forge.ai.ability.AnimateAi; import forge.game.GameEntity; import forge.game.ability.ApiType; +import forge.game.ability.effects.ProtectEffect; import forge.game.card.Card; import forge.game.card.CardFactory; import forge.game.card.CardLists; @@ -82,7 +83,7 @@ public class AiAttackController { public AiAttackController(final Player ai) { this.ai = ai; this.defendingOpponent = choosePreferredDefenderPlayer(); - getOpponentCreatures(); + this.oppList = getOpponentCreatures(this.defendingOpponent); this.myList = ai.getCreaturesInPlay(); this.attackers = new ArrayList(); for (Card c : myList) { @@ -96,7 +97,7 @@ public class AiAttackController { public AiAttackController(final Player ai, Card attacker) { this.ai = ai; this.defendingOpponent = choosePreferredDefenderPlayer(); - getOpponentCreatures(); + this.oppList = getOpponentCreatures(this.defendingOpponent); this.myList = ai.getCreaturesInPlay(); this.attackers = new ArrayList(); if (CombatUtil.canAttack(attacker, this.defendingOpponent)) { @@ -105,30 +106,31 @@ public class AiAttackController { this.blockers = this.getPossibleBlockers(oppList, this.attackers); } // overloaded constructor to evaluate single specified attacker - private void getOpponentCreatures() { - this.oppList = Lists.newArrayList(); - this.oppList.addAll(this.defendingOpponent.getCreaturesInPlay()); + public static List getOpponentCreatures(final Player defender) { + List defenders = Lists.newArrayList(); + defenders.addAll(defender.getCreaturesInPlay()); Predicate canAnimate = new Predicate() { @Override public boolean apply(Card c) { return !c.isCreature() && !c.isPlaneswalker(); } }; - for (Card c : CardLists.filter(this.defendingOpponent.getCardsIn(ZoneType.Battlefield), canAnimate)) { + for (Card c : CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), canAnimate)) { if (c.isToken() && !c.isCopiedToken()) { continue; } for (SpellAbility sa : c.getSpellAbilities()) { if (sa.getApi() == ApiType.Animate) { - if (ComputerUtilCost.canPayCost(sa, this.defendingOpponent) - && sa.getRestrictions().checkOtherRestrictions(c, sa, this.defendingOpponent)) { - Card animatedCopy = CardFactory.getCard(c.getPaperCard(), this.defendingOpponent); + if (ComputerUtilCost.canPayCost(sa, defender) + && sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) { + Card animatedCopy = CardFactory.getCard(c.getPaperCard(), defender); AnimateAi.becomeAnimated(animatedCopy, sa); - this.oppList.add(animatedCopy); + defenders.add(animatedCopy); } } } } + return defenders; } /** Choose opponent for AI to attack here. Expand as necessary. */ @@ -1074,6 +1076,62 @@ public class AiAttackController { return shouldAttack(ai, attacker, oppList, combat); } + public String toProtectAttacker(SpellAbility sa) { + if (sa.getApi() != ApiType.Protection || oppList.isEmpty()) { + return null; + } + final List choices = ProtectEffect.getProtectionList(sa); + String color = ComputerUtilCard.getMostProminentColor(oppList), artifact = null; + if (choices.contains("artifacts")) { + artifact = "artifacts"; + } + if (!choices.contains(color)) { + color = null; + } + for (Card c : oppList) { + if (!c.isArtifact()) { + artifact = null; + } + switch (color) { + case "black": + if (!c.isBlack()) { + color = null; + } + break; + case "blue": + if (!c.isBlue()) { + color = null; + } + break; + case "green": + if (!c.isGreen()) { + color = null; + } + break; + case "red": + if (!c.isRed()) { + color = null; + } + break; + case "white": + if (!c.isWhite()) { + color = null; + } + break; + } + if (color == null && artifact == null) { + return null; + } + } + if (color != null) { + return color; + } + if (artifact != null) { + return artifact; + } + return null; + } + public static boolean shouldThisAttack(final Player ai, Card attacker) { AiAttackController aiAtk = new AiAttackController(ai, attacker); Combat combat = ai.getGame().getCombat(); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index ddcc2b5ba5b..249dad87e54 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -20,6 +20,7 @@ package forge.ai; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import forge.ai.ability.ProtectAi; import forge.card.CardType; import forge.card.CardType.Constant; import forge.card.MagicColor; @@ -1358,6 +1359,11 @@ public class ComputerUtil { || saviour.getParam("KW").endsWith("Hexproof"))) { continue; } + + // cannot protect against source + if (saviourApi == ApiType.Protection && (ProtectAi.toProtectFrom(source, saviour) == null)) { + continue; + } // don't bounce or blink a permanent that the human // player owns or is a token @@ -1385,7 +1391,7 @@ public class ComputerUtil { else if ((threatApi == ApiType.Destroy || threatApi == ApiType.DestroyAll) && (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll) && !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump - || saviourApi == null)) { + || saviourApi == ApiType.Protection || saviourApi == null)) { for (final Object o : objects) { if (o instanceof Card) { final Card c = (Card) o; @@ -1405,6 +1411,11 @@ public class ComputerUtil { || saviour.getParam("KW").endsWith("Hexproof"))) { continue; } + if (saviourApi == ApiType.Protection) { + if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) { + continue; + } + } // don't bounce or blink a permanent that the human // player owns or is a token @@ -1422,7 +1433,8 @@ public class ComputerUtil { } // Exiling => bounce/shroud else if ((threatApi == ApiType.ChangeZone || threatApi == ApiType.ChangeZoneAll) - && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == null) + && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump + || saviourApi == ApiType.Protection || saviourApi == null) && topStack.hasParam("Destination") && topStack.getParam("Destination").equals("Exile")) { for (final Object o : objects) { @@ -1433,6 +1445,11 @@ public class ComputerUtil { && (saviour.getParam("KW").endsWith("Shroud") || saviour.getParam("KW").endsWith("Hexproof"))) { continue; } + if (saviourApi == ApiType.Protection) { + if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) { + continue; + } + } // don't bounce or blink a permanent that the human // player owns or is a token @@ -1446,7 +1463,8 @@ public class ComputerUtil { } //GainControl else if (threatApi == ApiType.GainControl - && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == null)) { + && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.Protection + || saviourApi == null)) { for (final Object o : objects) { if (o instanceof Card) { final Card c = (Card) o; @@ -1455,6 +1473,11 @@ public class ComputerUtil { && (saviour.getParam("KW").endsWith("Shroud") || saviour.getParam("KW").endsWith("Hexproof"))) { continue; } + if (saviourApi == ApiType.Protection) { + if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) { + continue; + } + } threatened.add(c); } } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 93df60fc65d..4e91ec7ad99 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -9,6 +9,7 @@ import com.google.common.collect.Multimap; import forge.ai.ability.ChangeZoneAi; import forge.ai.ability.CharmAi; +import forge.ai.ability.ProtectAi; import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaCost; @@ -28,6 +29,8 @@ import forge.game.cost.CostPart; import forge.game.cost.CostPartMana; import forge.game.mana.Mana; import forge.game.mana.ManaCostBeingPaid; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; import forge.game.player.LobbyPlayer; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; @@ -600,6 +603,41 @@ public class PlayerControllerAi extends PlayerController { @Override public String chooseProtectionType(String string, SpellAbility sa, List choices) { String choice = choices.get(0); + if (game.stack.size() > 1) { + for (SpellAbilityStackInstance si : game.getStack()) { + SpellAbility spell = si.getSpellAbility(); + if (sa != spell) { + String s = ProtectAi.toProtectFrom(spell.getHostCard(), sa); + if (s != null) { + return s; + } + break; + } + } + } + final Combat combat = game.getCombat(); + if (combat != null ) { + Card toSave = sa.getTargetCard(); + List threats = null; + if (combat.isBlocked(toSave)) { + threats = combat.getBlockers(toSave); + } + if (combat.isBlocking(toSave)) { + threats = combat.getAttackersBlockedBy(toSave); + } + if (threats != null) { + ComputerUtilCard.sortByEvaluateCreature(threats); + String s = ProtectAi.toProtectFrom(threats.get(0), sa); + if (s != null) { + return s; + } + } + } + final PhaseHandler ph = game.getPhaseHandler(); + if (ph.getPlayerTurn() == sa.getActivatingPlayer() && ph.getPhase() == PhaseType.MAIN1) { + AiAttackController aiAtk = new AiAttackController(sa.getActivatingPlayer(), sa.getTargetCard()); + return aiAtk.toProtectAttacker(sa); + } final String logic = sa.getParam("AILogic"); if (logic == null || logic.equals("MostProminentHumanCreatures")) { List list = new ArrayList(); diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java index 85e089beb6a..7f48e96abd8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java @@ -1,23 +1,28 @@ package forge.ai.ability; import com.google.common.base.Predicate; + import forge.ai.*; import forge.card.MagicColor; import forge.game.Game; import forge.game.ability.AbilityUtils; +import forge.game.ability.ApiType; import forge.game.ability.effects.ProtectEffect; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.combat.Combat; import forge.game.cost.Cost; +import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; +import forge.util.MyRandom; import java.util.ArrayList; import java.util.List; +import java.util.Random; public class ProtectAi extends SpellAbilityAi { private static boolean hasProtectionFrom(final Card card, final String color) { @@ -50,6 +55,38 @@ public class ProtectAi extends SpellAbilityAi { } return protect && !isEmpty; } + + /** + * \brief Find a choice for a Protect SpellAbility that protects from a specific threat card. + * @param threat Card to protect against + * @param sa Protect SpellAbility + * @return choice that can protect against the given threat, null if no such choice exists + */ + public static String toProtectFrom(final Card threat, SpellAbility sa) { + if (sa.getApi() != ApiType.Protection) { + return null; + } + final List choices = ProtectEffect.getProtectionList(sa); + if (threat.isArtifact() && choices.contains("artifacts")) { + return "artifacts"; + } + if (threat.isBlack() && choices.contains("black")) { + return "black"; + } + if (threat.isBlue() && choices.contains("blue")) { + return "blue"; + } + if (threat.isGreen() && choices.contains("green")) { + return "green"; + } + if (threat.isRed() && choices.contains("red")) { + return "red"; + } + if (threat.isWhite() && choices.contains("white")) { + return "white"; + } + return null; + } /** *

@@ -64,6 +101,7 @@ public class ProtectAi extends SpellAbilityAi { final List gains = ProtectEffect.getProtectionList(sa); final Game game = ai.getGame(); final Combat combat = game.getCombat(); + final PhaseHandler ph = game.getPhaseHandler(); List list = ai.getCreaturesInPlay(); list = CardLists.filter(list, new Predicate() { @@ -85,22 +123,51 @@ public class ProtectAi extends SpellAbilityAi { return true; } - if( combat != null ) { - // is the creature blocking and unable to destroy the attacker - // or would be destroyed itself? - if (combat.isBlocking(c) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, c, combat)) { - return true; - } - - // is the creature in blocked and the blocker would survive - // TODO Potential NPE here if no blockers are actually left - if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS) - && combat.isAttacking(c) && combat.isBlocked(c) - && ComputerUtilCombat.blockerWouldBeDestroyed(ai, combat.getBlockers(c).get(0), combat)) { - return true; + if (!game.stack.isEmpty()) { + //counter bad effect on stack + if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(c)) { + Card threat = game.getStack().peekAbility().getHostCard(); + //check to see if threat has already been countered by resolved protect + if (!c.hasProtectionFrom(threat) && (ProtectAi.toProtectFrom(threat, sa) != null)) { + return true; + } + } + } + + if (combat != null) { + //creature is blocking and would be destroyed itself + if (combat.isBlocking(c) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, c, combat)) { + List threats = combat.getAttackersBlockedBy(c); + return (true && (ProtectAi.toProtectFrom(threats.get(0), sa) != null)); + } + + //creature is attacking and would be destroyed itself + if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS) + && combat.isAttacking(c) && combat.isBlocked(c) ) { + List blockers = combat.getBlockers(c); + if (!blockers.isEmpty() && ComputerUtilCombat.blockerWouldBeDestroyed(ai, blockers.get(0), combat)) { + List threats = combat.getBlockers(c); + ComputerUtilCard.sortByEvaluateCreature(threats); + return (true && (ProtectAi.toProtectFrom(threats.get(0), sa) != null)); + } + + } + } + + //make unblockable + if (ph.getPlayerTurn() == ai && ph.getPhase() == PhaseType.MAIN1) { + AiAttackController aiAtk = new AiAttackController(ai, c); + String s = aiAtk.toProtectAttacker(sa); + if (s==null) { + return false; + } else { + Combat combat = ai.getGame().getCombat(); + int dmg = ComputerUtilCombat.damageIfUnblocked(c, ai.getOpponent(), combat); + float ratio = 1.0f * dmg / ai.getOpponent().getLife(); + Random r = MyRandom.getRandom(); + return r.nextFloat() < ratio; } } - return false; } }); @@ -111,6 +178,7 @@ public class ProtectAi extends SpellAbilityAi { protected boolean canPlayAI(Player ai, SpellAbility sa) { final Card hostCard = sa.getHostCard(); final Game game = ai.getGame(); + final PhaseHandler ph = game.getPhaseHandler(); // if there is no target and host card isn't in play, don't activate if ((sa.getTargetRestrictions() == null) && !hostCard.isInPlay()) { return false; @@ -136,16 +204,27 @@ public class ProtectAi extends SpellAbilityAi { } // Phase Restrictions - if (game.getStack().isEmpty() && game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE)) { - // Instant-speed protections should not be cast outside of combat - // when the stack is empty - if (!SpellAbilityAi.isSorcerySpeed(sa)) { + boolean notAiMain1 = !(ph.getPlayerTurn() == ai && ph.getPhase() == PhaseType.MAIN1); + if (SpellAbilityAi.isSorcerySpeed(sa)) { + //only non-instants are Floating Shield, Midvast Protector, Sejiri Steppe + //sorceries can only give protection in order to create an unblockable attacker + if (notAiMain1) { return false; } - } else if (!game.getStack().isEmpty()) { - // TODO protection something only if the top thing on the stack will - // kill it via damage or destroy - return false; + } else { + if (game.getStack().isEmpty()) { + //try to save attacker or blocker + if (ph.getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) { + if (notAiMain1) { + return false; + } + } + } else { + //prevent repeated protects + if (game.getStack().peekAbility().getApi() == ApiType.Protection) { + return false; + } + } } if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { diff --git a/forge-gui/res/cardsfolder/a/apostles_blessing.txt b/forge-gui/res/cardsfolder/a/apostles_blessing.txt index 29296965b24..903848b1b60 100644 --- a/forge-gui/res/cardsfolder/a/apostles_blessing.txt +++ b/forge-gui/res/cardsfolder/a/apostles_blessing.txt @@ -2,7 +2,5 @@ Name:Apostle's Blessing ManaCost:1 PW Types:Instant A:SP$ Protection | Cost$ 1 PW | ValidTgts$ Creature.YouCtrl,Artifact.YouCtrl | TgtPrompt$ Select target artifact or creature you control | Gains$ Choice | Choices$ AnyColor,artifacts | SpellDescription$ Target artifact or creature you control gains protection from artifacts or from the color of your choice until end of turn. -#Computer isn't very good at picking a color to get protection from yet -SVar:RemAIDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/apostles_blessing.jpg Oracle:({W/P} can be paid with either {W} or 2 life.)\nTarget artifact or creature you control gains protection from artifacts or from the color of your choice until end of turn. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/g/gods_willing.txt b/forge-gui/res/cardsfolder/g/gods_willing.txt index 099587e06d5..8ff652999ea 100644 --- a/forge-gui/res/cardsfolder/g/gods_willing.txt +++ b/forge-gui/res/cardsfolder/g/gods_willing.txt @@ -3,6 +3,5 @@ ManaCost:W Types:Instant A:SP$ Protection | Cost$ W | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | Gains$ Choice | Choices$ AnyColor | SubAbility$ DBScry | SpellDescription$ Target creature you control gains protection from the color of your choice until end of turn. Scry 1. (Look at the top card of your library. You may put that card on the bottom of your library.) SVar:DBScry:DB$ Scry | ScryNum$ 1 -SVar:RemAIDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/gods_willing.jpg Oracle:Target creature you control gains protection from the color of your choice until end of turn. Scry 1. (Look at the top card of your library. You may put that card on the bottom of your library.) \ No newline at end of file