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
This commit is contained in:
Agetian
2023-07-06 18:01:59 +03:00
committed by GitHub
parent bfd93368be
commit dbc2a5b5eb
11 changed files with 72 additions and 27 deletions

View File

@@ -17,11 +17,7 @@
*/ */
package forge.ai; package forge.ai;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
@@ -36,6 +32,7 @@ import forge.game.card.CardCollectionView;
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.card.CounterEnumType;
import forge.game.combat.AttackingBand;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -770,21 +767,52 @@ public class AiBlockController {
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(changesPTWhenBlocked(true))); tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(changesPTWhenBlocked(true)));
for (final Card attacker : tramplingAttackers) { for (final Card attacker : tramplingAttackers) {
boolean staticAssignCombatDamageAsUnblocked = StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker);
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size() 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.")) { || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue; continue;
} }
boolean needsMoreChumpBlockers = true;
// See if it's possible to tank up the damage with Banding
List<String> 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 = getPossibleBlockers(combat, attacker, blockersLeft, false);
chumpBlockers.removeAll(combat.getBlockers(attacker)); chumpBlockers.removeAll(combat.getBlockers(attacker));
// See if there's a Banding blocker that can tank the damage
for (final Card blocker : chumpBlockers) { for (final Card blocker : chumpBlockers) {
// Add an additional blocker if the current blockers are not if (blocker.hasKeyword(Keyword.BANDING) || blocker.hasAnyKeyword(bandsWithString)) {
// enough and the new one would suck some of the damage if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker))
if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker)) && ComputerUtilCombat.shieldDamage(attacker, blocker) > 0
&& ComputerUtilCombat.shieldDamage(attacker, blocker) > 0 && CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) {
&& CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) { combat.addBlocker(attacker, blocker);
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);
}
} }
} }
} }

View File

@@ -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 // Guilty Conscience
public static class GuiltyConscience { public static class GuiltyConscience {
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) { public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {

View File

@@ -2,11 +2,7 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import forge.ai.ComputerUtil; import forge.ai.*;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
@@ -32,9 +28,13 @@ public class FightAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
final Card source = sa.getHostCard(); 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()) { 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; return true;
} }
@@ -120,7 +120,7 @@ public class FightAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (canPlayAI(ai, sa)) { if (checkApiLogic(ai, sa)) {
return true; return true;
} }
if (!mandatory) { if (!mandatory) {

View File

@@ -799,7 +799,7 @@ public class Player extends GameEntity implements Comparable<Player> {
restDamage = 2; restDamage = 2;
} }
} else if (c.getName().equals("Elderscale Wurm")) { } 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; restDamage = getLife() - 7;
if (restDamage < 0) { if (restDamage < 0) {
restDamage = 0; restDamage = 0;

View File

@@ -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. 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:WasKicked:Count$Kicked.3.2
SVar:NeedsToPlayKickedVar:Z GE3 SVar:NeedsToPlayKickedVar:Z GE3
SVar:Z:Count$ValidHand Card.OppCtrl SVar:Z:Count$ValidHand Card.OppOwn
SVar:AIPreference:SacCost$Land.basic+YouCtrl 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. 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.

View File

@@ -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. 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:TrigDiscard:DB$ Discard | Defined$ Player.Opponent | NumCards$ 2 | Mode$ TgtChoose
SVar:NeedsToPlayKickedVar:Z GE1 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. 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.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Wurm
PT:10/8 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." 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: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. 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:TrigRepeat:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ TrigDraw
SVar:TrigDraw:DB$ Draw | Defined$ Remembered | NumCards$ X SVar:TrigDraw:DB$ Draw | Defined$ Remembered | NumCards$ X

View File

@@ -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. 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:X:Count$Kicked.3.1
SVar:NeedsToPlayKickedVar:Z GE2 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. 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.

View File

@@ -11,5 +11,5 @@ SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Remembered$Amount/Plus.1 SVar:X:Remembered$Amount/Plus.1
SVar:Y:Count$ValidHand Card.RememberedPlayerCtrl SVar:Y:Count$ValidHand Card.RememberedPlayerCtrl
SVar:NeedsToPlayVar:Z GE4 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. 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.

View File

@@ -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. 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 DeckHints:Color$Black
SVar:NeedsToPlayKickedVar:Z GE1 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. 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.

View File

@@ -6,4 +6,6 @@ T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.S
SVar:EachOpponent:DB$ RepeatEach | RepeatPlayers$ Player.Opponent | RepeatSubAbility$ TemptingChange 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: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: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. 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.