mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-18 19:58:00 +00:00
- 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:
@@ -2076,6 +2076,32 @@ public class ComputerUtil {
|
|||||||
return ComputerUtilCard.getBestCreatureAI(killables);
|
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) {
|
public static int getDamageForPlaying(final Player player, final SpellAbility sa) {
|
||||||
|
|
||||||
// check for bad spell cast triggers
|
// check for bad spell cast triggers
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import forge.game.card.CardLists;
|
|||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
public class ComputerUtilAbility {
|
public class ComputerUtilAbility {
|
||||||
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
||||||
@@ -106,4 +108,22 @@ public class ComputerUtilAbility {
|
|||||||
}
|
}
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean test, boolean checkPlayable) {
|
||||||
adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai);
|
adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai);
|
||||||
List<Mana> manaSpentToPay = test ? new ArrayList<Mana>() : sa.getPayingMana();
|
List<Mana> manaSpentToPay = test ? new ArrayList<Mana>() : sa.getPayingMana();
|
||||||
|
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||||
|
|
||||||
List<SpellAbility> paymentList = Lists.newArrayList();
|
List<SpellAbility> paymentList = Lists.newArrayList();
|
||||||
|
|
||||||
@@ -324,7 +325,7 @@ public class ComputerUtilMana {
|
|||||||
boolean hasConverge = sa.getHostCard().hasConverge();
|
boolean hasConverge = sa.getHostCard().hasConverge();
|
||||||
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test,
|
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test,
|
||||||
checkPlayable, manaSpentToPay, hasConverge);
|
checkPlayable, manaSpentToPay, hasConverge);
|
||||||
if (sourcesForShards == null) {
|
if (sourcesForShards == null && !purePhyrexian) {
|
||||||
return false; // no mana abilities to use for paying
|
return false; // no mana abilities to use for paying
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +354,11 @@ public class ComputerUtilMana {
|
|||||||
hasConverge = false;
|
hasConverge = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
saList = sourcesForShards.get(toPay);
|
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) {
|
if (saList == null) {
|
||||||
break;
|
break;
|
||||||
@@ -361,7 +366,7 @@ public class ComputerUtilMana {
|
|||||||
|
|
||||||
saList.removeAll(saExcludeList);
|
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")) {
|
if (saPayment != null && saPayment.hasParam("AILogic")) {
|
||||||
boolean consider = false;
|
boolean consider = false;
|
||||||
@@ -375,7 +380,7 @@ public class ComputerUtilMana {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (saPayment == null) {
|
if (saPayment == null) {
|
||||||
if (!toPay.isPhyrexian() || !ai.canPayLife(2)) {
|
if (!toPay.isPhyrexian() || !ai.canPayLife(2) || (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
|
||||||
break; // cannot pay
|
break; // cannot pay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
|
import forge.ai.ComputerUtilAbility;
|
||||||
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
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.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
@@ -16,7 +23,47 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
return false;
|
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
|
@Override
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.AiProps;
|
import forge.ai.AiProps;
|
||||||
|
import forge.ai.ComputerUtilAbility;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
@@ -54,7 +55,7 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
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
|
if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || topSA.getActivatingPlayer() == ai
|
||||||
|| ai.getAllies().contains(topSA.getActivatingPlayer())) {
|
|| ai.getAllies().contains(topSA.getActivatingPlayer())) {
|
||||||
// might as well check for player's friendliness
|
// 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);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,15 @@ public class ManaCostBeingPaid {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final boolean containsOnlyPhyrexianMana() {
|
||||||
|
for (ManaCostShard shard : unpaidShards.keySet()) {
|
||||||
|
if (!shard.isPhyrexian()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public final boolean payPhyrexian() {
|
public final boolean payPhyrexian() {
|
||||||
ManaCostShard phy = null;
|
ManaCostShard phy = null;
|
||||||
for (ManaCostShard mcs : unpaidShards.keySet()) {
|
for (ManaCostShard mcs : unpaidShards.keySet()) {
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ Name:Spellskite
|
|||||||
ManaCost:2
|
ManaCost:2
|
||||||
Types:Artifact Creature Horror
|
Types:Artifact Creature Horror
|
||||||
PT:0/4
|
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
|
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.)
|
Oracle:{P/U}: Change a target of target spell or ability to Spellskite. ({P/U} can be paid with either {U} or 2 life.)
|
||||||
|
|||||||
Reference in New Issue
Block a user