mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-16 10:48:00 +00:00
- 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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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.)
|
||||
Reference in New Issue
Block a user