diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index dfa47c071e7..0524435a4f5 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -359,7 +359,7 @@ public class AiAttackController { } }); - final List notNeededAsBlockers = new CardCollection(attackers); + final CardCollection notNeededAsBlockers = new CardCollection(attackers); // don't hold back creatures that can't block any of the human creatures final List blockers = getPossibleBlockers(attackers, opponentsAttackers, true); @@ -378,7 +378,7 @@ public class AiAttackController { int thresholdMod = 0; int lastAcceptableBaselineLife = 0; if (pilotsNonAggroDeck) { - lastAcceptableBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers)); + lastAcceptableBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, notNeededAsBlockers); if (!ai.isCardInPlay("Laboratory Maniac")) { // AI is getting milled out thresholdMod += 3 - Math.min(ai.getCardsIn(ZoneType.Library).size(), 3); @@ -397,7 +397,7 @@ public class AiAttackController { continue; } notNeededAsBlockers.add(c); - int currentBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers)); + int currentBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, notNeededAsBlockers); // AI doesn't know from what it will lose, so it might still keep an unnecessary blocker back sometimes if (currentBaselineLife == Integer.MIN_VALUE) { notNeededAsBlockers.remove(c); @@ -839,6 +839,7 @@ public class AiAttackController { if (attackMax != -1 && combat.getAttackers().size() >= attackMax) return aiAggression; + // TODO if lifeInDanger use chance to hold back some if (canAttackWrapper(attacker, defender) && isEffectiveAttacker(ai, attacker, combat, defender)) { combat.addAttacker(attacker, defender); } @@ -848,7 +849,7 @@ public class AiAttackController { } // Cards that are remembered to attack anyway (e.g. temporarily stolen creatures) - if (ai.getController() instanceof PlayerControllerAi) { + if (ai.getController().isAI()) { // Only do this if |ai| is actually an AI - as we could be trying to predict how the human will attack. for (Card attacker : this.attackers) { if (AiCardMemory.isRememberedCard(ai, attacker, AiCardMemory.MemorySet.MANDATORY_ATTACKERS)) { @@ -894,7 +895,7 @@ public class AiAttackController { aiAggression = 6; for (Card attacker : this.attackers) { // reached max, breakup - if (attackMax != -1 && combat.getAttackers().size() >= attackMax) + if (combat.getAttackers().size() >= attackMax) break; if (canAttackWrapper(attacker, defender) && shouldAttack(attacker, this.blockers, combat, defender)) { combat.addAttacker(attacker, defender); diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index f2e99eb4cf4..2ce4527e901 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -1061,7 +1061,7 @@ public class AiBlockController { // remove all attackers that can't be blocked anyway for (final Card a : attackers) { - if (!CombatUtil.canBeBlocked(a, ai)) { + if (!CombatUtil.canBeBlocked(a, null, ai)) { // pass null to skip redundant checks for performance attackersLeft.remove(a); } } diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 2c9294a2784..ad80719175d 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1055,11 +1055,9 @@ public class AiController { // Cheaper Spectacle costs should be preferred // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) - if (a.isSpectacle() && !b.isSpectacle() - && a.getPayCosts().getTotalMana().getCMC() < b.getPayCosts().getTotalMana().getCMC()) { + if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) { return 1; - } else if (b.isSpectacle() && !a.isSpectacle() - && b.getPayCosts().getTotalMana().getCMC() < a.getPayCosts().getTotalMana().getCMC()) { + } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) { return 1; } } @@ -1088,6 +1086,9 @@ public class AiController { if (source.hasSVar("AIPriorityModifier")) { p += Integer.parseInt(source.getSVar("AIPriorityModifier")); } + if (ComputerUtilCard.isCardRemAIDeck(source)) { + p -= 10; + } // don't play equipments before having any creatures if (source.isEquipment() && noCreatures) { p -= 9; @@ -1691,6 +1692,7 @@ public class AiController { Iterables.removeIf(saList, new Predicate() { @Override public boolean apply(final SpellAbility spellAbility) { //don't include removedAI cards if somehow the AI can play the ability or gain control of unsupported card + // TODO allow when experimental profile? return spellAbility instanceof LandAbility || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard())); } }); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index bc8446c56a8..690b6bf5f5f 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -385,6 +385,9 @@ public class ComputerUtilCard { * @return the card */ public static Card getBestCreatureAI(final Iterable list) { + if (Iterables.size(list) == 1) { + return Iterables.get(list, 0); + } return Aggregates.itemWithMax(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator); } @@ -397,6 +400,9 @@ public class ComputerUtilCard { * @return a {@link forge.game.card.Card} object. */ public static Card getWorstCreatureAI(final Iterable list) { + if (Iterables.size(list) == 1) { + return Iterables.get(list, 0); + } return Aggregates.itemWithMin(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator); } @@ -410,6 +416,9 @@ public class ComputerUtilCard { * @return a {@link forge.game.card.Card} object. */ public static Card getBestCreatureToBounceAI(final CardCollectionView list) { + if (Iterables.size(list) == 1) { + return Iterables.get(list, 0); + } final int tokenBonus = 60; Card biggest = null; int biggestvalue = -1; 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 3c6cf36ed51..ddfd254e478 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -1565,8 +1565,9 @@ public class AttachAi extends SpellAbilityAi { boolean canBeBlocked = false; for (Player opp : ai.getOpponents()) { - if (CombatUtil.canBeBlocked(card, opp)) { + if (CombatUtil.canBeBlocked(card, null, opp)) { canBeBlocked = true; + break; } } 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 91989e70f61..a330e4c36b7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -69,10 +69,10 @@ public class EffectAi extends SpellAbilityAi { randomReturn = true; } } else if (logic.equals("Fog")) { - if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) { + if (phase.isPlayerTurn(sa.getActivatingPlayer())) { return false; } - if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + if (!phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return false; } if (!game.getStack().isEmpty()) { @@ -216,9 +216,12 @@ public class EffectAi extends SpellAbilityAi { } else if (logic.equals("Fight")) { return FightAi.canFightAi(ai, sa, 0, 0); } else if (logic.equals("Pump")) { - if (SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(ai, sa)) { + List options = ai.getCreaturesInPlay(); + if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && !options.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(options)); return true; } + return false; } else if (logic.equals("Burn")) { // for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack) SpellAbility burn = sa.getSubAbility(); @@ -245,7 +248,7 @@ public class EffectAi extends SpellAbilityAi { Card host = sa.getHostCard(); Combat combat = game.getCombat(); if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host) - && game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS) + && phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) { AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something return true; diff --git a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java index a12fc2fe440..f5cc92f0134 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java @@ -102,7 +102,7 @@ public final class EncodeAi extends SpellAbilityAi { public boolean apply(final Card c) { boolean canAttackOpponent = false; for (Player opp : ai.getOpponents()) { - if (CombatUtil.canAttack(c, opp) && !CombatUtil.canBeBlocked(c, opp)) { + if (CombatUtil.canAttack(c, opp) && !CombatUtil.canBeBlocked(c, null, opp)) { canAttackOpponent = true; break; } 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 206946cc037..f742073fdc7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -336,7 +336,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { Keyword.FLANKING).isEmpty(); } else if (keyword.startsWith("Trample")) { return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card))) - && CombatUtil.canBeBlocked(card, opp) + && CombatUtil.canBeBlocked(card, null, opp) && !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && newPower > 1 && Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)); 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 bbfd148241e..6408d2e8738 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -555,10 +555,19 @@ public class CombatUtil { return false; } } + + // Unblockable check + for (final Card ca : attacker.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (stAb.applyAbility("CantBlockBy", attacker, null)) { + return false; + } + } + } + return canBeBlocked(attacker, defendingPlayer); } - // can the attacker be blocked at all? /** *

