From 459416630b243349dd05016714ca1981fdc49f9b Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Wed, 27 Apr 2022 07:45:48 +0300 Subject: [PATCH 1/4] - Don't limit the AI to shouldPumpCard logic for instants when deciding whether to put a +1/+1 counter on own card or not. --- forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 644c21b7480..661b70a7828 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -571,6 +571,9 @@ public class CountersPutAi extends CountersAi { Lists.newArrayList())) { choice = c; break; + } else if (!sa.getRestrictions().isSorcerySpeed() && !ComputerUtilCard.isUselessCreature(ai, c)) { + choice = c; + break; } } if (!source.isSpell()) { // does not cost a card From fd2d425d23dad8cca5b6c098f78fd0f79a2f6e16 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 26 Feb 2023 18:29:45 +0100 Subject: [PATCH 2/4] Logic tweak --- .../main/java/forge/ai/ComputerUtilCard.java | 49 ++++++------- .../main/java/forge/ai/ability/CharmAi.java | 7 +- .../java/forge/ai/ability/CountersPutAi.java | 70 ++++++++++--------- .../game/spellability/AbilityStatic.java | 6 -- forge-gui/res/cardsfolder/s/subtle_strike.txt | 2 +- 5 files changed, 70 insertions(+), 64 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 9725ba51afd..ac9f829abf6 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -1312,7 +1312,6 @@ public class ComputerUtilCard { final int power, final List 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 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(); diff --git a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java index 5d9c7167484..b1e981d9d6a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java @@ -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 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 diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 661b70a7828..cfc66522a27 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -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() { @@ -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,34 +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())) { - choice = c; - break; - } else if (!sa.getRestrictions().isSorcerySpeed() && !ComputerUtilCard.isUselessCreature(ai, c)) { + 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 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 { @@ -910,7 +917,6 @@ public class CountersPutAi extends CountersAi { // Didn't want to choose anything? list.clear(); } - } } return true; diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityStatic.java b/forge-game/src/main/java/forge/game/spellability/AbilityStatic.java index 726c45ec097..330b4e5ff0d 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityStatic.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityStatic.java @@ -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; /** *

@@ -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); diff --git a/forge-gui/res/cardsfolder/s/subtle_strike.txt b/forge-gui/res/cardsfolder/s/subtle_strike.txt index 1cbf89afd50..20f3502730b 100644 --- a/forge-gui/res/cardsfolder/s/subtle_strike.txt +++ b/forge-gui/res/cardsfolder/s/subtle_strike.txt @@ -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. From 1fa03b09b779ab4a6744201111aa903b16dfd5d4 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 26 Feb 2023 22:07:40 +0100 Subject: [PATCH 3/4] Some counter checks --- forge-ai/src/main/java/forge/ai/ComputerUtil.java | 8 ++++++++ .../src/main/java/forge/ai/ComputerUtilCombat.java | 14 +++++++++----- .../src/main/java/forge/ai/ability/DestroyAi.java | 6 +++++- .../main/java/forge/ai/ability/DestroyAllAi.java | 3 ++- .../src/main/java/forge/game/phase/Untap.java | 2 +- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 2ea5e039421..74806b1f430 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -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; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index c1f01f83ece..62519a46ce7 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -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; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index 02a5c9aa557..61026745190 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -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() { @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); } diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java index 633cbcbd2b8..5ddf3b677f1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java @@ -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 predicate = new Predicate() { @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")); } }; diff --git a/forge-game/src/main/java/forge/game/phase/Untap.java b/forge-game/src/main/java/forge/game/phase/Untap.java index bb66d38e071..b93ae6cf318 100644 --- a/forge-game/src/main/java/forge/game/phase/Untap.java +++ b/forge-game/src/main/java/forge/game/phase/Untap.java @@ -105,7 +105,7 @@ public class Untap extends Phase { public static final Predicate CANUNTAP = new Predicate() { @Override public boolean apply(Card c) { - return Untap.canUntap(c); + return canUntap(c); } }; From ebd0c64687dd5a95080bcd6af75b75f5bf4000b9 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 26 Feb 2023 23:35:30 +0100 Subject: [PATCH 4/4] Clean up --- .../game/trigger/TriggerSpellAbilityCastOrCopy.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java b/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java index 12cd844300b..13f846b847d 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java @@ -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; }