Merge branch 'aifix' into 'master'

Fix targeting of some effects (e.g. Axis of Mortality)

See merge request core-developers/forge!5866
This commit is contained in:
Michael Kamensky
2021-11-21 15:21:19 +00:00
15 changed files with 81 additions and 65 deletions

View File

@@ -218,7 +218,7 @@ public class ComputerUtilAbility {
public static boolean isFullyTargetable(SpellAbility sa) { public static boolean isFullyTargetable(SpellAbility sa) {
SpellAbility sub = sa; SpellAbility sub = sa;
while (sub != null) { while (sub != null) {
if (sub.usesTargeting() && !sub.getTargetRestrictions().hasCandidates(sub)) { if (sub.usesTargeting() && sub.getTargetRestrictions().getNumCandidates(sub, true) < sub.getMinTargets()) {
return false; return false;
} }
sub = sub.getSubAbility(); sub = sub.getSubAbility();

View File

@@ -54,7 +54,7 @@ public class AddTurnAi extends SpellAbilityAi {
break; break;
} }
} }
if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && sa.canTarget(opp)) { if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {
return false; return false;

View File

@@ -21,14 +21,13 @@ public class DamageEachAi extends DamageAiBase {
Player weakestOpp = targetableOpps.min(PlayerPredicates.compareByLife()); Player weakestOpp = targetableOpps.min(PlayerPredicates.compareByLife());
if (sa.usesTargeting() && weakestOpp != null) { if (sa.usesTargeting() && weakestOpp != null) {
if ("MadSarkhanUltimate".equals(logic) && !SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp)) {
return false;
}
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(weakestOpp); sa.getTargets().add(weakestOpp);
} }
if ("MadSarkhanUltimate".equals(logic)) {
return SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp);
}
final String damage = sa.getParam("NumDmg"); final String damage = sa.getParam("NumDmg");
final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
return shouldTgtP(ai, sa, iDmg, false); return shouldTgtP(ai, sa, iDmg, false);

View File

@@ -47,7 +47,7 @@ public class DigAi extends SpellAbilityAi {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) { if (!sa.canTarget(opp)) {
return false; return false;
} }
sa.getTargets().add(opp); sa.getTargets().add(opp);

View File

@@ -62,7 +62,6 @@ public final class EncodeAi extends SpellAbilityAi {
return true; return true;
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* *

View File

@@ -1,8 +1,9 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.util.MyRandom; import forge.util.MyRandom;
@@ -18,14 +19,15 @@ public class LifeExchangeAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final int myLife = aiPlayer.getLife();
Player opponent = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
final int hLife = opponent.getLife();
if (!aiPlayer.canGainLife()) { if (!aiPlayer.canGainLife()) {
return false; return false;
} }
final int myLife = aiPlayer.getLife();
final PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife());
final int hLife = opponent == null ? 0 : opponent.getLife();
// prevent run-away activations - first time will always return true // prevent run-away activations - first time will always return true
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
@@ -36,24 +38,23 @@ public class LifeExchangeAi extends SpellAbilityAi {
*/ */
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (opponent.canBeTargetedBy(sa)) { if (opponent != null && opponent.canLoseLife()) {
// never target self, that would be silly for exchange // never target self, that would be silly for exchange
sa.getTargets().add(opponent); sa.getTargets().add(opponent);
if (!opponent.canLoseLife()) { } else {
return false; return false;
}
} }
} }
// if life is in danger, always activate // if life is in danger, always activate
if ((myLife < 5) && (hLife > myLife)) { if (myLife < 5 && hLife > myLife) {
return true; return true;
} }
// cost includes sacrifice probably, so make sure it's worth it // cost includes sacrifice probably, so make sure it's worth it
chance &= (hLife > (myLife + 8)); chance &= (hLife > (myLife + 8));
return ((MyRandom.getRandom().nextFloat() < .6667) && chance); return MyRandom.getRandom().nextFloat() < .6667 && chance;
} }
/** /**
@@ -70,13 +71,16 @@ public class LifeExchangeAi extends SpellAbilityAi {
* @return a boolean. * @return a boolean.
*/ */
@Override @Override
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
final boolean mandatory) { PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); Player opp = targetableOpps.max(PlayerPredicates.compareByLife());
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
if (sa.canAddMoreTarget()) {
sa.getTargets().add(ai);
}
} else { } else {
return false; return false;
} }

View File

