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

View File

@@ -1312,7 +1312,6 @@ public class ComputerUtilCard {
final int power, final List<String> keywords) { final int power, final List<String> keywords) {
return shouldPumpCard(ai, sa, c, toughness, power, keywords, false); 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, 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 int power, final List<String> keywords, boolean immediately) {
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -1546,35 +1545,37 @@ public class ComputerUtilCard {
|| ("PumpForTrample".equals(sa.getParam("AILogic")))) { || ("PumpForTrample".equals(sa.getParam("AILogic")))) {
return true; return true;
} }
}
// try to determine if pumping a creature for more power will give lethal on board // 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 // considering all unblocked creatures after the blockers are already declared
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) { if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
int totalPowerUnblocked = 0; int totalPowerUnblocked = 0;
for (Card atk : combat.getAttackers()) { for (Card atk : combat.getAttackers()) {
if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) { if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) {
continue; continue;
} }
if (atk == c) { if (atk == c) {
totalPowerUnblocked += pumpedDmg; // this accounts for Trample by now totalPowerUnblocked += pumpedDmg; // this accounts for Trample by now
} else { } else {
totalPowerUnblocked += ComputerUtilCombat.damageIfUnblocked(atk, opp, combat, true); totalPowerUnblocked += ComputerUtilCombat.damageIfUnblocked(atk, opp, combat, true);
if (combat.isBlocked(atk)) { if (combat.isBlocked(atk)) {
// consider Trample damage properly for a blocked creature // consider Trample damage properly for a blocked creature
for (Card blk : combat.getBlockers(atk)) { for (Card blk : combat.getBlockers(atk)) {
totalPowerUnblocked -= ComputerUtilCombat.getDamageToKill(blk, false); totalPowerUnblocked -= ComputerUtilCombat.getDamageToKill(blk, false);
}
} }
} }
} }
} if (totalPowerUnblocked >= opp.getLife()) {
if (totalPowerUnblocked >= opp.getLife()) { return true;
return true; } else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) {
} else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) { if (sa.getPayCosts().hasNoManaCost()) {
if (sa.getPayCosts().hasNoManaCost()) { return true; // always activate abilities which cost no mana and which can increase unblocked damage
return true; // always activate abilities which cost no mana and which can increase unblocked damage }
} }
} }
} }
float value = 1.0f * (pumpedDmg - dmg); float value = 1.0f * (pumpedDmg - dmg);
if (c == sa.getHostCard() && power > 0) { if (c == sa.getHostCard() && power > 0) {
int divisor = sa.getPayCosts().getTotalMana().getCMC(); 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.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay")); // || attacker.hasSVar("EndOfTurnLeavePlay"));
// The creature won't untap next turn // 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. * a {@link forge.game.card.Card} object.
* @return a boolean. * @return a boolean.
*/ */
public static boolean combatantCantBeDestroyed(Player ai, final Card combatant) { public static boolean combatantCantBeDestroyed(final Player ai, final Card combatant) {
// either indestructible or may regenerate if (combatant.getCounters(CounterEnumType.SHIELD) > 0) {
if (combatant.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, combatant)) {
return true; return true;
} }
@@ -1639,6 +1638,11 @@ public class ComputerUtilCombat {
return true; return true;
} }
// either indestructible or may regenerate
if (combatant.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, combatant)) {
return true;
}
return false; return false;
} }
@@ -2150,7 +2154,7 @@ public class ComputerUtilCombat {
final boolean noPrevention) { final boolean noPrevention) {
final int killDamage = getDamageToKill(c, false); 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))) { if (!(source.hasKeyword(Keyword.WITHER) || source.hasKeyword(Keyword.INFECT))) {
return maxDamage + 1; return maxDamage + 1;
} }

View File

