- Implemented basic "spell magnet" AI (currently used by Spellskite).

- Made the AI able to pay mana costs consisting of only phyrexian mana symbols in absence of relevant colored mana sources (e.g. Mutagenic Growth, Spellskite activated ability).
- Fixed the AI killing itself when paying phyrexian mana while being at 2 life (we might want to consider actually implementing a certain "safe limit" above 2 life, maybe 4 or 5 life, at least outside the range of a typical burn spell).
This commit is contained in:
Agetian
2017-01-21 14:14:16 +00:00
parent c587ec8cb6
commit 68641a0cfe
7 changed files with 115 additions and 20 deletions

View File

@@ -2076,6 +2076,32 @@ public class ComputerUtil {
return ComputerUtilCard.getBestCreatureAI(killables);
}
public static int predictDamageFromSpell(final SpellAbility sa, final Player targetPlayer) {
int damage = -1; // returns -1 if the spell does not deal damage
final Card card = sa.getHostCard();
SpellAbility ab = sa;
while (ab != null) {
if (ab.getApi() == ApiType.DealDamage) {
if (damage == -1) { damage = 0; } // found a damage-dealing spell
if (!ab.hasParam("NumDmg")) {
continue;
}
damage += ComputerUtilCombat.predictDamageTo(targetPlayer,
AbilityUtils.calculateAmount(card, ab.getParam("NumDmg"), ab), card, false);
} else if (ab.getApi() == ApiType.LoseLife) {
if (damage == -1) { damage = 0; } // found a damage-dealing spell
if (!ab.hasParam("LifeAmount")) {
continue;
}
damage += AbilityUtils.calculateAmount(card, ab.getParam("LifeAmount"), ab);
}
ab = ab.getSubAbility();
}
return damage;
}
public static int getDamageForPlaying(final Player player, final SpellAbility sa) {
// check for bad spell cast triggers

View File

@@ -15,7 +15,9 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import java.util.Iterator;
public class ComputerUtilAbility {
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
@@ -106,4 +108,22 @@ public class ComputerUtilAbility {
}
return result;
}
public static SpellAbility getTopSpellAbilityOnStack(Game game, SpellAbility sa) {
Iterator<SpellAbilityStackInstance> it = game.getStack().iterator();
if (!it.hasNext()) {
return null;
}
SpellAbility tgtSA = it.next().getSpellAbility(true);
// Grab the topmost spellability that isn't this SA and use that for comparisons
if (sa.equals(tgtSA) && game.getStack().size() > 1) {
if (!it.hasNext()) {
return null;
}
tgtSA = it.next().getSpellAbility(true);
}
return tgtSA;
}
}

View File