@@ -149,8 +149,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
* @return a boolean. * @return a boolean.
*/ */
@Override @Override
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
final boolean mandatory) {
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();

View File

@@ -1,15 +1,21 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
public class LifeSetAi extends SpellAbilityAi { public class LifeSetAi extends SpellAbilityAi {
@@ -17,14 +23,11 @@ public class LifeSetAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final int myLife = ai.getLife(); final int myLife = ai.getLife();
final Player opponent = ai.getStrongestOpponent(); final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
final int hlife = opponent.getLife(); final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife());
final int hlife = opponent == null ? 0 : opponent.getLife();
final String amountStr = sa.getParam("LifeAmount"); final String amountStr = sa.getParam("LifeAmount");
if (!ai.canGainLife()) {
return false;
}
// Don't use setLife before main 2 if possible // Don't use setLife before main 2 if possible
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& !sa.hasParam("ActivationPhases")) { && !sa.hasParam("ActivationPhases")) {
@@ -55,20 +58,20 @@ public class LifeSetAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) { if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(opponent);
// if we can only target the human, and the Human's life // if we can only target the human, and the Human's life
// would go up, don't play it. // would go up, don't play it.
// possibly add a combo here for Magister Sphinx and // possibly add a combo here for Magister Sphinx and
// Higedetsu's (sp?) Second Rite // Higedetsu's (sp?) Second Rite
if ((amount > hlife) || !opponent.canLoseLife()) { if (opponent == null || amount > hlife || !opponent.canLoseLife()) {
return false; return false;
} }
sa.getTargets().add(opponent);
} else { } else {
if ((amount > myLife) && (myLife <= 10)) { if (amount > myLife && myLife <= 10 && ai.canGainLife()) {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} else if (hlife > amount) { } else if (hlife > amount) {
sa.getTargets().add(opponent); sa.getTargets().add(opponent);
} else if (amount > myLife) { } else if (amount > myLife && ai.canGainLife()) {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} else { } else {
return false; return false;
@@ -90,18 +93,19 @@ public class LifeSetAi extends SpellAbilityAi {
} }
// if life is in danger, always activate // if life is in danger, always activate
if ((myLife < 3) && (amount > myLife)) { if (myLife < 3 && amount > myLife && ai.canGainLife()) {
return true; return true;
} }
return ((MyRandom.getRandom().nextFloat() < .6667) && chance); return MyRandom.getRandom().nextFloat() < .6667 && chance;
} }
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final int myLife = ai.getLife(); final int myLife = ai.getLife();
final Player opponent = ai.getStrongestOpponent(); final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
final int hlife = opponent.getLife(); final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife());
final int hlife = opponent == null ? 0 : opponent.getLife();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
@@ -118,13 +122,13 @@ public class LifeSetAi extends SpellAbilityAi {
} }
// special cases when amount can't be calculated without targeting first // special cases when amount can't be calculated without targeting first
if (amount == 0 && "TargetedPlayer$StartingLife/HalfDown".equals(source.getSVar(amountStr))) { if (amount == 0 && opponent != null && "TargetedPlayer$StartingLife/HalfDown".equals(source.getSVar(amountStr))) {
// e.g. Torgaar, Famine Incarnate // e.g. Torgaar, Famine Incarnate
return doHalfStartingLifeLogic(ai, opponent, sa); return doHalfStartingLifeLogic(ai, opponent, sa);
} }
if (sourceName.equals("Eternity Vessel") if (sourceName.equals("Eternity Vessel")
&& (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterEnumType.CHARGE) == 0))) { && (Iterables.any(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Vampire Hexmage")) || (source.getCounters(CounterEnumType.CHARGE) == 0))) {
return false; return false;
} }
@@ -134,13 +138,16 @@ public class LifeSetAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) { if (tgt.canOnlyTgtOpponent()) {
if (opponent == null) {
return false;
}
sa.getTargets().add(opponent); sa.getTargets().add(opponent);
} else { } else {
if (amount > myLife && myLife <= 10) { if (amount > myLife && myLife <= 10) {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} else if (hlife > amount) { } else if (hlife > amount) {
sa.getTargets().add(opponent); sa.getTargets().add(opponent);
} else if (amount > myLife) { } else if (amount > myLife || mandatory) {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} else { } else {
return false; return false;

View File

@@ -25,8 +25,7 @@ public class PoisonAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
return !ph.getPhase().isBefore(PhaseType.MAIN2) return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases");
|| sa.hasParam("ActivationPhases");
} }
/* /*

View File

@@ -31,11 +31,10 @@ public class PowerExchangeAi extends SpellAbilityAi {
List<Card> list = List<Card> list =
CardLists.getValidCards(ai.getGame().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); CardLists.getValidCards(ai.getGame().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on purpose
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa) && c.getController() != ai; return c.canBeTargetedBy(sa) && c.getController() != ai;
} }
}); });
CardLists.sortByPowerAsc(list); CardLists.sortByPowerAsc(list);

View File

@@ -26,7 +26,6 @@ public class ProtectAllAi extends SpellAbilityAi {
return false; return false;
} // protectAllCanPlayAI() } // protectAllCanPlayAI()
@Override @Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return true; return true;

View File

@@ -53,7 +53,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife()); Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
final boolean canTgtAI = sa.canTarget(aiPlayer); final boolean canTgtAI = sa.canTarget(aiPlayer);
final boolean canTgtHuman = opp != null && sa.canTarget(opp); final boolean canTgtHuman = sa.canTarget(opp);
if (canTgtHuman && canTgtAI) { if (canTgtHuman && canTgtAI) {
// TODO: maybe some other consideration rather than random? // TODO: maybe some other consideration rather than random?

View File

@@ -138,12 +138,11 @@ public class RegenerateAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean chance = false; boolean chance = false;
final TargetRestrictions tgt = sa.getTargetRestrictions(); if (sa.usesTargeting()) {
if (tgt == null) { chance = regenMandatoryTarget(ai, sa, mandatory);
} else {
// If there's no target on the trigger, just say yes. // If there's no target on the trigger, just say yes.
chance = true; chance = true;
} else {
chance = regenMandatoryTarget(ai, sa, mandatory);
} }
return chance; return chance;
@@ -164,7 +163,7 @@ public class RegenerateAi extends SpellAbilityAi {
return false; return false;
} }
if (!mandatory && (compTargetables.size() == 0)) { if (!mandatory && compTargetables.size() == 0) {
return false; return false;
} }

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Collections;
import java.util.List; import java.util.List;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -15,6 +16,8 @@ import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -22,14 +25,14 @@ public class SacrificeAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
return sacrificeTgtAI(ai, sa); return sacrificeTgtAI(ai, sa, false);
} }
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) { public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI should only activate this during Human's turn // AI should only activate this during Human's turn
return sacrificeTgtAI(ai, sa); return sacrificeTgtAI(ai, sa, false);
} }
@Override @Override
@@ -48,21 +51,24 @@ public class SacrificeAi extends SpellAbilityAi {
// Eventually, we can call the trigger of ETB abilities with not // Eventually, we can call the trigger of ETB abilities with not
// mandatory as part of the checks to cast something // mandatory as part of the checks to cast something
return sacrificeTgtAI(ai, sa) || mandatory; return sacrificeTgtAI(ai, sa, mandatory) || mandatory;
} }
private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa) { private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final boolean destroy = sa.hasParam("Destroy"); final boolean destroy = sa.hasParam("Destroy");
final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = ai.getStrongestOpponent(); final Player opp = Collections.max(targetableOpps, PlayerPredicates.compareByLife());
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); if (opp == null) {
if (!opp.canBeTargetedBy(sa)) {
return false; return false;
} }
sa.resetTargets();
sa.getTargets().add(opp); sa.getTargets().add(opp);
if (mandatory) {
return true;
}
final String valid = sa.getParam("SacValid"); final String valid = sa.getParam("SacValid");
String num = sa.getParamOrDefault("Amount" , "1"); String num = sa.getParamOrDefault("Amount" , "1");
final int amount = AbilityUtils.calculateAmount(source, num, sa); final int amount = AbilityUtils.calculateAmount(source, num, sa);
@@ -79,7 +85,7 @@ public class SacrificeAi extends SpellAbilityAi {
for (Card c : list) { for (Card c : list) {
if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) { if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) {
return false; return false;
} }
} }
if (!destroy) { if (!destroy) {
@@ -131,7 +137,7 @@ public class SacrificeAi extends SpellAbilityAi {
List<Card> humanList = null; List<Card> humanList = null;
try { try {
humanList = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), source, sa); humanList = CardLists.getValidCards(ai.getStrongestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), source, sa);
} catch (NullPointerException e) { } catch (NullPointerException e) {
return false; return false;
} finally { } finally {

View File

@@ -16,6 +16,8 @@ import forge.game.card.CardPredicates;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
@@ -101,10 +103,14 @@ public class TapAllAi extends SpellAbilityAi {
CardCollectionView validTappables = getTapAllTargets(valid, source, sa); CardCollectionView validTappables = getTapAllTargets(valid, source, sa);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player target = targetableOpps.max(PlayerPredicates.compareByLife());
if (target == null && mandatory) {
target = ai;
}
sa.resetTargets(); sa.resetTargets();
Player opp = ai.getStrongestOpponent(); sa.getTargets().add(target);
sa.getTargets().add(opp); validTappables = target.getCardsIn(ZoneType.Battlefield);
validTappables = opp.getCardsIn(ZoneType.Battlefield);
} }
if (mandatory) { if (mandatory) {