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 ed8b003bf80..1c086665875 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -1,11 +1,27 @@ package forge.ai.ability; +import java.util.List; +import java.util.Map; + +import forge.ai.ComputerUtil; +import forge.ai.ComputerUtilCard; +import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; +import forge.game.Game; +import forge.game.GlobalRuleChange; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardCollectionView; +import forge.game.card.CardLists; +import forge.game.card.CardPredicates; import forge.game.card.CounterType; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.spellability.TargetRestrictions; +import forge.game.zone.ZoneType; public class CountersRemoveAi extends SpellAbilityAi { @@ -55,12 +71,11 @@ public class CountersRemoveAi extends SpellAbilityAi { final String type = sa.getParam("CounterType"); - // TODO currently, not targeted if (sa.usesTargeting()) { - return false; + return doTgt(ai, sa, false); } - if (!type.matches("Any")) { + if (!type.matches("Any") && !type.matches("All")) { final int currCounters = sa.getHostCard().getCounters(CounterType.valueOf(type)); if (currCounters < 1) { return false; @@ -70,21 +85,295 @@ public class CountersRemoveAi extends SpellAbilityAi { return super.checkApiLogic(ai, sa); } - @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - // AI needs to be expanded, since this function can be pretty complex - // based on what the - // expected targets could be - boolean chance = true; + private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) { + final Card source = sa.getHostCard(); + final Game game = ai.getGame(); - // TODO - currently, not targeted, only for Self + final String type = sa.getParam("CounterType"); + final String amountStr = sa.getParam("CounterNum"); - // Note: Not many cards even use Trigger and Remove Counters. And even - // fewer are not mandatory - // Since the targeting portion of this would be what + // remove counter with Time might use Exile Zone too + final TargetRestrictions tgt = sa.getTargetRestrictions(); + CardCollection list = new CardCollection(game.getCardsIn(tgt.getZone())); + // need to targetable + list = CardLists.getTargetableCards(list, sa); + if (list.isEmpty()) { + return false; + } - return chance; + if (sa.hasParam("AITgts")) { + String aiTgts = sa.getParam("AITgts"); + CardCollection prefList = CardLists.getValidCards(list, aiTgts.split(","), ai, source, sa); + if (!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) { + list = prefList; + } + } + + boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); + + if (type.matches("All")) { + // Logic Part for Vampire Hexmage + // Break Dark Depths + if (!ai.isCardInPlay("Marit Lage") || noLegendary) { + CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); + depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), + CardPredicates.hasCounter(CounterType.ICE, 3)); + + if (!depthsList.isEmpty()) { + sa.getTargets().add(depthsList.getFirst()); + return true; + } + } + + // Get rid of Planeswalkers: + list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); + list = CardLists.filter(list, CardPredicates.isTargetableBy(sa)); + + CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANEWALKERS, + CardPredicates.hasCounter(CounterType.LOYALTY, 5)); + + if (!planeswalkerList.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList)); + return true; + } + + } else if (type.matches("Any")) { + // variable amount for Hex Parasite + int amount; + boolean xPay = false; + if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) { + final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai); + + if (manaLeft == 0) { + return false; + } + amount = manaLeft; + xPay = true; + } else { + amount = AbilityUtils.calculateAmount(source, amountStr, sa); + } + // try to remove them from Dark Depths and Planeswalkers too + + if (!ai.isCardInPlay("Marit Lage") || noLegendary) { + CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); + depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), + CardPredicates.hasCounter(CounterType.ICE)); + + if (!depthsList.isEmpty()) { + Card depth = depthsList.getFirst(); + int ice = depth.getCounters(CounterType.ICE); + if (amount >= ice) { + sa.getTargets().add(depth); + if (xPay) { + source.setSVar("PayX", Integer.toString(ice)); + } + return true; + } + } + } + + // Get rid of Planeswalkers: + list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); + list = CardLists.filter(list, CardPredicates.isTargetableBy(sa)); + + CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANEWALKERS, + CardPredicates.hasLessCounter(CounterType.LOYALTY, amount)); + + if (!planeswalkerList.isEmpty()) { + Card best = ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList); + sa.getTargets().add(best); + if (xPay) { + source.setSVar("PayX", Integer.toString(best.getCurrentLoyalty())); + } + return true; + } + + // some rules only for amount = 1 + if (!xPay) { + // do as M1M1 part + CardCollection aiList = CardLists.filterControlledBy(list, ai); + + CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1)); + + CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist"); + if (!aiPersistList.isEmpty()) { + aiM1M1List = aiPersistList; + } + + if (!aiM1M1List.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List)); + return true; + } + + // do as P1P1 part + CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1)); + CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying"); + + if (!aiUndyingList.isEmpty()) { + aiP1P1List = aiUndyingList; + } + if (!aiP1P1List.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiP1P1List)); + return true; + } + + // fallback to remove any counter from opponent + CardCollection oppList = CardLists.filterControlledBy(list, ai.getOpponents()); + oppList = CardLists.filter(oppList, CardPredicates.hasCounters()); + if (!oppList.isEmpty()) { + final Card best = ComputerUtilCard.getBestAI(oppList); + + for (final CounterType aType : best.getCounters().keySet()) { + if (!ComputerUtil.isNegativeCounter(aType, best)) { + sa.getTargets().add(best); + return true; + } + } + } + } + } else if (type.equals("M1M1")) { + // no special amount for that one yet + int amount = AbilityUtils.calculateAmount(source, amountStr, sa); + CardCollection aiList = CardLists.filterControlledBy(list, ai); + aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount)); + + CardCollection aiPersist = CardLists.getKeyword(aiList, "Persist"); + if (!aiPersist.isEmpty()) { + aiList = aiPersist; + } + + // TODO do not remove -1/-1 counters from cards which does need + // them for abilities + + if (!aiList.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList)); + return true; + } + + } else if (type.equals("P1P1")) { + // no special amount for that one yet + int amount = AbilityUtils.calculateAmount(source, amountStr, sa); + + list = CardLists.filter(list, CardPredicates.hasCounter(CounterType.P1P1, amount)); + + // currently only logic for Bloodcrazed Hoplite, but add logic for + // targeting ai creatures too + CardCollection aiList = CardLists.filterControlledBy(list, ai); + if (!aiList.isEmpty()) { + CardCollection aiListUndying = CardLists.getKeyword(aiList, "Undying"); + if (!aiListUndying.isEmpty()) { + aiList = aiListUndying; + } + if (!aiList.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList)); + return true; + } + } + + // need to target opponent creatures + CardCollection oppList = CardLists.filterControlledBy(list, ai.getOpponents()); + if (!oppList.isEmpty()) { + CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, "Undying"); + if (!oppListNotUndying.isEmpty()) { + oppList = oppListNotUndying; + } + + if (!oppList.isEmpty()) { + sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList)); + return true; + } + } + + } else if (type.equals("TIME")) { + int amount; + boolean xPay = false; + // Timecrafting has X R + if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) { + final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai); + + if (manaLeft == 0) { + return false; + } + amount = manaLeft; + xPay = true; + } else { + amount = AbilityUtils.calculateAmount(source, amountStr, sa); + } + + CardCollection timeList = CardLists.filter(list, CardPredicates.hasLessCounter(CounterType.TIME, amount)); + + if (!timeList.isEmpty()) { + Card best = ComputerUtilCard.getBestAI(timeList); + + int timeCount = best.getCounters(CounterType.TIME); + sa.getTargets().add(best); + if (xPay) { + source.setSVar("PayX", Integer.toString(timeCount)); + } + return true; + } + } + if (mandatory) { + sa.getTargets().add(ComputerUtilCard.getWorstAI(list)); + return true; + } + return false; } + @Override + protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return doTgt(aiPlayer, sa, mandatory); + } + + /* + * (non-Javadoc) + * + * @see forge.ai.SpellAbilityAi#chooseNumber(forge.game.player.Player, + * forge.game.spellability.SpellAbility, int, int, java.util.Map) + */ + @Override + public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map params) { + // TODO Auto-generated method stub + return super.chooseNumber(player, sa, min, max, params); + } + + /* + * (non-Javadoc) + * + * @see forge.ai.SpellAbilityAi#chooseCounterType(java.util.List, + * forge.game.spellability.SpellAbility, java.util.Map) + */ + @Override + public CounterType chooseCounterType(List options, SpellAbility sa, Map params) { + if (options.size() <= 1) { + return super.chooseCounterType(options, sa, params); + } + Player ai = sa.getActivatingPlayer(); + Card target = (Card) params.get("Target"); + + if (target.getController().isOpponentOf(ai)) { + // if its a Planeswalker try to remove Loyality first + if (target.isPlaneswalker()) { + return CounterType.LOYALTY; + } + for (CounterType type : options) { + if (!ComputerUtil.isNegativeCounter(type, target)) { + return type; + } + } + } else { + if (options.contains(CounterType.M1M1) && target.hasKeyword("Persist")) { + return CounterType.M1M1; + } else if (options.contains(CounterType.P1P1) && target.hasKeyword("Undying")) { + return CounterType.M1M1; + } + for (CounterType type : options) { + if (ComputerUtil.isNegativeCounter(type, target)) { + return type; + } + } + } + return super.chooseCounterType(options, sa, params); + } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java index daa819eb3d0..c9b64ce35d8 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java @@ -8,7 +8,6 @@ import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.player.PlayerController; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; import forge.game.zone.Zone; import forge.game.zone.ZoneType; @@ -17,6 +16,7 @@ import java.util.Map; import org.apache.commons.lang3.tuple.Pair; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; public class CountersRemoveEffect extends SpellAbilityEffect { @Override @@ -24,14 +24,20 @@ public class CountersRemoveEffect extends SpellAbilityEffect { final StringBuilder sb = new StringBuilder(); final String counterName = sa.getParam("CounterType"); + final String num = sa.getParam("CounterNum"); - final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); + int amount = 0; + if (!num.equals("All") && !num.equals("Remembered")) { + amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa); + }; sb.append("Remove "); if (sa.hasParam("UpTo")) { sb.append("up to "); } - if ("Any".matches(counterName)) { + if ("All".matches(counterName)) { + sb.append("all counter"); + } else if ("Any".matches(counterName)) { if (amount == 1) { sb.append("a counter"); } else { @@ -64,32 +70,29 @@ public class CountersRemoveEffect extends SpellAbilityEffect { final Card card = sa.getHostCard(); final Game game = card.getGame(); final String type = sa.getParam("CounterType"); + final String num = sa.getParam("CounterNum"); int cntToRemove = 0; - if (!sa.getParam("CounterNum").equals("All") && !sa.getParam("CounterNum").equals("Remembered")) { - cntToRemove = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); + if (!num.equals("All") && !num.equals("Remembered")) { + cntToRemove = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa); } CounterType counterType = null; - try { - counterType = AbilityUtils.getCounterType(type, sa); - } catch (Exception e) { - if (!type.matches("Any")) { + if (!type.equals("Any") && !type.equals("All")) { + try { + counterType = AbilityUtils.getCounterType(type, sa); + } catch (Exception e) { System.out.println("Counter type doesn't match, nor does an SVar exist with the type name."); return; } } - final TargetRestrictions tgt = sa.getTargetRestrictions(); + boolean rememberRemoved = sa.hasParam("RememberRemoved"); - boolean rememberRemoved = false; - if (sa.hasParam("RememberRemoved")) { - rememberRemoved = true; - } for (final Player tgtPlayer : getTargetPlayers(sa)) { // Removing energy - if ((tgt == null) || tgtPlayer.canBeTargetedBy(sa)) { - if (sa.getParam("CounterNum").equals("All")) { + if (!sa.usesTargeting() || tgtPlayer.canBeTargetedBy(sa)) { + if (num.equals("All")) { cntToRemove = tgtPlayer.getCounters(counterType); } tgtPlayer.subtractCounter(counterType, cntToRemove); @@ -97,9 +100,14 @@ public class CountersRemoveEffect extends SpellAbilityEffect { } for (final Card tgtCard : getTargetCards(sa)) { - if ((tgt == null) || tgtCard.canBeTargetedBy(sa)) { + if (!sa.usesTargeting() || tgtCard.canBeTargetedBy(sa)) { final Zone zone = game.getZoneOf(tgtCard); - if (sa.getParam("CounterNum").equals("All")) { + if (type.equals("All")) { + for (Map.Entry e : tgtCard.getCounters().entrySet()) { + tgtCard.subtractCounter(e.getKey(), e.getValue()); + } + continue; + } else if (num.equals("All")) { cntToRemove = tgtCard.getCounters(counterType); } else if (sa.getParam("CounterNum").equals("Remembered")) { cntToRemove = tgtCard.getCountersAddedBy(card, counterType); @@ -107,13 +115,21 @@ public class CountersRemoveEffect extends SpellAbilityEffect { PlayerController pc = sa.getActivatingPlayer().getController(); - if (type.matches("Any")) { + if (type.equals("Any")) { while (cntToRemove > 0 && tgtCard.hasCounters()) { final Map tgtCounters = tgtCard.getCounters(); - - CounterType chosenType = pc.chooseCounterType(ImmutableList.copyOf(tgtCounters.keySet()), sa, "Select type of counters to remove"); - String prompt = "Select the number of " + chosenType.getName() + " counters to remove"; - int chosenAmount = pc.chooseNumber(sa, prompt, 1, Math.min(cntToRemove, tgtCounters.get(chosenType))); + Map params = Maps.newHashMap(); + params.put("Target", tgtCard); + + String prompt = "Select type of counters to remove"; + CounterType chosenType = pc.chooseCounterType( + ImmutableList.copyOf(tgtCounters.keySet()), sa, prompt, params); + prompt = "Select the number of " + chosenType.getName() + " counters to remove"; + int max = Math.min(cntToRemove, tgtCounters.get(chosenType)); + params = Maps.newHashMap(); + params.put("Target", tgtCard); + params.put("CounterType", chosenType); + int chosenAmount = pc.chooseNumber(sa, prompt, 1, max, params); tgtCard.subtractCounter(chosenType, chosenAmount); if (rememberRemoved) { @@ -124,14 +140,19 @@ public class CountersRemoveEffect extends SpellAbilityEffect { cntToRemove -= chosenAmount; } } else { + cntToRemove = Math.min(cntToRemove, tgtCard.getCounters(counterType)); + if (zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) { - if (sa.hasParam("UpTo")) - cntToRemove = pc.chooseNumber(sa, "Select the number of " + type + " counters to remove", 0, cntToRemove); + if (sa.hasParam("UpTo")) { + Map params = Maps.newHashMap(); + params.put("Target", tgtCard); + params.put("CounterType", type); + String title = "Select the number of " + type + " counters to remove"; + cntToRemove = pc.chooseNumber(sa, title, 0, cntToRemove, params); + } + } if (rememberRemoved) { - if (cntToRemove > tgtCard.getCounters(counterType)) { - cntToRemove = tgtCard.getCounters(counterType); - } for (int i = 0; i < cntToRemove; i++) { card.addRemembered(Pair.of(counterType, i)); }