@@ -14,6 +14,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -26,7 +27,6 @@ public class CharmAi extends SpellAbilityAi {
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa); List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
Collections.shuffle(choices);
final int num; final int num;
final int min; final int min;
@@ -37,6 +37,11 @@ public class CharmAi extends SpellAbilityAi {
min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num; 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? 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 // 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.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
@@ -456,25 +457,24 @@ public class CountersPutAi extends CountersAi {
} }
} }
if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa, ai)) { if (sa.usesTargeting()) {
// only evaluates case where all tokens are placed on a single target if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (sa.usesTargeting() && sa.getMinTargets() < 2) { // only evaluates case where all tokens are placed on a single target
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) { if (sa.getMinTargets() < 2) {
Card c = sa.getTargetCard(); if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
if (sa.getTargets().size() > 1) { Card c = sa.getTargetCard();
sa.resetTargets(); if (sa.getTargets().size() > 1) {
sa.getTargets().add(c); 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(); sa.resetTargets();
final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost); final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost);
@@ -482,7 +482,7 @@ public class CountersPutAi extends CountersAi {
if (sa.isCurse()) { if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else { } else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); list = ComputerUtil.getSafeTargets(ai, sa, ai.getCardsIn(ZoneType.Battlefield));
} }
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@@ -530,8 +530,7 @@ public class CountersPutAi extends CountersAi {
for (int i = 1; i < amount + 1; i++) { for (int i = 1; i < amount + 1; i++) {
int left = amount; int left = amount;
for (Card c : list) { for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, i, i, if (ComputerUtilCard.shouldPumpCard(ai, sa, c, i, i, Lists.newArrayList())) {
Lists.newArrayList())) {
sa.getTargets().add(c); sa.getTargets().add(c);
sa.addDividedAllocation(c, i); sa.addDividedAllocation(c, i);
left -= i; left -= i;
@@ -553,7 +552,7 @@ public class CountersPutAi extends CountersAi {
// target loop // target loop
while (sa.canAddMoreTarget()) { while (sa.canAddMoreTarget()) {
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) { if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets(); sa.resetTargets();
return false; return false;
} else { } else {
@@ -567,31 +566,42 @@ public class CountersPutAi extends CountersAi {
} else { } else {
if (type.equals("P1P1") && !SpellAbilityAi.isSorcerySpeed(sa, ai)) { if (type.equals("P1P1") && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
for (Card c : list) { for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount, if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount, Lists.newArrayList())) {
Lists.newArrayList())) {
choice = c; choice = c;
break; break;
} }
} }
if (!source.isSpell()) { // does not cost a card
if (choice == null) { // find generic target if (choice == null) {
if (abCost == 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))) { || (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) {
// only use at opponent EOT unless it is free // only use at opponent EOT unless it is free
choice = chooseBoonTarget(list, type); choice = chooseBoonTarget(list, type);
} }
} }
} }
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Dromoka's Command")) {
choice = chooseBoonTarget(list, type);
}
} else { } else {
choice = chooseBoonTarget(list, type); choice = chooseBoonTarget(list, type);
} }
} }
if (choice == null) { // can't find anything left if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets(); sa.resetTargets();
return false; return false;
} else { } else {
@@ -907,7 +917,6 @@ public class CountersPutAi extends CountersAi {
// Didn't want to choose anything? // Didn't want to choose anything?
list.clear(); list.clear();
} }
} }
} }
return true; return true;

View File

@@ -1,6 +1,7 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.*; import forge.ai.*;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -174,6 +175,8 @@ public class DestroyAi extends SpellAbilityAi {
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false); list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
} }
if (!SpellAbilityAi.playReusable(ai, sa)) { if (!SpellAbilityAi.playReusable(ai, sa)) {
list = CardLists.filter(list, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
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) {
@@ -196,7 +199,7 @@ public class DestroyAi extends SpellAbilityAi {
return false; return false;
} }
//Check for undying //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); CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents()); preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
preferred = CardLists.filter(preferred, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
if (CardLists.getNotType(preferred, "Creature").isEmpty()) { if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false); 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.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
@@ -21,7 +22,7 @@ public class DestroyAllAi extends SpellAbilityAi {
private static final Predicate<Card> predicate = new Predicate<Card>() { private static final Predicate<Card> predicate = new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { 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>() { public static final Predicate<Card> CANUNTAP = new Predicate<Card>() {
@Override @Override
public boolean apply(Card c) { 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.card.mana.ManaCost;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.player.Player;
/** /**
* <p> * <p>
@@ -51,11 +50,6 @@ public abstract class AbilityStatic extends Ability implements Cloneable {
} }
@Override @Override
public boolean canPlay() { public boolean canPlay() {
Player player = getActivatingPlayer();
if (player == null) {
player = this.getHostCard().getController();
}
final Card c = this.getHostCard(); final Card c = this.getHostCard();
return this.getRestrictions().canPlay(c, this); return this.getRestrictions().canPlay(c, this);

View File

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

View File

@@ -3,5 +3,5 @@ ManaCost:1 B
Types:Instant Types:Instant
A:SP$ Charm | Cost$ 1 B | MinCharmNum$ 1 | CharmNum$ 2 | Choices$ DBPump,DBPutCounter 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: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. Oracle:Choose one or both —\n• Target creature gets -1/-1 until end of turn.\n• Put a +1/+1 counter on target creature.