From dbc2a5b5ebb42a259464bca2211b2d018cd56a11 Mon Sep 17 00:00:00 2001 From: Agetian Date: Thu, 6 Jul 2023 18:01:59 +0300 Subject: [PATCH] AI: Various logic fixes and improvements (#3416) * - Fix logic for Elderscale Wurm damage prediction * - AI logic hint for Tempting Wurm - Several related AI hint fixes (cards in hand don't have a controller set) * - Implement AI logic for Grothama, All-Devouring. * - AI shouldn't fight its own Grothama for card draw (leads to silly results). * - Attempt two at fixing Elderscale Wurm damage preditions. * - Improve banding AI so that the AI knows how to tank damage from Trample when the other possibility is multiple chump blockers. * - Clean up * - Attempt to defer checking for "...as if it were unblocked" static until after the Banding blocker(s) are assigned. - Clean up * - Clean up --- .../main/java/forge/ai/AiBlockController.java | 52 ++++++++++++++----- .../src/main/java/forge/ai/SpecialCardAi.java | 15 ++++++ .../main/java/forge/ai/ability/FightAi.java | 16 +++--- .../main/java/forge/game/player/Player.java | 2 +- forge-gui/res/cardsfolder/b/bog_down.txt | 2 +- .../res/cardsfolder/c/caligo_skin_witch.txt | 2 +- .../cardsfolder/g/grothama_all_devouring.txt | 2 +- .../res/cardsfolder/h/hypnotic_cloud.txt | 2 +- .../res/cardsfolder/k/khorvaths_fury.txt | 2 +- forge-gui/res/cardsfolder/p/probe.txt | 2 +- forge-gui/res/cardsfolder/t/tempting_wurm.txt | 2 + 11 files changed, 72 insertions(+), 27 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index e2d3b50c347..16473ed204a 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -17,11 +17,7 @@ */ package forge.ai; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import java.util.*; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -36,6 +32,7 @@ import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CounterEnumType; +import forge.game.combat.AttackingBand; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.Cost; @@ -770,21 +767,52 @@ public class AiBlockController { tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(changesPTWhenBlocked(true))); for (final Card attacker : tramplingAttackers) { + boolean staticAssignCombatDamageAsUnblocked = StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker); + if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size() - || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker) || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { continue; } + boolean needsMoreChumpBlockers = true; + + // See if it's possible to tank up the damage with Banding + List bandsWithString = Arrays.asList("Bands with Other Legendary Creatures", + "Bands with Other Creatures named Wolves of the Hunt", + "Bands with Other Dinosaurs"); + if (AttackingBand.isValidBand(combat.getBlockers(attacker), true)) { + continue; + } + chumpBlockers = getPossibleBlockers(combat, attacker, blockersLeft, false); chumpBlockers.removeAll(combat.getBlockers(attacker)); + + // See if there's a Banding blocker that can tank the damage for (final Card blocker : chumpBlockers) { - // Add an additional blocker if the current blockers are not - // enough and the new one would suck some of the damage - if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker)) - && ComputerUtilCombat.shieldDamage(attacker, blocker) > 0 - && CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) { - combat.addBlocker(attacker, blocker); + if (blocker.hasKeyword(Keyword.BANDING) || blocker.hasAnyKeyword(bandsWithString)) { + if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker)) + && ComputerUtilCombat.shieldDamage(attacker, blocker) > 0 + && CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) { + combat.addBlocker(attacker, blocker); + needsMoreChumpBlockers = false; + break; + } + } + } + + if (staticAssignCombatDamageAsUnblocked) { + continue; + } + + if (needsMoreChumpBlockers) { + for (final Card blocker : chumpBlockers) { + // Add an additional blocker if the current blockers are not + // enough and the new one would suck some of the damage + if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker)) + && ComputerUtilCombat.shieldDamage(attacker, blocker) > 0 + && CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) { + combat.addBlocker(attacker, blocker); + } } } } diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index da837478f35..92385a56eee 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -651,6 +651,21 @@ public class SpecialCardAi { } } + // Grothama, All-Devouring + public static class GrothamaAllDevouring { + public static boolean consider(final Player ai, final SpellAbility sa) { + final Card source = sa.getHostCard(); + final Card devourer = AbilityUtils.getDefinedCards(source, sa.getParam("ExtraDefined"), sa).getFirst(); // maybe just getOriginalHost()? + if (ai.getTeamMates(true).contains(devourer.getController())) { + return false; // TODO: Currently, the AI doesn't ever fight its own (or allied) Grothama for card draw. This can be improved. + } + final Card fighter = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).getFirst(); + boolean goodTradeOrNoTrade = devourer.canBeDestroyed() && (devourer.getNetPower() < fighter.getNetToughness() || !fighter.canBeDestroyed() + || ComputerUtilCard.evaluateCreature(devourer) > ComputerUtilCard.evaluateCreature(fighter)); + return goodTradeOrNoTrade && fighter.getNetPower() >= devourer.getNetToughness(); + } + } + // Guilty Conscience public static class GuiltyConscience { public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List list) { diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java index 4a3765d8111..3d1eb307a79 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -2,11 +2,7 @@ package forge.ai.ability; import java.util.List; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; @@ -32,9 +28,13 @@ public class FightAi extends SpellAbilityAi { sa.resetTargets(); final Card source = sa.getHostCard(); - // everything is defined or targeted above, can't do anything there? + // everything is defined or targeted above, can't do anything there unless a specific logic is set if (sa.hasParam("Defined") && !sa.usesTargeting()) { - // TODO extend Logic for cards like Arena or Grothama + // TODO extend Logic for cards like Arena + if ("Grothama".equals(sa.getParam("AILogic"))) { // Grothama, All-Devouring + return SpecialCardAi.GrothamaAllDevouring.consider(ai, sa); + } + return true; } @@ -120,7 +120,7 @@ public class FightAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - if (canPlayAI(ai, sa)) { + if (checkApiLogic(ai, sa)) { return true; } if (!mandatory) { diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 4e4625340af..fbfe4c1a214 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -799,7 +799,7 @@ public class Player extends GameEntity implements Comparable { restDamage = 2; } } else if (c.getName().equals("Elderscale Wurm")) { - if (c.getController().equals(this) && getLife() - restDamage < 7) { + if (c.getController().equals(this) && getLife() >= 7 && getLife() - restDamage < 7) { restDamage = getLife() - 7; if (restDamage < 0) { restDamage = 0; diff --git a/forge-gui/res/cardsfolder/b/bog_down.txt b/forge-gui/res/cardsfolder/b/bog_down.txt index 3d5397ba72e..c4d6825b58f 100644 --- a/forge-gui/res/cardsfolder/b/bog_down.txt +++ b/forge-gui/res/cardsfolder/b/bog_down.txt @@ -5,6 +5,6 @@ K:Kicker:Sac<2/Land> A:SP$ Discard | Cost$ 2 B | ValidTgts$ Player | TgtPrompt$ Choose a player | NumCards$ WasKicked | Mode$ TgtChoose | SpellDescription$ Target player discards two cards. If CARDNAME was kicked, that player discards three cards instead. SVar:WasKicked:Count$Kicked.3.2 SVar:NeedsToPlayKickedVar:Z GE3 -SVar:Z:Count$ValidHand Card.OppCtrl +SVar:Z:Count$ValidHand Card.OppOwn SVar:AIPreference:SacCost$Land.basic+YouCtrl Oracle:Kicker—Sacrifice two lands. (You may sacrifice two lands in addition to any other costs as you cast this spell.)\nTarget player discards two cards. If this spell was kicked, that player discards three cards instead. diff --git a/forge-gui/res/cardsfolder/c/caligo_skin_witch.txt b/forge-gui/res/cardsfolder/c/caligo_skin_witch.txt index 09282fc143a..a666238bd2d 100644 --- a/forge-gui/res/cardsfolder/c/caligo_skin_witch.txt +++ b/forge-gui/res/cardsfolder/c/caligo_skin_witch.txt @@ -6,5 +6,5 @@ K:Kicker:3 B T:Mode$ ChangesZone | ValidCard$ Card.Self+kicked | Origin$ Any | Destination$ Battlefield | Execute$ TrigDiscard | TriggerDescription$ When CARDNAME enters the battlefield, if it was kicked, each opponent discards two cards. SVar:TrigDiscard:DB$ Discard | Defined$ Player.Opponent | NumCards$ 2 | Mode$ TgtChoose SVar:NeedsToPlayKickedVar:Z GE1 -SVar:Z:Count$ValidHand Card.OppCtrl +SVar:Z:Count$ValidHand Card.OppOwn Oracle:Kicker {3}{B} (You may pay an additional {3}{B} as you cast this spell.)\nWhen Caligo Skin-Witch enters the battlefield, if it was kicked, each opponent discards two cards. diff --git a/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt b/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt index d87de3f1dd7..c4d0d4ff244 100644 --- a/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt +++ b/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt @@ -4,7 +4,7 @@ Types:Legendary Creature Wurm PT:10/8 S:Mode$ Continuous | Affected$ Creature.Other | AddTrigger$ GrothamaAttack | AddSVar$ HasAttackEffect | Description$ Other creatures have "Whenever this creature attacks, you may have it fight CARDNAME." SVar:GrothamaAttack:Mode$ Attacks | ValidCard$ Card.Self | Execute$ GrothamaFight | OptionalDecider$ You | TriggerDescription$ Whenever this creature attacks, ABILITY. -SVar:GrothamaFight:DB$ Fight | Defined$ TriggeredAttackerLKICopy | ExtraDefined$ OriginalHost | SpellDescription$ You may have it fight ORIGINALHOST +SVar:GrothamaFight:DB$ Fight | Defined$ TriggeredAttackerLKICopy | ExtraDefined$ OriginalHost | AILogic$ Grothama | SpellDescription$ You may have it fight ORIGINALHOST T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ TrigRepeat | TriggerDescription$ When NICKNAME leaves the battlefield, each player draws cards equal to the amount of damage dealt to NICKNAME this turn by sources they controlled. SVar:TrigRepeat:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ TrigDraw SVar:TrigDraw:DB$ Draw | Defined$ Remembered | NumCards$ X diff --git a/forge-gui/res/cardsfolder/h/hypnotic_cloud.txt b/forge-gui/res/cardsfolder/h/hypnotic_cloud.txt index 9b698fef80b..bb93203d2c8 100644 --- a/forge-gui/res/cardsfolder/h/hypnotic_cloud.txt +++ b/forge-gui/res/cardsfolder/h/hypnotic_cloud.txt @@ -5,5 +5,5 @@ K:Kicker:4 A:SP$ Discard | Cost$ 1 B | NumCards$ X | ValidTgts$ Player | TgtPrompt$ Select target player | Mode$ TgtChoose | SpellDescription$ Target player discards a card. If this spell was kicked, that player discards three cards instead. SVar:X:Count$Kicked.3.1 SVar:NeedsToPlayKickedVar:Z GE2 -SVar:Z:Count$ValidHand Card.OppCtrl +SVar:Z:Count$ValidHand Card.OppOwn Oracle:Kicker {4} (You may pay an additional {4} as you cast this spell.)\nTarget player discards a card. If this spell was kicked, that player discards three cards instead. diff --git a/forge-gui/res/cardsfolder/k/khorvaths_fury.txt b/forge-gui/res/cardsfolder/k/khorvaths_fury.txt index edd45208b81..9cd69569c62 100644 --- a/forge-gui/res/cardsfolder/k/khorvaths_fury.txt +++ b/forge-gui/res/cardsfolder/k/khorvaths_fury.txt @@ -11,5 +11,5 @@ SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:X:Remembered$Amount/Plus.1 SVar:Y:Count$ValidHand Card.RememberedPlayerCtrl SVar:NeedsToPlayVar:Z GE4 -SVar:Z:Count$ValidHand Card.OppCtrl +SVar:Z:Count$ValidHand Card.OppOwn Oracle:For each player, choose friend or foe. Each friend discards all cards from their hand, then draws that many cards plus one. Khorvath's Fury deals damage to each foe equal to the number of cards in their hand. diff --git a/forge-gui/res/cardsfolder/p/probe.txt b/forge-gui/res/cardsfolder/p/probe.txt index c2c476a246a..e9c1361441e 100644 --- a/forge-gui/res/cardsfolder/p/probe.txt +++ b/forge-gui/res/cardsfolder/p/probe.txt @@ -7,5 +7,5 @@ SVar:DBDiscardYou:DB$ Discard | Defined$ You | NumCards$ 2 | SubAbility$ DBDisca SVar:DBDiscardTarget:DB$ Discard | Condition$ Kicked | ValidTgts$ Player | TgtPrompt$ Select target player | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ If CARDNAME was kicked, target player discards two cards. DeckHints:Color$Black SVar:NeedsToPlayKickedVar:Z GE1 -SVar:Z:Count$ValidHand Card.OppCtrl +SVar:Z:Count$ValidHand Card.OppOwn Oracle:Kicker {1}{B} (You may pay an additional {1}{B} as you cast this spell.)\nDraw three cards, then discard two cards. If this spell was kicked, target player discards two cards. diff --git a/forge-gui/res/cardsfolder/t/tempting_wurm.txt b/forge-gui/res/cardsfolder/t/tempting_wurm.txt index 79e81dfb952..0d21c78f7df 100644 --- a/forge-gui/res/cardsfolder/t/tempting_wurm.txt +++ b/forge-gui/res/cardsfolder/t/tempting_wurm.txt @@ -6,4 +6,6 @@ T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.S SVar:EachOpponent:DB$ RepeatEach | RepeatPlayers$ Player.Opponent | RepeatSubAbility$ TemptingChange SVar:TemptingChange:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | ChangeType$ Artifact,Creature,Enchantment,Land | DefinedPlayer$ Remembered | ChangeNum$ X SVar:X:Count$ValidHand Artifact.RememberedPlayerCtrl,Creature.RememberedPlayerCtrl,Enchantment.RememberedPlayerCtrl,Land.RememberedPlayerCtrl +SVar:NeedsToPlayVar:Y LE2 +SVar:Y:Count$ValidHand Card.OppOwn Oracle:When Tempting Wurm enters the battlefield, each opponent may put any number of artifact, creature, enchantment, and/or land cards from their hand onto the battlefield.