- 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

This commit is contained in:
excessum
2014-04-07 13:46:10 +00:00
parent 544afe181e
commit 67a5848b26
6 changed files with 233 additions and 38 deletions

View File

@@ -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<Card>();
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<Card>();
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<Card> getOpponentCreatures(final Player defender) {
List<Card> defenders = Lists.newArrayList();
defenders.addAll(defender.getCreaturesInPlay());
Predicate<Card> canAnimate = new Predicate<Card>() {
@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<String> 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();

View File

@@ -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;
@@ -1359,6 +1360,11 @@ public class ComputerUtil {
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
if (saviourApi == ApiType.ChangeZone && (c.getOwner().isOpponentOf(aiPlayer) || c.isToken())) {
@@ -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);
}
}

View File

@@ -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<String> 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<Card> 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<Card> list = new ArrayList<Card>();

View File

@@ -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) {
@@ -51,6 +56,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<String> 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;
}
/**
* <p>
* getProtectCreatures.
@@ -64,6 +101,7 @@ public class ProtectAi extends SpellAbilityAi {
final List<String> gains = ProtectEffect.getProtectionList(sa);
final Game game = ai.getGame();
final Combat combat = game.getCombat();
final PhaseHandler ph = game.getPhaseHandler();
List<Card> list = ai.getCreaturesInPlay();
list = CardLists.filter(list, new Predicate<Card>() {
@@ -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 (!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)) {
return true;
List<Card> threats = combat.getAttackersBlockedBy(c);
return (true && (ProtectAi.toProtectFrom(threats.get(0), sa) != null));
}
// is the creature in blocked and the blocker would survive
// TODO Potential NPE here if no blockers are actually left
//creature is attacking and would be destroyed itself
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;
&& combat.isAttacking(c) && combat.isBlocked(c) ) {
List<Card> blockers = combat.getBlockers(c);
if (!blockers.isEmpty() && ComputerUtilCombat.blockerWouldBeDestroyed(ai, blockers.get(0), combat)) {
List<Card> 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,17 +204,28 @@ 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
} 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()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);

View File

@@ -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.

View File

@@ -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.)