@@ -314,6 +314,7 @@ public class ComputerUtilMana {
private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean test, boolean checkPlayable) {
adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai);
List<Mana> manaSpentToPay = test ? new ArrayList<Mana>() : sa.getPayingMana();
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
List<SpellAbility> paymentList = Lists.newArrayList();
@@ -324,7 +325,7 @@ public class ComputerUtilMana {
boolean hasConverge = sa.getHostCard().hasConverge();
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test,
checkPlayable, manaSpentToPay, hasConverge);
if (sourcesForShards == null) {
if (sourcesForShards == null && !purePhyrexian) {
return false; // no mana abilities to use for paying
}
@@ -353,7 +354,11 @@ public class ComputerUtilMana {
hasConverge = false;
}
} else {
if (!(sourcesForShards == null && purePhyrexian)) {
saList = sourcesForShards.get(toPay);
} else {
saList = Lists.newArrayList(); // Phyrexian mana only: no valid mana sources, but can still pay life
}
}
if (saList == null) {
break;
@@ -361,7 +366,7 @@ public class ComputerUtilMana {
saList.removeAll(saExcludeList);
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, saList, checkPlayable || !test);
SpellAbility saPayment = saList.isEmpty() ? null : chooseManaAbility(cost, sa, ai, toPay, saList, checkPlayable || !test);
if (saPayment != null && saPayment.hasParam("AILogic")) {
boolean consider = false;
@@ -375,7 +380,7 @@ public class ComputerUtilMana {
}
if (saPayment == null) {
if (!toPay.isPhyrexian() || !ai.canPayLife(2)) {
if (!toPay.isPhyrexian() || !ai.canPayLife(2) || (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
break; // cannot pay
}

View File

@@ -1,6 +1,13 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
import forge.game.Game;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -16,8 +23,48 @@ public class ChangeTargetsAi extends SpellAbilityAi {
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final Game game = sa.getHostCard().getGame();
final SpellAbility topSa = game.getStack().isEmpty() ? null : ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
if (sa.hasParam("AILogic")) {
if ("SpellMagnet".equals(sa.getParam("AILogic"))) {
// Cards like Spellskite that retarget spells to itself
if (topSa == null) {
// nothing on stack, so nothing to target
return false;
}
if (!topSa.usesTargeting() || topSa.getTargets().getTargetCards().contains(sa.getHostCard())) {
// if this does not target at all or already targets host, no need to redirect it again
return false;
}
if (topSa.getHostCard() != null && !topSa.getHostCard().getController().isOpponentOf(aiPlayer)) {
// make sure not to redirect our own abilities
return false;
}
if (!topSa.canTarget(sa.getHostCard())) {
// don't try targeting it if we can't legally target Spellskite with it in the first place
return false;
}
if ("Spellskite".equals(sa.getHostCard().getName())) {
int potentialDmg = ComputerUtil.predictDamageFromSpell(topSa, aiPlayer);
boolean canPayBlue = ComputerUtilMana.canPayManaCost(new ManaCostBeingPaid(new ManaCost(new ManaCostParser("U"))), sa, aiPlayer);
if (potentialDmg != -1 && potentialDmg <= 2 && !canPayBlue && topSa.getTargets().getTargets().contains(aiPlayer)) {
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
return false;
}
}
sa.resetTargets();
sa.getTargets().add(topSa);
return true;
}
}
// The AI can't otherwise play this ability, but should at least not miss mandatory activations (e.g. triggers).
return sa.isMandatory();
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import forge.ai.AiProps;
import forge.ai.ComputerUtilAbility;
import java.util.Iterator;
import forge.ai.ComputerUtilCost;
@@ -54,7 +55,7 @@ public class CounterAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
final SpellAbility topSA = findTopSpellAbility(game, sa);
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || topSA.getActivatingPlayer() == ai
|| ai.getAllies().contains(topSA.getActivatingPlayer())) {
// might as well check for player's friendliness
@@ -257,17 +258,4 @@ public class CounterAi extends SpellAbilityAi {
return new ImmutablePair<>(bestOption != null ? bestOption : leastBadOption, bestOption != null);
}
public SpellAbility findTopSpellAbility(Game game, SpellAbility sa) {
Iterator<SpellAbilityStackInstance> it = game.getStack().iterator();
SpellAbility tgtSA = it.next().getSpellAbility(true);
// Grab the topmost spellability that isn't this SA and use that for comparisons
if (sa.equals(tgtSA) && game.getStack().size() > 1) {
if (!it.hasNext()) {
return null;
}
tgtSA = it.next().getSpellAbility(true);
}
return tgtSA;
}
}

View File

@@ -170,6 +170,15 @@ public class ManaCostBeingPaid {
return false;
}
public final boolean containsOnlyPhyrexianMana() {
for (ManaCostShard shard : unpaidShards.keySet()) {
if (!shard.isPhyrexian()) {
return false;
}
}
return true;
}
public final boolean payPhyrexian() {
ManaCostShard phy = null;
for (ManaCostShard mcs : unpaidShards.keySet()) {

View File

@@ -2,6 +2,6 @@ Name:Spellskite
ManaCost:2
Types:Artifact Creature Horror
PT:0/4
A:AB$ ChangeTargets | Cost$ PU | TargetType$ Spell,Activated,Triggered | ValidTgts$ Card | DefinedMagnet$ Self | ChangeSingleTarget$ True | SpellDescription$ Change a target of target spell or ability to CARDNAME.
A:AB$ ChangeTargets | Cost$ PU | TargetType$ Spell,Activated,Triggered | ValidTgts$ Card | DefinedMagnet$ Self | ChangeSingleTarget$ True | AILogic$ SpellMagnet | SpellDescription$ Change a target of target spell or ability to CARDNAME.
SVar:Picture:http://www.wizards.com/global/images/magic/general/spellskite.jpg
Oracle:{P/U}: Change a target of target spell or ability to Spellskite. ({P/U} can be paid with either {U} or 2 life.)