* canBeBlocked. diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index 9b78fc03bfe..03cb8ed4baf 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -302,7 +302,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone if (mode.equals("CantAttack")) { return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target); - } else if (mode.equals("CantBlockBy") && target instanceof Card) { + } else if (mode.equals("CantBlockBy")) { // null allowed, so no instanceof check return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target); } else if (mode.equals("CanAttackIfHaste")) { return StaticAbilityCantAttackBlock.applyCanAttackHasteAbility(this, card, target); diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java index ce8d05b32dd..46c2e4c4658 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java @@ -115,7 +115,7 @@ public class StaticAbilityCantAttackBlock { if (stAb.hasParam("ValidBlocker")) { boolean stillblock = true; for (final String v : stAb.getParam("ValidBlocker").split(",")) { - if (blocker.isValid(v, host.getController(), host, stAb)) { + if (blocker != null && blocker.isValid(v, host.getController(), host, stAb)) { stillblock = false; //Dragon Hunter check if (v.contains("withoutReach") && blocker.hasStartOfKeyword("IfReach")) { diff --git a/forge-gui/res/cardsfolder/a/azorius_knight_arbiter.txt b/forge-gui/res/cardsfolder/a/azorius_knight_arbiter.txt index 9461b74550e..4e529de9d82 100644 --- a/forge-gui/res/cardsfolder/a/azorius_knight_arbiter.txt +++ b/forge-gui/res/cardsfolder/a/azorius_knight_arbiter.txt @@ -3,5 +3,5 @@ ManaCost:3 W U Types:Creature Human Knight PT:2/5 K:Vigilance -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. Oracle:Vigilance\nAzorius Knight-Arbiter can't be blocked. diff --git a/forge-gui/res/cardsfolder/g/gray_harbor_merfolk.txt b/forge-gui/res/cardsfolder/g/gray_harbor_merfolk.txt index a38a79a1537..606a5783a9b 100644 --- a/forge-gui/res/cardsfolder/g/gray_harbor_merfolk.txt +++ b/forge-gui/res/cardsfolder/g/gray_harbor_merfolk.txt @@ -2,7 +2,7 @@ Name:Gray Harbor Merfolk ManaCost:1 U Types:Creature Merfolk Rogue PT:0/3 -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. S:Mode$ Continuous | Affected$ Card.Self | AddPower$ 2 | IsPresent$ Creature.IsCommander+YouCtrl,Planeswalker.IsCommander+YouCtrl | Description$ CARDNAME gets +2/+0 as long as you control a commander that's a creature or planeswalker. AI:RemoveDeck:NonCommander Oracle:Gray Harbor Merfolk can't be blocked.\nGray Harbor Merfolk gets +2/+0 as long as you control a commander that's a creature or planeswalker. diff --git a/forge-gui/res/cardsfolder/j/jhessian_infiltrator.txt b/forge-gui/res/cardsfolder/j/jhessian_infiltrator.txt index 09dd545d4ec..eb6fa1275ce 100644 --- a/forge-gui/res/cardsfolder/j/jhessian_infiltrator.txt +++ b/forge-gui/res/cardsfolder/j/jhessian_infiltrator.txt @@ -2,5 +2,5 @@ Name:Jhessian Infiltrator ManaCost:G U Types:Creature Human Rogue PT:2/2 -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. Oracle:Jhessian Infiltrator can't be blocked. diff --git a/forge-gui/res/cardsfolder/p/phantom_warrior.txt b/forge-gui/res/cardsfolder/p/phantom_warrior.txt index 3c257693854..7c47b1f2898 100644 --- a/forge-gui/res/cardsfolder/p/phantom_warrior.txt +++ b/forge-gui/res/cardsfolder/p/phantom_warrior.txt @@ -2,5 +2,5 @@ Name:Phantom Warrior ManaCost:1 U U Types:Creature Illusion Warrior PT:2/2 -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. Oracle:Phantom Warrior can't be blocked. diff --git a/forge-gui/res/cardsfolder/p/plasma_elemental.txt b/forge-gui/res/cardsfolder/p/plasma_elemental.txt index a41d195d109..d1794eeb84c 100644 --- a/forge-gui/res/cardsfolder/p/plasma_elemental.txt +++ b/forge-gui/res/cardsfolder/p/plasma_elemental.txt @@ -2,5 +2,5 @@ Name:Plasma Elemental ManaCost:5 U Types:Creature Elemental PT:4/1 -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. Oracle:Plasma Elemental can't be blocked. diff --git a/forge-gui/res/cardsfolder/s/soulsworn_spirit.txt b/forge-gui/res/cardsfolder/s/soulsworn_spirit.txt index 59341dc2c68..fe2d5aa30e4 100644 --- a/forge-gui/res/cardsfolder/s/soulsworn_spirit.txt +++ b/forge-gui/res/cardsfolder/s/soulsworn_spirit.txt @@ -2,7 +2,7 @@ Name:Soulsworn Spirit ManaCost:3 U Types:Creature Spirit PT:2/1 -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ Detain | TriggerDescription$ When CARDNAME enters the battlefield, detain target creature an opponent controls. (Until your next turn, that creature can't attack or block and its activated abilities can't be activated.) SVar:Detain:DB$ Pump | KW$ HIDDEN CARDNAME can't attack or block. & HIDDEN CARDNAME's activated abilities can't be activated. | IsCurse$ True | Duration$ UntilYourNextTurn | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature your opponent controls to detain. SVar:PlayMain1:TRUE diff --git a/forge-gui/res/cardsfolder/t/triton_shorestalker.txt b/forge-gui/res/cardsfolder/t/triton_shorestalker.txt index 50ef209536c..24f8c9e0c8e 100644 --- a/forge-gui/res/cardsfolder/t/triton_shorestalker.txt +++ b/forge-gui/res/cardsfolder/t/triton_shorestalker.txt @@ -2,5 +2,5 @@ Name:Triton Shorestalker ManaCost:U Types:Creature Merfolk Rogue PT:1/1 -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. Oracle:Triton Shorestalker can't be blocked. diff --git a/forge-gui/res/cardsfolder/v/vedalken_infiltrator.txt b/forge-gui/res/cardsfolder/v/vedalken_infiltrator.txt index fdbd97071e5..821a55555c9 100644 --- a/forge-gui/res/cardsfolder/v/vedalken_infiltrator.txt +++ b/forge-gui/res/cardsfolder/v/vedalken_infiltrator.txt @@ -2,7 +2,7 @@ Name:Vedalken Infiltrator ManaCost:1 U Types:Creature Vedalken Rogue PT:1/3 -K:Unblockable +S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked. S:Mode$ Continuous | Affected$ Card.Self | AddPower$ 1 | Condition$ Metalcraft | Description$ Metalcraft — CARDNAME gets +1/+0 as long as you control three or more artifacts. SVar:BuffedBy:Artifact Oracle:Vedalken Infiltrator can't be blocked.\nMetalcraft — Vedalken Infiltrator gets +1/+0 as long as you control three or more artifacts.