diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index eafafadcba2..a8f6e1e833b 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -1497,7 +1497,7 @@ public class ComputerUtil { } } - all.addAll(ai.getCardsActivableInExternalZones(true)); + all.addAll(ai.getCardsActivatableInExternalZones(true)); all.addAll(ai.getCardsIn(ZoneType.Hand)); for (final Card c : all) { @@ -1538,7 +1538,7 @@ public class ComputerUtil { public static boolean hasAFogEffect(final Player defender, final Player ai, boolean checkingOther) { final CardCollection all = new CardCollection(defender.getCardsIn(ZoneType.Battlefield)); - all.addAll(defender.getCardsActivableInExternalZones(true)); + all.addAll(defender.getCardsActivatableInExternalZones(true)); // TODO check if cards can be viewed instead if (!checkingOther) { all.addAll(defender.getCardsIn(ZoneType.Hand)); @@ -1590,7 +1590,7 @@ public class ComputerUtil { public static int possibleNonCombatDamage(final Player ai, final Player enemy) { int damage = 0; final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); - all.addAll(ai.getCardsActivableInExternalZones(true)); + all.addAll(ai.getCardsActivatableInExternalZones(true)); all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(Presets.PERMANENTS))); for (final Card c : all) { diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 40a2cd2e474..2933ebf3be0 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1299,6 +1299,8 @@ public class PlayerControllerAi extends PlayerController { name = ComputerUtilCard.getMostProminentCardName(cards); } else if (logic.equals("CursedScroll")) { name = SpecialCardAi.CursedScroll.chooseCard(player, sa); + } else if (logic.equals("PithingNeedle")) { + name = SpecialCardAi.PithingNeedle.chooseCard(player, sa); } if (!StringUtils.isBlank(name)) { diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index 753dec93015..c51a7a7d4af 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -293,6 +293,47 @@ public class SpecialCardAi { } } + public static class PithingNeedle { + public static String chooseCard(final Player ai, final SpellAbility sa) { + // TODO Remove names of cards already named by other Pithing Needles + Card best = null; + + CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa); + if (!oppPerms.isEmpty()) { + return chooseCardFromList(oppPerms).getName(); + } + + CardCollection visibleZones = CardLists.getValidCards(ai.getOpponents().getCardsIn(Lists.newArrayList(ZoneType.Graveyard, ZoneType.Exile)), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa); + if (!visibleZones.isEmpty()) { + // If nothing on the battlefield has a nonmana ability choose something + return chooseCardFromList(visibleZones).getName(); + } + + return chooseNonBattlefieldName(); + } + + static public Card chooseCardFromList(CardCollection cardlist) { + Card best = ComputerUtilCard.getBestPlaneswalkerAI(cardlist); + if (best != null) { + // No planeswalkers choose something! + return best; + } + + best = ComputerUtilCard.getBestCreatureAI(cardlist); + if (best == null) { + // If nothing on the battlefield has a nonmana ability choose something + Collections.shuffle(cardlist); + best = cardlist.getFirst(); + } + + return best; + } + + static public String chooseNonBattlefieldName() { + return "Liliana of the Veil"; + } + } + // Deathgorge Scavenger public static class DeathgorgeScavenger { public static boolean consider(final Player ai, final SpellAbility sa) { 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 0bfc1d85d43..86446e186e5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -1068,7 +1068,7 @@ public class DamageDealAi extends DamageAiBase { CardCollection cards = new CardCollection(); cards.addAll(ai.getCardsIn(ZoneType.Hand)); cards.addAll(ai.getCardsIn(ZoneType.Battlefield)); - cards.addAll(ai.getCardsActivableInExternalZones(true)); + cards.addAll(ai.getCardsActivatableInExternalZones(true)); for (Card c : cards) { if (c.getZone().getPlayer() != null && c.getZone().getPlayer() != ai && c.mayPlay(ai).isEmpty()) { continue; 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 f1e8ff3755a..b0fe07d4dcf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -13,7 +13,6 @@ import forge.ai.AiCardMemory; import forge.ai.AiController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; import forge.ai.PlayerControllerAi; import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; @@ -82,21 +81,11 @@ public class EffectAi extends SpellAbilityAi { randomReturn = true; } } else if (logic.equals("Fog")) { - if (phase.isPlayerTurn(sa.getActivatingPlayer())) { - return false; - } - if (!phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { - return false; - } - if (!game.getStack().isEmpty()) { - return false; - } - if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) { - return false; - } - if (!ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) { + FogAi fogAi = new FogAi(); + if (!fogAi.canPlayAI(ai, sa)) { return false; } + final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); @@ -258,6 +247,21 @@ public class EffectAi extends SpellAbilityAi { if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) { return false; } + } else if (logic.equals("PeaceTalks")) { + Player nextPlayer = game.getNextPlayerAfter(ai); + + // If opponent doesn't have creatures, preventing attacks don't mean as much + if (nextPlayer.getCreaturesInPlay().isEmpty()) { + return false; + } + + // Only cast Peace Talks after you attack just in case you have creatures + if (!phase.is(PhaseType.MAIN2)) { + return false; + } + + // Create a pseudo combat and see if my life is in danger + return randomReturn; } else if (logic.equals("Bribe")) { Card host = sa.getHostCard(); Combat combat = game.getCombat(); 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 2fe66864e3f..44c89517017 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FogAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FogAi.java @@ -10,13 +10,15 @@ import forge.ai.PlayerControllerAi; import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.GameObject; +import forge.game.ability.ApiType; import forge.game.card.Card; -import forge.game.card.CardPredicates; +import forge.game.card.CardLists; +import forge.game.combat.Combat; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.util.Aggregates; +import forge.game.zone.ZoneType; public class FogAi extends SpellAbilityAi { @@ -27,12 +29,70 @@ public class FogAi extends SpellAbilityAi { protected boolean canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); final Card hostCard = sa.getHostCard(); + final Combat combat = game.getCombat(); // Don't cast it, if the effect is already in place if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) { return false; } + // TODO Test if we can even Fog successfully + if (handleMemoryCheck(ai, sa)) { + return true; + } + + // Only cast when Stack is empty, so Human uses spells/abilities first + if (!game.getStack().isEmpty()) { + return false; + } + + // TODO Only cast outside of combat if I won't be able to cast inside of combat + if (combat == null) { + return false; + } + + // AI should only activate this during Opponents Declare Blockers phase + if (!game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai) || + !game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + // TODO Be careful of effects that don't let you cast spells during combat + return false; + } + + int remainingLife = ComputerUtilCombat.lifeThatWouldRemain(ai, combat); + int dmg = ai.getLife() - remainingLife; + + // Count the number of Fog spells in hand + int fogs = countAvailableFogs(ai); + if (fogs > 2 && dmg > 2) { + // Playing a fog deck. If you got them play them. + return true; + } + if (dmg > 2 && + hostCard.hasKeyword(Keyword.BUYBACK) && + CardLists.count(ai.getCardsIn(ZoneType.Battlefield), Card::isLand) > 3) { + // Constant mists sacrifices a land to buyback. But if AI is running it, they are probably ok sacrificing some lands + return true; + } + + if ("SeriousDamage".equals(sa.getParam("AILogic"))) { + if (dmg > ai.getLife() / 4) { + return true; + } else if (dmg >= 5) { + return true; + } else if (ai.getLife() < ai.getStartingLife() / 3) { + return true; + } + } + // TODO Compare to poison counters? + + // Cast it if life is in danger + return ComputerUtilCombat.lifeInDanger(ai, game.getCombat()); + } + + private boolean handleMemoryCheck(Player ai, SpellAbility sa) { + Card hostCard = sa.getHostCard(); + Game game = ai.getGame(); + // if card would be destroyed, react and use immediately if it's not own turn if ((AiCardMemory.isRememberedCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) && (!game.getStack().isEmpty()) @@ -54,43 +114,32 @@ public class FogAi extends SpellAbilityAi { AiCardMemory.rememberCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT); } } + return false; + } - // AI should only activate this during Human's Declare Blockers phase - if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) { - return false; - } - if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { - return false; - } - - // Only cast when Stack is empty, so Human uses spells/abilities first - if (!game.getStack().isEmpty()) { - return false; - } - - if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) { - int dmg = 0; - for (Card atk : game.getCombat().getAttackersOf(ai)) { - if (game.getCombat().isUnblocked(atk)) { - dmg += atk.getNetCombatDamage(); - } else if (atk.hasKeyword(Keyword.TRAMPLE)) { - dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness); + private int countAvailableFogs(Player ai) { + int fogs = 0; + for (Card c : ai.getCardsActivatableInExternalZones(false)) { + for (SpellAbility ability : c.getSpellAbilities()) { + if (ability.getApi().equals(ApiType.Fog)) { + fogs++; + break; } } - - if (dmg > ai.getLife() / 4) { - return true; - } else if (dmg >= 5) { - return true; - } else if (ai.getLife() < ai.getStartingLife() / 3) { - return true; - } } - // Cast it if life is in danger - return ComputerUtilCombat.lifeInDanger(ai, game.getCombat()); + for (Card c : ai.getCardsIn(ZoneType.Hand)) { + for (SpellAbility ability : c.getSpellAbilities()) { + if (ability.getApi().equals(ApiType.Fog)) { + fogs++; + break; + } + } + } + return fogs; } + @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { // AI should only activate this during Human's turn diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java index a4d9b8215c5..dad49ca6304 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import forge.ai.ComputerUtilAbility; +import forge.ai.ComputerUtilCard; import forge.game.Game; import forge.game.ability.AbilityFactory; import forge.game.card.Card; @@ -9,6 +10,7 @@ import forge.game.card.CardLists; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; +import forge.util.MyRandom; /** * AbilityFactory for Creature Spells. @@ -18,7 +20,31 @@ public class PermanentNoncreatureAi extends PermanentAi { @Override protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { - return !"Never".equals(aiLogic) && !"DontCast".equals(aiLogic); + if ("Never".equals(aiLogic) || "DontCast".equals(aiLogic)) { + return false; + } + + Game game = ai.getGame(); + + if ("PithingNeedle".equals(aiLogic)) { + // Make sure theres something in play worth Needlings. + // Planeswalker or equipment or something + + CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa); + if (oppPerms.isEmpty()) { + return false; + } + + Card card = ComputerUtilCard.getBestPlaneswalkerAI(oppPerms); + if (card != null) { + return true; + } + + // 5 percent chance to cast per opposing card with a non mana ability + return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size(); + } + + return true; } /** diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java index 00b6ecf2ec5..82d5d4fdbcf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -374,7 +374,7 @@ public class UntapAi extends SpellAbilityAi { return false; } - if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { // Blockers already set. Are there any dangerous unblocked creatures? Sort by creature that will deal the most damage? Card card = ComputerUtilCombat.mostDangerousAttacker(list, ai, activeCombat, true); diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index 4fc6e84b717..cf4b14cdd2b 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -1091,6 +1091,18 @@ public class CardProperty { if (!property.startsWith("without") && !card.hasStartOfUnHiddenKeyword(property.substring(4))) { return false; } + } else if (property.equals("hasNonmanaAbilities")) { + boolean hasAbilities = false; + for(SpellAbility sa : card.getSpellAbilities()) { + if (sa.isActivatedAbility() && !sa.isManaAbility()) { + hasAbilities = true; + break; + } + } + + if (!hasAbilities) { + return false; + } } else if (property.startsWith("activated")) { if (!card.activatedThisTurn()) { return false; 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 2c2629e6725..2690c84fd08 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -1336,7 +1336,7 @@ public class Player extends GameEntity implements Comparable { return cards; } else if (zoneType == ZoneType.Flashback) { - return getCardsActivableInExternalZones(true); + return getCardsActivatableInExternalZones(true); } PlayerZone zone = getZone(zoneType); @@ -1379,7 +1379,7 @@ public class Player extends GameEntity implements Comparable { return CardLists.filter(getCardsIn(zone), CardPredicates.nameEquals(cardName)); } - public CardCollectionView getCardsActivableInExternalZones(boolean includeCommandZone) { + public CardCollectionView getCardsActivatableInExternalZones(boolean includeCommandZone) { final CardCollection cl = new CardCollection(); cl.addAll(getZone(ZoneType.Graveyard).getCardsPlayerCanActivate(this)); diff --git a/forge-gui/res/cardsfolder/p/peace_talks.txt b/forge-gui/res/cardsfolder/p/peace_talks.txt index b6d15da42ed..81aeb41dd11 100644 --- a/forge-gui/res/cardsfolder/p/peace_talks.txt +++ b/forge-gui/res/cardsfolder/p/peace_talks.txt @@ -1,7 +1,7 @@ Name:Peace Talks ManaCost:1 W Types:Sorcery -A:SP$ Effect | StaticAbilities$ STCantAttack,STCantTarget,STCantTargetPlayer | Duration$ ThisTurnAndNextTurn | SpellDescription$ This turn and next turn, creatures can't attack, and players and permanents can't be the targets of spells or activated abilities. +A:SP$ Effect | AILogic$ PeaceTalks | Stackable$ False | StaticAbilities$ STCantAttack,STCantTarget,STCantTargetPlayer | Duration$ ThisTurnAndNextTurn | SpellDescription$ This turn and next turn, creatures can't attack, and players and permanents can't be the targets of spells or activated abilities. SVar:STCantAttack:Mode$ CantAttack | EffectZone$ Command | ValidCard$ Creature | Description$ Creatures can't attack. SVar:STCantTarget:Mode$ CantTarget | ValidCard$ Permanent | EffectZone$ Command | ValidSA$ Spell,Activated | Description$ Permanents can't be the targets of spells or activated abilities. SVar:STCantTargetPlayer:Mode$ CantTarget | ValidPlayer$ Player | EffectZone$ Command | ValidSA$ Spell,Activated | Description$ Players can't be the targets of spells or activated abilities. diff --git a/forge-gui/res/cardsfolder/p/pithing_needle.txt b/forge-gui/res/cardsfolder/p/pithing_needle.txt index 167d1bdfbbd..ebdb4d0c7f7 100644 --- a/forge-gui/res/cardsfolder/p/pithing_needle.txt +++ b/forge-gui/res/cardsfolder/p/pithing_needle.txt @@ -1,8 +1,9 @@ Name:Pithing Needle ManaCost:1 Types:Artifact +A:SP$ PermanentNoncreature | AILogic$ PithingNeedle K:ETBReplacement:Other:DBNameCard -SVar:DBNameCard:DB$ NameCard | Defined$ You | SpellDescription$ As CARDNAME enters the battlefield, choose a card name. +SVar:DBNameCard:DB$ NameCard | Defined$ You | AILogic$ PithingNeedle | SpellDescription$ As CARDNAME enters the battlefield, choose a card name. S:Mode$ CantBeActivated | ValidCard$ Card.NamedCard | ValidSA$ Activated.nonManaAbility | Description$ Activated abilities of sources with the chosen name can't be activated unless they're mana abilities. AI:RemoveDeck:Random Oracle:As Pithing Needle enters the battlefield, choose a card name.\nActivated abilities of sources with the chosen name can't be activated unless they're mana abilities. diff --git a/forge-gui/src/main/java/forge/model/FModel.java b/forge-gui/src/main/java/forge/model/FModel.java index abcf9415d0f..0bb1ef2610c 100644 --- a/forge-gui/src/main/java/forge/model/FModel.java +++ b/forge-gui/src/main/java/forge/model/FModel.java @@ -223,7 +223,11 @@ public final class FModel { magicDb.setBrawlPredicate(formats.get("Brawl").getFilterRules()); magicDb.setFilteredHandsEnabled(preferences.getPrefBoolean(FPref.FILTERED_HANDS)); - magicDb.setMulliganRule(MulliganDefs.MulliganRule.valueOf(preferences.getPref(FPref.MULLIGAN_RULE))); + try { + magicDb.setMulliganRule(MulliganDefs.MulliganRule.valueOf(preferences.getPref(FPref.MULLIGAN_RULE))); + } catch(Exception e) { + magicDb.setMulliganRule(MulliganDefs.MulliganRule.London); + } blocks = new StorageBase<>("Block definitions", new CardBlock.Reader(ForgeConstants.BLOCK_DATA_DIR + "blocks.txt", magicDb.getEditions())); //setblockLands