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) {
SpellAbility sub = sa;
while (sub != null) {
if (sub.usesTargeting() && !sub.getTargetRestrictions().hasCandidates(sub)) {
if (sub.usesTargeting() && sub.getTargetRestrictions().getNumCandidates(sub, true) < sub.getMinTargets()) {
return false;
}
sub = sub.getSubAbility();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,11 +31,10 @@ public class PowerExchangeAi extends SpellAbilityAi {
List<Card> list =
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>() {
@Override
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);

View File

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

View File

@@ -53,7 +53,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
final boolean canTgtAI = sa.canTarget(aiPlayer);
final boolean canTgtHuman = opp != null && sa.canTarget(opp);
final boolean canTgtHuman = sa.canTarget(opp);
if (canTgtHuman && canTgtAI) {
// 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) {
boolean chance = false;
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
if (sa.usesTargeting()) {
chance = regenMandatoryTarget(ai, sa, mandatory);
} else {
// If there's no target on the trigger, just say yes.
chance = true;
} else {
chance = regenMandatoryTarget(ai, sa, mandatory);
}
return chance;
@@ -164,7 +163,7 @@ public class RegenerateAi extends SpellAbilityAi {
return false;
}
if (!mandatory && (compTargetables.size() == 0)) {
if (!mandatory && compTargetables.size() == 0) {
return false;
}

View File

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

View File

@@ -16,6 +16,8 @@ import forge.game.card.CardPredicates;
import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
@@ -101,10 +103,14 @@ public class TapAllAi extends SpellAbilityAi {
CardCollectionView validTappables = getTapAllTargets(valid, source, sa);
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();
Player opp = ai.getStrongestOpponent();
sa.getTargets().add(opp);
validTappables = opp.getCardsIn(ZoneType.Battlefield);
sa.getTargets().add(target);
validTappables = target.getCardsIn(ZoneType.Battlefield);
}
if (mandatory) {