diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index f6ed3536e9b..f6658344486 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import forge.game.GameEntity; import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; @@ -616,6 +617,28 @@ public class SpecialCardAi { } } + // Goblin Polka Band + public static class GoblinPolkaBand { + public static boolean consider(final Player ai, final SpellAbility sa) { + int maxPotentialTgts = Lists.newArrayList(Iterables.filter(ai.getOpponents().getCreaturesInPlay(), CardPredicates.Presets.UNTAPPED)).size(); + int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R"); + + int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts); + if (numTgts == 0) { + return false; + } + + // Set Announce + sa.getHostCard().setSVar("TgtNum", String.valueOf(numTgts)); + + // Simulate random targeting + List validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true); + sa.resetTargets(); + sa.getTargets().addAll(Aggregates.random(validTgts, numTgts)); + return true; + } + } + // Guilty Conscience public static class GuiltyConscience { public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List list) { @@ -1157,6 +1180,33 @@ public class SpecialCardAi { } } + // Power Struggle + public static class PowerStruggle { + public static boolean considerFirstTarget(final Player ai, final SpellAbility sa) { + Card firstTgt = (Card)Aggregates.random(sa.getTargetRestrictions().getAllCandidates(sa, true)); + if (firstTgt != null) { + sa.getTargets().add(firstTgt); + return true; + } else { + return false; + } + } + + public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) { + Card firstTgt = sa.getParent().getTargetCard(); + Iterable candidates = Iterables.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), + Predicates.and(CardPredicates.sharesCardTypeWith(firstTgt), CardPredicates.isTargetableBy(sa))); + Card secondTgt = Aggregates.random(candidates); + if (secondTgt != null) { + sa.resetTargets(); + sa.getTargets().add(secondTgt); + return true; + } else { + return false; + } + } + } + // Price of Progress public static class PriceOfProgress { public static boolean consider(final Player ai, final SpellAbility sa) { 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 849005b58a2..3690b981f2c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -24,6 +24,7 @@ import forge.game.card.CardPredicates.Presets; import forge.game.card.CounterEnumType; import forge.game.combat.Combat; import forge.game.keyword.Keyword; +import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerPredicates; @@ -146,6 +147,16 @@ public class ChooseCardAi extends SpellAbilityAi { return checkApiLogic(ai, sa); } + protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) { + String aiLogic = sa.getParamOrDefault("AILogic", ""); + + if (aiLogic.equals("AtOppEOT")) { + return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN); + } + + return super.checkPhaseRestrictions(ai, sa, ph); + } + /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) */ diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java index a951f670074..34089303a29 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java @@ -4,6 +4,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Lists; import forge.ai.ComputerUtilCard; +import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -80,6 +81,10 @@ public class ControlExchangeAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); + if ("PowerStruggle".equals(sa.getParam("AILogic"))) { + return SpecialCardAi.PowerStruggle.considerSecondTarget(aiPlayer, sa); + } + // for TrigTwoTargets logic, only get the opponents' cards for the first target CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ? aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) : 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 3446454bcde..8a1195e9443 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -436,6 +436,11 @@ public class PumpAi extends PumpAiBase { final TargetRestrictions tgt = sa.getTargetRestrictions(); sa.resetTargets(); + + if ("PowerStruggle".equals(sa.getParam("AILogic"))) { + return SpecialCardAi.PowerStruggle.considerFirstTarget(ai, sa); + } + if (sa.hasParam("TargetingPlayer") && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) { Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); sa.setTargetingPlayer(targetingPlayer); diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java index 295cc2f5a9b..3a088d45dba 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java @@ -1,11 +1,6 @@ package forge.ai.ability; -import forge.ai.AiController; -import forge.ai.AiProps; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCost; -import forge.ai.PlayerControllerAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.cost.Cost; @@ -48,6 +43,10 @@ public class TapAi extends TapAiBase { final Card source = sa.getHostCard(); final Cost abCost = sa.getPayCosts(); + if ("GoblinPolkaBand".equals(sa.getParam("AILogic"))) { + return SpecialCardAi.GoblinPolkaBand.consider(ai, sa); + } + if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { 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 35398c0bf32..4c26284fea4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -98,7 +98,12 @@ public class TokenAi extends SpellAbilityAi { sa.getRootAbility().setXManaCostPaid(x); } if (x <= 0) { - return false; // 0 tokens or 0 toughness token(s) + if ("RandomPT".equals(sa.getParam("AILogic"))) { + // e.g. Necropolis of Azar - we're guaranteed at least 1 toughness from the ability + x = 1; + } else { + return false; // 0 tokens or 0 toughness token(s) + } } } diff --git a/forge-gui/res/cardsfolder/g/goblin_polka_band.txt b/forge-gui/res/cardsfolder/g/goblin_polka_band.txt index 4db2e326794..27265a7ef34 100644 --- a/forge-gui/res/cardsfolder/g/goblin_polka_band.txt +++ b/forge-gui/res/cardsfolder/g/goblin_polka_band.txt @@ -2,7 +2,7 @@ Name:Goblin Polka Band ManaCost:R R Types:Creature Goblin PT:1/1 -A:AB$ Tap | Announce$ TgtNum | AnnounceTitle$ any number of creatures to target | Cost$ X 2 T | XColor$ R | CostDesc$ {2}, {T}: | ValidTgts$ Creature.untapped | TargetMin$ TgtNum | TargetMax$ TgtNum | TargetsAtRandom$ True | RememberTargets$ True | SubAbility$ GoblinHangover | SpellDescription$ Tap any number of random target creatures. Goblins tapped in this way do not untap during their controllers' next untap phases. This ability costs {R} more to activate for each target. +A:AB$ Tap | Announce$ TgtNum | AnnounceTitle$ any number of creatures to target | Cost$ X 2 T | XColor$ R | CostDesc$ {2}, {T}: | ValidTgts$ Creature.untapped | TargetMin$ TgtNum | TargetMax$ TgtNum | TargetsAtRandom$ True | RememberTargets$ True | AILogic$ GoblinPolkaBand | SubAbility$ GoblinHangover | SpellDescription$ Tap any number of random target creatures. Goblins tapped in this way do not untap during their controllers' next untap phases. This ability costs {R} more to activate for each target. SVar:GoblinHangover:DB$ PumpAll | ValidCards$ Goblin.IsRemembered | KW$ HIDDEN This card doesn't untap during your next untap step. | Duration$ Permanent SVar:TgtNum:Number$0 SVar:X:SVar$TgtNum diff --git a/forge-gui/res/cardsfolder/g/power_struggle.txt b/forge-gui/res/cardsfolder/g/power_struggle.txt index 7b662e546c0..97d2730f7d0 100644 --- a/forge-gui/res/cardsfolder/g/power_struggle.txt +++ b/forge-gui/res/cardsfolder/g/power_struggle.txt @@ -2,6 +2,6 @@ Name:Power Struggle ManaCost:2 U U U Types:Enchantment T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Player | TriggerZones$ Battlefield | Execute$ TrigPump | TriggerController$ TriggeredPlayer | TriggerDescription$ At the beginning of each player's upkeep, that player exchanges control of random target artifact, creature or land he or she controls, for control of random target permanent of the same type that a random opponent controls. -SVar:TrigPump:DB$ Pump | TargetsWithDefinedController$ TriggeredPlayer | ValidTgts$ Artifact,Creature,Land | TargetsAtRandom$ True | SubAbility$ DBExchangeControl -SVar:DBExchangeControl:DB$ ExchangeControl | Defined$ ParentTarget | ValidTgts$ Artifact,Creature,Land | TargetsWithDefinedController$ Player.OpponentOf TriggeredPlayer | TargetsWithSharedCardType$ ParentTarget | TargetsAtRandom$ True +SVar:TrigPump:DB$ Pump | TargetsWithDefinedController$ TriggeredPlayer | ValidTgts$ Artifact,Creature,Land | TargetsAtRandom$ True | AILogic$ PowerStruggle | SubAbility$ DBExchangeControl +SVar:DBExchangeControl:DB$ ExchangeControl | Defined$ ParentTarget | ValidTgts$ Artifact,Creature,Land | TargetsWithDefinedController$ Player.OpponentOf TriggeredPlayer | TargetsWithSharedCardType$ ParentTarget | TargetsAtRandom$ True | AILogic$ PowerStruggle Oracle:At the beginning of each player's upkeep, that player exchanges control of random target artifact, creature or land he or she controls, for control of random target permanent of the same type that a random opponent controls. diff --git a/forge-gui/res/cardsfolder/n/necropolis_of_azar.txt b/forge-gui/res/cardsfolder/n/necropolis_of_azar.txt index 585a1c25ca6..ae2285e9610 100644 --- a/forge-gui/res/cardsfolder/n/necropolis_of_azar.txt +++ b/forge-gui/res/cardsfolder/n/necropolis_of_azar.txt @@ -3,7 +3,7 @@ ManaCost:2 B B Types:Enchantment T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.nonBlack | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever a nonblack creature dies, put a husk counter on CARDNAME. SVar:TrigPutCounter:DB$ PutCounter | CounterType$ HUSK -A:AB$ Token | Cost$ 5 SubCounter<1/HUSK> | TokenScript$ spawn_of_azar | TokenPower$ X | TokenToughness$ Y | SpellDescription$ Create an X/Y black Spawn creature token with swampwalk named Spawn of Azar, where X and Y are numbers chosen at random from 1 to 3. +A:AB$ Token | Cost$ 5 SubCounter<1/HUSK> | TokenScript$ spawn_of_azar | TokenPower$ X | TokenToughness$ Y | AILogic$ RandomPT | SpellDescription$ Create an X/Y black Spawn creature token with swampwalk named Spawn of Azar, where X and Y are numbers chosen at random from 1 to 3. SVar:X:Count$Random.1.3 SVar:Y:Count$Random.1.3 DeckHas:Ability$Counters & Ability$Token diff --git a/forge-gui/res/cardsfolder/p/pandoras_box.txt b/forge-gui/res/cardsfolder/p/pandoras_box.txt index 4474b1ab52f..f0a857ee66a 100644 --- a/forge-gui/res/cardsfolder/p/pandoras_box.txt +++ b/forge-gui/res/cardsfolder/p/pandoras_box.txt @@ -1,7 +1,7 @@ Name:Pandora's Box ManaCost:5 Types:Artifact -A:AB$ ChooseCard | Cost$ 3 T | Choices$ Creature | AtRandom$ True | AllCards$ True | SubAbility$ DBRepeatEach | StackDescription$ SpellDescription | SpellDescription$ Choose a creature card at random from all players' decklists. Each player flips a coin. Each player whose coin comes up heads creates a token that's a copy of that card. +A:AB$ ChooseCard | Cost$ 3 T | Choices$ Creature | AtRandom$ True | AllCards$ True | SubAbility$ DBRepeatEach | AILogic$ AtOppEOT | StackDescription$ SpellDescription | SpellDescription$ Choose a creature card at random from all players' decklists. Each player flips a coin. Each player whose coin comes up heads creates a token that's a copy of that card. SVar:DBRepeatEach:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ DBFlip | SubAbility$ DBCleanup SVar:DBFlip:DB$ FlipACoin | Flipper$ Remembered | NoCall$ True | HeadsSubAbility$ DBCopyPermanent SVar:DBCopyPermanent:DB$ CopyPermanent | Defined$ ChosenCard | Controller$ Remembered