Merge pull request #155 from Agetian/ai-countersput-shouldpump

CountersPutAI: shouldPumpCard is too restrictive for instant speed PutCounter
This commit is contained in:
Anthony Calosa
2023-02-27 12:31:52 +08:00
committed by GitHub
11 changed files with 97 additions and 78 deletions

View File

@@ -1741,6 +1741,10 @@ public class ComputerUtil {
continue;
}
if (c.getCounters(CounterEnumType.SHIELD) > 0) {
continue;
}
// already regenerated
if (c.getShieldCount() > 0) {
continue;
@@ -1855,6 +1859,10 @@ public class ComputerUtil {
continue;
}
if (c.getCounters(CounterEnumType.SHIELD) > 0) {
continue;
}
// already regenerated
if (c.getShieldCount() > 0) {
continue;

View File

@@ -1312,7 +1312,6 @@ public class ComputerUtilCard {
final int power, final List<String> keywords) {
return shouldPumpCard(ai, sa, c, toughness, power, keywords, false);
}
public static boolean shouldPumpCard(final Player ai, final SpellAbility sa, final Card c, final int toughness,
final int power, final List<String> keywords, boolean immediately) {
final Game game = ai.getGame();
@@ -1546,35 +1545,37 @@ public class ComputerUtilCard {
|| ("PumpForTrample".equals(sa.getParam("AILogic")))) {
return true;
}
}
// try to determine if pumping a creature for more power will give lethal on board
// considering all unblocked creatures after the blockers are already declared
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) {
int totalPowerUnblocked = 0;
for (Card atk : combat.getAttackers()) {
if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) {
continue;
}
if (atk == c) {
totalPowerUnblocked += pumpedDmg; // this accounts for Trample by now
} else {
totalPowerUnblocked += ComputerUtilCombat.damageIfUnblocked(atk, opp, combat, true);
if (combat.isBlocked(atk)) {
// consider Trample damage properly for a blocked creature
for (Card blk : combat.getBlockers(atk)) {
totalPowerUnblocked -= ComputerUtilCombat.getDamageToKill(blk, false);
// try to determine if pumping a creature for more power will give lethal on board
// considering all unblocked creatures after the blockers are already declared
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
int totalPowerUnblocked = 0;
for (Card atk : combat.getAttackers()) {
if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) {
continue;
}
if (atk == c) {
totalPowerUnblocked += pumpedDmg; // this accounts for Trample by now
} else {
totalPowerUnblocked += ComputerUtilCombat.damageIfUnblocked(atk, opp, combat, true);
if (combat.isBlocked(atk)) {
// consider Trample damage properly for a blocked creature
for (Card blk : combat.getBlockers(atk)) {
totalPowerUnblocked -= ComputerUtilCombat.getDamageToKill(blk, false);
}
}
}
}
}
if (totalPowerUnblocked >= opp.getLife()) {
return true;
} else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) {
if (sa.getPayCosts().hasNoManaCost()) {
return true; // always activate abilities which cost no mana and which can increase unblocked damage
if (totalPowerUnblocked >= opp.getLife()) {
return true;
} else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) {
if (sa.getPayCosts().hasNoManaCost()) {
return true; // always activate abilities which cost no mana and which can increase unblocked damage
}
}
}
}
float value = 1.0f * (pumpedDmg - dmg);
if (c == sa.getHostCard() && power > 0) {
int divisor = sa.getPayCosts().getTotalMana().getCMC();

View File

@@ -128,7 +128,7 @@ public class ComputerUtilCombat {
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay"));
// The creature won't untap next turn
return !attacker.isTapped() || Untap.canUntap(attacker);
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
}
/**
@@ -1628,9 +1628,8 @@ public class ComputerUtilCombat {
* a {@link forge.game.card.Card} object.
* @return a boolean.
*/
public static boolean combatantCantBeDestroyed(Player ai, final Card combatant) {
// either indestructible or may regenerate
if (combatant.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, combatant)) {
public static boolean combatantCantBeDestroyed(final Player ai, final Card combatant) {
if (combatant.getCounters(CounterEnumType.SHIELD) > 0) {
return true;
}
@@ -1639,6 +1638,11 @@ public class ComputerUtilCombat {
return true;
}
// either indestructible or may regenerate
if (combatant.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, combatant)) {
return true;
}
return false;
}
@@ -2150,7 +2154,7 @@ public class ComputerUtilCombat {
final boolean noPrevention) {
final int killDamage = getDamageToKill(c, false);
if (c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getShieldCount() > 0) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getCounters(CounterEnumType.SHIELD) > 0 || (c.getShieldCount() > 0 && c.canBeShielded())) {
if (!(source.hasKeyword(Keyword.WITHER) || source.hasKeyword(Keyword.INFECT))) {
return maxDamage + 1;
}

View File

@@ -14,6 +14,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -26,7 +27,6 @@ public class CharmAi extends SpellAbilityAi {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
Collections.shuffle(choices);
final int num;
final int min;
@@ -37,6 +37,11 @@ public class CharmAi extends SpellAbilityAi {
min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num;
}
// only randomize if not all possible together
if (num < choices.size() || source.hasKeyword(Keyword.ESCALATE)) {
Collections.shuffle(choices);
}
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
// Reset the chosen list otherwise it will be locked in forever by earlier calls

View File

@@ -45,6 +45,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.trigger.Trigger;
@@ -456,25 +457,24 @@ public class CountersPutAi extends CountersAi {
}
}
if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target
if (sa.usesTargeting() && sa.getMinTargets() < 2) {
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
Card c = sa.getTargetCard();
if (sa.getTargets().size() > 1) {
sa.resetTargets();
sa.getTargets().add(c);
if (sa.usesTargeting()) {
if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target
if (sa.getMinTargets() < 2) {
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
Card c = sa.getTargetCard();
if (sa.getTargets().size() > 1) {
sa.resetTargets();
sa.getTargets().add(c);
}
sa.addDividedAllocation(c, amount);
return true;
} else {
return false;
}
sa.addDividedAllocation(c, amount);
return true;
} else {
return false;
}
}
}
// Targeting
if (sa.usesTargeting()) {
sa.resetTargets();
final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost);
@@ -482,7 +482,7 @@ public class CountersPutAi extends CountersAi {
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
list = ComputerUtil.getSafeTargets(ai, sa, ai.getCardsIn(ZoneType.Battlefield));
}
list = CardLists.filter(list, new Predicate<Card>() {
@@ -530,8 +530,7 @@ public class CountersPutAi extends CountersAi {
for (int i = 1; i < amount + 1; i++) {
int left = amount;
for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, i, i,
Lists.newArrayList())) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, i, i, Lists.newArrayList())) {
sa.getTargets().add(c);
sa.addDividedAllocation(c, i);
left -= i;
@@ -553,7 +552,7 @@ public class CountersPutAi extends CountersAi {
// target loop
while (sa.canAddMoreTarget()) {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return false;
} else {
@@ -567,31 +566,42 @@ public class CountersPutAi extends CountersAi {
} else {
if (type.equals("P1P1") && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount,
Lists.newArrayList())) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount, Lists.newArrayList())) {
choice = c;
break;
}
}
if (!source.isSpell()) { // does not cost a card
if (choice == null) { // find generic target
if (abCost == null
if (choice == null) {
// try to use as cheap kill
choice = ComputerUtil.getKilledByTargeting(sa, CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa));
}
if (choice == null) {
// find generic target
boolean increasesCharmOutcome = false;
if (sa.getRootAbility().getApi() == ApiType.Charm && source.getStaticAbilities().isEmpty()) {
List<AbilitySub> choices = Lists.newArrayList(sa.getRootAbility().getAdditionalAbilityList("Choices"));
choices.remove(sa);
// check if other choice will already be played
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
}
if (!source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|| ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) {
if (abCost == null || abCost == Cost.Zero
|| (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) {
// only use at opponent EOT unless it is free
choice = chooseBoonTarget(list, type);
}
}
}
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Dromoka's Command")) {
choice = chooseBoonTarget(list, type);
}
} else {
choice = chooseBoonTarget(list, type);
}
}
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return false;
} else {
@@ -907,7 +917,6 @@ public class CountersPutAi extends CountersAi {
// Didn't want to choose anything?
list.clear();
}
}
}
return true;

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
@@ -174,6 +175,8 @@ public class DestroyAi extends SpellAbilityAi {
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
}
if (!SpellAbilityAi.playReusable(ai, sa)) {
list = CardLists.filter(list, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -196,7 +199,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
//Check for undying
return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterEnumType.P1P1) > 0);
return !c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterEnumType.P1P1) > 0;
}
});
}
@@ -333,6 +336,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
preferred = CardLists.filter(preferred, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);
}

