diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index f8309d07283..e98fd7b6a7e 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -30,6 +30,7 @@ import forge.game.card.*; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.combat.GlobalAttackRestrictions; +import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -201,8 +202,11 @@ public class AiAttackController { if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) { return true; } - if (this.attackers.size() == 1 && attacker.hasKeyword("Exalted") - && ComputerUtilCombat.predictDamageTo(opp, 1, attacker, true) > 0) { + + // TODO check if that makes sense + int exalted = ai.countExaltedBonus(); + if (this.attackers.size() == 1 && exalted > 0 + && ComputerUtilCombat.predictDamageTo(opp, exalted, attacker, true) > 0) { return true; } @@ -316,7 +320,7 @@ public class AiAttackController { } continue; } - if (c.hasKeyword("Vigilance")) { + if (c.hasKeyword(Keyword.VIGILANCE)) { vigilantes.add(c); notNeededAsBlockers.remove(c); // they will be re-added later if (canBlockAnAttacker(c, opponentsAttackers, false)) { @@ -543,7 +547,7 @@ public class AiAttackController { int trampleDamage = 0; for (Card attacker : blockedAttackers) { - if (attacker.hasKeyword("Trample")) { + if (attacker.hasKeyword(Keyword.TRAMPLE)) { int damage = ComputerUtilCombat.getAttack(attacker); for (Card blocker : this.blockers) { if (CombatUtil.canBlock(attacker, blocker)) { @@ -729,20 +733,15 @@ public class AiAttackController { // Exalted if (combat.getAttackers().isEmpty()) { - boolean exalted = false; - int exaltedCount = 0; - for (Card c : ai.getCardsIn(ZoneType.Battlefield)) { - if (c.getName().equals("Rafiq of the Many") || c.getName().equals("Battlegrace Angel")) { - exalted = true; - break; - } - if (c.getName().equals("Finest Hour") && ai.getGame().getPhaseHandler().isFirstCombat()) { - exalted = true; - break; - } - if (c.hasKeyword("Exalted")) { - exaltedCount++; - if (exaltedCount > 2) { + boolean exalted = ai.countExaltedBonus() > 2; + + if (!exalted) { + for (Card c : ai.getCardsIn(ZoneType.Battlefield)) { + if (c.getName().equals("Rafiq of the Many") || c.getName().equals("Battlegrace Angel")) { + exalted = true; + break; + } + if (c.getName().equals("Finest Hour") && ai.getGame().getPhaseHandler().isFirstCombat()) { exalted = true; break; } @@ -1061,7 +1060,7 @@ public class AiAttackController { public final static int getAttack(final Card c) { int n = c.getNetCombatDamage(); - if (c.hasKeyword("Double Strike")) { + if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) { n *= 2; } @@ -1092,7 +1091,7 @@ public class AiAttackController { // Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking? if ((attacker.hasSVar("NonCombatPriority")) - && (!attacker.hasKeyword("Vigilance"))) { + && (!attacker.hasKeyword(Keyword.VIGILANCE))) { // For each level of priority, enemy has to have life as much as the creature's power // so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks. // the lower the priroity, the less willing the AI is to use the creature for attacking. @@ -1144,7 +1143,7 @@ public class AiAttackController { && CombatUtil.canBlock(attacker, defender)) { numberOfPossibleBlockers += 1; if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false) - && !(attacker.hasKeyword("Undying") && attacker.getCounters(CounterType.P1P1) == 0)) { + && !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) { canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature // see if the defending creature is of higher or lower // value. We don't want to attack only to lose value @@ -1160,14 +1159,11 @@ public class AiAttackController { if (defender.getSVar("HasCombatEffect").equals("TRUE") || defender.getSVar("HasBlockEffect").equals("TRUE")) { canKillAllDangerous = false; } else { - for (KeywordInterface inst : defender.getKeywords()) { - String keyword = inst.getOriginal(); - if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) { - canKillAllDangerous = false; - break; - // there is a creature that can survive an attack from this creature - // and combat will have negative effects - } + if (defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT) + || defender.hasKeyword(Keyword.LIFELINK)) { + canKillAllDangerous = false; + // there is a creature that can survive an attack from this creature + // and combat will have negative effects } // Check if maybe we are too reckless in adding this attacker @@ -1191,7 +1187,7 @@ public class AiAttackController { } } - if (!attacker.hasKeyword("Vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) { + if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) { canKillAllDangerous = false; canBeKilled = true; canBeKilledByOne = true; @@ -1277,7 +1273,7 @@ public class AiAttackController { // creature would leave the battlefield // no pain in exerting it shouldExert = true; - } else if (c.hasKeyword("Vigilance")) { + } else if (c.hasKeyword(Keyword.VIGILANCE)) { // Free exert - why not? shouldExert = true; } diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index f5ee0467fb0..d674d866cec 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -25,6 +25,7 @@ import forge.game.GameEntity; import forge.game.card.*; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; @@ -117,11 +118,11 @@ public class AiBlockController { // If I don't have any planeswalkers then sorting doesn't really matter if (defenders.size() == 1) { - final CardCollection attackers = combat.getAttackersOf(defenders.get(0)); + final CardCollection attackers = combat.getAttackersOf(defenders.get(0)); // Begin with the attackers that pose the biggest threat ComputerUtilCard.sortByEvaluateCreature(attackers); CardLists.sortByPowerDesc(attackers); - //move cards like Phage the Untouchable to the front + //move cards like Phage the Untouchable to the front Collections.sort(attackers, new Comparator() { @Override public int compare(final Card o1, final Card o2) { @@ -142,16 +143,16 @@ public class AiBlockController { // defend planeswalkers with more loyalty before planeswalkers with less loyalty // if planeswalker will be too difficult to defend don't even bother for (GameEntity defender : defenders) { - if (defender instanceof Card) { - final CardCollection attackers = combat.getAttackersOf(defender); - // Begin with the attackers that pose the biggest threat - CardLists.sortByPowerDesc(attackers); - for (final Card c : attackers) { - sortedAttackers.add(c); - } - } else if (defender instanceof Player && defender.equals(ai)){ - firstAttacker = combat.getAttackersOf(defender); - } + if (defender instanceof Card) { + final CardCollection attackers = combat.getAttackersOf(defender); + // Begin with the attackers that pose the biggest threat + CardLists.sortByPowerDesc(attackers); + for (final Card c : attackers) { + sortedAttackers.add(c); + } + } else if (defender instanceof Player && defender.equals(ai)) { + firstAttacker = combat.getAttackersOf(defender); + } } if (bLifeInDanger) { @@ -180,7 +181,7 @@ public class AiBlockController { if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") - || attacker.hasKeyword("Menace")) { + || attacker.hasKeyword(Keyword.MENACE)) { continue; } @@ -204,11 +205,11 @@ public class AiBlockController { && !ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { blocker = ComputerUtilCard.getWorstCreatureAI(safeBlockers); // check whether it's better to block a creature without trample to absorb more damage - if (attacker.hasKeyword("Trample")) { + if (attacker.hasKeyword(Keyword.TRAMPLE)) { boolean doNotBlock = false; for (Card other : attackersLeft) { if (other.equals(attacker) || !CombatUtil.canBlock(other, blocker) - || other.hasKeyword("Trample") + || other.hasKeyword(Keyword.TRAMPLE) || ComputerUtilCombat.attackerHasThreateningAfflict(other, ai) || ComputerUtilCombat.canDestroyBlocker(ai, blocker, other, combat, false) || other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) { @@ -231,11 +232,10 @@ public class AiBlockController { // 3.Blockers that can destroy the attacker and have an upside when dying killingBlockers = getKillingBlockers(combat, attacker, blockers); for (Card b : killingBlockers) { - if ((b.hasKeyword("Undying") && b.getCounters(CounterType.P1P1) == 0) - || b.hasSVar("SacMe") - || (b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1) - || (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0) - || b.hasSVar("EndOfTurnLeavePlay")) { + if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe") + || (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1) + || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0) + || b.hasSVar("EndOfTurnLeavePlay")) { blocker = b; break; } @@ -245,7 +245,7 @@ public class AiBlockController { if (b.hasSVar("SacMe") && Integer.parseInt(b.getSVar("SacMe")) > 3) { blocker = b; if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) { - blockedButUnkilled.add(attacker); + blockedButUnkilled.add(attacker); } break; } @@ -293,8 +293,7 @@ public class AiBlockController { // 6. Blockers that don't survive until the next turn anyway for (final Card attacker : attackersLeft) { - if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") - || attacker.hasKeyword("Menace") + if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE) || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { continue; } @@ -304,12 +303,12 @@ public class AiBlockController { final List blockers = getPossibleBlockers(combat, attacker, blockersLeft, true); for (Card b : blockers) { - if ((b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1) - || (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0) - || b.hasSVar("EndOfTurnLeavePlay")) { + if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1) + || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0) + || b.hasSVar("EndOfTurnLeavePlay")) { blocker = b; if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) { - blockedButUnkilled.add(attacker); + blockedButUnkilled.add(attacker); } break; } @@ -531,7 +530,7 @@ public class AiBlockController { // Try to block a Menace attacker with two blockers, neither of which will die for (final Card attacker : attackersLeft) { - if (!attacker.hasKeyword("Menace") && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) { + if (!attacker.hasKeyword(Keyword.MENACE) && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) { continue; } @@ -590,7 +589,7 @@ public class AiBlockController { for (final Card attacker : attackersLeft) { if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") - || attacker.hasKeyword("Menace") + || attacker.hasKeyword(Keyword.MENACE) || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { continue; } @@ -645,7 +644,7 @@ public class AiBlockController { if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") - || attacker.hasKeyword("Menace") + || attacker.hasKeyword(Keyword.MENACE) || ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { attackers.remove(0); makeChumpBlocks(combat, attackers); @@ -657,7 +656,7 @@ public class AiBlockController { final Card blocker = ComputerUtilCard.getWorstCreatureAI(chumpBlockers); // check if it's better to block a creature with lower power and without trample - if (attacker.hasKeyword("Trample")) { + if (attacker.hasKeyword(Keyword.TRAMPLE)) { final int damageAbsorbed = blocker.getLethalDamage(); if (attacker.getNetCombatDamage() > damageAbsorbed) { for (Card other : attackers) { @@ -665,7 +664,7 @@ public class AiBlockController { continue; } if (other.getNetCombatDamage() >= damageAbsorbed - && !other.hasKeyword("Trample") + && !other.hasKeyword(Keyword.TRAMPLE) && !other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") && !ComputerUtilCombat.attackerHasThreateningAfflict(other, ai) && CombatUtil.canBlock(other, blocker, combat)) { @@ -696,7 +695,7 @@ public class AiBlockController { for (final Card attacker : currentAttackers) { if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") - && !attacker.hasKeyword("Menace") + && !attacker.hasKeyword(Keyword.MENACE) && !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { continue; } @@ -729,7 +728,7 @@ public class AiBlockController { List chumpBlockers; - List tramplingAttackers = CardLists.getKeyword(attackers, "Trample"); + List tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE); tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock)); // TODO - should check here for a "rampage-like" trigger that replaced @@ -738,7 +737,7 @@ public class AiBlockController { for (final Card attacker : tramplingAttackers) { - if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("Menace")) && !combat.isBlocked(attacker)) + if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) && !combat.isBlocked(attacker)) || attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { continue; @@ -775,29 +774,29 @@ public class AiBlockController { // Try to use safe blockers first if (blockers.size() > 0) { safeBlockers = getSafeBlockers(combat, attacker, blockers); - for (final Card blocker : safeBlockers) { - final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker) - + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false); - // Add an additional blocker if the current blockers are not - // enough and the new one would deal additional damage - if (damageNeeded > ComputerUtilCombat.totalDamageOfBlockers(attacker, combat.getBlockers(attacker)) - && ComputerUtilCombat.dealsDamageAsBlocker(attacker, blocker) > 0 - && CombatUtil.canBlock(attacker, blocker, combat)) { - combat.addBlocker(attacker, blocker); - } - blockers.remove(blocker); // Don't check them again next - } + for (final Card blocker : safeBlockers) { + final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker) + + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false); + // Add an additional blocker if the current blockers are not + // enough and the new one would deal additional damage + if (damageNeeded > ComputerUtilCombat.totalDamageOfBlockers(attacker, combat.getBlockers(attacker)) + && ComputerUtilCombat.dealsDamageAsBlocker(attacker, blocker) > 0 + && CombatUtil.canBlock(attacker, blocker, combat)) { + combat.addBlocker(attacker, blocker); + } + blockers.remove(blocker); // Don't check them again next + } } // don't try to kill what can't be killed - if (attacker.hasKeyword("indestructible") || ComputerUtil.canRegenerate(ai, attacker)) { - continue; + if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, attacker)) { + continue; } // Try to add blockers that could be destroyed, but are worth less than the attacker // Don't use blockers without First Strike or Double Strike if attacker has it if (ComputerUtilCombat.dealsFirstStrikeDamage(attacker, false, combat)) { - safeBlockers = CardLists.getKeyword(blockers, "First Strike"); - safeBlockers.addAll(CardLists.getKeyword(blockers, "Double Strike")); + safeBlockers = CardLists.getKeyword(blockers, Keyword.FIRST_STRIKE); + safeBlockers.addAll(CardLists.getKeyword(blockers, Keyword.DOUBLE_STRIKE)); } else { safeBlockers = new ArrayList<>(blockers); } @@ -870,7 +869,7 @@ public class AiBlockController { for (final Card attacker : attackers) { GameEntity def = combat.getDefenderByAttacker(attacker); if (def instanceof Card && threatenedPWs.contains((Card) def)) { - if (attacker.hasKeyword("Trample")) { + if (attacker.hasKeyword(Keyword.TRAMPLE)) { // don't bother trying to chump a trampling creature continue; } @@ -985,7 +984,7 @@ public class AiBlockController { diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) { - diff = 0; + diff = 0; } // remove all attackers that can't be blocked anyway @@ -1015,73 +1014,80 @@ public class AiBlockController { // When the AI holds some Fog effect, don't bother about lifeInDanger if (!ComputerUtil.hasAFogEffect(ai)) { - lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat); + lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat); makeTradeBlocks(combat); // choose necessary trade blocks - // if life is still in danger - if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { - makeChumpBlocks(combat); // choose necessary chump blocks - } else { - lifeInDanger = false; + // if life is still in danger + if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { + makeChumpBlocks(combat); // choose necessary chump blocks + } else { + lifeInDanger = false; } - // if life is still in danger - // Reinforce blockers blocking attackers with trample if life is still - // in danger - if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { - reinforceBlockersAgainstTrample(combat); - } else { - lifeInDanger = false; + // if life is still in danger + // Reinforce blockers blocking attackers with trample if life is + // still + // in danger + if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { + reinforceBlockersAgainstTrample(combat); + } else { + lifeInDanger = false; + } + // Support blockers not destroying the attacker with more blockers + // to + // try to kill the attacker + if (!lifeInDanger) { + reinforceBlockersToKill(combat); } - // Support blockers not destroying the attacker with more blockers to - // try to kill the attacker - if (!lifeInDanger) { - reinforceBlockersToKill(combat); - } - // == 2. If the AI life would still be in danger make a safer approach == - if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { - clearBlockers(combat, possibleBlockers); // reset every block assignment - makeTradeBlocks(combat); // choose necessary trade blocks - // if life is in danger - makeGoodBlocks(combat); - // choose necessary chump blocks if life is still in danger - if (ComputerUtilCombat.lifeInDanger(ai, combat)) { - makeChumpBlocks(combat); - } else { - lifeInDanger = false; - } - // Reinforce blockers blocking attackers with trample if life is - // still in danger - if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { - reinforceBlockersAgainstTrample(combat); - } else { - lifeInDanger = false; - } - makeGangBlocks(combat); - reinforceBlockersToKill(combat); - } + // == 2. If the AI life would still be in danger make a safer + // approach == + if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { + clearBlockers(combat, possibleBlockers); // reset every block + // assignment + makeTradeBlocks(combat); // choose necessary trade blocks + // if life is in danger + makeGoodBlocks(combat); + // choose necessary chump blocks if life is still in danger + if (ComputerUtilCombat.lifeInDanger(ai, combat)) { + makeChumpBlocks(combat); + } else { + lifeInDanger = false; + } + // Reinforce blockers blocking attackers with trample if life is + // still in danger + if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { + reinforceBlockersAgainstTrample(combat); + } else { + lifeInDanger = false; + } + makeGangBlocks(combat); + reinforceBlockersToKill(combat); + } - // == 3. If the AI life would be in serious danger make an even safer approach == - if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { - clearBlockers(combat, possibleBlockers); // reset every block assignment - makeChumpBlocks(combat); // choose chump blocks - if (ComputerUtilCombat.lifeInDanger(ai, combat)) { - makeTradeBlocks(combat); // choose necessary trade - } + // == 3. If the AI life would be in serious danger make an even + // safer approach == + if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { + clearBlockers(combat, possibleBlockers); // reset every block + // assignment + makeChumpBlocks(combat); // choose chump blocks + if (ComputerUtilCombat.lifeInDanger(ai, combat)) { + makeTradeBlocks(combat); // choose necessary trade + } - if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { - makeGoodBlocks(combat); - } - // Reinforce blockers blocking attackers with trample if life is - // still in danger - else { - reinforceBlockersAgainstTrample(combat); - } - makeGangBlocks(combat); - // Support blockers not destroying the attacker with more blockers - // to try to kill the attacker - reinforceBlockersToKill(combat); - } + if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { + makeGoodBlocks(combat); + } + // Reinforce blockers blocking attackers with trample if life is + // still in danger + else { + reinforceBlockersAgainstTrample(combat); + } + makeGangBlocks(combat); + // Support blockers not destroying the attacker with more + // blockers + // to try to kill the attacker + reinforceBlockersToKill(combat); + } } // assign blockers that have to block @@ -1167,10 +1173,11 @@ public class AiBlockController { * Orders a blocker that put onto the battlefield blocking. Depends heavily * on the implementation of orderBlockers(). */ - public static CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) { - // add blocker to existing ordering - // sort by evaluate, then insert it appropriately - // relies on current implementation of orderBlockers() + public static CardCollection orderBlocker(final Card attacker, final Card blocker, + final CardCollection oldBlockers) { + // add blocker to existing ordering + // sort by evaluate, then insert it appropriately + // relies on current implementation of orderBlockers() final CardCollection allBlockers = new CardCollection(oldBlockers); allBlockers.add(blocker); ComputerUtilCard.sortByEvaluateCreature(allBlockers); @@ -1182,24 +1189,28 @@ public class AiBlockController { boolean newBlockerIsAdded = false; // The new blocker comes right after this one final Card newBlockerRightAfter = (newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1)); - if (newBlockerRightAfter == null && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) { - result.add(blocker); - newBlockerIsAdded = true; + if (newBlockerRightAfter == null + && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) { + result.add(blocker); + newBlockerIsAdded = true; } - // Don't bother to keep damage up-to-date after the new blocker is added, as we can't modify the order of the other cards anyway + // Don't bother to keep damage up-to-date after the new blocker is + // added, as we can't modify the order of the other cards anyway for (final Card c : oldBlockers) { - final int lethal = ComputerUtilCombat.getEnoughDamageToKill(c, damage, attacker, true); - damage -= lethal; - result.add(c); - if (!newBlockerIsAdded && c == newBlockerRightAfter && damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) { - // If blocker is right after this card in priority and we have sufficient damage to kill it, add it here - result.add(blocker); - newBlockerIsAdded = true; - } + final int lethal = ComputerUtilCombat.getEnoughDamageToKill(c, damage, attacker, true); + damage -= lethal; + result.add(c); + if (!newBlockerIsAdded && c == newBlockerRightAfter + && damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) { + // If blocker is right after this card in priority and we have + // sufficient damage to kill it, add it here + result.add(blocker); + newBlockerIsAdded = true; + } } // We don't have sufficient damage, just add it at the end! if (!newBlockerIsAdded) { - result.add(blocker); + result.add(blocker); } return result; diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index a645ec1a81c..3eed6899f30 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -42,6 +42,7 @@ import forge.game.card.CardPredicates.Presets; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.*; +import forge.game.keyword.Keyword; import forge.game.mana.ManaCostBeingPaid; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -159,15 +160,15 @@ public class AiController { if ("NonActive".equals(curse) && !player.equals(game.getPhaseHandler().getPlayerTurn())) { return true; } else if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature() - && !sa.getHostCard().hasKeyword("Indestructible")) { + && !sa.getHostCard().hasKeyword(Keyword.INDESTRUCTIBLE)) { return true; } else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment() - && !sa.getHostCard().hasKeyword("CARDNAME can't be countered.")) { + && CardFactoryUtil.isCounterable(host)) { return true; - } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.") + } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host) && host.getCMC() == c.getCounters(CounterType.CHARGE)) { return true; - } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.")) { + } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) { for (Card card : game.getCardsIn(ZoneType.Battlefield)) { if (!card.isToken() && card.getName().equals(host.getName())) { return true; @@ -793,7 +794,7 @@ public class AiController { } } // if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count - if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) { + if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) { p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS)); } } @@ -1335,7 +1336,7 @@ public class AiController { continue; } - if (sa.getHostCard().hasKeyword("Storm") + if (sa.getHostCard().hasKeyword(Keyword.STORM) && sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell && CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.hasKeyword("Storm")))).size() > 0) { if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) { @@ -1563,11 +1564,11 @@ public class AiController { // and exaclty one counter of the specifice type gets high priority to keep the card if (allies.contains(crd.getController())) { // except if its a Chronozoa, because it WANTS to be removed to make more - if (crd.hasKeyword("Vanishing") && !"Chronozoa".equals(crd.getName())) { + if (crd.hasKeyword(Keyword.VANISHING) && !"Chronozoa".equals(crd.getName())) { if (crd.getCounters(CounterType.TIME) == 1) { return CounterType.TIME; } - } else if (crd.hasKeyword("Fading")) { + } else if (crd.hasKeyword(Keyword.FADING)) { if (crd.getCounters(CounterType.FADE) == 1) { return CounterType.FADE; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index faed8a063a9..cc829bbeb43 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -37,6 +37,7 @@ import forge.game.card.CardPredicates.Presets; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.*; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -789,7 +790,7 @@ public class ComputerUtil { } if (destroy) { - final CardCollection indestructibles = CardLists.getKeyword(remaining, "Indestructible"); + final CardCollection indestructibles = CardLists.getKeyword(remaining, Keyword.INDESTRUCTIBLE); if (!indestructibles.isEmpty()) { return indestructibles.get(0); } @@ -826,7 +827,7 @@ public class ComputerUtil { } public static boolean canRegenerate(Player ai, final Card card) { - if (card.hasKeyword("CARDNAME can't be regenerated.")) { + if (!card.canBeShielded()) { return false; } @@ -939,7 +940,7 @@ public class ComputerUtil { } // try not to cast Raid creatures in main 1 if an attack is likely - if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword("Haste")) { + if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword(Keyword.HASTE)) { for (Card potentialAtkr: ai.getCreaturesInPlay()) { if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) { return false; @@ -951,12 +952,12 @@ public class ComputerUtil { return true; } - if (card.isCreature() && !card.hasKeyword("Defender") - && (card.hasKeyword("Haste") || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) { + if (card.isCreature() && !card.hasKeyword(Keyword.DEFENDER) + && (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) { return true; } - if (card.hasKeyword("Exalted")) { + if (card.hasKeyword(Keyword.EXALTED)) { return true; } @@ -991,16 +992,16 @@ public class ComputerUtil { return true; } if (card.isCreature()) { - if (buffedcard.hasKeyword("Soulbond") && !buffedcard.isPaired()) { + if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) { return true; } - if (buffedcard.hasKeyword("Evolve")) { + if (buffedcard.hasKeyword(Keyword.EVOLVE)) { if (buffedcard.getNetPower() < card.getNetPower() || buffedcard.getNetToughness() < card.getNetToughness()) { return true; } } } - if (card.hasKeyword("Soulbond") && buffedcard.isCreature() && !buffedcard.isPaired()) { + if (card.hasKeyword(Keyword.SOULBOND) && buffedcard.isCreature() && !buffedcard.isPaired()) { return true; } @@ -1248,7 +1249,7 @@ public class ComputerUtil { // Special for Odric if (ai.isCardInPlay("Odric, Lunarch Marshal") - && !CardLists.getKeyword(all, "Haste").isEmpty()) { + && !CardLists.getKeyword(all, Keyword.HASTE).isEmpty()) { return true; } @@ -1317,7 +1318,7 @@ public class ComputerUtil { if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword") && params.get("AddKeyword").contains("Haste")) { - final ArrayList affected = Lists.newArrayList(params.get("Affected").split(",")); + final ArrayList affected = Lists.newArrayList(params.get("Affected").split(",")); if (affected.contains("Creature")) { return true; } @@ -1547,7 +1548,7 @@ public class ComputerUtil { final Card c = (Card) o; // indestructible - if (c.hasKeyword("Indestructible")) { + if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) { continue; } @@ -1594,7 +1595,7 @@ public class ComputerUtil { } else if (o instanceof Player) { final Player p = (Player) o; - if (source.hasKeyword("Infect")) { + if (source.hasKeyword(Keyword.INFECT)) { if (ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= p.getPoisonCounters()) { threatened.add(p); } @@ -1615,14 +1616,14 @@ public class ComputerUtil { if (o instanceof Card) { final Card c = (Card) o; final boolean canRemove = (c.getNetToughness() <= dmg) - || (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c))); + || (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c))); if (!canRemove) { continue; } if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) { final boolean cantSave = c.getNetToughness() + toughness <= dmg - || (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && !grantIndestructible + || (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && !grantIndestructible && (dmg >= toughness + ComputerUtilCombat.getDamageToKill(c))); if (cantSave && (tgt == null || !grantShroud)) { continue; @@ -1662,7 +1663,7 @@ public class ComputerUtil { if (o instanceof Card) { final Card c = (Card) o; // indestructible - if (c.hasKeyword("Indestructible")) { + if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) { continue; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 7b3975e8d8d..1e05d7a55e4 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -1081,7 +1081,7 @@ public class ComputerUtilCard { valueTempo *= 2; //deal with annoying things } if (!destination.equals(ZoneType.Graveyard) && //TODO:boat-load of "when blah dies" triggers - c.hasKeyword("Persist") || c.hasKeyword("Undying") || c.hasKeyword("Modular")) { + c.hasKeyword(Keyword.PERSIST) || c.hasKeyword(Keyword.UNDYING) || c.hasKeyword(Keyword.MODULAR)) { valueTempo *= 2; } if (destination.equals(ZoneType.Hand) && !c.isToken()) { @@ -1358,8 +1358,9 @@ public class ComputerUtilCard { //1. save combatant if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && !pumpedWillDie - && !c.hasKeyword("Indestructible")) { // hack because attackerWouldBeDestroyed() does not - // check for Indestructible when computing lethal damage + && !c.hasKeyword(Keyword.INDESTRUCTIBLE)) { + // hack because attackerWouldBeDestroyed() + // does not check for Indestructible when computing lethal damage return true; } @@ -1396,17 +1397,17 @@ public class ComputerUtilCard { int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0; // predict Infect - if (pumpedDmg == 0 && c.hasKeyword("Infect")) { + if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) { if (poisonPumped > poisonOrig) { pumpedDmg = poisonPumped; } } if (combat.isBlocked(c)) { - if (!c.hasKeyword("Trample")) { + if (!c.hasKeyword(Keyword.TRAMPLE)) { dmg = 0; } - if (c.hasKeyword("Trample") || keywords.contains("Trample")) { + if (c.hasKeyword(Keyword.TRAMPLE) || keywords.contains("Trample")) { for (Card b : combat.getBlockers(c)) { pumpedDmg -= ComputerUtilCombat.getDamageToKill(b); } @@ -1415,8 +1416,8 @@ public class ComputerUtilCard { } } if (pumpedDmg > dmg) { - if ((!c.hasKeyword("Infect") && pumpedDmg >= opp.getLife()) - || (c.hasKeyword("Infect") && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) { + if ((!c.hasKeyword(Keyword.INFECT) && pumpedDmg >= opp.getLife()) + || (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) { return true; } } @@ -1425,7 +1426,7 @@ public class ComputerUtilCard { if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) { int totalPowerUnblocked = 0; for (Card atk : combat.getAttackers()) { - if (combat.isBlocked(atk) && !atk.hasKeyword("Trample")) { + if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) { continue; } if (atk == c) { @@ -1458,7 +1459,7 @@ public class ComputerUtilCard { } //4. lifelink - if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword("Lifelink") && keywords.contains("Lifelink") + if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword(Keyword.LIFELINK) && keywords.contains("Lifelink") && (combat.isAttacking(c) || combat.isBlocking(c))) { int dmg = pumped.getNetCombatDamage(); //The actual dmg inflicted should be the sum of ComputerUtilCombat.predictDamageTo() for opposing creature @@ -1471,7 +1472,7 @@ public class ComputerUtilCard { List blockedBy = combat.getAttackersBlockedBy(c); boolean attackerHasTrample = false; for (Card b : blockedBy) { - attackerHasTrample |= b.hasKeyword("Trample"); + attackerHasTrample |= b.hasKeyword(Keyword.TRAMPLE); } if (attackerHasTrample && (sa.isAbility() || ComputerUtilCombat.lifeInDanger(ai, combat))) { return true; @@ -1713,10 +1714,10 @@ public class ComputerUtilCard { } public static boolean hasActiveUndyingOrPersist(final Card c) { - if (c.hasKeyword("Undying") && c.getCounters(CounterType.P1P1) == 0) { + if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterType.P1P1) == 0) { return true; } - if (c.hasKeyword("Persist") && c.getCounters(CounterType.M1M1) == 0) { + if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterType.M1M1) == 0) { return true; } return false; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index 7368fe77f8b..8f0198af2c0 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -202,10 +202,10 @@ public class ComputerUtilCombat { } damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities); - if (!attacker.hasKeyword("Infect")) { + if (!attacker.hasKeyword(Keyword.INFECT)) { sum = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true); - if (attacker.hasKeyword("Double Strike")) { - sum += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true); + if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { + sum *= 2; } } return sum; @@ -303,9 +303,9 @@ public class ComputerUtilCombat { || attacker.hasKeyword("You may have CARDNAME assign its combat damage " + "as though it weren't blocked.")) { unblocked.add(attacker); - } else if (attacker.hasKeyword("Trample") + } else if (attacker.hasKeyword(Keyword.TRAMPLE) && (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) { - if (!attacker.hasKeyword("Infect")) { + if (!attacker.hasKeyword(Keyword.INFECT)) { damage += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers); } } @@ -332,6 +332,11 @@ public class ComputerUtilCombat { */ public static int resultingPoison(final Player ai, final Combat combat) { + // ai can't get poision counters, so the value can't change + if (!ai.canReceiveCounters(CounterType.POISON)) { + return ai.getPoisonCounters(); + } + int poison = 0; final List attackers = combat.getAttackersOf(ai); @@ -578,7 +583,7 @@ public class ComputerUtilCombat { int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender); - if (defender.hasKeyword("Double Strike")) { + if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) { defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true); } @@ -592,7 +597,7 @@ public class ComputerUtilCombat { * @return */ private static int predictDamageByBlockerWithoutDoubleStrike(final Card attacker, final Card defender) { - if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword("Indestructible")) { + if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) { return 0; } @@ -923,9 +928,9 @@ public class ComputerUtilCombat { // if the attacker has first strike and wither the blocker will deal // less damage than expected if (dealsFirstStrikeDamage(attacker, withoutAbilities, null) - && (attacker.hasKeyword("Wither") || attacker.hasKeyword("Infect")) + && (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)) && !dealsFirstStrikeDamage(blocker, withoutAbilities, null) - && !blocker.hasKeyword("CARDNAME can't have counters put on it.")) { + && !blocker.canReceiveCounters(CounterType.M1M1)) { power -= attacker.getNetCombatDamage(); } @@ -1266,7 +1271,7 @@ public class ComputerUtilCombat { if (ComputerUtilCombat.dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)) && !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat) - && !attacker.hasKeyword("CARDNAME can't have counters put on it.")) { + && !attacker.canReceiveCounters(CounterType.M1M1)) { power -= blocker.getNetCombatDamage(); } theTriggers.addAll(blocker.getTriggers()); @@ -1493,7 +1498,7 @@ public class ComputerUtilCombat { } else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) { final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature"); if (!attacker.isValid(valid, card.getController(), card, null) - || attacker.hasKeyword("Vigilance")) { + || attacker.hasKeyword(Keyword.VIGILANCE)) { continue; } // remove the bonus, because it will no longer be granted @@ -1646,7 +1651,7 @@ public class ComputerUtilCombat { if (blocker.isEquippedBy("Godsend")) { return true; } - if (attacker.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(attacker.getController(), attacker)) { + if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(attacker.getController(), attacker)) { return false; } @@ -1711,12 +1716,12 @@ public class ComputerUtilCombat { */ public static boolean attackerCantBeDestroyedInCombat(Player ai, final Card attacker) { // attacker is either indestructible or may regenerate - if (attacker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, attacker))) { + if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker))) { return true; } // attacker will regenerate - if (attacker.getShieldCount() > 0 && !attacker.hasKeyword("CARDNAME can't be regenerated.")) { + if (attacker.getShieldCount() > 0 && attacker.canBeShielded()) { return true; } @@ -1829,7 +1834,7 @@ public class ComputerUtilCombat { final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker) + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); - if (blocker.hasKeyword("Double Strike")) { + if (blocker.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) { return true; } @@ -1839,7 +1844,8 @@ public class ComputerUtilCombat { // Attacker may kill the blocker before he can deal normal // (secondary) damage - if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat) && !blocker.hasKeyword("Indestructible")) { + if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat) + && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) { if (attackerDamage >= defenderLife) { return false; } @@ -1855,7 +1861,7 @@ public class ComputerUtilCombat { else { // no double strike for defender // Attacker may kill the blocker before he can deal any damage if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat) - && !blocker.hasKeyword("Indestructible") + && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE) && !dealsFirstStrikeDamage(blocker, withoutAbilities, combat)) { if (attackerDamage >= defenderLife) { @@ -2011,11 +2017,11 @@ public class ComputerUtilCombat { return true; } - if (((blocker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker - .hasKeyword("Wither") || attacker.hasKeyword("Infect"))) - || (blocker.hasKeyword("Persist") && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker + if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker + .hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) + || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker .getCounters(CounterType.M1M1) == 0)) - || (blocker.hasKeyword("Undying") && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker + || (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker .getCounters(CounterType.P1P1) == 0))) { return false; } @@ -2065,7 +2071,7 @@ public class ComputerUtilCombat { final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker) + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); - if (attacker.hasKeyword("Double Strike")) { + if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) { return true; } @@ -2075,7 +2081,8 @@ public class ComputerUtilCombat { // Attacker may kill the blocker before he can deal normal // (secondary) damage - if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible")) { + if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) + && !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)) { if (defenderDamage >= attackerLife) { return false; } @@ -2090,7 +2097,8 @@ public class ComputerUtilCombat { else { // no double strike for attacker // Defender may kill the attacker before he can deal any damage - if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible") + if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) + && !attacker.hasKeyword(Keyword.INDESTRUCTIBLE) && !dealsFirstStrikeDamage(attacker, withoutAbilities, combat)) { if (defenderDamage >= attackerLife) { @@ -2137,7 +2145,7 @@ public class ComputerUtilCombat { return damageMap; } - final boolean hasTrample = attacker.hasKeyword("Trample"); + final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE); if (block.size() == 1) { final Card blocker = block.getFirst(); @@ -2239,11 +2247,11 @@ public class ComputerUtilCombat { final boolean noPrevention) { final int killDamage = c.isPlaneswalker() ? c.getCurrentLoyalty() : ComputerUtilCombat.getDamageToKill(c); - if (c.hasKeyword("Indestructible") || c.getShieldCount() > 0) { - if (!(source.hasKeyword("Wither") || source.hasKeyword("Infect"))) { + if (c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getShieldCount() > 0) { + if (!(source.hasKeyword(Keyword.WITHER) || source.hasKeyword(Keyword.INFECT))) { return maxDamage + 1; } - } else if (source.hasKeyword("Deathtouch")) { + } else if (source.hasKeyword(Keyword.DEATHTOUCH)) { for (int i = 1; i <= maxDamage; i++) { if (noPrevention) { if (c.staticReplaceDamage(i, source, isCombat) > 0) { @@ -2406,7 +2414,7 @@ public class ComputerUtilCombat { public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) { - if (combatant.hasKeyword("Double Strike") || combatant.hasKeyword("First Strike")) { + if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) { return true; } @@ -2542,18 +2550,10 @@ public class ComputerUtilCombat { CardCollection withoutEvasion = new CardCollection(); for (Card atk : attackers) { - boolean hasProtection = false; - for (KeywordInterface inst : atk.getKeywords()) { - String kw = inst.getOriginal(); - if (kw.startsWith("Protection")) { - hasProtection = true; - break; - } - } - - if (atk.hasKeyword("Flying") || atk.hasKeyword("Shadow") - || atk.hasKeyword("Horsemanship") || (atk.hasKeyword("Fear") - || atk.hasKeyword("Intimidate") || atk.hasKeyword("Skulk") || hasProtection)) { + if (atk.hasKeyword(Keyword.FLYING) || atk.hasKeyword(Keyword.SHADOW) + || atk.hasKeyword(Keyword.HORSEMANSHIP) || (atk.hasKeyword(Keyword.FEAR) + || atk.hasKeyword(Keyword.INTIMIDATE) || atk.hasKeyword(Keyword.SKULK) + || atk.hasKeyword(Keyword.PROTECTION))) { withEvasion.add(atk); } else { withoutEvasion.add(atk); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index 73e4f20c520..2b32787350a 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -9,6 +9,7 @@ import forge.game.GameActionUtil; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; +import forge.game.card.CardFactoryUtil; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; import forge.game.card.CounterType; @@ -446,13 +447,13 @@ public class ComputerUtilCost { // Check for stuff like Nether Void int extraManaNeeded = 0; if (sa instanceof Spell) { - final boolean cannotBeCountered = sa.getHostCard().hasKeyword("CARDNAME can't be countered."); + final boolean cannotBeCountered = CardFactoryUtil.isCounterable(sa.getHostCard()); for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) { final String snem = c.getSVar("AI_SpellsNeedExtraMana"); if (!StringUtils.isBlank(snem)) { - if (cannotBeCountered && c.getName().equals("Nether Void")) { - continue; - } + if (cannotBeCountered && c.getName().equals("Nether Void")) { + continue; + } String[] parts = TextUtil.split(snem, ' '); boolean meetsRestriction = parts.length == 1 || player.isValid(parts[1], c.getController(), c, sa); if(!meetsRestriction) @@ -467,7 +468,7 @@ public class ComputerUtilCost { } for (Card c : player.getCardsIn(ZoneType.Command)) { if (cannotBeCountered) { - continue; + continue; } final String snem = c.getSVar("SpellsNeedExtraManaEffect"); if (!StringUtils.isBlank(snem)) { diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java index 5c54f7de1a3..3b7a9c7890a 100644 --- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java @@ -84,7 +84,7 @@ public class CreatureEvaluator implements Function { if (power > 0) { if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) { value += addValue(10 + (power * 15), "ds"); - } else if (c.hasKeyword("First Strike")) { + } else if (c.hasKeyword(Keyword.FIRST_STRIKE)) { value += addValue(10 + (power * 5), "fs"); } if (c.hasKeyword(Keyword.DEATHTOUCH)) { @@ -116,7 +116,7 @@ public class CreatureEvaluator implements Function { value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb"); // Keywords that may produce temporary or permanent buffs over time - if (c.hasKeyword("Prowess")) { + if (c.hasKeyword(Keyword.PROWESS)) { value += addValue(5, "prowess"); } if (c.hasKeyword(Keyword.OUTLAST)) { @@ -145,7 +145,7 @@ public class CreatureEvaluator implements Function { } else if (c.hasKeyword(Keyword.SHROUD)) { value += addValue(30, "shroud"); } - if (c.hasStartOfKeyword("Protection")) { + if (c.hasKeyword(Keyword.PROTECTION)) { value += addValue(20, "protection"); } @@ -186,7 +186,7 @@ public class CreatureEvaluator implements Function { value -= subValue(30, "cupkeep"); } else if (c.hasStartOfKeyword("UpkeepCost")) { value -= subValue(20, "sac-unless"); - } else if (c.hasStartOfKeyword("Echo") && c.cameUnderControlSinceLastUpkeep()) { + } else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) { value -= subValue(10, "echo-unpaid"); } diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index d69ddd0c7c8..db77d699d3e 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -33,6 +33,7 @@ import forge.game.card.*; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.CostPart; +import forge.game.keyword.Keyword; import forge.game.mana.ManaCostBeingPaid; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -206,7 +207,9 @@ public class SpecialCardAi { private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) { - CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach")))); + CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), + Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or( + CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH)))); boolean hasUsefulBlocker = false; for (Card c : flyingCreatures) { @@ -329,7 +332,7 @@ public class SpecialCardAi { boolean oppHasFirstStrike = false; boolean oppCantDie = true; boolean unblocked = opposition.isEmpty(); - boolean canTrample = source.hasKeyword("Trample"); + boolean canTrample = source.hasKeyword(Keyword.TRAMPLE); if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) { int loyalty = ((Card)combat.getDefenderByAttacker(source)).getCounters(CounterType.LOYALTY); @@ -351,7 +354,7 @@ public class SpecialCardAi { } for (Card c : opposition) { - if (c.hasKeyword("First Strike") || c.hasKeyword("Double Strike")) { + if (c.hasKeyword(Keyword.FIRST_STRIKE) || c.hasKeyword(Keyword.DOUBLE_STRIKE)) { oppHasFirstStrike = true; } if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) { @@ -382,8 +385,8 @@ public class SpecialCardAi { // Already enough to kill the blockers and survive, don't overpump return false; } - if (oppCantDie && !source.hasKeyword("Trample") && !source.hasKeyword("Wither") - && !source.hasKeyword("Infect") && predictedPT.getLeft() <= oppT) { + if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.hasKeyword(Keyword.WITHER) + && !source.hasKeyword(Keyword.INFECT) && predictedPT.getLeft() <= oppT) { // Can't kill or cripple anyone, as well as can't Trample over, so don't pump return false; } @@ -400,7 +403,7 @@ public class SpecialCardAi { CardCollection potentialBlockers = new CardCollection(); for (Card b : oppInPlay) { - if (CombatUtil.canBlock(sa.getHostCard(), b)) { + if (CombatUtil.canBlock(source, b)) { potentialBlockers.add(b); } } @@ -408,7 +411,7 @@ public class SpecialCardAi { Pair predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness()); int oppT = Aggregates.sum(potentialBlockers, CardPredicates.Accessors.fnGetNetToughness); - if (potentialBlockers.isEmpty() || (sa.getHostCard().hasKeyword("Trample") && predictedPT.getLeft() - oppT >= oppLife)) { + if (potentialBlockers.isEmpty() || (source.hasKeyword(Keyword.TRAMPLE) && predictedPT.getLeft() - oppT >= oppLife)) { return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index 06e690f12e6..902691478f8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -13,6 +13,7 @@ import forge.game.combat.CombatUtil; import forge.game.cost.Cost; import forge.game.cost.CostPart; import forge.game.cost.CostSacrifice; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -94,7 +95,7 @@ public class AttachAi extends SpellAbilityAi { final CardCollection targets = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof")); + return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF)); } }); if (targets.isEmpty()) { @@ -255,7 +256,7 @@ public class AttachAi extends SpellAbilityAi { @Override public boolean apply(final Card c) { // Don't do Untapped Vigilance cards - if (c.isCreature() && c.hasKeyword("Vigilance") && c.isUntapped()) { + if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) { return false; } @@ -397,7 +398,7 @@ public class AttachAi extends SpellAbilityAi { List evenBetterList = CardLists.filter(betterList, new Predicate() { @Override public boolean apply(final Card c) { - return c.hasKeyword("Indestructible") || c.hasKeyword("Hexproof"); + return c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF); } }); if (!evenBetterList.isEmpty()) { @@ -484,23 +485,23 @@ public class AttachAi extends SpellAbilityAi { for (Card card : list) { int cardPriority = 0; // Prefer Evasion - if (card.hasKeyword("Trample")) { + if (card.hasKeyword(Keyword.TRAMPLE)) { cardPriority += 10; } - if (card.hasKeyword("Menace")) { + if (card.hasKeyword(Keyword.MENACE)) { cardPriority += 10; } // Avoid this for Sleepers Robe? - if (card.hasKeyword("Fear")) { + if (card.hasKeyword(Keyword.FEAR)) { cardPriority += 15; } - if (card.hasKeyword("Flying")) { + if (card.hasKeyword(Keyword.FLYING)) { cardPriority += 20; } - if (card.hasKeyword("Shadow")) { + if (card.hasKeyword(Keyword.SHADOW)) { cardPriority += 30; } - if (card.hasKeyword("Horsemanship")) { + if (card.hasKeyword(Keyword.HORSEMANSHIP)) { cardPriority += 40; } if (card.hasKeyword("Unblockable")) { @@ -520,10 +521,10 @@ public class AttachAi extends SpellAbilityAi { if (card.getCurrentPower() <= 0) { cardPriority = -100; } - if (card.hasKeyword("Defender")) { + if (card.hasKeyword(Keyword.DEFENDER)) { cardPriority = -100; } - if (card.hasKeyword("Indestructible")) { + if (card.hasKeyword(Keyword.INDESTRUCTIBLE)) { cardPriority += 15; } if (cardPriority > priority) { @@ -725,7 +726,7 @@ public class AttachAi extends SpellAbilityAi { prefList = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - if (!c.hasKeyword("Indestructible") && (c.getLethalDamage() <= Math.abs(tgh))) { + if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && (c.getLethalDamage() <= Math.abs(tgh))) { return true; } @@ -1012,7 +1013,7 @@ public class AttachAi extends SpellAbilityAi { if (isUsefulAttachKeyword(keyword, c, sa, pow)) { return true; } - if (c.hasKeyword("Infect") && pow >= 2) { + if (c.hasKeyword(Keyword.INFECT) && pow >= 2) { // consider +2 power a significant bonus on Infect creatures return true; } @@ -1040,7 +1041,7 @@ public class AttachAi extends SpellAbilityAi { prefList = CardLists.filter(prefList, new Predicate() { @Override public boolean apply(final Card c) { - return !c.isEnchanted() || c.hasKeyword("Hexproof"); + return !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF); } }); } @@ -1375,7 +1376,7 @@ public class AttachAi extends SpellAbilityAi { return false; } } else if (keyword.equals("First Strike")) { - if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword("Double Strike") + if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword(Keyword.DOUBLE_STRIKE) || (!ComputerUtilCombat.canAttackNextTurn(card) && !CombatUtil.canBlock(card, true))) { return false; } @@ -1408,7 +1409,7 @@ public class AttachAi extends SpellAbilityAi { return false; } } else if (keyword.equals("Reach")) { - if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) { + if (card.hasKeyword(Keyword.FLYING) || !CombatUtil.canBlock(card, true)) { return false; } } else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) { @@ -1417,11 +1418,11 @@ public class AttachAi extends SpellAbilityAi { return false; } } else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) { - if (!card.hasKeyword("Defender") || card.getNetCombatDamage() + powerBonus <= 0) { + if (!card.hasKeyword(Keyword.DEFENDER) || card.getNetCombatDamage() + powerBonus <= 0) { return false; } } else if (keyword.equals("Shroud") || keyword.equals("Hexproof")) { - if (card.hasKeyword("Shroud") || card.hasKeyword("Hexproof")) { + if (card.hasKeyword(Keyword.SHROUD) || card.hasKeyword(Keyword.HEXPROOF)) { return false; } } else if (keyword.equals("Defender")) { diff --git a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java index 2d42c358ece..f3c44f1caa0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java @@ -7,6 +7,7 @@ import forge.game.Game; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -30,7 +31,7 @@ public class BecomesBlockedAi extends SpellAbilityAi { CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents()); list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); list = CardLists.getTargetableCards(list, sa); - list = CardLists.getNotKeyword(list, "Trample"); + list = CardLists.getNotKeyword(list, Keyword.TRAMPLE); while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) { Card choice = null; diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java index 8278642c9c3..dc23c4cdb7c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -22,6 +22,7 @@ import forge.game.card.CardPredicates; import forge.game.card.CardPredicates.Presets; import forge.game.card.CounterType; import forge.game.combat.Combat; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerPredicates; @@ -126,8 +127,8 @@ public class ChooseCardAi extends SpellAbilityAi { } else if (aiLogic.equals("Duneblast")) { CardCollection aiCreatures = ai.getCreaturesInPlay(); CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay(); - aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible"); - oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible"); + aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE); + oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE); // Use it as a wrath, when the human creatures threat the ai's life if (aiCreatures.isEmpty() && ComputerUtilCombat.sumDamageIfUnblocked(oppCreatures, ai) >= ai.getLife()) { @@ -261,7 +262,7 @@ public class ChooseCardAi extends SpellAbilityAi { } } else if (logic.equals("Duneblast")) { CardCollectionView aiCreatures = ai.getCreaturesInPlay(); - aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible"); + aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE); if (aiCreatures.isEmpty()) { return null; diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java index 8dd6367db26..2535c29505d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java @@ -27,6 +27,7 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CounterType; +import forge.game.keyword.Keyword; import forge.util.Aggregates; @@ -59,7 +60,7 @@ public abstract class CountersAi { if (type.equals("M1M1")) { // try to kill the best killable creature, or reduce the best one // but try not to target a Undying Creature - final List killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), "Undying"); + final List killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING); if (killable.size() > 0) { choice = ComputerUtilCard.getBestCreatureAI(killable); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java index 42e66c01d0a..cd82c59a5e5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java @@ -8,6 +8,7 @@ import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.*; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -286,7 +287,7 @@ public class CountersMoveAi extends SpellAbilityAi { // do not steal a P1P1 from Undying if it would die // this way if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { - if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying") || card.isToken()) { + if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken()) { return true; } return false; 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 3c38cfd40fa..79e89ba6f80 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -15,6 +15,7 @@ import forge.game.cost.Cost; import forge.game.cost.CostPart; import forge.game.cost.CostRemoveCounter; import forge.game.cost.CostSacrifice; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -154,7 +155,7 @@ public class CountersPutAi extends SpellAbilityAi { // receive counters, execpt it has undying CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa); CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1)); - oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, "Undying"); + oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING); oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate() { @Override @@ -339,14 +340,8 @@ public class CountersPutAi extends SpellAbilityAi { if ("Polukranos".equals(sa.getParam("AILogic"))) { - CardCollection humCreatures = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa); + CardCollection targets = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa); - final CardCollection targets = CardLists.filter(humCreatures, new Predicate() { - @Override - public boolean apply(final Card c) { - return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof")); - } - }); if (!targets.isEmpty()){ boolean canSurvive = false; for (Card humanCreature : targets) { @@ -792,12 +787,12 @@ public class CountersPutAi extends SpellAbilityAi { final List creats = player.getCreaturesInPlay(); final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); - final boolean isHaste = source.hasKeyword("Haste"); + final boolean isHaste = source.hasKeyword(Keyword.HASTE); List threatening = CardLists.filter(creats, new Predicate() { @Override public boolean apply(Card c) { return CombatUtil.canBlock(source, c, !isHaste) - && (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword("DeathTouch")); + && (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword(Keyword.DEATHTOUCH)); } }); if (!threatening.isEmpty()) { @@ -814,7 +809,7 @@ public class CountersPutAi extends SpellAbilityAi { List canBlock = CardLists.filter(creats, new Predicate() { @Override public boolean apply(Card c) { - return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword("DeathTouch")); + return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword(Keyword.DEATHTOUCH)); } }); if (!canBlock.isEmpty()) { @@ -907,7 +902,7 @@ public class CountersPutAi extends SpellAbilityAi { final CardCollection persist = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { - if (!input.hasKeyword("Persist")) + if (!input.hasKeyword(Keyword.PERSIST)) return false; return input.getCounters(CounterType.M1M1) <= amount; } @@ -920,7 +915,7 @@ public class CountersPutAi extends SpellAbilityAi { final CardCollection undying = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { - if (!input.hasKeyword("Undying")) + if (!input.hasKeyword(Keyword.UNDYING)) return false; return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount; } @@ -945,7 +940,7 @@ public class CountersPutAi extends SpellAbilityAi { if (e instanceof Card) { Card c = (Card) e; if (c.getController().isOpponentOf(ai)) { - if (options.contains(CounterType.M1M1) && !c.hasKeyword("Undying")) { + if (options.contains(CounterType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) { return CounterType.M1M1; } for (CounterType type : options) { diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java index 6df50568446..4195a7924b7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java @@ -23,6 +23,7 @@ import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.GlobalRuleChange; import forge.game.card.*; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.spellability.SpellAbility; @@ -56,7 +57,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) { - final Card source = sa.getHostCard(); final Game game = ai.getGame(); final int amount = Integer.valueOf(sa.getParam("CounterNum")); @@ -71,7 +71,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } // Filter AI-specific targets if provided - list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false); + list = ComputerUtil.filterAITgts(sa, ai, list, false); if (sa.hasParam("CounterType")) { // currently only Jhoira's Timebug @@ -125,7 +125,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1)); - CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist"); + CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST); if (!aiPersistList.isEmpty()) { aiM1M1List = aiPersistList; } @@ -137,7 +137,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { // do as P1P1 part CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1)); - CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying"); + CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING); if (!aiUndyingList.isEmpty()) { aiP1P1List = aiUndyingList; @@ -226,9 +226,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { return CounterType.ICE; } - } else if (tgt.hasKeyword("Undying") && options.contains(CounterType.P1P1)) { + } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.P1P1)) { return CounterType.P1P1; - } else if (tgt.hasKeyword("Persist") && options.contains(CounterType.M1M1)) { + } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.M1M1)) { return CounterType.M1M1; } @@ -272,9 +272,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { return false; } - } else if (type.equals(CounterType.M1M1) && tgt.hasKeyword("Persist")) { + } else if (type.equals(CounterType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) { return false; - } else if (type.equals(CounterType.P1P1) && tgt.hasKeyword("Undying")) { + } else if (type.equals(CounterType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java index ba2043efa14..5c500ecd7f2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -9,6 +9,7 @@ import forge.game.Game; import forge.game.GlobalRuleChange; import forge.game.ability.AbilityUtils; import forge.game.card.*; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -188,7 +189,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1)); - CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist"); + CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST); if (!aiPersistList.isEmpty()) { aiM1M1List = aiPersistList; } @@ -200,7 +201,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // do as P1P1 part CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1)); - CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying"); + CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING); if (!aiUndyingList.isEmpty()) { aiP1P1List = aiUndyingList; @@ -230,7 +231,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection aiList = CardLists.filterControlledBy(list, ai); aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount)); - CardCollection aiPersist = CardLists.getKeyword(aiList, "Persist"); + CardCollection aiPersist = CardLists.getKeyword(aiList, Keyword.PERSIST); if (!aiPersist.isEmpty()) { aiList = aiPersist; } @@ -253,7 +254,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // targeting ai creatures too CardCollection aiList = CardLists.filterControlledBy(list, ai); if (!aiList.isEmpty()) { - CardCollection aiListUndying = CardLists.getKeyword(aiList, "Undying"); + CardCollection aiListUndying = CardLists.getKeyword(aiList, Keyword.UNDYING); if (!aiListUndying.isEmpty()) { aiList = aiListUndying; } @@ -266,7 +267,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // need to target opponent creatures CardCollection oppList = CardLists.filterControlledBy(list, ai.getOpponents()); if (!oppList.isEmpty()) { - CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, "Undying"); + CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, Keyword.UNDYING); if (!oppListNotUndying.isEmpty()) { oppList = oppListNotUndying; } @@ -358,9 +359,9 @@ public class CountersRemoveAi extends SpellAbilityAi { } } } else { - if (options.contains(CounterType.M1M1) && target.hasKeyword("Persist")) { + if (options.contains(CounterType.M1M1) && target.hasKeyword(Keyword.PERSIST)) { return CounterType.M1M1; - } else if (options.contains(CounterType.P1P1) && target.hasKeyword("Undying")) { + } else if (options.contains(CounterType.P1P1) && target.hasKeyword(Keyword.UNDYING)) { return CounterType.M1M1; } for (CounterType type : options) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java index b252db592b1..49f864cd4e5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java @@ -8,6 +8,7 @@ import forge.game.Game; import forge.game.card.Card; import forge.game.card.CardCollectionView; import forge.game.card.CardPredicates; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -24,12 +25,14 @@ public abstract class DamageAiBase extends SpellAbilityAi { // Do not target a player if they aren't below 75% of our health. // Unless Lifelink will cancel the damage to us Card hostcard = sa.getHostCard(); - boolean lifelink = hostcard.hasKeyword("Lifelink"); - for (Card ench : hostcard.getEnchantedBy(false)) { - // Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink. - if (ench.hasSVar("LikeLifeLink")) { - if ("True".equals(ench.getSVar("LikeLifeLink"))) { - lifelink = true; + boolean lifelink = hostcard.hasKeyword(Keyword.LIFELINK); + if (!lifelink) { + for (Card ench : hostcard.getEnchantedBy(false)) { + // Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink. + if (ench.hasSVar("LikeLifeLink")) { + if ("True".equals(ench.getSVar("LikeLifeLink"))) { + lifelink = true; + } } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java index 8f2e82a901f..e43dfac49ab 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java @@ -8,6 +8,7 @@ import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CounterType; import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -268,7 +269,7 @@ public class DamageAllAi extends SpellAbilityAi { } }; - list = CardLists.getNotKeyword(list, "Indestructible"); + list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE); list = CardLists.filter(list, filterKillable); return list; diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index e62b2f230d7..1e78c806dda 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -10,6 +10,7 @@ import forge.game.ability.AbilityUtils; import forge.game.card.*; import forge.game.cost.Cost; import forge.game.cost.CostRemoveCounter; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -742,7 +743,7 @@ public class DamageDealAi extends DamageAiBase { if (o instanceof Card) { Card c = (Card) o; final int restDamage = ComputerUtilCombat.predictDamageTo(c, dmg, saMe.getHostCard(), false); - if (!c.hasKeyword("Indestructible") && ComputerUtilCombat.getDamageToKill(c) <= restDamage) { + if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && ComputerUtilCombat.getDamageToKill(c) <= restDamage) { if (c.getController().equals(ai)) { return false; } else { @@ -942,7 +943,7 @@ public class DamageDealAi extends DamageAiBase { for (Card c : creatures) { int power = c.getNetPower(); int toughness = c.getNetToughness(); - boolean canDie = !(c.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(c.getController(), c)); + boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c)); // Currently will target creatures with toughness 3+ (or power 5+) // and only if the creature can actually die, do not "underdrain" 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 e7e89fb8b95..742692e5c1d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -8,6 +8,7 @@ import forge.game.card.*; import forge.game.cost.Cost; import forge.game.cost.CostPart; import forge.game.cost.CostSacrifice; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -109,7 +110,7 @@ public class DestroyAi extends SpellAbilityAi { return false; } for (Card c : list) { - if (c.hasKeyword("Indestructible")) { + if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) { sa.getTargets().add(c); return true; } @@ -133,7 +134,7 @@ public class DestroyAi extends SpellAbilityAi { // Filter AI-specific targets if provided list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true); - list = CardLists.getNotKeyword(list, "Indestructible"); + list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE); if (CardLists.getNotType(list, "Creature").isEmpty()) { list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false); } @@ -160,7 +161,7 @@ public class DestroyAi extends SpellAbilityAi { return false; } //Check for undying - return (!c.hasKeyword("Undying") || c.getCounters(CounterType.P1P1) > 0); + return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterType.P1P1) > 0); } }); } @@ -294,7 +295,7 @@ public class DestroyAi extends SpellAbilityAi { if (list.isEmpty() || !CardLists.filterControlledBy(list, ai).isEmpty() - || CardLists.getNotKeyword(list, "Indestructible").isEmpty()) { + || CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) { return false; } } @@ -316,7 +317,7 @@ public class DestroyAi extends SpellAbilityAi { return false; } - CardCollection preferred = CardLists.getNotKeyword(list, "Indestructible"); + CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE); preferred = CardLists.filterControlledBy(preferred, ai.getOpponents()); 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 81e26eb32d4..c49cfbc3e26 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java @@ -7,6 +7,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -18,7 +19,7 @@ public class DestroyAllAi extends SpellAbilityAi { private static final Predicate predicate = new Predicate() { @Override public boolean apply(final Card c) { - return !(c.hasKeyword("Indestructible") || c.getSVar("SacMe").length() > 0); + return !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getSVar("SacMe").length() > 0); } }; diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java index 16f62f6c40f..59978ac5bee 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -17,6 +17,7 @@ import forge.game.GlobalRuleChange; import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.combat.CombatUtil; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -138,7 +139,8 @@ public class EffectAi extends SpellAbilityAi { final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() { @Override public boolean apply(final Card c) { - return (c.isInstant() || c.isSorcery()) && !c.hasKeyword("Rebound") && ComputerUtil.hasReasonToPlayCardThisTurn(ai, c); + return (c.isInstant() || c.isSorcery()) && !c.hasKeyword(Keyword.REBOUND) + && ComputerUtil.hasReasonToPlayCardThisTurn(ai, c); } }); 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 d597b5cfa6d..f9805e09954 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -7,6 +7,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; @@ -162,15 +163,15 @@ public class FightAi extends SpellAbilityAi { // Get sorted creature lists CardCollection aiCreatures = ai.getCreaturesInPlay(); CardCollection humCreatures = ai.getOpponents().getCreaturesInPlay(); - if ("Time to Feed".equals(sourceName)) { // flip sa - aiCreatures = CardLists.getTargetableCards(aiCreatures, tgtFight); - aiCreatures = ComputerUtil.getSafeTargets(ai, tgtFight, aiCreatures); - humCreatures = CardLists.getTargetableCards(humCreatures, sa); - } else { - aiCreatures = CardLists.getTargetableCards(aiCreatures, sa); - aiCreatures = ComputerUtil.getSafeTargets(ai, sa, aiCreatures); - humCreatures = CardLists.getTargetableCards(humCreatures, tgtFight); - } + if ("Time to Feed".equals(sourceName)) { // flip sa + aiCreatures = CardLists.getTargetableCards(aiCreatures, tgtFight); + aiCreatures = ComputerUtil.getSafeTargets(ai, tgtFight, aiCreatures); + humCreatures = CardLists.getTargetableCards(humCreatures, sa); + } else { + aiCreatures = CardLists.getTargetableCards(aiCreatures, sa); + aiCreatures = ComputerUtil.getSafeTargets(ai, sa, aiCreatures); + humCreatures = CardLists.getTargetableCards(humCreatures, tgtFight); + } ComputerUtilCard.sortByEvaluateCreature(aiCreatures); ComputerUtilCard.sortByEvaluateCreature(humCreatures); if (humCreatures.isEmpty() || aiCreatures.isEmpty()) { @@ -246,17 +247,19 @@ public class FightAi extends SpellAbilityAi { } return false; } + public static boolean canKill(Card fighter, Card opponent, int pumpAttack) { - if (opponent.getSVar("Targeting").equals("Dies")) { - return true; - } - if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed() - || opponent.getShieldCount() > 0 || ComputerUtil.canRegenerate(opponent.getController(), opponent)) { - return false; - } - if (fighter.hasKeyword("Deathtouch") || ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) { - return true; - } - return false; + if (opponent.getSVar("Targeting").equals("Dies")) { + return true; + } + if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed() || opponent.getShieldCount() > 0 + || ComputerUtil.canRegenerate(opponent.getController(), opponent)) { + return false; + } + if (fighter.hasKeyword(Keyword.DEATHTOUCH) + || ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) { + return true; + } + return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/FogAi.java b/forge-ai/src/main/java/forge/ai/ability/FogAi.java index 00a7386ea7d..09034ee8d7a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FogAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FogAi.java @@ -6,6 +6,7 @@ import forge.game.Game; import forge.game.GameObject; import forge.game.card.Card; import forge.game.card.CardPredicates; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -66,7 +67,7 @@ public class FogAi extends SpellAbilityAi { for (Card atk : game.getCombat().getAttackersOf(ai)) { if (game.getCombat().isUnblocked(atk)) { dmg += atk.getNetCombatDamage(); - } else if (atk.hasKeyword("Trample")) { + } else if (atk.hasKeyword(Keyword.TRAMPLE)) { dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java index 6c7bf2dc1aa..12292b4e44e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java @@ -9,6 +9,7 @@ import forge.game.ability.AbilityUtils; import forge.game.card.*; import forge.game.cost.CostPart; import forge.game.cost.CostRemoveCounter; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -170,7 +171,7 @@ public class ManaEffectAi extends SpellAbilityAi { } testSaNoCost.setActivatingPlayer(ai); if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSaNoCost) == AiPlayDecision.WillPlay) { - if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword("Haste") + if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword(Keyword.HASTE) && !ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) { // AI will waste a ritual in Main 1 unless the casted permanent is a haste creature continue; diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java index 3f26a5b377f..1b5dd90d4b1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java @@ -80,7 +80,7 @@ public class PermanentCreatureAi extends PermanentAi { } // save cards with flash for surprise blocking - if (card.hasKeyword("Flash") + if (card.withFlash(ai) && (ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize() || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) && ai.getManaPool().totalMana() <= 0 diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java index 89e3257d7f7..74aa623da32 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -11,6 +11,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.Spell; @@ -74,7 +75,7 @@ public class PlayAi extends SpellAbilityAi { return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost")); } - if (source != null && source.hasKeyword("Hideaway") && source.hasRemembered()) { + if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) { // AI is not very good at playing non-permanent spells this way, at least yet // (might be possible to enable it for Sorceries in Main1/Main2 if target is available, // but definitely not for most Instants) diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java index 2d6234fee8b..78d11c5cd30 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -14,6 +14,7 @@ import forge.game.cost.Cost; import forge.game.cost.CostPart; import forge.game.cost.CostRemoveCounter; import forge.game.cost.CostTapType; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -35,12 +36,7 @@ public class PumpAi extends PumpAiBase { if (cost == null) { return true; } - for (final CostPart part : cost.getCostParts()) { - if (part instanceof CostTapType) { - return true; - } - } - return false; + return cost.hasSpecificCostType(CostTapType.class); } @Override @@ -190,7 +186,7 @@ public class PumpAi extends PumpAiBase { srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount); if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { - if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying") + if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken()) { return true; } @@ -243,7 +239,7 @@ public class PumpAi extends PumpAiBase { srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount); if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { - if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying") + if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken()) { return true; } @@ -775,7 +771,7 @@ public class PumpAi extends PumpAiBase { if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) { if (source.isCreature()) { - if (!source.hasKeyword("Indestructible") && source.getNetToughness() + defense <= source.getDamage()) { + if (!source.hasKeyword(Keyword.INDESTRUCTIBLE) && source.getNetToughness() + defense <= source.getDamage()) { return false; } if (source.getNetToughness() + defense <= 0) { @@ -864,7 +860,7 @@ public class PumpAi extends PumpAiBase { final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; - final boolean isInfect = source.hasKeyword("Infect"); // Flesh-Eater Imp + final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) { @@ -970,7 +966,7 @@ public class PumpAi extends PumpAiBase { final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; - final boolean isInfect = source.hasKeyword("Infect"); + final boolean isInfect = source.hasKeyword(Keyword.INFECT); int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java index 5b2a6f756ba..89af0bf5722 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -12,6 +12,7 @@ import forge.game.Game; import forge.game.card.*; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.phase.Untap; @@ -135,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { || card.getNetCombatDamage() <= 0 || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) || ph.getPhase().isBefore(PhaseType.MAIN1) - || CardLists.getNotKeyword(ai.getCreaturesInPlay(), "Defender").isEmpty())) { + || CardLists.getNotKeyword(ai.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty())) { return false; } if (!ph.isPlayerTurn(ai) && (combat == null || !combat.isAttacking(card) || card.getNetCombatDamage() <= 0)) { @@ -193,21 +194,26 @@ public abstract class PumpAiBase extends SpellAbilityAi { return false; } } else if (keyword.endsWith("Flying")) { + CardCollectionView attackingFlyer = CardCollection.EMPTY; + if (combat != null) { + attackingFlyer = CardLists.getKeyword(combat.getAttackers(), Keyword.FLYING); + } + if (ph.isPlayerTurn(opp) && ph.getPhase() == PhaseType.COMBAT_DECLARE_ATTACKERS - && !CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty() - && !card.hasKeyword("Reach") + && !attackingFlyer.isEmpty() + && !card.hasKeyword(Keyword.REACH) && CombatUtil.canBlock(card) && ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) { return true; } - Predicate flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach")); + Predicate flyingOrReach = Predicates.or(CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH)); if (ph.isPlayerTurn(opp) && combat != null - && Iterables.any(combat.getAttackers(), CardPredicates.hasKeyword("Flying")) + && !attackingFlyer.isEmpty() && CombatUtil.canBlock(card)) { // Use defensively to destroy the opposing Flying creature when possible, or to block with an indestructible // creature buffed with Flying - for (Card c : CardLists.filter(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))) { + for (Card c : attackingFlyer) { if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c) && (card.getNetPower() >= c.getNetToughness() && card.getNetToughness() > c.getNetPower() || ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, card))) { @@ -225,7 +231,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { } else if (keyword.endsWith("Horsemanship")) { if (ph.isPlayerTurn(opp) && ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS) - && !CardLists.getKeyword(game.getCombat().getAttackers(), "Horsemanship").isEmpty() + && !CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.HORSEMANSHIP).isEmpty() && CombatUtil.canBlock(card) && ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) { return true; @@ -234,7 +240,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) || newPower <= 0 || CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)), - "Horsemanship").isEmpty()) { + Keyword.HORSEMANSHIP).isEmpty()) { return false; } } else if (keyword.endsWith("Intimidate")) { @@ -298,9 +304,9 @@ public abstract class PumpAiBase extends SpellAbilityAi { return false; } } else if (keyword.equals("First Strike")) { - if (card.hasKeyword("Double Strike")) { - return false; - } + if (card.hasKeyword(Keyword.DOUBLE_STRIKE)) { + return false; + } if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) { Card blocker = combat.getBlockers(card).get(0); if (ComputerUtilCombat.canDestroyAttacker(ai, card, blocker, combat, true) @@ -338,7 +344,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { || newPower <= 0 || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) || CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)), - "Flanking").isEmpty()) { + Keyword.FLANKING).isEmpty()) { return false; } } else if (keyword.startsWith("Trample")) { @@ -353,7 +359,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { if (newPower <= 0) { return false; } - if (combat != null && combat.isBlocking(card) && !card.hasKeyword("Wither")) { + if (combat != null && combat.isBlocking(card) && !card.hasKeyword(Keyword.WITHER)) { return true; } if ((ph.isPlayerTurn(opp)) @@ -362,7 +368,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { return false; } } else if (keyword.endsWith("Wither")) { - if (newPower <= 0 || card.hasKeyword("Infect")) { + if (newPower <= 0 || card.hasKeyword(Keyword.INFECT)) { return false; } return combat != null && ( combat.isBlocking(card) || (combat.isAttacking(card) && combat.isBlocked(card)) ); @@ -375,14 +381,14 @@ public abstract class PumpAiBase extends SpellAbilityAi { if (ph.isPlayerTurn(opp) || !CombatUtil.canAttack(card, opp) || newPower <= 0 || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) - || CardLists.getNotKeyword(opp.getCreaturesInPlay(), "Defender").isEmpty()) { + || CardLists.getNotKeyword(opp.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty()) { return false; } } else if (keyword.equals("Reach")) { if (ph.isPlayerTurn(ai) || !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS) - || CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty() - || card.hasKeyword("Flying") + || CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.FLYING).isEmpty() + || card.hasKeyword(Keyword.FLYING) || !CombatUtil.canBlock(card)) { return false; } @@ -409,7 +415,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { return false; } } else if (keyword.equals("Persist")) { - if (card.getBaseToughness() <= 1 || card.hasKeyword("Undying")) { + if (card.getBaseToughness() <= 1 || card.hasKeyword(Keyword.UNDYING)) { return false; } } else if (keyword.equals("Islandwalk")) { @@ -445,7 +451,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { return false; } } else if (keyword.endsWith("CARDNAME can attack as though it didn't have defender.")) { - if (!ph.isPlayerTurn(ai) || !card.hasKeyword("Defender") + if (!ph.isPlayerTurn(ai) || !card.hasKeyword(Keyword.DEFENDER) || ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN) || card.isTapped() || newPower <= 0) { return false; @@ -510,7 +516,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { if (c.getSVar("Targeting").equals("Dies") || c.getNetToughness() <= -defense) { return true; // can kill indestructible creatures } - return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword("Indestructible")); + return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword(Keyword.INDESTRUCTIBLE)); } }); // leaves all creatures that will be destroyed } // -X/-X end diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java index 6c14f2f5748..662ffc10a4c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java @@ -14,6 +14,7 @@ import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.combat.Combat; import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -85,7 +86,7 @@ public class PumpAllAi extends PumpAiBase { if (c.getNetToughness() <= -defense) { return true; // can kill indestructible creatures } - return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible")); + return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE)); } }); // leaves all creatures that will be destroyed human = CardLists.filter(human, new Predicate() { @@ -94,7 +95,7 @@ public class PumpAllAi extends PumpAiBase { if (c.getNetToughness() <= -defense) { return true; // can kill indestructible creatures } - return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible")); + return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE)); } }); // leaves all creatures that will be destroyed } // -X/-X end diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java index 79c634f2890..b66785bc0ee 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java @@ -8,6 +8,7 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.SpellAbility; @@ -82,7 +83,7 @@ public class SacrificeAi extends SpellAbilityAi { if (!destroy) { list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(sa)); } else { - if (!CardLists.getKeyword(list, "Indestructible").isEmpty()) { + if (!CardLists.getKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) { // human can choose to destroy indestructibles return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java index b1d5d86fe09..ec86828e6d8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -14,6 +14,7 @@ import forge.game.combat.Combat; import forge.game.cost.CostPart; import forge.game.cost.CostPutCounter; import forge.game.cost.CostRemoveCounter; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -200,29 +201,29 @@ public class TokenAi extends SpellAbilityAi { if (sa.canTarget(ai)) { sa.getTargets().add(ai); } else { - //Flash Foliage - CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); - list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); - list = CardLists.getTargetableCards(list, sa); - CardCollection betterList = CardLists.filter(list, new Predicate() { - @Override - public boolean apply(Card c) { - return c.getLethalDamage() == 1; - } - }); - if (!betterList.isEmpty()) { - list = betterList; - } - betterList = CardLists.getNotKeyword(list, "Trample"); - if (!betterList.isEmpty()) { - list = betterList; - } - if (!list.isEmpty()) { - sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); - } else { - return false; - } - + // Flash Foliage + CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), + ai.getOpponents()); + list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); + list = CardLists.getTargetableCards(list, sa); + CardCollection betterList = CardLists.filter(list, new Predicate() { + @Override + public boolean apply(Card c) { + return c.getLethalDamage() == 1; + } + }); + if (!betterList.isEmpty()) { + list = betterList; + } + betterList = CardLists.getNotKeyword(list, Keyword.TRAMPLE); + if (!betterList.isEmpty()) { + list = betterList; + } + if (!list.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); + } else { + return false; + } } } } diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java index d9f9db03937..210930785b7 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java @@ -140,13 +140,13 @@ public class SpellAbilityPicker { } } - private static boolean isSorcerySpeed(SpellAbility sa) { + private static boolean isSorcerySpeed(SpellAbility sa, Player player) { // TODO: Can we use the actual rules engine for this instead of trying to do the logic ourselves? if (sa instanceof PlayLandAbility) { return false; } if (sa.isSpell()) { - return !sa.getHostCard().isInstant() && !sa.getHostCard().hasKeyword("Flash"); + return !sa.getHostCard().isInstant() && !sa.getHostCard().withFlash(player); } if (sa.getRestrictions().isPwAbility()) { return !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."); @@ -167,7 +167,7 @@ public class SpellAbilityPicker { if (currentPhase.isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) { List candidateSAs2 = new ArrayList(); for (SpellAbility sa : candidateSAs) { - if (!isSorcerySpeed(sa)) { + if (!isSorcerySpeed(sa, player)) { System.err.println("Not sorcery: " + sa); candidateSAs2.add(sa); } diff --git a/forge-core/src/main/java/forge/util/Visitor.java b/forge-core/src/main/java/forge/util/Visitor.java index b8e6bf4ba6d..2a36ee062ee 100644 --- a/forge-core/src/main/java/forge/util/Visitor.java +++ b/forge-core/src/main/java/forge/util/Visitor.java @@ -1,11 +1,22 @@ package forge.util; public abstract class Visitor { + /** + * visit the object + * the Visitor should return true it can be visit again + * returning false means the outer function can stop + * + * @param object + * @return boolean + */ public abstract boolean visit(T object); - public void visitAll(Iterable objects) { + public boolean visitAll(Iterable objects) { for (T obj : objects) { - visit(obj); + if (!visit(obj)) { + return false; + } } + return true; } } diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 4f5fdc5c46a..f588e5a222a 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -527,25 +527,57 @@ public class Game { return cards; } - public Card getCardState(final Card card) { - for (final Card c : getCardsInGame()) { - if (card.equals(c)) { - return c; - } + private static class CardStateVisitor extends Visitor { + Card found = null; + Card old = null; + + private CardStateVisitor(final Card card) { + this.old = card; } - return card; + + @Override + public boolean visit(Card object) { + if (object.equals(old)) { + found = object; + } + return found == null; + } + + public Card getFound() { + return found == null ? found : old; + } + } + + public Card getCardState(final Card card) { + CardStateVisitor visit = new CardStateVisitor(card); + this.forEachCardInGame(visit); + return visit.getFound(); } // Allows visiting cards in game without allocating a temporary list. public void forEachCardInGame(Visitor visitor) { for (final Player player : getPlayers()) { - visitor.visitAll(player.getZone(ZoneType.Graveyard).getCards()); - visitor.visitAll(player.getZone(ZoneType.Hand).getCards()); - visitor.visitAll(player.getZone(ZoneType.Library).getCards()); - visitor.visitAll(player.getZone(ZoneType.Battlefield).getCards(false)); - visitor.visitAll(player.getZone(ZoneType.Exile).getCards()); - visitor.visitAll(player.getZone(ZoneType.Command).getCards()); - visitor.visitAll(player.getInboundTokens()); + if (!visitor.visitAll(player.getZone(ZoneType.Graveyard).getCards())) { + return; + } + if (!visitor.visitAll(player.getZone(ZoneType.Hand).getCards())) { + return; + } + if (!visitor.visitAll(player.getZone(ZoneType.Library).getCards())) { + return; + } + if (!visitor.visitAll(player.getZone(ZoneType.Battlefield).getCards(false))) { + return; + } + if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) { + return; + } + if (!visitor.visitAll(player.getZone(ZoneType.Command).getCards())) { + return; + } + if (!visitor.visitAll(player.getInboundTokens())) { + return; + } } visitor.visitAll(getStackZone().getCards()); } diff --git a/forge-game/src/main/java/forge/game/card/CardLists.java b/forge-game/src/main/java/forge/game/card/CardLists.java index 7b1c477f089..cbb9a147018 100644 --- a/forge-game/src/main/java/forge/game/card/CardLists.java +++ b/forge-game/src/main/java/forge/game/card/CardLists.java @@ -236,7 +236,11 @@ public class CardLists { return CardLists.filter(cardList, CardPredicates.isTargetableBy(source)); } - public static CardCollection getKeyword(Iterable cardList, String keyword) { + public static CardCollection getKeyword(Iterable cardList, final String keyword) { + return CardLists.filter(cardList, CardPredicates.hasKeyword(keyword)); + } + + public static CardCollection getKeyword(Iterable cardList, final Keyword keyword) { return CardLists.filter(cardList, CardPredicates.hasKeyword(keyword)); } @@ -244,6 +248,10 @@ public class CardLists { return CardLists.filter(cardList, Predicates.not(CardPredicates.hasKeyword(keyword))); } + public static CardCollection getNotKeyword(Iterable cardList, final Keyword keyword) { + return CardLists.filter(cardList, Predicates.not(CardPredicates.hasKeyword(keyword))); + } + public static int getAmountOfKeyword(final Iterable cardList, final String keyword) { int nKeyword = 0; for (final Card c : cardList) { diff --git a/forge-game/src/main/java/forge/game/card/CardPredicates.java b/forge-game/src/main/java/forge/game/card/CardPredicates.java index d462bddcabb..b30734de635 100644 --- a/forge-game/src/main/java/forge/game/card/CardPredicates.java +++ b/forge-game/src/main/java/forge/game/card/CardPredicates.java @@ -23,6 +23,7 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import forge.game.combat.CombatUtil; +import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -84,6 +85,15 @@ public final class CardPredicates { }; } + public static final Predicate hasKeyword(final Keyword keyword) { + return new Predicate() { + @Override + public boolean apply(final Card c) { + return c.hasKeyword(keyword); + } + }; + } + public static final Predicate containsKeyword(final String keyword) { return new Predicate() { @Override diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 90d0cd59bad..763268e813f 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -8,6 +8,7 @@ import forge.card.mana.ManaCost; import forge.game.Direction; import forge.game.GameEntityView; import forge.game.combat.Combat; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerView; import forge.game.zone.ZoneType; @@ -978,11 +979,11 @@ public class CardView extends GameEntityView { } void updateKeywords(Card c, CardState state) { c.updateKeywordsCache(state); - set(TrackableProperty.HasDeathtouch, c.hasKeyword("Deathtouch", state)); - set(TrackableProperty.HasHaste, c.hasKeyword("Haste", state)); - set(TrackableProperty.HasInfect, c.hasKeyword("Infect", state)); - set(TrackableProperty.HasStorm, c.hasKeyword("Storm", state)); - set(TrackableProperty.HasTrample, c.hasKeyword("Trample", state)); + set(TrackableProperty.HasDeathtouch, c.hasKeyword(Keyword.DEATHTOUCH, state)); + set(TrackableProperty.HasHaste, c.hasKeyword(Keyword.HASTE, state)); + set(TrackableProperty.HasInfect, c.hasKeyword(Keyword.INFECT, state)); + set(TrackableProperty.HasStorm, c.hasKeyword(Keyword.STORM, state)); + set(TrackableProperty.HasTrample, c.hasKeyword(Keyword.TRAMPLE, state)); set(TrackableProperty.BlockAdditional, c.getAmountOfKeyword("CARDNAME can block an additional creature each combat.", state)); updateAbilityText(c, state); } diff --git a/forge-game/src/main/java/forge/game/combat/AttackingBand.java b/forge-game/src/main/java/forge/game/combat/AttackingBand.java index 6058db6db28..3158cbcea73 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackingBand.java +++ b/forge-game/src/main/java/forge/game/combat/AttackingBand.java @@ -7,6 +7,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; +import forge.game.keyword.Keyword; public class AttackingBand { private CardCollection attackers = new CardCollection(); @@ -32,7 +33,7 @@ public class AttackingBand { return false; } - int bandingCreatures = CardLists.getKeyword(band, "Banding").size(); + int bandingCreatures = CardLists.getKeyword(band, Keyword.BANDING).size(); int neededBandingCreatures = shareDamage ? 1 : band.size() - 1; if (neededBandingCreatures <= bandingCreatures) { // For starting a band, only one can be non-Banding diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index a025cf5db21..388990b4ec1 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -659,7 +659,7 @@ public class CombatUtil { return 0; } // TODO: remove CantBeBlockedByAmount LT2 - if (attacker.hasKeyword("CantBeBlockedByAmount LT2") || attacker.hasKeyword("Menace")) { + if (attacker.hasKeyword("CantBeBlockedByAmount LT2") || attacker.hasKeyword(Keyword.MENACE)) { return 2; } else if (attacker.hasKeyword("CantBeBlockedByAmount LT3")) { return 3; @@ -703,7 +703,7 @@ public class CombatUtil { for (final Card attacker : attackers) { if (CombatUtil.canBlock(attacker, blocker, combat)) { boolean must = true; - if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("Menace")) { + if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) { final List possibleBlockers = Lists.newArrayList(defendersArmy); possibleBlockers.remove(blocker); if (!CombatUtil.canBeBlocked(attacker, possibleBlockers, combat)) { @@ -807,7 +807,7 @@ public class CombatUtil { for (final Card attacker : attackersWithLure) { if (CombatUtil.canBeBlocked(attacker, combat, defender) && CombatUtil.canBlock(attacker, blocker)) { boolean canBe = true; - if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("Menace")) { + if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) { final List blockers = combat.getDefenderPlayerByAttacker(attacker).getCreaturesInPlay(); blockers.remove(blocker); if (!CombatUtil.canBeBlocked(attacker, blockers, combat)) { @@ -825,7 +825,7 @@ public class CombatUtil { if (CombatUtil.canBeBlocked(attacker, combat, defender) && CombatUtil.canBlock(attacker, blocker) && combat.isAttacking(attacker)) { boolean canBe = true; - if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("Menace")) { + if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) { final List blockers = combat.getDefenderPlayerByAttacker(attacker).getCreaturesInPlay(); blockers.remove(blocker); if (!CombatUtil.canBeBlocked(attacker, blockers, combat)) { @@ -1004,7 +1004,7 @@ public class CombatUtil { return false; } - if ((attacker.hasKeyword("Creatures with power greater than CARDNAME's power can't block it.") || attacker.hasKeyword("Skulk")) + if ((attacker.hasKeyword("Creatures with power greater than CARDNAME's power can't block it.") || attacker.hasKeyword(Keyword.SKULK)) && attacker.getNetPower() < blocker.getNetPower()) { return false; } diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 4ffc1d30099..a10ca915a34 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -33,6 +33,7 @@ import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.Cost; import forge.game.event.*; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.player.PlayerController.ManaPaymentPurpose; @@ -507,7 +508,7 @@ public class PhaseHandler implements java.io.Serializable { } for (final Card attacker : combat.getAttackers()) { - final boolean shouldTapForAttack = !attacker.hasKeyword("Vigilance") && !attacker.hasKeyword("Attacking doesn't cause CARDNAME to tap."); + final boolean shouldTapForAttack = !attacker.hasKeyword(Keyword.VIGILANCE) && !attacker.hasKeyword("Attacking doesn't cause CARDNAME to tap."); if (shouldTapForAttack) { // set tapped to true without firing triggers because it may affect propaganda costs attacker.setTapped(true); 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 fdc08f31644..e06f7ad7252 100644 --- a/forge-game/src/main/java/forge/game/phase/Untap.java +++ b/forge-game/src/main/java/forge/game/phase/Untap.java @@ -36,6 +36,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.spellability.SpellAbility; @@ -257,7 +258,7 @@ public class Untap extends Phase { @Override public boolean apply(final Card c) { - return ((c.isPhasedOut() && c.isDirectlyPhasedOut()) || c.hasKeyword("Phasing")); + return ((c.isPhasedOut() && c.isDirectlyPhasedOut()) || c.hasKeyword(Keyword.PHASING)); } }); @@ -268,7 +269,7 @@ public class Untap extends Phase { for (final Card c : list) { if (c.isPhasedOut()) { c.phase(); - } else if (c.hasKeyword("Phasing")) { + } else if (c.hasKeyword(Keyword.PHASING)) { // 702.23g If an object would simultaneously phase out directly // and indirectly, it just phases out indirectly. if (c.isAura()) { diff --git a/forge-game/src/main/java/forge/game/player/AchievementTracker.java b/forge-game/src/main/java/forge/game/player/AchievementTracker.java index 434a2467a1a..7f7d0d0d82e 100644 --- a/forge-game/src/main/java/forge/game/player/AchievementTracker.java +++ b/forge-game/src/main/java/forge/game/player/AchievementTracker.java @@ -5,6 +5,7 @@ import java.util.Set; import forge.card.ColorSet; import forge.game.card.Card; +import forge.game.keyword.Keyword; import forge.game.spellability.SpellAbility; //class for storing information during a game that is used at the end of the game to determine achievements @@ -28,7 +29,7 @@ public class AchievementTracker { public void onSpellResolve(final SpellAbility spell) { final Card card = spell.getHostCard(); - if (card.hasKeyword("Epic")) { + if (card.hasKeyword(Keyword.EPIC)) { challengesCompleted.add("Epic"); } } 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 b2a2f6c54b9..685eb2f827b 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -530,7 +530,7 @@ public class Player extends GameEntity implements Comparable { //String additionalLog = ""; source.addDealtDamageToPlayerThisTurn(getName(), amount); - boolean infect = source.hasKeyword("Infect") + boolean infect = source.hasKeyword(Keyword.INFECT) || hasKeyword("All damage is dealt to you as though its source had infect."); if (infect) { diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 103ea7aea66..8f9a469506d 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -35,6 +35,7 @@ import forge.game.cost.Cost; import forge.game.cost.CostPart; import forge.game.cost.CostPartMana; import forge.game.cost.CostRemoveCounter; +import forge.game.keyword.Keyword; import forge.game.mana.Mana; import forge.game.player.Player; import forge.game.staticability.StaticAbility; @@ -1606,7 +1607,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public boolean tracksManaSpent() { if (hostCard == null || hostCard.getRules() == null) { return false; } - if (hostCard.hasKeyword("Sunburst")) { + if (hostCard.hasKeyword(Keyword.SUNBURST)) { return true; } String text = hostCard.getRules().getOracleText(); diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/special/CardZoomer.java b/forge-gui-desktop/src/main/java/forge/toolbox/special/CardZoomer.java index 3867f0a8d6c..aa9af422a1e 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/special/CardZoomer.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/special/CardZoomer.java @@ -22,6 +22,7 @@ import forge.StaticData; import forge.assets.FSkinProp; import forge.game.card.Card; import forge.game.card.CardView.CardStateView; +import forge.game.keyword.Keyword; import forge.gui.SOverlayUtils; import forge.item.PaperCard; import forge.toolbox.FOverlay; @@ -230,7 +231,7 @@ public enum CardZoomer { if (cardName.isEmpty()) { cardName = thisCard.getCard().getAlternateState().getName(); } PaperCard pc = StaticData.instance().getCommonCards().getCard(cardName); - boolean isAftermath = pc != null && Card.getCardForUi(pc).hasKeyword("Aftermath"); + boolean isAftermath = pc != null && Card.getCardForUi(pc).hasKeyword(Keyword.AFTERMATH); return thisCard.getCard().isFaceDown() || isSplitRotated ? 0 : isAftermath ? 270 : 90; // rotate Aftermath splits the other way to correctly show the right split (graveyard) half } diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index 7a0f2d39fad..e8f606962b1 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -25,6 +25,7 @@ import forge.card.mana.ManaCost; import forge.game.card.Card; import forge.game.card.CardView; import forge.game.card.CardView.CardStateView; +import forge.game.keyword.Keyword; import forge.game.card.CounterType; import forge.gui.CardContainer; import forge.item.PaperCard; @@ -434,7 +435,7 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl } else { if (!card.isFaceDown()) { // no need to draw mana symbols on face down split cards (e.g. manifested) PaperCard pc = StaticData.instance().getCommonCards().getCard(card.getName()); - int ofs = pc != null && Card.getCardForUi(pc).hasKeyword("Aftermath") ? -12 : 12; + int ofs = pc != null && Card.getCardForUi(pc).hasKeyword(Keyword.AFTERMATH) ? -12 : 12; drawManaCost(g, card.getCurrentState().getManaCost(), ofs); drawManaCost(g, card.getAlternateState().getManaCost(), -ofs); diff --git a/forge-gui-mobile/src/forge/card/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index 977331d214c..cc82751f248 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -25,6 +25,7 @@ import forge.card.mana.ManaCost; import forge.game.card.Card; import forge.game.card.CardView; import forge.game.card.CardView.CardStateView; +import forge.game.keyword.Keyword; import forge.game.card.CounterType; import forge.item.IPaperCard; import forge.item.PaperCard; @@ -417,7 +418,7 @@ public class CardRenderer { float dy = manaSymbolSize / 2 + Utils.scale(5); PaperCard pc = StaticData.instance().getCommonCards().getCard(card.getName()); - if (Card.getCardForUi(pc).hasKeyword("Aftermath")){ + if (Card.getCardForUi(pc).hasKeyword(Keyword.AFTERMATH)){ dy *= -1; // flip card costs for Aftermath cards } diff --git a/forge-gui/src/main/java/forge/match/input/InputAttack.java b/forge-gui/src/main/java/forge/match/input/InputAttack.java index 30059c70403..cf46d7c3e17 100644 --- a/forge-gui/src/main/java/forge/match/input/InputAttack.java +++ b/forge-gui/src/main/java/forge/match/input/InputAttack.java @@ -26,6 +26,7 @@ import forge.game.card.CardPredicates.Presets; import forge.game.combat.AttackingBand; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerView; import forge.game.zone.ZoneType; @@ -313,7 +314,7 @@ public class InputAttack extends InputSyncronizedBase { private boolean isBandingPossible() { final CardCollectionView possibleAttackers = playerAttacks.getCardsIn(ZoneType.Battlefield); for (final Card c : possibleAttackers) { - if ((c.hasKeyword("Banding") || c.hasStartOfKeyword("Bands with Other")) && + if ((c.hasKeyword(Keyword.BANDING) || c.hasStartOfKeyword("Bands with Other")) && CombatUtil.canAttack(c, currentDefender)) { return true; } diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 0433328c302..94f599f8b22 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -280,7 +280,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont map.put(null, damageDealt); } else { final List vBlockers = CardView.getCollection(blockers); - if ((attacker.hasKeyword("Trample") && defender != null) || (blockers.size() > 1)) { + if ((attacker.hasKeyword(Keyword.TRAMPLE) && defender != null) || (blockers.size() > 1)) { final CardView vAttacker = CardView.get(attacker); final GameEntityView vDefender = GameEntityView.get(defender); final Map result = getGui().assignDamage(vAttacker, vBlockers, damageDealt, @@ -2296,7 +2296,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont if (forgeCard.isPermanent() && !forgeCard.isAura()) { if (forgeCard.isCreature()) { if (!repeatLast) { - if (forgeCard.hasKeyword("Haste")) { + if (forgeCard.hasKeyword(Keyword.HASTE)) { lastSummoningSickness = true; } else { lastSummoningSickness = getGui().confirm(forgeCard.getView(),