From efc3c2c1596e973bb5ff4cfddf88dfc2eeb1cb03 Mon Sep 17 00:00:00 2001 From: friarsol Date: Tue, 8 Aug 2023 21:09:57 -0400 Subject: [PATCH] Add ai usage for Maze of Ith --- .../java/forge/ai/ComputerUtilCombat.java | 58 ++++++++++++++++++- .../main/java/forge/ai/ability/UntapAi.java | 58 +++++++++++++++++++ forge-gui/res/cardsfolder/m/maze_of_ith.txt | 4 +- .../res/cardsfolder/m/maze_of_shadows.txt | 4 +- .../res/cardsfolder/r/riftstone_portal.txt | 2 + 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index 4b045ddba84..6332a55df68 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -223,7 +223,7 @@ public class ComputerUtilCombat { int damage = attacker.getNetCombatDamage(); int poison = 0; damage += predictPowerBonusOfAttacker(attacker, null, null, false); - if (attacker.hasKeyword(Keyword.INFECT)) { + if (attacker.hasKeyword(Keyword.INFECT) || attacker.hasKeyword(Keyword.TOXIC)) { int pd = predictDamageTo(attacked, damage, attacker, true); // opponent can always order it so that he gets 0 if (pd == 1 && Iterables.any(attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) { @@ -2390,6 +2390,62 @@ public class ComputerUtilCombat { return categorizedAttackers; } + public static Card mostDangerousAttacker(CardCollection list, Player ai, Combat combat, boolean withAbilities) { + Card damageCard = null; + Card poisonCard = null; + + int damageScore = 0; + int poisonScore = 0; + + + for(Card c : list) { + int estimatedDmg = damageIfUnblocked(c, ai, combat, withAbilities); + int estimatedPoison = poisonIfUnblocked(c, ai); + + if (combat.isBlocked(c)) { + if (!c.hasKeyword(Keyword.TRAMPLE)) { + continue; + } + + int absorbedByToughness = 0; + for (Card blocker : combat.getBlockers(c)) { + absorbedByToughness += blocker.getNetToughness(); + } + estimatedPoison -= absorbedByToughness; + estimatedDmg -= absorbedByToughness; + } + + if (estimatedDmg > damageScore) { + damageScore = estimatedDmg; + damageCard = c; + } + + if (estimatedPoison > poisonScore) { + poisonScore = estimatedPoison; + poisonCard = c; + } + } + + if (damageCard == null && poisonCard == null) { + return null; + } else if (damageCard == null) { + return poisonCard; + } else if (poisonCard == null) { + return damageCard; + } + + int life = ai.getLife(); + int poisonLife = 10 - ai.getPoisonCounters(); + double percentLife = life * 1.0 / damageScore; + double percentPoison = poisonLife * 1.0 / poisonScore; + + if (percentLife >= percentPoison) { + return damageCard; + } else { + return poisonCard; + } + } + public static Card applyPotentialAttackCloneTriggers(Card attacker) { // This method returns the potentially cloned card if the creature turns into something else during the attack // (currently looks for the creature with maximum raw power since that's what the AI usually judges by when diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java index 0e301fcf322..00b6ecf2ec5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -13,6 +13,7 @@ import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardPredicates.Presets; +import forge.game.combat.Combat; import forge.game.cost.Cost; import forge.game.cost.CostTap; import forge.game.mana.ManaCostBeingPaid; @@ -37,6 +38,10 @@ public class UntapAi extends SpellAbilityAi { return false; } else if ("PoolExtraMana".equals(aiLogic)) { return doPoolExtraManaLogic(ai, sa); + } else if ("PreventCombatDamage".equals(aiLogic)) { + return doPreventCombatDamageLogic(ai, sa); + // In the future if you want to give Pseudo vigilance to a creature you attacked with + // activate during your own during the end of combat step } return !("Never".equals(aiLogic)); @@ -63,6 +68,8 @@ public class UntapAi extends SpellAbilityAi { final List pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai); } else { + // If we already selected a target just use that + return untapPrefTargeting(ai, sa, false); } } @@ -118,6 +125,13 @@ public class UntapAi extends SpellAbilityAi { */ private static boolean untapPrefTargeting(final Player ai, final SpellAbility sa, final boolean mandatory) { final Card source = sa.getHostCard(); + + if (alreadyAssignedTarget(sa)) { + if (sa.getTargets().size() > 0) { + // If we selected something lets assume its valid + return true; + } + } sa.resetTargets(); final PlayerCollection targetController = new PlayerCollection(); @@ -337,6 +351,50 @@ public class UntapAi extends SpellAbilityAi { return null; } + private boolean doPreventCombatDamageLogic(final Player ai, final SpellAbility sa) { + // Only Maze of Ith and Maze of Shadows uses this. Feel free to use it aggressively. + Game game = ai.getGame(); + Card source = sa.getHostCard(); + sa.resetTargets(); + + if (!game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) { + return false; + } + + // If damage can't be prevented. Just return false. + + Combat activeCombat = game.getCombat(); + if (activeCombat == null) { + return false; + } + + CardCollection list = CardLists.getTargetableCards(activeCombat.getAttackers(), sa); + + if (list.isEmpty()) { + return false; + } + + if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + // Blockers already set. Are there any dangerous unblocked creatures? Sort by creature that will deal the most damage? + Card card = ComputerUtilCombat.mostDangerousAttacker(list, ai, activeCombat, true); + + if (card == null) { return false; } + + sa.getTargets().add(card); + return true; + } + + return false; + } + + private static boolean alreadyAssignedTarget(final SpellAbility sa) { + if (sa.hasParam("AILogic")) { + String aiLogic = sa.getParam("AILogic"); + return "PreventCombatDamage".equals(aiLogic); + } + return false; + } + private boolean doPoolExtraManaLogic(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); final PhaseHandler ph = source.getGame().getPhaseHandler(); diff --git a/forge-gui/res/cardsfolder/m/maze_of_ith.txt b/forge-gui/res/cardsfolder/m/maze_of_ith.txt index c766ff9b417..5f5f165a42f 100644 --- a/forge-gui/res/cardsfolder/m/maze_of_ith.txt +++ b/forge-gui/res/cardsfolder/m/maze_of_ith.txt @@ -1,9 +1,9 @@ Name:Maze of Ith ManaCost:no cost Types:Land -A:AB$ Untap | Cost$ T | ValidTgts$ Creature.attacking | TgtPrompt$ Select target attacking creature | SubAbility$ DBPump | SpellDescription$ Untap target attacking creature. +A:AB$ Untap | Cost$ T | ValidTgts$ Creature.attacking | TgtPrompt$ Select target attacking creature | AILogic$ PreventCombatDamage | SubAbility$ DBPump | SpellDescription$ Untap target attacking creature. SVar:DBPump:DB$ Effect | ReplacementEffects$ RPrevent1,RPrevent2 | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | SpellDescription$ Prevent all combat damage that would be dealt to and dealt by that creature this turn. SVar:RPrevent1:Event$ DamageDone | Prevent$ True | IsCombat$ True | ValidSource$ Card.IsRemembered | Description$ Prevent all combat damage that would be dealt to and dealt by that creature this turn. SVar:RPrevent2:Event$ DamageDone | Prevent$ True | IsCombat$ True | ValidTarget$ Card.IsRemembered | Description$ Prevent all combat damage that would be dealt to and dealt by that creature this turn. | Secondary$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:{T}: Untap target attacking creature. Prevent all combat damage that would be dealt to and dealt by that creature this turn. diff --git a/forge-gui/res/cardsfolder/m/maze_of_shadows.txt b/forge-gui/res/cardsfolder/m/maze_of_shadows.txt index 21e2516e8ab..38ae92f87c7 100644 --- a/forge-gui/res/cardsfolder/m/maze_of_shadows.txt +++ b/forge-gui/res/cardsfolder/m/maze_of_shadows.txt @@ -2,9 +2,9 @@ Name:Maze of Shadows ManaCost:no cost Types:Land A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}. -A:AB$ Untap | Cost$ T | ValidTgts$ Creature.attacking+withShadow | TgtPrompt$ Select target attacking creature with shadow | SubAbility$ DBPump | SpellDescription$ Untap target attacking creature with shadow. +A:AB$ Untap | Cost$ T | ValidTgts$ Creature.attacking+withShadow | TgtPrompt$ Select target attacking creature with shadow | AILogic$ PreventCombatDamage | SubAbility$ DBPump | SpellDescription$ Untap target attacking creature with shadow. SVar:DBPump:DB$ Effect | ReplacementEffects$ RPrevent1,RPrevent2 | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | SpellDescription$ Prevent all combat damage that would be dealt to and dealt by that creature this turn. SVar:RPrevent1:Event$ DamageDone | Prevent$ True | IsCombat$ True | ValidSource$ Card.IsRemembered | Description$ Prevent all combat damage that would be dealt to and dealt by that creature this turn. SVar:RPrevent2:Event$ DamageDone | Prevent$ True | IsCombat$ True | ValidTarget$ Card.IsRemembered | Description$ Prevent all combat damage that would be dealt to and dealt by that creature this turn. | Secondary$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:{T}: Add {C}.\n{T}: Untap target attacking creature with shadow. Prevent all combat damage that would be dealt to and dealt by that creature this turn. diff --git a/forge-gui/res/cardsfolder/r/riftstone_portal.txt b/forge-gui/res/cardsfolder/r/riftstone_portal.txt index 161e2c9c4e3..d970365c647 100644 --- a/forge-gui/res/cardsfolder/r/riftstone_portal.txt +++ b/forge-gui/res/cardsfolder/r/riftstone_portal.txt @@ -4,4 +4,6 @@ Types:Land A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}. S:Mode$ Continuous | EffectZone$ Graveyard | Affected$ Land.YouCtrl | AddAbility$ AddMana | Description$ As long as Riftstone Portal is in your graveyard, lands you control have "{T}: Add {G} or {W}." SVar:AddMana:AB$ Mana | Cost$ T | Produced$ Combo G W | SpellDescription$ Add {G} or {W}. +SVar:SacMe:2 +SVar:DiscardMe:1 Oracle:{T}: Add {C}.\nAs long as Riftstone Portal is in your graveyard, lands you control have "{T}: Add {G} or {W}."