View File

@@ -8,6 +8,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
@@ -21,7 +22,7 @@ public class DestroyAllAi extends SpellAbilityAi {
private static final Predicate<Card> predicate = new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getSVar("SacMe").length() > 0);
return !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getCounters(CounterEnumType.SHIELD) > 0 || c.hasSVar("SacMe"));
}
};

View File

@@ -105,7 +105,7 @@ public class Untap extends Phase {
public static final Predicate<Card> CANUNTAP = new Predicate<Card>() {
@Override
public boolean apply(Card c) {
return Untap.canUntap(c);
return canUntap(c);
}
};

View File

@@ -20,7 +20,6 @@ package forge.game.spellability;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.player.Player;
/**
* <p>
@@ -51,11 +50,6 @@ public abstract class AbilityStatic extends Ability implements Cloneable {
}
@Override
public boolean canPlay() {
Player player = getActivatingPlayer();
if (player == null) {
player = this.getHostCard().getController();
}
final Card c = this.getHostCard();
return this.getRestrictions().canPlay(c, this);

View File

@@ -126,15 +126,8 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
boolean validTgtFound = false;
while (sa != null && !validTgtFound) {
for (final Card tgt : sa.getTargets().getTargetCards()) {
if (matchesValid(tgt, getParam("TargetsValid").split(","))) {
validTgtFound = true;
break;
}
}
for (final Player p : sa.getTargets().getTargetPlayers()) {
if (matchesValid(p, getParam("TargetsValid").split(","))) {
for (final GameEntity ge : sa.getTargets().getTargetEntities()) {
if (matchesValid(ge, getParam("TargetsValid").split(","))) {
validTgtFound = true;
break;
}

View File

@@ -3,5 +3,5 @@ ManaCost:1 B
Types:Instant
A:SP$ Charm | Cost$ 1 B | MinCharmNum$ 1 | CharmNum$ 2 | Choices$ DBPump,DBPutCounter
SVar:DBPump:DB$ Pump | ValidTgts$ Creature | TgtPrompt$ Select target creature (-1/-1) | NumAtt$ -1 | NumDef$ -1 | IsCurse$ True | SpellDescription$ Target creature gets -1/-1 until end of turn.
SVar:DBPutCounter:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select target creature (+1/+1 counter) | AILogic$ Good | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on target creature.
SVar:DBPutCounter:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select target creature (+1/+1 counter) | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on target creature.
Oracle:Choose one or both —\n• Target creature gets -1/-1 until end of turn.\n• Put a +1/+1 counter on target creature.