From 12d9a5e84bd042eb1e52c6086a5dddb0f5c2a64c Mon Sep 17 00:00:00 2001 From: jendave Date: Sun, 7 Aug 2011 00:43:01 +0000 Subject: [PATCH] - The AI will now anticipate regeneration abilities. - Improved the AI of curse pump etb abilities. - Improved the AI of untap triggers. --- .gitattributes | 2 +- res/cardsfolder/blister_beetle.txt | 22 +- res/cardsfolder/bringer_of_the_black_dawn.txt | 1 + res/cardsfolder/keening_banshee.txt | 2 +- res/cardsfolder/lignify.txt | 2 +- src/forge/CombatUtil.java | 44 ++- src/forge/ComputerUtil.java | 257 ++++++++++++++++++ src/forge/ComputerUtil_Attack2.java | 4 +- src/forge/ComputerUtil_Block2.java | 4 +- src/forge/RunTest.java | 2 +- .../AbilityFactory_PermanentState.java | 4 + .../abilityFactory/AbilityFactory_Pump.java | 4 +- 12 files changed, 315 insertions(+), 33 deletions(-) diff --git a/.gitattributes b/.gitattributes index ab0b51d4f0a..d6943633a2a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -791,7 +791,7 @@ res/cardsfolder/blinking_spirit.txt -text svneol=native#text/plain res/cardsfolder/blinkmoth_infusion.txt -text svneol=native#text/plain res/cardsfolder/blinkmoth_nexus.txt -text svneol=native#text/plain res/cardsfolder/blinkmoth_well.txt -text svneol=native#text/plain -res/cardsfolder/blister_beetle.txt svneol=native#text/plain +res/cardsfolder/blister_beetle.txt -text svneol=native#text/plain res/cardsfolder/blistergrub.txt -text svneol=native#text/plain res/cardsfolder/blistering_barrier.txt -text svneol=native#text/plain res/cardsfolder/blistering_dieflyn.txt -text svneol=native#text/plain diff --git a/res/cardsfolder/blister_beetle.txt b/res/cardsfolder/blister_beetle.txt index ba24884c743..e5a62afdfeb 100644 --- a/res/cardsfolder/blister_beetle.txt +++ b/res/cardsfolder/blister_beetle.txt @@ -1,12 +1,12 @@ -Name:Blister Beetle -ManaCost:1 B -Types:Creature Insect -Text:no text -PT:1/1 -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPump | TriggerDescription$ When CARDNAME enters the battlefield, target creature gets -1/-1 until end of turn. -SVar:TrigPump:AB$Pump | Cost$ 0 | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ -1 | NumDef$ -1 | IsCurse$ True -SVar:PlayMain1:TRUE -SVar:Rarity:Common -SVar:Picture:http://www.wizards.com/global/images/magic/general/blister_beetle.jpg -SetInfo:ALA|Common|http://magiccards.info/scans/en/ala/66.jpg +Name:Blister Beetle +ManaCost:1 B +Types:Creature Insect +Text:no text +PT:1/1 +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPump | TriggerDescription$ When CARDNAME enters the battlefield, target creature gets -1/-1 until end of turn. +SVar:TrigPump:DB$Pump | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ -1 | NumDef$ -1 | IsCurse$ True +SVar:PlayMain1:TRUE +SVar:Rarity:Common +SVar:Picture:http://www.wizards.com/global/images/magic/general/blister_beetle.jpg +SetInfo:ALA|Common|http://magiccards.info/scans/en/ala/66.jpg End \ No newline at end of file diff --git a/res/cardsfolder/bringer_of_the_black_dawn.txt b/res/cardsfolder/bringer_of_the_black_dawn.txt index bc8897ab193..dde10e8e32a 100644 --- a/res/cardsfolder/bringer_of_the_black_dawn.txt +++ b/res/cardsfolder/bringer_of_the_black_dawn.txt @@ -7,6 +7,7 @@ K:Trample SVar:AltCost:W U B R G T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigChange | TriggerDescription$ At the beginning of your upkeep, you may pay 2 life. If you do, search your library for a card, then shuffle your library and put that card on top of it. SVar:TrigChange:AB$ChangeZone | Cost$ PayLife<2> | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Card | ChangeNum$ 1 +SVar:RemAIDeck:True SVar:Rarity:Rare SVar:Picture:http://www.wizards.com/global/images/magic/general/bringer_of_the_black_dawn.jpg SetInfo:5DN|Rare|http://magiccards.info/scans/en/5dn/43.jpg diff --git a/res/cardsfolder/keening_banshee.txt b/res/cardsfolder/keening_banshee.txt index fe917f1a213..0cdab306947 100644 --- a/res/cardsfolder/keening_banshee.txt +++ b/res/cardsfolder/keening_banshee.txt @@ -4,7 +4,7 @@ Types:Creature Spirit Text:no text PT:2/2 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPumpCurse | TriggerDescription$ When CARDNAME enters the battlefield, target creature gets -2/-2 until end of turn. -SVar:TrigPumpCurse:AB$Pump | Cost$ 0 | ValidTgts$ Creature | TgtPrompt$ Select target creature | IsCurse$ True | NumAtt$ -2 | NumDef$ -2 +SVar:TrigPumpCurse:DB$Pump| ValidTgts$ Creature | TgtPrompt$ Select target creature | IsCurse$ True | NumAtt$ -2 | NumDef$ -2 K:Flying SVar:PlayMain1:TRUE SVar:Rarity:Uncommon diff --git a/res/cardsfolder/lignify.txt b/res/cardsfolder/lignify.txt index 11469de2c84..3f0d0812cbe 100644 --- a/res/cardsfolder/lignify.txt +++ b/res/cardsfolder/lignify.txt @@ -4,7 +4,7 @@ Types:Tribal Enchantment Treefolk Aura Text:no text K:Enchant Creature Curse K:stAnimateEnchanted:Creature:0/4/Overwrite:Creature,Treefolk,Overwrite:no colors:Overwrite:No condition:Enchanted creature is a 0/4 Treefolk with no abilities. -SVar:RemRandomDeck:True +SVar:RemAIDeck:True SVar:Rarity:Common SVar:Picture:http://www.wizards.com/global/images/magic/general/lignify.jpg SetInfo:LRW|Common|http://magiccards.info/scans/en/lw/228.jpg diff --git a/src/forge/CombatUtil.java b/src/forge/CombatUtil.java index 6169ef782f1..b770eb48afc 100644 --- a/src/forge/CombatUtil.java +++ b/src/forge/CombatUtil.java @@ -2,6 +2,8 @@ package forge; +import static forge.error.ErrorViewer.showError; + import java.util.ArrayList; import java.util.HashMap; import java.util.regex.Matcher; @@ -12,6 +14,7 @@ import com.esotericsoftware.minlog.Log; import forge.card.abilityFactory.AbilityFactory; import forge.card.cardFactory.CardFactoryUtil; import forge.card.spellability.Ability; +import forge.card.spellability.SpellAbility; import forge.card.trigger.Trigger; import forge.gui.GuiUtils; import forge.gui.input.Input_PayManaCost_Ability; @@ -778,7 +781,7 @@ public class CombatUtil { // This calculates the amount of damage a blocker in a blockgang can take from the attacker (for trampling attackers) public static int shieldDamage(Card attacker, Card defender) { - if (!canDestroyBlocker(defender,attacker, null)) return 100; + if (!canDestroyBlocker(defender,attacker, null, false)) return 100; int flankingMagnitude = 0; if(attacker.getKeyword().contains("Flanking") && !defender.getKeyword().contains("Flanking")) { @@ -813,7 +816,7 @@ public class CombatUtil { CardList blockers = AllZone.Combat.getBlockers(attacker); for (Card defender:blockers) { - if(CombatUtil.canDestroyAttacker(attacker, defender, AllZone.Combat) && + if(CombatUtil.canDestroyAttacker(attacker, defender, AllZone.Combat, true) && !(defender.getKeyword().contains("Wither") || defender.getKeyword().contains("Infect"))) return true; } @@ -1061,8 +1064,30 @@ public class CombatUtil { return toughness; } + public static boolean canRegenerate(Card card) { + Player controller = card.getController(); + CardList l = AllZoneUtil.getCardsInPlay(); + for(Card c:l) + for(SpellAbility sa:c.getSpellAbility()) + // if SA is from AF_Counter don't add to getPlayable + //This try/catch should fix the "computer is thinking" bug + try { + if(sa.canPlay() && ComputerUtil.canPayCost(sa,controller) && sa.getAbilityFactory() != null && sa.isAbility()){ + AbilityFactory af = sa.getAbilityFactory(); + HashMap mapParams = af.getMapParams(); + if (mapParams.get("AB").equals("Regenerate")) + if (AbilityFactory.getDefinedCards(sa.getSourceCard(), mapParams.get("Defined"), sa).contains(card)) + return true; + } + } catch(Exception ex) { + showError(ex, "There is an error in the card code for %s:%n", c.getName(), ex.getMessage()); + } + + return false; + } + //can the blocker destroy the attacker? - public static boolean canDestroyAttacker(Card attacker, Card defender, Combat combat) { + public static boolean canDestroyAttacker(Card attacker, Card defender, Combat combat, boolean noRegen) { if(attacker.getName().equals("Sylvan Basilisk") && !defender.getKeyword().contains("Indestructible")) return false; @@ -1077,12 +1102,9 @@ public class CombatUtil { }//flanking - if(attacker.getKeyword().contains("Indestructible") && + if((attacker.getKeyword().contains("Indestructible") || (canRegenerate(attacker) && !noRegen)) && !(defender.getKeyword().contains("Wither") || defender.getKeyword().contains("Infect"))) return false; - //unused - //int attBushidoMagnitude = attacker.getKeywordMagnitude("Bushido"); - int defenderDamage = defender.getNetAttack() + predictPowerBonusOfBlocker(attacker, defender); int attackerDamage = attacker.getNetAttack() + predictPowerBonusOfAttacker(attacker, defender, combat); if (AllZoneUtil.isCardInPlay("Doran, the Siege Tower")) { @@ -1133,14 +1155,14 @@ public class CombatUtil { public static boolean blockerWouldBeDestroyed(Card blocker) { Card attacker = AllZone.Combat.getAttackerBlockedBy(blocker); - if(canDestroyBlocker(blocker, attacker, AllZone.Combat) && + if(canDestroyBlocker(blocker, attacker, AllZone.Combat, true) && !(attacker.getKeyword().contains("Wither") || attacker.getKeyword().contains("Infect"))) return true; return false; } //can the attacker destroy this blocker? - public static boolean canDestroyBlocker(Card defender, Card attacker, Combat combat) { + public static boolean canDestroyBlocker(Card defender, Card attacker, Combat combat, boolean noRegen) { int flankingMagnitude = 0; if(attacker.getKeyword().contains("Flanking") && !defender.getKeyword().contains("Flanking")) { @@ -1151,7 +1173,7 @@ public class CombatUtil { if((flankingMagnitude >= defender.getKillDamage()) && !defender.getKeyword().contains("Indestructible")) return true; }//flanking - if(defender.getKeyword().contains("Indestructible") && + if((defender.getKeyword().contains("Indestructible") || (canRegenerate(defender) && !noRegen)) && !(attacker.getKeyword().contains("Wither") || attacker.getKeyword().contains("Infect"))) return false; if(attacker.getName().equals("Sylvan Basilisk") && !defender.getKeyword().contains("Indestructible")) return true; @@ -1190,7 +1212,7 @@ public class CombatUtil { && !attacker.getKeyword().contains("Indestructible") && !attacker.getKeyword().contains("First Strike")) { if(defenderDamage >= attackerLife) return false; - if(defenderDamage > 0 && defender.getKeyword().contains("Deathtouch") ) return false; + if(defenderDamage > 0 && defender.getKeyword().contains("Deathtouch")) return false; } if(attacker.getKeyword().contains("Deathtouch") && attackerDamage > 0) return true; diff --git a/src/forge/ComputerUtil.java b/src/forge/ComputerUtil.java index d31d49051b0..0889d4e4027 100644 --- a/src/forge/ComputerUtil.java +++ b/src/forge/ComputerUtil.java @@ -357,6 +357,60 @@ public class ComputerUtil return false; }//canPayCost() + static public boolean canPayCost(SpellAbility sa, Player player) + { + Card card = sa.getSourceCard(); + + CardList land = getAvailableMana(player); + + if(card.isLand() && sa.getPayCosts().getTap()) + { + land.remove(card); + } + // Beached - Delete old + String mana = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : sa.getManaCost(); + + ManaCost cost = new ManaCost(mana); + + // Tack xMana Payments into mana here if X is a set value + if (sa.getPayCosts() != null && cost.getXcounter() > 0){ + String xSvar = card.getSVar("X").equals("Count$xPaid") ? "PayX" : "X"; + // For Count$xPaid set PayX in the AFs then use that here + // Else calculate it as appropriate. + if (!card.getSVar(xSvar).equals("")){ + int manaToAdd = AbilityFactory.calculateAmount(card, xSvar, sa) * cost.getXcounter(); + cost.increaseColorlessMana(manaToAdd); + } + } + + cost = AllZone.GameAction.getSpellCostChange(sa, cost); + if(cost.isPaid()) + return canPayAdditionalCosts(sa, player); + // Beached - Delete old + ArrayList colors; + + for(int i = 0; i < land.size(); i++) + { + colors = getColors(land.get(i)); + int once = 0; + + for(int j =0; j < colors.size(); j++) + { + if(cost.isNeeded(colors.get(j)) && once == 0) + { + cost.payMana(colors.get(j)); + once++; + } + + if(cost.isPaid()) { + return canPayAdditionalCosts(sa, player); + } + } + } + return false; + }//canPayCost() + + static public int determineLeftoverMana(SpellAbility sa){ // This function should mostly be called to determine how much mana AI has leftover to pay X costs // This function is basically getAvailableMana.size() - sa.getConvertedManaCost() @@ -570,6 +624,174 @@ public class ComputerUtil return true; } + static public boolean canPayAdditionalCosts(SpellAbility sa, Player player) + { + // Add additional cost checks here before attempting to activate abilities + Cost cost = sa.getPayCosts(); + if (cost == null) + return true; + Card card = sa.getSourceCard(); + + if (cost.getTap() && (card.isTapped() || card.isSick())) + return false; + + if (cost.getUntap() && (card.isUntapped() || card.isSick())) + return false; + + if (cost.getTapXTypeCost()) + { + CardList typeList = AllZoneUtil.getPlayerCardsInPlay(player); + typeList = typeList.getValidCards(cost.getTapXType().split(","),sa.getActivatingPlayer() ,sa.getSourceCard()); + + if (cost.getTap()) + typeList.remove(sa.getSourceCard()); + typeList = typeList.filter(AllZoneUtil.untapped); + + if (cost.getTapXTypeAmount() > typeList.size()) + return false; + } + + if (cost.getSubCounter()){ + Counters c = cost.getCounterType(); + if (card.getCounters(c) - cost.getCounterNum() < 0 || !AllZoneUtil.isCardInPlay(card)){ + return false; + } + } + + if (cost.getAddCounter()){ + // this should always be true + } + + if (cost.getLifeCost()){ + if (player.getLife() <= cost.getLifeAmount()) + return false; + } + + if (cost.getDiscardCost()){ + CardList handList = AllZoneUtil.getPlayerHand(player); + String discType = cost.getDiscardType(); + int discAmount = cost.getDiscardAmount(); + + if (cost.getDiscardThis()){ + if (!AllZone.getZone(card).getZoneName().equals(Constant.Zone.Hand)) + return false; + } + else if( discType.equals("LastDrawn")) { + //compy can't yet use this effectively + return false; + } + else if (discType.equals("Hand")){ + // this will always work + } + else{ + if (!discType.equals("Any") && !discType.equals("Random")){ + String validType[] = discType.split(","); + handList = handList.getValidCards(validType, sa.getActivatingPlayer(), sa.getSourceCard()); + } + if (discAmount > handList.size()){ + // not enough cards in hand to pay + return false; + } + } + } + + if (cost.getSacCost()){ + // if there's a sacrifice in the cost, just because we can Pay it doesn't mean we want to. + if (!cost.getSacThis()){ + CardList typeList = AllZoneUtil.getPlayerCardsInPlay(player); + typeList = typeList.getValidCards(cost.getSacType().split(","), sa.getActivatingPlayer(), sa.getSourceCard()); + Card target = sa.getTargetCard(); + if (target != null && target.getController().isPlayer(player)) // don't sacrifice the card we're pumping + typeList.remove(target); + + if (cost.getSacAmount() > typeList.size()) + return false; + } + else if (cost.getSacThis() && !AllZoneUtil.isCardInPlay(card)) + return false; + } + + if (cost.getExileCost()){ + // if there's an exile in the cost, just because we can Pay it doesn't mean we want to. + if (!cost.getExileThis()){ + CardList typeList = AllZoneUtil.getPlayerCardsInPlay(player); + typeList = typeList.getValidCards(cost.getExileType().split(","), sa.getActivatingPlayer(), sa.getSourceCard()); + Card target = sa.getTargetCard(); + if (target != null && target.getController().isPlayer(player)) // don't exile the card we're pumping + typeList.remove(target); + + if (cost.getExileAmount() > typeList.size()) + return false; + } + else if (cost.getExileThis() && !AllZoneUtil.isCardInPlay(card)) + return false; + } + + if (cost.getExileFromHandCost()){ + // if there's an exile in the cost, just because we can Pay it doesn't mean we want to. + if (!cost.getExileFromHandThis()){ + CardList typeList = AllZoneUtil.getPlayerHand(player); + typeList = typeList.getValidCards(cost.getExileFromHandType().split(","), sa.getActivatingPlayer(), sa.getSourceCard()); + Card target = sa.getTargetCard(); + if (target != null && target.getController().isPlayer(player)) // don't exile the card we're pumping + typeList.remove(target); + + if (cost.getExileFromHandAmount() > typeList.size()) + return false; + } + else if (cost.getExileFromHandThis() && !AllZoneUtil.isCardInPlayerHand(player, card)) + return false; + } + + if (cost.getExileFromGraveCost()){ + if (!cost.getExileFromGraveThis()){ + CardList typeList = AllZoneUtil.getPlayerGraveyard(player); + typeList = typeList.getValidCards(cost.getExileFromGraveType().split(","), sa.getActivatingPlayer(), sa.getSourceCard()); + Card target = sa.getTargetCard(); + if (target != null && target.getController().isPlayer(player)) // don't exile the card we're pumping + typeList.remove(target); + + if (cost.getExileFromGraveAmount() > typeList.size()) + return false; + } + else if (cost.getExileFromGraveThis() && !AllZoneUtil.isCardInPlayerGraveyard(player, card)) + return false; + } + + if(cost.getExileFromTopCost()){ + if(!cost.getExileFromTopThis()){ + CardList typeList = AllZoneUtil.getPlayerCardsInLibrary(player); + typeList = typeList.getValidCards(cost.getExileFromTopType().split(","), sa.getActivatingPlayer(), sa.getSourceCard()); + Card target = sa.getTargetCard(); + if (target != null && target.getController().isPlayer(player)) // don't exile the card we're pumping + typeList.remove(target); + + if (cost.getExileFromTopAmount() > typeList.size()) + return false; + } + else if (cost.getExileFromTopThis() && !AllZoneUtil.isCardInPlayerLibrary(player, card)) + return false; + } + + if (cost.getReturnCost()){ + // if there's a return in the cost, just because we can Pay it doesn't mean we want to. + if (!cost.getReturnThis()){ + CardList typeList = AllZoneUtil.getPlayerCardsInPlay(player); + typeList = typeList.getValidCards(cost.getReturnType().split(","), sa.getActivatingPlayer(), sa.getSourceCard()); + Card target = sa.getTargetCard(); + if (target != null && target.getController().isPlayer(player)) // don't bounce the card we're pumping + typeList.remove(target); + + if (cost.getReturnAmount() > typeList.size()) + return false; + } + else if (!AllZoneUtil.isCardInPlay(card)) + return false; + } + + return true; + } + static public boolean canPayCost(String cost) { if(cost.equals(("0"))) @@ -748,6 +970,41 @@ public class ComputerUtil return sortedMana; }//getAvailableMana() + + static public CardList getAvailableMana(final Player player) + { + CardList list = AllZoneUtil.getPlayerCardsInPlay(player); + CardList mana = list.filter(new CardListFilter() + { + public boolean addCard(Card c) + { + for (Ability_Mana am : c.getAIPlayableMana()) { + am.setActivatingPlayer(player); + if (am.canPlay()) return true; + } + + return false; + } + });//CardListFilter + + CardList sortedMana = new CardList(); + + for (int i=0; i params = af.getMapParams(); if (!ComputerUtil.canPayCost(sa)) return false; @@ -190,6 +191,9 @@ public class AbilityFactory_PermanentState { return true; // TODO: use Defined to determine, if this is an unfavorable result + ArrayList pDefined = AbilityFactory.getDefinedCards(sa.getSourceCard(), params.get("Defined"), sa); + if (pDefined != null && pDefined.get(0).isUntapped()) + return false; return true; } diff --git a/src/forge/card/abilityFactory/AbilityFactory_Pump.java b/src/forge/card/abilityFactory/AbilityFactory_Pump.java index b0f8dfea05b..0f1292bb495 100644 --- a/src/forge/card/abilityFactory/AbilityFactory_Pump.java +++ b/src/forge/card/abilityFactory/AbilityFactory_Pump.java @@ -390,7 +390,7 @@ public class AbilityFactory_Pump { if (AllZone.Stack.size() == 0){ // If the cost is tapping, don't activate before declare attack/block - if (sa.getPayCosts().getTap()){ + if (sa.getPayCosts() != null && sa.getPayCosts().getTap()){ if (AllZone.Phase.isBefore(Constant.Phase.Combat_Declare_Attackers) && AllZone.Phase.isPlayerTurn(AllZone.ComputerPlayer)) list.remove(sa.getSourceCard()); if (AllZone.Phase.isBefore(Constant.Phase.Combat_Declare_Blockers) && AllZone.Phase.isPlayerTurn(AllZone.HumanPlayer)) @@ -541,8 +541,6 @@ public class AbilityFactory_Pump { if (sa.getTarget() == null){ if (mandatory) return true; - - } else{ return doTgtAI(sa, defense, attack, mandatory);