diff --git a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java index 79f2b118f0..e293a529f2 100644 --- a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java +++ b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java @@ -1,161 +1,161 @@ -package mage.cards.g; - -import mage.abilities.Ability; -import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; -import mage.abilities.dynamicvalue.common.StaticValue; -import mage.abilities.effects.ReplacementEffectImpl; -import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; -import mage.cards.Card; -import mage.cards.CardImpl; -import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledPermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; -import mage.players.Player; -import java.util.UUID; -import mage.Mana; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.decorator.ConditionalAsThoughEffect; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.watchers.common.ManaSpentToCastWatcher; - -/** - * - * @author jeffwadsworth - */ - -public class GlimpseTheCosmos extends CardImpl { - - public GlimpseTheCosmos(UUID ownerId, CardSetInfo setInfo) { - super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{U}"); - - // Look at the top three cards of your library. Put one of them into your hand and the rest on the bottom of your library in any order. - this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( - StaticValue.get(3), false, StaticValue.get(1), - StaticFilters.FILTER_CARD, Zone.LIBRARY, false, - false, false, Zone.HAND, false - ).setText("look at the top three cards of your library. " - + "Put one of them into your hand and the rest on the bottom of your library in any order")); - - //As long as you control a Giant, you may cast Glimpse the Cosmos from your graveyard by paying {U} rather than paying its mana cost. If you cast Glimpse the Cosmos this way and it would be put into your graveyard, exile it instead. - this.addAbility(new SimpleStaticAbility(Zone.GRAVEYARD, - new ConditionalAsThoughEffect( - new GlimpseTheCosmosPlayEffect(), - new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT))))); - - this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect())); - - } - - private GlimpseTheCosmos(final GlimpseTheCosmos card) { - super(card); - } - - @Override - public GlimpseTheCosmos copy() { - return new GlimpseTheCosmos(this); - } - -} - -class GlimpseTheCosmosPlayEffect extends AsThoughEffectImpl { - - public GlimpseTheCosmosPlayEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); - staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost"; - } - - public GlimpseTheCosmosPlayEffect(final GlimpseTheCosmosPlayEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public GlimpseTheCosmosPlayEffect copy() { - return new GlimpseTheCosmosPlayEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (sourceId.equals(source.getSourceId()) - && source.isControlledBy(affectedControllerId)) { - if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { - Player controller = game.getPlayer(affectedControllerId); - if (controller != null) { - controller.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{U}"), null); - return true; - } - } - } - return false; - } - -} - -class GlimpseTheCosmosReplacementEffect extends ReplacementEffectImpl { - - public GlimpseTheCosmosReplacementEffect() { - super(Duration.OneUse, Outcome.Exile); - staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost. If you cast {this} this way and it would be put into your graveyard, exile it instead"; - } - - public GlimpseTheCosmosReplacementEffect(final GlimpseTheCosmosReplacementEffect effect) { - super(effect); - } - - @Override - public GlimpseTheCosmosReplacementEffect copy() { - return new GlimpseTheCosmosReplacementEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Card card = game.getCard(event.getTargetId()); - if (card != null) { - discard(); - return controller.moveCards( - card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects()); - } - } - return false; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ZONE_CHANGE; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class); - if (watcher == null) { - return false; - } - Mana payment = watcher.getLastManaPayment(source.getSourceId()); - if (payment != null - && payment.getBlue() == 1 // must be blue mana - && payment.count() == 1) { // must be just one - if (event.getTargetId().equals(source.getSourceId()) - && ((ZoneChangeEvent) event).getFromZone() == Zone.STACK - && ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) { - return true; - } - } - return false; - } -} +package mage.cards.g; + +import mage.abilities.Ability; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import java.util.UUID; +import mage.Mana; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.watchers.common.ManaSpentToCastWatcher; + +/** + * + * @author jeffwadsworth + */ + +public class GlimpseTheCosmos extends CardImpl { + + public GlimpseTheCosmos(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{U}"); + + // Look at the top three cards of your library. Put one of them into your hand and the rest on the bottom of your library in any order. + this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( + StaticValue.get(3), false, StaticValue.get(1), + StaticFilters.FILTER_CARD, Zone.LIBRARY, false, + false, false, Zone.HAND, false + ).setText("look at the top three cards of your library. " + + "Put one of them into your hand and the rest on the bottom of your library in any order")); + + //As long as you control a Giant, you may cast Glimpse the Cosmos from your graveyard by paying {U} rather than paying its mana cost. If you cast Glimpse the Cosmos this way and it would be put into your graveyard, exile it instead. + this.addAbility(new SimpleStaticAbility(Zone.GRAVEYARD, + new ConditionalAsThoughEffect( + new GlimpseTheCosmosPlayEffect(), + new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT))))); + + this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect())); + + } + + private GlimpseTheCosmos(final GlimpseTheCosmos card) { + super(card); + } + + @Override + public GlimpseTheCosmos copy() { + return new GlimpseTheCosmos(this); + } + +} + +class GlimpseTheCosmosPlayEffect extends AsThoughEffectImpl { + + public GlimpseTheCosmosPlayEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); + staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost"; + } + + public GlimpseTheCosmosPlayEffect(final GlimpseTheCosmosPlayEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public GlimpseTheCosmosPlayEffect copy() { + return new GlimpseTheCosmosPlayEffect(this); + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + if (sourceId.equals(source.getSourceId()) + && source.isControlledBy(affectedControllerId)) { + if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { + Player controller = game.getPlayer(affectedControllerId); + if (controller != null) { + controller.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{U}"), null); + return true; + } + } + } + return false; + } + +} + +class GlimpseTheCosmosReplacementEffect extends ReplacementEffectImpl { + + public GlimpseTheCosmosReplacementEffect() { + super(Duration.OneUse, Outcome.Exile); + staticText = "As long as you control a Giant, you may cast {this} from your graveyard by paying {U} rather than paying its mana cost. If you cast {this} this way and it would be put into your graveyard, exile it instead"; + } + + public GlimpseTheCosmosReplacementEffect(final GlimpseTheCosmosReplacementEffect effect) { + super(effect); + } + + @Override + public GlimpseTheCosmosReplacementEffect copy() { + return new GlimpseTheCosmosReplacementEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Card card = game.getCard(event.getTargetId()); + if (card != null) { + discard(); + return controller.moveCards( + card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects()); + } + } + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class); + if (watcher == null) { + return false; + } + Mana payment = watcher.getLastManaPayment(source.getSourceId()); + if (payment != null + && payment.getBlue() == 1 // must be blue mana + && payment.count() == 1) { // must be just one + if (event.getTargetId().equals(source.getSourceId()) + && ((ZoneChangeEvent) event).getFromZone() == Zone.STACK + && ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) { + return true; + } + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/MercadianMasques.java b/Mage.Sets/src/mage/sets/MercadianMasques.java index 866c8cb036..a7398bf630 100644 --- a/Mage.Sets/src/mage/sets/MercadianMasques.java +++ b/Mage.Sets/src/mage/sets/MercadianMasques.java @@ -1,375 +1,375 @@ -package mage.sets; - -import mage.cards.ExpansionSet; -import mage.constants.Rarity; -import mage.constants.SetType; - -/** - * @author North - */ -public final class MercadianMasques extends ExpansionSet { - - private static final MercadianMasques instance = new MercadianMasques(); - - public static MercadianMasques getInstance() { - return instance; - } - - private MercadianMasques() { - super("Mercadian Masques", "MMQ", ExpansionSet.buildDate(1999, 8, 25), SetType.EXPANSION); - this.blockName = "Masques"; - this.hasBoosters = true; - this.numBoosterLands = 0; - this.numBoosterCommon = 11; - this.numBoosterUncommon = 3; - this.numBoosterRare = 1; - this.ratioBoosterMythic = 0; - cards.add(new SetCardInfo("Aerial Caravan", 58, Rarity.RARE, mage.cards.a.AerialCaravan.class)); - cards.add(new SetCardInfo("Afterlife", 1, Rarity.UNCOMMON, mage.cards.a.Afterlife.class)); - cards.add(new SetCardInfo("Alabaster Wall", 2, Rarity.COMMON, mage.cards.a.AlabasterWall.class)); - cards.add(new SetCardInfo("Alley Grifters", 115, Rarity.COMMON, mage.cards.a.AlleyGrifters.class)); - cards.add(new SetCardInfo("Ancestral Mask", 229, Rarity.COMMON, mage.cards.a.AncestralMask.class)); - cards.add(new SetCardInfo("Armistice", 3, Rarity.RARE, mage.cards.a.Armistice.class)); - cards.add(new SetCardInfo("Arms Dealer", 172, Rarity.UNCOMMON, mage.cards.a.ArmsDealer.class)); - cards.add(new SetCardInfo("Arrest", 4, Rarity.UNCOMMON, mage.cards.a.Arrest.class)); - cards.add(new SetCardInfo("Assembly Hall", 286, Rarity.RARE, mage.cards.a.AssemblyHall.class)); - cards.add(new SetCardInfo("Ballista Squad", 5, Rarity.UNCOMMON, mage.cards.b.BallistaSquad.class)); - cards.add(new SetCardInfo("Balloon Peddler", 59, Rarity.COMMON, mage.cards.b.BalloonPeddler.class)); - cards.add(new SetCardInfo("Barbed Wire", 287, Rarity.UNCOMMON, mage.cards.b.BarbedWire.class)); - cards.add(new SetCardInfo("Battle Rampart", 173, Rarity.COMMON, mage.cards.b.BattleRampart.class)); - cards.add(new SetCardInfo("Battle Squadron", 174, Rarity.RARE, mage.cards.b.BattleSquadron.class)); - cards.add(new SetCardInfo("Bifurcate", 230, Rarity.RARE, mage.cards.b.Bifurcate.class)); - cards.add(new SetCardInfo("Black Market", 116, Rarity.RARE, mage.cards.b.BlackMarket.class)); - cards.add(new SetCardInfo("Blaster Mage", 175, Rarity.COMMON, mage.cards.b.BlasterMage.class)); - cards.add(new SetCardInfo("Blockade Runner", 60, Rarity.COMMON, mage.cards.b.BlockadeRunner.class)); - cards.add(new SetCardInfo("Blood Hound", 176, Rarity.RARE, mage.cards.b.BloodHound.class)); - cards.add(new SetCardInfo("Blood Oath", 177, Rarity.RARE, mage.cards.b.BloodOath.class)); - cards.add(new SetCardInfo("Boa Constrictor", 231, Rarity.UNCOMMON, mage.cards.b.BoaConstrictor.class)); - cards.add(new SetCardInfo("Bog Smugglers", 117, Rarity.COMMON, mage.cards.b.BogSmugglers.class)); - cards.add(new SetCardInfo("Bog Witch", 118, Rarity.COMMON, mage.cards.b.BogWitch.class)); - cards.add(new SetCardInfo("Brainstorm", 61, Rarity.COMMON, mage.cards.b.Brainstorm.class)); - cards.add(new SetCardInfo("Brawl", 178, Rarity.RARE, mage.cards.b.Brawl.class)); - cards.add(new SetCardInfo("Briar Patch", 232, Rarity.UNCOMMON, mage.cards.b.BriarPatch.class)); - cards.add(new SetCardInfo("Bribery", 62, Rarity.RARE, mage.cards.b.Bribery.class)); - cards.add(new SetCardInfo("Buoyancy", 63, Rarity.COMMON, mage.cards.b.Buoyancy.class)); - cards.add(new SetCardInfo("Cackling Witch", 119, Rarity.UNCOMMON, mage.cards.c.CacklingWitch.class)); - cards.add(new SetCardInfo("Caller of the Hunt", 233, Rarity.RARE, mage.cards.c.CallerOfTheHunt.class)); - cards.add(new SetCardInfo("Cateran Brute", 120, Rarity.COMMON, mage.cards.c.CateranBrute.class)); - cards.add(new SetCardInfo("Cateran Enforcer", 121, Rarity.UNCOMMON, mage.cards.c.CateranEnforcer.class)); - cards.add(new SetCardInfo("Cateran Kidnappers", 122, Rarity.UNCOMMON, mage.cards.c.CateranKidnappers.class)); - cards.add(new SetCardInfo("Cateran Overlord", 123, Rarity.RARE, mage.cards.c.CateranOverlord.class)); - cards.add(new SetCardInfo("Cateran Persuader", 124, Rarity.COMMON, mage.cards.c.CateranPersuader.class)); - cards.add(new SetCardInfo("Cateran Slaver", 125, Rarity.RARE, mage.cards.c.CateranSlaver.class)); - cards.add(new SetCardInfo("Cateran Summons", 126, Rarity.UNCOMMON, mage.cards.c.CateranSummons.class)); - cards.add(new SetCardInfo("Caustic Wasps", 234, Rarity.UNCOMMON, mage.cards.c.CausticWasps.class)); - cards.add(new SetCardInfo("Cave-In", 180, Rarity.RARE, mage.cards.c.CaveIn.class)); - cards.add(new SetCardInfo("Cavern Crawler", 181, Rarity.COMMON, mage.cards.c.CavernCrawler.class)); - cards.add(new SetCardInfo("Cave Sense", 179, Rarity.COMMON, mage.cards.c.CaveSense.class)); - cards.add(new SetCardInfo("Ceremonial Guard", 182, Rarity.COMMON, mage.cards.c.CeremonialGuard.class)); - cards.add(new SetCardInfo("Chambered Nautilus", 64, Rarity.UNCOMMON, mage.cards.c.ChamberedNautilus.class)); - cards.add(new SetCardInfo("Chameleon Spirit", 65, Rarity.UNCOMMON, mage.cards.c.ChameleonSpirit.class)); - cards.add(new SetCardInfo("Charisma", 66, Rarity.RARE, mage.cards.c.Charisma.class)); - cards.add(new SetCardInfo("Charm Peddler", 6, Rarity.COMMON, mage.cards.c.CharmPeddler.class)); - cards.add(new SetCardInfo("Charmed Griffin", 7, Rarity.UNCOMMON, mage.cards.c.CharmedGriffin.class)); - cards.add(new SetCardInfo("Cho-Arrim Alchemist", 8, Rarity.RARE, mage.cards.c.ChoArrimAlchemist.class)); - cards.add(new SetCardInfo("Cho-Arrim Bruiser", 9, Rarity.RARE, mage.cards.c.ChoArrimBruiser.class)); - cards.add(new SetCardInfo("Cho-Arrim Legate", 10, Rarity.UNCOMMON, mage.cards.c.ChoArrimLegate.class)); - cards.add(new SetCardInfo("Cho-Manno, Revolutionary", 11, Rarity.RARE, mage.cards.c.ChoMannoRevolutionary.class)); - cards.add(new SetCardInfo("Cho-Manno's Blessing", 12, Rarity.COMMON, mage.cards.c.ChoMannosBlessing.class)); - cards.add(new SetCardInfo("Cinder Elemental", 183, Rarity.UNCOMMON, mage.cards.c.CinderElemental.class)); - cards.add(new SetCardInfo("Clear the Land", 235, Rarity.RARE, mage.cards.c.ClearTheLand.class)); - cards.add(new SetCardInfo("Close Quarters", 184, Rarity.UNCOMMON, mage.cards.c.CloseQuarters.class)); - cards.add(new SetCardInfo("Cloud Sprite", 67, Rarity.COMMON, mage.cards.c.CloudSprite.class)); - cards.add(new SetCardInfo("Coastal Piracy", 68, Rarity.UNCOMMON, mage.cards.c.CoastalPiracy.class)); - cards.add(new SetCardInfo("Collective Unconscious", 236, Rarity.RARE, mage.cards.c.CollectiveUnconscious.class)); - cards.add(new SetCardInfo("Common Cause", 13, Rarity.RARE, mage.cards.c.CommonCause.class)); - cards.add(new SetCardInfo("Conspiracy", 127, Rarity.RARE, mage.cards.c.Conspiracy.class)); - cards.add(new SetCardInfo("Cornered Market", 14, Rarity.RARE, mage.cards.c.CorneredMarket.class)); - cards.add(new SetCardInfo("Corrupt Official", 128, Rarity.RARE, mage.cards.c.CorruptOfficial.class)); - cards.add(new SetCardInfo("Counterspell", 69, Rarity.COMMON, mage.cards.c.Counterspell.class)); - cards.add(new SetCardInfo("Cowardice", 70, Rarity.RARE, mage.cards.c.Cowardice.class)); - cards.add(new SetCardInfo("Crackdown", 15, Rarity.RARE, mage.cards.c.Crackdown.class)); - cards.add(new SetCardInfo("Crag Saurian", 185, Rarity.RARE, mage.cards.c.CragSaurian.class)); - cards.add(new SetCardInfo("Crash", 186, Rarity.COMMON, mage.cards.c.Crash.class)); - cards.add(new SetCardInfo("Credit Voucher", 289, Rarity.UNCOMMON, mage.cards.c.CreditVoucher.class)); - cards.add(new SetCardInfo("Crenellated Wall", 290, Rarity.UNCOMMON, mage.cards.c.CrenellatedWall.class)); - cards.add(new SetCardInfo("Crooked Scales", 291, Rarity.RARE, mage.cards.c.CrookedScales.class)); - cards.add(new SetCardInfo("Crossbow Infantry", 16, Rarity.COMMON, mage.cards.c.CrossbowInfantry.class)); - cards.add(new SetCardInfo("Crumbling Sanctuary", 292, Rarity.RARE, mage.cards.c.CrumblingSanctuary.class)); - cards.add(new SetCardInfo("Customs Depot", 71, Rarity.UNCOMMON, mage.cards.c.CustomsDepot.class)); - cards.add(new SetCardInfo("Dark Ritual", 129, Rarity.COMMON, mage.cards.d.DarkRitual.class)); - cards.add(new SetCardInfo("Darting Merfolk", 72, Rarity.COMMON, mage.cards.d.DartingMerfolk.class)); - cards.add(new SetCardInfo("Dawnstrider", 237, Rarity.RARE, mage.cards.d.Dawnstrider.class)); - cards.add(new SetCardInfo("Deadly Insect", 238, Rarity.COMMON, mage.cards.d.DeadlyInsect.class)); - cards.add(new SetCardInfo("Deathgazer", 130, Rarity.UNCOMMON, mage.cards.d.Deathgazer.class)); - cards.add(new SetCardInfo("Deepwood Drummer", 239, Rarity.COMMON, mage.cards.d.DeepwoodDrummer.class)); - cards.add(new SetCardInfo("Deepwood Elder", 240, Rarity.RARE, mage.cards.d.DeepwoodElder.class)); - cards.add(new SetCardInfo("Deepwood Ghoul", 131, Rarity.COMMON, mage.cards.d.DeepwoodGhoul.class)); - cards.add(new SetCardInfo("Deepwood Legate", 132, Rarity.UNCOMMON, mage.cards.d.DeepwoodLegate.class)); - cards.add(new SetCardInfo("Deepwood Tantiv", 241, Rarity.UNCOMMON, mage.cards.d.DeepwoodTantiv.class)); - cards.add(new SetCardInfo("Deepwood Wolverine", 242, Rarity.COMMON, mage.cards.d.DeepwoodWolverine.class)); - cards.add(new SetCardInfo("Dehydration", 73, Rarity.COMMON, mage.cards.d.Dehydration.class)); - cards.add(new SetCardInfo("Delraich", 133, Rarity.RARE, mage.cards.d.Delraich.class)); - cards.add(new SetCardInfo("Desert Twister", 243, Rarity.UNCOMMON, mage.cards.d.DesertTwister.class)); - cards.add(new SetCardInfo("Devout Witness", 17, Rarity.COMMON, mage.cards.d.DevoutWitness.class)); - cards.add(new SetCardInfo("Diplomatic Escort", 74, Rarity.UNCOMMON, mage.cards.d.DiplomaticEscort.class)); - cards.add(new SetCardInfo("Diplomatic Immunity", 75, Rarity.COMMON, mage.cards.d.DiplomaticImmunity.class)); - cards.add(new SetCardInfo("Disenchant", 18, Rarity.COMMON, mage.cards.d.Disenchant.class)); - cards.add(new SetCardInfo("Distorting Lens", 293, Rarity.RARE, mage.cards.d.DistortingLens.class)); - cards.add(new SetCardInfo("Drake Hatchling", 76, Rarity.COMMON, mage.cards.d.DrakeHatchling.class)); - cards.add(new SetCardInfo("Dust Bowl", 316, Rarity.RARE, mage.cards.d.DustBowl.class)); - cards.add(new SetCardInfo("Embargo", 77, Rarity.RARE, mage.cards.e.Embargo.class)); - cards.add(new SetCardInfo("Energy Flux", 78, Rarity.UNCOMMON, mage.cards.e.EnergyFlux.class)); - cards.add(new SetCardInfo("Enslaved Horror", 134, Rarity.UNCOMMON, mage.cards.e.EnslavedHorror.class)); - cards.add(new SetCardInfo("Extortion", 135, Rarity.RARE, mage.cards.e.Extortion.class)); - cards.add(new SetCardInfo("Extravagant Spirit", 79, Rarity.RARE, mage.cards.e.ExtravagantSpirit.class)); - cards.add(new SetCardInfo("Eye of Ramos", 294, Rarity.RARE, mage.cards.e.EyeOfRamos.class)); - cards.add(new SetCardInfo("False Demise", 80, Rarity.UNCOMMON, mage.cards.f.FalseDemise.class)); - cards.add(new SetCardInfo("Ferocity", 245, Rarity.COMMON, mage.cards.f.Ferocity.class)); - cards.add(new SetCardInfo("Flailing Manticore", 187, Rarity.RARE, mage.cards.f.FlailingManticore.class)); - cards.add(new SetCardInfo("Flailing Ogre", 188, Rarity.UNCOMMON, mage.cards.f.FlailingOgre.class)); - cards.add(new SetCardInfo("Flailing Soldier", 189, Rarity.COMMON, mage.cards.f.FlailingSoldier.class)); - cards.add(new SetCardInfo("Flaming Sword", 190, Rarity.COMMON, mage.cards.f.FlamingSword.class)); - cards.add(new SetCardInfo("Food Chain", 246, Rarity.RARE, mage.cards.f.FoodChain.class)); - cards.add(new SetCardInfo("Forced March", 136, Rarity.RARE, mage.cards.f.ForcedMarch.class)); - cards.add(new SetCardInfo("Forest", 347, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Forest", 348, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Forest", 349, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Forest", 350, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Foster", 247, Rarity.RARE, mage.cards.f.Foster.class)); - cards.add(new SetCardInfo("Fountain of Cho", 317, Rarity.UNCOMMON, mage.cards.f.FountainOfCho.class)); - cards.add(new SetCardInfo("Fountain Watch", 19, Rarity.RARE, mage.cards.f.FountainWatch.class)); - cards.add(new SetCardInfo("Fresh Volunteers", 20, Rarity.COMMON, mage.cards.f.FreshVolunteers.class)); - cards.add(new SetCardInfo("Furious Assault", 191, Rarity.COMMON, mage.cards.f.FuriousAssault.class)); - cards.add(new SetCardInfo("Game Preserve", 248, Rarity.RARE, mage.cards.g.GamePreserve.class)); - cards.add(new SetCardInfo("General's Regalia", 295, Rarity.RARE, mage.cards.g.GeneralsRegalia.class)); - cards.add(new SetCardInfo("Gerrard's Irregulars", 192, Rarity.COMMON, mage.cards.g.GerrardsIrregulars.class)); - cards.add(new SetCardInfo("Ghoul's Feast", 137, Rarity.UNCOMMON, mage.cards.g.GhoulsFeast.class)); - cards.add(new SetCardInfo("Giant Caterpillar", 249, Rarity.COMMON, mage.cards.g.GiantCaterpillar.class)); - cards.add(new SetCardInfo("Glowing Anemone", 81, Rarity.UNCOMMON, mage.cards.g.GlowingAnemone.class)); - cards.add(new SetCardInfo("Groundskeeper", 250, Rarity.UNCOMMON, mage.cards.g.Groundskeeper.class)); - cards.add(new SetCardInfo("Gush", 82, Rarity.COMMON, mage.cards.g.Gush.class)); - cards.add(new SetCardInfo("Hammer Mage", 193, Rarity.UNCOMMON, mage.cards.h.HammerMage.class)); - cards.add(new SetCardInfo("Haunted Crossroads", 138, Rarity.UNCOMMON, mage.cards.h.HauntedCrossroads.class)); - cards.add(new SetCardInfo("Heart of Ramos", 296, Rarity.RARE, mage.cards.h.HeartOfRamos.class)); - cards.add(new SetCardInfo("Henge Guardian", 297, Rarity.UNCOMMON, mage.cards.h.HengeGuardian.class)); - cards.add(new SetCardInfo("Henge of Ramos", 318, Rarity.UNCOMMON, mage.cards.h.HengeOfRamos.class)); - cards.add(new SetCardInfo("Hickory Woodlot", 319, Rarity.COMMON, mage.cards.h.HickoryWoodlot.class)); - cards.add(new SetCardInfo("High Market", 320, Rarity.RARE, mage.cards.h.HighMarket.class)); - cards.add(new SetCardInfo("High Seas", 83, Rarity.UNCOMMON, mage.cards.h.HighSeas.class)); - cards.add(new SetCardInfo("Highway Robber", 139, Rarity.COMMON, mage.cards.h.HighwayRobber.class)); - cards.add(new SetCardInfo("Hired Giant", 194, Rarity.UNCOMMON, mage.cards.h.HiredGiant.class)); - cards.add(new SetCardInfo("Honor the Fallen", 21, Rarity.RARE, mage.cards.h.HonorTheFallen.class)); - cards.add(new SetCardInfo("Hoodwink", 84, Rarity.COMMON, mage.cards.h.Hoodwink.class)); - cards.add(new SetCardInfo("Horned Troll", 251, Rarity.COMMON, mage.cards.h.HornedTroll.class)); - cards.add(new SetCardInfo("Horn of Plenty", 298, Rarity.RARE, mage.cards.h.HornOfPlenty.class)); - cards.add(new SetCardInfo("Horn of Ramos", 299, Rarity.RARE, mage.cards.h.HornOfRamos.class)); - cards.add(new SetCardInfo("Howling Wolf", 252, Rarity.COMMON, mage.cards.h.HowlingWolf.class)); - cards.add(new SetCardInfo("Hunted Wumpus", 253, Rarity.UNCOMMON, mage.cards.h.HuntedWumpus.class)); - cards.add(new SetCardInfo("Ignoble Soldier", 22, Rarity.UNCOMMON, mage.cards.i.IgnobleSoldier.class)); - cards.add(new SetCardInfo("Indentured Djinn", 85, Rarity.UNCOMMON, mage.cards.i.IndenturedDjinn.class)); - cards.add(new SetCardInfo("Instigator", 140, Rarity.RARE, mage.cards.i.Instigator.class)); - cards.add(new SetCardInfo("Insubordination", 141, Rarity.COMMON, mage.cards.i.Insubordination.class)); - cards.add(new SetCardInfo("Intimidation", 142, Rarity.UNCOMMON, mage.cards.i.Intimidation.class)); - cards.add(new SetCardInfo("Invigorate", 254, Rarity.COMMON, mage.cards.i.Invigorate.class)); - cards.add(new SetCardInfo("Inviolability", 23, Rarity.COMMON, mage.cards.i.Inviolability.class)); - cards.add(new SetCardInfo("Iron Lance", 300, Rarity.UNCOMMON, mage.cards.i.IronLance.class)); - cards.add(new SetCardInfo("Island", 335, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Island", 336, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Island", 337, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Island", 338, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Ivory Mask", 24, Rarity.RARE, mage.cards.i.IvoryMask.class)); - cards.add(new SetCardInfo("Jeweled Torque", 301, Rarity.UNCOMMON, mage.cards.j.JeweledTorque.class)); - cards.add(new SetCardInfo("Jhovall Queen", 25, Rarity.RARE, mage.cards.j.JhovallQueen.class)); - cards.add(new SetCardInfo("Jhovall Rider", 26, Rarity.UNCOMMON, mage.cards.j.JhovallRider.class)); - cards.add(new SetCardInfo("Karn's Touch", 86, Rarity.RARE, mage.cards.k.KarnsTouch.class)); - cards.add(new SetCardInfo("Kris Mage", 195, Rarity.COMMON, mage.cards.k.KrisMage.class)); - cards.add(new SetCardInfo("Kyren Archive", 302, Rarity.RARE, mage.cards.k.KyrenArchive.class)); - cards.add(new SetCardInfo("Kyren Glider", 196, Rarity.COMMON, mage.cards.k.KyrenGlider.class)); - cards.add(new SetCardInfo("Kyren Legate", 197, Rarity.UNCOMMON, mage.cards.k.KyrenLegate.class)); - cards.add(new SetCardInfo("Kyren Negotiations", 198, Rarity.UNCOMMON, mage.cards.k.KyrenNegotiations.class)); - cards.add(new SetCardInfo("Kyren Sniper", 199, Rarity.COMMON, mage.cards.k.KyrenSniper.class)); - cards.add(new SetCardInfo("Kyren Toy", 303, Rarity.RARE, mage.cards.k.KyrenToy.class)); - cards.add(new SetCardInfo("Land Grant", 255, Rarity.COMMON, mage.cards.l.LandGrant.class)); - cards.add(new SetCardInfo("Larceny", 143, Rarity.UNCOMMON, mage.cards.l.Larceny.class)); - cards.add(new SetCardInfo("Last Breath", 27, Rarity.UNCOMMON, mage.cards.l.LastBreath.class)); - cards.add(new SetCardInfo("Lava Runner", 200, Rarity.RARE, mage.cards.l.LavaRunner.class)); - cards.add(new SetCardInfo("Liability", 144, Rarity.RARE, mage.cards.l.Liability.class)); - cards.add(new SetCardInfo("Lightning Hounds", 201, Rarity.COMMON, mage.cards.l.LightningHounds.class)); - cards.add(new SetCardInfo("Lithophage", 202, Rarity.RARE, mage.cards.l.Lithophage.class)); - cards.add(new SetCardInfo("Lumbering Satyr", 257, Rarity.UNCOMMON, mage.cards.l.LumberingSatyr.class)); - cards.add(new SetCardInfo("Lunge", 203, Rarity.COMMON, mage.cards.l.Lunge.class)); - cards.add(new SetCardInfo("Lure", 258, Rarity.UNCOMMON, mage.cards.l.Lure.class)); - cards.add(new SetCardInfo("Maggot Therapy", 145, Rarity.COMMON, mage.cards.m.MaggotTherapy.class)); - cards.add(new SetCardInfo("Magistrate's Scepter", 304, Rarity.RARE, mage.cards.m.MagistratesScepter.class)); - cards.add(new SetCardInfo("Magistrate's Veto", 204, Rarity.UNCOMMON, mage.cards.m.MagistratesVeto.class)); - cards.add(new SetCardInfo("Megatherium", 259, Rarity.RARE, mage.cards.m.Megatherium.class)); - cards.add(new SetCardInfo("Mercadia's Downfall", 205, Rarity.UNCOMMON, mage.cards.m.MercadiasDownfall.class)); - cards.add(new SetCardInfo("Mercadian Atlas", 305, Rarity.RARE, mage.cards.m.MercadianAtlas.class)); - cards.add(new SetCardInfo("Mercadian Bazaar", 321, Rarity.UNCOMMON, mage.cards.m.MercadianBazaar.class)); - cards.add(new SetCardInfo("Mercadian Lift", 306, Rarity.RARE, mage.cards.m.MercadianLift.class)); - cards.add(new SetCardInfo("Midnight Ritual", 146, Rarity.RARE, mage.cards.m.MidnightRitual.class)); - cards.add(new SetCardInfo("Misdirection", 87, Rarity.RARE, mage.cards.m.Misdirection.class)); - cards.add(new SetCardInfo("Misshapen Fiend", 147, Rarity.COMMON, mage.cards.m.MisshapenFiend.class)); - cards.add(new SetCardInfo("Misstep", 88, Rarity.COMMON, mage.cards.m.Misstep.class)); - cards.add(new SetCardInfo("Molting Harpy", 148, Rarity.UNCOMMON, mage.cards.m.MoltingHarpy.class)); - cards.add(new SetCardInfo("Moment of Silence", 28, Rarity.COMMON, mage.cards.m.MomentOfSilence.class)); - cards.add(new SetCardInfo("Monkey Cage", 307, Rarity.RARE, mage.cards.m.MonkeyCage.class)); - cards.add(new SetCardInfo("Moonlit Wake", 29, Rarity.UNCOMMON, mage.cards.m.MoonlitWake.class)); - cards.add(new SetCardInfo("Mountain", 343, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mountain", 344, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mountain", 345, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mountain", 346, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Muzzle", 30, Rarity.COMMON, mage.cards.m.Muzzle.class)); - cards.add(new SetCardInfo("Natural Affinity", 260, Rarity.RARE, mage.cards.n.NaturalAffinity.class)); - cards.add(new SetCardInfo("Nether Spirit", 149, Rarity.RARE, mage.cards.n.NetherSpirit.class)); - cards.add(new SetCardInfo("Nightwind Glider", 31, Rarity.COMMON, mage.cards.n.NightwindGlider.class)); - cards.add(new SetCardInfo("Noble Purpose", 32, Rarity.UNCOMMON, mage.cards.n.NoblePurpose.class)); - cards.add(new SetCardInfo("Notorious Assassin", 150, Rarity.RARE, mage.cards.n.NotoriousAssassin.class)); - cards.add(new SetCardInfo("Ogre Taskmaster", 206, Rarity.UNCOMMON, mage.cards.o.OgreTaskmaster.class)); - cards.add(new SetCardInfo("Orim's Cure", 33, Rarity.COMMON, mage.cards.o.OrimsCure.class)); - cards.add(new SetCardInfo("Overtaker", 89, Rarity.RARE, mage.cards.o.Overtaker.class)); - cards.add(new SetCardInfo("Panacea", 308, Rarity.UNCOMMON, mage.cards.p.Panacea.class)); - cards.add(new SetCardInfo("Pangosaur", 261, Rarity.RARE, mage.cards.p.Pangosaur.class)); - cards.add(new SetCardInfo("Peat Bog", 322, Rarity.COMMON, mage.cards.p.PeatBog.class)); - cards.add(new SetCardInfo("Pious Warrior", 34, Rarity.COMMON, mage.cards.p.PiousWarrior.class)); - cards.add(new SetCardInfo("Plains", 331, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Plains", 332, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Plains", 333, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Plains", 334, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Port Inspector", 90, Rarity.COMMON, mage.cards.p.PortInspector.class)); - cards.add(new SetCardInfo("Power Matrix", 309, Rarity.RARE, mage.cards.p.PowerMatrix.class)); - cards.add(new SetCardInfo("Pretender's Claim", 151, Rarity.UNCOMMON, mage.cards.p.PretendersClaim.class)); - cards.add(new SetCardInfo("Primeval Shambler", 152, Rarity.UNCOMMON, mage.cards.p.PrimevalShambler.class)); - cards.add(new SetCardInfo("Puffer Extract", 310, Rarity.UNCOMMON, mage.cards.p.PufferExtract.class)); - cards.add(new SetCardInfo("Pulverize", 207, Rarity.RARE, mage.cards.p.Pulverize.class)); - cards.add(new SetCardInfo("Putrefaction", 153, Rarity.UNCOMMON, mage.cards.p.Putrefaction.class)); - cards.add(new SetCardInfo("Puppet's Verdict", 208, Rarity.RARE, mage.cards.p.PuppetsVerdict.class)); - cards.add(new SetCardInfo("Quagmire Lamprey", 154, Rarity.UNCOMMON, mage.cards.q.QuagmireLamprey.class)); - cards.add(new SetCardInfo("Rain of Tears", 155, Rarity.UNCOMMON, mage.cards.r.RainOfTears.class)); - cards.add(new SetCardInfo("Ramosian Captain", 35, Rarity.UNCOMMON, mage.cards.r.RamosianCaptain.class)); - cards.add(new SetCardInfo("Ramosian Commander", 36, Rarity.UNCOMMON, mage.cards.r.RamosianCommander.class)); - cards.add(new SetCardInfo("Ramosian Lieutenant", 37, Rarity.COMMON, mage.cards.r.RamosianLieutenant.class)); - cards.add(new SetCardInfo("Ramosian Rally", 38, Rarity.COMMON, mage.cards.r.RamosianRally.class)); - cards.add(new SetCardInfo("Ramosian Sergeant", 39, Rarity.COMMON, mage.cards.r.RamosianSergeant.class)); - cards.add(new SetCardInfo("Ramosian Sky Marshal", 40, Rarity.RARE, mage.cards.r.RamosianSkyMarshal.class)); - cards.add(new SetCardInfo("Rampart Crawler", 156, Rarity.COMMON, mage.cards.r.RampartCrawler.class)); - cards.add(new SetCardInfo("Rappelling Scouts", 41, Rarity.RARE, mage.cards.r.RappellingScouts.class)); - cards.add(new SetCardInfo("Remote Farm", 323, Rarity.COMMON, mage.cards.r.RemoteFarm.class)); - cards.add(new SetCardInfo("Renounce", 42, Rarity.UNCOMMON, mage.cards.r.Renounce.class)); - cards.add(new SetCardInfo("Revered Elder", 43, Rarity.COMMON, mage.cards.r.ReveredElder.class)); - cards.add(new SetCardInfo("Reverent Mantra", 44, Rarity.RARE, mage.cards.r.ReverentMantra.class)); - cards.add(new SetCardInfo("Revive", 262, Rarity.UNCOMMON, mage.cards.r.Revive.class)); - cards.add(new SetCardInfo("Righteous Aura", 45, Rarity.UNCOMMON, mage.cards.r.RighteousAura.class)); - cards.add(new SetCardInfo("Righteous Indignation", 46, Rarity.UNCOMMON, mage.cards.r.RighteousIndignation.class)); - cards.add(new SetCardInfo("Rishadan Airship", 91, Rarity.COMMON, mage.cards.r.RishadanAirship.class)); - cards.add(new SetCardInfo("Rishadan Brigand", 92, Rarity.RARE, mage.cards.r.RishadanBrigand.class)); - cards.add(new SetCardInfo("Rishadan Cutpurse", 93, Rarity.COMMON, mage.cards.r.RishadanCutpurse.class)); - cards.add(new SetCardInfo("Rishadan Footpad", 94, Rarity.UNCOMMON, mage.cards.r.RishadanFootpad.class)); - cards.add(new SetCardInfo("Rishadan Pawnshop", 311, Rarity.RARE, mage.cards.r.RishadanPawnshop.class)); - cards.add(new SetCardInfo("Rishadan Port", 324, Rarity.RARE, mage.cards.r.RishadanPort.class)); - cards.add(new SetCardInfo("Robber Fly", 209, Rarity.UNCOMMON, mage.cards.r.RobberFly.class)); - cards.add(new SetCardInfo("Rock Badger", 210, Rarity.UNCOMMON, mage.cards.r.RockBadger.class)); - cards.add(new SetCardInfo("Rouse", 157, Rarity.COMMON, mage.cards.r.Rouse.class)); - cards.add(new SetCardInfo("Rushwood Dryad", 263, Rarity.COMMON, mage.cards.r.RushwoodDryad.class)); - cards.add(new SetCardInfo("Rushwood Elemental", 264, Rarity.RARE, mage.cards.r.RushwoodElemental.class)); - cards.add(new SetCardInfo("Rushwood Grove", 325, Rarity.UNCOMMON, mage.cards.r.RushwoodGrove.class)); - cards.add(new SetCardInfo("Rushwood Herbalist", 265, Rarity.COMMON, mage.cards.r.RushwoodHerbalist.class)); - cards.add(new SetCardInfo("Rushwood Legate", 266, Rarity.UNCOMMON, mage.cards.r.RushwoodLegate.class)); - cards.add(new SetCardInfo("Saber Ants", 267, Rarity.UNCOMMON, mage.cards.s.SaberAnts.class)); - cards.add(new SetCardInfo("Sacred Prey", 268, Rarity.COMMON, mage.cards.s.SacredPrey.class)); - cards.add(new SetCardInfo("Sailmonger", 95, Rarity.UNCOMMON, mage.cards.s.Sailmonger.class)); - cards.add(new SetCardInfo("Sand Squid", 96, Rarity.RARE, mage.cards.s.SandSquid.class)); - cards.add(new SetCardInfo("Sandstone Needle", 326, Rarity.COMMON, mage.cards.s.SandstoneNeedle.class)); - cards.add(new SetCardInfo("Saprazzan Bailiff", 97, Rarity.RARE, mage.cards.s.SaprazzanBailiff.class)); - cards.add(new SetCardInfo("Saprazzan Breaker", 98, Rarity.UNCOMMON, mage.cards.s.SaprazzanBreaker.class)); - cards.add(new SetCardInfo("Saprazzan Cove", 327, Rarity.UNCOMMON, mage.cards.s.SaprazzanCove.class)); - cards.add(new SetCardInfo("Saprazzan Heir", 99, Rarity.RARE, mage.cards.s.SaprazzanHeir.class)); - cards.add(new SetCardInfo("Saprazzan Legate", 100, Rarity.UNCOMMON, mage.cards.s.SaprazzanLegate.class)); - cards.add(new SetCardInfo("Saprazzan Outrigger", 101, Rarity.COMMON, mage.cards.s.SaprazzanOutrigger.class)); - cards.add(new SetCardInfo("Saprazzan Raider", 102, Rarity.COMMON, mage.cards.s.SaprazzanRaider.class)); - cards.add(new SetCardInfo("Saprazzan Skerry", 328, Rarity.COMMON, mage.cards.s.SaprazzanSkerry.class)); - cards.add(new SetCardInfo("Scandalmonger", 158, Rarity.UNCOMMON, mage.cards.s.Scandalmonger.class)); - cards.add(new SetCardInfo("Security Detail", 47, Rarity.RARE, mage.cards.s.SecurityDetail.class)); - cards.add(new SetCardInfo("Seismic Mage", 211, Rarity.RARE, mage.cards.s.SeismicMage.class)); - cards.add(new SetCardInfo("Sever Soul", 159, Rarity.COMMON, mage.cards.s.SeverSoul.class)); - cards.add(new SetCardInfo("Shock Troops", 212, Rarity.COMMON, mage.cards.s.ShockTroops.class)); - cards.add(new SetCardInfo("Shoving Match", 103, Rarity.UNCOMMON, mage.cards.s.ShovingMatch.class)); - cards.add(new SetCardInfo("Silent Assassin", 160, Rarity.RARE, mage.cards.s.SilentAssassin.class)); - cards.add(new SetCardInfo("Silverglade Elemental", 269, Rarity.COMMON, mage.cards.s.SilvergladeElemental.class)); - cards.add(new SetCardInfo("Silverglade Pathfinder", 270, Rarity.UNCOMMON, mage.cards.s.SilvergladePathfinder.class)); - cards.add(new SetCardInfo("Sizzle", 213, Rarity.COMMON, mage.cards.s.Sizzle.class)); - cards.add(new SetCardInfo("Skulking Fugitive", 161, Rarity.COMMON, mage.cards.s.SkulkingFugitive.class)); - cards.add(new SetCardInfo("Skull of Ramos", 312, Rarity.RARE, mage.cards.s.SkullOfRamos.class)); - cards.add(new SetCardInfo("Snake Pit", 271, Rarity.UNCOMMON, mage.cards.s.SnakePit.class)); - cards.add(new SetCardInfo("Snorting Gahr", 272, Rarity.COMMON, mage.cards.s.SnortingGahr.class)); - cards.add(new SetCardInfo("Snuff Out", 162, Rarity.COMMON, mage.cards.s.SnuffOut.class)); - cards.add(new SetCardInfo("Soothing Balm", 48, Rarity.COMMON, mage.cards.s.SoothingBalm.class)); - cards.add(new SetCardInfo("Soothsaying", 104, Rarity.UNCOMMON, mage.cards.s.Soothsaying.class)); - cards.add(new SetCardInfo("Soul Channeling", 163, Rarity.COMMON, mage.cards.s.SoulChanneling.class)); - cards.add(new SetCardInfo("Specter's Wail", 164, Rarity.COMMON, mage.cards.s.SpectersWail.class)); - cards.add(new SetCardInfo("Spidersilk Armor", 273, Rarity.COMMON, mage.cards.s.SpidersilkArmor.class)); - cards.add(new SetCardInfo("Spiritual Focus", 49, Rarity.RARE, mage.cards.s.SpiritualFocus.class)); - cards.add(new SetCardInfo("Spontaneous Generation", 274, Rarity.RARE, mage.cards.s.SpontaneousGeneration.class)); - cards.add(new SetCardInfo("Squall", 275, Rarity.COMMON, mage.cards.s.Squall.class)); - cards.add(new SetCardInfo("Squallmonger", 276, Rarity.UNCOMMON, mage.cards.s.Squallmonger.class)); - cards.add(new SetCardInfo("Squee, Goblin Nabob", 214, Rarity.RARE, mage.cards.s.SqueeGoblinNabob.class)); - cards.add(new SetCardInfo("Squeeze", 105, Rarity.RARE, mage.cards.s.Squeeze.class)); - cards.add(new SetCardInfo("Stamina", 277, Rarity.UNCOMMON, mage.cards.s.Stamina.class)); - cards.add(new SetCardInfo("Statecraft", 106, Rarity.RARE, mage.cards.s.Statecraft.class)); - cards.add(new SetCardInfo("Steadfast Guard", 50, Rarity.COMMON, mage.cards.s.SteadfastGuard.class)); - cards.add(new SetCardInfo("Stinging Barrier", 107, Rarity.COMMON, mage.cards.s.StingingBarrier.class)); - cards.add(new SetCardInfo("Stone Rain", 215, Rarity.COMMON, mage.cards.s.StoneRain.class)); - cards.add(new SetCardInfo("Story Circle", 51, Rarity.UNCOMMON, mage.cards.s.StoryCircle.class)); - cards.add(new SetCardInfo("Strongarm Thug", 165, Rarity.UNCOMMON, mage.cards.s.StrongarmThug.class)); - cards.add(new SetCardInfo("Subterranean Hangar", 329, Rarity.UNCOMMON, mage.cards.s.SubterraneanHangar.class)); - cards.add(new SetCardInfo("Sustenance", 278, Rarity.UNCOMMON, mage.cards.s.Sustenance.class)); - cards.add(new SetCardInfo("Swamp", 339, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Swamp", 340, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Swamp", 341, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Swamp", 342, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Task Force", 52, Rarity.COMMON, mage.cards.t.TaskForce.class)); - cards.add(new SetCardInfo("Tectonic Break", 216, Rarity.RARE, mage.cards.t.TectonicBreak.class)); - cards.add(new SetCardInfo("Territorial Dispute", 217, Rarity.RARE, mage.cards.t.TerritorialDispute.class)); - cards.add(new SetCardInfo("Thermal Glider", 53, Rarity.COMMON, mage.cards.t.ThermalGlider.class)); - cards.add(new SetCardInfo("Thieves' Auction", 218, Rarity.RARE, mage.cards.t.ThievesAuction.class)); - cards.add(new SetCardInfo("Thrashing Wumpus", 166, Rarity.RARE, mage.cards.t.ThrashingWumpus.class)); - cards.add(new SetCardInfo("Thunderclap", 219, Rarity.COMMON, mage.cards.t.Thunderclap.class)); - cards.add(new SetCardInfo("Thwart", 108, Rarity.UNCOMMON, mage.cards.t.Thwart.class)); - cards.add(new SetCardInfo("Tidal Bore", 109, Rarity.COMMON, mage.cards.t.TidalBore.class)); - cards.add(new SetCardInfo("Tidal Kraken", 110, Rarity.RARE, mage.cards.t.TidalKraken.class)); - cards.add(new SetCardInfo("Tiger Claws", 279, Rarity.COMMON, mage.cards.t.TigerClaws.class)); - cards.add(new SetCardInfo("Timid Drake", 111, Rarity.UNCOMMON, mage.cards.t.TimidDrake.class)); - cards.add(new SetCardInfo("Tonic Peddler", 54, Rarity.UNCOMMON, mage.cards.t.TonicPeddler.class)); - cards.add(new SetCardInfo("Tooth of Ramos", 313, Rarity.RARE, mage.cards.t.ToothOfRamos.class)); - cards.add(new SetCardInfo("Tower of the Magistrate", 330, Rarity.RARE, mage.cards.t.TowerOfTheMagistrate.class)); - cards.add(new SetCardInfo("Toymaker", 314, Rarity.UNCOMMON, mage.cards.t.Toymaker.class)); - cards.add(new SetCardInfo("Trade Routes", 112, Rarity.RARE, mage.cards.t.TradeRoutes.class)); - cards.add(new SetCardInfo("Tranquility", 280, Rarity.COMMON, mage.cards.t.Tranquility.class)); - cards.add(new SetCardInfo("Trap Runner", 55, Rarity.UNCOMMON, mage.cards.t.TrapRunner.class)); - cards.add(new SetCardInfo("Tremor", 220, Rarity.COMMON, mage.cards.t.Tremor.class)); - cards.add(new SetCardInfo("Two-Headed Dragon", 221, Rarity.RARE, mage.cards.t.TwoHeadedDragon.class)); - cards.add(new SetCardInfo("Undertaker", 167, Rarity.COMMON, mage.cards.u.Undertaker.class)); - cards.add(new SetCardInfo("Unmask", 168, Rarity.RARE, mage.cards.u.Unmask.class)); - cards.add(new SetCardInfo("Unnatural Hunger", 169, Rarity.RARE, mage.cards.u.UnnaturalHunger.class)); - cards.add(new SetCardInfo("Uphill Battle", 222, Rarity.UNCOMMON, mage.cards.u.UphillBattle.class)); - cards.add(new SetCardInfo("Vendetta", 170, Rarity.COMMON, mage.cards.v.Vendetta.class)); - cards.add(new SetCardInfo("Venomous Breath", 281, Rarity.UNCOMMON, mage.cards.v.VenomousBreath.class)); - cards.add(new SetCardInfo("Venomous Dragonfly", 282, Rarity.COMMON, mage.cards.v.VenomousDragonfly.class)); - cards.add(new SetCardInfo("Vernal Equinox", 283, Rarity.RARE, mage.cards.v.VernalEquinox.class)); - cards.add(new SetCardInfo("Vine Dryad", 284, Rarity.RARE, mage.cards.v.VineDryad.class)); - cards.add(new SetCardInfo("Vine Trellis", 285, Rarity.COMMON, mage.cards.v.VineTrellis.class)); - cards.add(new SetCardInfo("Volcanic Wind", 223, Rarity.UNCOMMON, mage.cards.v.VolcanicWind.class)); - cards.add(new SetCardInfo("Wall of Distortion", 171, Rarity.COMMON, mage.cards.w.WallOfDistortion.class)); - cards.add(new SetCardInfo("War Cadence", 224, Rarity.UNCOMMON, mage.cards.w.WarCadence.class)); - cards.add(new SetCardInfo("War Tax", 113, Rarity.UNCOMMON, mage.cards.w.WarTax.class)); - cards.add(new SetCardInfo("Warmonger", 225, Rarity.UNCOMMON, mage.cards.w.Warmonger.class)); - cards.add(new SetCardInfo("Warpath", 226, Rarity.UNCOMMON, mage.cards.w.Warpath.class)); - cards.add(new SetCardInfo("Waterfront Bouncer", 114, Rarity.COMMON, mage.cards.w.WaterfrontBouncer.class)); - cards.add(new SetCardInfo("Wave of Reckoning", 56, Rarity.RARE, mage.cards.w.WaveOfReckoning.class)); - cards.add(new SetCardInfo("Wild Jhovall", 227, Rarity.COMMON, mage.cards.w.WildJhovall.class)); - cards.add(new SetCardInfo("Wishmonger", 57, Rarity.UNCOMMON, mage.cards.w.Wishmonger.class)); - cards.add(new SetCardInfo("Word of Blasting", 228, Rarity.UNCOMMON, mage.cards.w.WordOfBlasting.class)); - cards.add(new SetCardInfo("Worry Beads", 315, Rarity.RARE, mage.cards.w.WorryBeads.class)); - } -} +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * @author North + */ +public final class MercadianMasques extends ExpansionSet { + + private static final MercadianMasques instance = new MercadianMasques(); + + public static MercadianMasques getInstance() { + return instance; + } + + private MercadianMasques() { + super("Mercadian Masques", "MMQ", ExpansionSet.buildDate(1999, 8, 25), SetType.EXPANSION); + this.blockName = "Masques"; + this.hasBoosters = true; + this.numBoosterLands = 0; + this.numBoosterCommon = 11; + this.numBoosterUncommon = 3; + this.numBoosterRare = 1; + this.ratioBoosterMythic = 0; + cards.add(new SetCardInfo("Aerial Caravan", 58, Rarity.RARE, mage.cards.a.AerialCaravan.class)); + cards.add(new SetCardInfo("Afterlife", 1, Rarity.UNCOMMON, mage.cards.a.Afterlife.class)); + cards.add(new SetCardInfo("Alabaster Wall", 2, Rarity.COMMON, mage.cards.a.AlabasterWall.class)); + cards.add(new SetCardInfo("Alley Grifters", 115, Rarity.COMMON, mage.cards.a.AlleyGrifters.class)); + cards.add(new SetCardInfo("Ancestral Mask", 229, Rarity.COMMON, mage.cards.a.AncestralMask.class)); + cards.add(new SetCardInfo("Armistice", 3, Rarity.RARE, mage.cards.a.Armistice.class)); + cards.add(new SetCardInfo("Arms Dealer", 172, Rarity.UNCOMMON, mage.cards.a.ArmsDealer.class)); + cards.add(new SetCardInfo("Arrest", 4, Rarity.UNCOMMON, mage.cards.a.Arrest.class)); + cards.add(new SetCardInfo("Assembly Hall", 286, Rarity.RARE, mage.cards.a.AssemblyHall.class)); + cards.add(new SetCardInfo("Ballista Squad", 5, Rarity.UNCOMMON, mage.cards.b.BallistaSquad.class)); + cards.add(new SetCardInfo("Balloon Peddler", 59, Rarity.COMMON, mage.cards.b.BalloonPeddler.class)); + cards.add(new SetCardInfo("Barbed Wire", 287, Rarity.UNCOMMON, mage.cards.b.BarbedWire.class)); + cards.add(new SetCardInfo("Battle Rampart", 173, Rarity.COMMON, mage.cards.b.BattleRampart.class)); + cards.add(new SetCardInfo("Battle Squadron", 174, Rarity.RARE, mage.cards.b.BattleSquadron.class)); + cards.add(new SetCardInfo("Bifurcate", 230, Rarity.RARE, mage.cards.b.Bifurcate.class)); + cards.add(new SetCardInfo("Black Market", 116, Rarity.RARE, mage.cards.b.BlackMarket.class)); + cards.add(new SetCardInfo("Blaster Mage", 175, Rarity.COMMON, mage.cards.b.BlasterMage.class)); + cards.add(new SetCardInfo("Blockade Runner", 60, Rarity.COMMON, mage.cards.b.BlockadeRunner.class)); + cards.add(new SetCardInfo("Blood Hound", 176, Rarity.RARE, mage.cards.b.BloodHound.class)); + cards.add(new SetCardInfo("Blood Oath", 177, Rarity.RARE, mage.cards.b.BloodOath.class)); + cards.add(new SetCardInfo("Boa Constrictor", 231, Rarity.UNCOMMON, mage.cards.b.BoaConstrictor.class)); + cards.add(new SetCardInfo("Bog Smugglers", 117, Rarity.COMMON, mage.cards.b.BogSmugglers.class)); + cards.add(new SetCardInfo("Bog Witch", 118, Rarity.COMMON, mage.cards.b.BogWitch.class)); + cards.add(new SetCardInfo("Brainstorm", 61, Rarity.COMMON, mage.cards.b.Brainstorm.class)); + cards.add(new SetCardInfo("Brawl", 178, Rarity.RARE, mage.cards.b.Brawl.class)); + cards.add(new SetCardInfo("Briar Patch", 232, Rarity.UNCOMMON, mage.cards.b.BriarPatch.class)); + cards.add(new SetCardInfo("Bribery", 62, Rarity.RARE, mage.cards.b.Bribery.class)); + cards.add(new SetCardInfo("Buoyancy", 63, Rarity.COMMON, mage.cards.b.Buoyancy.class)); + cards.add(new SetCardInfo("Cackling Witch", 119, Rarity.UNCOMMON, mage.cards.c.CacklingWitch.class)); + cards.add(new SetCardInfo("Caller of the Hunt", 233, Rarity.RARE, mage.cards.c.CallerOfTheHunt.class)); + cards.add(new SetCardInfo("Cateran Brute", 120, Rarity.COMMON, mage.cards.c.CateranBrute.class)); + cards.add(new SetCardInfo("Cateran Enforcer", 121, Rarity.UNCOMMON, mage.cards.c.CateranEnforcer.class)); + cards.add(new SetCardInfo("Cateran Kidnappers", 122, Rarity.UNCOMMON, mage.cards.c.CateranKidnappers.class)); + cards.add(new SetCardInfo("Cateran Overlord", 123, Rarity.RARE, mage.cards.c.CateranOverlord.class)); + cards.add(new SetCardInfo("Cateran Persuader", 124, Rarity.COMMON, mage.cards.c.CateranPersuader.class)); + cards.add(new SetCardInfo("Cateran Slaver", 125, Rarity.RARE, mage.cards.c.CateranSlaver.class)); + cards.add(new SetCardInfo("Cateran Summons", 126, Rarity.UNCOMMON, mage.cards.c.CateranSummons.class)); + cards.add(new SetCardInfo("Caustic Wasps", 234, Rarity.UNCOMMON, mage.cards.c.CausticWasps.class)); + cards.add(new SetCardInfo("Cave-In", 180, Rarity.RARE, mage.cards.c.CaveIn.class)); + cards.add(new SetCardInfo("Cavern Crawler", 181, Rarity.COMMON, mage.cards.c.CavernCrawler.class)); + cards.add(new SetCardInfo("Cave Sense", 179, Rarity.COMMON, mage.cards.c.CaveSense.class)); + cards.add(new SetCardInfo("Ceremonial Guard", 182, Rarity.COMMON, mage.cards.c.CeremonialGuard.class)); + cards.add(new SetCardInfo("Chambered Nautilus", 64, Rarity.UNCOMMON, mage.cards.c.ChamberedNautilus.class)); + cards.add(new SetCardInfo("Chameleon Spirit", 65, Rarity.UNCOMMON, mage.cards.c.ChameleonSpirit.class)); + cards.add(new SetCardInfo("Charisma", 66, Rarity.RARE, mage.cards.c.Charisma.class)); + cards.add(new SetCardInfo("Charm Peddler", 6, Rarity.COMMON, mage.cards.c.CharmPeddler.class)); + cards.add(new SetCardInfo("Charmed Griffin", 7, Rarity.UNCOMMON, mage.cards.c.CharmedGriffin.class)); + cards.add(new SetCardInfo("Cho-Arrim Alchemist", 8, Rarity.RARE, mage.cards.c.ChoArrimAlchemist.class)); + cards.add(new SetCardInfo("Cho-Arrim Bruiser", 9, Rarity.RARE, mage.cards.c.ChoArrimBruiser.class)); + cards.add(new SetCardInfo("Cho-Arrim Legate", 10, Rarity.UNCOMMON, mage.cards.c.ChoArrimLegate.class)); + cards.add(new SetCardInfo("Cho-Manno, Revolutionary", 11, Rarity.RARE, mage.cards.c.ChoMannoRevolutionary.class)); + cards.add(new SetCardInfo("Cho-Manno's Blessing", 12, Rarity.COMMON, mage.cards.c.ChoMannosBlessing.class)); + cards.add(new SetCardInfo("Cinder Elemental", 183, Rarity.UNCOMMON, mage.cards.c.CinderElemental.class)); + cards.add(new SetCardInfo("Clear the Land", 235, Rarity.RARE, mage.cards.c.ClearTheLand.class)); + cards.add(new SetCardInfo("Close Quarters", 184, Rarity.UNCOMMON, mage.cards.c.CloseQuarters.class)); + cards.add(new SetCardInfo("Cloud Sprite", 67, Rarity.COMMON, mage.cards.c.CloudSprite.class)); + cards.add(new SetCardInfo("Coastal Piracy", 68, Rarity.UNCOMMON, mage.cards.c.CoastalPiracy.class)); + cards.add(new SetCardInfo("Collective Unconscious", 236, Rarity.RARE, mage.cards.c.CollectiveUnconscious.class)); + cards.add(new SetCardInfo("Common Cause", 13, Rarity.RARE, mage.cards.c.CommonCause.class)); + cards.add(new SetCardInfo("Conspiracy", 127, Rarity.RARE, mage.cards.c.Conspiracy.class)); + cards.add(new SetCardInfo("Cornered Market", 14, Rarity.RARE, mage.cards.c.CorneredMarket.class)); + cards.add(new SetCardInfo("Corrupt Official", 128, Rarity.RARE, mage.cards.c.CorruptOfficial.class)); + cards.add(new SetCardInfo("Counterspell", 69, Rarity.COMMON, mage.cards.c.Counterspell.class)); + cards.add(new SetCardInfo("Cowardice", 70, Rarity.RARE, mage.cards.c.Cowardice.class)); + cards.add(new SetCardInfo("Crackdown", 15, Rarity.RARE, mage.cards.c.Crackdown.class)); + cards.add(new SetCardInfo("Crag Saurian", 185, Rarity.RARE, mage.cards.c.CragSaurian.class)); + cards.add(new SetCardInfo("Crash", 186, Rarity.COMMON, mage.cards.c.Crash.class)); + cards.add(new SetCardInfo("Credit Voucher", 289, Rarity.UNCOMMON, mage.cards.c.CreditVoucher.class)); + cards.add(new SetCardInfo("Crenellated Wall", 290, Rarity.UNCOMMON, mage.cards.c.CrenellatedWall.class)); + cards.add(new SetCardInfo("Crooked Scales", 291, Rarity.RARE, mage.cards.c.CrookedScales.class)); + cards.add(new SetCardInfo("Crossbow Infantry", 16, Rarity.COMMON, mage.cards.c.CrossbowInfantry.class)); + cards.add(new SetCardInfo("Crumbling Sanctuary", 292, Rarity.RARE, mage.cards.c.CrumblingSanctuary.class)); + cards.add(new SetCardInfo("Customs Depot", 71, Rarity.UNCOMMON, mage.cards.c.CustomsDepot.class)); + cards.add(new SetCardInfo("Dark Ritual", 129, Rarity.COMMON, mage.cards.d.DarkRitual.class)); + cards.add(new SetCardInfo("Darting Merfolk", 72, Rarity.COMMON, mage.cards.d.DartingMerfolk.class)); + cards.add(new SetCardInfo("Dawnstrider", 237, Rarity.RARE, mage.cards.d.Dawnstrider.class)); + cards.add(new SetCardInfo("Deadly Insect", 238, Rarity.COMMON, mage.cards.d.DeadlyInsect.class)); + cards.add(new SetCardInfo("Deathgazer", 130, Rarity.UNCOMMON, mage.cards.d.Deathgazer.class)); + cards.add(new SetCardInfo("Deepwood Drummer", 239, Rarity.COMMON, mage.cards.d.DeepwoodDrummer.class)); + cards.add(new SetCardInfo("Deepwood Elder", 240, Rarity.RARE, mage.cards.d.DeepwoodElder.class)); + cards.add(new SetCardInfo("Deepwood Ghoul", 131, Rarity.COMMON, mage.cards.d.DeepwoodGhoul.class)); + cards.add(new SetCardInfo("Deepwood Legate", 132, Rarity.UNCOMMON, mage.cards.d.DeepwoodLegate.class)); + cards.add(new SetCardInfo("Deepwood Tantiv", 241, Rarity.UNCOMMON, mage.cards.d.DeepwoodTantiv.class)); + cards.add(new SetCardInfo("Deepwood Wolverine", 242, Rarity.COMMON, mage.cards.d.DeepwoodWolverine.class)); + cards.add(new SetCardInfo("Dehydration", 73, Rarity.COMMON, mage.cards.d.Dehydration.class)); + cards.add(new SetCardInfo("Delraich", 133, Rarity.RARE, mage.cards.d.Delraich.class)); + cards.add(new SetCardInfo("Desert Twister", 243, Rarity.UNCOMMON, mage.cards.d.DesertTwister.class)); + cards.add(new SetCardInfo("Devout Witness", 17, Rarity.COMMON, mage.cards.d.DevoutWitness.class)); + cards.add(new SetCardInfo("Diplomatic Escort", 74, Rarity.UNCOMMON, mage.cards.d.DiplomaticEscort.class)); + cards.add(new SetCardInfo("Diplomatic Immunity", 75, Rarity.COMMON, mage.cards.d.DiplomaticImmunity.class)); + cards.add(new SetCardInfo("Disenchant", 18, Rarity.COMMON, mage.cards.d.Disenchant.class)); + cards.add(new SetCardInfo("Distorting Lens", 293, Rarity.RARE, mage.cards.d.DistortingLens.class)); + cards.add(new SetCardInfo("Drake Hatchling", 76, Rarity.COMMON, mage.cards.d.DrakeHatchling.class)); + cards.add(new SetCardInfo("Dust Bowl", 316, Rarity.RARE, mage.cards.d.DustBowl.class)); + cards.add(new SetCardInfo("Embargo", 77, Rarity.RARE, mage.cards.e.Embargo.class)); + cards.add(new SetCardInfo("Energy Flux", 78, Rarity.UNCOMMON, mage.cards.e.EnergyFlux.class)); + cards.add(new SetCardInfo("Enslaved Horror", 134, Rarity.UNCOMMON, mage.cards.e.EnslavedHorror.class)); + cards.add(new SetCardInfo("Extortion", 135, Rarity.RARE, mage.cards.e.Extortion.class)); + cards.add(new SetCardInfo("Extravagant Spirit", 79, Rarity.RARE, mage.cards.e.ExtravagantSpirit.class)); + cards.add(new SetCardInfo("Eye of Ramos", 294, Rarity.RARE, mage.cards.e.EyeOfRamos.class)); + cards.add(new SetCardInfo("False Demise", 80, Rarity.UNCOMMON, mage.cards.f.FalseDemise.class)); + cards.add(new SetCardInfo("Ferocity", 245, Rarity.COMMON, mage.cards.f.Ferocity.class)); + cards.add(new SetCardInfo("Flailing Manticore", 187, Rarity.RARE, mage.cards.f.FlailingManticore.class)); + cards.add(new SetCardInfo("Flailing Ogre", 188, Rarity.UNCOMMON, mage.cards.f.FlailingOgre.class)); + cards.add(new SetCardInfo("Flailing Soldier", 189, Rarity.COMMON, mage.cards.f.FlailingSoldier.class)); + cards.add(new SetCardInfo("Flaming Sword", 190, Rarity.COMMON, mage.cards.f.FlamingSword.class)); + cards.add(new SetCardInfo("Food Chain", 246, Rarity.RARE, mage.cards.f.FoodChain.class)); + cards.add(new SetCardInfo("Forced March", 136, Rarity.RARE, mage.cards.f.ForcedMarch.class)); + cards.add(new SetCardInfo("Forest", 347, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Forest", 348, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Forest", 349, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Forest", 350, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Foster", 247, Rarity.RARE, mage.cards.f.Foster.class)); + cards.add(new SetCardInfo("Fountain of Cho", 317, Rarity.UNCOMMON, mage.cards.f.FountainOfCho.class)); + cards.add(new SetCardInfo("Fountain Watch", 19, Rarity.RARE, mage.cards.f.FountainWatch.class)); + cards.add(new SetCardInfo("Fresh Volunteers", 20, Rarity.COMMON, mage.cards.f.FreshVolunteers.class)); + cards.add(new SetCardInfo("Furious Assault", 191, Rarity.COMMON, mage.cards.f.FuriousAssault.class)); + cards.add(new SetCardInfo("Game Preserve", 248, Rarity.RARE, mage.cards.g.GamePreserve.class)); + cards.add(new SetCardInfo("General's Regalia", 295, Rarity.RARE, mage.cards.g.GeneralsRegalia.class)); + cards.add(new SetCardInfo("Gerrard's Irregulars", 192, Rarity.COMMON, mage.cards.g.GerrardsIrregulars.class)); + cards.add(new SetCardInfo("Ghoul's Feast", 137, Rarity.UNCOMMON, mage.cards.g.GhoulsFeast.class)); + cards.add(new SetCardInfo("Giant Caterpillar", 249, Rarity.COMMON, mage.cards.g.GiantCaterpillar.class)); + cards.add(new SetCardInfo("Glowing Anemone", 81, Rarity.UNCOMMON, mage.cards.g.GlowingAnemone.class)); + cards.add(new SetCardInfo("Groundskeeper", 250, Rarity.UNCOMMON, mage.cards.g.Groundskeeper.class)); + cards.add(new SetCardInfo("Gush", 82, Rarity.COMMON, mage.cards.g.Gush.class)); + cards.add(new SetCardInfo("Hammer Mage", 193, Rarity.UNCOMMON, mage.cards.h.HammerMage.class)); + cards.add(new SetCardInfo("Haunted Crossroads", 138, Rarity.UNCOMMON, mage.cards.h.HauntedCrossroads.class)); + cards.add(new SetCardInfo("Heart of Ramos", 296, Rarity.RARE, mage.cards.h.HeartOfRamos.class)); + cards.add(new SetCardInfo("Henge Guardian", 297, Rarity.UNCOMMON, mage.cards.h.HengeGuardian.class)); + cards.add(new SetCardInfo("Henge of Ramos", 318, Rarity.UNCOMMON, mage.cards.h.HengeOfRamos.class)); + cards.add(new SetCardInfo("Hickory Woodlot", 319, Rarity.COMMON, mage.cards.h.HickoryWoodlot.class)); + cards.add(new SetCardInfo("High Market", 320, Rarity.RARE, mage.cards.h.HighMarket.class)); + cards.add(new SetCardInfo("High Seas", 83, Rarity.UNCOMMON, mage.cards.h.HighSeas.class)); + cards.add(new SetCardInfo("Highway Robber", 139, Rarity.COMMON, mage.cards.h.HighwayRobber.class)); + cards.add(new SetCardInfo("Hired Giant", 194, Rarity.UNCOMMON, mage.cards.h.HiredGiant.class)); + cards.add(new SetCardInfo("Honor the Fallen", 21, Rarity.RARE, mage.cards.h.HonorTheFallen.class)); + cards.add(new SetCardInfo("Hoodwink", 84, Rarity.COMMON, mage.cards.h.Hoodwink.class)); + cards.add(new SetCardInfo("Horned Troll", 251, Rarity.COMMON, mage.cards.h.HornedTroll.class)); + cards.add(new SetCardInfo("Horn of Plenty", 298, Rarity.RARE, mage.cards.h.HornOfPlenty.class)); + cards.add(new SetCardInfo("Horn of Ramos", 299, Rarity.RARE, mage.cards.h.HornOfRamos.class)); + cards.add(new SetCardInfo("Howling Wolf", 252, Rarity.COMMON, mage.cards.h.HowlingWolf.class)); + cards.add(new SetCardInfo("Hunted Wumpus", 253, Rarity.UNCOMMON, mage.cards.h.HuntedWumpus.class)); + cards.add(new SetCardInfo("Ignoble Soldier", 22, Rarity.UNCOMMON, mage.cards.i.IgnobleSoldier.class)); + cards.add(new SetCardInfo("Indentured Djinn", 85, Rarity.UNCOMMON, mage.cards.i.IndenturedDjinn.class)); + cards.add(new SetCardInfo("Instigator", 140, Rarity.RARE, mage.cards.i.Instigator.class)); + cards.add(new SetCardInfo("Insubordination", 141, Rarity.COMMON, mage.cards.i.Insubordination.class)); + cards.add(new SetCardInfo("Intimidation", 142, Rarity.UNCOMMON, mage.cards.i.Intimidation.class)); + cards.add(new SetCardInfo("Invigorate", 254, Rarity.COMMON, mage.cards.i.Invigorate.class)); + cards.add(new SetCardInfo("Inviolability", 23, Rarity.COMMON, mage.cards.i.Inviolability.class)); + cards.add(new SetCardInfo("Iron Lance", 300, Rarity.UNCOMMON, mage.cards.i.IronLance.class)); + cards.add(new SetCardInfo("Island", 335, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Island", 336, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Island", 337, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Island", 338, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ivory Mask", 24, Rarity.RARE, mage.cards.i.IvoryMask.class)); + cards.add(new SetCardInfo("Jeweled Torque", 301, Rarity.UNCOMMON, mage.cards.j.JeweledTorque.class)); + cards.add(new SetCardInfo("Jhovall Queen", 25, Rarity.RARE, mage.cards.j.JhovallQueen.class)); + cards.add(new SetCardInfo("Jhovall Rider", 26, Rarity.UNCOMMON, mage.cards.j.JhovallRider.class)); + cards.add(new SetCardInfo("Karn's Touch", 86, Rarity.RARE, mage.cards.k.KarnsTouch.class)); + cards.add(new SetCardInfo("Kris Mage", 195, Rarity.COMMON, mage.cards.k.KrisMage.class)); + cards.add(new SetCardInfo("Kyren Archive", 302, Rarity.RARE, mage.cards.k.KyrenArchive.class)); + cards.add(new SetCardInfo("Kyren Glider", 196, Rarity.COMMON, mage.cards.k.KyrenGlider.class)); + cards.add(new SetCardInfo("Kyren Legate", 197, Rarity.UNCOMMON, mage.cards.k.KyrenLegate.class)); + cards.add(new SetCardInfo("Kyren Negotiations", 198, Rarity.UNCOMMON, mage.cards.k.KyrenNegotiations.class)); + cards.add(new SetCardInfo("Kyren Sniper", 199, Rarity.COMMON, mage.cards.k.KyrenSniper.class)); + cards.add(new SetCardInfo("Kyren Toy", 303, Rarity.RARE, mage.cards.k.KyrenToy.class)); + cards.add(new SetCardInfo("Land Grant", 255, Rarity.COMMON, mage.cards.l.LandGrant.class)); + cards.add(new SetCardInfo("Larceny", 143, Rarity.UNCOMMON, mage.cards.l.Larceny.class)); + cards.add(new SetCardInfo("Last Breath", 27, Rarity.UNCOMMON, mage.cards.l.LastBreath.class)); + cards.add(new SetCardInfo("Lava Runner", 200, Rarity.RARE, mage.cards.l.LavaRunner.class)); + cards.add(new SetCardInfo("Liability", 144, Rarity.RARE, mage.cards.l.Liability.class)); + cards.add(new SetCardInfo("Lightning Hounds", 201, Rarity.COMMON, mage.cards.l.LightningHounds.class)); + cards.add(new SetCardInfo("Lithophage", 202, Rarity.RARE, mage.cards.l.Lithophage.class)); + cards.add(new SetCardInfo("Lumbering Satyr", 257, Rarity.UNCOMMON, mage.cards.l.LumberingSatyr.class)); + cards.add(new SetCardInfo("Lunge", 203, Rarity.COMMON, mage.cards.l.Lunge.class)); + cards.add(new SetCardInfo("Lure", 258, Rarity.UNCOMMON, mage.cards.l.Lure.class)); + cards.add(new SetCardInfo("Maggot Therapy", 145, Rarity.COMMON, mage.cards.m.MaggotTherapy.class)); + cards.add(new SetCardInfo("Magistrate's Scepter", 304, Rarity.RARE, mage.cards.m.MagistratesScepter.class)); + cards.add(new SetCardInfo("Magistrate's Veto", 204, Rarity.UNCOMMON, mage.cards.m.MagistratesVeto.class)); + cards.add(new SetCardInfo("Megatherium", 259, Rarity.RARE, mage.cards.m.Megatherium.class)); + cards.add(new SetCardInfo("Mercadia's Downfall", 205, Rarity.UNCOMMON, mage.cards.m.MercadiasDownfall.class)); + cards.add(new SetCardInfo("Mercadian Atlas", 305, Rarity.RARE, mage.cards.m.MercadianAtlas.class)); + cards.add(new SetCardInfo("Mercadian Bazaar", 321, Rarity.UNCOMMON, mage.cards.m.MercadianBazaar.class)); + cards.add(new SetCardInfo("Mercadian Lift", 306, Rarity.RARE, mage.cards.m.MercadianLift.class)); + cards.add(new SetCardInfo("Midnight Ritual", 146, Rarity.RARE, mage.cards.m.MidnightRitual.class)); + cards.add(new SetCardInfo("Misdirection", 87, Rarity.RARE, mage.cards.m.Misdirection.class)); + cards.add(new SetCardInfo("Misshapen Fiend", 147, Rarity.COMMON, mage.cards.m.MisshapenFiend.class)); + cards.add(new SetCardInfo("Misstep", 88, Rarity.COMMON, mage.cards.m.Misstep.class)); + cards.add(new SetCardInfo("Molting Harpy", 148, Rarity.UNCOMMON, mage.cards.m.MoltingHarpy.class)); + cards.add(new SetCardInfo("Moment of Silence", 28, Rarity.COMMON, mage.cards.m.MomentOfSilence.class)); + cards.add(new SetCardInfo("Monkey Cage", 307, Rarity.RARE, mage.cards.m.MonkeyCage.class)); + cards.add(new SetCardInfo("Moonlit Wake", 29, Rarity.UNCOMMON, mage.cards.m.MoonlitWake.class)); + cards.add(new SetCardInfo("Mountain", 343, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 344, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 345, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 346, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Muzzle", 30, Rarity.COMMON, mage.cards.m.Muzzle.class)); + cards.add(new SetCardInfo("Natural Affinity", 260, Rarity.RARE, mage.cards.n.NaturalAffinity.class)); + cards.add(new SetCardInfo("Nether Spirit", 149, Rarity.RARE, mage.cards.n.NetherSpirit.class)); + cards.add(new SetCardInfo("Nightwind Glider", 31, Rarity.COMMON, mage.cards.n.NightwindGlider.class)); + cards.add(new SetCardInfo("Noble Purpose", 32, Rarity.UNCOMMON, mage.cards.n.NoblePurpose.class)); + cards.add(new SetCardInfo("Notorious Assassin", 150, Rarity.RARE, mage.cards.n.NotoriousAssassin.class)); + cards.add(new SetCardInfo("Ogre Taskmaster", 206, Rarity.UNCOMMON, mage.cards.o.OgreTaskmaster.class)); + cards.add(new SetCardInfo("Orim's Cure", 33, Rarity.COMMON, mage.cards.o.OrimsCure.class)); + cards.add(new SetCardInfo("Overtaker", 89, Rarity.RARE, mage.cards.o.Overtaker.class)); + cards.add(new SetCardInfo("Panacea", 308, Rarity.UNCOMMON, mage.cards.p.Panacea.class)); + cards.add(new SetCardInfo("Pangosaur", 261, Rarity.RARE, mage.cards.p.Pangosaur.class)); + cards.add(new SetCardInfo("Peat Bog", 322, Rarity.COMMON, mage.cards.p.PeatBog.class)); + cards.add(new SetCardInfo("Pious Warrior", 34, Rarity.COMMON, mage.cards.p.PiousWarrior.class)); + cards.add(new SetCardInfo("Plains", 331, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 332, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 333, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 334, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Port Inspector", 90, Rarity.COMMON, mage.cards.p.PortInspector.class)); + cards.add(new SetCardInfo("Power Matrix", 309, Rarity.RARE, mage.cards.p.PowerMatrix.class)); + cards.add(new SetCardInfo("Pretender's Claim", 151, Rarity.UNCOMMON, mage.cards.p.PretendersClaim.class)); + cards.add(new SetCardInfo("Primeval Shambler", 152, Rarity.UNCOMMON, mage.cards.p.PrimevalShambler.class)); + cards.add(new SetCardInfo("Puffer Extract", 310, Rarity.UNCOMMON, mage.cards.p.PufferExtract.class)); + cards.add(new SetCardInfo("Pulverize", 207, Rarity.RARE, mage.cards.p.Pulverize.class)); + cards.add(new SetCardInfo("Putrefaction", 153, Rarity.UNCOMMON, mage.cards.p.Putrefaction.class)); + cards.add(new SetCardInfo("Puppet's Verdict", 208, Rarity.RARE, mage.cards.p.PuppetsVerdict.class)); + cards.add(new SetCardInfo("Quagmire Lamprey", 154, Rarity.UNCOMMON, mage.cards.q.QuagmireLamprey.class)); + cards.add(new SetCardInfo("Rain of Tears", 155, Rarity.UNCOMMON, mage.cards.r.RainOfTears.class)); + cards.add(new SetCardInfo("Ramosian Captain", 35, Rarity.UNCOMMON, mage.cards.r.RamosianCaptain.class)); + cards.add(new SetCardInfo("Ramosian Commander", 36, Rarity.UNCOMMON, mage.cards.r.RamosianCommander.class)); + cards.add(new SetCardInfo("Ramosian Lieutenant", 37, Rarity.COMMON, mage.cards.r.RamosianLieutenant.class)); + cards.add(new SetCardInfo("Ramosian Rally", 38, Rarity.COMMON, mage.cards.r.RamosianRally.class)); + cards.add(new SetCardInfo("Ramosian Sergeant", 39, Rarity.COMMON, mage.cards.r.RamosianSergeant.class)); + cards.add(new SetCardInfo("Ramosian Sky Marshal", 40, Rarity.RARE, mage.cards.r.RamosianSkyMarshal.class)); + cards.add(new SetCardInfo("Rampart Crawler", 156, Rarity.COMMON, mage.cards.r.RampartCrawler.class)); + cards.add(new SetCardInfo("Rappelling Scouts", 41, Rarity.RARE, mage.cards.r.RappellingScouts.class)); + cards.add(new SetCardInfo("Remote Farm", 323, Rarity.COMMON, mage.cards.r.RemoteFarm.class)); + cards.add(new SetCardInfo("Renounce", 42, Rarity.UNCOMMON, mage.cards.r.Renounce.class)); + cards.add(new SetCardInfo("Revered Elder", 43, Rarity.COMMON, mage.cards.r.ReveredElder.class)); + cards.add(new SetCardInfo("Reverent Mantra", 44, Rarity.RARE, mage.cards.r.ReverentMantra.class)); + cards.add(new SetCardInfo("Revive", 262, Rarity.UNCOMMON, mage.cards.r.Revive.class)); + cards.add(new SetCardInfo("Righteous Aura", 45, Rarity.UNCOMMON, mage.cards.r.RighteousAura.class)); + cards.add(new SetCardInfo("Righteous Indignation", 46, Rarity.UNCOMMON, mage.cards.r.RighteousIndignation.class)); + cards.add(new SetCardInfo("Rishadan Airship", 91, Rarity.COMMON, mage.cards.r.RishadanAirship.class)); + cards.add(new SetCardInfo("Rishadan Brigand", 92, Rarity.RARE, mage.cards.r.RishadanBrigand.class)); + cards.add(new SetCardInfo("Rishadan Cutpurse", 93, Rarity.COMMON, mage.cards.r.RishadanCutpurse.class)); + cards.add(new SetCardInfo("Rishadan Footpad", 94, Rarity.UNCOMMON, mage.cards.r.RishadanFootpad.class)); + cards.add(new SetCardInfo("Rishadan Pawnshop", 311, Rarity.RARE, mage.cards.r.RishadanPawnshop.class)); + cards.add(new SetCardInfo("Rishadan Port", 324, Rarity.RARE, mage.cards.r.RishadanPort.class)); + cards.add(new SetCardInfo("Robber Fly", 209, Rarity.UNCOMMON, mage.cards.r.RobberFly.class)); + cards.add(new SetCardInfo("Rock Badger", 210, Rarity.UNCOMMON, mage.cards.r.RockBadger.class)); + cards.add(new SetCardInfo("Rouse", 157, Rarity.COMMON, mage.cards.r.Rouse.class)); + cards.add(new SetCardInfo("Rushwood Dryad", 263, Rarity.COMMON, mage.cards.r.RushwoodDryad.class)); + cards.add(new SetCardInfo("Rushwood Elemental", 264, Rarity.RARE, mage.cards.r.RushwoodElemental.class)); + cards.add(new SetCardInfo("Rushwood Grove", 325, Rarity.UNCOMMON, mage.cards.r.RushwoodGrove.class)); + cards.add(new SetCardInfo("Rushwood Herbalist", 265, Rarity.COMMON, mage.cards.r.RushwoodHerbalist.class)); + cards.add(new SetCardInfo("Rushwood Legate", 266, Rarity.UNCOMMON, mage.cards.r.RushwoodLegate.class)); + cards.add(new SetCardInfo("Saber Ants", 267, Rarity.UNCOMMON, mage.cards.s.SaberAnts.class)); + cards.add(new SetCardInfo("Sacred Prey", 268, Rarity.COMMON, mage.cards.s.SacredPrey.class)); + cards.add(new SetCardInfo("Sailmonger", 95, Rarity.UNCOMMON, mage.cards.s.Sailmonger.class)); + cards.add(new SetCardInfo("Sand Squid", 96, Rarity.RARE, mage.cards.s.SandSquid.class)); + cards.add(new SetCardInfo("Sandstone Needle", 326, Rarity.COMMON, mage.cards.s.SandstoneNeedle.class)); + cards.add(new SetCardInfo("Saprazzan Bailiff", 97, Rarity.RARE, mage.cards.s.SaprazzanBailiff.class)); + cards.add(new SetCardInfo("Saprazzan Breaker", 98, Rarity.UNCOMMON, mage.cards.s.SaprazzanBreaker.class)); + cards.add(new SetCardInfo("Saprazzan Cove", 327, Rarity.UNCOMMON, mage.cards.s.SaprazzanCove.class)); + cards.add(new SetCardInfo("Saprazzan Heir", 99, Rarity.RARE, mage.cards.s.SaprazzanHeir.class)); + cards.add(new SetCardInfo("Saprazzan Legate", 100, Rarity.UNCOMMON, mage.cards.s.SaprazzanLegate.class)); + cards.add(new SetCardInfo("Saprazzan Outrigger", 101, Rarity.COMMON, mage.cards.s.SaprazzanOutrigger.class)); + cards.add(new SetCardInfo("Saprazzan Raider", 102, Rarity.COMMON, mage.cards.s.SaprazzanRaider.class)); + cards.add(new SetCardInfo("Saprazzan Skerry", 328, Rarity.COMMON, mage.cards.s.SaprazzanSkerry.class)); + cards.add(new SetCardInfo("Scandalmonger", 158, Rarity.UNCOMMON, mage.cards.s.Scandalmonger.class)); + cards.add(new SetCardInfo("Security Detail", 47, Rarity.RARE, mage.cards.s.SecurityDetail.class)); + cards.add(new SetCardInfo("Seismic Mage", 211, Rarity.RARE, mage.cards.s.SeismicMage.class)); + cards.add(new SetCardInfo("Sever Soul", 159, Rarity.COMMON, mage.cards.s.SeverSoul.class)); + cards.add(new SetCardInfo("Shock Troops", 212, Rarity.COMMON, mage.cards.s.ShockTroops.class)); + cards.add(new SetCardInfo("Shoving Match", 103, Rarity.UNCOMMON, mage.cards.s.ShovingMatch.class)); + cards.add(new SetCardInfo("Silent Assassin", 160, Rarity.RARE, mage.cards.s.SilentAssassin.class)); + cards.add(new SetCardInfo("Silverglade Elemental", 269, Rarity.COMMON, mage.cards.s.SilvergladeElemental.class)); + cards.add(new SetCardInfo("Silverglade Pathfinder", 270, Rarity.UNCOMMON, mage.cards.s.SilvergladePathfinder.class)); + cards.add(new SetCardInfo("Sizzle", 213, Rarity.COMMON, mage.cards.s.Sizzle.class)); + cards.add(new SetCardInfo("Skulking Fugitive", 161, Rarity.COMMON, mage.cards.s.SkulkingFugitive.class)); + cards.add(new SetCardInfo("Skull of Ramos", 312, Rarity.RARE, mage.cards.s.SkullOfRamos.class)); + cards.add(new SetCardInfo("Snake Pit", 271, Rarity.UNCOMMON, mage.cards.s.SnakePit.class)); + cards.add(new SetCardInfo("Snorting Gahr", 272, Rarity.COMMON, mage.cards.s.SnortingGahr.class)); + cards.add(new SetCardInfo("Snuff Out", 162, Rarity.COMMON, mage.cards.s.SnuffOut.class)); + cards.add(new SetCardInfo("Soothing Balm", 48, Rarity.COMMON, mage.cards.s.SoothingBalm.class)); + cards.add(new SetCardInfo("Soothsaying", 104, Rarity.UNCOMMON, mage.cards.s.Soothsaying.class)); + cards.add(new SetCardInfo("Soul Channeling", 163, Rarity.COMMON, mage.cards.s.SoulChanneling.class)); + cards.add(new SetCardInfo("Specter's Wail", 164, Rarity.COMMON, mage.cards.s.SpectersWail.class)); + cards.add(new SetCardInfo("Spidersilk Armor", 273, Rarity.COMMON, mage.cards.s.SpidersilkArmor.class)); + cards.add(new SetCardInfo("Spiritual Focus", 49, Rarity.RARE, mage.cards.s.SpiritualFocus.class)); + cards.add(new SetCardInfo("Spontaneous Generation", 274, Rarity.RARE, mage.cards.s.SpontaneousGeneration.class)); + cards.add(new SetCardInfo("Squall", 275, Rarity.COMMON, mage.cards.s.Squall.class)); + cards.add(new SetCardInfo("Squallmonger", 276, Rarity.UNCOMMON, mage.cards.s.Squallmonger.class)); + cards.add(new SetCardInfo("Squee, Goblin Nabob", 214, Rarity.RARE, mage.cards.s.SqueeGoblinNabob.class)); + cards.add(new SetCardInfo("Squeeze", 105, Rarity.RARE, mage.cards.s.Squeeze.class)); + cards.add(new SetCardInfo("Stamina", 277, Rarity.UNCOMMON, mage.cards.s.Stamina.class)); + cards.add(new SetCardInfo("Statecraft", 106, Rarity.RARE, mage.cards.s.Statecraft.class)); + cards.add(new SetCardInfo("Steadfast Guard", 50, Rarity.COMMON, mage.cards.s.SteadfastGuard.class)); + cards.add(new SetCardInfo("Stinging Barrier", 107, Rarity.COMMON, mage.cards.s.StingingBarrier.class)); + cards.add(new SetCardInfo("Stone Rain", 215, Rarity.COMMON, mage.cards.s.StoneRain.class)); + cards.add(new SetCardInfo("Story Circle", 51, Rarity.UNCOMMON, mage.cards.s.StoryCircle.class)); + cards.add(new SetCardInfo("Strongarm Thug", 165, Rarity.UNCOMMON, mage.cards.s.StrongarmThug.class)); + cards.add(new SetCardInfo("Subterranean Hangar", 329, Rarity.UNCOMMON, mage.cards.s.SubterraneanHangar.class)); + cards.add(new SetCardInfo("Sustenance", 278, Rarity.UNCOMMON, mage.cards.s.Sustenance.class)); + cards.add(new SetCardInfo("Swamp", 339, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Swamp", 340, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Swamp", 341, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Swamp", 342, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Task Force", 52, Rarity.COMMON, mage.cards.t.TaskForce.class)); + cards.add(new SetCardInfo("Tectonic Break", 216, Rarity.RARE, mage.cards.t.TectonicBreak.class)); + cards.add(new SetCardInfo("Territorial Dispute", 217, Rarity.RARE, mage.cards.t.TerritorialDispute.class)); + cards.add(new SetCardInfo("Thermal Glider", 53, Rarity.COMMON, mage.cards.t.ThermalGlider.class)); + cards.add(new SetCardInfo("Thieves' Auction", 218, Rarity.RARE, mage.cards.t.ThievesAuction.class)); + cards.add(new SetCardInfo("Thrashing Wumpus", 166, Rarity.RARE, mage.cards.t.ThrashingWumpus.class)); + cards.add(new SetCardInfo("Thunderclap", 219, Rarity.COMMON, mage.cards.t.Thunderclap.class)); + cards.add(new SetCardInfo("Thwart", 108, Rarity.UNCOMMON, mage.cards.t.Thwart.class)); + cards.add(new SetCardInfo("Tidal Bore", 109, Rarity.COMMON, mage.cards.t.TidalBore.class)); + cards.add(new SetCardInfo("Tidal Kraken", 110, Rarity.RARE, mage.cards.t.TidalKraken.class)); + cards.add(new SetCardInfo("Tiger Claws", 279, Rarity.COMMON, mage.cards.t.TigerClaws.class)); + cards.add(new SetCardInfo("Timid Drake", 111, Rarity.UNCOMMON, mage.cards.t.TimidDrake.class)); + cards.add(new SetCardInfo("Tonic Peddler", 54, Rarity.UNCOMMON, mage.cards.t.TonicPeddler.class)); + cards.add(new SetCardInfo("Tooth of Ramos", 313, Rarity.RARE, mage.cards.t.ToothOfRamos.class)); + cards.add(new SetCardInfo("Tower of the Magistrate", 330, Rarity.RARE, mage.cards.t.TowerOfTheMagistrate.class)); + cards.add(new SetCardInfo("Toymaker", 314, Rarity.UNCOMMON, mage.cards.t.Toymaker.class)); + cards.add(new SetCardInfo("Trade Routes", 112, Rarity.RARE, mage.cards.t.TradeRoutes.class)); + cards.add(new SetCardInfo("Tranquility", 280, Rarity.COMMON, mage.cards.t.Tranquility.class)); + cards.add(new SetCardInfo("Trap Runner", 55, Rarity.UNCOMMON, mage.cards.t.TrapRunner.class)); + cards.add(new SetCardInfo("Tremor", 220, Rarity.COMMON, mage.cards.t.Tremor.class)); + cards.add(new SetCardInfo("Two-Headed Dragon", 221, Rarity.RARE, mage.cards.t.TwoHeadedDragon.class)); + cards.add(new SetCardInfo("Undertaker", 167, Rarity.COMMON, mage.cards.u.Undertaker.class)); + cards.add(new SetCardInfo("Unmask", 168, Rarity.RARE, mage.cards.u.Unmask.class)); + cards.add(new SetCardInfo("Unnatural Hunger", 169, Rarity.RARE, mage.cards.u.UnnaturalHunger.class)); + cards.add(new SetCardInfo("Uphill Battle", 222, Rarity.UNCOMMON, mage.cards.u.UphillBattle.class)); + cards.add(new SetCardInfo("Vendetta", 170, Rarity.COMMON, mage.cards.v.Vendetta.class)); + cards.add(new SetCardInfo("Venomous Breath", 281, Rarity.UNCOMMON, mage.cards.v.VenomousBreath.class)); + cards.add(new SetCardInfo("Venomous Dragonfly", 282, Rarity.COMMON, mage.cards.v.VenomousDragonfly.class)); + cards.add(new SetCardInfo("Vernal Equinox", 283, Rarity.RARE, mage.cards.v.VernalEquinox.class)); + cards.add(new SetCardInfo("Vine Dryad", 284, Rarity.RARE, mage.cards.v.VineDryad.class)); + cards.add(new SetCardInfo("Vine Trellis", 285, Rarity.COMMON, mage.cards.v.VineTrellis.class)); + cards.add(new SetCardInfo("Volcanic Wind", 223, Rarity.UNCOMMON, mage.cards.v.VolcanicWind.class)); + cards.add(new SetCardInfo("Wall of Distortion", 171, Rarity.COMMON, mage.cards.w.WallOfDistortion.class)); + cards.add(new SetCardInfo("War Cadence", 224, Rarity.UNCOMMON, mage.cards.w.WarCadence.class)); + cards.add(new SetCardInfo("War Tax", 113, Rarity.UNCOMMON, mage.cards.w.WarTax.class)); + cards.add(new SetCardInfo("Warmonger", 225, Rarity.UNCOMMON, mage.cards.w.Warmonger.class)); + cards.add(new SetCardInfo("Warpath", 226, Rarity.UNCOMMON, mage.cards.w.Warpath.class)); + cards.add(new SetCardInfo("Waterfront Bouncer", 114, Rarity.COMMON, mage.cards.w.WaterfrontBouncer.class)); + cards.add(new SetCardInfo("Wave of Reckoning", 56, Rarity.RARE, mage.cards.w.WaveOfReckoning.class)); + cards.add(new SetCardInfo("Wild Jhovall", 227, Rarity.COMMON, mage.cards.w.WildJhovall.class)); + cards.add(new SetCardInfo("Wishmonger", 57, Rarity.UNCOMMON, mage.cards.w.Wishmonger.class)); + cards.add(new SetCardInfo("Word of Blasting", 228, Rarity.UNCOMMON, mage.cards.w.WordOfBlasting.class)); + cards.add(new SetCardInfo("Worry Beads", 315, Rarity.RARE, mage.cards.w.WorryBeads.class)); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java index 8c00b671fd..a5fc11b7f3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java @@ -1,110 +1,110 @@ - -package org.mage.test.cards.abilities.keywords; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class CumulativeUpkeepTest extends CardTestPlayerBase { - - @Test - public void basicTest() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); - // Flying; fear - // Cumulative upkeep {B} - addCard(Zone.HAND, playerA, "Phobian Phantasm"); // Creature {1}{B}{B} 3/3 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm"); - - // Phobian Phantasm - CumulativeUpkeepAbility: Cumulative upkeep {B} - setChoice(playerA, true); // Pay {B}? - attack(3, playerA, "Phobian Phantasm"); - checkPermanentCounters("Age counters", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 1); - - setChoice(playerA, true); // Pay {B}{B}? - attack(5, playerA, "Phobian Phantasm"); - checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 2); - - setChoice(playerA, false); // Pay {B}{B}{B}? - - setStopAt(7, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Phobian Phantasm", 1); - - assertLife(playerA, 20); - assertLife(playerB, 14); - } - - - /** - I changed control of a Illusions of Grandeur to an AI after cumulative upkeep had triggered but before it resolved. - I chose not to pay the upkeep cost and then either the AI sacrificed it or I sacrificed it, neither of which should happen. - I can't sacrifice it because it's not under my control. The AI can't sacrifice it because they are not instructed to do so. - - Here is the reminder text for cumulative upkeep: - At the beginning of your upkeep, put an age counter on this permanent, then sacrifice it unless you pay its upkeep cost for each age counter on it. - */ - @Test - public void controlChangeTest() { - setStrictChooseMode(true); - - // Whenever Kor Celebrant or another creature enters the battlefield under your control, you gain 1 life. - addCard(Zone.HAND, playerB, "Kor Celebrant", 1); // Creature {2}{W} - addCard(Zone.BATTLEFIELD, playerB, "Plains", 3); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 6); - // Cumulative upkeep {2} - // When Illusions of Grandeur enters the battlefield, you gain 20 life. - // When Illusions of Grandeur leaves the battlefield, you lose 20 life. - addCard(Zone.HAND, playerA, "Illusions of Grandeur"); // Enchantment {3}{U} - - // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. - addCard(Zone.HAND, playerA, "Puca's Mischief"); // Enchantment {3}{U} - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Illusions of Grandeur"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Kor Celebrant"); - - // Illusions of Grandeur - CumulativeUpkeepAbility: Cumulative upkeep {2} - setChoice(playerA, true); // Pay {2}? - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Puca's Mischief"); - - setChoice(playerA, "Cumulative upkeep"); // Triggered list (total 2) which trigger goes first on the stack - addTarget(playerA, "Illusions of Grandeur"); // Own target permanent of Puca's Mischief - addTarget(playerA, "Kor Celebrant"); // Opponent's target permanent of Puca's Mischief - - setChoice(playerA, true); // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. - setChoice(playerA, false); // Pay {2}{2}? - - checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerB, "Illusions of Grandeur", CounterType.AGE, 2); - - setStopAt(5, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertLife(playerA, 40); - assertLife(playerB, 21); - - assertPermanentCount(playerA, "Kor Celebrant", 1); - assertPermanentCount(playerB, "Illusions of Grandeur", 1); - - - } - - + +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class CumulativeUpkeepTest extends CardTestPlayerBase { + + @Test + public void basicTest() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // Flying; fear + // Cumulative upkeep {B} + addCard(Zone.HAND, playerA, "Phobian Phantasm"); // Creature {1}{B}{B} 3/3 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm"); + + // Phobian Phantasm - CumulativeUpkeepAbility: Cumulative upkeep {B} + setChoice(playerA, true); // Pay {B}? + attack(3, playerA, "Phobian Phantasm"); + checkPermanentCounters("Age counters", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 1); + + setChoice(playerA, true); // Pay {B}{B}? + attack(5, playerA, "Phobian Phantasm"); + checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Phobian Phantasm", CounterType.AGE, 2); + + setChoice(playerA, false); // Pay {B}{B}{B}? + + setStopAt(7, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Phobian Phantasm", 1); + + assertLife(playerA, 20); + assertLife(playerB, 14); + } + + + /** + I changed control of a Illusions of Grandeur to an AI after cumulative upkeep had triggered but before it resolved. + I chose not to pay the upkeep cost and then either the AI sacrificed it or I sacrificed it, neither of which should happen. + I can't sacrifice it because it's not under my control. The AI can't sacrifice it because they are not instructed to do so. + + Here is the reminder text for cumulative upkeep: + At the beginning of your upkeep, put an age counter on this permanent, then sacrifice it unless you pay its upkeep cost for each age counter on it. + */ + @Test + public void controlChangeTest() { + setStrictChooseMode(true); + + // Whenever Kor Celebrant or another creature enters the battlefield under your control, you gain 1 life. + addCard(Zone.HAND, playerB, "Kor Celebrant", 1); // Creature {2}{W} + addCard(Zone.BATTLEFIELD, playerB, "Plains", 3); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + // Cumulative upkeep {2} + // When Illusions of Grandeur enters the battlefield, you gain 20 life. + // When Illusions of Grandeur leaves the battlefield, you lose 20 life. + addCard(Zone.HAND, playerA, "Illusions of Grandeur"); // Enchantment {3}{U} + + // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. + addCard(Zone.HAND, playerA, "Puca's Mischief"); // Enchantment {3}{U} + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Illusions of Grandeur"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Kor Celebrant"); + + // Illusions of Grandeur - CumulativeUpkeepAbility: Cumulative upkeep {2} + setChoice(playerA, true); // Pay {2}? + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Puca's Mischief"); + + setChoice(playerA, "Cumulative upkeep"); // Triggered list (total 2) which trigger goes first on the stack + addTarget(playerA, "Illusions of Grandeur"); // Own target permanent of Puca's Mischief + addTarget(playerA, "Kor Celebrant"); // Opponent's target permanent of Puca's Mischief + + setChoice(playerA, true); // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. + setChoice(playerA, false); // Pay {2}{2}? + + checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerB, "Illusions of Grandeur", CounterType.AGE, 2); + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertLife(playerA, 40); + assertLife(playerB, 21); + + assertPermanentCount(playerA, "Kor Celebrant", 1); + assertPermanentCount(playerB, "Illusions of Grandeur", 1); + + + } + + } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java index 787d9d59f0..1946604d6f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java @@ -1,179 +1,179 @@ -package org.mage.test.cards.abilities.keywords; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2, JayDi85 - */ - -public class EntwineTest extends CardTestPlayerBase { - - @Test - public void test_CastWithoutEntwine() { - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, false); // not use Entwine - setModeChoice(playerA, "1"); // target creature - addTarget(playerA, "Balduvian Bears"); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 3); - } - - @Test - public void test_CastEntwine_Normal() { - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2); - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 3 + 2); - } - - @Test - public void test_CastEntwine_CostReduction() { - addCustomEffect_SpellCostModification(playerA, -4); - - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // -4 as cost reduction - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 1); - } - - @Test - public void test_CastEntwine_CostIncreasing() { - addCustomEffect_SpellCostModification(playerA, 5); - - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2 + 5); - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Mountain", true, 3 + 2 + 5); - } - - @Test - public void test_CastEntwine_FreeFromHand() { - // You may cast nonland cards from your hand without paying their mana costs. - addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); - - // Choose one — - //• Barbed Lightning deals 3 damage to target creature. - //• Barbed Lightning deals 3 damage to target player or planeswalker. - // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); // only Entwine pay need - // - addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // cast for free - setChoice(playerA, true); // use Entwine - addTarget(playerA, "Balduvian Bears"); - addTarget(playerA, playerA); - - setStrictChooseMode(true); - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - assertPermanentCount(playerA, "Balduvian Bears", 0); - assertTappedCount("Plains", true, 2); - } - - @Test - public void test_ToothAndNail() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); - addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 9); - // Choose one - - // Search your library for up to two creature cards, reveal them, put them into your hand, then shuffle your library; - // or put up to two creature cards from your hand onto the battlefield. - // Entwine {2} - addCard(Zone.HAND, playerA, "Tooth and Nail"); // Sorcery {5}{G}{G} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tooth and Nail"); - setChoice(playerA, true); // Message: Pay Entwine {2} ? - addTarget(playerA, "Silvercoat Lion^Pillarfield Ox"); - setChoice(playerA, "Silvercoat Lion^Pillarfield Ox"); - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Silvercoat Lion", 1); - assertPermanentCount(playerA, "Pillarfield Ox", 1); - } -} +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2, JayDi85 + */ + +public class EntwineTest extends CardTestPlayerBase { + + @Test + public void test_CastWithoutEntwine() { + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, false); // not use Entwine + setModeChoice(playerA, "1"); // target creature + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3); + } + + @Test + public void test_CastEntwine_Normal() { + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3 + 2); + } + + @Test + public void test_CastEntwine_CostReduction() { + addCustomEffect_SpellCostModification(playerA, -4); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // -4 as cost reduction + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 1); + } + + @Test + public void test_CastEntwine_CostIncreasing() { + addCustomEffect_SpellCostModification(playerA, 5); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2 + 5); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3 + 2 + 5); + } + + @Test + public void test_CastEntwine_FreeFromHand() { + // You may cast nonland cards from your hand without paying their mana costs. + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); // only Entwine pay need + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, true); // cast for free + setChoice(playerA, true); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Plains", true, 2); + } + + @Test + public void test_ToothAndNail() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); + addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 9); + // Choose one - + // Search your library for up to two creature cards, reveal them, put them into your hand, then shuffle your library; + // or put up to two creature cards from your hand onto the battlefield. + // Entwine {2} + addCard(Zone.HAND, playerA, "Tooth and Nail"); // Sorcery {5}{G}{G} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tooth and Nail"); + setChoice(playerA, true); // Message: Pay Entwine {2} ? + addTarget(playerA, "Silvercoat Lion^Pillarfield Ox"); + setChoice(playerA, "Silvercoat Lion^Pillarfield Ox"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Silvercoat Lion", 1); + assertPermanentCount(playerA, "Pillarfield Ox", 1); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java index a3c7ccfce2..125476f40e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java @@ -1,540 +1,540 @@ -package org.mage.test.cards.abilities.keywords; - -import mage.cards.Card; -import mage.constants.EmptyNames; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.game.permanent.Permanent; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2 - */ -public class ManifestTest extends CardTestPlayerBase { - - /** - * Tests that ETB triggered abilities did not trigger for manifested cards - */ - @Test - public void testETBTriggeredAbilities() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Manifest the top card of your library {1}{W} - addCard(Zone.HAND, playerA, "Soul Summons"); - - // Tranquil Cove enters the battlefield tapped. - // When Tranquil Cove enters the battlefield, you gain 1 life. - // {T}: Add {W} or {U}. - addCard(Zone.LIBRARY, playerA, "Tranquil Cove"); - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - // not tapped - assertTapped(EmptyNames.FACE_DOWN_CREATURE.toString(), false); - } - - /** - * If Doomwake Giant gets manifested, it's Constellation trigger may not - * trigger - */ - @Test - public void testETBTriggeredAbilities2() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Manifest the top card of your library {1}{W} - addCard(Zone.HAND, playerA, "Soul Summons"); - - // Constellation - When Doomwake Giant or another enchantment enters the battlefield - // under your control, creatures your opponents control get -1/-1 until end of turn. - addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - // PlayerB's Silvercoat Lion should not have get -1/-1/ - assertPermanentCount(playerB, "Silvercoat Lion", 1); - assertPowerToughness(playerB, "Silvercoat Lion", 2, 2); - } - - /** - * If Doomwake Giant gets manifested, it's Constellation trigger may not - * trigger - */ - @Test - public void testETBTriggeredAbilities3() { - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - - // Constellation - When Doomwake Giant or another enchantment enters the battlefield - // under your control, creatures your opponents control get -1/-1 until end of turn. - addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); - - addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox"); - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - // PlayerA's Pillarfield Ox should not have get -1/-1/ - assertPermanentCount(playerB, "Pillarfield Ox", 1); - assertPowerToughness(playerB, "Pillarfield Ox", 2, 4); - } - - /** - * If Doomwake Giant gets manifested, it's Constellation trigger may not - * trigger - */ - @Test - public void testNylea() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - - // As long as your devotion to white is less than five, Nylea isn't a creature. - // (Each {G} in the mana costs of permanents you control counts towards your devotion to green.) - addCard(Zone.LIBRARY, playerA, "Nylea, God of the Hunt"); - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - - } - - /* - Had a Foundry Street Denizen and another creature out. - Opponent Reality Shift'ed the other creature, manifested card was a red creature. This pumped the foundry street denizen even though it shouldn't. - */ - @Test - public void testColorOfManifestedCardDoesNotCount() { - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - - // Gore Swine {2}{R} - // 4/1 - addCard(Zone.LIBRARY, playerA, "Gore Swine"); - - // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. - addCard(Zone.BATTLEFIELD, playerA, "Foundry Street Denizen"); - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - // a facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); - assertPowerToughness(playerA, "Foundry Street Denizen", 1, 1); - - } - - /* - I casted a Silence the Believers on a manifested card. It moved to the exile zone face-down. - */ - @Test - public void testCardGetsExiledFaceUp() { - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); - // Exile target creature. Its controller manifests the top card of their library {1}{U} - addCard(Zone.HAND, playerB, "Reality Shift"); - // Silence the Believers - Instant {2}{B}{B} - // Strive — Silence the Believers costs more to cast for each target beyond the first. - // Exile any number of target creatures and all Auras attached to them. - addCard(Zone.HAND, playerB, "Silence the Believers"); - // Gore Swine {2}{R} - // 4/1 - addCard(Zone.LIBRARY, playerA, "Gore Swine"); - - // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); - // showBattlefield("A battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerA); - // showBattlefield("B battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerB); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Silence the Believers", EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - assertGraveyardCount(playerB, "Reality Shift", 1); - assertExileCount("Silvercoat Lion", 1); - assertExileCount("Gore Swine", 1); - // no facedown creature is on the battlefield - assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); - - for (Card card : currentGame.getExile().getAllCards(currentGame)) { - if (card.getName().equals("Gore Swine")) { - Assert.assertTrue("Gore Swine may not be face down in exile", !card.isFaceDown(currentGame)); - } - } - - } - - // Qarsi High Priest went to manifest Illusory Gains, - // but it made me choose a target for gains, then enchanted the card to that creature. - @Test - public void testManifestAura() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - addCard(Zone.LIBRARY, playerB, "Illusory Gains", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Illusory Gains", 0); - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - // a facedown creature is on the battlefield - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - - } - - // Check if a Megamorph card is manifested and turned face up by their megamorph ability - // it gets the +1/+1 counter. - // 701.33c - // If a card with morph is manifested, its controller may turn that card face up using - // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up - // or the procedure described above to turn a manifested permanent face up. - @Test - public void testManifestMegamorph_TurnUpByMegamorphCost() { - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - addCard(Zone.BATTLEFIELD, playerB, "Forest", 6); - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // Reach (This creature can block creatures with flying.) - // Megamorph {5}{G} - addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{5}{G}: Turn"); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); - assertPermanentCount(playerB, "Aerie Bowmasters", 1); - assertPowerToughness(playerB, "Aerie Bowmasters", 4, 5); // 3/4 and the +1/+1 counter from Megamorph - Permanent aerie = getPermanent("Aerie Bowmasters", playerB); - Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); - } - - @Test - public void testManifestMegamorph_TurnUpBySimpleCost() { - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - addCard(Zone.BATTLEFIELD, playerB, "Forest", 4); - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // {2}{G}{G} - // Reach (This creature can block creatures with flying.) - // Megamorph {5}{G} - addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{2}{G}{G}: Turn"); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); - assertPermanentCount(playerB, "Aerie Bowmasters", 1); - assertPowerToughness(playerB, "Aerie Bowmasters", 3, 4); // 3/4 without counter (megamorph not used) - Permanent aerie = getPermanent("Aerie Bowmasters", playerB); - Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); - } - - /** - * When a Forest came manifested into play my Courser of Kruphix gained me a - * life. - */ - @Test - public void testManifestForest() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - // Play with the top card of your library revealed. - // You may play the top card of your library if it's a land card. - // Whenever a land enters the battlefield under your control, you gain 1 life. - addCard(Zone.BATTLEFIELD, playerB, "Courser of Kruphix", 1); - - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - addCard(Zone.LIBRARY, playerB, "Forest", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - - } - - /** - * Whisperwood Elemental - Its sacrifice ability doesn't work.. - */ - @Test - public void testWhisperwoodElemental() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - // Seismic Rupture deals 2 damage to each creature without flying. - addCard(Zone.HAND, playerA, "Seismic Rupture", 1); - - // At the beginning of your end step, manifest the top card of your library. - // Sacrifice Whisperwood Elemental: Until end of turn, face-up, nontoken creatures you control gain "When this creature dies, manifest the top card of your library." - addCard(Zone.BATTLEFIELD, playerB, "Whisperwood Elemental", 1); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Sacrifice"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Seismic Rupture"); - setChoice(playerB, "When {this} dies"); // Order of triggers - - setStopAt(1, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertGraveyardCount(playerA, "Seismic Rupture", 1); - assertGraveyardCount(playerB, "Whisperwood Elemental", 1); - assertGraveyardCount(playerB, "Silvercoat Lion", 2); - - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 2); - - } - - /** - * I sacrificed a manifested face-down Smothering Abomination to Nantuko - * Husk and it made me draw a card. - */ - @Test - public void testDiesTriggeredAbilitiesOfManifestedCreatures() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); - - // Sacrifice a creature: Nantuko Husk gets +2/+2 until end of turn. - addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk", 1); - - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // Devoid - // Flying - // At the beginning of your upkeep, sacrifice a creature - // Whenever you sacrifice a creature, draw a card. - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - addCard(Zone.LIBRARY, playerB, "Smothering Abomination", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Sacrifice a creature"); - setChoice(playerB, EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertPermanentCount(playerB, "Qarsi High Priest", 1); - assertPermanentCount(playerB, "Nantuko Husk", 1); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - assertGraveyardCount(playerB, "Smothering Abomination", 1); - - assertPowerToughness(playerB, "Nantuko Husk", 4, 4); - - assertHandCount(playerB, "Mountain", 1); - - } - - @Test - public void test_ManifestSorceryAndBlinkIt() { - - addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); - addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); - - // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. - addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - // Exile target creature you control, then return that card to the battlefield under your control. - addCard(Zone.HAND, playerB, "Cloudshift", 1); //Instant {W} - - - // Devoid - // Flying - // At the beginning of your upkeep, sacrifice a creature - // Whenever you sacrifice a creature, draw a card. - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - addCard(Zone.LIBRARY, playerB, "Lightning Bolt", 1); - addCard(Zone.LIBRARY, playerB, "Mountain", 1); - - skipInitShuffling(); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); - setChoice(playerB, "Silvercoat Lion"); - - waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN, playerB); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cloudshift", EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStrictChooseMode(true); - setStopAt(2, PhaseStep.END_TURN); - execute(); - assertAllCommandsUsed(); - - // no life gain - assertLife(playerA, 20); - assertLife(playerB, 20); - - assertPermanentCount(playerB, "Qarsi High Priest", 1); - - assertGraveyardCount(playerB, "Silvercoat Lion", 1); - assertGraveyardCount(playerB, "Cloudshift", 1); - - assertPermanentCount(playerB, "Lightning Bolt", 0); - assertExileCount(playerB, "Lightning Bolt", 1); - - assertHandCount(playerB, "Mountain", 1); - - } -} +package org.mage.test.cards.abilities.keywords; + +import mage.cards.Card; +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2 + */ +public class ManifestTest extends CardTestPlayerBase { + + /** + * Tests that ETB triggered abilities did not trigger for manifested cards + */ + @Test + public void testETBTriggeredAbilities() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Manifest the top card of your library {1}{W} + addCard(Zone.HAND, playerA, "Soul Summons"); + + // Tranquil Cove enters the battlefield tapped. + // When Tranquil Cove enters the battlefield, you gain 1 life. + // {T}: Add {W} or {U}. + addCard(Zone.LIBRARY, playerA, "Tranquil Cove"); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + // not tapped + assertTapped(EmptyNames.FACE_DOWN_CREATURE.toString(), false); + } + + /** + * If Doomwake Giant gets manifested, it's Constellation trigger may not + * trigger + */ + @Test + public void testETBTriggeredAbilities2() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Manifest the top card of your library {1}{W} + addCard(Zone.HAND, playerA, "Soul Summons"); + + // Constellation - When Doomwake Giant or another enchantment enters the battlefield + // under your control, creatures your opponents control get -1/-1 until end of turn. + addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion"); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + // PlayerB's Silvercoat Lion should not have get -1/-1/ + assertPermanentCount(playerB, "Silvercoat Lion", 1); + assertPowerToughness(playerB, "Silvercoat Lion", 2, 2); + } + + /** + * If Doomwake Giant gets manifested, it's Constellation trigger may not + * trigger + */ + @Test + public void testETBTriggeredAbilities3() { + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + + // Constellation - When Doomwake Giant or another enchantment enters the battlefield + // under your control, creatures your opponents control get -1/-1 until end of turn. + addCard(Zone.LIBRARY, playerA, "Doomwake Giant"); + + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox"); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + // PlayerA's Pillarfield Ox should not have get -1/-1/ + assertPermanentCount(playerB, "Pillarfield Ox", 1); + assertPowerToughness(playerB, "Pillarfield Ox", 2, 4); + } + + /** + * If Doomwake Giant gets manifested, it's Constellation trigger may not + * trigger + */ + @Test + public void testNylea() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + + // As long as your devotion to white is less than five, Nylea isn't a creature. + // (Each {G} in the mana costs of permanents you control counts towards your devotion to green.) + addCard(Zone.LIBRARY, playerA, "Nylea, God of the Hunt"); + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + + } + + /* + Had a Foundry Street Denizen and another creature out. + Opponent Reality Shift'ed the other creature, manifested card was a red creature. This pumped the foundry street denizen even though it shouldn't. + */ + @Test + public void testColorOfManifestedCardDoesNotCount() { + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + + // Gore Swine {2}{R} + // 4/1 + addCard(Zone.LIBRARY, playerA, "Gore Swine"); + + // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Foundry Street Denizen"); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + // a facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + assertPowerToughness(playerA, "Foundry Street Denizen", 1, 1); + + } + + /* + I casted a Silence the Believers on a manifested card. It moved to the exile zone face-down. + */ + @Test + public void testCardGetsExiledFaceUp() { + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); + // Exile target creature. Its controller manifests the top card of their library {1}{U} + addCard(Zone.HAND, playerB, "Reality Shift"); + // Silence the Believers - Instant {2}{B}{B} + // Strive — Silence the Believers costs more to cast for each target beyond the first. + // Exile any number of target creatures and all Auras attached to them. + addCard(Zone.HAND, playerB, "Silence the Believers"); + // Gore Swine {2}{R} + // 4/1 + addCard(Zone.LIBRARY, playerA, "Gore Swine"); + + // Whenever another red creature enters the battlefield under your control, Foundry Street Denizen gets +1/+0 until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Reality Shift", "Silvercoat Lion"); + // showBattlefield("A battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerA); + // showBattlefield("B battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerB); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Silence the Believers", EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Reality Shift", 1); + assertExileCount("Silvercoat Lion", 1); + assertExileCount("Gore Swine", 1); + // no facedown creature is on the battlefield + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + for (Card card : currentGame.getExile().getAllCards(currentGame)) { + if (card.getName().equals("Gore Swine")) { + Assert.assertTrue("Gore Swine may not be face down in exile", !card.isFaceDown(currentGame)); + } + } + + } + + // Qarsi High Priest went to manifest Illusory Gains, + // but it made me choose a target for gains, then enchanted the card to that creature. + @Test + public void testManifestAura() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + addCard(Zone.LIBRARY, playerB, "Illusory Gains", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Illusory Gains", 0); + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + // a facedown creature is on the battlefield + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + + } + + // Check if a Megamorph card is manifested and turned face up by their megamorph ability + // it gets the +1/+1 counter. + // 701.33c + // If a card with morph is manifested, its controller may turn that card face up using + // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up + // or the procedure described above to turn a manifested permanent face up. + @Test + public void testManifestMegamorph_TurnUpByMegamorphCost() { + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 6); + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // Reach (This creature can block creatures with flying.) + // Megamorph {5}{G} + addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{5}{G}: Turn"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + assertPermanentCount(playerB, "Aerie Bowmasters", 1); + assertPowerToughness(playerB, "Aerie Bowmasters", 4, 5); // 3/4 and the +1/+1 counter from Megamorph + Permanent aerie = getPermanent("Aerie Bowmasters", playerB); + Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); + } + + @Test + public void testManifestMegamorph_TurnUpBySimpleCost() { + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 4); + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // {2}{G}{G} + // Reach (This creature can block creatures with flying.) + // Megamorph {5}{G} + addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{2}{G}{G}: Turn"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + assertPermanentCount(playerB, "Aerie Bowmasters", 1); + assertPowerToughness(playerB, "Aerie Bowmasters", 3, 4); // 3/4 without counter (megamorph not used) + Permanent aerie = getPermanent("Aerie Bowmasters", playerB); + Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); + } + + /** + * When a Forest came manifested into play my Courser of Kruphix gained me a + * life. + */ + @Test + public void testManifestForest() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + // Play with the top card of your library revealed. + // You may play the top card of your library if it's a land card. + // Whenever a land enters the battlefield under your control, you gain 1 life. + addCard(Zone.BATTLEFIELD, playerB, "Courser of Kruphix", 1); + + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + addCard(Zone.LIBRARY, playerB, "Forest", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + + } + + /** + * Whisperwood Elemental - Its sacrifice ability doesn't work.. + */ + @Test + public void testWhisperwoodElemental() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // Seismic Rupture deals 2 damage to each creature without flying. + addCard(Zone.HAND, playerA, "Seismic Rupture", 1); + + // At the beginning of your end step, manifest the top card of your library. + // Sacrifice Whisperwood Elemental: Until end of turn, face-up, nontoken creatures you control gain "When this creature dies, manifest the top card of your library." + addCard(Zone.BATTLEFIELD, playerB, "Whisperwood Elemental", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Sacrifice"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Seismic Rupture"); + setChoice(playerB, "When {this} dies"); // Order of triggers + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerA, "Seismic Rupture", 1); + assertGraveyardCount(playerB, "Whisperwood Elemental", 1); + assertGraveyardCount(playerB, "Silvercoat Lion", 2); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 2); + + } + + /** + * I sacrificed a manifested face-down Smothering Abomination to Nantuko + * Husk and it made me draw a card. + */ + @Test + public void testDiesTriggeredAbilitiesOfManifestedCreatures() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + + // Sacrifice a creature: Nantuko Husk gets +2/+2 until end of turn. + addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk", 1); + + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // Devoid + // Flying + // At the beginning of your upkeep, sacrifice a creature + // Whenever you sacrifice a creature, draw a card. + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + addCard(Zone.LIBRARY, playerB, "Smothering Abomination", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Sacrifice a creature"); + setChoice(playerB, EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertPermanentCount(playerB, "Qarsi High Priest", 1); + assertPermanentCount(playerB, "Nantuko Husk", 1); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + assertGraveyardCount(playerB, "Smothering Abomination", 1); + + assertPowerToughness(playerB, "Nantuko Husk", 4, 4); + + assertHandCount(playerB, "Mountain", 1); + + } + + @Test + public void test_ManifestSorceryAndBlinkIt() { + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); + + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // Exile target creature you control, then return that card to the battlefield under your control. + addCard(Zone.HAND, playerB, "Cloudshift", 1); //Instant {W} + + + // Devoid + // Flying + // At the beginning of your upkeep, sacrifice a creature + // Whenever you sacrifice a creature, draw a card. + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + addCard(Zone.LIBRARY, playerB, "Lightning Bolt", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN, playerB); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cloudshift", EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertPermanentCount(playerB, "Qarsi High Priest", 1); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + assertGraveyardCount(playerB, "Cloudshift", 1); + + assertPermanentCount(playerB, "Lightning Bolt", 0); + assertExileCount(playerB, "Lightning Bolt", 1); + + assertHandCount(playerB, "Mountain", 1); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java index 2370ee55c1..b74dfe0b12 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/FerventChampionTest.java @@ -1,39 +1,39 @@ -package org.mage.test.cards.cost.modification; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class FerventChampionTest extends CardTestPlayerBase { - - @Test - public void testFerventChampion() { - setStrictChooseMode(true); - // First strike, Haste - // Whenever Fervent Champion attacks, another target attacking Knight you control gets +1/+0 until end of turn. - // Equip abilities you activate that target Fervent Champion cost {3} less to activate. - addCard(Zone.BATTLEFIELD, playerA, "Fervent Champion"); - - // Equipped creature gets +2/+2 and has protection from red and from blue. - // Whenever equipped creature deals combat damage to a player, Sword of Fire - // and Ice deals 2 damage to any target and you draw a card. - // Equip {2} - addCard(Zone.BATTLEFIELD, playerA, "Sword of Fire and Ice", 1); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip"); - addTarget(playerA, "Fervent Champion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, "Fervent Champion", 3,3); - } +package org.mage.test.cards.cost.modification; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class FerventChampionTest extends CardTestPlayerBase { + + @Test + public void testFerventChampion() { + setStrictChooseMode(true); + // First strike, Haste + // Whenever Fervent Champion attacks, another target attacking Knight you control gets +1/+0 until end of turn. + // Equip abilities you activate that target Fervent Champion cost {3} less to activate. + addCard(Zone.BATTLEFIELD, playerA, "Fervent Champion"); + + // Equipped creature gets +2/+2 and has protection from red and from blue. + // Whenever equipped creature deals combat damage to a player, Sword of Fire + // and Ice deals 2 damage to any target and you draw a card. + // Equip {2} + addCard(Zone.BATTLEFIELD, playerA, "Sword of Fire and Ice", 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip"); + addTarget(playerA, "Fervent Champion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Fervent Champion", 3,3); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java index bf7a287c1d..0167be3c33 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/PrimordialMistTest.java @@ -1,56 +1,56 @@ -package org.mage.test.cards.facedown; - -import mage.constants.EmptyNames; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PrimordialMistTest extends CardTestPlayerBase { - - /** - * I have Brine Elemental played face down as a morph, an artifact which has - * been manifested and Kadena which has been turned face by Ixidron. I can't - * seem to activate Primordial Mist's second ability for any of these kinds - * of face down creatures: - */ - @Test - public void test_ExileAndCastMorphFaceDownCard() { - setStrictChooseMode(true); - - // At the beginning of your end step, you may manifest the top card of your library. - // Exile a face-down permanent you control face-up: You may play that card this turn - addCard(Zone.BATTLEFIELD, playerA, "Primordial Mist"); - // Morph {5}{U}{U} - // When Brine Elemental is turned face up, each opponent skips their next untap step. - addCard(Zone.HAND, playerA, "Brine Elemental"); // Creature {5}{U}{U} (5/4) - addCard(Zone.BATTLEFIELD, playerA, "Island", 9); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brine Elemental"); - setChoice(playerA, true); // cast it face down as 2/2 creature - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exile a face-down permanent you control"); - setChoice(playerA, EmptyNames.FACE_DOWN_CREATURE.toString()); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Brine Elemental"); - setChoice(playerA, false); // cast it face down as 2/2 creature - - setChoice(playerA, true); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertExileCount(playerA, 0); - - assertPowerToughness(playerA, "Brine Elemental", 5, 4); - - } -} +package org.mage.test.cards.facedown; + +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PrimordialMistTest extends CardTestPlayerBase { + + /** + * I have Brine Elemental played face down as a morph, an artifact which has + * been manifested and Kadena which has been turned face by Ixidron. I can't + * seem to activate Primordial Mist's second ability for any of these kinds + * of face down creatures: + */ + @Test + public void test_ExileAndCastMorphFaceDownCard() { + setStrictChooseMode(true); + + // At the beginning of your end step, you may manifest the top card of your library. + // Exile a face-down permanent you control face-up: You may play that card this turn + addCard(Zone.BATTLEFIELD, playerA, "Primordial Mist"); + // Morph {5}{U}{U} + // When Brine Elemental is turned face up, each opponent skips their next untap step. + addCard(Zone.HAND, playerA, "Brine Elemental"); // Creature {5}{U}{U} (5/4) + addCard(Zone.BATTLEFIELD, playerA, "Island", 9); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brine Elemental"); + setChoice(playerA, true); // cast it face down as 2/2 creature + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exile a face-down permanent you control"); + setChoice(playerA, EmptyNames.FACE_DOWN_CREATURE.toString()); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Brine Elemental"); + setChoice(playerA, false); // cast it face down as 2/2 creature + + setChoice(playerA, true); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertExileCount(playerA, 0); + + assertPowerToughness(playerA, "Brine Elemental", 5, 4); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java index 54035a4cfe..2a673dd635 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/facedown/TriggerTest.java @@ -1,62 +1,62 @@ - -package org.mage.test.cards.facedown; - -import mage.cards.Card; -import mage.constants.EmptyNames; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - - - -public class TriggerTest extends CardTestPlayerBase { - - /** - * Midnight Reaper triggers when dies face down #7063 - * Ixidron has turned Midnight Reaper and Balduvian Bears face down: - * - */ - - // test that cards imprinted using Summoner's Egg are face down - @Test - public void testReaperDoesNotTriggerDiesTriggerFaceDown() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 5); - // As Ixidron enters the battlefield, turn all other nontoken creatures face down. - // Ixidron's power and toughness are each equal to the number of face-down creatures on the battlefield. - addCard(Zone.HAND, playerA, "Ixidron"); // Creature {3}{U}{U} (*/*) - // Whenever a nontoken creature you control dies, Midnight Reaper deals 1 damage to you and you draw a card. - addCard(Zone.BATTLEFIELD, playerA, "Midnight Reaper"); // Creature {2}{B} - - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); - addCard(Zone.HAND, playerB, "Lightning Bolt"); // Instant 3 damage - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ixidron"); - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", EmptyNames.FACE_DOWN_CREATURE.toString()); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Lightning Bolt", 1); - - assertGraveyardCount(playerA, "Midnight Reaper", 1); - assertGraveyardCount(playerA, "Ixidron", 1); - - assertHandCount(playerA, 0); - assertLife(playerA, 20); - - } + +package org.mage.test.cards.facedown; + +import mage.cards.Card; +import mage.constants.EmptyNames; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + + + +public class TriggerTest extends CardTestPlayerBase { + + /** + * Midnight Reaper triggers when dies face down #7063 + * Ixidron has turned Midnight Reaper and Balduvian Bears face down: + * + */ + + // test that cards imprinted using Summoner's Egg are face down + @Test + public void testReaperDoesNotTriggerDiesTriggerFaceDown() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + // As Ixidron enters the battlefield, turn all other nontoken creatures face down. + // Ixidron's power and toughness are each equal to the number of face-down creatures on the battlefield. + addCard(Zone.HAND, playerA, "Ixidron"); // Creature {3}{U}{U} (*/*) + // Whenever a nontoken creature you control dies, Midnight Reaper deals 1 damage to you and you draw a card. + addCard(Zone.BATTLEFIELD, playerA, "Midnight Reaper"); // Creature {2}{B} + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.HAND, playerB, "Lightning Bolt"); // Instant 3 damage + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ixidron"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", EmptyNames.FACE_DOWN_CREATURE.toString()); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Lightning Bolt", 1); + + assertGraveyardCount(playerA, "Midnight Reaper", 1); + assertGraveyardCount(playerA, "Ixidron", 1); + + assertHandCount(playerA, 0); + assertLife(playerA, 20); + + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java index f6fe75c5fe..83a9f0f0df 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/MultipleTimesUsableActivatedManaAbilitiesTest.java @@ -1,45 +1,45 @@ -package org.mage.test.cards.mana; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class MultipleTimesUsableActivatedManaAbilitiesTest extends CardTestPlayerBase { - - /** - * Seton, Krosan Protector - only seems to get counted as if it were one - * mana for determining if a spell can be cast, regardless of how many - * druids you have in playF - */ - @Test - public void testCanBeCastWithSetonKrosanProtector() { - // Tap an untapped Druid you control: Add {G}. - addCard(Zone.BATTLEFIELD, playerA, "Seton, Krosan Protector", 1); // Creature {G}{G}{G} - addCard(Zone.BATTLEFIELD, playerA, "Citanul Druid", 3); - - addCard(Zone.HAND, playerA, "Leatherback Baloth", 1); // Creature 4/5 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Leatherback Baloth"); - - setChoice(playerA, "Citanul Druid"); - setChoice(playerA, "Citanul Druid"); - setChoice(playerA, "Citanul Druid"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - - - setStrictChooseMode(true); - execute(); - - assertAllCommandsUsed(); - - assertTappedCount("Citanul Druid", true, 3); - assertPermanentCount(playerA, "Leatherback Baloth", 1); - } - -} +package org.mage.test.cards.mana; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class MultipleTimesUsableActivatedManaAbilitiesTest extends CardTestPlayerBase { + + /** + * Seton, Krosan Protector - only seems to get counted as if it were one + * mana for determining if a spell can be cast, regardless of how many + * druids you have in playF + */ + @Test + public void testCanBeCastWithSetonKrosanProtector() { + // Tap an untapped Druid you control: Add {G}. + addCard(Zone.BATTLEFIELD, playerA, "Seton, Krosan Protector", 1); // Creature {G}{G}{G} + addCard(Zone.BATTLEFIELD, playerA, "Citanul Druid", 3); + + addCard(Zone.HAND, playerA, "Leatherback Baloth", 1); // Creature 4/5 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Leatherback Baloth"); + + setChoice(playerA, "Citanul Druid"); + setChoice(playerA, "Citanul Druid"); + setChoice(playerA, "Citanul Druid"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertTappedCount("Citanul Druid", true, 3); + assertPermanentCount(playerA, "Leatherback Baloth", 1); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java index 20c0c9f449..0cdf671f50 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java @@ -1,112 +1,112 @@ -package org.mage.test.cards.mana.conditional; - -import mage.abilities.mana.ManaOptions; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; -import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; - -/** - * - * @author LevelX2 - */ - -public class CrypticTrilobiteTest extends CardTestPlayerBase { - - @Test - public void testAvailableManaCalculation(){ - setStrictChooseMode(true); - - // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. - // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. - // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. - addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); - setChoice(playerA, "X=5"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cryptic Trilobite", 1); - - ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); - Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); - } - - @Test - public void testUse(){ - setStrictChooseMode(true); - - // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. - // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. - // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. - addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - // Flying - // {2}: Deathknell Kami gets +1/+1 until end of turn. Sacrifice it at the beginning of the next end step. - // Soulshift 1 (When this creature dies, you may return target Spirit card with converted mana cost 1 or less from your graveyard to your hand.) - addCard(Zone.BATTLEFIELD, playerA, "Deathknell Kami"); // Creature (0/1) - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); - setChoice(playerA, "X=5"); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cryptic Trilobite", 1); - assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 3); - - assertPowerToughness(playerA, "Deathknell Kami", 2, 3); - - ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); - Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); - } - - @Test - public void testCantUse(){ - setStrictChooseMode(true); - - // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. - // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. - // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. - addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - - // {4}{W}: Return another target creature you control to its owner's hand. - addCard(Zone.HAND, playerA, "Aegis Automaton"); // Creature {2} (0/2) - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); - setChoice(playerA, "X=5"); - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); - - checkPlayableAbility("can't play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Aegis Automaton", false); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cryptic Trilobite", 1); - assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5); - } - - +package org.mage.test.cards.mana.conditional; + +import mage.abilities.mana.ManaOptions; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; +import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; + +/** + * + * @author LevelX2 + */ + +public class CrypticTrilobiteTest extends CardTestPlayerBase { + + @Test + public void testAvailableManaCalculation(){ + setStrictChooseMode(true); + + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. + // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. + // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. + addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); + setChoice(playerA, "X=5"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cryptic Trilobite", 1); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + assertManaOptions("{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); + } + + @Test + public void testUse(){ + setStrictChooseMode(true); + + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. + // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. + // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. + addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} + // Flying + // {2}: Deathknell Kami gets +1/+1 until end of turn. Sacrifice it at the beginning of the next end step. + // Soulshift 1 (When this creature dies, you may return target Spirit card with converted mana cost 1 or less from your graveyard to your hand.) + addCard(Zone.BATTLEFIELD, playerA, "Deathknell Kami"); // Creature (0/1) + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); + setChoice(playerA, "X=5"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cryptic Trilobite", 1); + assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 3); + + assertPowerToughness(playerA, "Deathknell Kami", 2, 3); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + assertManaOptions("{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); + } + + @Test + public void testCantUse(){ + setStrictChooseMode(true); + + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. + // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. + // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. + addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} + + // {4}{W}: Return another target creature you control to its owner's hand. + addCard(Zone.HAND, playerA, "Aegis Automaton"); // Creature {2} (0/2) + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); + setChoice(playerA, "X=5"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + checkPlayableAbility("can't play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Aegis Automaton", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cryptic Trilobite", 1); + assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5); + } + + } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java index 89cfbed009..a61f627ded 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/TitansNestTest.java @@ -1,44 +1,44 @@ - -package org.mage.test.cards.mana.conditional; - -import mage.abilities.mana.ManaOptions; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; -import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; - -/** - * - * @author LevelX2 - */ -public class TitansNestTest extends CardTestPlayerBase { - - @Test - public void testTitansNest(){ - setStrictChooseMode(true); - - // At the beginning of your upkeep, look at the top card of your library. You may put that card into your graveyard. - // Exile a card from your graveyard: Add {C}. Spend this mana only to cast a colored spell without {X} in its mana cost. - addCard(Zone.HAND, playerA, "Titans' Nest"); // Enchantment {1}{B}{G}{U} - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - addCard(Zone.BATTLEFIELD, playerA, "Island", 1); - - addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Titans' Nest"); - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Titans' Nest", 1); - - ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); - Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}[{TitansNestManaCondition}]", manaOptions); - } + +package org.mage.test.cards.mana.conditional; + +import mage.abilities.mana.ManaOptions; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; +import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions; + +/** + * + * @author LevelX2 + */ +public class TitansNestTest extends CardTestPlayerBase { + + @Test + public void testTitansNest(){ + setStrictChooseMode(true); + + // At the beginning of your upkeep, look at the top card of your library. You may put that card into your graveyard. + // Exile a card from your graveyard: Add {C}. Spend this mana only to cast a colored spell without {X} in its mana cost. + addCard(Zone.HAND, playerA, "Titans' Nest"); // Enchantment {1}{B}{G}{U} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + + addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Titans' Nest"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Titans' Nest", 1); + + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); + Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); + assertManaOptions("{C}{C}[{TitansNestManaCondition}]", manaOptions); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java index 28e27aa624..c6fe1401b8 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventAllDamageTest.java @@ -1,145 +1,145 @@ -package org.mage.test.cards.prevention; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PreventAllDamageTest extends CardTestPlayerBase { - - @Test - public void test_SafePassage() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - // Prevent all damage that would be dealt to you and creatures you control this turn. - addCard(Zone.HAND, playerA, "Safe Passage"); // Instant {2}{W} - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 2); // (2/4) - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); - - addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instnat {R} - - castSpell(2, PhaseStep.UPKEEP, playerA, "Safe Passage"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); - - attack(2, playerB, "Pillarfield Ox"); - attack(2, playerB, "Pillarfield Ox"); - - block(2, playerA, "Silvercoat Lion", "Pillarfield Ox"); - - setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Safe Passage", 1); - assertPermanentCount(playerA, "Silvercoat Lion", 1); - - assertGraveyardCount(playerB, "Lightning Bolt", 2); - - assertLife(playerA, 20); - assertLife(playerB, 20); - - } - - @Test - public void test_EtherealHaze() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); - // Prevent all damage that would be dealt by creatures this turn. - addCard(Zone.HAND, playerA, "Ethereal Haze"); // Instant {W} - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); // (2/4) - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); - - addCard(Zone.HAND, playerB, "Lightning Bolt", 1); // Instant {R} - - castSpell(2, PhaseStep.UPKEEP, playerA, "Ethereal Haze"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); - - attack(2, playerB, "Silvercoat Lion"); - attack(2, playerB, "Silvercoat Lion"); - - block(2, playerA, "Silvercoat Lion", "Silvercoat Lion"); - - setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Ethereal Haze", 1); - assertPermanentCount(playerA, "Silvercoat Lion", 1); - - assertPermanentCount(playerB, "Silvercoat Lion", 2); - assertGraveyardCount(playerB, "Lightning Bolt", 1); - - assertLife(playerA, 17); - assertLife(playerB, 20); - - } - - @Test - public void test_EnergyStorm() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Cumulative upkeep {1} - // Prevent all damage that would be dealt by instant and sorcery spells. - // Creatures with flying don't untap during their controllers' untap steps. - addCard(Zone.HAND, playerA, "Energy Storm"); // ENCHANTMENT {1}{W} - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); // (2/2) - - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); - addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instant {R} - // Fire Ambush deals 3 damage to any target. - addCard(Zone.HAND, playerB, "Fire Ambush", 2); // Sorcery {1}{R} - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Energy Storm"); - - attack(1, playerA, "Abbey Griffin"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", playerA); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Abbey Griffin"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", playerA); - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", "Abbey Griffin"); - - attack(2, playerB, "Silvercoat Lion"); - - setChoice(playerA, false); // Pay {1}? Energy Storm - CumulativeUpkeepAbility: Cumulative upkeep {1} - - setStopAt(3, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Energy Storm", 1); - assertPermanentCount(playerA, "Abbey Griffin", 1); - - assertPermanentCount(playerB, "Silvercoat Lion", 1); - assertGraveyardCount(playerB, "Lightning Bolt", 2); - assertGraveyardCount(playerB, "Fire Ambush", 2); - - assertLife(playerA, 18); - assertLife(playerB, 18); - - } -} +package org.mage.test.cards.prevention; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PreventAllDamageTest extends CardTestPlayerBase { + + @Test + public void test_SafePassage() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + // Prevent all damage that would be dealt to you and creatures you control this turn. + addCard(Zone.HAND, playerA, "Safe Passage"); // Instant {2}{W} + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 2); // (2/4) + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); + + addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instnat {R} + + castSpell(2, PhaseStep.UPKEEP, playerA, "Safe Passage"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); + + attack(2, playerB, "Pillarfield Ox"); + attack(2, playerB, "Pillarfield Ox"); + + block(2, playerA, "Silvercoat Lion", "Pillarfield Ox"); + + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Safe Passage", 1); + assertPermanentCount(playerA, "Silvercoat Lion", 1); + + assertGraveyardCount(playerB, "Lightning Bolt", 2); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + } + + @Test + public void test_EtherealHaze() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + // Prevent all damage that would be dealt by creatures this turn. + addCard(Zone.HAND, playerA, "Ethereal Haze"); // Instant {W} + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); // (2/4) + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); + + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); // Instant {R} + + castSpell(2, PhaseStep.UPKEEP, playerA, "Ethereal Haze"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + + attack(2, playerB, "Silvercoat Lion"); + attack(2, playerB, "Silvercoat Lion"); + + block(2, playerA, "Silvercoat Lion", "Silvercoat Lion"); + + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Ethereal Haze", 1); + assertPermanentCount(playerA, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, "Silvercoat Lion", 2); + assertGraveyardCount(playerB, "Lightning Bolt", 1); + + assertLife(playerA, 17); + assertLife(playerB, 20); + + } + + @Test + public void test_EnergyStorm() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Cumulative upkeep {1} + // Prevent all damage that would be dealt by instant and sorcery spells. + // Creatures with flying don't untap during their controllers' untap steps. + addCard(Zone.HAND, playerA, "Energy Storm"); // ENCHANTMENT {1}{W} + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); // (2/2) + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); + addCard(Zone.HAND, playerB, "Lightning Bolt", 2); // Instant {R} + // Fire Ambush deals 3 damage to any target. + addCard(Zone.HAND, playerB, "Fire Ambush", 2); // Sorcery {1}{R} + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Energy Storm"); + + attack(1, playerA, "Abbey Griffin"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Abbey Griffin"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", playerA); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Fire Ambush", "Abbey Griffin"); + + attack(2, playerB, "Silvercoat Lion"); + + setChoice(playerA, false); // Pay {1}? Energy Storm - CumulativeUpkeepAbility: Cumulative upkeep {1} + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Energy Storm", 1); + assertPermanentCount(playerA, "Abbey Griffin", 1); + + assertPermanentCount(playerB, "Silvercoat Lion", 1); + assertGraveyardCount(playerB, "Lightning Bolt", 2); + assertGraveyardCount(playerB, "Fire Ambush", 2); + + assertLife(playerA, 18); + assertLife(playerB, 18); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java index 4fd955ec87..69f900e8db 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/protection/EightAndAHalfTailsTest.java @@ -1,48 +1,48 @@ -package org.mage.test.cards.protection; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - - -/** - * - * @author LevelX2 - */ -public class EightAndAHalfTailsTest extends CardTestPlayerBase { - - @Test - public void testProtectingPlaneswalker() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - - // Activated abilities of artifacts your opponents control can't be activated. - // +1: Until your next turn, up to one target noncreature artifact becomes an artifact creature with power and toughness equal to its converted mana cost. - // -2: You may choose an artifact card you own from outside the game or in exile, reveal that card, and put it into your hand. - addCard(Zone.BATTLEFIELD, playerA, "Karn, the Great Creator"); // Planeswalker (5) - - // {1}{W}: Target permanent you control gains protection from white until end of turn. - // {1}: Target spell or permanent becomes white until end of turn. - addCard(Zone.BATTLEFIELD, playerA, "Eight-and-a-Half-Tails"); // Creature - - // Flying, double strike - // Whenever a creature you control deals combat damage to a player, you and that player each gain that much life. - // At the beginning of your end step, if you have at least 15 life more than your starting life total, each player Angel of Destiny attacked this turn loses the game. - addCard(Zone.BATTLEFIELD, playerB, "Angel of Destiny"); // Creature - - attack(2, playerB, "Angel of Destiny", "Karn, the Great Creator"); - activateAbility(2, PhaseStep.DECLARE_ATTACKERS, playerA, "{1}{W}: Target permanent you control gains protection from white until end of turn."); - addTarget(playerA, "Karn, the Great Creator"); - - setStopAt(2, PhaseStep.END_COMBAT); - execute(); - - assertPermanentCount(playerA, "Karn, the Great Creator", 1); - assertCounterCount("Karn, the Great Creator", CounterType.LOYALTY, 5); - - } - +package org.mage.test.cards.protection; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +/** + * + * @author LevelX2 + */ +public class EightAndAHalfTailsTest extends CardTestPlayerBase { + + @Test + public void testProtectingPlaneswalker() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + // Activated abilities of artifacts your opponents control can't be activated. + // +1: Until your next turn, up to one target noncreature artifact becomes an artifact creature with power and toughness equal to its converted mana cost. + // -2: You may choose an artifact card you own from outside the game or in exile, reveal that card, and put it into your hand. + addCard(Zone.BATTLEFIELD, playerA, "Karn, the Great Creator"); // Planeswalker (5) + + // {1}{W}: Target permanent you control gains protection from white until end of turn. + // {1}: Target spell or permanent becomes white until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Eight-and-a-Half-Tails"); // Creature + + // Flying, double strike + // Whenever a creature you control deals combat damage to a player, you and that player each gain that much life. + // At the beginning of your end step, if you have at least 15 life more than your starting life total, each player Angel of Destiny attacked this turn loses the game. + addCard(Zone.BATTLEFIELD, playerB, "Angel of Destiny"); // Creature + + attack(2, playerB, "Angel of Destiny", "Karn, the Great Creator"); + activateAbility(2, PhaseStep.DECLARE_ATTACKERS, playerA, "{1}{W}: Target permanent you control gains protection from white until end of turn."); + addTarget(playerA, "Karn, the Great Creator"); + + setStopAt(2, PhaseStep.END_COMBAT); + execute(); + + assertPermanentCount(playerA, "Karn, the Great Creator", 1); + assertCounterCount("Karn, the Great Creator", CounterType.LOYALTY, 5); + + } + } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java index 727dda1f04..83eaf3783d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/ConduitOfRuinTest.java @@ -1,51 +1,51 @@ -package org.mage.test.cards.single.bfz; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class ConduitOfRuinTest extends CardTestPlayerBase { - - @Test - public void testCast() { - setStrictChooseMode(true); - - // Emrakul, the Aeons Torn can't be countered. - // When you cast Emrakul, take an extra turn after this one. - // Flying, protection from colored spells, annihilator 6 - // When Emrakul is put into a graveyard from anywhere, its owner shuffles their graveyard into their library. - addCard(Zone.LIBRARY, playerA, "Emrakul, the Aeons Torn"); // Creature {15} 15/15 - - // When you cast Conduit of Ruin, you may search your library for a colorless creature card with converted mana cost 7 or greater, then shuffle your library and put that card on top of it. - // The first creature spell you cast each turn costs {2} less to cast. - addCard(Zone.HAND, playerA, "Conduit of Ruin"); // Creature {6} 5/5 - addCard(Zone.BATTLEFIELD, playerA, "Plains", 13); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Conduit of Ruin"); - setChoice(playerA, true); // When you cast this spell, you may search... - addTarget(playerA, "Emrakul, the Aeons Torn"); - - setStopAt(3, PhaseStep.DRAW); - - execute(); - - assertLibraryCount(playerA, "Emrakul, the Aeons Torn", 0); - assertHandCount(playerA, "Emrakul, the Aeons Torn", 1); - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Aeons Torn"); - - setStopAt(3, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Conduit of Ruin", 1); - assertPermanentCount(playerA, "Emrakul, the Aeons Torn", 1); - } +package org.mage.test.cards.single.bfz; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class ConduitOfRuinTest extends CardTestPlayerBase { + + @Test + public void testCast() { + setStrictChooseMode(true); + + // Emrakul, the Aeons Torn can't be countered. + // When you cast Emrakul, take an extra turn after this one. + // Flying, protection from colored spells, annihilator 6 + // When Emrakul is put into a graveyard from anywhere, its owner shuffles their graveyard into their library. + addCard(Zone.LIBRARY, playerA, "Emrakul, the Aeons Torn"); // Creature {15} 15/15 + + // When you cast Conduit of Ruin, you may search your library for a colorless creature card with converted mana cost 7 or greater, then shuffle your library and put that card on top of it. + // The first creature spell you cast each turn costs {2} less to cast. + addCard(Zone.HAND, playerA, "Conduit of Ruin"); // Creature {6} 5/5 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 13); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Conduit of Ruin"); + setChoice(playerA, true); // When you cast this spell, you may search... + addTarget(playerA, "Emrakul, the Aeons Torn"); + + setStopAt(3, PhaseStep.DRAW); + + execute(); + + assertLibraryCount(playerA, "Emrakul, the Aeons Torn", 0); + assertHandCount(playerA, "Emrakul, the Aeons Torn", 1); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Aeons Torn"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Conduit of Ruin", 1); + assertPermanentCount(playerA, "Emrakul, the Aeons Torn", 1); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java index a1e26783b4..8d6b756001 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c17/TheUrDragonTest.java @@ -1,49 +1,49 @@ -package org.mage.test.cards.single.c17; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class TheUrDragonTest extends CardTestPlayerBase { - - - @Test - public void test_basic() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); - skipInitShuffling(); - // Eminence — As long as The Ur-Dragon is in the command zone or on the battlefield, other Dragon spells you cast cost 1 less to cast. - // Flying - // Whenever one or more Dragons you control attack, draw that many cards, then you may put a permanent card from your hand onto the battlefield. - addCard(Zone.BATTLEFIELD, playerA, "The Ur-Dragon", 1); // Creature (10/10) - // Flying - // {R}: Dragon Hatchling gets +1/+0 until end of turn. - addCard(Zone.HAND, playerA, "Dragon Hatchling", 2); // Creature Dragon {1}{R} (0/1) - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); - - attack(3, playerA, "The Ur-Dragon"); - attack(3, playerA, "Dragon Hatchling"); - attack(3, playerA, "Dragon Hatchling"); - setChoice(playerA, true); // Put a permanent card from your hand onto the battlefield? - setChoice(playerA, "Silvercoat Lion"); - - setStopAt(3, PhaseStep.END_COMBAT); - - execute(); - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Dragon Hatchling", 2); - assertPermanentCount(playerA, "Silvercoat Lion", 1 ); - assertHandCount(playerA, 3); - - } +package org.mage.test.cards.single.c17; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class TheUrDragonTest extends CardTestPlayerBase { + + + @Test + public void test_basic() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); + skipInitShuffling(); + // Eminence — As long as The Ur-Dragon is in the command zone or on the battlefield, other Dragon spells you cast cost 1 less to cast. + // Flying + // Whenever one or more Dragons you control attack, draw that many cards, then you may put a permanent card from your hand onto the battlefield. + addCard(Zone.BATTLEFIELD, playerA, "The Ur-Dragon", 1); // Creature (10/10) + // Flying + // {R}: Dragon Hatchling gets +1/+0 until end of turn. + addCard(Zone.HAND, playerA, "Dragon Hatchling", 2); // Creature Dragon {1}{R} (0/1) + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dragon Hatchling"); + + attack(3, playerA, "The Ur-Dragon"); + attack(3, playerA, "Dragon Hatchling"); + attack(3, playerA, "Dragon Hatchling"); + setChoice(playerA, true); // Put a permanent card from your hand onto the battlefield? + setChoice(playerA, "Silvercoat Lion"); + + setStopAt(3, PhaseStep.END_COMBAT); + + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Dragon Hatchling", 2); + assertPermanentCount(playerA, "Silvercoat Lion", 1 ); + assertHandCount(playerA, 3); + + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java index a8d36115ce..167c09d5ad 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/AminatousAuguryTest.java @@ -1,75 +1,75 @@ - -package org.mage.test.cards.single.c18; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class AminatousAuguryTest extends CardTestPlayerBase { - - @Test - public void testCastMultiple() { - setStrictChooseMode(true); - - - addCard(Zone.LIBRARY, playerA, "Pillarfield Ox"); // Creature (2/4) - // As an additional cost to cast this spell, discard a card. - // Draw two cards. - addCard(Zone.LIBRARY, playerA, "Tormenting Voice"); // Sorcery - // {1}: Adarkar Sentinel gets +0/+1 until end of turn. - addCard(Zone.LIBRARY, playerA, "Adarkar Sentinel"); // Artifact Creature {5} (3/3) - addCard(Zone.LIBRARY, playerA, "Storm Crow"); - // You have hexproof. (You can't be the target of spells or abilities your opponents control.) - addCard(Zone.LIBRARY, playerA, "Aegis of the Gods"); // Enchantment Creature {1}{W} (2/1) - addCard(Zone.LIBRARY, playerA, "Lightning Bolt"); // Instant - addCard(Zone.LIBRARY, playerA, "Badlands"); - skipInitShuffling(); - // Exile the top eight cards of your library. You may put a land card from among them onto the battlefield. - // Until end of turn, for each nonland card type, you may cast a card of that type from among the exiled cards - // without paying its mana cost. - addCard(Zone.HAND, playerA, "Aminatou's Augury"); // SORCERY {6}{U}{U} - addCard(Zone.HAND, playerA, "Mountain"); - addCard(Zone.HAND, playerA, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerA, "Island", 8); - - playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aminatou's Augury"); - setChoice(playerA, true); // Put a land from among the exiled cards into play? - setChoice(playerA, "Badlands"); // Select a land card - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Adarkar Sentinel"); - setChoice(playerA, "Artifact"); // Which card type do you want to consume? - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aegis of the Gods"); - setChoice(playerA, "Enchantment"); // Which card type do you want to consume? - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Storm Crow"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tormenting Voice"); - setChoice(playerA, "Silvercoat Lion"); // Select a card (discard cost) - - checkPlayableAbility("Cannot cast second creature from exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Pillarfield Ox", Boolean.FALSE); // Type Creature type is already consumed - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Aminatou's Augury", 1); - assertPermanentCount(playerA, "Mountain", 1); - assertPermanentCount(playerA, "Badlands", 1); - assertPermanentCount(playerA, "Adarkar Sentinel", 1); - assertPermanentCount(playerA, "Aegis of the Gods", 1); - assertPermanentCount(playerA, "Storm Crow", 1); - assertGraveyardCount(playerA, "Lightning Bolt", 1); - - assertLife(playerA, 20); - assertLife(playerB, 17); - - assertHandCount(playerA, 2); - assertGraveyardCount(playerA, "Silvercoat Lion",1); - assertExileCount(playerA, 2); - } - -} + +package org.mage.test.cards.single.c18; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class AminatousAuguryTest extends CardTestPlayerBase { + + @Test + public void testCastMultiple() { + setStrictChooseMode(true); + + + addCard(Zone.LIBRARY, playerA, "Pillarfield Ox"); // Creature (2/4) + // As an additional cost to cast this spell, discard a card. + // Draw two cards. + addCard(Zone.LIBRARY, playerA, "Tormenting Voice"); // Sorcery + // {1}: Adarkar Sentinel gets +0/+1 until end of turn. + addCard(Zone.LIBRARY, playerA, "Adarkar Sentinel"); // Artifact Creature {5} (3/3) + addCard(Zone.LIBRARY, playerA, "Storm Crow"); + // You have hexproof. (You can't be the target of spells or abilities your opponents control.) + addCard(Zone.LIBRARY, playerA, "Aegis of the Gods"); // Enchantment Creature {1}{W} (2/1) + addCard(Zone.LIBRARY, playerA, "Lightning Bolt"); // Instant + addCard(Zone.LIBRARY, playerA, "Badlands"); + skipInitShuffling(); + // Exile the top eight cards of your library. You may put a land card from among them onto the battlefield. + // Until end of turn, for each nonland card type, you may cast a card of that type from among the exiled cards + // without paying its mana cost. + addCard(Zone.HAND, playerA, "Aminatou's Augury"); // SORCERY {6}{U}{U} + addCard(Zone.HAND, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 8); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aminatou's Augury"); + setChoice(playerA, true); // Put a land from among the exiled cards into play? + setChoice(playerA, "Badlands"); // Select a land card + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Adarkar Sentinel"); + setChoice(playerA, "Artifact"); // Which card type do you want to consume? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aegis of the Gods"); + setChoice(playerA, "Enchantment"); // Which card type do you want to consume? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Storm Crow"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tormenting Voice"); + setChoice(playerA, "Silvercoat Lion"); // Select a card (discard cost) + + checkPlayableAbility("Cannot cast second creature from exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Pillarfield Ox", Boolean.FALSE); // Type Creature type is already consumed + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Aminatou's Augury", 1); + assertPermanentCount(playerA, "Mountain", 1); + assertPermanentCount(playerA, "Badlands", 1); + assertPermanentCount(playerA, "Adarkar Sentinel", 1); + assertPermanentCount(playerA, "Aegis of the Gods", 1); + assertPermanentCount(playerA, "Storm Crow", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + + assertLife(playerA, 20); + assertLife(playerB, 17); + + assertHandCount(playerA, 2); + assertGraveyardCount(playerA, "Silvercoat Lion",1); + assertExileCount(playerA, 2); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java index 5f7ab83c38..849e3ede9b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c20/PakoArcaneRetrieverTest.java @@ -1,96 +1,96 @@ - -package org.mage.test.cards.single.c20; - -import mage.cards.Card; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class PakoArcaneRetrieverTest extends CardTestPlayerBase { - - @Test - public void test_CheckExiled() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); - addCard(Zone.LIBRARY, playerB, "Pillarfield Ox", 1); - - skipInitShuffling(); - // Partner with Pako, Arcane Retriever - // You may play noncreature cards from exile with fetch counters on them if you - // exiled them, and you may spend mana as though it were mana of any color to cast those spells. - addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); - // Partner with Haldan, Avid Arcanist - // Haste - // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. - addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) - - attack(1, playerA, "Pako, Arcane Retriever"); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertLife(playerA, 20); - assertLife(playerB, 17); - - assertExileCount(playerA, "Silvercoat Lion", 1); - assertExileCount(playerB, "Pillarfield Ox", 1); - - for(Card card :currentGame.getExile().getAllCards(currentGame)) { - Assert.assertTrue(card.getName() + " has a fetch counter",card.getCounters(currentGame).getCount(CounterType.FETCH) == 1); - } - - } - - @Test - public void test_CastExiled() { - // setStrictChooseMode(true); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); - - addCard(Zone.LIBRARY, playerA, "Lightning Bolt", 1); // Instant 3 damge - // Create a 3/3 green Centaur creature token. - addCard(Zone.LIBRARY, playerB, "Call of the Conclave", 1); // Sorcery {W}{G} - - skipInitShuffling(); - // Partner with Pako, Arcane Retriever - // You may play noncreature cards from exile with fetch counters on them if you - // exiled them, and you may spend mana as though it were mana of any color to cast those spells. - addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); - // Partner with Haldan, Avid Arcanist - // Haste - // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. - addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) - - attack(1, playerA, "Pako, Arcane Retriever"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Call of the Conclave"); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertLife(playerA, 20); - assertLife(playerB, 12); // 3+2 (attack) + 3 Lighning Bolt - - assertGraveyardCount(playerA, "Lightning Bolt", 1); - assertGraveyardCount(playerB, "Call of the Conclave", 1); - - - } -} + +package org.mage.test.cards.single.c20; + +import mage.cards.Card; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class PakoArcaneRetrieverTest extends CardTestPlayerBase { + + @Test + public void test_CheckExiled() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); + addCard(Zone.LIBRARY, playerB, "Pillarfield Ox", 1); + + skipInitShuffling(); + // Partner with Pako, Arcane Retriever + // You may play noncreature cards from exile with fetch counters on them if you + // exiled them, and you may spend mana as though it were mana of any color to cast those spells. + addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); + // Partner with Haldan, Avid Arcanist + // Haste + // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. + addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) + + attack(1, playerA, "Pako, Arcane Retriever"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 17); + + assertExileCount(playerA, "Silvercoat Lion", 1); + assertExileCount(playerB, "Pillarfield Ox", 1); + + for(Card card :currentGame.getExile().getAllCards(currentGame)) { + Assert.assertTrue(card.getName() + " has a fetch counter",card.getCounters(currentGame).getCount(CounterType.FETCH) == 1); + } + + } + + @Test + public void test_CastExiled() { + // setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + addCard(Zone.LIBRARY, playerA, "Lightning Bolt", 1); // Instant 3 damge + // Create a 3/3 green Centaur creature token. + addCard(Zone.LIBRARY, playerB, "Call of the Conclave", 1); // Sorcery {W}{G} + + skipInitShuffling(); + // Partner with Pako, Arcane Retriever + // You may play noncreature cards from exile with fetch counters on them if you + // exiled them, and you may spend mana as though it were mana of any color to cast those spells. + addCard(Zone.BATTLEFIELD, playerA, "Haldan, Avid Arcanist", 1); + // Partner with Haldan, Avid Arcanist + // Haste + // Whenever Pako, Arcane Retriever attacks, exile the top card of each player's library and put a fetch counter on each of them. Put a +1/+1 counter on Pako for each noncreature card exiled this way. + addCard(Zone.BATTLEFIELD, playerA, "Pako, Arcane Retriever", 1); // Creature {3}{R}{G} (3/3) + + attack(1, playerA, "Pako, Arcane Retriever"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Call of the Conclave"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 12); // 3+2 (attack) + 3 Lighning Bolt + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertGraveyardCount(playerB, "Call of the Conclave", 1); + + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java index 6f91f4cbde..c8c47a1bee 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dst/DemonsHornTest.java @@ -1,72 +1,72 @@ -package org.mage.test.cards.single.dst; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class DemonsHornTest extends CardTestPlayerBase { - - - @Test - public void testWithBlackSpell() { - setStrictChooseMode(true); - - // When Abyssal Gatekeeper dies, each player sacrifices a creature. - addCard(Zone.HAND, playerA, "Abyssal Gatekeeper", 1); // Creature {2}{B} 1/1 - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); - - // Whenever a player casts a black spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Abyssal Gatekeeper"); - setChoice(playerB, true); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Abyssal Gatekeeper", 1); - - assertLife(playerA, 20); - assertLife(playerB, 21); - } - - /** - * https://github.com/magefree/mage/issues/6890 - * - * Color == Color Identity #6890 - * - * Alesha, Who Smiles at Death triggers Demon's Horn - */ - @Test - public void testSpellWithBlackManaOnlyInTriggeredOptionalCost() { - setStrictChooseMode(true); - - // First strike - // Whenever Alesha, Who Smiles at Death attacks, you may pay {W/B}{W/B}. If you do, return target creature card with power 2 or less from your graveyard to the battlefield tapped and attacking. - addCard(Zone.HAND, playerA, "Alesha, Who Smiles at Death", 1); // Creature {2}{R} 3/2 - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - - // Whenever a player casts a black spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alesha, Who Smiles at Death"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Alesha, Who Smiles at Death", 1); - - assertLife(playerA, 20); - assertLife(playerB, 20); - } - -} +package org.mage.test.cards.single.dst; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class DemonsHornTest extends CardTestPlayerBase { + + + @Test + public void testWithBlackSpell() { + setStrictChooseMode(true); + + // When Abyssal Gatekeeper dies, each player sacrifices a creature. + addCard(Zone.HAND, playerA, "Abyssal Gatekeeper", 1); // Creature {2}{B} 1/1 + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + + // Whenever a player casts a black spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Abyssal Gatekeeper"); + setChoice(playerB, true); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Abyssal Gatekeeper", 1); + + assertLife(playerA, 20); + assertLife(playerB, 21); + } + + /** + * https://github.com/magefree/mage/issues/6890 + * + * Color == Color Identity #6890 + * + * Alesha, Who Smiles at Death triggers Demon's Horn + */ + @Test + public void testSpellWithBlackManaOnlyInTriggeredOptionalCost() { + setStrictChooseMode(true); + + // First strike + // Whenever Alesha, Who Smiles at Death attacks, you may pay {W/B}{W/B}. If you do, return target creature card with power 2 or less from your graveyard to the battlefield tapped and attacking. + addCard(Zone.HAND, playerA, "Alesha, Who Smiles at Death", 1); // Creature {2}{R} 3/2 + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // Whenever a player casts a black spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerB, "Demon's Horn", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alesha, Who Smiles at Death"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Alesha, Who Smiles at Death", 1); + + assertLife(playerA, 20); + assertLife(playerB, 20); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java index 4e951974c1..7be461ae76 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/BarteredCowTest.java @@ -1,88 +1,88 @@ -package org.mage.test.cards.single.eld; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class BarteredCowTest extends CardTestPlayerBase { - - @Test - public void testDiesTrigger() { - setStrictChooseMode(true); - - // When Bartered Cow dies or when you discard it, create a Food token. - addCard(Zone.BATTLEFIELD, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 - - addCard(Zone.HAND, playerB, "Lightning Bolt", 1); - addCard(Zone.BATTLEFIELD, playerB, "Mountain"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Bartered Cow"); - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Lightning Bolt", 1); - assertGraveyardCount(playerA, "Bartered Cow", 1); - assertPermanentCount(playerA, "Food", 1); - } - - @Test - public void testDiscardTrigger() { - setStrictChooseMode(true); - - // When Bartered Cow dies or when you discard it, create a Food token. - addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 - // Choose one — - // • Target player discards a card. - // • Target creature gets +2/-1 until end of turn. - // • Target creature gains swampwalk until end of turn. (It can't be blocked as long as defending player controls a Swamp.) - addCard(Zone.HAND, playerB, "Funeral Charm", 1); // Instant {B} - addCard(Zone.BATTLEFIELD, playerB, "Swamp"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Funeral Charm"); - setModeChoice(playerB, "1"); - addTarget(playerB, playerA); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Funeral Charm", 1); - assertGraveyardCount(playerA, "Bartered Cow", 1); - assertPermanentCount(playerA, "Food", 1); - } - - @Test - public void testDiscardTriggerWithTorturedExistence() { - setStrictChooseMode(true); - - // When Bartered Cow dies or when you discard it, create a Food token. - addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 - - // {B}, Discard a creature card: Return target creature card from your graveyard to your hand. - addCard(Zone.BATTLEFIELD, playerA, "Tortured Existence", 1); // Instant {B} - addCard(Zone.BATTLEFIELD, playerA, "Swamp"); - addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion"); - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B}, Discard a creature card"); - setChoice(playerA, "Bartered Cow"); - addTarget(playerA, "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Bartered Cow", 1); - assertHandCount(playerA, "Silvercoat Lion", 1); - assertPermanentCount(playerA, "Food", 1); - } - -} +package org.mage.test.cards.single.eld; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class BarteredCowTest extends CardTestPlayerBase { + + @Test + public void testDiesTrigger() { + setStrictChooseMode(true); + + // When Bartered Cow dies or when you discard it, create a Food token. + addCard(Zone.BATTLEFIELD, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 + + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Bartered Cow"); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Lightning Bolt", 1); + assertGraveyardCount(playerA, "Bartered Cow", 1); + assertPermanentCount(playerA, "Food", 1); + } + + @Test + public void testDiscardTrigger() { + setStrictChooseMode(true); + + // When Bartered Cow dies or when you discard it, create a Food token. + addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 + // Choose one — + // • Target player discards a card. + // • Target creature gets +2/-1 until end of turn. + // • Target creature gains swampwalk until end of turn. (It can't be blocked as long as defending player controls a Swamp.) + addCard(Zone.HAND, playerB, "Funeral Charm", 1); // Instant {B} + addCard(Zone.BATTLEFIELD, playerB, "Swamp"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Funeral Charm"); + setModeChoice(playerB, "1"); + addTarget(playerB, playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Funeral Charm", 1); + assertGraveyardCount(playerA, "Bartered Cow", 1); + assertPermanentCount(playerA, "Food", 1); + } + + @Test + public void testDiscardTriggerWithTorturedExistence() { + setStrictChooseMode(true); + + // When Bartered Cow dies or when you discard it, create a Food token. + addCard(Zone.HAND, playerA, "Bartered Cow"); // Creature {3}{W} 3/3 + + // {B}, Discard a creature card: Return target creature card from your graveyard to your hand. + addCard(Zone.BATTLEFIELD, playerA, "Tortured Existence", 1); // Instant {B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B}, Discard a creature card"); + setChoice(playerA, "Bartered Cow"); + addTarget(playerA, "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Bartered Cow", 1); + assertHandCount(playerA, "Silvercoat Lion", 1); + assertPermanentCount(playerA, "Food", 1); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java index ec736c19a5..2982fe655c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java @@ -1,89 +1,89 @@ -package org.mage.test.cards.single.eld; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class OnceUponATimeTest extends CardTestPlayerBase { - - @Test - public void test_castRegularly() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); - addCard(Zone.LIBRARY, playerA, "Plains", 4); - skipInitShuffling(); - - // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. - // Look at the top five cards of your library. - // You may reveal a creature or land card from among them and put it into your hand. - // Put the rest on the bottom of your library in a random order. - addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} - addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); - addCard(Zone.HAND, playerA, "Forest", 1); - - playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); - - setChoice(playerA, false); // Cast without paying its mana cost? - - setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? - setChoice(playerA, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Once Upon a Time", 1); - assertTappedCount("Forest", true, 2); - assertHandCount(playerA, "Silvercoat Lion", 1); - } - - @Test - public void test_castForFree() { - setStrictChooseMode(true); - - addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); - addCard(Zone.LIBRARY, playerA, "Plains", 4); - - addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 5); - - skipInitShuffling(); - - // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. - // Look at the top five cards of your library. - // You may reveal a creature or land card from among them and put it into your hand. - // Put the rest on the bottom of your library in a random order. - addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} - addCard(Zone.HAND, playerB, "Once Upon a Time"); // Instant {1}{G} - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); - setChoice(playerA, true); // Cast without paying its mana cost? - setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? - setChoice(playerA, "Silvercoat Lion"); - - castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Once Upon a Time"); - setChoice(playerB, true); // Cast without paying its mana cost? - setChoice(playerB, true); // Do you wish to reveal a creature or land card and put into your hand? - setChoice(playerB, "Silvercoat Lion"); - - setStopAt(2, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Once Upon a Time", 1); - assertGraveyardCount(playerB, "Once Upon a Time", 1); - - assertHandCount(playerA, "Silvercoat Lion", 1); - assertHandCount(playerB, "Silvercoat Lion", 2); - } +package org.mage.test.cards.single.eld; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class OnceUponATimeTest extends CardTestPlayerBase { + + @Test + public void test_castRegularly() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); + addCard(Zone.LIBRARY, playerA, "Plains", 4); + skipInitShuffling(); + + // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. + // Look at the top five cards of your library. + // You may reveal a creature or land card from among them and put it into your hand. + // Put the rest on the bottom of your library in a random order. + addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.HAND, playerA, "Forest", 1); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); + + setChoice(playerA, false); // Cast without paying its mana cost? + + setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? + setChoice(playerA, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Once Upon a Time", 1); + assertTappedCount("Forest", true, 2); + assertHandCount(playerA, "Silvercoat Lion", 1); + } + + @Test + public void test_castForFree() { + setStrictChooseMode(true); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); + addCard(Zone.LIBRARY, playerA, "Plains", 4); + + addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 5); + + skipInitShuffling(); + + // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. + // Look at the top five cards of your library. + // You may reveal a creature or land card from among them and put it into your hand. + // Put the rest on the bottom of your library in a random order. + addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} + addCard(Zone.HAND, playerB, "Once Upon a Time"); // Instant {1}{G} + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); + setChoice(playerA, true); // Cast without paying its mana cost? + setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? + setChoice(playerA, "Silvercoat Lion"); + + castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Once Upon a Time"); + setChoice(playerB, true); // Cast without paying its mana cost? + setChoice(playerB, true); // Do you wish to reveal a creature or land card and put into your hand? + setChoice(playerB, "Silvercoat Lion"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Once Upon a Time", 1); + assertGraveyardCount(playerB, "Once Upon a Time", 1); + + assertHandCount(playerA, "Silvercoat Lion", 1); + assertHandCount(playerB, "Silvercoat Lion", 2); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java index d00136c897..eab21ba875 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/SyrGwynHeroOfAshvaleTest.java @@ -1,57 +1,57 @@ -package org.mage.test.cards.single.eld; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class SyrGwynHeroOfAshvaleTest extends CardTestPlayerBase { - - @Test - public void equipKnightTest() { - // Equipped creature gets +2/+2 and has trample and lifelink. - addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} - - // Vigilance, menace - // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. - // Equipment you control have equip Knight {0}. - addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); - - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); - - } - - @Test - public void equipKnightTestInstantSpeed() { - // Equipped creature gets +2/+2 and has trample and lifelink. - addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} - - // You may activate equip abilities any time you could cast an instant. - addCard(Zone.BATTLEFIELD, playerA, "Leonin Shikari", 2); // Creature 2/2 - - // Vigilance, menace - // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. - // Equipment you control have equip Knight {0}. - addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight - - activateAbility(1, PhaseStep.DECLARE_ATTACKERS, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); - - - setStopAt(1, PhaseStep.END_COMBAT); - execute(); - - assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); - - } -} +package org.mage.test.cards.single.eld; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class SyrGwynHeroOfAshvaleTest extends CardTestPlayerBase { + + @Test + public void equipKnightTest() { + // Equipped creature gets +2/+2 and has trample and lifelink. + addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} + + // Vigilance, menace + // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. + // Equipment you control have equip Knight {0}. + addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); + + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); + + } + + @Test + public void equipKnightTestInstantSpeed() { + // Equipped creature gets +2/+2 and has trample and lifelink. + addCard(Zone.BATTLEFIELD, playerA, "Behemoth Sledge"); // Artifact - Equipment {1}{G}{W} + + // You may activate equip abilities any time you could cast an instant. + addCard(Zone.BATTLEFIELD, playerA, "Leonin Shikari", 2); // Creature 2/2 + + // Vigilance, menace + // Whenever an equipped creature you control attacks, you draw a card and you lose 1 life. + // Equipment you control have equip Knight {0}. + addCard(Zone.BATTLEFIELD, playerA, "Syr Gwyn, Hero of Ashvale"); // Legendary Creature {3}{R}{W}{B} 5/5 Human Knight + + activateAbility(1, PhaseStep.DECLARE_ATTACKERS, playerA, "Equip Knight", "Syr Gwyn, Hero of Ashvale"); + + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertPowerToughness(playerA, "Syr Gwyn, Hero of Ashvale", 7, 7); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java index 1833604d25..ea87437b73 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/PeltCollectorTest.java @@ -1,245 +1,245 @@ -package org.mage.test.cards.single.grn; - -import mage.abilities.keyword.TrampleAbility; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.counters.CounterType; -import mage.filter.Filter; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2, TheElk801 - */ -public class PeltCollectorTest extends CardTestPlayerBase { - - private static final String collector = "Pelt Collector"; - private static final String lion = "Silvercoat Lion"; - private static final String trostani = "Trostani Discordant"; - private static final String bear = "Grizzly Bears"; - private static final String murder = "Murder"; - private static final String courser = "Centaur Courser"; - private static final String growth = "Giant Growth"; - private static final String karstoderm = "Karstoderm"; - - @Test - public void test_Simple() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. - // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. - addCard(Zone.HAND, playerA, collector, 1); // Creature {G} - addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} - - addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, collector); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerB, collector, 1, 1); - - assertPowerToughness(playerA, lion, 2, 2); - assertPowerToughness(playerA, collector, 2, 2); - assertAbility(playerA, collector, TrampleAbility.getInstance(), false); - assertAbility(playerB, collector, TrampleAbility.getInstance(), false); - } - - /** - * To determine if Pelt Collector’s first ability triggers when a creature - * enters the battlefield, use the creature’s power after applying any - * static abilities (such as that of Trostani Discordant) that modify its - * power. - */ - @Test - public void test_TrostaniDiscordant() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. - // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. - addCard(Zone.HAND, playerA, collector, 1); // Creature {G} - addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} - // Other creatures you control get +1/+1. - // When Trostani Discordant enters the battlefield, create two 1/1 white Soldier creature tokens with lifelink. - // At the beginning of your end step, each player gains control of all creatures they own. - addCard(Zone.HAND, playerA, trostani, 1); // Creature {3}{G}{W} /1/4) - - addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, trostani); - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, collector); - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, lion); - - setStopAt(3, PhaseStep.BEGIN_COMBAT); - - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerB, collector, 1, 1); - - assertPowerToughness(playerA, "Soldier", 2, 2, Filter.ComparisonScope.All); - - assertPowerToughness(playerA, lion, 3, 3); - assertPowerToughness(playerA, collector, 3, 3); - assertAbility(playerA, collector, TrampleAbility.getInstance(), false); - assertAbility(playerB, collector, TrampleAbility.getInstance(), false); - - } - - - @Test - public void testEntersTrigger() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 2, 2); - assertCounterCount(collector, CounterType.P1P1, 1); - } - - @Test - public void testEntersTrigger2() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, karstoderm); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 2, 2); - assertCounterCount(collector, CounterType.P1P1, 1); - } - - @Test - public void testDiesTrigger() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.HAND, playerA, murder); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 5); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, bear); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 2, 2); - assertCounterCount(collector, CounterType.P1P1, 1); - } - - @Test - public void testDiesTrigger2() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, courser); - addCard(Zone.HAND, playerA, murder); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 6); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courser); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, courser); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 3, 3); - assertCounterCount(collector, CounterType.P1P1, 2); - } - - @Test - public void testDiesTrigger3() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, karstoderm); - addCard(Zone.HAND, playerA, murder); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 7); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, karstoderm); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 3, 3); - assertCounterCount(collector, CounterType.P1P1, 2); - } - - @Test - public void testInterveningIf() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.HAND, playerA, growth); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, growth, collector); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 4, 4); - assertCounterCount(collector, CounterType.P1P1, 0); - } - - @Test - public void testInterveningIf2() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, collector); - addCard(Zone.HAND, playerA, bear); - addCard(Zone.HAND, playerA, "Scar"); - addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scar", bear); - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, collector, 1, 1); - assertCounterCount(collector, CounterType.P1P1, 0); - } -} +package org.mage.test.cards.single.grn; + +import mage.abilities.keyword.TrampleAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.Filter; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2, TheElk801 + */ +public class PeltCollectorTest extends CardTestPlayerBase { + + private static final String collector = "Pelt Collector"; + private static final String lion = "Silvercoat Lion"; + private static final String trostani = "Trostani Discordant"; + private static final String bear = "Grizzly Bears"; + private static final String murder = "Murder"; + private static final String courser = "Centaur Courser"; + private static final String growth = "Giant Growth"; + private static final String karstoderm = "Karstoderm"; + + @Test + public void test_Simple() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. + // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. + addCard(Zone.HAND, playerA, collector, 1); // Creature {G} + addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} + + addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, collector); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerB, collector, 1, 1); + + assertPowerToughness(playerA, lion, 2, 2); + assertPowerToughness(playerA, collector, 2, 2); + assertAbility(playerA, collector, TrampleAbility.getInstance(), false); + assertAbility(playerB, collector, TrampleAbility.getInstance(), false); + } + + /** + * To determine if Pelt Collector’s first ability triggers when a creature + * enters the battlefield, use the creature’s power after applying any + * static abilities (such as that of Trostani Discordant) that modify its + * power. + */ + @Test + public void test_TrostaniDiscordant() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + // Whenever another creature you control enters the battlefield or dies, if that creature's power is greater than Pelt Collector's, put a +1/+1 counter on Pelt Collector. + // As long as Pelt Collector has three or more +1/+1 counters on it, it has trample. + addCard(Zone.HAND, playerA, collector, 1); // Creature {G} + addCard(Zone.HAND, playerA, lion, 1); // Creature {1}{W} + // Other creatures you control get +1/+1. + // When Trostani Discordant enters the battlefield, create two 1/1 white Soldier creature tokens with lifelink. + // At the beginning of your end step, each player gains control of all creatures they own. + addCard(Zone.HAND, playerA, trostani, 1); // Creature {3}{G}{W} /1/4) + + addCard(Zone.BATTLEFIELD, playerB, collector, 1);// Creature {G} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, trostani); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, collector); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerB, collector, 1, 1); + + assertPowerToughness(playerA, "Soldier", 2, 2, Filter.ComparisonScope.All); + + assertPowerToughness(playerA, lion, 3, 3); + assertPowerToughness(playerA, collector, 3, 3); + assertAbility(playerA, collector, TrampleAbility.getInstance(), false); + assertAbility(playerB, collector, TrampleAbility.getInstance(), false); + + } + + + @Test + public void testEntersTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 2, 2); + assertCounterCount(collector, CounterType.P1P1, 1); + } + + @Test + public void testEntersTrigger2() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, karstoderm); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 2, 2); + assertCounterCount(collector, CounterType.P1P1, 1); + } + + @Test + public void testDiesTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.HAND, playerA, murder); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 5); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, bear); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 2, 2); + assertCounterCount(collector, CounterType.P1P1, 1); + } + + @Test + public void testDiesTrigger2() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, courser); + addCard(Zone.HAND, playerA, murder); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 6); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courser); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, courser); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 3, 3); + assertCounterCount(collector, CounterType.P1P1, 2); + } + + @Test + public void testDiesTrigger3() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, karstoderm); + addCard(Zone.HAND, playerA, murder); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 7); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, karstoderm); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, karstoderm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 3, 3); + assertCounterCount(collector, CounterType.P1P1, 2); + } + + @Test + public void testInterveningIf() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.HAND, playerA, growth); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, growth, collector); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 4, 4); + assertCounterCount(collector, CounterType.P1P1, 0); + } + + @Test + public void testInterveningIf2() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, collector); + addCard(Zone.HAND, playerA, bear); + addCard(Zone.HAND, playerA, "Scar"); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bear); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scar", bear); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, collector, 1, 1); + assertCounterCount(collector, CounterType.P1P1, 0); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java index 4d2d4d2aff..7dc01ca0e7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/hou/TormentOfHailfireTest.java @@ -1,88 +1,88 @@ -package org.mage.test.cards.single.hou; - -import java.io.FileNotFoundException; - -import mage.constants.MultiplayerAttackOption; -import mage.constants.PhaseStep; -import mage.constants.RangeOfInfluence; -import mage.constants.Zone; -import mage.game.FreeForAll; -import mage.game.Game; -import mage.game.GameException; -import mage.game.mulligan.MulliganType; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestMultiPlayerBase; - -/** - * - * @author LevelX2 - */ -public class TormentOfHailfireTest extends CardTestMultiPlayerBase { - - @Override - protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - // Start Life = 2 - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); - // Player order: A -> D -> C -> B - playerA = createPlayer(game, playerA, "PlayerA"); - playerB = createPlayer(game, playerB, "PlayerB"); - playerC = createPlayer(game, playerC, "PlayerC"); - playerD = createPlayer(game, playerD, "PlayerD"); - return game; - } - - @Test - public void test_Normal() { - setStrictChooseMode(true); - - // Repeat the following process X times. Each opponent loses 3 life unless they sacrifice a nonland permanent or discards a card. - addCard(Zone.HAND, playerA, "Torment of Hailfire", 1); // Sorcery {X}{B}{B} - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 12); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); - addCard(Zone.HAND, playerB, "Plains", 1); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 3); - - addCard(Zone.BATTLEFIELD, playerD, "Silvercoat Lion", 3); - addCard(Zone.HAND, playerD, "Plains", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Torment of Hailfire"); - setChoice(playerA, "X=10"); - - setChoice(playerD, true);// Sacrifices a nonland permanent? - setChoice(playerD, "Silvercoat Lion"); - - setChoice(playerB, true);// Sacrifices a nonland permanent? - setChoice(playerB, "Silvercoat Lion"); - - setChoice(playerD, true);// Sacrifices a nonland permanent? - setChoice(playerD, "Silvercoat Lion"); - - setChoice(playerB, true);// Sacrifices a nonland permanent? - setChoice(playerB, "Silvercoat Lion"); - - setChoice(playerD, false);// Sacrifices a nonland permanent? - setChoice(playerD, true);// Discard a card? - - setChoice(playerB, true);// Discard a card? - - setChoice(playerD, true);// Sacrifices a nonland permanent? - setChoice(playerD, "Silvercoat Lion"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Torment of Hailfire", 1); - - assertLife(playerA, 20); - assertLife(playerC, 20); - assertLife(playerD, 2); - assertLife(playerB, -1); - Assert.assertFalse("Player B is dead", playerB.isInGame()); - - } -} +package org.mage.test.cards.single.hou; + +import java.io.FileNotFoundException; + +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.mulligan.MulliganType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +/** + * + * @author LevelX2 + */ +public class TormentOfHailfireTest extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // Start Life = 2 + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, playerA, "PlayerA"); + playerB = createPlayer(game, playerB, "PlayerB"); + playerC = createPlayer(game, playerC, "PlayerC"); + playerD = createPlayer(game, playerD, "PlayerD"); + return game; + } + + @Test + public void test_Normal() { + setStrictChooseMode(true); + + // Repeat the following process X times. Each opponent loses 3 life unless they sacrifice a nonland permanent or discards a card. + addCard(Zone.HAND, playerA, "Torment of Hailfire", 1); // Sorcery {X}{B}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 12); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2); + addCard(Zone.HAND, playerB, "Plains", 1); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 3); + + addCard(Zone.BATTLEFIELD, playerD, "Silvercoat Lion", 3); + addCard(Zone.HAND, playerD, "Plains", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Torment of Hailfire"); + setChoice(playerA, "X=10"); + + setChoice(playerD, true);// Sacrifices a nonland permanent? + setChoice(playerD, "Silvercoat Lion"); + + setChoice(playerB, true);// Sacrifices a nonland permanent? + setChoice(playerB, "Silvercoat Lion"); + + setChoice(playerD, true);// Sacrifices a nonland permanent? + setChoice(playerD, "Silvercoat Lion"); + + setChoice(playerB, true);// Sacrifices a nonland permanent? + setChoice(playerB, "Silvercoat Lion"); + + setChoice(playerD, false);// Sacrifices a nonland permanent? + setChoice(playerD, true);// Discard a card? + + setChoice(playerB, true);// Discard a card? + + setChoice(playerD, true);// Sacrifices a nonland permanent? + setChoice(playerD, "Silvercoat Lion"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Torment of Hailfire", 1); + + assertLife(playerA, 20); + assertLife(playerC, 20); + assertLife(playerD, 2); + assertLife(playerB, -1); + Assert.assertFalse("Player B is dead", playerB.isInGame()); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java index c80bb289e7..0614756504 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/OboshThePreypiercerTest.java @@ -1,34 +1,34 @@ -package org.mage.test.cards.single.iko; - - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author LevelX2 - */ - -public class OboshThePreypiercerTest extends CardTestPlayerBase { - - @Test - public void testZeroCMSIsHandledAsOdd() { - setStrictChooseMode(true); - // At the beginning of your upkeep, flip a coin. If you lose the flip, Mana Crypt deals 3 damage to you. - // {T}: Add {C}{C}. - addCard(Zone.BATTLEFIELD, playerA, "Mana Crypt"); - // Companion — Your starting deck contains only cards with odd converted mana costs and land cards. - // If a source you control with an odd converted mana cost would deal damage to a permanent or player, it deals double that damage to that permanent or player instead. - addCard(Zone.BATTLEFIELD, playerA, "Obosh, the Preypiercer"); - - // lose the flip - setFlipCoinResult(playerA, false); - - setStopAt(1, PhaseStep.PRECOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertLife(playerA, 20 - 3); - } +package org.mage.test.cards.single.iko; + + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author LevelX2 + */ + +public class OboshThePreypiercerTest extends CardTestPlayerBase { + + @Test + public void testZeroCMSIsHandledAsOdd() { + setStrictChooseMode(true); + // At the beginning of your upkeep, flip a coin. If you lose the flip, Mana Crypt deals 3 damage to you. + // {T}: Add {C}{C}. + addCard(Zone.BATTLEFIELD, playerA, "Mana Crypt"); + // Companion — Your starting deck contains only cards with odd converted mana costs and land cards. + // If a source you control with an odd converted mana cost would deal damage to a permanent or player, it deals double that damage to that permanent or player instead. + addCard(Zone.BATTLEFIELD, playerA, "Obosh, the Preypiercer"); + + // lose the flip + setFlipCoinResult(playerA, false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java index ddd6ca85e2..d94ae7d1b5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/SkycatSovereignTest.java @@ -1,90 +1,90 @@ -package org.mage.test.cards.single.iko; - -import mage.ObjectColor; -import mage.abilities.keyword.FlyingAbility; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class SkycatSovereignTest extends CardTestPlayerBase { - - @Test - public void test_BoostFromFlyers() { - setStrictChooseMode(true); - - // Flying - // Skycat Sovereign gets +1/+1 for each other creature you control with flying. - // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. - addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) - - setStopAt(1, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - assertPowerToughness(playerA, "Skycat Sovereign", 3, 3); - } - - /** - * Skycat Sovereign still gets +1/+1 for each creature that is supposed to have flying when there's an opposing Archetype of Imagination. - */ - @Test - public void test_NoBoostIfFlyingLost() { - setStrictChooseMode(true); - - // Flying - // Skycat Sovereign gets +1/+1 for each other creature you control with flying. - // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. - addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) - - // Creatures you control have flying. - // Creatures your opponents control lose flying and can't have or gain flying. - addCard(Zone.BATTLEFIELD, playerB, "Archetype of Imagination"); // - - setStopAt(1, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertAllCommandsUsed(); - assertPowerToughness(playerA, "Skycat Sovereign", 1, 1); - } - - @Test - public void test_BoostFromToken() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Plains"); - addCard(Zone.BATTLEFIELD, playerA, "Island", 3); - // Flying - // Skycat Sovereign gets +1/+1 for each other creature you control with flying. - // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. - addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) - - // Flying, vigilance - addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) - - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{W}{U}: Create"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Cat Bird", 1); - assertColor(playerA, "Cat Bird", ObjectColor.WHITE, true); - assertColor(playerA, "Cat Bird", ObjectColor.BLUE, false); - assertAbility(playerA, "Cat Bird", FlyingAbility.getInstance(), true); - - assertPowerToughness(playerA, "Skycat Sovereign", 4, 4); - } +package org.mage.test.cards.single.iko; + +import mage.ObjectColor; +import mage.abilities.keyword.FlyingAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class SkycatSovereignTest extends CardTestPlayerBase { + + @Test + public void test_BoostFromFlyers() { + setStrictChooseMode(true); + + // Flying + // Skycat Sovereign gets +1/+1 for each other creature you control with flying. + // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. + addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + assertPowerToughness(playerA, "Skycat Sovereign", 3, 3); + } + + /** + * Skycat Sovereign still gets +1/+1 for each creature that is supposed to have flying when there's an opposing Archetype of Imagination. + */ + @Test + public void test_NoBoostIfFlyingLost() { + setStrictChooseMode(true); + + // Flying + // Skycat Sovereign gets +1/+1 for each other creature you control with flying. + // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. + addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) + + // Creatures you control have flying. + // Creatures your opponents control lose flying and can't have or gain flying. + addCard(Zone.BATTLEFIELD, playerB, "Archetype of Imagination"); // + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAllCommandsUsed(); + assertPowerToughness(playerA, "Skycat Sovereign", 1, 1); + } + + @Test + public void test_BoostFromToken() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + // Flying + // Skycat Sovereign gets +1/+1 for each other creature you control with flying. + // {2}{W}{U}: Create a 1/1 white Cat Bird creature token with flying. + addCard(Zone.BATTLEFIELD, playerA, "Skycat Sovereign"); // Creature {W}{U} (1/1) + + // Flying, vigilance + addCard(Zone.BATTLEFIELD, playerA, "Abbey Griffin", 2); // Creature — Griffin (2/2) + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{W}{U}: Create"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Cat Bird", 1); + assertColor(playerA, "Cat Bird", ObjectColor.WHITE, true); + assertColor(playerA, "Cat Bird", ObjectColor.BLUE, false); + assertAbility(playerA, "Cat Bird", FlyingAbility.getInstance(), true); + + assertPowerToughness(playerA, "Skycat Sovereign", 4, 4); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java index 2ef638aea1..8626a0bc76 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh1/PlagueEngineerTest.java @@ -1,39 +1,39 @@ -package org.mage.test.cards.single.mh1; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PlagueEngineerTest extends CardTestPlayerBase { - - @Test - public void test_Standard() { - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); - // Deathtouch - // As Plague Engineer enters the battlefield, choose a creature type. - // Creatures of the chosen type your opponents control get -1/-1. - addCard(Zone.HAND, playerA, "Plague Engineer"); // Creature {2}{B} (2/2) - - addCard(Zone.BATTLEFIELD, playerA, "Defiant Elf", 2); //Creature - Elf (1/1) - addCard(Zone.BATTLEFIELD, playerB, "Defiant Elf", 2); //Creature - Elf (1/1) - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plague Engineer"); - setChoice(playerA, "Elf"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPermanentCount(playerA, "Plague Engineer", 1); - - assertPermanentCount(playerA, "Defiant Elf", 2); - assertGraveyardCount(playerB, "Defiant Elf", 2); - - assertLife(playerA, 20); - assertLife(playerB, 20); - } +package org.mage.test.cards.single.mh1; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PlagueEngineerTest extends CardTestPlayerBase { + + @Test + public void test_Standard() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // Deathtouch + // As Plague Engineer enters the battlefield, choose a creature type. + // Creatures of the chosen type your opponents control get -1/-1. + addCard(Zone.HAND, playerA, "Plague Engineer"); // Creature {2}{B} (2/2) + + addCard(Zone.BATTLEFIELD, playerA, "Defiant Elf", 2); //Creature - Elf (1/1) + addCard(Zone.BATTLEFIELD, playerB, "Defiant Elf", 2); //Creature - Elf (1/1) + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plague Engineer"); + setChoice(playerA, "Elf"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Plague Engineer", 1); + + assertPermanentCount(playerA, "Defiant Elf", 2); + assertGraveyardCount(playerB, "Defiant Elf", 2); + + assertLife(playerA, 20); + assertLife(playerB, 20); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java index 24990a6699..44104a5f8c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/som/NimDeathmantleTest.java @@ -1,51 +1,51 @@ -package org.mage.test.cards.single.som; - -import mage.abilities.keyword.IntimidateAbility; -import mage.constants.CardType; -import mage.constants.PhaseStep; -import mage.constants.SubType; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class NimDeathmantleTest extends CardTestPlayerBase { - - @Test - public void test_Basic() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); - // Equipped creature gets +2/+2, has intimidate, and is a black Zombie. - // Whenever a nontoken creature is put into your graveyard from the battlefield, you may pay {4}. If you do, return that card to the battlefield and attach Nim Deathmantle to it. - addCard(Zone.HAND, playerA, "Nim Deathmantle"); // Artifact Equipment {2} - - addCard(Zone.BATTLEFIELD, playerB, "Mountain"); - addCard(Zone.HAND, playerB, "Lightning Bolt"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nim Deathmantle"); - - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); - setChoice(playerA, true); // Message: Nim Deathmantle - Pay {4}? - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertGraveyardCount(playerB, "Lightning Bolt",1); - - assertPermanentCount(playerA, "Nim Deathmantle", 1); - - assertPowerToughness(playerA, "Silvercoat Lion", 4, 4); - assertAbility(playerA, "Silvercoat Lion", IntimidateAbility.getInstance(), true); - assertType("Silvercoat Lion", CardType.CREATURE, SubType.ZOMBIE); - - } +package org.mage.test.cards.single.som; + +import mage.abilities.keyword.IntimidateAbility; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class NimDeathmantleTest extends CardTestPlayerBase { + + @Test + public void test_Basic() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + // Equipped creature gets +2/+2, has intimidate, and is a black Zombie. + // Whenever a nontoken creature is put into your graveyard from the battlefield, you may pay {4}. If you do, return that card to the battlefield and attach Nim Deathmantle to it. + addCard(Zone.HAND, playerA, "Nim Deathmantle"); // Artifact Equipment {2} + + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nim Deathmantle"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); + setChoice(playerA, true); // Message: Nim Deathmantle - Pay {4}? + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Lightning Bolt",1); + + assertPermanentCount(playerA, "Nim Deathmantle", 1); + + assertPowerToughness(playerA, "Silvercoat Lion", 4, 4); + assertAbility(playerA, "Silvercoat Lion", IntimidateAbility.getInstance(), true); + assertType("Silvercoat Lion", CardType.CREATURE, SubType.ZOMBIE); + + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java index 4895bca974..206a3dea49 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/KardurDoomscourgeAndKithkinMourncallerTest.java @@ -1,103 +1,103 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.mage.test.cards.triggers; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author jeffwadsworth - */ -public class KardurDoomscourgeAndKithkinMourncallerTest extends CardTestPlayerBase { - - @Test - public void testKDRemovedFromCombatViaRegenerateAbility() { - setStrictChooseMode(true); - // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life - addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); - addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); - addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); - addCard(Zone.HAND, playerA, "Regenerate"); - addCard(Zone.HAND, playerA, "Terror"); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Regenerate", "Elvish Archers"); - - attack(1, playerA, "Elvish Archers"); - block(1, playerB, "Serra Angel", "Elvish Archers"); // regeneration shield used up and EA is removed from combat - - castSpell(1, PhaseStep.END_COMBAT, playerA, "Terror", "Elvish Archers"); // still within the combat phase, the EA is destroyed/dies - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Elvish Archers", 1); - - // does not fire due to the Elvish Archers not in an attacking state - assertLife(playerA, 20); - assertLife(playerB, 20); - - } - - @Test - public void testSuccessfulKDTrigger() { - setStrictChooseMode(true); - // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life - addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); - addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/2 first strike - addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance - - attack(1, playerA, "Elvish Archers"); - block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Archer dies causing KD to trigger - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Elvish Archers", 1); - - // successful fire so playerA gains 1 life and playerB loses 1 life - assertLife(playerA, 21); - assertLife(playerB, 19); - - } - - @Test - public void testKMTrigger() { - setStrictChooseMode(true); - // Kithkin Mourncaller: if an elf or kithkin dies, you may draw a card - addCard(Zone.BATTLEFIELD, playerA, "Kithkin Mourncaller"); - addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/1 first strike - addCard(Zone.BATTLEFIELD, playerA, "Pearled Unicorn"); // 2/2 - addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance - addCard(Zone.BATTLEFIELD, playerB, "Runeclaw Bear"); // 2/2 - addCard(Zone.LIBRARY, playerA, "Island", 2); // used for draw trigger - - attack(1, playerA, "Elvish Archers"); - attack(1, playerA, "Pearled Unicorn"); - block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Achers will die and trigger KM - block(1, playerB, "Runeclaw Bear", "Pearled Unicorn"); // Pearled Unicorn will die but not trigger KM - - setChoice(playerA, "Yes"); // accept the drawing of a card from the single trigger (Elvish Archers "elf type") - - setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); - execute(); - assertAllCommandsUsed(); - - assertGraveyardCount(playerA, "Elvish Archers", 1); - assertGraveyardCount(playerA, "Pearled Unicorn", 1); - - // successful fire due to dead Elvish Archers (elf) so playerA draws a card - assertHandCount(playerA, 1); - - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.cards.triggers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author jeffwadsworth + */ +public class KardurDoomscourgeAndKithkinMourncallerTest extends CardTestPlayerBase { + + @Test + public void testKDRemovedFromCombatViaRegenerateAbility() { + setStrictChooseMode(true); + // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life + addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); + addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); + addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); + addCard(Zone.HAND, playerA, "Regenerate"); + addCard(Zone.HAND, playerA, "Terror"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Regenerate", "Elvish Archers"); + + attack(1, playerA, "Elvish Archers"); + block(1, playerB, "Serra Angel", "Elvish Archers"); // regeneration shield used up and EA is removed from combat + + castSpell(1, PhaseStep.END_COMBAT, playerA, "Terror", "Elvish Archers"); // still within the combat phase, the EA is destroyed/dies + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Elvish Archers", 1); + + // does not fire due to the Elvish Archers not in an attacking state + assertLife(playerA, 20); + assertLife(playerB, 20); + + } + + @Test + public void testSuccessfulKDTrigger() { + setStrictChooseMode(true); + // Kardur, Doomscourge: if an attacking creature dies, each opponent loses 1 life and you gain 1 life + addCard(Zone.BATTLEFIELD, playerA, "Kardur, Doomscourge"); + addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/2 first strike + addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance + + attack(1, playerA, "Elvish Archers"); + block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Archer dies causing KD to trigger + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Elvish Archers", 1); + + // successful fire so playerA gains 1 life and playerB loses 1 life + assertLife(playerA, 21); + assertLife(playerB, 19); + + } + + @Test + public void testKMTrigger() { + setStrictChooseMode(true); + // Kithkin Mourncaller: if an elf or kithkin dies, you may draw a card + addCard(Zone.BATTLEFIELD, playerA, "Kithkin Mourncaller"); + addCard(Zone.BATTLEFIELD, playerA, "Elvish Archers"); // 2/1 first strike + addCard(Zone.BATTLEFIELD, playerA, "Pearled Unicorn"); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4 vigilance + addCard(Zone.BATTLEFIELD, playerB, "Runeclaw Bear"); // 2/2 + addCard(Zone.LIBRARY, playerA, "Island", 2); // used for draw trigger + + attack(1, playerA, "Elvish Archers"); + attack(1, playerA, "Pearled Unicorn"); + block(1, playerB, "Serra Angel", "Elvish Archers"); // Elvish Achers will die and trigger KM + block(1, playerB, "Runeclaw Bear", "Pearled Unicorn"); // Pearled Unicorn will die but not trigger KM + + setChoice(playerA, "Yes"); // accept the drawing of a card from the single trigger (Elvish Archers "elf type") + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Elvish Archers", 1); + assertGraveyardCount(playerA, "Pearled Unicorn", 1); + + // successful fire due to dead Elvish Archers (elf) so playerA draws a card + assertHandCount(playerA, 1); + + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java index ed02d06706..9de90e04f4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/AshenRiderTest.java @@ -1,68 +1,68 @@ -package org.mage.test.cards.triggers.dies; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ - -public class AshenRiderTest extends CardTestPlayerBase { - - /* - * Volrath, the Shapestealer and Ashen Rider, Ashen Rider has a counter on it: - Turn Volrath into Ashen Rider: - Destroy the Volrath (who's the Ashen Rider) with Putrefy: - The death trigger for the Volrath copying Ashen Rider did not trigger. - */ - @Test - public void cartelAristrocraftInteractionOpponentDoesNotPayLife() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - addCard(Zone.BATTLEFIELD, playerA, "Island", 2); - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); - - // Flying - // When Ashen Rider enters the battlefield or dies, exile target permanent. - addCard(Zone.BATTLEFIELD, playerA, "Ashen Rider"); // Creature {4}{W}{W}{B}{B} - - // At the beginning of combat on your turn, put a -1/-1 counter on up to one target creature. - // {1}: Until your next turn, Volrath, the Shapestealer becomes a copy of target creature with a counter on it, except it's 7/5 and it has this ability. - addCard(Zone.HAND, playerA, "Volrath, the Shapestealer"); // Creature {2}{B}{G}{U} - addTarget(playerA, "Ashen Rider"); - - // Destroy target artifact or creature. It can't be regenerated. - addCard(Zone.HAND, playerA, "Putrefy"); // Instant {1}{B}{G} - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volrath, the Shapestealer"); - - activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}: Until your next turn"); - addTarget(playerA, "Ashen Rider"); - - waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); - - castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Putrefy", "Ashen Rider[only copy]"); - - addTarget(playerA, "Silvercoat Lion"); // Dies trigger of Volrath, the Shapestealer copied from Ashen Rider - - setStopAt(3, PhaseStep.BEGIN_COMBAT); - execute(); - - assertAllCommandsUsed(); - - assertPowerToughness(playerA, "Ashen Rider", 4,4); - - assertGraveyardCount(playerA, "Putrefy", 1); - assertGraveyardCount(playerA, "Volrath, the Shapestealer", 1); - - assertExileCount(playerB, "Silvercoat Lion", 1); - - } -} +package org.mage.test.cards.triggers.dies; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ + +public class AshenRiderTest extends CardTestPlayerBase { + + /* + * Volrath, the Shapestealer and Ashen Rider, Ashen Rider has a counter on it: + Turn Volrath into Ashen Rider: + Destroy the Volrath (who's the Ashen Rider) with Putrefy: + The death trigger for the Volrath copying Ashen Rider did not trigger. + */ + @Test + public void cartelAristrocraftInteractionOpponentDoesNotPayLife() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + + // Flying + // When Ashen Rider enters the battlefield or dies, exile target permanent. + addCard(Zone.BATTLEFIELD, playerA, "Ashen Rider"); // Creature {4}{W}{W}{B}{B} + + // At the beginning of combat on your turn, put a -1/-1 counter on up to one target creature. + // {1}: Until your next turn, Volrath, the Shapestealer becomes a copy of target creature with a counter on it, except it's 7/5 and it has this ability. + addCard(Zone.HAND, playerA, "Volrath, the Shapestealer"); // Creature {2}{B}{G}{U} + addTarget(playerA, "Ashen Rider"); + + // Destroy target artifact or creature. It can't be regenerated. + addCard(Zone.HAND, playerA, "Putrefy"); // Instant {1}{B}{G} + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volrath, the Shapestealer"); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}: Until your next turn"); + addTarget(playerA, "Ashen Rider"); + + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Putrefy", "Ashen Rider[only copy]"); + + addTarget(playerA, "Silvercoat Lion"); // Dies trigger of Volrath, the Shapestealer copied from Ashen Rider + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Ashen Rider", 4,4); + + assertGraveyardCount(playerA, "Putrefy", 1); + assertGraveyardCount(playerA, "Volrath, the Shapestealer", 1); + + assertExileCount(playerB, "Silvercoat Lion", 1); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java index e3cf24ae1e..c4f23c1ebf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CommanderColorChangeTest.java @@ -1,122 +1,122 @@ -package org.mage.test.commander.duel; - -import java.io.FileNotFoundException; -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.GameException; -import mage.game.permanent.Permanent; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestCommanderDuelBase; - -/** - * - * @author LevelX2 - */ - -public class CommanderColorChangeTest extends CardTestCommanderDuelBase { - - @Override - protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - // When a player casts a spell or a creature attacks, exile Norin the Wary. Return it to the battlefield under its owner's control at the beginning of the next end step. - setDecknamePlayerA("CMDNorinTheWary.dck"); // Commander = Norin the Wary {R} - return super.createNewGameAndPlayers(); - } - - @Test - public void castCommanderWithAddedBlueColor() { - setStrictChooseMode(true); - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - - // As Painter's Servant enters the battlefield, choose a color. - // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. - addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} - - // Whenever a player casts a blue spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); - - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); - setChoice(playerA, "Blue"); - - // When a player casts a spell or a creature attacks, exile Norin the Wary. - // Return it to the battlefield under its owner's control at the beginning of the next end step. - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); - setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - assertAllCommandsUsed(); - - assertPermanentCount(playerA, "Norin the Wary", 1); - - Permanent norin = getPermanent("Norin the Wary", playerA); - Assert.assertEquals(true, norin.getColor(currentGame).isBlue()); - Assert.assertEquals(true, norin.getColor(currentGame).isRed()); - - Permanent kraken = getPermanent("Kraken's Eye", playerA); - Assert.assertEquals(true, kraken.getColor(currentGame).isBlue()); - - assertLife(playerA, 41); - assertLife(playerB, 40); - - } - - - /** - * I played a Painter's Servant, named black, but the other commanders get a extra colors - * Later it got removed but the commanders and some cards still have the extra color - * I played it again later, named green, and the previously affected cards get the extra color - * so now they have 2 extra colors and the commander get and additional color on top of that - * And finally I got the empty hand error #6738 on my turn for what I assume is the Painter's Servant + Grindstone combo I have, - * but nonetheless manage to tie the game so it go into a second game and the issue carry over, - * all the commanders have all the extra colors they gain from the first game - */ - - @Test - public void castCommanderWithoutAddedBlueColor() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - - // As Painter's Servant enters the battlefield, choose a color. - // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. - addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} - - // Whenever a player casts a blue spell, you may gain 1 life. - addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); - - - // Exile target artifact or enchantment. - addCard(Zone.HAND, playerB, "Altar's Light", 1); // Instant {2}{W}{W} - addCard(Zone.BATTLEFIELD, playerB, "Plains", 4); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); - setChoice(playerA, "Blue"); - - // When a player casts a spell or a creature attacks, exile Norin the Wary. - // Return it to the battlefield under its owner's control at the beginning of the next end step. - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); - setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Altar's Light", "Painter's Servant", "Norin the Wary"); - setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPermanentCount(playerA, "Norin the Wary", 1); - assertAllCommandsUsed(); - - Permanent norin = getPermanent("Norin the Wary", playerA); - Assert.assertEquals(false, norin.getColor(currentGame).isBlue()); - Assert.assertEquals(true, norin.getColor(currentGame).isRed()); - - Permanent kraken = getPermanent("Kraken's Eye", playerA); - Assert.assertEquals(false, kraken.getColor(currentGame).isBlue()); - - assertLife(playerA, 42); - assertLife(playerB, 40); - - } -} +package org.mage.test.commander.duel; + +import java.io.FileNotFoundException; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.GameException; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommanderDuelBase; + +/** + * + * @author LevelX2 + */ + +public class CommanderColorChangeTest extends CardTestCommanderDuelBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // When a player casts a spell or a creature attacks, exile Norin the Wary. Return it to the battlefield under its owner's control at the beginning of the next end step. + setDecknamePlayerA("CMDNorinTheWary.dck"); // Commander = Norin the Wary {R} + return super.createNewGameAndPlayers(); + } + + @Test + public void castCommanderWithAddedBlueColor() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // As Painter's Servant enters the battlefield, choose a color. + // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. + addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} + + // Whenever a player casts a blue spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); + setChoice(playerA, "Blue"); + + // When a player casts a spell or a creature attacks, exile Norin the Wary. + // Return it to the battlefield under its owner's control at the beginning of the next end step. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); + setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Norin the Wary", 1); + + Permanent norin = getPermanent("Norin the Wary", playerA); + Assert.assertEquals(true, norin.getColor(currentGame).isBlue()); + Assert.assertEquals(true, norin.getColor(currentGame).isRed()); + + Permanent kraken = getPermanent("Kraken's Eye", playerA); + Assert.assertEquals(true, kraken.getColor(currentGame).isBlue()); + + assertLife(playerA, 41); + assertLife(playerB, 40); + + } + + + /** + * I played a Painter's Servant, named black, but the other commanders get a extra colors + * Later it got removed but the commanders and some cards still have the extra color + * I played it again later, named green, and the previously affected cards get the extra color + * so now they have 2 extra colors and the commander get and additional color on top of that + * And finally I got the empty hand error #6738 on my turn for what I assume is the Painter's Servant + Grindstone combo I have, + * but nonetheless manage to tie the game so it go into a second game and the issue carry over, + * all the commanders have all the extra colors they gain from the first game + */ + + @Test + public void castCommanderWithoutAddedBlueColor() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // As Painter's Servant enters the battlefield, choose a color. + // All cards that aren't on the battlefield, spells, and permanents are the chosen color in addition to their other colors. + addCard(Zone.HAND, playerA, "Painter's Servant", 1); // Artifact Creature {2} + + // Whenever a player casts a blue spell, you may gain 1 life. + addCard(Zone.BATTLEFIELD, playerA, "Kraken's Eye", 1); + + + // Exile target artifact or enchantment. + addCard(Zone.HAND, playerB, "Altar's Light", 1); // Instant {2}{W}{W} + addCard(Zone.BATTLEFIELD, playerB, "Plains", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Painter's Servant"); + setChoice(playerA, "Blue"); + + // When a player casts a spell or a creature attacks, exile Norin the Wary. + // Return it to the battlefield under its owner's control at the beginning of the next end step. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Norin the Wary"); + setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Altar's Light", "Painter's Servant", "Norin the Wary"); + setChoice(playerA, true); // Whenever a player casts a blue spell, you may gain 1 life. Choices: Yes - No + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Norin the Wary", 1); + assertAllCommandsUsed(); + + Permanent norin = getPermanent("Norin the Wary", playerA); + Assert.assertEquals(false, norin.getColor(currentGame).isBlue()); + Assert.assertEquals(true, norin.getColor(currentGame).isRed()); + + Permanent kraken = getPermanent("Kraken's Eye", playerA); + Assert.assertEquals(false, kraken.getColor(currentGame).isBlue()); + + assertLife(playerA, 42); + assertLife(playerB, 40); + + } +} diff --git a/Mage/src/main/java/mage/ApprovingObject.java b/Mage/src/main/java/mage/ApprovingObject.java index 3701880d71..9468aad157 100644 --- a/Mage/src/main/java/mage/ApprovingObject.java +++ b/Mage/src/main/java/mage/ApprovingObject.java @@ -1,28 +1,28 @@ -package mage; - -import mage.abilities.Ability; -import mage.game.Game; - -/** - * - * @author LevelX2 - */ -public class ApprovingObject { - - private final Ability approvingAbility; - private final MageObjectReference approvingMageObjectReference; - - public ApprovingObject(Ability source, Game game) { - this.approvingAbility = source; - this.approvingMageObjectReference = new MageObjectReference(source.getSourceId(), game); - } - - public Ability getApprovingAbility() { - return approvingAbility; - } - - public MageObjectReference getApprovingMageObjectReference() { - return approvingMageObjectReference; - } - -} +package mage; + +import mage.abilities.Ability; +import mage.game.Game; + +/** + * + * @author LevelX2 + */ +public class ApprovingObject { + + private final Ability approvingAbility; + private final MageObjectReference approvingMageObjectReference; + + public ApprovingObject(Ability source, Game game) { + this.approvingAbility = source; + this.approvingMageObjectReference = new MageObjectReference(source.getSourceId(), game); + } + + public Ability getApprovingAbility() { + return approvingAbility; + } + + public MageObjectReference getApprovingMageObjectReference() { + return approvingMageObjectReference; + } + +} diff --git a/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java index 58f656fdd8..8f83c2834c 100644 --- a/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/AttackingCreaturePutIntoGraveyardTriggeredAbility.java @@ -1,124 +1,124 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package mage.abilities.common; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; -import mage.constants.Zone; -import mage.filter.FilterPermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import static mage.game.events.GameEvent.EventType.END_COMBAT_STEP_POST; -import static mage.game.events.GameEvent.EventType.REMOVED_FROM_COMBAT; -import static mage.game.events.GameEvent.EventType.ZONE_CHANGE; -import mage.game.events.ZoneChangeEvent; -import mage.game.permanent.Permanent; - -/** - * - * @author weirddan455 and jeffwadsworth - */ -public class AttackingCreaturePutIntoGraveyardTriggeredAbility extends TriggeredAbilityImpl { - - protected FilterPermanent filterPermanent; - private final boolean onlyToControllerGraveyard; - private final boolean itDies; - - public AttackingCreaturePutIntoGraveyardTriggeredAbility(Effect effect, FilterPermanent filterPermanent, Boolean onlyToControllerGraveyard, Boolean itDies, Boolean optional) { - super(Zone.BATTLEFIELD, effect, optional); - this.filterPermanent = filterPermanent; - this.onlyToControllerGraveyard = onlyToControllerGraveyard; - this.itDies = itDies; - } - - private AttackingCreaturePutIntoGraveyardTriggeredAbility(final AttackingCreaturePutIntoGraveyardTriggeredAbility ability) { - super(ability); - this.filterPermanent = ability.filterPermanent; - this.onlyToControllerGraveyard = ability.onlyToControllerGraveyard; - this.itDies = ability.itDies; - } - - @Override - public AttackingCreaturePutIntoGraveyardTriggeredAbility copy() { - return new AttackingCreaturePutIntoGraveyardTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - switch (event.getType()) { - case ATTACKER_DECLARED: - case END_COMBAT_STEP_POST: - case ZONE_CHANGE: - case REMOVED_FROM_COMBAT: - return true; - default: - return false; - } - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - switch (event.getType()) { - case ATTACKER_DECLARED: - Permanent permanent = game.getPermanent(event.getSourceId()); - if (permanent != null - && !filterPermanent.match(permanent, game)) { - return false; - } - List attackersList = new ArrayList<>(); - List attackersListCopy = (List) game.getState().getValue(this.getSourceId() + "Attackers"); - if (attackersListCopy == null) { - attackersListCopy = attackersList; - } - attackersListCopy.add(event.getSourceId()); // add the filtered creature to the list - game.getState().setValue(this.getSourceId() + "Attackers", attackersListCopy); - return false; - case END_COMBAT_STEP_POST: - game.getState().setValue(this.getSourceId() + "Attackers", null); - return false; - case ZONE_CHANGE: - ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (zEvent.getFromZone() == Zone.BATTLEFIELD - && zEvent.getToZone() == Zone.GRAVEYARD) { - if (onlyToControllerGraveyard - && !this.isControlledBy(game.getOwnerId(zEvent.getTargetId()))) { - return false; - } - if (itDies - && !zEvent.isDiesEvent()) { - return false; - } - List attackers = (List) game.getState().getValue(this.getSourceId() + "Attackers"); - return attackers != null - && attackers.contains(zEvent.getTargetId()); - } - case REMOVED_FROM_COMBAT: - // a card removed from combat is no longer an attacker or blocker so remove it from the list - List attackersListRFC = (List) game.getState().getValue(this.getSourceId() + "Attackers"); - if (attackersListRFC != null - && attackersListRFC.contains(event.getTargetId())) { - attackersListRFC.remove(event.getTargetId()); - game.getState().setValue(this.getSourceId() + "Attackers", attackersListRFC); - } - - default: - return false; - } - } - - @Override - public String getTriggerPhrase() { - if (itDies) { - return "Whenever " + filterPermanent.getMessage() + " dies, "; - } - return "Whenever " + filterPermanent.getMessage() + " is put into " + (onlyToControllerGraveyard ? "your" : "a") - + " graveyard from the battlefield, "; - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.abilities.common; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import static mage.game.events.GameEvent.EventType.END_COMBAT_STEP_POST; +import static mage.game.events.GameEvent.EventType.REMOVED_FROM_COMBAT; +import static mage.game.events.GameEvent.EventType.ZONE_CHANGE; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; + +/** + * + * @author weirddan455 and jeffwadsworth + */ +public class AttackingCreaturePutIntoGraveyardTriggeredAbility extends TriggeredAbilityImpl { + + protected FilterPermanent filterPermanent; + private final boolean onlyToControllerGraveyard; + private final boolean itDies; + + public AttackingCreaturePutIntoGraveyardTriggeredAbility(Effect effect, FilterPermanent filterPermanent, Boolean onlyToControllerGraveyard, Boolean itDies, Boolean optional) { + super(Zone.BATTLEFIELD, effect, optional); + this.filterPermanent = filterPermanent; + this.onlyToControllerGraveyard = onlyToControllerGraveyard; + this.itDies = itDies; + } + + private AttackingCreaturePutIntoGraveyardTriggeredAbility(final AttackingCreaturePutIntoGraveyardTriggeredAbility ability) { + super(ability); + this.filterPermanent = ability.filterPermanent; + this.onlyToControllerGraveyard = ability.onlyToControllerGraveyard; + this.itDies = ability.itDies; + } + + @Override + public AttackingCreaturePutIntoGraveyardTriggeredAbility copy() { + return new AttackingCreaturePutIntoGraveyardTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + switch (event.getType()) { + case ATTACKER_DECLARED: + case END_COMBAT_STEP_POST: + case ZONE_CHANGE: + case REMOVED_FROM_COMBAT: + return true; + default: + return false; + } + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + switch (event.getType()) { + case ATTACKER_DECLARED: + Permanent permanent = game.getPermanent(event.getSourceId()); + if (permanent != null + && !filterPermanent.match(permanent, game)) { + return false; + } + List attackersList = new ArrayList<>(); + List attackersListCopy = (List) game.getState().getValue(this.getSourceId() + "Attackers"); + if (attackersListCopy == null) { + attackersListCopy = attackersList; + } + attackersListCopy.add(event.getSourceId()); // add the filtered creature to the list + game.getState().setValue(this.getSourceId() + "Attackers", attackersListCopy); + return false; + case END_COMBAT_STEP_POST: + game.getState().setValue(this.getSourceId() + "Attackers", null); + return false; + case ZONE_CHANGE: + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.getFromZone() == Zone.BATTLEFIELD + && zEvent.getToZone() == Zone.GRAVEYARD) { + if (onlyToControllerGraveyard + && !this.isControlledBy(game.getOwnerId(zEvent.getTargetId()))) { + return false; + } + if (itDies + && !zEvent.isDiesEvent()) { + return false; + } + List attackers = (List) game.getState().getValue(this.getSourceId() + "Attackers"); + return attackers != null + && attackers.contains(zEvent.getTargetId()); + } + case REMOVED_FROM_COMBAT: + // a card removed from combat is no longer an attacker or blocker so remove it from the list + List attackersListRFC = (List) game.getState().getValue(this.getSourceId() + "Attackers"); + if (attackersListRFC != null + && attackersListRFC.contains(event.getTargetId())) { + attackersListRFC.remove(event.getTargetId()); + game.getState().setValue(this.getSourceId() + "Attackers", attackersListRFC); + } + + default: + return false; + } + } + + @Override + public String getTriggerPhrase() { + if (itDies) { + return "Whenever " + filterPermanent.getMessage() + " dies, "; + } + return "Whenever " + filterPermanent.getMessage() + " is put into " + (onlyToControllerGraveyard ? "your" : "a") + + " graveyard from the battlefield, "; + } + +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java index 80fc43a452..a45b5d5eaf 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/DidNotAttackThisTurnEnchantedCondition.java @@ -1,31 +1,31 @@ -package mage.abilities.condition.common; - -import mage.MageObjectReference; -import mage.abilities.Ability; -import mage.abilities.condition.Condition; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.watchers.common.AttackedThisTurnWatcher; - -/** - * - * @author jeffwadsworth - */ -public enum DidNotAttackThisTurnEnchantedCondition implements Condition { - - instance; - - @Override - public boolean apply(Game game, Ability source) { - Permanent auraPermanent = game.getPermanentOrLKIBattlefield(source.getSourceId()); - if (auraPermanent != null) { - Permanent enchantedPermanent = game.getPermanent(auraPermanent.getAttachedTo()); - AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class); - return enchantedPermanent != null - && watcher != null - && !watcher.getAttackedThisTurnCreatures().contains( - new MageObjectReference(enchantedPermanent, game)); - } - return false; - } -} +package mage.abilities.condition.common; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.watchers.common.AttackedThisTurnWatcher; + +/** + * + * @author jeffwadsworth + */ +public enum DidNotAttackThisTurnEnchantedCondition implements Condition { + + instance; + + @Override + public boolean apply(Game game, Ability source) { + Permanent auraPermanent = game.getPermanentOrLKIBattlefield(source.getSourceId()); + if (auraPermanent != null) { + Permanent enchantedPermanent = game.getPermanent(auraPermanent.getAttachedTo()); + AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class); + return enchantedPermanent != null + && watcher != null + && !watcher.getAttackedThisTurnCreatures().contains( + new MageObjectReference(enchantedPermanent, game)); + } + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java index d6a3138440..351dcea28b 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerLifeDividedValue.java @@ -1,48 +1,48 @@ -package mage.abilities.dynamicvalue.common; - -import mage.abilities.Ability; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; -import mage.game.Game; -import mage.players.Player; - -/** - * - * @author LevelX2 - */ -public class ControllerLifeDividedValue implements DynamicValue { - - private final Integer divider; - - public ControllerLifeDividedValue(Integer divider) { - this.divider = divider; - } - - public ControllerLifeDividedValue(final ControllerLifeDividedValue dynamicValue) { - this.divider = dynamicValue.divider; - } - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - Player p = game.getPlayer(sourceAbility.getControllerId()); - if (p != null) { - return p.getLife() / divider; - } - return 0; - } - - @Override - public ControllerLifeDividedValue copy() { - return new ControllerLifeDividedValue(this); - } - - @Override - public String toString() { - return "X"; - } - - @Override - public String getMessage() { - return ""; - } -} +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author LevelX2 + */ +public class ControllerLifeDividedValue implements DynamicValue { + + private final Integer divider; + + public ControllerLifeDividedValue(Integer divider) { + this.divider = divider; + } + + public ControllerLifeDividedValue(final ControllerLifeDividedValue dynamicValue) { + this.divider = dynamicValue.divider; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Player p = game.getPlayer(sourceAbility.getControllerId()); + if (p != null) { + return p.getLife() / divider; + } + return 0; + } + + @Override + public ControllerLifeDividedValue copy() { + return new ControllerLifeDividedValue(this); + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return ""; + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java index 338e4b9b51..65bac59159 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/HighestCMCOfPermanentValue.java @@ -1,62 +1,62 @@ -package mage.abilities.dynamicvalue.common; - -import mage.abilities.Ability; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; -import mage.filter.FilterPermanent; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; - -/** - * - * @author LevelX2 - */ -public class HighestCMCOfPermanentValue implements DynamicValue { - - private final FilterPermanent filter; - private final boolean onlyIfCanBeSacrificed; - - public HighestCMCOfPermanentValue(FilterPermanent filter, boolean onlyIfCanBeSacrificed) { - super(); - this.filter = filter; - this.onlyIfCanBeSacrificed = onlyIfCanBeSacrificed; - } - - public HighestCMCOfPermanentValue(final HighestCMCOfPermanentValue dynamicValue) { - this.filter = dynamicValue.filter; - this.onlyIfCanBeSacrificed = dynamicValue.onlyIfCanBeSacrificed; - } - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - int value = 0; - Player controller = game.getPlayer(sourceAbility.getControllerId()); - if (controller != null) { - for (Permanent permanent : game.getBattlefield() - .getActivePermanents(filter, sourceAbility.getControllerId(), sourceAbility.getSourceId(), game)) { - if ((!onlyIfCanBeSacrificed || controller.canPaySacrificeCost(permanent, sourceAbility, sourceAbility.getControllerId(), game)) - && permanent.getManaValue() > value) { - value = permanent.getManaValue(); - } - - } - } - return value; - } - - @Override - public HighestCMCOfPermanentValue copy() { - return new HighestCMCOfPermanentValue(this); - } - - @Override - public String toString() { - return "X"; - } - - @Override - public String getMessage() { - return filter.getMessage(); - } -} +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * + * @author LevelX2 + */ +public class HighestCMCOfPermanentValue implements DynamicValue { + + private final FilterPermanent filter; + private final boolean onlyIfCanBeSacrificed; + + public HighestCMCOfPermanentValue(FilterPermanent filter, boolean onlyIfCanBeSacrificed) { + super(); + this.filter = filter; + this.onlyIfCanBeSacrificed = onlyIfCanBeSacrificed; + } + + public HighestCMCOfPermanentValue(final HighestCMCOfPermanentValue dynamicValue) { + this.filter = dynamicValue.filter; + this.onlyIfCanBeSacrificed = dynamicValue.onlyIfCanBeSacrificed; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + int value = 0; + Player controller = game.getPlayer(sourceAbility.getControllerId()); + if (controller != null) { + for (Permanent permanent : game.getBattlefield() + .getActivePermanents(filter, sourceAbility.getControllerId(), sourceAbility.getSourceId(), game)) { + if ((!onlyIfCanBeSacrificed || controller.canPaySacrificeCost(permanent, sourceAbility, sourceAbility.getControllerId(), game)) + && permanent.getManaValue() > value) { + value = permanent.getManaValue(); + } + + } + } + return value; + } + + @Override + public HighestCMCOfPermanentValue copy() { + return new HighestCMCOfPermanentValue(this); + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return filter.getMessage(); + } +} diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index d6c8c2a37b..6a2eb625d3 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1,5084 +1,5084 @@ -package mage.players; - -import com.google.common.collect.ImmutableMap; -import mage.*; -import mage.abilities.*; -import mage.abilities.ActivatedAbility.ActivationStatus; -import mage.abilities.common.PassAbility; -import mage.abilities.common.PlayLandAsCommanderAbility; -import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; -import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; -import mage.abilities.costs.*; -import mage.abilities.costs.mana.AlternateManaPaymentAbility; -import mage.abilities.costs.mana.ManaCost; -import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.RestrictionEffect; -import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; -import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; -import mage.abilities.keyword.*; -import mage.abilities.mana.ActivatedManaAbilityImpl; -import mage.abilities.mana.ManaOptions; -import mage.actions.MageDrawAction; -import mage.cards.*; -import mage.cards.decks.Deck; -import mage.choices.Choice; -import mage.choices.ChoiceImpl; -import mage.constants.*; -import mage.counters.Counter; -import mage.counters.CounterType; -import mage.counters.Counters; -import mage.designations.Designation; -import mage.designations.DesignationType; -import mage.filter.FilterCard; -import mage.filter.FilterMana; -import mage.filter.FilterPermanent; -import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.common.FilterCreatureForCombat; -import mage.filter.common.FilterCreatureForCombatBlock; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.permanent.PermanentIdPredicate; -import mage.game.*; -import mage.game.combat.CombatGroup; -import mage.game.command.CommandObject; -import mage.game.events.*; -import mage.game.match.MatchPlayer; -import mage.game.permanent.Permanent; -import mage.game.permanent.PermanentCard; -import mage.game.permanent.PermanentToken; -import mage.game.permanent.token.SquirrelToken; -import mage.game.stack.Spell; -import mage.game.stack.StackAbility; -import mage.game.stack.StackObject; -import mage.game.turn.Step; -import mage.players.net.UserData; -import mage.target.Target; -import mage.target.TargetAmount; -import mage.target.TargetCard; -import mage.target.TargetPermanent; -import mage.target.common.TargetCardInLibrary; -import mage.target.common.TargetDiscard; -import mage.util.CardUtil; -import mage.util.GameLog; -import mage.util.RandomUtil; -import org.apache.log4j.Logger; - -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -public abstract class PlayerImpl implements Player, Serializable { - - private static final Logger logger = Logger.getLogger(PlayerImpl.class); - - /** - * Used to cancel waiting requests send to the player - */ - protected boolean abort; - - protected final UUID playerId; - protected String name; - protected boolean human; - protected int life; - protected boolean wins; - protected boolean draws; - protected boolean loses; - protected Library library; - protected Cards sideboard; - protected Cards hand; - protected Graveyard graveyard; - protected Set commandersIds = new HashSet<>(0); - protected Abilities abilities; - protected Counters counters; - protected int landsPlayed; - protected int landsPerTurn = 1; - protected int loyaltyUsePerTurn = 1; - protected int maxHandSize = 7; - protected int maxAttackedBy = Integer.MAX_VALUE; - protected ManaPool manaPool; - // priority control - protected boolean passed; // player passed priority - protected boolean passedTurn; // F4 - protected boolean passedTurnSkipStack; // F6 // TODO: research - protected boolean passedUntilEndOfTurn; // F5 - protected boolean passedUntilNextMain; // F7 - protected boolean passedUntilStackResolved; // F10 - protected Date dateLastAddedToStack; - protected boolean passedUntilEndStepBeforeMyTurn; // F11 - protected boolean skippedAtLeastOnce; // used to track if passed started in specific phase - /** - * This indicates that player passed all turns until their own turn starts - * (F9). Note! This differs from passedTurn as it doesn't care about spells - * and abilities in the stack and will pass them as well. - */ - protected boolean passedAllTurns; // F9 - protected AbilityType justActivatedType; // used to check if priority can be passed automatically - - protected int turns; - protected int storedBookmark = -1; - protected int priorityTimeLeft = Integer.MAX_VALUE; - - // conceded or connection lost game - protected boolean left; - // set if the player quits the complete match - protected boolean quit; - // set if the player lost match because of priority timeout - protected boolean timerTimeout; - // set if the player lost match because of idle timeout - protected boolean idleTimeout; - - protected RangeOfInfluence range; - protected Set inRange = new HashSet<>(); // players list in current range of influence (updates each turn) - - protected boolean isTestMode = false; - protected boolean canGainLife = true; - protected boolean canLoseLife = true; - protected boolean canPayLifeCost = true; - protected boolean loseByZeroOrLessLife = true; - protected boolean canPlayCardsFromGraveyard = true; - protected boolean drawsOnOpponentsTurn = false; - - protected FilterPermanent sacrificeCostFilter; - - protected final List alternativeSourceCosts = new ArrayList<>(); - - protected boolean isGameUnderControl = true; - protected UUID turnController; - protected List turnControllers = new ArrayList<>(); - protected Set playersUnderYourControl = new HashSet<>(); - - protected Set usersAllowedToSeeHandCards = new HashSet<>(); - - protected List attachments = new ArrayList<>(); - - protected boolean topCardRevealed = false; - - // 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn - // or until a specific point in that turn will last until that turn would have begun. - // They neither expire immediately nor last indefinitely. - protected boolean reachedNextTurnAfterLeaving = false; - - // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) - // support multiple cards with alternative mana cost - protected Set castSourceIdWithAlternateMana = new HashSet<>(); - protected Map> castSourceIdManaCosts = new HashMap<>(); - protected Map> castSourceIdCosts = new HashMap<>(); - - // indicates that the player is in mana payment phase - protected boolean payManaMode = false; - - protected UserData userData; - protected MatchPlayer matchPlayer; - - protected List designations = new ArrayList<>(); - - // mana colors the player can handle like Phyrexian mana - protected FilterMana phyrexianColors; - - // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) - protected final List> availableTriggeredManaList = new ArrayList<>(); - - /** - * During some steps we can't play anything - */ - protected final Map silentPhaseSteps = ImmutableMap.builder(). - put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); - - public PlayerImpl(String name, RangeOfInfluence range) { - this(UUID.randomUUID()); - this.name = name; - this.range = range; - hand = new CardsImpl(); - graveyard = new Graveyard(); - abilities = new AbilitiesImpl<>(); - counters = new Counters(); - manaPool = new ManaPool(playerId); - library = new Library(playerId); - sideboard = new CardsImpl(); - phyrexianColors = null; - } - - protected PlayerImpl(UUID id) { - this.playerId = id; - } - - public PlayerImpl(final PlayerImpl player) { - this.abort = player.abort; - this.playerId = player.playerId; - - this.name = player.name; - this.human = player.human; - this.life = player.life; - this.wins = player.wins; - this.draws = player.draws; - this.loses = player.loses; - - this.library = player.library.copy(); - this.sideboard = player.sideboard.copy(); - this.hand = player.hand.copy(); - this.graveyard = player.graveyard.copy(); - this.commandersIds = player.commandersIds; - this.abilities = player.abilities.copy(); - this.counters = player.counters.copy(); - - this.landsPlayed = player.landsPlayed; - this.landsPerTurn = player.landsPerTurn; - this.loyaltyUsePerTurn = player.loyaltyUsePerTurn; - this.maxHandSize = player.maxHandSize; - this.maxAttackedBy = player.maxAttackedBy; - this.manaPool = player.manaPool.copy(); - this.turns = player.turns; - - this.left = player.left; - this.quit = player.quit; - this.timerTimeout = player.timerTimeout; - this.idleTimeout = player.idleTimeout; - this.range = player.range; - this.canGainLife = player.canGainLife; - this.canLoseLife = player.canLoseLife; - this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; - this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; - this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; - - this.attachments.addAll(player.attachments); - - this.inRange.addAll(player.inRange); - this.userData = player.userData; - this.matchPlayer = player.matchPlayer; - - this.canPayLifeCost = player.canPayLifeCost; - this.sacrificeCostFilter = player.sacrificeCostFilter; - this.alternativeSourceCosts.addAll(player.alternativeSourceCosts); - this.storedBookmark = player.storedBookmark; - - this.topCardRevealed = player.topCardRevealed; - this.playersUnderYourControl.addAll(player.playersUnderYourControl); - this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); - - this.isTestMode = player.isTestMode; - this.isGameUnderControl = player.isGameUnderControl; - - this.turnController = player.turnController; - this.turnControllers.addAll(player.turnControllers); - - this.passed = player.passed; - this.passedTurn = player.passedTurn; - this.passedTurnSkipStack = player.passedTurnSkipStack; - this.passedUntilEndOfTurn = player.passedUntilEndOfTurn; - this.passedUntilNextMain = player.passedUntilNextMain; - this.passedUntilStackResolved = player.passedUntilStackResolved; - this.dateLastAddedToStack = player.dateLastAddedToStack; - this.passedUntilEndStepBeforeMyTurn = player.passedUntilEndStepBeforeMyTurn; - this.skippedAtLeastOnce = player.skippedAtLeastOnce; - this.passedAllTurns = player.passedAllTurns; - this.justActivatedType = player.justActivatedType; - - this.priorityTimeLeft = player.getPriorityTimeLeft(); - this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; - - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - this.payManaMode = player.payManaMode; - this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; - for (Designation object : player.designations) { - this.designations.add(object.copy()); - } - } - - @Override - public void restore(Player player) { - this.name = player.getName(); - this.human = player.isHuman(); - this.life = player.getLife(); - - this.passed = player.isPassed(); - - // Don't restore more global states. If restored they are probably cause for unintended draws (https://github.com/magefree/mage/issues/1205). -// this.wins = player.hasWon(); -// this.loses = player.hasLost(); -// this.left = player.hasLeft(); -// this.quit = player.hasQuit(); - // Makes no sense to restore -// this.priorityTimeLeft = player.getPriorityTimeLeft(); -// this.idleTimeout = player.hasIdleTimeout(); -// this.timerTimeout = player.hasTimerTimeout(); - // can't change so no need to restore -// this.isTestMode = player.isTestMode(); - // This is meta data and should'nt be restored by rollback -// this.userData = player.getUserData(); - this.library = player.getLibrary().copy(); - this.sideboard = player.getSideboard().copy(); - this.hand = player.getHand().copy(); - this.graveyard = player.getGraveyard().copy(); - - //noinspection deprecation - it's ok to use it in inner methods - this.commandersIds = new HashSet<>(player.getCommandersIds()); - - this.abilities = player.getAbilities().copy(); - this.counters = player.getCounters().copy(); - - this.landsPlayed = player.getLandsPlayed(); - this.landsPerTurn = player.getLandsPerTurn(); - this.loyaltyUsePerTurn = player.getLoyaltyUsePerTurn(); - this.maxHandSize = player.getMaxHandSize(); - this.maxAttackedBy = player.getMaxAttackedBy(); - this.manaPool = player.getManaPool().copy(); - // Restore user specific settings in case changed since state save - this.manaPool.setAutoPayment(this.getUserData().isManaPoolAutomatic()); - this.manaPool.setAutoPaymentRestricted(this.getUserData().isManaPoolAutomaticRestricted()); - - this.turns = player.getTurns(); - - this.range = player.getRange(); - this.canGainLife = player.isCanGainLife(); - this.canLoseLife = player.isCanLoseLife(); - this.attachments.clear(); - this.attachments.addAll(player.getAttachments()); - - this.inRange.clear(); - this.inRange.addAll(player.getInRange()); - this.canPayLifeCost = player.getCanPayLifeCost(); - this.sacrificeCostFilter = player.getSacrificeCostFilter() != null - ? player.getSacrificeCostFilter().copy() : null; - this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); - this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); - this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); - this.alternativeSourceCosts.clear(); - this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); - - this.topCardRevealed = player.isTopCardRevealed(); - this.playersUnderYourControl.clear(); - this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); - this.isGameUnderControl = player.isGameUnderControl(); - - this.turnController = player.getTurnControlledBy(); - this.turnControllers.clear(); - this.turnControllers.addAll(player.getTurnControllers()); - this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); - - this.clearCastSourceIdManaCosts(); - this.castSourceIdWithAlternateMana.clear(); - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); - } - - this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; - - this.designations.clear(); - for (Designation object : player.getDesignations()) { - this.designations.add(object.copy()); - } - - // Don't restore! - // this.storedBookmark - // this.usersAllowedToSeeHandCards - } - - @Override - public void useDeck(Deck deck, Game game) { - library.clear(); - library.addAll(deck.getCards(), game); - sideboard.clear(); - for (Card card : deck.getSideboard()) { - sideboard.add(card); - } - } - - /** - * Cast e.g. from Karn Liberated to restart the current game - * - * @param game - */ - @Override - public void init(Game game) { - init(game, false); - } - - @Override - public void init(Game game, boolean testMode) { - this.abort = false; - if (!testMode) { - this.hand.clear(); - this.graveyard.clear(); - } - this.library.reset(); - this.abilities.clear(); - this.counters.clear(); - this.wins = false; - this.draws = false; - this.loses = false; - this.left = false; - // reset is necessary because in tournament player will be used for each round - this.quit = false; - this.timerTimeout = false; - this.idleTimeout = false; - - this.turns = 0; - this.isGameUnderControl = true; - this.turnController = this.getId(); - this.turnControllers.clear(); - this.playersUnderYourControl.clear(); - - this.passed = false; - this.passedTurn = false; - this.passedTurnSkipStack = false; - this.passedUntilEndOfTurn = false; - this.passedUntilNextMain = false; - this.passedUntilStackResolved = false; - this.dateLastAddedToStack = null; - this.passedUntilEndStepBeforeMyTurn = false; - this.skippedAtLeastOnce = false; - this.passedAllTurns = false; - this.justActivatedType = null; - - this.canGainLife = true; - this.canLoseLife = true; - this.topCardRevealed = false; - this.payManaMode = false; - this.setLife(game.getStartingLife(), game, null); - this.setReachedNextTurnAfterLeaving(false); - - this.clearCastSourceIdManaCosts(); - - this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left - this.phyrexianColors = null; - - this.designations.clear(); - } - - /** - * called before apply effects - */ - @Override - public void reset() { - this.abilities.clear(); - this.landsPerTurn = 1; - this.loyaltyUsePerTurn = 1; - this.maxHandSize = 7; - this.maxAttackedBy = Integer.MAX_VALUE; - this.canGainLife = true; - this.canLoseLife = true; - this.canPayLifeCost = true; - this.sacrificeCostFilter = null; - this.loseByZeroOrLessLife = true; - this.canPlayCardsFromGraveyard = false; - this.drawsOnOpponentsTurn = false; - this.topCardRevealed = false; - this.alternativeSourceCosts.clear(); - this.clearCastSourceIdManaCosts(); - this.getManaPool().clearEmptyManaPoolRules(); - this.phyrexianColors = null; - } - - @Override - public Counters getCounters() { - return counters; - } - - @Override - public void beginTurn(Game game) { - this.landsPlayed = 0; - updateRange(game); - } - - @Override - public RangeOfInfluence getRange() { - return range; - } - - @Override - public void updateRange(Game game) { - // 20100423 - 801.2c - // 801.2c The particular players within each player’s range of influence are determined as each turn begins. - // BUT it also uses before game start to fill game and card data in starting game events - inRange.clear(); - inRange.add(this.playerId); - inRange.addAll(getAllNearPlayers(game, true)); - inRange.addAll(getAllNearPlayers(game, false)); - } - - private Set getAllNearPlayers(Game game, boolean needPrevious) { - // find all near players (search from current player position) - Set foundedList = new HashSet<>(); - PlayerList players = game.getState().getPlayerList(this.playerId); - int needAmount = this.getRange().getRange(); // distance to search (0 - ALL range) - int foundedAmount = 0; - while (needAmount == 0 || foundedAmount < needAmount) { - Player foundedPlayer = needPrevious ? players.getPrevious(game) : players.getNext(game, false); - - // PlayerList is inifine, so stops on repeats - if (foundedPlayer == null || foundedPlayer.getId().equals(this.playerId) || foundedList.contains(foundedPlayer.getId())) { - break; - } - // skip leaved player (no needs cause next/previous code already checks it) - - foundedList.add(foundedPlayer.getId()); - foundedAmount++; - } - return foundedList; - } - - @Override - public Set getInRange() { - if (inRange.isEmpty()) { - // runtime check: inRange filled on beginTurn, but unit tests adds cards by cheat engine before game starting, - // so inRange will be empty and some ETB effects can be broken (example: Spark Double puts direct to battlefield). - // Cheat engine already have a workaround, so that error must not be visible in normal situation. - throw new IllegalStateException("Wrong code usage (game is not started, but you call getInRange in some effects)."); - } - - return inRange; - } - - @Override - public Set getPlayersUnderYourControl() { - return this.playersUnderYourControl; - } - - @Override - public void controlPlayersTurn(Game game, UUID playerId) { - Player player = game.getPlayer(playerId); - player.setTurnControlledBy(this.getId()); - game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); - if (!playerId.equals(this.getId())) { - this.playersUnderYourControl.add(playerId); - if (!player.hasLeft() && !player.hasLost()) { - player.setGameUnderYourControl(false); - } - DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( - new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); - ability.setSourceId(getId()); - ability.setControllerId(getId()); - game.addDelayedTriggeredAbility(ability, null); - } - } - - @Override - public void setTurnControlledBy(UUID playerId) { - this.turnController = playerId; - this.turnControllers.add(playerId); - } - - @Override - public List getTurnControllers() { - return this.turnControllers; - } - - @Override - public UUID getTurnControlledBy() { - return this.turnController; - } - - @Override - public void resetOtherTurnsControlled() { - playersUnderYourControl.clear(); - } - - /** - * returns true if the player has the control itself - false if the player - * is controlled by another player - * - * @return - */ - @Override - public boolean isGameUnderControl() { - return isGameUnderControl; - } - - @Override - public void setGameUnderYourControl(boolean value) { - setGameUnderYourControl(value, true); - } - - @Override - public void setGameUnderYourControl(boolean value, boolean fullRestore) { - this.isGameUnderControl = value; - if (isGameUnderControl) { - if (fullRestore) { - this.turnControllers.clear(); - this.turnController = getId(); - } else { - if (turnControllers.size() > 0) { - this.turnControllers.remove(turnControllers.size() - 1); - } - if (turnControllers.isEmpty()) { - this.turnController = getId(); - } else { - this.turnController = turnControllers.get(turnControllers.size() - 1); - isGameUnderControl = false; - } - } - } - } - - @Override - public void endOfTurn(Game game) { - this.passedTurn = false; - this.passedTurnSkipStack = false; - } - - @Override - public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { - if (this.hasLost() || this.hasLeft()) { - return false; - } - if (source != null) { - // there is only variant of shroud, so check the instance and any asthougheffects that would ignore it - if (abilities.containsKey(ShroudAbility.getInstance().getId()) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game) == null) { - return false; - } - // check for all variants of hexproof and any asthougheffects that would ignore it - // TODO there may be "prevented by rule-modification" effects, so add them if known - for (Ability a : abilities) { - if (a instanceof HexproofBaseAbility - && ((HexproofBaseAbility) a).checkObject(source, game) - && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { - return false; - } - } - return !hasProtectionFrom(source, game); - } - return true; - } - - @Override - public boolean hasProtectionFrom(MageObject source, Game game) { - for (ProtectionAbility ability : abilities.getProtectionAbilities()) { - if (!ability.canTarget(source, game)) { - return true; - } - } - return false; - } - - @Override - public int drawCards(int num, Ability source, Game game) { - if (num > 0) { - return game.doAction(source, new MageDrawAction(this, num, null)); - } - return 0; - } - - @Override - public int drawCards(int num, Ability source, Game game, GameEvent event) { - return game.doAction(source, new MageDrawAction(this, num, event)); - } - - @Override - public void discardToMax(Game game) { - if (hand.size() > this.maxHandSize) { - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " discards down to " - + this.maxHandSize - + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); - } - discard(hand.size() - this.maxHandSize, false, false, null, game); - } - } - - /** - * Don't use this in normal card code, it's for more internal use. Always - * use the [Player].moveCards methods if possible for card movement of card - * code. - * - * @param card - * @param game - * @return - */ - @Override - public boolean putInHand(Card card, Game game) { - if (card.isOwnedBy(playerId)) { - card.setZone(Zone.HAND, game); - this.hand.add(card); - } else { - return game.getPlayer(card.getOwnerId()).putInHand(card, game); - } - return true; - } - - @Override - public boolean removeFromHand(Card card, Game game) { - return hand.remove(card.getId()); - } - - @Override - public boolean removeFromLibrary(Card card, Game game) { - if (card == null) { - return false; - } - library.remove(card.getId(), game); - // must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop) - // TODO: replace removeFromTop logic to normal with moveToZone - return true; - } - - @Override - public Card discardOne(boolean random, boolean payForCost, Ability source, Game game) { - return discard(1, random, payForCost, source, game).getRandom(game); - } - - @Override - public Cards discard(int amount, boolean random, boolean payForCost, Ability source, Game game) { - if (random) { - return discard(getRandomToDiscard(amount, source, game), payForCost, source, game); - } - return discard(amount, amount, payForCost, source, game); - } - - @Override - public Cards discard(int minAmount, int maxAmount, boolean payForCost, Ability source, Game game) { - return discard(getToDiscard(minAmount, maxAmount, source, game), payForCost, source, game); - } - - @Override - public Cards discard(Cards cards, boolean payForCost, Ability source, Game game) { - Cards discardedCards = new CardsImpl(); - if (cards == null) { - return discardedCards; - } - for (Card card : cards.getCards(game)) { - if (doDiscard(card, source, game, payForCost, false)) { - discardedCards.add(card); - } - } - if (!discardedCards.isEmpty()) { - game.fireEvent(new DiscardedCardsEvent(source, playerId, discardedCards.size(), discardedCards)); - } - return discardedCards; - } - - @Override - public boolean discard(Card card, boolean payForCost, Ability source, Game game) { - return doDiscard(card, source, game, payForCost, true); - } - - private Cards getToDiscard(int minAmount, int maxAmount, Ability source, Game game) { - Cards toDiscard = new CardsImpl(); - if (minAmount > maxAmount) { - return getToDiscard(maxAmount, minAmount, source, game); - } - if (maxAmount < 1) { - return toDiscard; - } - if (getHand().size() <= minAmount) { - toDiscard.addAll(getHand()); - return toDiscard; - } - TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId()); - choose(Outcome.Discard, target, source != null ? source.getSourceId() : null, game); - toDiscard.addAll(target.getTargets()); - return toDiscard; - } - - private Cards getRandomToDiscard(int amount, Ability source, Game game) { - Cards toDiscard = new CardsImpl(); - Cards hand = getHand().copy(); - for (int i = 0; i < amount; i++) { - if (hand.isEmpty()) { - break; - } - Card card = hand.getRandom(game); - hand.remove(card); - toDiscard.add(card); - } - return toDiscard; - } - - private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) { - //20100716 - 701.7 - /* 701.7. Discard # - 701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. - 701.7b By default, effects that cause a player to discard a card allow the affected - player to choose which card to discard. Some effects, however, require a random - discard or allow another player to choose which card is discarded. - 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone - instead of into its owner’s graveyard without being revealed, all values of that - card’s characteristics are considered to be undefined. - TODO: - If a card is discarded this way to pay a cost that specifies a characteristic - about the discarded card, that cost payment is illegal; the game returns to - the moment before the cost was paid (see rule 717, "Handling Illegal Actions"). - */ - if (card == null) { - return false; - } - GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source, playerId); - gameEvent.setFlag(!payForCost); // event from effect (1) or from cost (0) - if (game.replaceEvent(gameEvent, source)) { - return false; - } - // write info to game log first so game log infos from triggered or replacement effects follow in the game log - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " discards " + card.getLogName() + CardUtil.getSourceLogName(game, source)); - } - /* If a card is discarded while Rest in Peace is on the battlefield, abilities that function - * when a card is discarded (such as madness) still work, even though that card never reaches - * a graveyard. In addition, spells or abilities that check the characteristics of a discarded - * card (such as Chandra Ablaze's first ability) can find that card in exile. */ - card.moveToZone(Zone.GRAVEYARD, source, game, false); - // So discard is also successful if card is moved to another zone by replacement effect! - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), source, playerId)); - - if (fireFinalEvent) { - game.fireEvent(new DiscardedCardsEvent(source, playerId, 1, new CardsImpl(card))); - } - return true; - } - - @Override - public List getAttachments() { - return attachments; - } - - @Override - public boolean addAttachment(UUID permanentId, Ability source, Game game) { - if (!this.attachments.contains(permanentId)) { - Permanent aura = game.getPermanent(permanentId); - if (aura == null) { - aura = game.getPermanentEntering(permanentId); - } - if (aura != null) { - if (!game.replaceEvent(new EnchantPlayerEvent(playerId, aura, source))) { - this.attachments.add(permanentId); - aura.attachTo(playerId, source, game); - game.fireEvent(new EnchantedPlayerEvent(playerId, aura, source)); - return true; - } - } - } - return false; - } - - @Override - public boolean removeAttachment(Permanent attachment, Ability source, Game game) { - if (this.attachments.contains(attachment.getId())) { - if (!game.replaceEvent(new UnattachEvent(playerId, attachment.getId(), attachment, source))) { - this.attachments.remove(attachment.getId()); - attachment.attachTo(null, source, game); - game.fireEvent(new UnattachedEvent(playerId, attachment.getId(), attachment, source)); - return true; - } - } - return false; - } - - @Override - public boolean removeFromBattlefield(Permanent permanent, Ability source, Game game) { - permanent.removeFromCombat(game, false); - game.getBattlefield().removePermanent(permanent.getId()); - if (permanent.getAttachedTo() != null) { - Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); - if (attachedTo != null) { - attachedTo.removeAttachment(permanent.getId(), source, game); - } else { - Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo()); - if (attachedToPlayer != null) { - attachedToPlayer.removeAttachment(permanent, source, game); - } else { - Card attachedToCard = game.getCard(permanent.getAttachedTo()); - if (attachedToCard != null) { - attachedToCard.removeAttachment(permanent.getId(), source, game); - } - } - } - - } - if (permanent.getPairedCard() != null) { - Permanent pairedCard = permanent.getPairedCard().getPermanent(game); - if (pairedCard != null) { - pairedCard.clearPairedCard(); - } - } - if (permanent.getBandedCards() != null && !permanent.getBandedCards().isEmpty()) { - for (UUID bandedId : permanent.getBandedCards()) { - Permanent banded = game.getPermanent(bandedId); - if (banded != null) { - banded.removeBandedCard(permanent.getId()); - } - } - } - return true; - } - - @Override - public boolean putInGraveyard(Card card, Game game) { - if (card.isOwnedBy(playerId)) { - this.graveyard.add(card); - } else { - return game.getPlayer(card.getOwnerId()).putInGraveyard(card, game); - } - return true; - } - - @Override - public boolean removeFromGraveyard(Card card, Game game) { - return this.graveyard.remove(card); - } - - @Override - public boolean putCardsOnBottomOfLibrary(Card card, Game game, Ability source, boolean anyOrder) { - return putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, anyOrder); - } - - @Override - public boolean putCardsOnBottomOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { - if (!cardsToLibrary.isEmpty()) { - Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException - if (!anyOrder) { - // random order - List ids = new ArrayList<>(cards); - Collections.shuffle(ids); - for (UUID id : ids) { - moveObjectToLibrary(id, source, game, false, false); - } - } else { - // user defined order - TargetCard target = new TargetCard(Zone.ALL, - new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)")); - target.setRequired(true); - while (cards.size() > 1 && this.canRespond() - && this.choose(Outcome.Neutral, cards, target, game)) { - UUID targetObjectId = target.getFirstTarget(); - if (targetObjectId == null) { - break; - } - cards.remove(targetObjectId); - moveObjectToLibrary(targetObjectId, source, game, false, false); - target.clearChosen(); - } - for (UUID c : cards) { - moveObjectToLibrary(c, source, game, false, false); - } - } - } - return true; - } - - @Override - public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) { - if (cards.isEmpty()) { - return true; - } - game.informPlayers(getLogName() + " shuffles " + CardUtil.numberToText(cards.size(), "a") - + " card" + (cards.size() == 1 ? "" : "s") - + " into their library" + CardUtil.getSourceLogName(game, source)); - boolean status = moveCards(cards, Zone.LIBRARY, source, game); - shuffleLibrary(source, game); - return status; - } - - @Override - public boolean shuffleCardsToLibrary(Card card, Game game, Ability source) { - if (card == null) { - return true; - } - return shuffleCardsToLibrary(new CardsImpl(card), game, source); - } - - @Override - public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int xFromTheTop, boolean withName) { - if (card.isOwnedBy(getId())) { - if (library.size() + 1 < xFromTheTop) { - putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true); - } else { - if (card.moveToZone(Zone.LIBRARY, source, game, true) - && !(card instanceof PermanentToken) && !card.isCopy()) { - Card cardInLib = getLibrary().getFromTop(game); - if (cardInLib != null && cardInLib.getId().equals(card.getId())) { // check needed because e.g. commander can go to command zone - cardInLib = getLibrary().removeFromTop(game); - getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); - game.informPlayers(withName ? cardInLib.getLogName() : "A card" - + " is put into " - + getLogName() - + "'s library " - + CardUtil.numberToOrdinalText(xFromTheTop) - + " from the top" + CardUtil.getSourceLogName(game, source, cardInLib.getId())); - } - } else { - return false; - } - } - } else { - return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop, withName); - } - return true; - } - - /** - * Can be cards or permanents that go to library - * - * @param cardsToLibrary - * @param game - * @param source - * @param anyOrder - * @return - */ - @Override - public boolean putCardsOnTopOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { - if (cardsToLibrary != null && !cardsToLibrary.isEmpty()) { - Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException - if (!anyOrder) { - // random order - List ids = new ArrayList<>(cards); - Collections.shuffle(ids); - for (UUID id : ids) { - moveObjectToLibrary(id, source, game, true, false); - } - } else { - // user defined order - TargetCard target = new TargetCard(Zone.ALL, - new FilterCard("card ORDER to put on the TOP of your library (last one chosen will be topmost)")); - target.setRequired(true); - while (cards.size() > 1 - && this.canRespond() - && this.choose(Outcome.Neutral, cards, target, game)) { - UUID targetObjectId = target.getFirstTarget(); - if (targetObjectId == null) { - break; - } - cards.remove(targetObjectId); - moveObjectToLibrary(targetObjectId, source, game, true, false); - target.clearChosen(); - } - for (UUID c : cards) { - moveObjectToLibrary(c, source, game, true, false); - } - } - } - return true; - } - - @Override - public boolean putCardsOnTopOfLibrary(Card cardToLibrary, Game game, Ability source, boolean anyOrder) { - if (cardToLibrary != null) { - return putCardsOnTopOfLibrary(new CardsImpl(cardToLibrary), game, source, anyOrder); - } - return true; - } - - private boolean moveObjectToLibrary(UUID objectId, Ability source, Game game, boolean toTop, boolean withName) { - MageObject mageObject = game.getObject(objectId); - if (mageObject instanceof Spell && mageObject.isCopy()) { - // Spell copies are not moved as cards, so here the no copy spell has to be selected to move - // (but because copy and original have the same objectId the wrong sepell can be selected from stack). - // So let's check if the original spell is on the stack and has to be selected. // TODO: Better handling so each spell could be selected by a unique id - Spell spellNoCopy = game.getStack().getSpell(source.getSourceId(), false); - if (spellNoCopy != null) { - mageObject = spellNoCopy; - } - } - if (mageObject != null) { - Zone fromZone = game.getState().getZone(objectId); - if ((mageObject instanceof Permanent)) { - return this.moveCardToLibraryWithInfo((Permanent) mageObject, source, game, fromZone, toTop, withName); - } else if (mageObject instanceof Card) { - return this.moveCardToLibraryWithInfo((Card) mageObject, source, game, fromZone, toTop, withName); - } - } - return false; - } - - @Override - public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { - // cost must be copied for data consistence between game simulations - castSourceIdWithAlternateMana.add(sourceId); - castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); - castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); - } - - @Override - public Set getCastSourceIdWithAlternateMana() { - return castSourceIdWithAlternateMana; - } - - @Override - public Map> getCastSourceIdCosts() { - return castSourceIdCosts; - } - - @Override - public Map> getCastSourceIdManaCosts() { - return castSourceIdManaCosts; - } - - @Override - public void clearCastSourceIdManaCosts() { - this.castSourceIdCosts.clear(); - this.castSourceIdManaCosts.clear(); - this.castSourceIdWithAlternateMana.clear(); - } - - @Override - public void setPayManaMode(boolean payManaMode) { - this.payManaMode = payManaMode; - } - - @Override - public boolean isInPayManaMode() { - return payManaMode; - } - - @Override - public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { - if (card == null) { - return false; - } - - // play without timing and from any zone - boolean result; - if (card.isLand(game)) { - result = playLand(card, game, true); - } else { - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - } - - if (!result) { - game.informPlayer(this, "You can't play " + card.getIdName() + '.'); - } - return result; - } - - /** - * @param originalAbility - * @param game - * @param noMana cast it without paying mana costs - * @param approvingObject which object approved the cast - * @return - */ - @Override - public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, ApprovingObject approvingObject) { - if (game == null || originalAbility == null) { - return false; - } - - // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). - SpellAbility ability = originalAbility.copy(); - ability.setControllerId(getId()); - ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); - - //20091005 - 601.2a - if (ability.getSourceId() == null) { - logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule()); - return false; - } - Card card = game.getCard(ability.getSourceId()); - if (card != null) { - Zone fromZone = game.getState().getZone(card.getMainCard().getId()); - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - ability.getId(), ability, playerId, approvingObject); - castEvent.setZone(fromZone); - if (!game.replaceEvent(castEvent, ability)) { - int bookmark = game.bookmarkState(); - setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) - card.cast(game, fromZone, ability, playerId); - Spell spell = game.getStack().getSpell(ability.getId()); - if (spell == null) { - logger.error("Got no spell from stack. ability: " + ability.getRule()); - return false; - } - if (card.isCopy()) { - spell.setCopy(true, null); - } - // Update the zcc to the stack - ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); - - // ALTERNATIVE COST from dynamic effects - // some effects set sourceId to cast without paying mana costs or other costs - if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { - Ability spellAbility = spell.getSpellAbility(); - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); - Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); - if (alternateCosts == null) { - noMana = true; - } else { - spellAbility.getManaCosts().clear(); - spellAbility.getManaCostsToPay().clear(); - spellAbility.getManaCosts().add(alternateCosts.copy()); - spellAbility.getManaCostsToPay().add(alternateCosts.copy()); - } - spellAbility.getCosts().clear(); - if (costs != null) { - spellAbility.getCosts().addAll(costs); - } - } - clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time - - castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); - castEvent.setZone(fromZone); - game.fireEvent(castEvent); - if (spell.activate(game, noMana)) { - GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, - spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); - castedEvent.setZone(fromZone); - game.fireEvent(castedEvent); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + spell.getActivatedMessage(game)); - } - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - } - return false; - } - - @Override - public boolean playLand(Card card, Game game, boolean ignoreTiming) { - // Check for alternate casting possibilities: e.g. land with Morph - if (card == null) { - return false; - } - ActivatedAbility playLandAbility = null; - boolean foundAlternative = false; - for (Ability ability : card.getAbilities(game)) { - // if cast for noMana no Alternative costs are allowed - if ((ability instanceof AlternativeSourceCosts) - || (ability instanceof OptionalAdditionalSourceCosts)) { - foundAlternative = true; - } - if (ability instanceof PlayLandAbility) { - playLandAbility = (ActivatedAbility) ability; - } - } - - // try alternative cast (face down) - if (foundAlternative) { - SpellAbility spellAbility = new SpellAbility(null, "", - game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); - spellAbility.setControllerId(this.getId()); - spellAbility.setSourceId(card.getId()); - if (cast(spellAbility, game, false, null)) { - return true; - } - } - - if (playLandAbility == null) { - return false; - } - - //20091005 - 114.2a - ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); - if (ignoreTiming) { - if (!canPlayLand()) { - return false; // ignore timing does not mean that more lands than normal can be played - } - } else { - if (!activationStatus.canActivate()) { - return false; - } - } - - //20091005 - 305.1 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { - // int bookmark = game.bookmarkState(); - // land events must return original zone (uses for commander watcher) - Zone cardZoneBefore = game.getState().getZone(card.getId()); - GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); - landEventBefore.setZone(cardZoneBefore); - game.fireEvent(landEventBefore); - - if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { - landsPlayed++; - GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); - landEventAfter.setZone(cardZoneBefore); - game.fireEvent(landEventAfter); - - String playText = getLogName() + " plays " + card.getLogName(); - if (card instanceof ModalDoubleFacesCardHalf) { - ModalDoubleFacesCard mdfCard = (ModalDoubleFacesCard) card.getMainCard(); - playText = getLogName() + " plays " + GameLog.replaceNameByColoredName(card, card.getName(), mdfCard) - + " as MDF side of " + GameLog.getColoredObjectIdName(mdfCard); - } - game.fireInformEvent(playText); - // game.removeBookmark(bookmark); - resetStoredBookmark(game); // prevent undo after playing a land - return true; - } - // putOntoBattlefield returned false if putOntoBattlefield was replaced by replacement effect (e.g. Kjeldoran Outpost). - // But that would undo the effect completely, - // what makes no real sense. So it makes no sense to generally do a restoreState here. - // restoreState(bookmark, card.getName(), game); - } - // if the to play the land is replaced (e.g. Kjeldoran Outpost and don't sacrificing a Plains) it's a valid state so returning true here - return true; - } - - protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, - ability.getId(), ability, playerId))) { - int bookmark = game.bookmarkState(); - if (ability.activate(game, false)) { - if (ability.resolve(game)) { - if (ability.isUndoPossible()) { - if (storedBookmark == -1 || storedBookmark > bookmark) { // e.g. useful for undo Nykthos, Shrine to Nyx - setStoredBookmark(bookmark); - } - } else { - resetStoredBookmark(game); - } - return true; - } - } - restoreState(bookmark, ability.getRule(), game); - } - return false; - } - - protected boolean playAbility(ActivatedAbility ability, Game game) { - //20091005 - 602.2a - if (ability.isUsesStack()) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, - ability.getId(), ability, playerId))) { - int bookmark = game.bookmarkState(); - setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) - ability.newId(); - ability.setControllerId(playerId); - game.getStack().push(new StackAbility(ability, playerId)); - if (ability.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, - ability.getId(), ability, playerId)); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + ability.getGameLogMessage(game)); - } - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - } else { - int bookmark = game.bookmarkState(); - if (ability.activate(game, false)) { - ability.resolve(game); - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - restoreState(bookmark, ability.getRule(), game); - } - return false; - } - - protected boolean specialAction(SpecialAction action, Game game) { - //20091005 - 114 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_ACTION, - action.getId(), action, getId()))) { - int bookmark = game.bookmarkState(); - if (action.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_ACTION, - action.getId(), action, getId())); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + action.getGameLogMessage(game)); - } - if (action.resolve(game)) { - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - } - restoreState(bookmark, action.getRule(), game); - } - return false; - } - - protected boolean specialManaPayment(SpecialAction action, Game game) { - //20091005 - 114 - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_MANA_PAYMENT, - action.getId(), action, getId()))) { - int bookmark = game.bookmarkState(); - if (action.activate(game, false)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_MANA_PAYMENT, - action.getId(), action, getId())); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + action.getGameLogMessage(game)); - } - if (action.resolve(game)) { - game.removeBookmark(bookmark); - resetStoredBookmark(game); - return true; - } - } - restoreState(bookmark, action.getRule(), game); - } - return false; - } - - @Override - public boolean activateAbility(ActivatedAbility ability, Game game) { - if (ability == null) { - return false; - } - boolean result; - if (ability instanceof PassAbility) { - pass(game); - return true; - } - Card card = game.getCard(ability.getSourceId()); - if (ability instanceof PlayLandAsCommanderAbility) { - - // LAND as commander: play land with cost, but without stack - ActivationStatus activationStatus = ability.canActivate(this.playerId, game); - if (!activationStatus.canActivate() || !this.canPlayLand()) { - return false; - } - if (card == null) { - return false; - } - - // as copy, tries to applie cost effects and pays - Ability activatingAbility = ability.copy(); - if (activatingAbility.activate(game, false)) { - result = playLand(card, game, false); - } else { - result = false; - } - - } else if (ability instanceof PlayLandAbility) { - - // LAND as normal card: without cost and stack - result = playLand(card, game, false); - - } else { - - // ABILITY - ActivationStatus activationStatus = ability.canActivate(this.playerId, game); - if (!activationStatus.canActivate()) { - return false; - } - - switch (ability.getAbilityType()) { - case SPECIAL_ACTION: - result = specialAction((SpecialAction) ability.copy(), game); - break; - case SPECIAL_MANA_PAYMENT: - result = specialManaPayment((SpecialAction) ability.copy(), game); - break; - case MANA: - result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); - break; - case SPELL: - result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); - break; - default: - result = playAbility(ability.copy(), game); - break; - } - } - - //if player has taken an action then reset all player passed flags - justActivatedType = null; - if (result) { - if (isHuman() - && (ability.getAbilityType() == AbilityType.SPELL - || ability.getAbilityType() == AbilityType.ACTIVATED)) { - if (ability.isUsesStack()) { // if the ability does not use the stack (e.g. Suspend) auto pass would go to next phase unintended - setJustActivatedType(ability.getAbilityType()); - } - } - game.getPlayers().resetPassed(); - } - return result; - } - - @Override - public boolean triggerAbility(TriggeredAbility triggeredAbility, Game game) { - if (triggeredAbility == null) { - logger.warn("Null source in triggerAbility method"); - throw new IllegalArgumentException("source TriggeredAbility must not be null"); - } - //20091005 - 603.3c, 603.3d - int bookmark = game.bookmarkState(); - TriggeredAbility ability = triggeredAbility.copy(); - MageObject sourceObject = ability.getSourceObject(game); - if (sourceObject != null) { - sourceObject.adjustTargets(ability, game); - } - UUID triggerId = null; - if (ability.canChooseTarget(game, playerId)) { - if (ability.isUsesStack()) { - game.getStack().push(new StackAbility(ability, playerId)); - } - if (ability.activate(game, false)) { - if ((ability.isUsesStack() - || ability.getRuleVisible()) - && !game.isSimulation()) { - game.informPlayers(getLogName() + " - " + ability.getGameLogMessage(game)); - } - if (!ability.isUsesStack()) { - ability.resolve(game); - } else { - game.fireEvent(new GameEvent( - GameEvent.EventType.TRIGGERED_ABILITY, - ability.getId(), ability, ability.getControllerId() - )); - triggerId = ability.getId(); - } - game.removeBookmark(bookmark); - return true; - } - } - restoreState(bookmark, triggeredAbility.getRule(), game); // why restore is needed here? (to remove the triggered ability from the stack because of no possible targets) - GameEvent event = new GameEvent( - GameEvent.EventType.ABILITY_TRIGGERED, - triggerId, ability, ability.getControllerId() - ); - game.getState().setValue(event.getId().toString(), ability.getTriggerEvent()); - game.fireEvent(event); - return false; - } - - /** - * Return spells for possible cast Uses in GUI to show only playable spells - * for choosing from the card (example: effect allow to cast card and player - * must choose the spell ability) - * - * @param game - * @param playerId - * @param object - * @param zone - * @param noMana - * @return - */ - public static LinkedHashMap getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { - // it uses simple check from spellCanBeActivatedRegularlyNow - // reason: no approved info here (e.g. forced to choose spell ability from cast card) - LinkedHashMap useable = new LinkedHashMap<>(); - Abilities allAbilities; - if (object instanceof Card) { - allAbilities = ((Card) object).getAbilities(game); - } else { - allAbilities = object.getAbilities(); - } - - for (Ability ability : allAbilities) { - if (ability instanceof SpellAbility) { - SpellAbility spellAbility = (SpellAbility) ability; - - switch (spellAbility.getSpellAbilityType()) { - case BASE_ALTERNATE: - // rules: - // If you cast a spell “without paying its mana cost,” you can’t choose to cast it for - // any alternative costs. You can, however, pay additional costs, such as kicker costs. - // If the card has any mandatory additional costs, those must be paid to cast the spell. - // (2021-02-05) - if (!noMana) { - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability - } - return useable; - } - break; - case SPLIT_FUSED: - // rules: - // If you cast a split card with fuse from your hand without paying its mana cost, - // you can choose to use its fuse ability and cast both halves without paying their mana costs. - if (zone == Zone.HAND) { - if (spellAbility.canChooseTarget(game, playerId)) { - useable.put(spellAbility.getId(), spellAbility); - } - } - case SPLIT: - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getLeftHalfCard().getSpellAbility()); - } - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getRightHalfCard().getSpellAbility()); - } - return useable; - case SPLIT_AFTERMATH: - if (zone == Zone.GRAVEYARD) { - if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getRightHalfCard().getSpellAbility()); - } - } else { - if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { - useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), - ((SplitCard) object).getLeftHalfCard().getSpellAbility()); - } - } - return useable; - default: - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { - useable.put(spellAbility.getId(), spellAbility); - } - } - } - } - return useable; - } - - @Override - public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { - LinkedHashMap useable = new LinkedHashMap<>(); - // stack abilities - can't activate anything - // spell ability - can activate additional abilities (example: "Lightning Storm") - if (object instanceof StackAbility || object == null) { - return useable; - } - boolean previousState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - try { - // collect and filter playable activated abilities - // GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right) - Set needIds = CardUtil.getObjectParts(object); - - // workaround to find all abilities first and filter it for one object - List allPlayable = getPlayable(game, true, zone, false); - for (ActivatedAbility ability : allPlayable) { - if (needIds.contains(ability.getSourceId())) { - useable.putIfAbsent(ability.getId(), ability); - } - } - } finally { - game.setCheckPlayableState(previousState); - } - return useable; - } - - protected LinkedHashMap getUseableManaAbilities(MageObject object, Zone zone, Game game) { - LinkedHashMap useable = new LinkedHashMap<>(); - boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); - for (ActivatedManaAbilityImpl ability : object.getAbilities().getActivatedManaAbilities(zone)) { - if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - if (ability.canActivate(playerId, game).canActivate()) { - useable.put(ability.getId(), ability); - } - } - } - return useable; - } - - @Override - public int getLandsPlayed() { - return landsPlayed; - } - - @Override - public boolean canPlayLand() { - //20091005 - 114.2a - return landsPlayed < landsPerTurn; - } - - protected boolean isActivePlayer(Game game) { - return game.isActivePlayer(this.playerId); - } - - @Override - public void shuffleLibrary(Ability source, Game game) { - if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SHUFFLE_LIBRARY, playerId, source, playerId))) { - this.library.shuffle(); - if (!game.isSimulation()) { - game.informPlayers(getLogName() + "'s library is shuffled" + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, source, playerId)); - } - } - - @Override - public void revealCards(Ability source, Cards cards, Game game) { - revealCards(source, null, cards, game, true); - } - - @Override - public void revealCards(String titleSuffix, Cards cards, Game game) { - revealCards(titleSuffix, cards, game, true); - } - - @Override - public void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog) { - revealCards(null, titleSuffix, cards, game, postToLog); - } - - @Override - public void revealCards(Ability source, String titleSuffix, Cards cards, Game game) { - revealCards(source, titleSuffix, cards, game, true); - } - - @Override - public void revealCards(Ability source, String titleSuffix, Cards cards, Game game, boolean postToLog) { - if (cards == null || cards.isEmpty()) { - return; - } - if (postToLog) { - game.getState().getRevealed().add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - } else { - game.getState().getRevealed().update(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - } - if (postToLog && !game.isSimulation()) { - StringBuilder sb = new StringBuilder(getLogName()).append(" reveals "); - int current = 0, last = cards.size(); - for (Card card : cards.getCards(game)) { - current++; - sb.append(GameLog.getColoredObjectName(card)); - if (current < last) { - sb.append(", "); - } - } - sb.append(CardUtil.getSourceLogName(game, source)); - game.informPlayers(sb.toString()); - } - } - - @Override - public void lookAtCards(String titleSuffix, Card card, Game game) { - game.getState().getLookedAt(this.playerId).add(titleSuffix, card); - game.fireUpdatePlayersEvent(); - } - - @Override - public void lookAtCards(String titleSuffix, Cards cards, Game game) { - this.lookAtCards(null, titleSuffix, cards, game); - } - - @Override - public void lookAtCards(Ability source, String titleSuffix, Cards cards, Game game) { - game.getState().getLookedAt(this.playerId).add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); - game.fireUpdatePlayersEvent(); - } - - @Override - public void phasing(Game game) { - //20091005 - 502.1 - List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); - for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { - // 502.15i When a permanent phases out, any local enchantments or Equipment - // attached to that permanent phase out at the same time. This alternate way of - // phasing out is known as phasing out "indirectly." An enchantment or Equipment - // that phased out indirectly won't phase in by itself, but instead phases in - // along with the card it's attached to. - Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); - if (!(attachedTo != null && attachedTo.isControlledBy(this.getId()))) { - permanent.phaseOut(game, false); - } - } - for (Permanent permanent : phasedOut) { - if (!permanent.isPhasedOutIndirectly()) { - permanent.phaseIn(game); - } - } - } - - @Override - public void untap(Game game) { - // create list of all "notMoreThan" effects to track which one are consumed - Map>, Integer> notMoreThanEffectsUsage = new HashMap<>(); - for (Entry> restrictionEffect - : game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { - notMoreThanEffectsUsage.put(restrictionEffect, restrictionEffect.getKey().getNumber()); - } - - if (!notMoreThanEffectsUsage.isEmpty()) { - // create list of all permanents that can be untapped generally - List canBeUntapped = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - boolean untap = true; - for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { - untap &= effect.canBeUntapped(permanent, null, game, true); - } - if (untap) { - canBeUntapped.add(permanent); - } - } - // selected permanents to untap - List selectedToUntap = new ArrayList<>(); - - // player can cancel the selection of an effect to use a preferred order of restriction effects - boolean playerCanceledSelection; - do { - playerCanceledSelection = false; - // select permanents to untap to consume the "notMoreThan" effects - for (Map.Entry>, Integer> handledEntry : notMoreThanEffectsUsage.entrySet()) { - // select a permanent to untap for this entry - int numberToUntap = handledEntry.getValue(); - if (numberToUntap > 0) { - - List leftForUntap = getPermanentsThatCanBeUntapped(game, - canBeUntapped, - handledEntry.getKey().getKey(), - notMoreThanEffectsUsage); - - FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); - String message = filter.getMessage(); - // omit already from other untap effects selected permanents - for (Permanent permanent : selectedToUntap) { - filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); - } - // while targets left and there is still allowed to untap - while (canRespond() && !leftForUntap.isEmpty() && numberToUntap > 0) { - // player has to select the permanent they want to untap for this restriction - Ability ability = handledEntry.getKey().getValue().iterator().next(); - if (ability != null) { - StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), - numberToUntap)).append(" in total"); - MageObject effectSource = game.getObject(ability.getSourceId()); - if (effectSource != null) { - sb.append(" from ").append(effectSource.getLogName()); - } - sb.append(')'); - filter.setMessage(sb.toString()); - Target target = new TargetPermanent(1, 1, filter, true); - if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { - // player canceled, go on with the next effect (if no other effect available, this effect will be active again) - playerCanceledSelection = true; - break; - } - Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); - if (leftForUntap.contains(selectedPermanent)) { - selectedToUntap.add(selectedPermanent); - numberToUntap--; - // don't allow to select same permanent twice - filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); - // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getValue() > 0 - && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { - notMoreThanEffect.setValue(notMoreThanEffect.getValue() - 1); - } - } - // update the left for untap list - leftForUntap = getPermanentsThatCanBeUntapped(game, - canBeUntapped, - handledEntry.getKey().getKey(), - notMoreThanEffectsUsage); - // remove already selected permanents - for (Permanent permanent : selectedToUntap) { - leftForUntap.remove(permanent); - } - - } else { - // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) - filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); - if (this.isHuman() && !game.isSimulation()) { - game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); - } - } - } - } - } - } - - } while (canRespond() && playerCanceledSelection); - - if (!game.isSimulation()) { - // show in log which permanents were selected to untap - for (Permanent permanent : selectedToUntap) { - game.informPlayers(this.getLogName() + " untapped " + permanent.getLogName()); - } - } - // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList - for (Permanent permanent : canBeUntapped) { - boolean doUntap = true; - if (!selectedToUntap.contains(permanent)) { - // if the permanent is covered by one of the restriction effects, don't untap it - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { - doUntap = false; - break; - } - } - } - if (permanent != null && doUntap) { - permanent.untap(game); - } - - } - - } else { - //20091005 - 502.2 - - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - boolean untap = true; - for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { - untap &= effect.canBeUntapped(permanent, null, game, true); - } - if (untap) { - permanent.untap(game); - } - } - } - } - - private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, Map>, Integer> notMoreThanEffectsUsage) { - List leftForUntap = new ArrayList<>(); - // select permanents that can still be untapped - for (Permanent permanent : canBeUntapped) { - if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry - boolean canBeSelected = true; - // check if the permanent is restricted by another restriction that has left no permanent - for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { - if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) - && notMoreThanEffect.getValue() == 0) { - canBeSelected = false; - break; - } - } - if (canBeSelected) { - leftForUntap.add(permanent); - } - } - } - return leftForUntap; - } - - @Override - public UUID getId() { - return playerId; - } - - @Override - public Cards getHand() { - return hand; - } - - @Override - public Graveyard getGraveyard() { - return graveyard; - } - - @Override - public ManaPool getManaPool() { - return this.manaPool; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getLogName() { - return GameLog.getColoredPlayerName(name); - } - - @Override - public boolean isHuman() { - return human; - } - - @Override - public Library getLibrary() { - return library; - } - - @Override - public Cards getSideboard() { - return sideboard; - } - - @Override - public int getLife() { - return life; - } - - @Override - public void initLife(int life) { - this.life = life; - } - - @Override - public void setLife(int life, Game game, Ability source) { - // rule 118.5 - if (life > this.life) { - gainLife(life - this.life, game, source); - } else if (life < this.life) { - loseLife(this.life - life, game, source, false); - } - } - - @Override - public void setLifeTotalCanChange(boolean lifeTotalCanChange) { - this.canGainLife = lifeTotalCanChange; - this.canLoseLife = lifeTotalCanChange; - } - - @Override - public boolean isLifeTotalCanChange() { - return canGainLife || canLoseLife; - } - - @Override - public List getAlternativeSourceCosts() { - return alternativeSourceCosts; - } - - @Override - public boolean isCanLoseLife() { - return canLoseLife; - } - - @Override - public void setCanLoseLife(boolean canLoseLife) { - this.canLoseLife = canLoseLife; - } - - @Override - public int loseLife(int amount, Game game, Ability source, boolean atCombat, UUID attackerId) { - if (!canLoseLife || !this.isInGame()) { - return 0; - } - GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE, - playerId, source, playerId, amount, atCombat); - if (!game.replaceEvent(event)) { - this.life = CardUtil.overflowDec(this.life, event.getAmount()); - if (!game.isSimulation()) { - UUID needId = attackerId; - if (needId == null) { - needId = source == null ? null : source.getSourceId(); - } - game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life" - + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); - } - if (amount > 0) { - game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, - playerId, source, playerId, amount, atCombat)); - } - return amount; - } - return 0; - } - - @Override - public int loseLife(int amount, Game game, Ability source, boolean atCombat) { - return loseLife(amount, game, source, atCombat, null); - } - - @Override - public boolean isCanGainLife() { - return canGainLife; - } - - @Override - public void setCanGainLife(boolean canGainLife) { - this.canGainLife = canGainLife; - } - - @Override - public int gainLife(int amount, Game game, Ability source) { - if (!canGainLife || amount <= 0) { - return 0; - } - GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE, - playerId, source, playerId, amount, false); - if (!game.replaceEvent(event)) { - // TODO: lock life at Integer.MAX_VALUE if reached, until it's set to a different amount - // (https://magic.wizards.com/en/articles/archive/news/unstable-faqawaslfaqpaftidawabiajtbt-2017-12-06 - "infinite" life total stays infinite no matter how much is gained or lost) - // this.life += event.getAmount(); - this.life = CardUtil.overflowInc(this.life, event.getAmount()); - if (!game.isSimulation()) { - game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life" + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE, - playerId, source, playerId, event.getAmount())); - return event.getAmount(); - } - return 0; - } - - @Override - public void exchangeLife(Player player, Ability source, Game game) { - int lifePlayer1 = getLife(); - int lifePlayer2 = player.getLife(); - if ((lifePlayer1 != lifePlayer2 && this.isLifeTotalCanChange() && player.isLifeTotalCanChange()) - && (lifePlayer1 >= lifePlayer2 || (this.isCanGainLife() && player.isCanLoseLife())) - && (lifePlayer1 <= lifePlayer2 || (this.isCanLoseLife() && player.isCanGainLife()))) { - this.setLife(lifePlayer2, game, source); - player.setLife(lifePlayer1, game, source); - } - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game) { - return doDamage(damage, attackerId, source, game, false, true, null); - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) { - return doDamage(damage, attackerId, source, game, combatDamage, preventable, null); - } - - @Override - public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { - return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects); - } - - private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { - if (!this.isInGame()) { - return 0; - } - - if (damage < 1) { - return 0; - } - if (!canDamage(game.getObject(attackerId), game)) { - MageObject sourceObject = game.getObject(attackerId); - game.informPlayers(damage + " damage " - + (sourceObject == null ? "" : "from " + sourceObject.getLogName()) - + " to " + getLogName() - + (damage > 1 ? " were" : "was") + " prevented because of protection"); - return 0; - } - DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage); - event.setAppliedEffects(appliedEffects); - if (game.replaceEvent(event)) { - return 0; - } - int actualDamage = event.getAmount(); - if (actualDamage < 1) { - return 0; - } - UUID sourceControllerId = null; - Abilities sourceAbilities = null; - MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId); - if (attacker == null) { - StackObject stackObject = game.getStack().getStackObject(attackerId); - if (stackObject != null) { - attacker = stackObject.getStackAbility().getSourceObject(game); - } else { - attacker = game.getObject(attackerId); - } - if (attacker instanceof Spell) { - sourceAbilities = ((Spell) attacker).getAbilities(game); - sourceControllerId = ((Spell) attacker).getControllerId(); - } else if (attacker instanceof Card) { - sourceAbilities = ((Card) attacker).getAbilities(game); - sourceControllerId = ((Card) attacker).getOwnerId(); - } else if (attacker instanceof CommandObject) { - sourceControllerId = ((CommandObject) attacker).getControllerId(); - sourceAbilities = attacker.getAbilities(); - } - } else { - sourceAbilities = ((Permanent) attacker).getAbilities(game); - sourceControllerId = ((Permanent) attacker).getControllerId(); - } - if (event.isAsThoughInfect() || (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId()))) { - addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game); - } else { - GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS, - playerId, source, playerId, actualDamage, combatDamage); - if (!game.replaceEvent(damageToLifeLossEvent)) { - this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId); - } - } - if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { - if (combatDamage) { - game.getPermanent(attackerId).markLifelink(actualDamage); - } else { - Player player = game.getPlayer(sourceControllerId); - player.gainLife(actualDamage, game, source); - } - } - // Unstable ability - Earl of Squirrel - if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) { - Player player = game.getPlayer(sourceControllerId); - new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId()); - } - DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage); - game.fireEvent(damagedEvent); - game.getState().addSimultaneousDamage(damagedEvent, game); - return actualDamage; - } - - @Override - public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { - boolean returnCode = true; - GameEvent addingAllEvent = GameEvent.getEvent( - GameEvent.EventType.ADD_COUNTERS, playerId, source, - playerAddingCounters, counter.getName(), counter.getCount() - ); - if (!game.replaceEvent(addingAllEvent)) { - int amount = addingAllEvent.getAmount(); - int finalAmount = amount; - boolean isEffectFlag = addingAllEvent.getFlag(); - for (int i = 0; i < amount; i++) { - Counter eventCounter = counter.copy(); - eventCounter.remove(eventCounter.getCount() - 1); - GameEvent addingOneEvent = GameEvent.getEvent( - GameEvent.EventType.ADD_COUNTER, playerId, source, - playerAddingCounters, counter.getName(), 1 - ); - addingOneEvent.setFlag(isEffectFlag); - if (!game.replaceEvent(addingOneEvent)) { - getCounters().addCounter(eventCounter); - GameEvent addedOneEvent = GameEvent.getEvent( - GameEvent.EventType.COUNTER_ADDED, playerId, source, - playerAddingCounters, counter.getName(), 1 - ); - addedOneEvent.setFlag(addingOneEvent.getFlag()); - game.fireEvent(addedOneEvent); - } else { - finalAmount--; - returnCode = false; - } - } - if (finalAmount > 0) { - GameEvent addedAllEvent = GameEvent.getEvent( - GameEvent.EventType.COUNTERS_ADDED, playerId, source, - playerAddingCounters, counter.getName(), amount - ); - addedAllEvent.setFlag(addingAllEvent.getFlag()); - game.fireEvent(addedAllEvent); - } - } else { - returnCode = false; - } - return returnCode; - } - - @Override - public void removeCounters(String name, int amount, Ability source, Game game) { - int finalAmount = 0; - for (int i = 0; i < amount; i++) { - if (!counters.removeCounter(name, 1)) { - break; - } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(1); - game.fireEvent(event); - finalAmount++; - } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(finalAmount); - game.fireEvent(event); - } - - protected boolean canDamage(MageObject source, Game game) { - for (ProtectionAbility ability : abilities.getProtectionAbilities()) { - if (!ability.canTarget(source, game)) { - return false; - } - } - return true; - } - - @Override - public Abilities getAbilities() { - return this.abilities; - } - - @Override - public void addAbility(Ability ability) { - ability.setSourceId(playerId); - this.abilities.add(ability); - } - - @Override - public int getLandsPerTurn() { - return this.landsPerTurn; - } - - @Override - public void setLandsPerTurn(int landsPerTurn) { - this.landsPerTurn = landsPerTurn; - } - - @Override - public int getLoyaltyUsePerTurn() { - return this.loyaltyUsePerTurn; - } - - @Override - public void setLoyaltyUsePerTurn(int loyaltyUsePerTurn) { - this.loyaltyUsePerTurn = loyaltyUsePerTurn; - } - - @Override - public int getMaxHandSize() { - return maxHandSize; - } - - @Override - public void setMaxHandSize(int maxHandSize) { - this.maxHandSize = maxHandSize; - } - - @Override - public void setMaxAttackedBy(int maxAttackedBy) { - this.maxAttackedBy = maxAttackedBy; - } - - @Override - public int getMaxAttackedBy() { - return maxAttackedBy; - } - - @Override - public void setResponseString(String responseString) { - } - - @Override - public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) { - } - - @Override - public void setResponseUUID(UUID responseUUID) { - } - - @Override - public void setResponseBoolean(Boolean responseBoolean) { - } - - @Override - public void setResponseInteger(Integer responseInteger) { - } - - @Override - public boolean isPassed() { - return passed; - } - - @Override - public void pass(Game game) { - this.passed = true; - resetStoredBookmark(game); - } - - @Override - public void resetPassed() { - this.passed = this.loses || this.hasLeft(); - } - - @Override - public void resetPlayerPassedActions() { - this.passed = false; - this.passedTurn = false; - this.passedTurnSkipStack = false; - this.passedUntilEndOfTurn = false; - this.passedUntilNextMain = false; - this.passedUntilStackResolved = false; - this.dateLastAddedToStack = null; - this.passedUntilEndStepBeforeMyTurn = false; - this.skippedAtLeastOnce = false; - this.passedAllTurns = false; - this.justActivatedType = null; - } - - @Override - public void quit(Game game) { - quit = true; - this.concede(game); - logger.debug(getName() + " quits the match."); - game.informPlayers(getLogName() + " quits the match."); - } - - @Override - public void timerTimeout(Game game) { - quit = true; - timerTimeout = true; - this.concede(game); - game.informPlayers(getLogName() + " has run out of time, losing the match."); - } - - @Override - public void idleTimeout(Game game) { - quit = true; - idleTimeout = true; - this.concede(game); - game.informPlayers(getLogName() + " was idle for too long, losing the Match."); - } - - @Override - public void concede(Game game) { - game.setConcedingPlayer(playerId); - lost(game); -// this.left = true; - } - - @Override - public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { - switch (playerAction) { - case PASS_PRIORITY_UNTIL_MY_NEXT_TURN: // F9 - resetPlayerPassedActions(); - passedAllTurns = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_TURN_END_STEP: // F5 - resetPlayerPassedActions(); - passedUntilEndOfTurn = true; - skippedAtLeastOnce = PhaseStep.END_TURN != game.getTurn().getStepType(); - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4 - resetPlayerPassedActions(); - passedTurn = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_TURN_SKIP_STACK: // F6 - resetPlayerPassedActions(); - passedTurnSkipStack = true; - this.skip(); - break; - case PASS_PRIORITY_UNTIL_NEXT_MAIN_PHASE: //F7 - resetPlayerPassedActions(); - passedUntilNextMain = true; - skippedAtLeastOnce = !(game.getTurn().getStepType() == PhaseStep.POSTCOMBAT_MAIN - || game.getTurn().getStepType() == PhaseStep.PRECOMBAT_MAIN); - this.skip(); - break; - case PASS_PRIORITY_UNTIL_STACK_RESOLVED: // Default F10 - Skips until the current stack is resolved - if (!game.getStack().isEmpty()) { // If stack is empty do nothing - resetPlayerPassedActions(); - passedUntilStackResolved = true; - dateLastAddedToStack = game.getStack().getDateLastAdded(); - this.skip(); - } - break; - case PASS_PRIORITY_UNTIL_END_STEP_BEFORE_MY_NEXT_TURN: //F11 - resetPlayerPassedActions(); - passedUntilEndStepBeforeMyTurn = true; - this.skip(); - break; - case PASS_PRIORITY_CANCEL_ALL_ACTIONS: - resetPlayerPassedActions(); - break; - case PERMISSION_REQUESTS_ALLOWED_OFF: - userData.setAllowRequestShowHandCards(false); - break; - case PERMISSION_REQUESTS_ALLOWED_ON: - userData.setAllowRequestShowHandCards(true); - userData.resetRequestedHandPlayersList(game.getId()); // users can send request again - break; - } - logger.trace("PASS Priority: " + playerAction); - } - - @Override - public void leave() { - this.passed = true; - this.loses = true; - this.left = true; - this.abort(); - //20100423 - 800.4a - this.hand.clear(); - this.graveyard.clear(); - this.library.clear(); - } - - @Override - public boolean hasLeft() { - return this.left; - } - - @Override - public void lost(Game game) { - if (canLose(game)) { - lostForced(game); - } - } - - @Override - public void lostForced(Game game) { - logger.debug(this.getName() + " has lost gameId: " + game.getId()); - //20100423 - 603.9 - if (!this.wins) { - this.loses = true; - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LOST, null, null, playerId)); - game.informPlayers(this.getLogName() + " has lost the game."); - } else { - logger.debug(this.getName() + " has already won - stop lost"); - } - // for draw - first all players that have lost have to be set to lost - if (!hasLeft()) { - logger.debug("Game over playerId: " + playerId); - game.setConcedingPlayer(playerId); - } - } - - @Override - public boolean canLose(Game game) { - return hasLeft() // If a player concedes or has left the match they lose also if effect would say otherwise - || !game.replaceEvent(new GameEvent(GameEvent.EventType.LOSES, null, null, playerId)); - } - - @Override - public void won(Game game) { - boolean opponentInGame = false; - for (UUID opponentId : game.getOpponents(playerId)) { - Player opponent = game.getPlayer(opponentId); - - if (opponent != null && opponent.isInGame()) { - opponentInGame = true; - break; - } - } - if (!opponentInGame - || // if no more opponent is in game the wins event may no longer be replaced - !game.replaceEvent(new GameEvent(GameEvent.EventType.WINS, null, null, playerId))) { - logger.debug("player won -> start: " + this.getName()); - if (!this.loses) { - //20130501 - 800.7, 801.16 - // all opponents in range loose the game - for (UUID opponentId : game.getOpponents(playerId)) { - Player opponent = game.getPlayer(opponentId); - if (opponent != null && !opponent.hasLost()) { - logger.debug("player won -> calling opponent lost: " - + this.getName() + " opponent: " + opponent.getName()); - opponent.lostForced(game); - } - } - // if no more opponents alive (independant from range), you win and the game ends - int opponentsAlive = 0; - for (UUID playerIdToCheck : game.getPlayerList()) { - if (game.isOpponent(this, playerIdToCheck)) { // Check without range - Player opponent = game.getPlayer(playerIdToCheck); - if (opponent != null && !opponent.hasLost()) { - opponentsAlive++; - } - } - } - if (opponentsAlive == 0 && !hasWon()) { - logger.debug("player won -> No more opponents alive game won: " + this.getName()); - game.informPlayers(this.getLogName() + " has won the game"); - this.wins = true; - game.end(); - } - } else { - logger.debug("player won -> but already lost before or other players still alive: " + this.getName()); - } - } - } - - @Override - public void drew(Game game) { - if (!hasLost()) { - this.draws = true; - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); - game.informPlayers("For " + this.getLogName() + " the game is a draw."); - game.setConcedingPlayer(playerId); - } - } - - @Override - public boolean hasLost() { - return this.loses; - } - - @Override - public boolean isInGame() { - return !hasQuit() && !hasLost() && !hasWon() && !hasDrew() && !hasLeft(); - } - - @Override - public boolean canRespond() { // abort is checked here to get out of player requests (as example: after disconnect) - return isInGame() && !abort; - } - - @Override - public boolean hasWon() { - return !this.loses && this.wins; - } - - @Override - public boolean hasDrew() { - return this.draws; - } - - @Override - public void declareAttacker(UUID attackerId, UUID defenderId, Game game, boolean allowUndo) { - if (allowUndo) { - setStoredBookmark(game.bookmarkState()); // makes it possible to UNDO a declared attacker with costs from e.g. Propaganda - } - Permanent attacker = game.getPermanent(attackerId); - if (attacker != null - && attacker.canAttack(defenderId, game) - && attacker.isControlledBy(playerId)) { - if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) { - game.undo(playerId); - } - } - } - - @Override - public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game) { - declareBlocker(defenderId, blockerId, attackerId, game, true); - } - - @Override - public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game, boolean allowUndo) { - if (isHuman() && allowUndo) { - setStoredBookmark(game.bookmarkState()); - } - Permanent blocker = game.getPermanent(blockerId); - CombatGroup group = game.getCombat().findGroup(attackerId); - if (blocker != null && group != null && group.canBlock(blocker, game)) { - group.addBlocker(blockerId, playerId, game); - game.getCombat().addBlockingGroup(blockerId, attackerId, playerId, game); - } else if (this.isHuman() && !game.isSimulation()) { - game.informPlayer(this, "You can't block this creature."); - } - } - - @Override - public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game) { - return searchLibrary(target, source, game, playerId); - } - - @Override - public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { - //20091005 - 701.14c - - // searching control can be intercepted by another player, see Opposition Agent - SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source, playerId, Integer.MAX_VALUE); - if (game.replaceEvent(searchEvent)) { - return false; - } - - Player targetPlayer = game.getPlayer(targetPlayerId); - Player searchingPlayer = this; - Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId()); - if (targetPlayer == null || searchingController == null) { - return false; - } - - String searchInfo = searchingPlayer.getLogName(); - if (!searchingPlayer.getId().equals(searchingController.getId())) { - searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName(); - } - if (targetPlayer.getId().equals(searchingPlayer.getId())) { - searchInfo = searchInfo + " searches their library"; - } else { - searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName(); - } - - if (!game.isSimulation()) { - game.informPlayers(searchInfo + CardUtil.getSourceLogName(game, source)); - } - - // https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/ - // You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect: - // * While you’re searching your library, you may cast Panglacial Wurm from your library. - // So use here same code as Word of Command - // P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest - boolean takeControl = false; - if (!searchingPlayer.getId().equals(searchingController.getId())) { - CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true); - takeControl = true; - } - - Library searchingLibrary = targetPlayer.getLibrary(); - TargetCardInLibrary newTarget = target.copy(); - int count; - int librarySearchLimit = searchEvent.getAmount(); - List cardsFromTop = null; - do { - // TODO: prevent shuffling from moving the visualized cards - if (librarySearchLimit == Integer.MAX_VALUE) { - count = searchingLibrary.count(target.getFilter(), game); - } else { - if (cardsFromTop == null) { - cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit)); - } else { - cardsFromTop.retainAll(searchingLibrary.getCards(game)); - } - newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); - count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit); - } - - if (count < target.getNumberOfTargets()) { - newTarget.setMinNumberOfTargets(count); - } - - // handling Panglacial Wurm - cast cards while searching from own library - if (targetPlayer.getId().equals(searchingPlayer.getId())) { - if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) { - // clear all choices to start from scratch (casted cards must be removed from library) - newTarget.clearChosen(); - continue; - } - } - - if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) { - target.getTargets().clear(); - for (UUID targetId : newTarget.getTargets()) { - target.add(targetId, game); - } - } - - // END SEARCH - if (takeControl) { - CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer); - game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back" + CardUtil.getSourceLogName(game, source)); - } - - LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source, searchingPlayer.getId(), target); - if (!game.replaceEvent(searchedEvent)) { - game.fireEvent(searchedEvent); - } - break; - } while (true); - - return true; - } - - @Override - public boolean seekCard(FilterCard filter, Ability source, Game game) { - Set cards = this.getLibrary() - .getCards(game) - .stream() - .filter(card -> filter.match(card, source.getSourceId(), getId(), game)) - .collect(Collectors.toSet()); - Card card = RandomUtil.randomFromCollection(cards); - if (card == null) { - return false; - } - game.informPlayers(this.getLogName() + " seeks a card from their library"); - this.moveCards(card, Zone.HAND, source, game); - return true; - } - - @Override - public void lookAtAllLibraries(Ability source, Game game) { - for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { - Player player = game.getPlayer(playerId); - String playerName = this.getName().equals(player.getName()) ? "Your " : player.getName() + "'s "; - playerName += "library"; - Cards cardsInLibrary = new CardsImpl(player.getLibrary().getTopCards(game, player.getLibrary().size())); - lookAtCards(playerName, cardsInLibrary, game); - } - } - - private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) { - // must return true after cast try (to restart searching process without casted cards) - // uses for handling Panglacial Wurm: - // * While you're searching your library, you may cast Panglacial Wurm from your library. - - List castableCards = library.getCards(game).stream() - .filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class)) - .map(MageItem::getId) - .collect(Collectors.toList()); - if (castableCards.size() == 0) { - return false; - } - - // only humans can use it - if (targetPlayer.isComputer()) { - return false; - } - - if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "There are " + castableCards.size() + " cards you can cast while searching your library. Cast any of them?", null, game)) { - return false; - } - - boolean casted = false; - TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD); - targetCard.setTargetName("card to cast from library"); - targetCard.setNotTarget(true); - while (castableCards.size() > 0) { - targetCard.clearChosen(); - if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) { - break; - } - - Card card = game.getCard(targetCard.getFirstTarget()); - if (card == null) { - break; - } - - // AI NOTICE: if you want AI implement here then remove selected card from castable after each - // choice (otherwise you catch infinite freeze on uncastable use case) - // casting selected card - // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - castableCards.remove(card.getId()); - casted = true; - } - return casted; - } - - /** - * @param source - * @param game - * @param winnable - * @return if winnable, true if player won the toss, if not winnable, true - * for heads and false for tails - */ - @Override - public boolean flipCoin(Ability source, Game game, boolean winnable) { - boolean chosen = false; - if (winnable) { - chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); - game.informPlayers(getLogName() + " chose " + CardUtil.booleanToFlipName(chosen)); - } - boolean result = this.flipCoinResult(game); - FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable); - game.replaceEvent(event); - game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult()) - + CardUtil.getSourceLogName(game, source)); - if (event.getFlipCount() > 1) { - boolean canChooseHeads = event.getResult(); - boolean canChooseTails = !event.getResult(); - for (int i = 1; i < event.getFlipCount(); i++) { - boolean tempFlip = this.flipCoinResult(game); - canChooseHeads = canChooseHeads || tempFlip; - canChooseTails = canChooseTails || !tempFlip; - game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(tempFlip)); - } - if (canChooseHeads && canChooseTails) { - event.setResult(chooseUse(Outcome.Benefit, "Choose which flip to keep", - (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), - "Heads", "Tails", source, game - )); - } else { - event.setResult(canChooseHeads); - } - game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); - } - if (event.isWinnable()) { - game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip" - + CardUtil.getSourceLogName(game, source)); - } - game.fireEvent(event.createFlippedEvent()); - if (event.isWinnable()) { - return event.getResult() == event.getChosen(); - } - return event.getResult(); - } - - /** - * Return result for next flip coint try (can be contolled in tests) - * - * @return - */ - @Override - public boolean flipCoinResult(Game game) { - return RandomUtil.nextBoolean(); - } - - private static final class RollDieResult { - - // 706.2. - // After the roll, the number indicated on the top face of the die before any modifiers is - // the natural result. The instruction may include modifiers to the roll which add to or - // subtract from the natural result. Modifiers may also come from other sources. After - // considering all applicable modifiers, the final number is the result of the die roll. - private final int naturalResult; - private final int modifier; - private final PlanarDieRollResult planarResult; - - RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) { - this.naturalResult = naturalResult; - this.modifier = modifier; - this.planarResult = planarResult; - } - - public int getResult() { - return this.naturalResult + this.modifier; - } - - public PlanarDieRollResult getPlanarResult() { - return this.planarResult; - } - } - - @Override - public int rollDieResult(int sides, Game game) { - return RandomUtil.nextInt(sides) + 1; - } - - /** - * Roll single die. Support both die types: planar and numerical. - * - * @param outcome - * @param game - * @param source - * @param rollDieType - * @param sidesAmount - * @param chaosSidesAmount - * @param planarSidesAmount - * @param rollsAmount - * @return - */ - private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { - if (rollsAmount == 1) { - return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); - } - Set choices = new HashSet<>(); - for (int j = 0; j < rollsAmount; j++) { - choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount)); - } - if (choices.size() == 1) { - return choices.stream().findFirst().orElse(0); - } - - // AI hint - use max/min values - if (this.isComputer()) { - if (rollDieType == RollDieType.NUMERICAL) { - // numerical - if (outcome.isGood()) { - return choices.stream() - .map(Integer.class::cast) - .max(Comparator.naturalOrder()) - .orElse(null); - } else { - return choices.stream() - .map(Integer.class::cast) - .min(Comparator.naturalOrder()) - .orElse(null); - } - } else { - // planar - // priority: chaos -> planar -> blank - return choices.stream() - .map(PlanarDieRollResult.class::cast) - .max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority)) - .orElse(null); - } - } - - Choice choice = new ChoiceImpl(true); - choice.setMessage("Choose which die roll result to keep (the rest will be ignored)"); - choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet())); - - this.choose(Outcome.Neutral, choice, game); - Object defaultChoice = choices.iterator().next(); - return choices.stream() - .filter(o -> o.toString().equals(choice.getChoice())) - .findFirst() - .orElse(defaultChoice); - } - - private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) { - switch (rollDieType) { - - case NUMERICAL: { - int result = rollDieResult(numSides, game); - // Clam-I-Am workaround: - // If you roll a 3 on a six-sided die, you may reroll that die. - if (numSides == 6 - && result == 3 - && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId())) - && chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) { - result = rollDieResult(numSides, game); - } - return result; - } - - case PLANAR: { - if (numChaosSides + numPlanarSides > numSides) { - numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES; - numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES; - } - // for 9 sides: - // 1..2 - chaos - // 3..7 - blank - // 8..9 - planar - int result = this.rollDieResult(numSides, game); - PlanarDieRollResult roll; - if (result <= numChaosSides) { - roll = PlanarDieRollResult.CHAOS_ROLL; - } else if (result > numSides - numPlanarSides) { - roll = PlanarDieRollResult.PLANAR_ROLL; - } else { - roll = PlanarDieRollResult.BLANK_ROLL; - } - return roll; - } - - default: { - throw new IllegalArgumentException("Unknown roll die type " + rollDieType); - } - } - } - - /** - * @param outcome - * @param source - * @param game - * @param sidesAmount number of sides the dice has - * @param rollsAmount number of tries to roll the dice - * @param ignoreLowestAmount remove the lowest rolls from the results - * @return the number that the player rolled - */ - @Override - public List rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) { - return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount) - .stream() - .map(Integer.class::cast) - .collect(Collectors.toList()); - } - - /** - * Inner code to roll a dice. Support normal and planar types. - * - * @param outcome - * @param source - * @param game - * @param rollDieType die type to roll, e.g. planar or numerical - * @param sidesAmount sides per die - * @param chaosSidesAmount for planar die: chaos sides - * @param planarSidesAmount for planar die: planar sides - * @param rollsAmount rolls - * @param ignoreLowestAmount for numerical die: ignore multiple rolls with - * the lowest values - * @return - */ - private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, - int sidesAmount, int chaosSidesAmount, int planarSidesAmount, - int rollsAmount, int ignoreLowestAmount) { - RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); - if (ignoreLowestAmount > 0) { - rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); - } - game.replaceEvent(rollDiceEvent); - - // 706.6. - // In a Planechase game, rolling the planar die will cause any ability that triggers whenever a - // player rolls one or more dice to trigger. However, any effect that refers to a numerical - // result of a die roll, including ones that compare the results of that roll to other rolls - // or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.” - // ROLL MULTIPLE dies - // results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x) - List dieResults = new ArrayList<>(); - List dieRolls = new ArrayList<>(); - for (int i = 0; i < rollDiceEvent.getAmount(); i++) { - // ROLL SINGLE die - RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides()); - game.replaceEvent(rollDieEvent); - - Object rollResult; - // big idea logic for numerical rolls only - if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) { - // rolls 2x + sum results - // The Big Idea: roll two six-sided dice and use the total of those results - int totalSum = 0; - for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) { - int singleResult = (Integer) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount()); - totalSum += singleResult; - dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null)); - } - rollResult = totalSum; - } else { - // rolls 1x - switch (rollDieEvent.getRollDieType()) { - default: - case NUMERICAL: { - int naturalResult = (Integer) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount() - ); - dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null)); - rollResult = naturalResult; - break; - } - - case PLANAR: { - PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner( - outcome, - game, - source, - rollDieEvent.getRollDieType(), - rollDieEvent.getSides(), - chaosSidesAmount, - planarSidesAmount, - rollDieEvent.getRollsAmount() - ); - dieRolls.add(new RollDieResult(0, 0, planarResult)); - rollResult = planarResult; - break; - } - } - } - dieResults.add(rollResult); - } - - // ignore the lowest results - // planar dies: due to 706.6. planar die results must be fully ignored - // - // 706.5. - // If a player is instructed to roll two or more dice and ignore the lowest roll, the roll - // that yielded the lowest result is considered to have never happened. No abilities trigger - // because of the ignored roll, and no effects apply to that roll. If multiple results are tied - // for the lowest, the player chooses one of those rolls to be ignored. - int diceRolledTotal = dieRolls.size(); - String ignoreMessage; - if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) { - // find ignored values - List ignoredResults = new ArrayList<>(); - for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) { - int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0); - dieResults.remove(Integer.valueOf(min)); - ignoredResults.add(min); - } - ignoreMessage = String.format( - ignoredResults.size() > 1 ? ", ignoring [%s]" : ", ignoring %s", - ignoredResults - .stream() - .map(x -> "" + x) - .collect(Collectors.joining(", ")) - ); - // remove ignored rolls (they not exist anymore) - List newRolls = new ArrayList<>(); - for (RollDieResult rollDieResult : dieRolls) { - if (ignoredResults.contains(rollDieResult.getResult())) { - ignoredResults.remove((Integer) rollDieResult.getResult()); - } else { - newRolls.add(rollDieResult); - } - } - dieRolls.clear(); - dieRolls.addAll(newRolls); - } else { - ignoreMessage = ""; - } - - // raise affected roll events - for (RollDieResult result : dieRolls) { - game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult)); - } - game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source)); - - String resultString = dieResults - .stream() - .map(Object::toString) - .collect(Collectors.joining(", ")); - String message; - switch (rollDiceEvent.getRollDieType()) { - default: - case NUMERICAL: - // [Roll a die] user rolled 4d6, results: [4, 6], ignoring [1, 3] (source: xxx) - message = String.format("[Roll a die] %s rolled %sd%s, result%s: %s%s%s", - getLogName(), - diceRolledTotal > 1 ? diceRolledTotal : "a ", - rollDiceEvent.getSides(), - dieResults.size() > 1 ? 's' : "", - dieResults.size() > 1 ? '[' + resultString + ']' : resultString, - ignoreMessage, - CardUtil.getSourceLogName(game, source)); - break; - case PLANAR: - // [Roll a planar die] user rolled CHAOS (source: xxx) - message = String.format("[Roll a planar die] %s rolled %s%s", - getLogName(), - dieResults.size() > 1 ? '[' + resultString + ']' : resultString, - CardUtil.getSourceLogName(game, source)); - break; - } - game.informPlayers(message); - return dieResults; - } - - /** - * @param source - * @param game - * @param chaosSidesAmount The number of chaos sides the planar die - * currently has (normally 1 but can be 5) - * @param planarSidesAmount The number of chaos sides the planar die - * currently has (normally 1) - * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll - * or BlankRoll - */ - @Override - public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) { - return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0) - .stream() - .map(o -> (PlanarDieRollResult) o) - .findFirst() - .orElse(PlanarDieRollResult.BLANK_ROLL); - } - - @Override - public List getAvailableAttackers(Game game) { - // TODO: get available opponents and their planeswalkers, check for each if permanent can attack one - return getAvailableAttackers(null, game); - } - - @Override - public List getAvailableAttackers(UUID defenderId, Game game) { - FilterCreatureForCombat filter = new FilterCreatureForCombat(); - List attackers = game.getBattlefield().getAllActivePermanents(filter, playerId, game); - attackers.removeIf(entry -> !entry.canAttack(defenderId, game)); - return attackers; - } - - @Override - public List getAvailableBlockers(Game game) { - FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); - return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game); - } - - /** - * Returns the mana options the player currently has. That means which - * combinations of mana are available to cast spells or activate abilities - * etc. - * - * @param game - * @return - */ - @Override - public ManaOptions getManaAvailable(Game game) { - boolean oldState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - - ManaOptions availableMana = new ManaOptions(); - availableMana.addMana(manaPool.getMana()); - // conditional mana - for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { - availableMana.addMana(conditionalMana); - } - - List> sourceWithoutManaCosts = new ArrayList<>(); - List> sourceWithCosts = new ArrayList<>(); - for (Card card : getHand().getCards(game)) { - Abilities manaAbilities - = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { - ActivatedManaAbilityImpl ability = it.next(); - Abilities noTapAbilities = new AbilitiesImpl<>(ability); - if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { - sourceWithoutManaCosts.add(noTapAbilities); - } else { - sourceWithCosts.add(noTapAbilities); - } - } - } - - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range - Boolean canUse = null; - boolean canAdd = false; - boolean useLater = false; // sources with mana costs or mana pool dependency - Abilities manaAbilities - = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true - for (Iterator it = manaAbilities.iterator(); it.hasNext();) { - ActivatedManaAbilityImpl ability = it.next(); - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse) { - // abilities without Tap costs have to be handled as separate sources, because they can be used also - if (!ability.hasTapCost()) { - it.remove(); - Abilities noTapAbilities = new AbilitiesImpl<>(ability); - if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { - sourceWithoutManaCosts.add(noTapAbilities); - } else { - sourceWithCosts.add(noTapAbilities); - } - continue; - } - - canAdd = true; - if (!ability.getManaCosts().isEmpty() || ability.isPoolDependant()) { - useLater = true; - break; - } - } - } - if (canAdd) { - if (useLater) { - sourceWithCosts.add(manaAbilities); - } else { - sourceWithoutManaCosts.add(manaAbilities); - } - } - } - - for (Abilities manaAbilities : sourceWithoutManaCosts) { - availableMana.addMana(manaAbilities, game); - } - - boolean anAbilityWasUsed = true; - boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production - while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { - anAbilityWasUsed = false; - for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { - Abilities manaAbilities = iterator.next(); - if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { - boolean used; - if (manaAbilities.hasPoolDependantAbilities()) { - used = availableMana.addManaPoolDependant(manaAbilities, game); - } else { - used = availableMana.addManaWithCost(manaAbilities, game); - } - if (used) { - iterator.remove(); - availableMana.removeDuplicated(); - anAbilityWasUsed = true; - } - } - } - if (!anAbilityWasUsed && !usePoolDependantAbilities) { - usePoolDependantAbilities = true; - anAbilityWasUsed = true; - } - } - - // remove duplicated variants (see ManaOptionsTest for info - when that rises) - availableMana.removeDuplicated(); - - game.setCheckPlayableState(oldState); - return availableMana; - } - - /** - * Used during calculation of available mana to gather the amount of - * producable triggered mana caused by using mana sources. So the set value - * is only used during the calculation of the mana produced by one source - * and cleared thereafter - * - * @param netManaAvailable the net mana produced by the triggered mana - * abaility - */ - @Override - public void addAvailableTriggeredMana(List netManaAvailable - ) { - this.availableTriggeredManaList.add(netManaAvailable); - } - - /** - * Used during calculation of available mana to get the amount of producable - * triggered mana caused by using mana sources. The list is cleared as soon - * the value is retrieved during available mana calculation. - * - * @return - */ - @Override - public List> getAvailableTriggeredMana() { - return availableTriggeredManaList; - } - // returns only mana producers that don't require mana payment - - protected List getAvailableManaProducers(Game game) { - List result = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range - Boolean canUse = null; - boolean canAdd = false; - for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { - if (!ability.getManaCosts().isEmpty()) { - canAdd = false; - break; - } - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse && ability.canActivate(playerId, game).canActivate()) { - canAdd = true; - } - } - if (canAdd) { - result.add(permanent); - } - } - for (Card card : getHand().getCards(game)) { - boolean canAdd = false; - for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.HAND)) { - if (!ability.getManaCosts().isEmpty()) { - canAdd = false; - break; - } - if (ability.canActivate(playerId, game).canActivate()) { - canAdd = true; - } - } - if (canAdd) { - result.add(card); - } - } - return result; - } - - // returns only mana producers that require mana payment - public List getAvailableManaProducersWithCost(Game game) { - List result = new ArrayList<>(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { - Boolean canUse = null; - for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { - if (canUse == null) { - canUse = permanent.canUseActivatedAbilities(game); - } - if (canUse && ability.canActivate(playerId, game).canActivate() - && !ability.getManaCosts().isEmpty()) { - result.add(permanent); - break; - } - } - } - return result; - } - - /** - * @param ability - * @param availableMana if null, it won't be checked if enough mana is - * available - * @param sourceObject - * @param game - * @return - */ - protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { - if (!(ability instanceof ActivatedManaAbilityImpl)) { - ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability - if (!copy.canActivate(playerId, game).canActivate()) { - return false; - } - if (availableMana != null) { - sourceObject.adjustCosts(copy, game); - game.getContinuousEffects().costModification(copy, game); - } - boolean canBeCastRegularly = true; - if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { - // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... - // 117.6a (...) If an alternative cost is applied to an unpayable cost, - // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. - canBeCastRegularly = false; - } - if (canBeCastRegularly) { - if (canPayMinimumManaCost(copy, availableMana, game)) { - return true; - } - } - - // ALTERNATIVE COST FROM dynamic effects - if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); - Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); - - boolean canPutToPlay = true; - if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - if (costs != null && !costs.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - - if (canPutToPlay) { - return true; - } - } - - // ALTERNATIVE COST from source card (any AlternativeSourceCosts) - if (AbilityType.SPELL.equals(ability.getAbilityType())) { - return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); - } - } - return false; - } - - protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { - ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); - if (abilityOptions.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - // Check for pay option with like phyrexian mana - if (getPhyrexianColors() != null) { - addPhyrexianLikePayOptions(abilityOptions, availableMana, game); - } - - ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), - AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); - for (Mana mana : abilityOptions) { - if (mana.count() == 0) { - return true; - } - for (Mana avail : availableMana) { - // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, - // but that code processing it as any color, need to test and fix another use cases - // (example: Sunglasses of Urza - may spend white mana as though it were red mana) - - // - // add tests for non any color like Sunglasses of Urza - if (approvingObject != null && mana.count() <= avail.count()) { - return true; - } - if (avail instanceof ConditionalMana && !((ConditionalMana) avail).apply(ability, game, getId(), ability.getManaCosts())) { - continue; - } - if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost - return true; - } - } - } - } - return false; - } - - private void addPhyrexianLikePayOptions(ManaOptions abilityOptions, ManaOptions availableMana, Game game) { - int maxLifeMana = getLife() / 2; - if (maxLifeMana > 0) { - Set phyrexianOptions = new HashSet<>(); - for (Mana mana : abilityOptions) { - int availableLifeMana = maxLifeMana; - if (getPhyrexianColors().isBlack()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLACK); - } - if (getPhyrexianColors().isBlue()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLUE); - } - if (getPhyrexianColors().isRed()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.RED); - } - if (getPhyrexianColors().isGreen()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.GREEN); - } - if (getPhyrexianColors().isWhite()) { - createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.WHITE); - } - } - abilityOptions.addAll(phyrexianOptions); - } - } - - private int createReducedManaPayOption(int availableLifeMana, Mana oldPayOption, Set phyrexianOptions, ManaType manaType) { - if (oldPayOption.get(manaType) > 0) { - Mana manaCopy = oldPayOption.copy(); - int restVal; - if (availableLifeMana > oldPayOption.get(manaType)) { - restVal = 0; - availableLifeMana -= oldPayOption.get(manaType); - } else { - restVal = CardUtil.overflowDec(oldPayOption.get(manaType), availableLifeMana); - availableLifeMana = 0; - } - manaCopy.set(manaType, restVal); - phyrexianOptions.add(manaCopy); - } - return availableLifeMana; - } - - protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { - if (sourceObject != null && !(sourceObject instanceof Permanent)) { - Ability copyAbility; // for alternative cost and reduce tries - for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { - // if cast for noMana no Alternative costs are allowed - if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { - if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { - if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : alternateSourceCostsAbility.getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.getManaCostsToPay().clear(); - copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); - sourceObject.adjustCosts(copyAbility, game); - game.getContinuousEffects().costModification(copyAbility, game); - - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } - - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } - } - } - } - - // controller specific alternate spell costs - for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { - if (alternateSourceCosts instanceof Ability) { - if (alternateSourceCosts.isAvailable(ability, game)) { - if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost2) { - if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.getManaCostsToPay().clear(); - copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); - sourceObject.adjustCosts(copyAbility, game); - game.getContinuousEffects().costModification(copyAbility, game); - - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } - - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } - } - } - } - } - return false; - } - - protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { - - // special mana to pay spell cost - ManaOptions manaFull = availableMana.copy(); - if (ability instanceof SpellAbility) { - for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() - .filter(a -> a instanceof AlternateManaPaymentAbility) - .map(a -> (AlternateManaPaymentAbility) a) - .collect(Collectors.toList())) { - ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay()); - manaFull.addMana(manaSpecial); - } - } - - // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) - if (ability instanceof ActivatedManaAbilityImpl) { - // mana ability - if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { - return (ActivatedManaAbilityImpl) ability; - } - } else if (ability instanceof AlternativeSourceCosts) { - // alternative cost must be replaced by real play ability - return findActivatedAbilityFromAlternativeSourceCost(object, manaFull, ability, game); - } else if (ability instanceof ActivatedAbility) { - // all other activated ability - if (canPlay((ActivatedAbility) ability, manaFull, object, game)) { - return (ActivatedAbility) ability; - } - } - - // non playable abilities like static - return null; - } - - protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { - // return play ability that can activate AlternativeSourceCosts - if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { - ActivatedAbility playAbility = null; - if (object.isLand(game)) { - playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); - } else if (object instanceof Card) { - playAbility = ((Card) object).getSpellAbility(); - } - if (playAbility == null) { - return null; - } - - // 707.4.Objects that are cast face down are turned face down before they are put onto the stack - // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc - // Even mana cost can't be checked here without lookahead - // So make it available all the time - boolean canUse; - if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) - || (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { - canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); - } else { - canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions - } - - if (canUse) { - return playAbility; - } - } - return null; - } - - private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { - if (fromZone == null || object == null) { - return; - } - - // BASIC abilities - if (object instanceof SplitCard) { - SplitCard mainCard = (SplitCard) object; - getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof ModalDoubleFacesCard) { - ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) object; - getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof AdventureCard) { - // adventure must use different card characteristics for different spells (main or adventure) - AdventureCard adventureCard = (AdventureCard) object; - getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof Card) { - getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); - } else if (object instanceof StackObject) { - // spells on stack are processing by Card above, other stack objects must be ignored - } else { - // other things like CommandObject - getPlayableFromObjectSingle(game, fromZone, object, object.getAbilities(), availableMana, output); - } - - // DYNAMIC ADDED abilities are adds in getAbilities(game) - } - - private void getPlayableFromObjectSingle(Game game, Zone fromZone, MageObject object, Abilities candidateAbilities, ManaOptions availableMana, List output) { - // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) - // must check all abilities, not activated only - for (Ability ability : candidateAbilities) { - if (!(ability instanceof ActivatedAbility)) { - continue; - } - boolean isPlaySpell = (ability instanceof SpellAbility); - boolean isPlayLand = (ability instanceof PlayLandAbility); - - // as original controller - // play land restrictions - if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( - GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), - ability, this.getId()), ability, game, true)) { - continue; - } - // cast spell restrictions 1 - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, this.getId()); - castEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castEvent, ability, game, true)) { - continue; - } - // cast spell restrictions 2 - GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, - ability.getId(), ability, this.getId()); - castLateEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castLateEvent, ability, game, true)) { - continue; - } - - ApprovingObject approvingObject; - if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { - // play hand from non hand zone (except battlefield - you can't play already played permanents) - approvingObject = game.getContinuousEffects().asThough(object.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); - } else { - // other abilities from direct zones - approvingObject = null; - } - - boolean canActivateAsHandZone = approvingObject != null - || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); - boolean possibleToPlay = canActivateAsHandZone - && ability.getZone().match(Zone.HAND) - && (isPlaySpell || isPlayLand); - - // spell/hand abilities (play from all zones) - // need permitingObject or canPlayCardsFromGraveyard - // zone's abilities (play from specific zone) - // no need in permitingObject - if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { - possibleToPlay = true; - } - - if (!possibleToPlay) { - continue; - } - - // direct mode (with original controller) - ActivatedAbility playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); - if (playAbility != null && !output.contains(playAbility)) { - output.add(playAbility); - continue; - } - - // from non hand mode (with affected controller) - if (canActivateAsHandZone && ability.getControllerId() != this.getId()) { - UUID savedControllerId = ability.getControllerId(); - ability.setControllerId(this.getId()); - try { - playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); - if (playAbility != null && !output.contains(playAbility)) { - output.add(playAbility); - } - } finally { - ability.setControllerId(savedControllerId); - } - } - } - } - - @Override - public List getPlayable(Game game, boolean hidden) { - return getPlayable(game, hidden, Zone.ALL, true); - } - - /** - * Returns a list of all available spells and abilities the player can - * currently cast/activate with his available resources - * - * @param game - * @param hidden also from hidden objects (e.g. turned face down cards ?) - * @param fromZone of objects from which zone (ALL = from all zones) - * @param hideDuplicatedAbilities if equal abilities exist return only the - * first instance - * @return - */ - public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { - List playable = new ArrayList<>(); - if (shouldSkipGettingPlayable(game)) { - return playable; - } - - boolean previousState = game.inCheckPlayableState(); - game.setCheckPlayableState(true); - try { - ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) - boolean fromAll = fromZone.equals(Zone.ALL); - if (hidden && (fromAll || fromZone == Zone.HAND)) { - for (Card card : hand.getCards(game)) { - for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) - if (ability.getZone().match(Zone.HAND)) { - boolean isPlaySpell = (ability instanceof SpellAbility); - boolean isPlayLand = (ability instanceof PlayLandAbility); - - // play land restrictions - if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( - GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), - ability, this.getId()), ability, game, true)) { - continue; - } - // cast spell restrictions 1 - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, - ability.getId(), ability, this.getId()); - castEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castEvent, ability, game, true)) { - continue; - } - // cast spell restrictions 2 - GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, - ability.getId(), ability, this.getId()); - castLateEvent.setZone(fromZone); - if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( - castLateEvent, ability, game, true)) { - continue; - } - - ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); - if (playAbility != null && !playable.contains(playAbility)) { - playable.add(playAbility); - } - } - } - } - } - - if (fromAll || fromZone == Zone.GRAVEYARD) { - for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerId); - if (player == null) { - continue; - } - for (Card card : player.getGraveyard().getCards(game)) { - getPlayableFromObjectAll(game, Zone.GRAVEYARD, card, availableMana, playable); - } - } - } - - if (fromAll || fromZone == Zone.EXILED) { - for (ExileZone exile : game.getExile().getExileZones()) { - for (Card card : exile.getCards(game)) { - getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); - } - } - } - - // check to play revealed cards - if (fromAll) { - for (Cards revealedCards : game.getState().getRevealed().values()) { - for (Card card : revealedCards.getCards(game)) { - // revealed cards can be from any zones - getPlayableFromObjectAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); - } - } - } - - // outside cards - if (fromAll || fromZone == Zone.OUTSIDE) { - // companion cards - for (Cards companionCards : game.getState().getCompanion().values()) { - for (Card card : companionCards.getCards(game)) { - getPlayableFromObjectAll(game, Zone.OUTSIDE, card, availableMana, playable); - } - } - - // sideboard cards (example: Wish) - for (UUID sideboardCardId : this.getSideboard()) { - Card sideboardCard = game.getCard(sideboardCardId); - if (sideboardCard != null) { - getPlayableFromObjectAll(game, Zone.OUTSIDE, sideboardCard, availableMana, playable); - } - } - } - - // check if it's possible to play the top card of a library - if (fromAll || fromZone == Zone.LIBRARY) { - for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerInRangeId); - if (player != null && player.getLibrary().hasCards()) { - Card card = player.getLibrary().getFromTop(game); - if (card != null) { - getPlayableFromObjectAll(game, Zone.LIBRARY, card, availableMana, playable); - } - } - } - } - - // check the hand zone (Sen Triplets) - // TODO: remove direct hand check (reveal fix in Sen Triplets)? - // human games: cards from opponent's hand must be revealed before play - // AI games: computer can see and play cards from opponent's hand without reveal - if (fromAll || fromZone == Zone.HAND) { - for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { - Player player = game.getPlayer(playerInRangeId); - if (player != null && !player.getHand().isEmpty()) { - for (Card card : player.getHand().getCards(game)) { - if (card != null) { - getPlayableFromObjectAll(game, Zone.HAND, card, availableMana, playable); - } - } - } - } - } - - // eliminate duplicate activated abilities (uses for AI plays) - Map activatedUnique = new HashMap<>(); - List activatedAll = new ArrayList<>(); - - // activated abilities from battlefield objects - if (fromAll || fromZone == Zone.BATTLEFIELD) { - for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { - boolean canUseActivated = permanent.canUseActivatedAbilities(game); - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.BATTLEFIELD, permanent, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - if (ability instanceof SpecialAction || canUseActivated) { - activatedUnique.putIfAbsent(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - } - - // activated abilities from stack objects - if (fromAll || fromZone == Zone.STACK) { - for (StackObject stackObject : game.getState().getStack()) { - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - activatedUnique.put(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - - // activated abilities from objects in the command zone (emblems or commanders) - if (fromAll || fromZone == Zone.COMMAND) { - for (CommandObject commandObject : game.getState().getCommand()) { - List currentPlayable = new ArrayList<>(); - getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); - for (ActivatedAbility ability : currentPlayable) { - activatedUnique.put(ability.toString(), ability); - activatedAll.add(ability); - } - } - } - - if (hideDuplicatedAbilities) { - playable.addAll(activatedUnique.values()); - } else { - playable.addAll(activatedAll); - } - } finally { - game.setCheckPlayableState(previousState); - } - - return playable; - } - - /** - * Creates a list of card ids that are currently playable.
- * Used to mark the playable cards in GameView Also contains number of - * playable abilities for that object (it's just info, server decides to - * show choose dialog or not) - * - * @param game - * @return A Set of cardIds that are playable and amount of playable - * abilities - */ - @Override - public PlayableObjectsList getPlayableObjects(Game game, Zone zone) { - // collect abilities per object - List playableAbilities = getPlayable(game, true, zone, false); // do not hide duplicated abilities/cards - Map> playableObjects = new HashMap<>(); - for (ActivatedAbility ability : playableAbilities) { - if (ability.getSourceId() != null) { - - // normal card - putToPlayableObjects(playableObjects, ability.getSourceId(), ability); - - // main card - must be marked playable in GUI - Card card = game.getCard(ability.getSourceId()); - if (card != null && card.getMainCard().getId() != card.getId()) { - putToPlayableObjects(playableObjects, card.getMainCard().getId(), ability); - } - - // spell on stack - can have activated abilities, - // so mark it as playable too (users must able to clicks on stack objects) - // example: Lightning Storm - Spell spell = game.getSpell(ability.getSourceId()); - if (spell != null) { - putToPlayableObjects(playableObjects, spell.getId(), ability); - } - } - } - - // collect stats - PlayableObjectsList playableObjectsList = new PlayableObjectsList(playableObjects); - return playableObjectsList; - } - - private void putToPlayableObjects(Map> playableObjects, UUID objectId, ActivatedAbility ability) { - if (!playableObjects.containsKey(objectId)) { - playableObjects.put(objectId, new ArrayList<>()); - } - playableObjects.get(objectId).add(ability); - } - - /** - * Skip "silent" phase step when players are not allowed to cast anything. - * E.g. players can't play or cast anything during declaring attackers. - * - * @param game - * @return - */ - private boolean shouldSkipGettingPlayable(Game game) { - if (game.getStep() == null) { // happens at the start of the game - return true; - } - for (Entry phaseStep : silentPhaseSteps.entrySet()) { - if (game.getPhase() != null - && game.getPhase().getStep() != null - && phaseStep.getKey() == game.getPhase().getStep().getType()) { - if (phaseStep.getValue() == null - || phaseStep.getValue() == game.getPhase().getStep().getStepPart()) { - return true; - } - } - } - return false; - } - - /** - * Only used for AIs - * - * @param ability - * @param game - * @return - */ - @Override - public List getPlayableOptions(Ability ability, Game game) { - List options = new ArrayList<>(); - if (ability.isModal()) { - addModeOptions(options, ability, game); - } else if (!ability.getTargets().getUnchosen().isEmpty()) { - // TODO: Handle other variable costs than mana costs - if (!ability.getManaCosts().getVariableCosts().isEmpty()) { - addVariableXOptions(options, ability, 0, game); - } else { - addTargetOptions(options, ability, 0, game); - } - } else if (!ability.getCosts().getTargets().getUnchosen().isEmpty()) { - addCostTargetOptions(options, ability, 0, game); - } - - return options; - } - - private void addModeOptions(List options, Ability option, Game game) { - // TODO: Support modal spells with more than one selectable mode - for (Mode mode : option.getModes().values()) { - Ability newOption = option.copy(); - newOption.getModes().clearSelectedModes(); - newOption.getModes().addSelectedMode(mode.getId()); - newOption.getModes().setActiveMode(mode); - if (!newOption.getTargets().getUnchosen().isEmpty()) { - if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { - addVariableXOptions(options, newOption, 0, game); - } else { - addTargetOptions(options, newOption, 0, game); - } - } else if (!newOption.getCosts().getTargets().getUnchosen().isEmpty()) { - addCostTargetOptions(options, newOption, 0, game); - } else { - options.add(newOption); - } - } - } - - protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { - addTargetOptions(options, option, targetNum, game); - } - - protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { - for (Target target : option.getTargets().getUnchosen().get(targetNum).getTargetOptions(option, game)) { - Ability newOption = option.copy(); - if (target instanceof TargetAmount) { - for (UUID targetId : target.getTargets()) { - int amount = target.getTargetAmount(targetId); - newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); - } - } else { - for (UUID targetId : target.getTargets()) { - newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); - } - } - if (targetNum < option.getTargets().size() - 2) { - addTargetOptions(options, newOption, targetNum + 1, game); - } else if (!option.getCosts().getTargets().isEmpty()) { - addCostTargetOptions(options, newOption, 0, game); - } else { - options.add(newOption); - } - } - } - - private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { - for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(option.getSourceId(), playerId, game)) { - Ability newOption = option.copy(); - newOption.getCosts().getTargets().get(targetNum).addTarget(targetId, option, game, true); - if (targetNum < option.getCosts().getTargets().size() - 1) { - addCostTargetOptions(options, newOption, targetNum + 1, game); - } else { - options.add(newOption); - } - } - } - - @Override - public boolean isTestsMode() { - return isTestMode; - } - - @Override - public void setTestMode(boolean value) { - this.isTestMode = value; - } - - @Override - public boolean isTopCardRevealed() { - return topCardRevealed; - } - - @Override - public void setTopCardRevealed(boolean topCardRevealed) { - this.topCardRevealed = topCardRevealed; - } - - @Override - public UserData getUserData() { - return this.userData; - } - - public UserData getControllingPlayersUserData(Game game) { - if (!isGameUnderControl()) { - Player player = game.getPlayer(getTurnControlledBy()); - if (player.isHuman()) { - return player.getUserData(); - } - } - return this.userData; - } - - @Override - public void setUserData(UserData userData) { - this.userData = userData; - getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); - getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); - } - - @Override - public void addAction(String action - ) { - // do nothing - } - - @Override - public int getActionCount() { - return 0; - } - - @Override - public void setAllowBadMoves(boolean allowBadMoves) { - // do nothing - } - - @Override - public boolean canPayLifeCost(Ability ability) { - if (!canPayLifeCost - && (AbilityType.ACTIVATED.equals(ability.getAbilityType()) - || AbilityType.SPELL.equals(ability.getAbilityType()))) { - return false; - } - return isLifeTotalCanChange(); - } - - @Override - public boolean getCanPayLifeCost() { - return canPayLifeCost; - } - - @Override - public void setCanPayLifeCost(boolean canPayLifeCost) { - this.canPayLifeCost = canPayLifeCost; - } - - @Override - public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { - return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, source.getSourceId(), controllerId, game); - } - - @Override - public void setCanPaySacrificeCostFilter(FilterPermanent filter - ) { - this.sacrificeCostFilter = filter; - } - - @Override - public FilterPermanent getSacrificeCostFilter() { - return sacrificeCostFilter; - } - - @Override - public boolean canLoseByZeroOrLessLife() { - return loseByZeroOrLessLife; - } - - @Override - public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife) { - this.loseByZeroOrLessLife = loseByZeroOrLessLife; - } - - @Override - public boolean canPlayCardsFromGraveyard() { - return canPlayCardsFromGraveyard; - } - - @Override - public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard) { - this.canPlayCardsFromGraveyard = playCardsFromGraveyard; - } - - @Override - public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { - this.drawsOnOpponentsTurn = drawsOnOpponentsTurn; - } - - @Override - public boolean isDrawsOnOpponentsTurn() { - return drawsOnOpponentsTurn; - } - - @Override - public boolean autoLoseGame() { - return false; - } - - @Override - public void becomesActivePlayer() { - this.passedAllTurns = false; - this.passedUntilEndStepBeforeMyTurn = false; - this.turns++; - } - - @Override - public int getTurns() { - return turns; - } - - @Override - public int getStoredBookmark() { - return storedBookmark; - } - - @Override - public void setStoredBookmark(int storedBookmark) { - this.storedBookmark = storedBookmark; - } - - @Override - public synchronized void resetStoredBookmark(Game game) { - if (this.storedBookmark != -1) { - game.removeBookmark(this.storedBookmark); - } - setStoredBookmark(-1); - } - - @Override - public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { - if (null != game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { - // two modes: look at the card or do not look and activate other abilities - String lookMessage = "Look at " + card.getIdName(); - String lookYes = "Yes, look at the card"; - String lookNo = "No, play/activate the card/ability"; - if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { - Cards cards = new CardsImpl(card); - this.lookAtCards(getName() + " - " + card.getIdName() + " - " - + CardUtil.sdf.format(System.currentTimeMillis()), cards, game); - return true; - } - } - return false; - } - - @Override - public void setPriorityTimeLeft(int timeLeft - ) { - priorityTimeLeft = timeLeft; - } - - @Override - public int getPriorityTimeLeft() { - return priorityTimeLeft; - } - - @Override - public boolean hasQuit() { - return quit; - } - - @Override - public boolean hasTimerTimeout() { - return timerTimeout; - } - - @Override - public boolean hasIdleTimeout() { - return idleTimeout; - } - - @Override - public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { - this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; - } - - @Override - public boolean hasReachedNextTurnAfterLeaving() { - return reachedNextTurnAfterLeaving; - } - - @Override - public boolean canJoinTable(Table table - ) { - return !table.userIsBanned(name); - } - - @Override - public void addCommanderId(UUID commanderId - ) { - this.commandersIds.add(commanderId); - } - - @Override - public Set getCommandersIds() { - return this.commandersIds; - } - - @Override - public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { - return moveCards(card, toZone, source, game, false, false, false, null); - } - - @Override - public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { - Set cardList = new HashSet<>(); - if (card != null) { - cardList.add(card); - } - return moveCards(cardList, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); - } - - @Override - public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game) { - return moveCards(cards.getCards(game), toZone, source, game); - } - - @Override - public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game - ) { - return moveCards(cards, toZone, source, game, false, false, false, null); - } - - @Override - public boolean moveCards(Set cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { - if (cards.isEmpty()) { - return true; - } - Set successfulMovedCards = new LinkedHashSet<>(); - Zone fromZone = null; - switch (toZone) { - case GRAVEYARD: - fromZone = game.getState().getZone(cards.iterator().next().getId()); - successfulMovedCards = moveCardsToGraveyardWithInfo(cards, source, game, fromZone); - return !successfulMovedCards.isEmpty(); - case BATTLEFIELD: // new logic that does not yet add the permanents to battlefield while replacement effects are handled - List infoList = new ArrayList<>(); - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source, - byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); - infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source)); - } - infoList = ZonesHandler.moveCards(infoList, game, source); - for (ZoneChangeInfo info : infoList) { - Permanent permanent = game.getPermanent(info.event.getTargetId()); - if (permanent != null) { - successfulMovedCards.add(permanent); - if (!game.isSimulation()) { - Player eventPlayer = game.getPlayer(info.event.getPlayerId()); - if (eventPlayer != null && fromZone != null) { - game.informPlayers(eventPlayer.getLogName() + " puts " - + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " - + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield" - + CardUtil.getSourceLogName(game, source, permanent.getId())); - } - } - } - } - // TODO: must be replaced by game.getState().processAction(game), see isInUseableZoneDiesTrigger comments - // about short living LKI problem - //game.getState().processAction(game); - game.applyEffects(); - break; - case HAND: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - boolean hideCard = fromZone == Zone.LIBRARY - || (card.isFaceDown(game) - && fromZone != Zone.STACK - && fromZone != Zone.BATTLEFIELD); - if (moveCardToHandWithInfo(card, source, game, !hideCard)) { - successfulMovedCards.add(card); - } - } - break; - case EXILED: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - boolean withName = (fromZone == Zone.BATTLEFIELD - || fromZone == Zone.STACK) - || !card.isFaceDown(game); - if (moveCardToExileWithInfo(card, null, "", source, game, fromZone, withName)) { - successfulMovedCards.add(card); - } - } - break; - case LIBRARY: - for (Card card : cards) { - if (card instanceof Spell) { - fromZone = game.getState().getZone(((Spell) card).getSourceId()); - } else { - fromZone = game.getState().getZone(card.getId()); - } - boolean hideCard = fromZone == Zone.HAND || fromZone == Zone.LIBRARY; - if (moveCardToLibraryWithInfo(card, source, game, fromZone, true, !hideCard)) { - successfulMovedCards.add(card); - } - } - break; - case COMMAND: - for (Card card : cards) { - fromZone = game.getState().getZone(card.getId()); - if (moveCardToCommandWithInfo(card, source, game, fromZone)) { - successfulMovedCards.add(card); - } - } - break; - case OUTSIDE: - for (Card card : cards) { - if (card instanceof Permanent) { - game.getBattlefield().removePermanent(card.getId()); - ZoneChangeEvent event = new ZoneChangeEvent((Permanent) card, source, - byOwner ? card.getOwnerId() : getId(), Zone.BATTLEFIELD, Zone.OUTSIDE, appliedEffects); - game.fireEvent(event); - } - } - break; - default: - throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet"); - } - return !successfulMovedCards.isEmpty(); - } - - @Override - public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { - Set cards = new HashSet<>(); - if (card != null) { - cards.add(card); - } - return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); - } - - @Override - public boolean moveCardsToExile(Set cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { - if (cards.isEmpty()) { - return true; - } - boolean result = false; - for (Card card : cards) { - Zone fromZone = game.getState().getZone(card.getId()); - result |= moveCardToExileWithInfo(card, exileId, exileZoneName, source, game, fromZone, withName); - } - return result; - } - - @Override - public boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName) { - boolean result = false; - Zone fromZone = game.getState().getZone(card.getId()); - if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { - card = game.getPermanent(card.getId()); - } - if (card.moveToZone(Zone.HAND, source, game, false)) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - if (!game.isSimulation()) { - game.informPlayers(getLogName() + " puts " - + (withName ? card.getLogName() : (card.isFaceDown(game) ? "a face down card" : "a card")) - + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' - + (card.isOwnedBy(this.getId()) ? "into their hand" : "into its owner's hand" - + CardUtil.getSourceLogName(game, source, card.getId())) - ); - } - result = true; - } - return result; - } - - @Override - public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, Game game, Zone fromZone) { - Set movedCards = new LinkedHashSet<>(); - while (!allCards.isEmpty()) { - // identify cards from one owner - Cards cards = new CardsImpl(); - UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext();) { - Card card = it.next(); - if (cards.isEmpty()) { - ownerId = card.getOwnerId(); - } - if (card.isOwnedBy(ownerId)) { - it.remove(); - cards.add(card); - } - } - // move cards to graveyard in order the owner decides - if (!cards.isEmpty()) { - Player choosingPlayer = this; - if (!Objects.equals(ownerId, this.getId())) { - choosingPlayer = game.getPlayer(ownerId); - } - if (choosingPlayer == null) { - continue; - } - boolean chooseOrder = false; - if (userData.askMoveToGraveOrder()) { - if (cards.size() > 1) { - chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral, - "Choose the order in which the cards go to the graveyard?", source, game); - } - } - if (chooseOrder) { - TargetCard target = new TargetCard(fromZone, - new FilterCard("card to put on the top of your graveyard (last one chosen will be topmost)")); - target.setRequired(true); - while (choosingPlayer.canRespond() && cards.size() > 1) { - choosingPlayer.chooseTarget(Outcome.Neutral, cards, target, source, game); - UUID targetObjectId = target.getFirstTarget(); - Card card = cards.get(targetObjectId, game); - cards.remove(targetObjectId); - if (card != null) { - fromZone = game.getState().getZone(card.getId()); - if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - target.clearChosen(); - } - if (cards.size() == 1) { - Card card = cards.getCards(game).iterator().next(); - if (card != null && choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - } else { - for (Card card : cards.getCards(game)) { - if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { - movedCards.add(card); - } - } - } - } - } - return movedCards; - } - - @Override - public boolean moveCardToGraveyardWithInfo(Card card, Ability source, Game game, Zone fromZone) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.GRAVEYARD, source, game, false)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(card.getLogName()).append(' ').append(card.isCopy() ? "(Copy) " : "") - .append(fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : ""); - if (card.isOwnedBy(getId())) { - sb.append("into their graveyard"); - } else { - sb.append("it into its owner's graveyard"); - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToLibraryWithInfo(Card card, Ability source, Game game, Zone fromZone, boolean toTop, boolean withName) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.LIBRARY, source, game, toTop)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(withName ? card.getLogName() : "a card").append(' '); - if (fromZone != null) { - sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); - } - sb.append("to the ").append(toTop ? "top" : "bottom"); - if (card.isOwnedBy(getId())) { - sb.append(" of their library"); - } else { - Player player = game.getPlayer(card.getOwnerId()); - if (player != null) { - sb.append(" of ").append(player.getLogName()).append("'s library"); - } - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToCommandWithInfo(Card card, Ability source, Game game, Zone fromZone) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToZone(Zone.COMMAND, source, game, true)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { - card = game.getCard(card.getId()); - } - StringBuilder sb = new StringBuilder(this.getLogName()) - .append(" puts ").append(card.getLogName()).append(' '); - if (fromZone != null) { - sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); - } - if (card.isOwnedBy(getId())) { - sb.append(" to their command zone"); - } else { - Player player = game.getPlayer(card.getOwnerId()); - if (player != null) { - sb.append(" to ").append(player.getLogName()).append("'s command zone"); - } - } - sb.append(CardUtil.getSourceLogName(game, source, card.getId())); - game.informPlayers(sb.toString()); - } - result = true; - } - return result; - } - - @Override - public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, Ability source, Game game, Zone fromZone, boolean withName) { - if (card == null) { - return false; - } - boolean result = false; - if (card.moveToExile(exileId, exileName, source, game)) { - if (!game.isSimulation()) { - if (card instanceof PermanentCard) { - // in case it's face down or name was changed by copying from other permanent - Card basicCard = game.getCard(card.getId()); - if (basicCard != null) { - card = basicCard; - } - } else if (card instanceof Spell) { - final Spell spell = (Spell) card; - if (spell.isCopy()) { - // copied spell, only remove from stack - game.getStack().remove(spell, game); - } - } - if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced - game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() - + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' - + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); - } - - } - result = true; - } - return result; - } - - @Override - public Cards millCards(int toMill, Ability source, Game game) { - GameEvent event = GameEvent.getEvent(GameEvent.EventType.MILL_CARDS, getId(), source, getId(), toMill); - if (game.replaceEvent(event)) { - return new CardsImpl(); - } - Cards cards = new CardsImpl(this.getLibrary().getTopCards(game, event.getAmount())); - this.moveCards(cards, Zone.GRAVEYARD, source, game); - for (Card card : cards.getCards(game)) { - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.MILLED_CARD, card.getId(), source, getId())); - } - return cards; - } - - @Override - public boolean hasOpponent(UUID playerToCheckId, Game game) { - return !this.getId().equals(playerToCheckId) - && game.isOpponent(this, playerToCheckId) - && getInRange().contains(playerToCheckId); - } - - @Override - public void cleanUpOnMatchEnd() { - - } - - @Override - public boolean getPassedAllTurns() { - return passedAllTurns; - } - - @Override - public boolean getPassedUntilNextMain() { - return passedUntilNextMain; - } - - @Override - public boolean getPassedUntilEndOfTurn() { - return passedUntilEndOfTurn; - } - - @Override - public boolean getPassedTurn() { - return passedTurn; - } - - @Override - public boolean getPassedUntilStackResolved() { - return passedUntilStackResolved; - } - - @Override - public boolean getPassedUntilEndStepBeforeMyTurn() { - return passedUntilEndStepBeforeMyTurn; - } - - @Override - public AbilityType getJustActivatedType() { - return justActivatedType; - } - - @Override - public void setJustActivatedType(AbilityType justActivatedType - ) { - this.justActivatedType = justActivatedType; - } - - @Override - public void revokePermissionToSeeHandCards() { - usersAllowedToSeeHandCards.clear(); - } - - @Override - public void addPermissionToShowHandCards(UUID watcherUserId - ) { - usersAllowedToSeeHandCards.add(watcherUserId); - } - - @Override - public boolean isPlayerAllowedToRequestHand(UUID gameId, UUID requesterPlayerId) { - return userData.isAllowRequestHandToPlayer(gameId, requesterPlayerId); - } - - @Override - public void addPlayerToRequestedHandList(UUID gameId, UUID requesterPlayerId) { - userData.addPlayerToRequestedHandList(gameId, requesterPlayerId); - } - - @Override - public boolean hasUserPermissionToSeeHand(UUID userId - ) { - return usersAllowedToSeeHandCards.contains(userId); - } - - @Override - public Set getUsersAllowedToSeeHandCards() { - return usersAllowedToSeeHandCards; - } - - @Override - public void setMatchPlayer(MatchPlayer matchPlayer - ) { - this.matchPlayer = matchPlayer; - } - - @Override - public MatchPlayer getMatchPlayer() { - return matchPlayer; - } - - @Override - public void abortReset() { - abort = false; - } - - @Override - public void signalPlayerConcede() { - - } - - @Override - public boolean scry(int value, Ability source, Game game) { - GameEvent event = new GameEvent(GameEvent.EventType.SCRY, getId(), source, getId(), value, true); - if (game.replaceEvent(event)) { - return false; - } - game.informPlayers(getLogName() + " scries " + event.getAmount() + CardUtil.getSourceLogName(game, source)); - Cards cards = new CardsImpl(); - cards.addAll(getLibrary().getTopCards(game, event.getAmount())); - if (!cards.isEmpty()) { - TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, - new FilterCard("card" + (cards.size() == 1 ? "" : "s") - + " to PUT on the BOTTOM of your library (Scry)")); - chooseTarget(Outcome.Benefit, cards, target, source, game); - putCardsOnBottomOfLibrary(new CardsImpl(target.getTargets()), game, source, true); - cards.removeAll(target.getTargets()); - putCardsOnTopOfLibrary(cards, game, source, true); - } - game.fireEvent(new GameEvent(GameEvent.EventType.SCRIED, getId(), source, getId(), event.getAmount(), true)); - return true; - } - - @Override - public boolean surveil(int value, Ability source, Game game) { - GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source, getId(), value, true); - if (game.replaceEvent(event)) { - return false; - } - game.informPlayers(getLogName() + " surveils " + event.getAmount() + CardUtil.getSourceLogName(game, source)); - Cards cards = new CardsImpl(); - cards.addAll(getLibrary().getTopCards(game, event.getAmount())); - if (!cards.isEmpty()) { - TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, - new FilterCard("cards to PUT into your GRAVEYARD (Surveil)")); - chooseTarget(Outcome.Benefit, cards, target, source, game); - moveCards(new CardsImpl(target.getTargets()), Zone.GRAVEYARD, source, game); - cards.removeAll(target.getTargets()); - putCardsOnTopOfLibrary(cards, game, source, true); - } - game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source, getId(), event.getAmount(), true)); - return true; - } - - @Override - public boolean addTargets(Ability ability, Game game - ) { - // only used for TestPlayer to preSet Targets - return true; - } - - @Override - public String getHistory() { - return "no available"; - } - - @Override - public boolean hasDesignation(DesignationType designationName) { - for (Designation designation : designations) { - if (designation.getDesignationType().equals(designationName)) { - return true; - } - } - return false; - } - - @Override - public void addDesignation(Designation designation) { - if (!designation.isUnique() || !this.hasDesignation(designation.getDesignationType())) { - designations.add(designation); - } - } - - @Override - public List getDesignations() { - return designations; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - Player obj = (Player) o; - if (this.getId() == null || obj.getId() == null) { - return false; - } - - return this.getId().equals(obj.getId()); - } - - @Override - public int hashCode() { - int hash = 7; - hash = 89 * hash + Objects.hashCode(this.playerId); - return hash; - } - - @Override - public void addPhyrexianToColors(FilterMana colors) { - if (phyrexianColors == null) { - phyrexianColors = colors.copy(); - } else { - if (colors.isWhite()) { - this.phyrexianColors.setWhite(true); - } - if (colors.isBlue()) { - this.phyrexianColors.setBlue(true); - } - if (colors.isBlack()) { - this.phyrexianColors.setBlack(true); - } - if (colors.isRed()) { - this.phyrexianColors.setRed(true); - } - if (colors.isGreen()) { - this.phyrexianColors.setGreen(true); - } - } - } - - @Override - public FilterMana getPhyrexianColors() { - return this.phyrexianColors; - } - - @Override - public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { - return card.getSpellAbility(); - } - - @Override - public String toString() { - return getName() + " (" + super.getClass().getSimpleName() + ")"; - } -} +package mage.players; + +import com.google.common.collect.ImmutableMap; +import mage.*; +import mage.abilities.*; +import mage.abilities.ActivatedAbility.ActivationStatus; +import mage.abilities.common.PassAbility; +import mage.abilities.common.PlayLandAsCommanderAbility; +import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; +import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; +import mage.abilities.costs.*; +import mage.abilities.costs.mana.AlternateManaPaymentAbility; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; +import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; +import mage.abilities.keyword.*; +import mage.abilities.mana.ActivatedManaAbilityImpl; +import mage.abilities.mana.ManaOptions; +import mage.actions.MageDrawAction; +import mage.cards.*; +import mage.cards.decks.Deck; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; +import mage.constants.*; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.designations.Designation; +import mage.designations.DesignationType; +import mage.filter.FilterCard; +import mage.filter.FilterMana; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.common.FilterCreatureForCombat; +import mage.filter.common.FilterCreatureForCombatBlock; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.game.*; +import mage.game.combat.CombatGroup; +import mage.game.command.CommandObject; +import mage.game.events.*; +import mage.game.match.MatchPlayer; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentCard; +import mage.game.permanent.PermanentToken; +import mage.game.permanent.token.SquirrelToken; +import mage.game.stack.Spell; +import mage.game.stack.StackAbility; +import mage.game.stack.StackObject; +import mage.game.turn.Step; +import mage.players.net.UserData; +import mage.target.Target; +import mage.target.TargetAmount; +import mage.target.TargetCard; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetDiscard; +import mage.util.CardUtil; +import mage.util.GameLog; +import mage.util.RandomUtil; +import org.apache.log4j.Logger; + +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public abstract class PlayerImpl implements Player, Serializable { + + private static final Logger logger = Logger.getLogger(PlayerImpl.class); + + /** + * Used to cancel waiting requests send to the player + */ + protected boolean abort; + + protected final UUID playerId; + protected String name; + protected boolean human; + protected int life; + protected boolean wins; + protected boolean draws; + protected boolean loses; + protected Library library; + protected Cards sideboard; + protected Cards hand; + protected Graveyard graveyard; + protected Set commandersIds = new HashSet<>(0); + protected Abilities abilities; + protected Counters counters; + protected int landsPlayed; + protected int landsPerTurn = 1; + protected int loyaltyUsePerTurn = 1; + protected int maxHandSize = 7; + protected int maxAttackedBy = Integer.MAX_VALUE; + protected ManaPool manaPool; + // priority control + protected boolean passed; // player passed priority + protected boolean passedTurn; // F4 + protected boolean passedTurnSkipStack; // F6 // TODO: research + protected boolean passedUntilEndOfTurn; // F5 + protected boolean passedUntilNextMain; // F7 + protected boolean passedUntilStackResolved; // F10 + protected Date dateLastAddedToStack; + protected boolean passedUntilEndStepBeforeMyTurn; // F11 + protected boolean skippedAtLeastOnce; // used to track if passed started in specific phase + /** + * This indicates that player passed all turns until their own turn starts + * (F9). Note! This differs from passedTurn as it doesn't care about spells + * and abilities in the stack and will pass them as well. + */ + protected boolean passedAllTurns; // F9 + protected AbilityType justActivatedType; // used to check if priority can be passed automatically + + protected int turns; + protected int storedBookmark = -1; + protected int priorityTimeLeft = Integer.MAX_VALUE; + + // conceded or connection lost game + protected boolean left; + // set if the player quits the complete match + protected boolean quit; + // set if the player lost match because of priority timeout + protected boolean timerTimeout; + // set if the player lost match because of idle timeout + protected boolean idleTimeout; + + protected RangeOfInfluence range; + protected Set inRange = new HashSet<>(); // players list in current range of influence (updates each turn) + + protected boolean isTestMode = false; + protected boolean canGainLife = true; + protected boolean canLoseLife = true; + protected boolean canPayLifeCost = true; + protected boolean loseByZeroOrLessLife = true; + protected boolean canPlayCardsFromGraveyard = true; + protected boolean drawsOnOpponentsTurn = false; + + protected FilterPermanent sacrificeCostFilter; + + protected final List alternativeSourceCosts = new ArrayList<>(); + + protected boolean isGameUnderControl = true; + protected UUID turnController; + protected List turnControllers = new ArrayList<>(); + protected Set playersUnderYourControl = new HashSet<>(); + + protected Set usersAllowedToSeeHandCards = new HashSet<>(); + + protected List attachments = new ArrayList<>(); + + protected boolean topCardRevealed = false; + + // 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn + // or until a specific point in that turn will last until that turn would have begun. + // They neither expire immediately nor last indefinitely. + protected boolean reachedNextTurnAfterLeaving = false; + + // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) + // support multiple cards with alternative mana cost + protected Set castSourceIdWithAlternateMana = new HashSet<>(); + protected Map> castSourceIdManaCosts = new HashMap<>(); + protected Map> castSourceIdCosts = new HashMap<>(); + + // indicates that the player is in mana payment phase + protected boolean payManaMode = false; + + protected UserData userData; + protected MatchPlayer matchPlayer; + + protected List designations = new ArrayList<>(); + + // mana colors the player can handle like Phyrexian mana + protected FilterMana phyrexianColors; + + // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) + protected final List> availableTriggeredManaList = new ArrayList<>(); + + /** + * During some steps we can't play anything + */ + protected final Map silentPhaseSteps = ImmutableMap.builder(). + put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); + + public PlayerImpl(String name, RangeOfInfluence range) { + this(UUID.randomUUID()); + this.name = name; + this.range = range; + hand = new CardsImpl(); + graveyard = new Graveyard(); + abilities = new AbilitiesImpl<>(); + counters = new Counters(); + manaPool = new ManaPool(playerId); + library = new Library(playerId); + sideboard = new CardsImpl(); + phyrexianColors = null; + } + + protected PlayerImpl(UUID id) { + this.playerId = id; + } + + public PlayerImpl(final PlayerImpl player) { + this.abort = player.abort; + this.playerId = player.playerId; + + this.name = player.name; + this.human = player.human; + this.life = player.life; + this.wins = player.wins; + this.draws = player.draws; + this.loses = player.loses; + + this.library = player.library.copy(); + this.sideboard = player.sideboard.copy(); + this.hand = player.hand.copy(); + this.graveyard = player.graveyard.copy(); + this.commandersIds = player.commandersIds; + this.abilities = player.abilities.copy(); + this.counters = player.counters.copy(); + + this.landsPlayed = player.landsPlayed; + this.landsPerTurn = player.landsPerTurn; + this.loyaltyUsePerTurn = player.loyaltyUsePerTurn; + this.maxHandSize = player.maxHandSize; + this.maxAttackedBy = player.maxAttackedBy; + this.manaPool = player.manaPool.copy(); + this.turns = player.turns; + + this.left = player.left; + this.quit = player.quit; + this.timerTimeout = player.timerTimeout; + this.idleTimeout = player.idleTimeout; + this.range = player.range; + this.canGainLife = player.canGainLife; + this.canLoseLife = player.canLoseLife; + this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; + this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; + this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; + + this.attachments.addAll(player.attachments); + + this.inRange.addAll(player.inRange); + this.userData = player.userData; + this.matchPlayer = player.matchPlayer; + + this.canPayLifeCost = player.canPayLifeCost; + this.sacrificeCostFilter = player.sacrificeCostFilter; + this.alternativeSourceCosts.addAll(player.alternativeSourceCosts); + this.storedBookmark = player.storedBookmark; + + this.topCardRevealed = player.topCardRevealed; + this.playersUnderYourControl.addAll(player.playersUnderYourControl); + this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); + + this.isTestMode = player.isTestMode; + this.isGameUnderControl = player.isGameUnderControl; + + this.turnController = player.turnController; + this.turnControllers.addAll(player.turnControllers); + + this.passed = player.passed; + this.passedTurn = player.passedTurn; + this.passedTurnSkipStack = player.passedTurnSkipStack; + this.passedUntilEndOfTurn = player.passedUntilEndOfTurn; + this.passedUntilNextMain = player.passedUntilNextMain; + this.passedUntilStackResolved = player.passedUntilStackResolved; + this.dateLastAddedToStack = player.dateLastAddedToStack; + this.passedUntilEndStepBeforeMyTurn = player.passedUntilEndStepBeforeMyTurn; + this.skippedAtLeastOnce = player.skippedAtLeastOnce; + this.passedAllTurns = player.passedAllTurns; + this.justActivatedType = player.justActivatedType; + + this.priorityTimeLeft = player.getPriorityTimeLeft(); + this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; + + this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); + for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + this.payManaMode = player.payManaMode; + this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; + for (Designation object : player.designations) { + this.designations.add(object.copy()); + } + } + + @Override + public void restore(Player player) { + this.name = player.getName(); + this.human = player.isHuman(); + this.life = player.getLife(); + + this.passed = player.isPassed(); + + // Don't restore more global states. If restored they are probably cause for unintended draws (https://github.com/magefree/mage/issues/1205). +// this.wins = player.hasWon(); +// this.loses = player.hasLost(); +// this.left = player.hasLeft(); +// this.quit = player.hasQuit(); + // Makes no sense to restore +// this.priorityTimeLeft = player.getPriorityTimeLeft(); +// this.idleTimeout = player.hasIdleTimeout(); +// this.timerTimeout = player.hasTimerTimeout(); + // can't change so no need to restore +// this.isTestMode = player.isTestMode(); + // This is meta data and should'nt be restored by rollback +// this.userData = player.getUserData(); + this.library = player.getLibrary().copy(); + this.sideboard = player.getSideboard().copy(); + this.hand = player.getHand().copy(); + this.graveyard = player.getGraveyard().copy(); + + //noinspection deprecation - it's ok to use it in inner methods + this.commandersIds = new HashSet<>(player.getCommandersIds()); + + this.abilities = player.getAbilities().copy(); + this.counters = player.getCounters().copy(); + + this.landsPlayed = player.getLandsPlayed(); + this.landsPerTurn = player.getLandsPerTurn(); + this.loyaltyUsePerTurn = player.getLoyaltyUsePerTurn(); + this.maxHandSize = player.getMaxHandSize(); + this.maxAttackedBy = player.getMaxAttackedBy(); + this.manaPool = player.getManaPool().copy(); + // Restore user specific settings in case changed since state save + this.manaPool.setAutoPayment(this.getUserData().isManaPoolAutomatic()); + this.manaPool.setAutoPaymentRestricted(this.getUserData().isManaPoolAutomaticRestricted()); + + this.turns = player.getTurns(); + + this.range = player.getRange(); + this.canGainLife = player.isCanGainLife(); + this.canLoseLife = player.isCanLoseLife(); + this.attachments.clear(); + this.attachments.addAll(player.getAttachments()); + + this.inRange.clear(); + this.inRange.addAll(player.getInRange()); + this.canPayLifeCost = player.getCanPayLifeCost(); + this.sacrificeCostFilter = player.getSacrificeCostFilter() != null + ? player.getSacrificeCostFilter().copy() : null; + this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); + this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); + this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); + this.alternativeSourceCosts.clear(); + this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); + + this.topCardRevealed = player.isTopCardRevealed(); + this.playersUnderYourControl.clear(); + this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); + this.isGameUnderControl = player.isGameUnderControl(); + + this.turnController = player.getTurnControlledBy(); + this.turnControllers.clear(); + this.turnControllers.addAll(player.getTurnControllers()); + this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); + + this.clearCastSourceIdManaCosts(); + this.castSourceIdWithAlternateMana.clear(); + this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); + for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + } + + this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; + + this.designations.clear(); + for (Designation object : player.getDesignations()) { + this.designations.add(object.copy()); + } + + // Don't restore! + // this.storedBookmark + // this.usersAllowedToSeeHandCards + } + + @Override + public void useDeck(Deck deck, Game game) { + library.clear(); + library.addAll(deck.getCards(), game); + sideboard.clear(); + for (Card card : deck.getSideboard()) { + sideboard.add(card); + } + } + + /** + * Cast e.g. from Karn Liberated to restart the current game + * + * @param game + */ + @Override + public void init(Game game) { + init(game, false); + } + + @Override + public void init(Game game, boolean testMode) { + this.abort = false; + if (!testMode) { + this.hand.clear(); + this.graveyard.clear(); + } + this.library.reset(); + this.abilities.clear(); + this.counters.clear(); + this.wins = false; + this.draws = false; + this.loses = false; + this.left = false; + // reset is necessary because in tournament player will be used for each round + this.quit = false; + this.timerTimeout = false; + this.idleTimeout = false; + + this.turns = 0; + this.isGameUnderControl = true; + this.turnController = this.getId(); + this.turnControllers.clear(); + this.playersUnderYourControl.clear(); + + this.passed = false; + this.passedTurn = false; + this.passedTurnSkipStack = false; + this.passedUntilEndOfTurn = false; + this.passedUntilNextMain = false; + this.passedUntilStackResolved = false; + this.dateLastAddedToStack = null; + this.passedUntilEndStepBeforeMyTurn = false; + this.skippedAtLeastOnce = false; + this.passedAllTurns = false; + this.justActivatedType = null; + + this.canGainLife = true; + this.canLoseLife = true; + this.topCardRevealed = false; + this.payManaMode = false; + this.setLife(game.getStartingLife(), game, null); + this.setReachedNextTurnAfterLeaving(false); + + this.clearCastSourceIdManaCosts(); + + this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left + this.phyrexianColors = null; + + this.designations.clear(); + } + + /** + * called before apply effects + */ + @Override + public void reset() { + this.abilities.clear(); + this.landsPerTurn = 1; + this.loyaltyUsePerTurn = 1; + this.maxHandSize = 7; + this.maxAttackedBy = Integer.MAX_VALUE; + this.canGainLife = true; + this.canLoseLife = true; + this.canPayLifeCost = true; + this.sacrificeCostFilter = null; + this.loseByZeroOrLessLife = true; + this.canPlayCardsFromGraveyard = false; + this.drawsOnOpponentsTurn = false; + this.topCardRevealed = false; + this.alternativeSourceCosts.clear(); + this.clearCastSourceIdManaCosts(); + this.getManaPool().clearEmptyManaPoolRules(); + this.phyrexianColors = null; + } + + @Override + public Counters getCounters() { + return counters; + } + + @Override + public void beginTurn(Game game) { + this.landsPlayed = 0; + updateRange(game); + } + + @Override + public RangeOfInfluence getRange() { + return range; + } + + @Override + public void updateRange(Game game) { + // 20100423 - 801.2c + // 801.2c The particular players within each player’s range of influence are determined as each turn begins. + // BUT it also uses before game start to fill game and card data in starting game events + inRange.clear(); + inRange.add(this.playerId); + inRange.addAll(getAllNearPlayers(game, true)); + inRange.addAll(getAllNearPlayers(game, false)); + } + + private Set getAllNearPlayers(Game game, boolean needPrevious) { + // find all near players (search from current player position) + Set foundedList = new HashSet<>(); + PlayerList players = game.getState().getPlayerList(this.playerId); + int needAmount = this.getRange().getRange(); // distance to search (0 - ALL range) + int foundedAmount = 0; + while (needAmount == 0 || foundedAmount < needAmount) { + Player foundedPlayer = needPrevious ? players.getPrevious(game) : players.getNext(game, false); + + // PlayerList is inifine, so stops on repeats + if (foundedPlayer == null || foundedPlayer.getId().equals(this.playerId) || foundedList.contains(foundedPlayer.getId())) { + break; + } + // skip leaved player (no needs cause next/previous code already checks it) + + foundedList.add(foundedPlayer.getId()); + foundedAmount++; + } + return foundedList; + } + + @Override + public Set getInRange() { + if (inRange.isEmpty()) { + // runtime check: inRange filled on beginTurn, but unit tests adds cards by cheat engine before game starting, + // so inRange will be empty and some ETB effects can be broken (example: Spark Double puts direct to battlefield). + // Cheat engine already have a workaround, so that error must not be visible in normal situation. + throw new IllegalStateException("Wrong code usage (game is not started, but you call getInRange in some effects)."); + } + + return inRange; + } + + @Override + public Set getPlayersUnderYourControl() { + return this.playersUnderYourControl; + } + + @Override + public void controlPlayersTurn(Game game, UUID playerId) { + Player player = game.getPlayer(playerId); + player.setTurnControlledBy(this.getId()); + game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); + if (!playerId.equals(this.getId())) { + this.playersUnderYourControl.add(playerId); + if (!player.hasLeft() && !player.hasLost()) { + player.setGameUnderYourControl(false); + } + DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( + new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); + ability.setSourceId(getId()); + ability.setControllerId(getId()); + game.addDelayedTriggeredAbility(ability, null); + } + } + + @Override + public void setTurnControlledBy(UUID playerId) { + this.turnController = playerId; + this.turnControllers.add(playerId); + } + + @Override + public List getTurnControllers() { + return this.turnControllers; + } + + @Override + public UUID getTurnControlledBy() { + return this.turnController; + } + + @Override + public void resetOtherTurnsControlled() { + playersUnderYourControl.clear(); + } + + /** + * returns true if the player has the control itself - false if the player + * is controlled by another player + * + * @return + */ + @Override + public boolean isGameUnderControl() { + return isGameUnderControl; + } + + @Override + public void setGameUnderYourControl(boolean value) { + setGameUnderYourControl(value, true); + } + + @Override + public void setGameUnderYourControl(boolean value, boolean fullRestore) { + this.isGameUnderControl = value; + if (isGameUnderControl) { + if (fullRestore) { + this.turnControllers.clear(); + this.turnController = getId(); + } else { + if (turnControllers.size() > 0) { + this.turnControllers.remove(turnControllers.size() - 1); + } + if (turnControllers.isEmpty()) { + this.turnController = getId(); + } else { + this.turnController = turnControllers.get(turnControllers.size() - 1); + isGameUnderControl = false; + } + } + } + } + + @Override + public void endOfTurn(Game game) { + this.passedTurn = false; + this.passedTurnSkipStack = false; + } + + @Override + public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { + if (this.hasLost() || this.hasLeft()) { + return false; + } + if (source != null) { + // there is only variant of shroud, so check the instance and any asthougheffects that would ignore it + if (abilities.containsKey(ShroudAbility.getInstance().getId()) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game) == null) { + return false; + } + // check for all variants of hexproof and any asthougheffects that would ignore it + // TODO there may be "prevented by rule-modification" effects, so add them if known + for (Ability a : abilities) { + if (a instanceof HexproofBaseAbility + && ((HexproofBaseAbility) a).checkObject(source, game) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null) { + return false; + } + } + return !hasProtectionFrom(source, game); + } + return true; + } + + @Override + public boolean hasProtectionFrom(MageObject source, Game game) { + for (ProtectionAbility ability : abilities.getProtectionAbilities()) { + if (!ability.canTarget(source, game)) { + return true; + } + } + return false; + } + + @Override + public int drawCards(int num, Ability source, Game game) { + if (num > 0) { + return game.doAction(source, new MageDrawAction(this, num, null)); + } + return 0; + } + + @Override + public int drawCards(int num, Ability source, Game game, GameEvent event) { + return game.doAction(source, new MageDrawAction(this, num, event)); + } + + @Override + public void discardToMax(Game game) { + if (hand.size() > this.maxHandSize) { + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " discards down to " + + this.maxHandSize + + (this.maxHandSize == 1 + ? " hand card" : " hand cards")); + } + discard(hand.size() - this.maxHandSize, false, false, null, game); + } + } + + /** + * Don't use this in normal card code, it's for more internal use. Always + * use the [Player].moveCards methods if possible for card movement of card + * code. + * + * @param card + * @param game + * @return + */ + @Override + public boolean putInHand(Card card, Game game) { + if (card.isOwnedBy(playerId)) { + card.setZone(Zone.HAND, game); + this.hand.add(card); + } else { + return game.getPlayer(card.getOwnerId()).putInHand(card, game); + } + return true; + } + + @Override + public boolean removeFromHand(Card card, Game game) { + return hand.remove(card.getId()); + } + + @Override + public boolean removeFromLibrary(Card card, Game game) { + if (card == null) { + return false; + } + library.remove(card.getId(), game); + // must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop) + // TODO: replace removeFromTop logic to normal with moveToZone + return true; + } + + @Override + public Card discardOne(boolean random, boolean payForCost, Ability source, Game game) { + return discard(1, random, payForCost, source, game).getRandom(game); + } + + @Override + public Cards discard(int amount, boolean random, boolean payForCost, Ability source, Game game) { + if (random) { + return discard(getRandomToDiscard(amount, source, game), payForCost, source, game); + } + return discard(amount, amount, payForCost, source, game); + } + + @Override + public Cards discard(int minAmount, int maxAmount, boolean payForCost, Ability source, Game game) { + return discard(getToDiscard(minAmount, maxAmount, source, game), payForCost, source, game); + } + + @Override + public Cards discard(Cards cards, boolean payForCost, Ability source, Game game) { + Cards discardedCards = new CardsImpl(); + if (cards == null) { + return discardedCards; + } + for (Card card : cards.getCards(game)) { + if (doDiscard(card, source, game, payForCost, false)) { + discardedCards.add(card); + } + } + if (!discardedCards.isEmpty()) { + game.fireEvent(new DiscardedCardsEvent(source, playerId, discardedCards.size(), discardedCards)); + } + return discardedCards; + } + + @Override + public boolean discard(Card card, boolean payForCost, Ability source, Game game) { + return doDiscard(card, source, game, payForCost, true); + } + + private Cards getToDiscard(int minAmount, int maxAmount, Ability source, Game game) { + Cards toDiscard = new CardsImpl(); + if (minAmount > maxAmount) { + return getToDiscard(maxAmount, minAmount, source, game); + } + if (maxAmount < 1) { + return toDiscard; + } + if (getHand().size() <= minAmount) { + toDiscard.addAll(getHand()); + return toDiscard; + } + TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId()); + choose(Outcome.Discard, target, source != null ? source.getSourceId() : null, game); + toDiscard.addAll(target.getTargets()); + return toDiscard; + } + + private Cards getRandomToDiscard(int amount, Ability source, Game game) { + Cards toDiscard = new CardsImpl(); + Cards hand = getHand().copy(); + for (int i = 0; i < amount; i++) { + if (hand.isEmpty()) { + break; + } + Card card = hand.getRandom(game); + hand.remove(card); + toDiscard.add(card); + } + return toDiscard; + } + + private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) { + //20100716 - 701.7 + /* 701.7. Discard # + 701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. + 701.7b By default, effects that cause a player to discard a card allow the affected + player to choose which card to discard. Some effects, however, require a random + discard or allow another player to choose which card is discarded. + 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone + instead of into its owner’s graveyard without being revealed, all values of that + card’s characteristics are considered to be undefined. + TODO: + If a card is discarded this way to pay a cost that specifies a characteristic + about the discarded card, that cost payment is illegal; the game returns to + the moment before the cost was paid (see rule 717, "Handling Illegal Actions"). + */ + if (card == null) { + return false; + } + GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source, playerId); + gameEvent.setFlag(!payForCost); // event from effect (1) or from cost (0) + if (game.replaceEvent(gameEvent, source)) { + return false; + } + // write info to game log first so game log infos from triggered or replacement effects follow in the game log + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " discards " + card.getLogName() + CardUtil.getSourceLogName(game, source)); + } + /* If a card is discarded while Rest in Peace is on the battlefield, abilities that function + * when a card is discarded (such as madness) still work, even though that card never reaches + * a graveyard. In addition, spells or abilities that check the characteristics of a discarded + * card (such as Chandra Ablaze's first ability) can find that card in exile. */ + card.moveToZone(Zone.GRAVEYARD, source, game, false); + // So discard is also successful if card is moved to another zone by replacement effect! + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), source, playerId)); + + if (fireFinalEvent) { + game.fireEvent(new DiscardedCardsEvent(source, playerId, 1, new CardsImpl(card))); + } + return true; + } + + @Override + public List getAttachments() { + return attachments; + } + + @Override + public boolean addAttachment(UUID permanentId, Ability source, Game game) { + if (!this.attachments.contains(permanentId)) { + Permanent aura = game.getPermanent(permanentId); + if (aura == null) { + aura = game.getPermanentEntering(permanentId); + } + if (aura != null) { + if (!game.replaceEvent(new EnchantPlayerEvent(playerId, aura, source))) { + this.attachments.add(permanentId); + aura.attachTo(playerId, source, game); + game.fireEvent(new EnchantedPlayerEvent(playerId, aura, source)); + return true; + } + } + } + return false; + } + + @Override + public boolean removeAttachment(Permanent attachment, Ability source, Game game) { + if (this.attachments.contains(attachment.getId())) { + if (!game.replaceEvent(new UnattachEvent(playerId, attachment.getId(), attachment, source))) { + this.attachments.remove(attachment.getId()); + attachment.attachTo(null, source, game); + game.fireEvent(new UnattachedEvent(playerId, attachment.getId(), attachment, source)); + return true; + } + } + return false; + } + + @Override + public boolean removeFromBattlefield(Permanent permanent, Ability source, Game game) { + permanent.removeFromCombat(game, false); + game.getBattlefield().removePermanent(permanent.getId()); + if (permanent.getAttachedTo() != null) { + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (attachedTo != null) { + attachedTo.removeAttachment(permanent.getId(), source, game); + } else { + Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo()); + if (attachedToPlayer != null) { + attachedToPlayer.removeAttachment(permanent, source, game); + } else { + Card attachedToCard = game.getCard(permanent.getAttachedTo()); + if (attachedToCard != null) { + attachedToCard.removeAttachment(permanent.getId(), source, game); + } + } + } + + } + if (permanent.getPairedCard() != null) { + Permanent pairedCard = permanent.getPairedCard().getPermanent(game); + if (pairedCard != null) { + pairedCard.clearPairedCard(); + } + } + if (permanent.getBandedCards() != null && !permanent.getBandedCards().isEmpty()) { + for (UUID bandedId : permanent.getBandedCards()) { + Permanent banded = game.getPermanent(bandedId); + if (banded != null) { + banded.removeBandedCard(permanent.getId()); + } + } + } + return true; + } + + @Override + public boolean putInGraveyard(Card card, Game game) { + if (card.isOwnedBy(playerId)) { + this.graveyard.add(card); + } else { + return game.getPlayer(card.getOwnerId()).putInGraveyard(card, game); + } + return true; + } + + @Override + public boolean removeFromGraveyard(Card card, Game game) { + return this.graveyard.remove(card); + } + + @Override + public boolean putCardsOnBottomOfLibrary(Card card, Game game, Ability source, boolean anyOrder) { + return putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, anyOrder); + } + + @Override + public boolean putCardsOnBottomOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { + if (!cardsToLibrary.isEmpty()) { + Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException + if (!anyOrder) { + // random order + List ids = new ArrayList<>(cards); + Collections.shuffle(ids); + for (UUID id : ids) { + moveObjectToLibrary(id, source, game, false, false); + } + } else { + // user defined order + TargetCard target = new TargetCard(Zone.ALL, + new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)")); + target.setRequired(true); + while (cards.size() > 1 && this.canRespond() + && this.choose(Outcome.Neutral, cards, target, game)) { + UUID targetObjectId = target.getFirstTarget(); + if (targetObjectId == null) { + break; + } + cards.remove(targetObjectId); + moveObjectToLibrary(targetObjectId, source, game, false, false); + target.clearChosen(); + } + for (UUID c : cards) { + moveObjectToLibrary(c, source, game, false, false); + } + } + } + return true; + } + + @Override + public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) { + if (cards.isEmpty()) { + return true; + } + game.informPlayers(getLogName() + " shuffles " + CardUtil.numberToText(cards.size(), "a") + + " card" + (cards.size() == 1 ? "" : "s") + + " into their library" + CardUtil.getSourceLogName(game, source)); + boolean status = moveCards(cards, Zone.LIBRARY, source, game); + shuffleLibrary(source, game); + return status; + } + + @Override + public boolean shuffleCardsToLibrary(Card card, Game game, Ability source) { + if (card == null) { + return true; + } + return shuffleCardsToLibrary(new CardsImpl(card), game, source); + } + + @Override + public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int xFromTheTop, boolean withName) { + if (card.isOwnedBy(getId())) { + if (library.size() + 1 < xFromTheTop) { + putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true); + } else { + if (card.moveToZone(Zone.LIBRARY, source, game, true) + && !(card instanceof PermanentToken) && !card.isCopy()) { + Card cardInLib = getLibrary().getFromTop(game); + if (cardInLib != null && cardInLib.getId().equals(card.getId())) { // check needed because e.g. commander can go to command zone + cardInLib = getLibrary().removeFromTop(game); + getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game); + game.informPlayers(withName ? cardInLib.getLogName() : "A card" + + " is put into " + + getLogName() + + "'s library " + + CardUtil.numberToOrdinalText(xFromTheTop) + + " from the top" + CardUtil.getSourceLogName(game, source, cardInLib.getId())); + } + } else { + return false; + } + } + } else { + return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop, withName); + } + return true; + } + + /** + * Can be cards or permanents that go to library + * + * @param cardsToLibrary + * @param game + * @param source + * @param anyOrder + * @return + */ + @Override + public boolean putCardsOnTopOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { + if (cardsToLibrary != null && !cardsToLibrary.isEmpty()) { + Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException + if (!anyOrder) { + // random order + List ids = new ArrayList<>(cards); + Collections.shuffle(ids); + for (UUID id : ids) { + moveObjectToLibrary(id, source, game, true, false); + } + } else { + // user defined order + TargetCard target = new TargetCard(Zone.ALL, + new FilterCard("card ORDER to put on the TOP of your library (last one chosen will be topmost)")); + target.setRequired(true); + while (cards.size() > 1 + && this.canRespond() + && this.choose(Outcome.Neutral, cards, target, game)) { + UUID targetObjectId = target.getFirstTarget(); + if (targetObjectId == null) { + break; + } + cards.remove(targetObjectId); + moveObjectToLibrary(targetObjectId, source, game, true, false); + target.clearChosen(); + } + for (UUID c : cards) { + moveObjectToLibrary(c, source, game, true, false); + } + } + } + return true; + } + + @Override + public boolean putCardsOnTopOfLibrary(Card cardToLibrary, Game game, Ability source, boolean anyOrder) { + if (cardToLibrary != null) { + return putCardsOnTopOfLibrary(new CardsImpl(cardToLibrary), game, source, anyOrder); + } + return true; + } + + private boolean moveObjectToLibrary(UUID objectId, Ability source, Game game, boolean toTop, boolean withName) { + MageObject mageObject = game.getObject(objectId); + if (mageObject instanceof Spell && mageObject.isCopy()) { + // Spell copies are not moved as cards, so here the no copy spell has to be selected to move + // (but because copy and original have the same objectId the wrong sepell can be selected from stack). + // So let's check if the original spell is on the stack and has to be selected. // TODO: Better handling so each spell could be selected by a unique id + Spell spellNoCopy = game.getStack().getSpell(source.getSourceId(), false); + if (spellNoCopy != null) { + mageObject = spellNoCopy; + } + } + if (mageObject != null) { + Zone fromZone = game.getState().getZone(objectId); + if ((mageObject instanceof Permanent)) { + return this.moveCardToLibraryWithInfo((Permanent) mageObject, source, game, fromZone, toTop, withName); + } else if (mageObject instanceof Card) { + return this.moveCardToLibraryWithInfo((Card) mageObject, source, game, fromZone, toTop, withName); + } + } + return false; + } + + @Override + public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { + // cost must be copied for data consistence between game simulations + castSourceIdWithAlternateMana.add(sourceId); + castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); + castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); + } + + @Override + public Set getCastSourceIdWithAlternateMana() { + return castSourceIdWithAlternateMana; + } + + @Override + public Map> getCastSourceIdCosts() { + return castSourceIdCosts; + } + + @Override + public Map> getCastSourceIdManaCosts() { + return castSourceIdManaCosts; + } + + @Override + public void clearCastSourceIdManaCosts() { + this.castSourceIdCosts.clear(); + this.castSourceIdManaCosts.clear(); + this.castSourceIdWithAlternateMana.clear(); + } + + @Override + public void setPayManaMode(boolean payManaMode) { + this.payManaMode = payManaMode; + } + + @Override + public boolean isInPayManaMode() { + return payManaMode; + } + + @Override + public boolean playCard(Card card, Game game, boolean noMana, ApprovingObject approvingObject) { + if (card == null) { + return false; + } + + // play without timing and from any zone + boolean result; + if (card.isLand(game)) { + result = playLand(card, game, true); + } else { + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + } + + if (!result) { + game.informPlayer(this, "You can't play " + card.getIdName() + '.'); + } + return result; + } + + /** + * @param originalAbility + * @param game + * @param noMana cast it without paying mana costs + * @param approvingObject which object approved the cast + * @return + */ + @Override + public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, ApprovingObject approvingObject) { + if (game == null || originalAbility == null) { + return false; + } + + // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). + SpellAbility ability = originalAbility.copy(); + ability.setControllerId(getId()); + ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); + + //20091005 - 601.2a + if (ability.getSourceId() == null) { + logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule()); + return false; + } + Card card = game.getCard(ability.getSourceId()); + if (card != null) { + Zone fromZone = game.getState().getZone(card.getMainCard().getId()); + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + ability.getId(), ability, playerId, approvingObject); + castEvent.setZone(fromZone); + if (!game.replaceEvent(castEvent, ability)) { + int bookmark = game.bookmarkState(); + setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) + card.cast(game, fromZone, ability, playerId); + Spell spell = game.getStack().getSpell(ability.getId()); + if (spell == null) { + logger.error("Got no spell from stack. ability: " + ability.getRule()); + return false; + } + if (card.isCopy()) { + spell.setCopy(true, null); + } + // Update the zcc to the stack + ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); + + // ALTERNATIVE COST from dynamic effects + // some effects set sourceId to cast without paying mana costs or other costs + if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { + Ability spellAbility = spell.getSpellAbility(); + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); + Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); + if (alternateCosts == null) { + noMana = true; + } else { + spellAbility.getManaCosts().clear(); + spellAbility.getManaCostsToPay().clear(); + spellAbility.getManaCosts().add(alternateCosts.copy()); + spellAbility.getManaCostsToPay().add(alternateCosts.copy()); + } + spellAbility.getCosts().clear(); + if (costs != null) { + spellAbility.getCosts().addAll(costs); + } + } + clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time + + castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); + castEvent.setZone(fromZone); + game.fireEvent(castEvent); + if (spell.activate(game, noMana)) { + GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, + spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); + castedEvent.setZone(fromZone); + game.fireEvent(castedEvent); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + spell.getActivatedMessage(game)); + } + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + } + return false; + } + + @Override + public boolean playLand(Card card, Game game, boolean ignoreTiming) { + // Check for alternate casting possibilities: e.g. land with Morph + if (card == null) { + return false; + } + ActivatedAbility playLandAbility = null; + boolean foundAlternative = false; + for (Ability ability : card.getAbilities(game)) { + // if cast for noMana no Alternative costs are allowed + if ((ability instanceof AlternativeSourceCosts) + || (ability instanceof OptionalAdditionalSourceCosts)) { + foundAlternative = true; + } + if (ability instanceof PlayLandAbility) { + playLandAbility = (ActivatedAbility) ability; + } + } + + // try alternative cast (face down) + if (foundAlternative) { + SpellAbility spellAbility = new SpellAbility(null, "", + game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); + spellAbility.setControllerId(this.getId()); + spellAbility.setSourceId(card.getId()); + if (cast(spellAbility, game, false, null)) { + return true; + } + } + + if (playLandAbility == null) { + return false; + } + + //20091005 - 114.2a + ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); + if (ignoreTiming) { + if (!canPlayLand()) { + return false; // ignore timing does not mean that more lands than normal can be played + } + } else { + if (!activationStatus.canActivate()) { + return false; + } + } + + //20091005 - 305.1 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { + // int bookmark = game.bookmarkState(); + // land events must return original zone (uses for commander watcher) + Zone cardZoneBefore = game.getState().getZone(card.getId()); + GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + landEventBefore.setZone(cardZoneBefore); + game.fireEvent(landEventBefore); + + if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { + landsPlayed++; + GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, + card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + landEventAfter.setZone(cardZoneBefore); + game.fireEvent(landEventAfter); + + String playText = getLogName() + " plays " + card.getLogName(); + if (card instanceof ModalDoubleFacesCardHalf) { + ModalDoubleFacesCard mdfCard = (ModalDoubleFacesCard) card.getMainCard(); + playText = getLogName() + " plays " + GameLog.replaceNameByColoredName(card, card.getName(), mdfCard) + + " as MDF side of " + GameLog.getColoredObjectIdName(mdfCard); + } + game.fireInformEvent(playText); + // game.removeBookmark(bookmark); + resetStoredBookmark(game); // prevent undo after playing a land + return true; + } + // putOntoBattlefield returned false if putOntoBattlefield was replaced by replacement effect (e.g. Kjeldoran Outpost). + // But that would undo the effect completely, + // what makes no real sense. So it makes no sense to generally do a restoreState here. + // restoreState(bookmark, card.getName(), game); + } + // if the to play the land is replaced (e.g. Kjeldoran Outpost and don't sacrificing a Plains) it's a valid state so returning true here + return true; + } + + protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, + ability.getId(), ability, playerId))) { + int bookmark = game.bookmarkState(); + if (ability.activate(game, false)) { + if (ability.resolve(game)) { + if (ability.isUndoPossible()) { + if (storedBookmark == -1 || storedBookmark > bookmark) { // e.g. useful for undo Nykthos, Shrine to Nyx + setStoredBookmark(bookmark); + } + } else { + resetStoredBookmark(game); + } + return true; + } + } + restoreState(bookmark, ability.getRule(), game); + } + return false; + } + + protected boolean playAbility(ActivatedAbility ability, Game game) { + //20091005 - 602.2a + if (ability.isUsesStack()) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, + ability.getId(), ability, playerId))) { + int bookmark = game.bookmarkState(); + setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) + ability.newId(); + ability.setControllerId(playerId); + game.getStack().push(new StackAbility(ability, playerId)); + if (ability.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, + ability.getId(), ability, playerId)); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + ability.getGameLogMessage(game)); + } + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + } else { + int bookmark = game.bookmarkState(); + if (ability.activate(game, false)) { + ability.resolve(game); + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + restoreState(bookmark, ability.getRule(), game); + } + return false; + } + + protected boolean specialAction(SpecialAction action, Game game) { + //20091005 - 114 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_ACTION, + action.getId(), action, getId()))) { + int bookmark = game.bookmarkState(); + if (action.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_ACTION, + action.getId(), action, getId())); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + action.getGameLogMessage(game)); + } + if (action.resolve(game)) { + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + } + restoreState(bookmark, action.getRule(), game); + } + return false; + } + + protected boolean specialManaPayment(SpecialAction action, Game game) { + //20091005 - 114 + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_MANA_PAYMENT, + action.getId(), action, getId()))) { + int bookmark = game.bookmarkState(); + if (action.activate(game, false)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_MANA_PAYMENT, + action.getId(), action, getId())); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + action.getGameLogMessage(game)); + } + if (action.resolve(game)) { + game.removeBookmark(bookmark); + resetStoredBookmark(game); + return true; + } + } + restoreState(bookmark, action.getRule(), game); + } + return false; + } + + @Override + public boolean activateAbility(ActivatedAbility ability, Game game) { + if (ability == null) { + return false; + } + boolean result; + if (ability instanceof PassAbility) { + pass(game); + return true; + } + Card card = game.getCard(ability.getSourceId()); + if (ability instanceof PlayLandAsCommanderAbility) { + + // LAND as commander: play land with cost, but without stack + ActivationStatus activationStatus = ability.canActivate(this.playerId, game); + if (!activationStatus.canActivate() || !this.canPlayLand()) { + return false; + } + if (card == null) { + return false; + } + + // as copy, tries to applie cost effects and pays + Ability activatingAbility = ability.copy(); + if (activatingAbility.activate(game, false)) { + result = playLand(card, game, false); + } else { + result = false; + } + + } else if (ability instanceof PlayLandAbility) { + + // LAND as normal card: without cost and stack + result = playLand(card, game, false); + + } else { + + // ABILITY + ActivationStatus activationStatus = ability.canActivate(this.playerId, game); + if (!activationStatus.canActivate()) { + return false; + } + + switch (ability.getAbilityType()) { + case SPECIAL_ACTION: + result = specialAction((SpecialAction) ability.copy(), game); + break; + case SPECIAL_MANA_PAYMENT: + result = specialManaPayment((SpecialAction) ability.copy(), game); + break; + case MANA: + result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); + break; + case SPELL: + result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); + break; + default: + result = playAbility(ability.copy(), game); + break; + } + } + + //if player has taken an action then reset all player passed flags + justActivatedType = null; + if (result) { + if (isHuman() + && (ability.getAbilityType() == AbilityType.SPELL + || ability.getAbilityType() == AbilityType.ACTIVATED)) { + if (ability.isUsesStack()) { // if the ability does not use the stack (e.g. Suspend) auto pass would go to next phase unintended + setJustActivatedType(ability.getAbilityType()); + } + } + game.getPlayers().resetPassed(); + } + return result; + } + + @Override + public boolean triggerAbility(TriggeredAbility triggeredAbility, Game game) { + if (triggeredAbility == null) { + logger.warn("Null source in triggerAbility method"); + throw new IllegalArgumentException("source TriggeredAbility must not be null"); + } + //20091005 - 603.3c, 603.3d + int bookmark = game.bookmarkState(); + TriggeredAbility ability = triggeredAbility.copy(); + MageObject sourceObject = ability.getSourceObject(game); + if (sourceObject != null) { + sourceObject.adjustTargets(ability, game); + } + UUID triggerId = null; + if (ability.canChooseTarget(game, playerId)) { + if (ability.isUsesStack()) { + game.getStack().push(new StackAbility(ability, playerId)); + } + if (ability.activate(game, false)) { + if ((ability.isUsesStack() + || ability.getRuleVisible()) + && !game.isSimulation()) { + game.informPlayers(getLogName() + " - " + ability.getGameLogMessage(game)); + } + if (!ability.isUsesStack()) { + ability.resolve(game); + } else { + game.fireEvent(new GameEvent( + GameEvent.EventType.TRIGGERED_ABILITY, + ability.getId(), ability, ability.getControllerId() + )); + triggerId = ability.getId(); + } + game.removeBookmark(bookmark); + return true; + } + } + restoreState(bookmark, triggeredAbility.getRule(), game); // why restore is needed here? (to remove the triggered ability from the stack because of no possible targets) + GameEvent event = new GameEvent( + GameEvent.EventType.ABILITY_TRIGGERED, + triggerId, ability, ability.getControllerId() + ); + game.getState().setValue(event.getId().toString(), ability.getTriggerEvent()); + game.fireEvent(event); + return false; + } + + /** + * Return spells for possible cast Uses in GUI to show only playable spells + * for choosing from the card (example: effect allow to cast card and player + * must choose the spell ability) + * + * @param game + * @param playerId + * @param object + * @param zone + * @param noMana + * @return + */ + public static LinkedHashMap getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { + // it uses simple check from spellCanBeActivatedRegularlyNow + // reason: no approved info here (e.g. forced to choose spell ability from cast card) + LinkedHashMap useable = new LinkedHashMap<>(); + Abilities allAbilities; + if (object instanceof Card) { + allAbilities = ((Card) object).getAbilities(game); + } else { + allAbilities = object.getAbilities(); + } + + for (Ability ability : allAbilities) { + if (ability instanceof SpellAbility) { + SpellAbility spellAbility = (SpellAbility) ability; + + switch (spellAbility.getSpellAbilityType()) { + case BASE_ALTERNATE: + // rules: + // If you cast a spell “without paying its mana cost,” you can’t choose to cast it for + // any alternative costs. You can, however, pay additional costs, such as kicker costs. + // If the card has any mandatory additional costs, those must be paid to cast the spell. + // (2021-02-05) + if (!noMana) { + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability + } + return useable; + } + break; + case SPLIT_FUSED: + // rules: + // If you cast a split card with fuse from your hand without paying its mana cost, + // you can choose to use its fuse ability and cast both halves without paying their mana costs. + if (zone == Zone.HAND) { + if (spellAbility.canChooseTarget(game, playerId)) { + useable.put(spellAbility.getId(), spellAbility); + } + } + case SPLIT: + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getLeftHalfCard().getSpellAbility()); + } + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getRightHalfCard().getSpellAbility()); + } + return useable; + case SPLIT_AFTERMATH: + if (zone == Zone.GRAVEYARD) { + if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getRightHalfCard().getSpellAbility()); + } + } else { + if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) { + useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), + ((SplitCard) object).getLeftHalfCard().getSpellAbility()); + } + } + return useable; + default: + if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + useable.put(spellAbility.getId(), spellAbility); + } + } + } + } + return useable; + } + + @Override + public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { + LinkedHashMap useable = new LinkedHashMap<>(); + // stack abilities - can't activate anything + // spell ability - can activate additional abilities (example: "Lightning Storm") + if (object instanceof StackAbility || object == null) { + return useable; + } + boolean previousState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + try { + // collect and filter playable activated abilities + // GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right) + Set needIds = CardUtil.getObjectParts(object); + + // workaround to find all abilities first and filter it for one object + List allPlayable = getPlayable(game, true, zone, false); + for (ActivatedAbility ability : allPlayable) { + if (needIds.contains(ability.getSourceId())) { + useable.putIfAbsent(ability.getId(), ability); + } + } + } finally { + game.setCheckPlayableState(previousState); + } + return useable; + } + + protected LinkedHashMap getUseableManaAbilities(MageObject object, Zone zone, Game game) { + LinkedHashMap useable = new LinkedHashMap<>(); + boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); + for (ActivatedManaAbilityImpl ability : object.getAbilities().getActivatedManaAbilities(zone)) { + if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { + if (ability.canActivate(playerId, game).canActivate()) { + useable.put(ability.getId(), ability); + } + } + } + return useable; + } + + @Override + public int getLandsPlayed() { + return landsPlayed; + } + + @Override + public boolean canPlayLand() { + //20091005 - 114.2a + return landsPlayed < landsPerTurn; + } + + protected boolean isActivePlayer(Game game) { + return game.isActivePlayer(this.playerId); + } + + @Override + public void shuffleLibrary(Ability source, Game game) { + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SHUFFLE_LIBRARY, playerId, source, playerId))) { + this.library.shuffle(); + if (!game.isSimulation()) { + game.informPlayers(getLogName() + "'s library is shuffled" + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, source, playerId)); + } + } + + @Override + public void revealCards(Ability source, Cards cards, Game game) { + revealCards(source, null, cards, game, true); + } + + @Override + public void revealCards(String titleSuffix, Cards cards, Game game) { + revealCards(titleSuffix, cards, game, true); + } + + @Override + public void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog) { + revealCards(null, titleSuffix, cards, game, postToLog); + } + + @Override + public void revealCards(Ability source, String titleSuffix, Cards cards, Game game) { + revealCards(source, titleSuffix, cards, game, true); + } + + @Override + public void revealCards(Ability source, String titleSuffix, Cards cards, Game game, boolean postToLog) { + if (cards == null || cards.isEmpty()) { + return; + } + if (postToLog) { + game.getState().getRevealed().add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + } else { + game.getState().getRevealed().update(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + } + if (postToLog && !game.isSimulation()) { + StringBuilder sb = new StringBuilder(getLogName()).append(" reveals "); + int current = 0, last = cards.size(); + for (Card card : cards.getCards(game)) { + current++; + sb.append(GameLog.getColoredObjectName(card)); + if (current < last) { + sb.append(", "); + } + } + sb.append(CardUtil.getSourceLogName(game, source)); + game.informPlayers(sb.toString()); + } + } + + @Override + public void lookAtCards(String titleSuffix, Card card, Game game) { + game.getState().getLookedAt(this.playerId).add(titleSuffix, card); + game.fireUpdatePlayersEvent(); + } + + @Override + public void lookAtCards(String titleSuffix, Cards cards, Game game) { + this.lookAtCards(null, titleSuffix, cards, game); + } + + @Override + public void lookAtCards(Ability source, String titleSuffix, Cards cards, Game game) { + game.getState().getLookedAt(this.playerId).add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); + game.fireUpdatePlayersEvent(); + } + + @Override + public void phasing(Game game) { + //20091005 - 502.1 + List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); + for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { + // 502.15i When a permanent phases out, any local enchantments or Equipment + // attached to that permanent phase out at the same time. This alternate way of + // phasing out is known as phasing out "indirectly." An enchantment or Equipment + // that phased out indirectly won't phase in by itself, but instead phases in + // along with the card it's attached to. + Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); + if (!(attachedTo != null && attachedTo.isControlledBy(this.getId()))) { + permanent.phaseOut(game, false); + } + } + for (Permanent permanent : phasedOut) { + if (!permanent.isPhasedOutIndirectly()) { + permanent.phaseIn(game); + } + } + } + + @Override + public void untap(Game game) { + // create list of all "notMoreThan" effects to track which one are consumed + Map>, Integer> notMoreThanEffectsUsage = new HashMap<>(); + for (Entry> restrictionEffect + : game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { + notMoreThanEffectsUsage.put(restrictionEffect, restrictionEffect.getKey().getNumber()); + } + + if (!notMoreThanEffectsUsage.isEmpty()) { + // create list of all permanents that can be untapped generally + List canBeUntapped = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, null, game, true); + } + if (untap) { + canBeUntapped.add(permanent); + } + } + // selected permanents to untap + List selectedToUntap = new ArrayList<>(); + + // player can cancel the selection of an effect to use a preferred order of restriction effects + boolean playerCanceledSelection; + do { + playerCanceledSelection = false; + // select permanents to untap to consume the "notMoreThan" effects + for (Map.Entry>, Integer> handledEntry : notMoreThanEffectsUsage.entrySet()) { + // select a permanent to untap for this entry + int numberToUntap = handledEntry.getValue(); + if (numberToUntap > 0) { + + List leftForUntap = getPermanentsThatCanBeUntapped(game, + canBeUntapped, + handledEntry.getKey().getKey(), + notMoreThanEffectsUsage); + + FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); + String message = filter.getMessage(); + // omit already from other untap effects selected permanents + for (Permanent permanent : selectedToUntap) { + filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); + } + // while targets left and there is still allowed to untap + while (canRespond() && !leftForUntap.isEmpty() && numberToUntap > 0) { + // player has to select the permanent they want to untap for this restriction + Ability ability = handledEntry.getKey().getValue().iterator().next(); + if (ability != null) { + StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), + numberToUntap)).append(" in total"); + MageObject effectSource = game.getObject(ability.getSourceId()); + if (effectSource != null) { + sb.append(" from ").append(effectSource.getLogName()); + } + sb.append(')'); + filter.setMessage(sb.toString()); + Target target = new TargetPermanent(1, 1, filter, true); + if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { + // player canceled, go on with the next effect (if no other effect available, this effect will be active again) + playerCanceledSelection = true; + break; + } + Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); + if (leftForUntap.contains(selectedPermanent)) { + selectedToUntap.add(selectedPermanent); + numberToUntap--; + // don't allow to select same permanent twice + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getValue() > 0 + && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { + notMoreThanEffect.setValue(notMoreThanEffect.getValue() - 1); + } + } + // update the left for untap list + leftForUntap = getPermanentsThatCanBeUntapped(game, + canBeUntapped, + handledEntry.getKey().getKey(), + notMoreThanEffectsUsage); + // remove already selected permanents + for (Permanent permanent : selectedToUntap) { + leftForUntap.remove(permanent); + } + + } else { + // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) + filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); + if (this.isHuman() && !game.isSimulation()) { + game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); + } + } + } + } + } + } + + } while (canRespond() && playerCanceledSelection); + + if (!game.isSimulation()) { + // show in log which permanents were selected to untap + for (Permanent permanent : selectedToUntap) { + game.informPlayers(this.getLogName() + " untapped " + permanent.getLogName()); + } + } + // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList + for (Permanent permanent : canBeUntapped) { + boolean doUntap = true; + if (!selectedToUntap.contains(permanent)) { + // if the permanent is covered by one of the restriction effects, don't untap it + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { + doUntap = false; + break; + } + } + } + if (permanent != null && doUntap) { + permanent.untap(game); + } + + } + + } else { + //20091005 - 502.2 + + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { + boolean untap = true; + for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { + untap &= effect.canBeUntapped(permanent, null, game, true); + } + if (untap) { + permanent.untap(game); + } + } + } + } + + private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, Map>, Integer> notMoreThanEffectsUsage) { + List leftForUntap = new ArrayList<>(); + // select permanents that can still be untapped + for (Permanent permanent : canBeUntapped) { + if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry + boolean canBeSelected = true; + // check if the permanent is restricted by another restriction that has left no permanent + for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { + if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) + && notMoreThanEffect.getValue() == 0) { + canBeSelected = false; + break; + } + } + if (canBeSelected) { + leftForUntap.add(permanent); + } + } + } + return leftForUntap; + } + + @Override + public UUID getId() { + return playerId; + } + + @Override + public Cards getHand() { + return hand; + } + + @Override + public Graveyard getGraveyard() { + return graveyard; + } + + @Override + public ManaPool getManaPool() { + return this.manaPool; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getLogName() { + return GameLog.getColoredPlayerName(name); + } + + @Override + public boolean isHuman() { + return human; + } + + @Override + public Library getLibrary() { + return library; + } + + @Override + public Cards getSideboard() { + return sideboard; + } + + @Override + public int getLife() { + return life; + } + + @Override + public void initLife(int life) { + this.life = life; + } + + @Override + public void setLife(int life, Game game, Ability source) { + // rule 118.5 + if (life > this.life) { + gainLife(life - this.life, game, source); + } else if (life < this.life) { + loseLife(this.life - life, game, source, false); + } + } + + @Override + public void setLifeTotalCanChange(boolean lifeTotalCanChange) { + this.canGainLife = lifeTotalCanChange; + this.canLoseLife = lifeTotalCanChange; + } + + @Override + public boolean isLifeTotalCanChange() { + return canGainLife || canLoseLife; + } + + @Override + public List getAlternativeSourceCosts() { + return alternativeSourceCosts; + } + + @Override + public boolean isCanLoseLife() { + return canLoseLife; + } + + @Override + public void setCanLoseLife(boolean canLoseLife) { + this.canLoseLife = canLoseLife; + } + + @Override + public int loseLife(int amount, Game game, Ability source, boolean atCombat, UUID attackerId) { + if (!canLoseLife || !this.isInGame()) { + return 0; + } + GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE, + playerId, source, playerId, amount, atCombat); + if (!game.replaceEvent(event)) { + this.life = CardUtil.overflowDec(this.life, event.getAmount()); + if (!game.isSimulation()) { + UUID needId = attackerId; + if (needId == null) { + needId = source == null ? null : source.getSourceId(); + } + game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life" + + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); + } + if (amount > 0) { + game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, + playerId, source, playerId, amount, atCombat)); + } + return amount; + } + return 0; + } + + @Override + public int loseLife(int amount, Game game, Ability source, boolean atCombat) { + return loseLife(amount, game, source, atCombat, null); + } + + @Override + public boolean isCanGainLife() { + return canGainLife; + } + + @Override + public void setCanGainLife(boolean canGainLife) { + this.canGainLife = canGainLife; + } + + @Override + public int gainLife(int amount, Game game, Ability source) { + if (!canGainLife || amount <= 0) { + return 0; + } + GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE, + playerId, source, playerId, amount, false); + if (!game.replaceEvent(event)) { + // TODO: lock life at Integer.MAX_VALUE if reached, until it's set to a different amount + // (https://magic.wizards.com/en/articles/archive/news/unstable-faqawaslfaqpaftidawabiajtbt-2017-12-06 - "infinite" life total stays infinite no matter how much is gained or lost) + // this.life += event.getAmount(); + this.life = CardUtil.overflowInc(this.life, event.getAmount()); + if (!game.isSimulation()) { + game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life" + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE, + playerId, source, playerId, event.getAmount())); + return event.getAmount(); + } + return 0; + } + + @Override + public void exchangeLife(Player player, Ability source, Game game) { + int lifePlayer1 = getLife(); + int lifePlayer2 = player.getLife(); + if ((lifePlayer1 != lifePlayer2 && this.isLifeTotalCanChange() && player.isLifeTotalCanChange()) + && (lifePlayer1 >= lifePlayer2 || (this.isCanGainLife() && player.isCanLoseLife())) + && (lifePlayer1 <= lifePlayer2 || (this.isCanLoseLife() && player.isCanGainLife()))) { + this.setLife(lifePlayer2, game, source); + player.setLife(lifePlayer1, game, source); + } + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game) { + return doDamage(damage, attackerId, source, game, false, true, null); + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) { + return doDamage(damage, attackerId, source, game, combatDamage, preventable, null); + } + + @Override + public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { + return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects); + } + + private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { + if (!this.isInGame()) { + return 0; + } + + if (damage < 1) { + return 0; + } + if (!canDamage(game.getObject(attackerId), game)) { + MageObject sourceObject = game.getObject(attackerId); + game.informPlayers(damage + " damage " + + (sourceObject == null ? "" : "from " + sourceObject.getLogName()) + + " to " + getLogName() + + (damage > 1 ? " were" : "was") + " prevented because of protection"); + return 0; + } + DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage); + event.setAppliedEffects(appliedEffects); + if (game.replaceEvent(event)) { + return 0; + } + int actualDamage = event.getAmount(); + if (actualDamage < 1) { + return 0; + } + UUID sourceControllerId = null; + Abilities sourceAbilities = null; + MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId); + if (attacker == null) { + StackObject stackObject = game.getStack().getStackObject(attackerId); + if (stackObject != null) { + attacker = stackObject.getStackAbility().getSourceObject(game); + } else { + attacker = game.getObject(attackerId); + } + if (attacker instanceof Spell) { + sourceAbilities = ((Spell) attacker).getAbilities(game); + sourceControllerId = ((Spell) attacker).getControllerId(); + } else if (attacker instanceof Card) { + sourceAbilities = ((Card) attacker).getAbilities(game); + sourceControllerId = ((Card) attacker).getOwnerId(); + } else if (attacker instanceof CommandObject) { + sourceControllerId = ((CommandObject) attacker).getControllerId(); + sourceAbilities = attacker.getAbilities(); + } + } else { + sourceAbilities = ((Permanent) attacker).getAbilities(game); + sourceControllerId = ((Permanent) attacker).getControllerId(); + } + if (event.isAsThoughInfect() || (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId()))) { + addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game); + } else { + GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS, + playerId, source, playerId, actualDamage, combatDamage); + if (!game.replaceEvent(damageToLifeLossEvent)) { + this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId); + } + } + if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { + if (combatDamage) { + game.getPermanent(attackerId).markLifelink(actualDamage); + } else { + Player player = game.getPlayer(sourceControllerId); + player.gainLife(actualDamage, game, source); + } + } + // Unstable ability - Earl of Squirrel + if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) { + Player player = game.getPlayer(sourceControllerId); + new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId()); + } + DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage); + game.fireEvent(damagedEvent); + game.getState().addSimultaneousDamage(damagedEvent, game); + return actualDamage; + } + + @Override + public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) { + boolean returnCode = true; + GameEvent addingAllEvent = GameEvent.getEvent( + GameEvent.EventType.ADD_COUNTERS, playerId, source, + playerAddingCounters, counter.getName(), counter.getCount() + ); + if (!game.replaceEvent(addingAllEvent)) { + int amount = addingAllEvent.getAmount(); + int finalAmount = amount; + boolean isEffectFlag = addingAllEvent.getFlag(); + for (int i = 0; i < amount; i++) { + Counter eventCounter = counter.copy(); + eventCounter.remove(eventCounter.getCount() - 1); + GameEvent addingOneEvent = GameEvent.getEvent( + GameEvent.EventType.ADD_COUNTER, playerId, source, + playerAddingCounters, counter.getName(), 1 + ); + addingOneEvent.setFlag(isEffectFlag); + if (!game.replaceEvent(addingOneEvent)) { + getCounters().addCounter(eventCounter); + GameEvent addedOneEvent = GameEvent.getEvent( + GameEvent.EventType.COUNTER_ADDED, playerId, source, + playerAddingCounters, counter.getName(), 1 + ); + addedOneEvent.setFlag(addingOneEvent.getFlag()); + game.fireEvent(addedOneEvent); + } else { + finalAmount--; + returnCode = false; + } + } + if (finalAmount > 0) { + GameEvent addedAllEvent = GameEvent.getEvent( + GameEvent.EventType.COUNTERS_ADDED, playerId, source, + playerAddingCounters, counter.getName(), amount + ); + addedAllEvent.setFlag(addingAllEvent.getFlag()); + game.fireEvent(addedAllEvent); + } + } else { + returnCode = false; + } + return returnCode; + } + + @Override + public void removeCounters(String name, int amount, Ability source, Game game) { + int finalAmount = 0; + for (int i = 0; i < amount; i++) { + if (!counters.removeCounter(name, 1)) { + break; + } + GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, + getId(), source, (source == null ? null : source.getControllerId())); + event.setData(name); + event.setAmount(1); + game.fireEvent(event); + finalAmount++; + } + GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, + getId(), source, (source == null ? null : source.getControllerId())); + event.setData(name); + event.setAmount(finalAmount); + game.fireEvent(event); + } + + protected boolean canDamage(MageObject source, Game game) { + for (ProtectionAbility ability : abilities.getProtectionAbilities()) { + if (!ability.canTarget(source, game)) { + return false; + } + } + return true; + } + + @Override + public Abilities getAbilities() { + return this.abilities; + } + + @Override + public void addAbility(Ability ability) { + ability.setSourceId(playerId); + this.abilities.add(ability); + } + + @Override + public int getLandsPerTurn() { + return this.landsPerTurn; + } + + @Override + public void setLandsPerTurn(int landsPerTurn) { + this.landsPerTurn = landsPerTurn; + } + + @Override + public int getLoyaltyUsePerTurn() { + return this.loyaltyUsePerTurn; + } + + @Override + public void setLoyaltyUsePerTurn(int loyaltyUsePerTurn) { + this.loyaltyUsePerTurn = loyaltyUsePerTurn; + } + + @Override + public int getMaxHandSize() { + return maxHandSize; + } + + @Override + public void setMaxHandSize(int maxHandSize) { + this.maxHandSize = maxHandSize; + } + + @Override + public void setMaxAttackedBy(int maxAttackedBy) { + this.maxAttackedBy = maxAttackedBy; + } + + @Override + public int getMaxAttackedBy() { + return maxAttackedBy; + } + + @Override + public void setResponseString(String responseString) { + } + + @Override + public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) { + } + + @Override + public void setResponseUUID(UUID responseUUID) { + } + + @Override + public void setResponseBoolean(Boolean responseBoolean) { + } + + @Override + public void setResponseInteger(Integer responseInteger) { + } + + @Override + public boolean isPassed() { + return passed; + } + + @Override + public void pass(Game game) { + this.passed = true; + resetStoredBookmark(game); + } + + @Override + public void resetPassed() { + this.passed = this.loses || this.hasLeft(); + } + + @Override + public void resetPlayerPassedActions() { + this.passed = false; + this.passedTurn = false; + this.passedTurnSkipStack = false; + this.passedUntilEndOfTurn = false; + this.passedUntilNextMain = false; + this.passedUntilStackResolved = false; + this.dateLastAddedToStack = null; + this.passedUntilEndStepBeforeMyTurn = false; + this.skippedAtLeastOnce = false; + this.passedAllTurns = false; + this.justActivatedType = null; + } + + @Override + public void quit(Game game) { + quit = true; + this.concede(game); + logger.debug(getName() + " quits the match."); + game.informPlayers(getLogName() + " quits the match."); + } + + @Override + public void timerTimeout(Game game) { + quit = true; + timerTimeout = true; + this.concede(game); + game.informPlayers(getLogName() + " has run out of time, losing the match."); + } + + @Override + public void idleTimeout(Game game) { + quit = true; + idleTimeout = true; + this.concede(game); + game.informPlayers(getLogName() + " was idle for too long, losing the Match."); + } + + @Override + public void concede(Game game) { + game.setConcedingPlayer(playerId); + lost(game); +// this.left = true; + } + + @Override + public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { + switch (playerAction) { + case PASS_PRIORITY_UNTIL_MY_NEXT_TURN: // F9 + resetPlayerPassedActions(); + passedAllTurns = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_TURN_END_STEP: // F5 + resetPlayerPassedActions(); + passedUntilEndOfTurn = true; + skippedAtLeastOnce = PhaseStep.END_TURN != game.getTurn().getStepType(); + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4 + resetPlayerPassedActions(); + passedTurn = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_TURN_SKIP_STACK: // F6 + resetPlayerPassedActions(); + passedTurnSkipStack = true; + this.skip(); + break; + case PASS_PRIORITY_UNTIL_NEXT_MAIN_PHASE: //F7 + resetPlayerPassedActions(); + passedUntilNextMain = true; + skippedAtLeastOnce = !(game.getTurn().getStepType() == PhaseStep.POSTCOMBAT_MAIN + || game.getTurn().getStepType() == PhaseStep.PRECOMBAT_MAIN); + this.skip(); + break; + case PASS_PRIORITY_UNTIL_STACK_RESOLVED: // Default F10 - Skips until the current stack is resolved + if (!game.getStack().isEmpty()) { // If stack is empty do nothing + resetPlayerPassedActions(); + passedUntilStackResolved = true; + dateLastAddedToStack = game.getStack().getDateLastAdded(); + this.skip(); + } + break; + case PASS_PRIORITY_UNTIL_END_STEP_BEFORE_MY_NEXT_TURN: //F11 + resetPlayerPassedActions(); + passedUntilEndStepBeforeMyTurn = true; + this.skip(); + break; + case PASS_PRIORITY_CANCEL_ALL_ACTIONS: + resetPlayerPassedActions(); + break; + case PERMISSION_REQUESTS_ALLOWED_OFF: + userData.setAllowRequestShowHandCards(false); + break; + case PERMISSION_REQUESTS_ALLOWED_ON: + userData.setAllowRequestShowHandCards(true); + userData.resetRequestedHandPlayersList(game.getId()); // users can send request again + break; + } + logger.trace("PASS Priority: " + playerAction); + } + + @Override + public void leave() { + this.passed = true; + this.loses = true; + this.left = true; + this.abort(); + //20100423 - 800.4a + this.hand.clear(); + this.graveyard.clear(); + this.library.clear(); + } + + @Override + public boolean hasLeft() { + return this.left; + } + + @Override + public void lost(Game game) { + if (canLose(game)) { + lostForced(game); + } + } + + @Override + public void lostForced(Game game) { + logger.debug(this.getName() + " has lost gameId: " + game.getId()); + //20100423 - 603.9 + if (!this.wins) { + this.loses = true; + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LOST, null, null, playerId)); + game.informPlayers(this.getLogName() + " has lost the game."); + } else { + logger.debug(this.getName() + " has already won - stop lost"); + } + // for draw - first all players that have lost have to be set to lost + if (!hasLeft()) { + logger.debug("Game over playerId: " + playerId); + game.setConcedingPlayer(playerId); + } + } + + @Override + public boolean canLose(Game game) { + return hasLeft() // If a player concedes or has left the match they lose also if effect would say otherwise + || !game.replaceEvent(new GameEvent(GameEvent.EventType.LOSES, null, null, playerId)); + } + + @Override + public void won(Game game) { + boolean opponentInGame = false; + for (UUID opponentId : game.getOpponents(playerId)) { + Player opponent = game.getPlayer(opponentId); + + if (opponent != null && opponent.isInGame()) { + opponentInGame = true; + break; + } + } + if (!opponentInGame + || // if no more opponent is in game the wins event may no longer be replaced + !game.replaceEvent(new GameEvent(GameEvent.EventType.WINS, null, null, playerId))) { + logger.debug("player won -> start: " + this.getName()); + if (!this.loses) { + //20130501 - 800.7, 801.16 + // all opponents in range loose the game + for (UUID opponentId : game.getOpponents(playerId)) { + Player opponent = game.getPlayer(opponentId); + if (opponent != null && !opponent.hasLost()) { + logger.debug("player won -> calling opponent lost: " + + this.getName() + " opponent: " + opponent.getName()); + opponent.lostForced(game); + } + } + // if no more opponents alive (independant from range), you win and the game ends + int opponentsAlive = 0; + for (UUID playerIdToCheck : game.getPlayerList()) { + if (game.isOpponent(this, playerIdToCheck)) { // Check without range + Player opponent = game.getPlayer(playerIdToCheck); + if (opponent != null && !opponent.hasLost()) { + opponentsAlive++; + } + } + } + if (opponentsAlive == 0 && !hasWon()) { + logger.debug("player won -> No more opponents alive game won: " + this.getName()); + game.informPlayers(this.getLogName() + " has won the game"); + this.wins = true; + game.end(); + } + } else { + logger.debug("player won -> but already lost before or other players still alive: " + this.getName()); + } + } + } + + @Override + public void drew(Game game) { + if (!hasLost()) { + this.draws = true; + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); + game.informPlayers("For " + this.getLogName() + " the game is a draw."); + game.setConcedingPlayer(playerId); + } + } + + @Override + public boolean hasLost() { + return this.loses; + } + + @Override + public boolean isInGame() { + return !hasQuit() && !hasLost() && !hasWon() && !hasDrew() && !hasLeft(); + } + + @Override + public boolean canRespond() { // abort is checked here to get out of player requests (as example: after disconnect) + return isInGame() && !abort; + } + + @Override + public boolean hasWon() { + return !this.loses && this.wins; + } + + @Override + public boolean hasDrew() { + return this.draws; + } + + @Override + public void declareAttacker(UUID attackerId, UUID defenderId, Game game, boolean allowUndo) { + if (allowUndo) { + setStoredBookmark(game.bookmarkState()); // makes it possible to UNDO a declared attacker with costs from e.g. Propaganda + } + Permanent attacker = game.getPermanent(attackerId); + if (attacker != null + && attacker.canAttack(defenderId, game) + && attacker.isControlledBy(playerId)) { + if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) { + game.undo(playerId); + } + } + } + + @Override + public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game) { + declareBlocker(defenderId, blockerId, attackerId, game, true); + } + + @Override + public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game, boolean allowUndo) { + if (isHuman() && allowUndo) { + setStoredBookmark(game.bookmarkState()); + } + Permanent blocker = game.getPermanent(blockerId); + CombatGroup group = game.getCombat().findGroup(attackerId); + if (blocker != null && group != null && group.canBlock(blocker, game)) { + group.addBlocker(blockerId, playerId, game); + game.getCombat().addBlockingGroup(blockerId, attackerId, playerId, game); + } else if (this.isHuman() && !game.isSimulation()) { + game.informPlayer(this, "You can't block this creature."); + } + } + + @Override + public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game) { + return searchLibrary(target, source, game, playerId); + } + + @Override + public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { + //20091005 - 701.14c + + // searching control can be intercepted by another player, see Opposition Agent + SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source, playerId, Integer.MAX_VALUE); + if (game.replaceEvent(searchEvent)) { + return false; + } + + Player targetPlayer = game.getPlayer(targetPlayerId); + Player searchingPlayer = this; + Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId()); + if (targetPlayer == null || searchingController == null) { + return false; + } + + String searchInfo = searchingPlayer.getLogName(); + if (!searchingPlayer.getId().equals(searchingController.getId())) { + searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName(); + } + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + searchInfo = searchInfo + " searches their library"; + } else { + searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName(); + } + + if (!game.isSimulation()) { + game.informPlayers(searchInfo + CardUtil.getSourceLogName(game, source)); + } + + // https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/ + // You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect: + // * While you’re searching your library, you may cast Panglacial Wurm from your library. + // So use here same code as Word of Command + // P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest + boolean takeControl = false; + if (!searchingPlayer.getId().equals(searchingController.getId())) { + CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true); + takeControl = true; + } + + Library searchingLibrary = targetPlayer.getLibrary(); + TargetCardInLibrary newTarget = target.copy(); + int count; + int librarySearchLimit = searchEvent.getAmount(); + List cardsFromTop = null; + do { + // TODO: prevent shuffling from moving the visualized cards + if (librarySearchLimit == Integer.MAX_VALUE) { + count = searchingLibrary.count(target.getFilter(), game); + } else { + if (cardsFromTop == null) { + cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit)); + } else { + cardsFromTop.retainAll(searchingLibrary.getCards(game)); + } + newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); + count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit); + } + + if (count < target.getNumberOfTargets()) { + newTarget.setMinNumberOfTargets(count); + } + + // handling Panglacial Wurm - cast cards while searching from own library + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) { + // clear all choices to start from scratch (casted cards must be removed from library) + newTarget.clearChosen(); + continue; + } + } + + if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) { + target.getTargets().clear(); + for (UUID targetId : newTarget.getTargets()) { + target.add(targetId, game); + } + } + + // END SEARCH + if (takeControl) { + CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer); + game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back" + CardUtil.getSourceLogName(game, source)); + } + + LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source, searchingPlayer.getId(), target); + if (!game.replaceEvent(searchedEvent)) { + game.fireEvent(searchedEvent); + } + break; + } while (true); + + return true; + } + + @Override + public boolean seekCard(FilterCard filter, Ability source, Game game) { + Set cards = this.getLibrary() + .getCards(game) + .stream() + .filter(card -> filter.match(card, source.getSourceId(), getId(), game)) + .collect(Collectors.toSet()); + Card card = RandomUtil.randomFromCollection(cards); + if (card == null) { + return false; + } + game.informPlayers(this.getLogName() + " seeks a card from their library"); + this.moveCards(card, Zone.HAND, source, game); + return true; + } + + @Override + public void lookAtAllLibraries(Ability source, Game game) { + for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { + Player player = game.getPlayer(playerId); + String playerName = this.getName().equals(player.getName()) ? "Your " : player.getName() + "'s "; + playerName += "library"; + Cards cardsInLibrary = new CardsImpl(player.getLibrary().getTopCards(game, player.getLibrary().size())); + lookAtCards(playerName, cardsInLibrary, game); + } + } + + private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) { + // must return true after cast try (to restart searching process without casted cards) + // uses for handling Panglacial Wurm: + // * While you're searching your library, you may cast Panglacial Wurm from your library. + + List castableCards = library.getCards(game).stream() + .filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class)) + .map(MageItem::getId) + .collect(Collectors.toList()); + if (castableCards.size() == 0) { + return false; + } + + // only humans can use it + if (targetPlayer.isComputer()) { + return false; + } + + if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "There are " + castableCards.size() + " cards you can cast while searching your library. Cast any of them?", null, game)) { + return false; + } + + boolean casted = false; + TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD); + targetCard.setTargetName("card to cast from library"); + targetCard.setNotTarget(true); + while (castableCards.size() > 0) { + targetCard.clearChosen(); + if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) { + break; + } + + Card card = game.getCard(targetCard.getFirstTarget()); + if (card == null) { + break; + } + + // AI NOTICE: if you want AI implement here then remove selected card from castable after each + // choice (otherwise you catch infinite freeze on uncastable use case) + // casting selected card + // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + castableCards.remove(card.getId()); + casted = true; + } + return casted; + } + + /** + * @param source + * @param game + * @param winnable + * @return if winnable, true if player won the toss, if not winnable, true + * for heads and false for tails + */ + @Override + public boolean flipCoin(Ability source, Game game, boolean winnable) { + boolean chosen = false; + if (winnable) { + chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); + game.informPlayers(getLogName() + " chose " + CardUtil.booleanToFlipName(chosen)); + } + boolean result = this.flipCoinResult(game); + FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable); + game.replaceEvent(event); + game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult()) + + CardUtil.getSourceLogName(game, source)); + if (event.getFlipCount() > 1) { + boolean canChooseHeads = event.getResult(); + boolean canChooseTails = !event.getResult(); + for (int i = 1; i < event.getFlipCount(); i++) { + boolean tempFlip = this.flipCoinResult(game); + canChooseHeads = canChooseHeads || tempFlip; + canChooseTails = canChooseTails || !tempFlip; + game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(tempFlip)); + } + if (canChooseHeads && canChooseTails) { + event.setResult(chooseUse(Outcome.Benefit, "Choose which flip to keep", + (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), + "Heads", "Tails", source, game + )); + } else { + event.setResult(canChooseHeads); + } + game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); + } + if (event.isWinnable()) { + game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip" + + CardUtil.getSourceLogName(game, source)); + } + game.fireEvent(event.createFlippedEvent()); + if (event.isWinnable()) { + return event.getResult() == event.getChosen(); + } + return event.getResult(); + } + + /** + * Return result for next flip coint try (can be contolled in tests) + * + * @return + */ + @Override + public boolean flipCoinResult(Game game) { + return RandomUtil.nextBoolean(); + } + + private static final class RollDieResult { + + // 706.2. + // After the roll, the number indicated on the top face of the die before any modifiers is + // the natural result. The instruction may include modifiers to the roll which add to or + // subtract from the natural result. Modifiers may also come from other sources. After + // considering all applicable modifiers, the final number is the result of the die roll. + private final int naturalResult; + private final int modifier; + private final PlanarDieRollResult planarResult; + + RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) { + this.naturalResult = naturalResult; + this.modifier = modifier; + this.planarResult = planarResult; + } + + public int getResult() { + return this.naturalResult + this.modifier; + } + + public PlanarDieRollResult getPlanarResult() { + return this.planarResult; + } + } + + @Override + public int rollDieResult(int sides, Game game) { + return RandomUtil.nextInt(sides) + 1; + } + + /** + * Roll single die. Support both die types: planar and numerical. + * + * @param outcome + * @param game + * @param source + * @param rollDieType + * @param sidesAmount + * @param chaosSidesAmount + * @param planarSidesAmount + * @param rollsAmount + * @return + */ + private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { + if (rollsAmount == 1) { + return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); + } + Set choices = new HashSet<>(); + for (int j = 0; j < rollsAmount; j++) { + choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount)); + } + if (choices.size() == 1) { + return choices.stream().findFirst().orElse(0); + } + + // AI hint - use max/min values + if (this.isComputer()) { + if (rollDieType == RollDieType.NUMERICAL) { + // numerical + if (outcome.isGood()) { + return choices.stream() + .map(Integer.class::cast) + .max(Comparator.naturalOrder()) + .orElse(null); + } else { + return choices.stream() + .map(Integer.class::cast) + .min(Comparator.naturalOrder()) + .orElse(null); + } + } else { + // planar + // priority: chaos -> planar -> blank + return choices.stream() + .map(PlanarDieRollResult.class::cast) + .max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority)) + .orElse(null); + } + } + + Choice choice = new ChoiceImpl(true); + choice.setMessage("Choose which die roll result to keep (the rest will be ignored)"); + choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet())); + + this.choose(Outcome.Neutral, choice, game); + Object defaultChoice = choices.iterator().next(); + return choices.stream() + .filter(o -> o.toString().equals(choice.getChoice())) + .findFirst() + .orElse(defaultChoice); + } + + private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) { + switch (rollDieType) { + + case NUMERICAL: { + int result = rollDieResult(numSides, game); + // Clam-I-Am workaround: + // If you roll a 3 on a six-sided die, you may reroll that die. + if (numSides == 6 + && result == 3 + && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId())) + && chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) { + result = rollDieResult(numSides, game); + } + return result; + } + + case PLANAR: { + if (numChaosSides + numPlanarSides > numSides) { + numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES; + numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES; + } + // for 9 sides: + // 1..2 - chaos + // 3..7 - blank + // 8..9 - planar + int result = this.rollDieResult(numSides, game); + PlanarDieRollResult roll; + if (result <= numChaosSides) { + roll = PlanarDieRollResult.CHAOS_ROLL; + } else if (result > numSides - numPlanarSides) { + roll = PlanarDieRollResult.PLANAR_ROLL; + } else { + roll = PlanarDieRollResult.BLANK_ROLL; + } + return roll; + } + + default: { + throw new IllegalArgumentException("Unknown roll die type " + rollDieType); + } + } + } + + /** + * @param outcome + * @param source + * @param game + * @param sidesAmount number of sides the dice has + * @param rollsAmount number of tries to roll the dice + * @param ignoreLowestAmount remove the lowest rolls from the results + * @return the number that the player rolled + */ + @Override + public List rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) { + return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount) + .stream() + .map(Integer.class::cast) + .collect(Collectors.toList()); + } + + /** + * Inner code to roll a dice. Support normal and planar types. + * + * @param outcome + * @param source + * @param game + * @param rollDieType die type to roll, e.g. planar or numerical + * @param sidesAmount sides per die + * @param chaosSidesAmount for planar die: chaos sides + * @param planarSidesAmount for planar die: planar sides + * @param rollsAmount rolls + * @param ignoreLowestAmount for numerical die: ignore multiple rolls with + * the lowest values + * @return + */ + private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, + int rollsAmount, int ignoreLowestAmount) { + RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); + if (ignoreLowestAmount > 0) { + rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); + } + game.replaceEvent(rollDiceEvent); + + // 706.6. + // In a Planechase game, rolling the planar die will cause any ability that triggers whenever a + // player rolls one or more dice to trigger. However, any effect that refers to a numerical + // result of a die roll, including ones that compare the results of that roll to other rolls + // or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.” + // ROLL MULTIPLE dies + // results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x) + List dieResults = new ArrayList<>(); + List dieRolls = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getAmount(); i++) { + // ROLL SINGLE die + RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides()); + game.replaceEvent(rollDieEvent); + + Object rollResult; + // big idea logic for numerical rolls only + if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) { + // rolls 2x + sum results + // The Big Idea: roll two six-sided dice and use the total of those results + int totalSum = 0; + for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) { + int singleResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount()); + totalSum += singleResult; + dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null)); + } + rollResult = totalSum; + } else { + // rolls 1x + switch (rollDieEvent.getRollDieType()) { + default: + case NUMERICAL: { + int naturalResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null)); + rollResult = naturalResult; + break; + } + + case PLANAR: { + PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(0, 0, planarResult)); + rollResult = planarResult; + break; + } + } + } + dieResults.add(rollResult); + } + + // ignore the lowest results + // planar dies: due to 706.6. planar die results must be fully ignored + // + // 706.5. + // If a player is instructed to roll two or more dice and ignore the lowest roll, the roll + // that yielded the lowest result is considered to have never happened. No abilities trigger + // because of the ignored roll, and no effects apply to that roll. If multiple results are tied + // for the lowest, the player chooses one of those rolls to be ignored. + int diceRolledTotal = dieRolls.size(); + String ignoreMessage; + if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) { + // find ignored values + List ignoredResults = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) { + int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0); + dieResults.remove(Integer.valueOf(min)); + ignoredResults.add(min); + } + ignoreMessage = String.format( + ignoredResults.size() > 1 ? ", ignoring [%s]" : ", ignoring %s", + ignoredResults + .stream() + .map(x -> "" + x) + .collect(Collectors.joining(", ")) + ); + // remove ignored rolls (they not exist anymore) + List newRolls = new ArrayList<>(); + for (RollDieResult rollDieResult : dieRolls) { + if (ignoredResults.contains(rollDieResult.getResult())) { + ignoredResults.remove((Integer) rollDieResult.getResult()); + } else { + newRolls.add(rollDieResult); + } + } + dieRolls.clear(); + dieRolls.addAll(newRolls); + } else { + ignoreMessage = ""; + } + + // raise affected roll events + for (RollDieResult result : dieRolls) { + game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult)); + } + game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source)); + + String resultString = dieResults + .stream() + .map(Object::toString) + .collect(Collectors.joining(", ")); + String message; + switch (rollDiceEvent.getRollDieType()) { + default: + case NUMERICAL: + // [Roll a die] user rolled 4d6, results: [4, 6], ignoring [1, 3] (source: xxx) + message = String.format("[Roll a die] %s rolled %sd%s, result%s: %s%s%s", + getLogName(), + diceRolledTotal > 1 ? diceRolledTotal : "a ", + rollDiceEvent.getSides(), + dieResults.size() > 1 ? 's' : "", + dieResults.size() > 1 ? '[' + resultString + ']' : resultString, + ignoreMessage, + CardUtil.getSourceLogName(game, source)); + break; + case PLANAR: + // [Roll a planar die] user rolled CHAOS (source: xxx) + message = String.format("[Roll a planar die] %s rolled %s%s", + getLogName(), + dieResults.size() > 1 ? '[' + resultString + ']' : resultString, + CardUtil.getSourceLogName(game, source)); + break; + } + game.informPlayers(message); + return dieResults; + } + + /** + * @param source + * @param game + * @param chaosSidesAmount The number of chaos sides the planar die + * currently has (normally 1 but can be 5) + * @param planarSidesAmount The number of chaos sides the planar die + * currently has (normally 1) + * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll + * or BlankRoll + */ + @Override + public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) { + return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0) + .stream() + .map(o -> (PlanarDieRollResult) o) + .findFirst() + .orElse(PlanarDieRollResult.BLANK_ROLL); + } + + @Override + public List getAvailableAttackers(Game game) { + // TODO: get available opponents and their planeswalkers, check for each if permanent can attack one + return getAvailableAttackers(null, game); + } + + @Override + public List getAvailableAttackers(UUID defenderId, Game game) { + FilterCreatureForCombat filter = new FilterCreatureForCombat(); + List attackers = game.getBattlefield().getAllActivePermanents(filter, playerId, game); + attackers.removeIf(entry -> !entry.canAttack(defenderId, game)); + return attackers; + } + + @Override + public List getAvailableBlockers(Game game) { + FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); + return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game); + } + + /** + * Returns the mana options the player currently has. That means which + * combinations of mana are available to cast spells or activate abilities + * etc. + * + * @param game + * @return + */ + @Override + public ManaOptions getManaAvailable(Game game) { + boolean oldState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + + ManaOptions availableMana = new ManaOptions(); + availableMana.addMana(manaPool.getMana()); + // conditional mana + for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { + availableMana.addMana(conditionalMana); + } + + List> sourceWithoutManaCosts = new ArrayList<>(); + List> sourceWithCosts = new ArrayList<>(); + for (Card card : getHand().getCards(game)) { + Abilities manaAbilities + = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); + for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + ActivatedManaAbilityImpl ability = it.next(); + Abilities noTapAbilities = new AbilitiesImpl<>(ability); + if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { + sourceWithoutManaCosts.add(noTapAbilities); + } else { + sourceWithCosts.add(noTapAbilities); + } + } + } + + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range + Boolean canUse = null; + boolean canAdd = false; + boolean useLater = false; // sources with mana costs or mana pool dependency + Abilities manaAbilities + = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true + for (Iterator it = manaAbilities.iterator(); it.hasNext();) { + ActivatedManaAbilityImpl ability = it.next(); + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse) { + // abilities without Tap costs have to be handled as separate sources, because they can be used also + if (!ability.hasTapCost()) { + it.remove(); + Abilities noTapAbilities = new AbilitiesImpl<>(ability); + if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { + sourceWithoutManaCosts.add(noTapAbilities); + } else { + sourceWithCosts.add(noTapAbilities); + } + continue; + } + + canAdd = true; + if (!ability.getManaCosts().isEmpty() || ability.isPoolDependant()) { + useLater = true; + break; + } + } + } + if (canAdd) { + if (useLater) { + sourceWithCosts.add(manaAbilities); + } else { + sourceWithoutManaCosts.add(manaAbilities); + } + } + } + + for (Abilities manaAbilities : sourceWithoutManaCosts) { + availableMana.addMana(manaAbilities, game); + } + + boolean anAbilityWasUsed = true; + boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production + while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { + anAbilityWasUsed = false; + for (Iterator> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { + Abilities manaAbilities = iterator.next(); + if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { + boolean used; + if (manaAbilities.hasPoolDependantAbilities()) { + used = availableMana.addManaPoolDependant(manaAbilities, game); + } else { + used = availableMana.addManaWithCost(manaAbilities, game); + } + if (used) { + iterator.remove(); + availableMana.removeDuplicated(); + anAbilityWasUsed = true; + } + } + } + if (!anAbilityWasUsed && !usePoolDependantAbilities) { + usePoolDependantAbilities = true; + anAbilityWasUsed = true; + } + } + + // remove duplicated variants (see ManaOptionsTest for info - when that rises) + availableMana.removeDuplicated(); + + game.setCheckPlayableState(oldState); + return availableMana; + } + + /** + * Used during calculation of available mana to gather the amount of + * producable triggered mana caused by using mana sources. So the set value + * is only used during the calculation of the mana produced by one source + * and cleared thereafter + * + * @param netManaAvailable the net mana produced by the triggered mana + * abaility + */ + @Override + public void addAvailableTriggeredMana(List netManaAvailable + ) { + this.availableTriggeredManaList.add(netManaAvailable); + } + + /** + * Used during calculation of available mana to get the amount of producable + * triggered mana caused by using mana sources. The list is cleared as soon + * the value is retrieved during available mana calculation. + * + * @return + */ + @Override + public List> getAvailableTriggeredMana() { + return availableTriggeredManaList; + } + // returns only mana producers that don't require mana payment + + protected List getAvailableManaProducers(Game game) { + List result = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { // Some permanents allow use of abilities from non controlling players. so check all permanents in range + Boolean canUse = null; + boolean canAdd = false; + for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + if (!ability.getManaCosts().isEmpty()) { + canAdd = false; + break; + } + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse && ability.canActivate(playerId, game).canActivate()) { + canAdd = true; + } + } + if (canAdd) { + result.add(permanent); + } + } + for (Card card : getHand().getCards(game)) { + boolean canAdd = false; + for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.HAND)) { + if (!ability.getManaCosts().isEmpty()) { + canAdd = false; + break; + } + if (ability.canActivate(playerId, game).canActivate()) { + canAdd = true; + } + } + if (canAdd) { + result.add(card); + } + } + return result; + } + + // returns only mana producers that require mana payment + public List getAvailableManaProducersWithCost(Game game) { + List result = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents(playerId, game)) { + Boolean canUse = null; + for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + if (canUse == null) { + canUse = permanent.canUseActivatedAbilities(game); + } + if (canUse && ability.canActivate(playerId, game).canActivate() + && !ability.getManaCosts().isEmpty()) { + result.add(permanent); + break; + } + } + } + return result; + } + + /** + * @param ability + * @param availableMana if null, it won't be checked if enough mana is + * available + * @param sourceObject + * @param game + * @return + */ + protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { + if (!(ability instanceof ActivatedManaAbilityImpl)) { + ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability + if (!copy.canActivate(playerId, game).canActivate()) { + return false; + } + if (availableMana != null) { + sourceObject.adjustCosts(copy, game); + game.getContinuousEffects().costModification(copy, game); + } + boolean canBeCastRegularly = true; + if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { + // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... + // 117.6a (...) If an alternative cost is applied to an unpayable cost, + // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. + canBeCastRegularly = false; + } + if (canBeCastRegularly) { + if (canPayMinimumManaCost(copy, availableMana, game)) { + return true; + } + } + + // ALTERNATIVE COST FROM dynamic effects + if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); + Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); + + boolean canPutToPlay = true; + if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; + } + if (costs != null && !costs.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; + } + + if (canPutToPlay) { + return true; + } + } + + // ALTERNATIVE COST from source card (any AlternativeSourceCosts) + if (AbilityType.SPELL.equals(ability.getAbilityType())) { + return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); + } + } + return false; + } + + protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { + ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); + if (abilityOptions.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + // Check for pay option with like phyrexian mana + if (getPhyrexianColors() != null) { + addPhyrexianLikePayOptions(abilityOptions, availableMana, game); + } + + ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), + AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); + for (Mana mana : abilityOptions) { + if (mana.count() == 0) { + return true; + } + for (Mana avail : availableMana) { + // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, + // but that code processing it as any color, need to test and fix another use cases + // (example: Sunglasses of Urza - may spend white mana as though it were red mana) + + // + // add tests for non any color like Sunglasses of Urza + if (approvingObject != null && mana.count() <= avail.count()) { + return true; + } + if (avail instanceof ConditionalMana && !((ConditionalMana) avail).apply(ability, game, getId(), ability.getManaCosts())) { + continue; + } + if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost + return true; + } + } + } + } + return false; + } + + private void addPhyrexianLikePayOptions(ManaOptions abilityOptions, ManaOptions availableMana, Game game) { + int maxLifeMana = getLife() / 2; + if (maxLifeMana > 0) { + Set phyrexianOptions = new HashSet<>(); + for (Mana mana : abilityOptions) { + int availableLifeMana = maxLifeMana; + if (getPhyrexianColors().isBlack()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLACK); + } + if (getPhyrexianColors().isBlue()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.BLUE); + } + if (getPhyrexianColors().isRed()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.RED); + } + if (getPhyrexianColors().isGreen()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.GREEN); + } + if (getPhyrexianColors().isWhite()) { + createReducedManaPayOption(availableLifeMana, mana, phyrexianOptions, ManaType.WHITE); + } + } + abilityOptions.addAll(phyrexianOptions); + } + } + + private int createReducedManaPayOption(int availableLifeMana, Mana oldPayOption, Set phyrexianOptions, ManaType manaType) { + if (oldPayOption.get(manaType) > 0) { + Mana manaCopy = oldPayOption.copy(); + int restVal; + if (availableLifeMana > oldPayOption.get(manaType)) { + restVal = 0; + availableLifeMana -= oldPayOption.get(manaType); + } else { + restVal = CardUtil.overflowDec(oldPayOption.get(manaType), availableLifeMana); + availableLifeMana = 0; + } + manaCopy.set(manaType, restVal); + phyrexianOptions.add(manaCopy); + } + return availableLifeMana; + } + + protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { + if (sourceObject != null && !(sourceObject instanceof Permanent)) { + Ability copyAbility; // for alternative cost and reduce tries + for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { + // if cast for noMana no Alternative costs are allowed + if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { + if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { + if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { + ManaCostsImpl manaCosts = new ManaCostsImpl(); + for (Cost cost : alternateSourceCostsAbility.getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost2) { + if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } + + if (manaCosts.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.getManaCostsToPay().clear(); + copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); + sourceObject.adjustCosts(copyAbility, game); + game.getContinuousEffects().costModification(copyAbility, game); + + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } + + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; + } + } + } + } + } + } + } + + // controller specific alternate spell costs + for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { + if (alternateSourceCosts instanceof Ability) { + if (alternateSourceCosts.isAvailable(ability, game)) { + if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { + ManaCostsImpl manaCosts = new ManaCostsImpl(); + for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost2) { + if (((AlternativeCost2) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } + + if (manaCosts.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } + + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.getManaCostsToPay().clear(); + copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); + sourceObject.adjustCosts(copyAbility, game); + game.getContinuousEffects().costModification(copyAbility, game); + + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } + + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; + } + } + } + } + } + } + } + } + return false; + } + + protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { + + // special mana to pay spell cost + ManaOptions manaFull = availableMana.copy(); + if (ability instanceof SpellAbility) { + for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() + .filter(a -> a instanceof AlternateManaPaymentAbility) + .map(a -> (AlternateManaPaymentAbility) a) + .collect(Collectors.toList())) { + ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay()); + manaFull.addMana(manaSpecial); + } + } + + // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) + if (ability instanceof ActivatedManaAbilityImpl) { + // mana ability + if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { + return (ActivatedManaAbilityImpl) ability; + } + } else if (ability instanceof AlternativeSourceCosts) { + // alternative cost must be replaced by real play ability + return findActivatedAbilityFromAlternativeSourceCost(object, manaFull, ability, game); + } else if (ability instanceof ActivatedAbility) { + // all other activated ability + if (canPlay((ActivatedAbility) ability, manaFull, object, game)) { + return (ActivatedAbility) ability; + } + } + + // non playable abilities like static + return null; + } + + protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { + // return play ability that can activate AlternativeSourceCosts + if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { + ActivatedAbility playAbility = null; + if (object.isLand(game)) { + playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); + } else if (object instanceof Card) { + playAbility = ((Card) object).getSpellAbility(); + } + if (playAbility == null) { + return null; + } + + // 707.4.Objects that are cast face down are turned face down before they are put onto the stack + // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc + // Even mana cost can't be checked here without lookahead + // So make it available all the time + boolean canUse; + if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) + || (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { + canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); + } else { + canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions + } + + if (canUse) { + return playAbility; + } + } + return null; + } + + private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { + if (fromZone == null || object == null) { + return; + } + + // BASIC abilities + if (object instanceof SplitCard) { + SplitCard mainCard = (SplitCard) object; + getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof ModalDoubleFacesCard) { + ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) object; + getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof AdventureCard) { + // adventure must use different card characteristics for different spells (main or adventure) + AdventureCard adventureCard = (AdventureCard) object; + getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); + } else if (object instanceof Card) { + getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); + } else if (object instanceof StackObject) { + // spells on stack are processing by Card above, other stack objects must be ignored + } else { + // other things like CommandObject + getPlayableFromObjectSingle(game, fromZone, object, object.getAbilities(), availableMana, output); + } + + // DYNAMIC ADDED abilities are adds in getAbilities(game) + } + + private void getPlayableFromObjectSingle(Game game, Zone fromZone, MageObject object, Abilities candidateAbilities, ManaOptions availableMana, List output) { + // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) + // must check all abilities, not activated only + for (Ability ability : candidateAbilities) { + if (!(ability instanceof ActivatedAbility)) { + continue; + } + boolean isPlaySpell = (ability instanceof SpellAbility); + boolean isPlayLand = (ability instanceof PlayLandAbility); + + // as original controller + // play land restrictions + if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( + GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), + ability, this.getId()), ability, game, true)) { + continue; + } + // cast spell restrictions 1 + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, this.getId()); + castEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castEvent, ability, game, true)) { + continue; + } + // cast spell restrictions 2 + GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, + ability.getId(), ability, this.getId()); + castLateEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castLateEvent, ability, game, true)) { + continue; + } + + ApprovingObject approvingObject; + if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { + // play hand from non hand zone (except battlefield - you can't play already played permanents) + approvingObject = game.getContinuousEffects().asThough(object.getId(), + AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); + } else { + // other abilities from direct zones + approvingObject = null; + } + + boolean canActivateAsHandZone = approvingObject != null + || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); + boolean possibleToPlay = canActivateAsHandZone + && ability.getZone().match(Zone.HAND) + && (isPlaySpell || isPlayLand); + + // spell/hand abilities (play from all zones) + // need permitingObject or canPlayCardsFromGraveyard + // zone's abilities (play from specific zone) + // no need in permitingObject + if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { + possibleToPlay = true; + } + + if (!possibleToPlay) { + continue; + } + + // direct mode (with original controller) + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + continue; + } + + // from non hand mode (with affected controller) + if (canActivateAsHandZone && ability.getControllerId() != this.getId()) { + UUID savedControllerId = ability.getControllerId(); + ability.setControllerId(this.getId()); + try { + playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); + if (playAbility != null && !output.contains(playAbility)) { + output.add(playAbility); + } + } finally { + ability.setControllerId(savedControllerId); + } + } + } + } + + @Override + public List getPlayable(Game game, boolean hidden) { + return getPlayable(game, hidden, Zone.ALL, true); + } + + /** + * Returns a list of all available spells and abilities the player can + * currently cast/activate with his available resources + * + * @param game + * @param hidden also from hidden objects (e.g. turned face down cards ?) + * @param fromZone of objects from which zone (ALL = from all zones) + * @param hideDuplicatedAbilities if equal abilities exist return only the + * first instance + * @return + */ + public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { + List playable = new ArrayList<>(); + if (shouldSkipGettingPlayable(game)) { + return playable; + } + + boolean previousState = game.inCheckPlayableState(); + game.setCheckPlayableState(true); + try { + ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) + boolean fromAll = fromZone.equals(Zone.ALL); + if (hidden && (fromAll || fromZone == Zone.HAND)) { + for (Card card : hand.getCards(game)) { + for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) + if (ability.getZone().match(Zone.HAND)) { + boolean isPlaySpell = (ability instanceof SpellAbility); + boolean isPlayLand = (ability instanceof PlayLandAbility); + + // play land restrictions + if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( + GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), + ability, this.getId()), ability, game, true)) { + continue; + } + // cast spell restrictions 1 + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, + ability.getId(), ability, this.getId()); + castEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castEvent, ability, game, true)) { + continue; + } + // cast spell restrictions 2 + GameEvent castLateEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, + ability.getId(), ability, this.getId()); + castLateEvent.setZone(fromZone); + if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( + castLateEvent, ability, game, true)) { + continue; + } + + ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); + if (playAbility != null && !playable.contains(playAbility)) { + playable.add(playAbility); + } + } + } + } + } + + if (fromAll || fromZone == Zone.GRAVEYARD) { + for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + for (Card card : player.getGraveyard().getCards(game)) { + getPlayableFromObjectAll(game, Zone.GRAVEYARD, card, availableMana, playable); + } + } + } + + if (fromAll || fromZone == Zone.EXILED) { + for (ExileZone exile : game.getExile().getExileZones()) { + for (Card card : exile.getCards(game)) { + getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); + } + } + } + + // check to play revealed cards + if (fromAll) { + for (Cards revealedCards : game.getState().getRevealed().values()) { + for (Card card : revealedCards.getCards(game)) { + // revealed cards can be from any zones + getPlayableFromObjectAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); + } + } + } + + // outside cards + if (fromAll || fromZone == Zone.OUTSIDE) { + // companion cards + for (Cards companionCards : game.getState().getCompanion().values()) { + for (Card card : companionCards.getCards(game)) { + getPlayableFromObjectAll(game, Zone.OUTSIDE, card, availableMana, playable); + } + } + + // sideboard cards (example: Wish) + for (UUID sideboardCardId : this.getSideboard()) { + Card sideboardCard = game.getCard(sideboardCardId); + if (sideboardCard != null) { + getPlayableFromObjectAll(game, Zone.OUTSIDE, sideboardCard, availableMana, playable); + } + } + } + + // check if it's possible to play the top card of a library + if (fromAll || fromZone == Zone.LIBRARY) { + for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerInRangeId); + if (player != null && player.getLibrary().hasCards()) { + Card card = player.getLibrary().getFromTop(game); + if (card != null) { + getPlayableFromObjectAll(game, Zone.LIBRARY, card, availableMana, playable); + } + } + } + } + + // check the hand zone (Sen Triplets) + // TODO: remove direct hand check (reveal fix in Sen Triplets)? + // human games: cards from opponent's hand must be revealed before play + // AI games: computer can see and play cards from opponent's hand without reveal + if (fromAll || fromZone == Zone.HAND) { + for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { + Player player = game.getPlayer(playerInRangeId); + if (player != null && !player.getHand().isEmpty()) { + for (Card card : player.getHand().getCards(game)) { + if (card != null) { + getPlayableFromObjectAll(game, Zone.HAND, card, availableMana, playable); + } + } + } + } + } + + // eliminate duplicate activated abilities (uses for AI plays) + Map activatedUnique = new HashMap<>(); + List activatedAll = new ArrayList<>(); + + // activated abilities from battlefield objects + if (fromAll || fromZone == Zone.BATTLEFIELD) { + for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { + boolean canUseActivated = permanent.canUseActivatedAbilities(game); + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.BATTLEFIELD, permanent, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + if (ability instanceof SpecialAction || canUseActivated) { + activatedUnique.putIfAbsent(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + } + + // activated abilities from stack objects + if (fromAll || fromZone == Zone.STACK) { + for (StackObject stackObject : game.getState().getStack()) { + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + activatedUnique.put(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + + // activated abilities from objects in the command zone (emblems or commanders) + if (fromAll || fromZone == Zone.COMMAND) { + for (CommandObject commandObject : game.getState().getCommand()) { + List currentPlayable = new ArrayList<>(); + getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); + for (ActivatedAbility ability : currentPlayable) { + activatedUnique.put(ability.toString(), ability); + activatedAll.add(ability); + } + } + } + + if (hideDuplicatedAbilities) { + playable.addAll(activatedUnique.values()); + } else { + playable.addAll(activatedAll); + } + } finally { + game.setCheckPlayableState(previousState); + } + + return playable; + } + + /** + * Creates a list of card ids that are currently playable.
+ * Used to mark the playable cards in GameView Also contains number of + * playable abilities for that object (it's just info, server decides to + * show choose dialog or not) + * + * @param game + * @return A Set of cardIds that are playable and amount of playable + * abilities + */ + @Override + public PlayableObjectsList getPlayableObjects(Game game, Zone zone) { + // collect abilities per object + List playableAbilities = getPlayable(game, true, zone, false); // do not hide duplicated abilities/cards + Map> playableObjects = new HashMap<>(); + for (ActivatedAbility ability : playableAbilities) { + if (ability.getSourceId() != null) { + + // normal card + putToPlayableObjects(playableObjects, ability.getSourceId(), ability); + + // main card - must be marked playable in GUI + Card card = game.getCard(ability.getSourceId()); + if (card != null && card.getMainCard().getId() != card.getId()) { + putToPlayableObjects(playableObjects, card.getMainCard().getId(), ability); + } + + // spell on stack - can have activated abilities, + // so mark it as playable too (users must able to clicks on stack objects) + // example: Lightning Storm + Spell spell = game.getSpell(ability.getSourceId()); + if (spell != null) { + putToPlayableObjects(playableObjects, spell.getId(), ability); + } + } + } + + // collect stats + PlayableObjectsList playableObjectsList = new PlayableObjectsList(playableObjects); + return playableObjectsList; + } + + private void putToPlayableObjects(Map> playableObjects, UUID objectId, ActivatedAbility ability) { + if (!playableObjects.containsKey(objectId)) { + playableObjects.put(objectId, new ArrayList<>()); + } + playableObjects.get(objectId).add(ability); + } + + /** + * Skip "silent" phase step when players are not allowed to cast anything. + * E.g. players can't play or cast anything during declaring attackers. + * + * @param game + * @return + */ + private boolean shouldSkipGettingPlayable(Game game) { + if (game.getStep() == null) { // happens at the start of the game + return true; + } + for (Entry phaseStep : silentPhaseSteps.entrySet()) { + if (game.getPhase() != null + && game.getPhase().getStep() != null + && phaseStep.getKey() == game.getPhase().getStep().getType()) { + if (phaseStep.getValue() == null + || phaseStep.getValue() == game.getPhase().getStep().getStepPart()) { + return true; + } + } + } + return false; + } + + /** + * Only used for AIs + * + * @param ability + * @param game + * @return + */ + @Override + public List getPlayableOptions(Ability ability, Game game) { + List options = new ArrayList<>(); + if (ability.isModal()) { + addModeOptions(options, ability, game); + } else if (!ability.getTargets().getUnchosen().isEmpty()) { + // TODO: Handle other variable costs than mana costs + if (!ability.getManaCosts().getVariableCosts().isEmpty()) { + addVariableXOptions(options, ability, 0, game); + } else { + addTargetOptions(options, ability, 0, game); + } + } else if (!ability.getCosts().getTargets().getUnchosen().isEmpty()) { + addCostTargetOptions(options, ability, 0, game); + } + + return options; + } + + private void addModeOptions(List options, Ability option, Game game) { + // TODO: Support modal spells with more than one selectable mode + for (Mode mode : option.getModes().values()) { + Ability newOption = option.copy(); + newOption.getModes().clearSelectedModes(); + newOption.getModes().addSelectedMode(mode.getId()); + newOption.getModes().setActiveMode(mode); + if (!newOption.getTargets().getUnchosen().isEmpty()) { + if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { + addVariableXOptions(options, newOption, 0, game); + } else { + addTargetOptions(options, newOption, 0, game); + } + } else if (!newOption.getCosts().getTargets().getUnchosen().isEmpty()) { + addCostTargetOptions(options, newOption, 0, game); + } else { + options.add(newOption); + } + } + } + + protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { + addTargetOptions(options, option, targetNum, game); + } + + protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { + for (Target target : option.getTargets().getUnchosen().get(targetNum).getTargetOptions(option, game)) { + Ability newOption = option.copy(); + if (target instanceof TargetAmount) { + for (UUID targetId : target.getTargets()) { + int amount = target.getTargetAmount(targetId); + newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); + } + } else { + for (UUID targetId : target.getTargets()) { + newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); + } + } + if (targetNum < option.getTargets().size() - 2) { + addTargetOptions(options, newOption, targetNum + 1, game); + } else if (!option.getCosts().getTargets().isEmpty()) { + addCostTargetOptions(options, newOption, 0, game); + } else { + options.add(newOption); + } + } + } + + private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { + for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(option.getSourceId(), playerId, game)) { + Ability newOption = option.copy(); + newOption.getCosts().getTargets().get(targetNum).addTarget(targetId, option, game, true); + if (targetNum < option.getCosts().getTargets().size() - 1) { + addCostTargetOptions(options, newOption, targetNum + 1, game); + } else { + options.add(newOption); + } + } + } + + @Override + public boolean isTestsMode() { + return isTestMode; + } + + @Override + public void setTestMode(boolean value) { + this.isTestMode = value; + } + + @Override + public boolean isTopCardRevealed() { + return topCardRevealed; + } + + @Override + public void setTopCardRevealed(boolean topCardRevealed) { + this.topCardRevealed = topCardRevealed; + } + + @Override + public UserData getUserData() { + return this.userData; + } + + public UserData getControllingPlayersUserData(Game game) { + if (!isGameUnderControl()) { + Player player = game.getPlayer(getTurnControlledBy()); + if (player.isHuman()) { + return player.getUserData(); + } + } + return this.userData; + } + + @Override + public void setUserData(UserData userData) { + this.userData = userData; + getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); + getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); + } + + @Override + public void addAction(String action + ) { + // do nothing + } + + @Override + public int getActionCount() { + return 0; + } + + @Override + public void setAllowBadMoves(boolean allowBadMoves) { + // do nothing + } + + @Override + public boolean canPayLifeCost(Ability ability) { + if (!canPayLifeCost + && (AbilityType.ACTIVATED.equals(ability.getAbilityType()) + || AbilityType.SPELL.equals(ability.getAbilityType()))) { + return false; + } + return isLifeTotalCanChange(); + } + + @Override + public boolean getCanPayLifeCost() { + return canPayLifeCost; + } + + @Override + public void setCanPayLifeCost(boolean canPayLifeCost) { + this.canPayLifeCost = canPayLifeCost; + } + + @Override + public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { + return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, source.getSourceId(), controllerId, game); + } + + @Override + public void setCanPaySacrificeCostFilter(FilterPermanent filter + ) { + this.sacrificeCostFilter = filter; + } + + @Override + public FilterPermanent getSacrificeCostFilter() { + return sacrificeCostFilter; + } + + @Override + public boolean canLoseByZeroOrLessLife() { + return loseByZeroOrLessLife; + } + + @Override + public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife) { + this.loseByZeroOrLessLife = loseByZeroOrLessLife; + } + + @Override + public boolean canPlayCardsFromGraveyard() { + return canPlayCardsFromGraveyard; + } + + @Override + public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard) { + this.canPlayCardsFromGraveyard = playCardsFromGraveyard; + } + + @Override + public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) { + this.drawsOnOpponentsTurn = drawsOnOpponentsTurn; + } + + @Override + public boolean isDrawsOnOpponentsTurn() { + return drawsOnOpponentsTurn; + } + + @Override + public boolean autoLoseGame() { + return false; + } + + @Override + public void becomesActivePlayer() { + this.passedAllTurns = false; + this.passedUntilEndStepBeforeMyTurn = false; + this.turns++; + } + + @Override + public int getTurns() { + return turns; + } + + @Override + public int getStoredBookmark() { + return storedBookmark; + } + + @Override + public void setStoredBookmark(int storedBookmark) { + this.storedBookmark = storedBookmark; + } + + @Override + public synchronized void resetStoredBookmark(Game game) { + if (this.storedBookmark != -1) { + game.removeBookmark(this.storedBookmark); + } + setStoredBookmark(-1); + } + + @Override + public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { + if (null != game.getContinuousEffects().asThough(card.getId(), + AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { + // two modes: look at the card or do not look and activate other abilities + String lookMessage = "Look at " + card.getIdName(); + String lookYes = "Yes, look at the card"; + String lookNo = "No, play/activate the card/ability"; + if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { + Cards cards = new CardsImpl(card); + this.lookAtCards(getName() + " - " + card.getIdName() + " - " + + CardUtil.sdf.format(System.currentTimeMillis()), cards, game); + return true; + } + } + return false; + } + + @Override + public void setPriorityTimeLeft(int timeLeft + ) { + priorityTimeLeft = timeLeft; + } + + @Override + public int getPriorityTimeLeft() { + return priorityTimeLeft; + } + + @Override + public boolean hasQuit() { + return quit; + } + + @Override + public boolean hasTimerTimeout() { + return timerTimeout; + } + + @Override + public boolean hasIdleTimeout() { + return idleTimeout; + } + + @Override + public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { + this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; + } + + @Override + public boolean hasReachedNextTurnAfterLeaving() { + return reachedNextTurnAfterLeaving; + } + + @Override + public boolean canJoinTable(Table table + ) { + return !table.userIsBanned(name); + } + + @Override + public void addCommanderId(UUID commanderId + ) { + this.commandersIds.add(commanderId); + } + + @Override + public Set getCommandersIds() { + return this.commandersIds; + } + + @Override + public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { + return moveCards(card, toZone, source, game, false, false, false, null); + } + + @Override + public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { + Set cardList = new HashSet<>(); + if (card != null) { + cardList.add(card); + } + return moveCards(cardList, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); + } + + @Override + public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game) { + return moveCards(cards.getCards(game), toZone, source, game); + } + + @Override + public boolean moveCards(Set cards, Zone toZone, + Ability source, Game game + ) { + return moveCards(cards, toZone, source, game, false, false, false, null); + } + + @Override + public boolean moveCards(Set cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects) { + if (cards.isEmpty()) { + return true; + } + Set successfulMovedCards = new LinkedHashSet<>(); + Zone fromZone = null; + switch (toZone) { + case GRAVEYARD: + fromZone = game.getState().getZone(cards.iterator().next().getId()); + successfulMovedCards = moveCardsToGraveyardWithInfo(cards, source, game, fromZone); + return !successfulMovedCards.isEmpty(); + case BATTLEFIELD: // new logic that does not yet add the permanents to battlefield while replacement effects are handled + List infoList = new ArrayList<>(); + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source, + byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); + infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source)); + } + infoList = ZonesHandler.moveCards(infoList, game, source); + for (ZoneChangeInfo info : infoList) { + Permanent permanent = game.getPermanent(info.event.getTargetId()); + if (permanent != null) { + successfulMovedCards.add(permanent); + if (!game.isSimulation()) { + Player eventPlayer = game.getPlayer(info.event.getPlayerId()); + if (eventPlayer != null && fromZone != null) { + game.informPlayers(eventPlayer.getLogName() + " puts " + + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " + + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield" + + CardUtil.getSourceLogName(game, source, permanent.getId())); + } + } + } + } + // TODO: must be replaced by game.getState().processAction(game), see isInUseableZoneDiesTrigger comments + // about short living LKI problem + //game.getState().processAction(game); + game.applyEffects(); + break; + case HAND: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + boolean hideCard = fromZone == Zone.LIBRARY + || (card.isFaceDown(game) + && fromZone != Zone.STACK + && fromZone != Zone.BATTLEFIELD); + if (moveCardToHandWithInfo(card, source, game, !hideCard)) { + successfulMovedCards.add(card); + } + } + break; + case EXILED: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + boolean withName = (fromZone == Zone.BATTLEFIELD + || fromZone == Zone.STACK) + || !card.isFaceDown(game); + if (moveCardToExileWithInfo(card, null, "", source, game, fromZone, withName)) { + successfulMovedCards.add(card); + } + } + break; + case LIBRARY: + for (Card card : cards) { + if (card instanceof Spell) { + fromZone = game.getState().getZone(((Spell) card).getSourceId()); + } else { + fromZone = game.getState().getZone(card.getId()); + } + boolean hideCard = fromZone == Zone.HAND || fromZone == Zone.LIBRARY; + if (moveCardToLibraryWithInfo(card, source, game, fromZone, true, !hideCard)) { + successfulMovedCards.add(card); + } + } + break; + case COMMAND: + for (Card card : cards) { + fromZone = game.getState().getZone(card.getId()); + if (moveCardToCommandWithInfo(card, source, game, fromZone)) { + successfulMovedCards.add(card); + } + } + break; + case OUTSIDE: + for (Card card : cards) { + if (card instanceof Permanent) { + game.getBattlefield().removePermanent(card.getId()); + ZoneChangeEvent event = new ZoneChangeEvent((Permanent) card, source, + byOwner ? card.getOwnerId() : getId(), Zone.BATTLEFIELD, Zone.OUTSIDE, appliedEffects); + game.fireEvent(event); + } + } + break; + default: + throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet"); + } + return !successfulMovedCards.isEmpty(); + } + + @Override + public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { + Set cards = new HashSet<>(); + if (card != null) { + cards.add(card); + } + return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); + } + + @Override + public boolean moveCardsToExile(Set cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { + if (cards.isEmpty()) { + return true; + } + boolean result = false; + for (Card card : cards) { + Zone fromZone = game.getState().getZone(card.getId()); + result |= moveCardToExileWithInfo(card, exileId, exileZoneName, source, game, fromZone, withName); + } + return result; + } + + @Override + public boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName) { + boolean result = false; + Zone fromZone = game.getState().getZone(card.getId()); + if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { + card = game.getPermanent(card.getId()); + } + if (card.moveToZone(Zone.HAND, source, game, false)) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + if (!game.isSimulation()) { + game.informPlayers(getLogName() + " puts " + + (withName ? card.getLogName() : (card.isFaceDown(game) ? "a face down card" : "a card")) + + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' + + (card.isOwnedBy(this.getId()) ? "into their hand" : "into its owner's hand" + + CardUtil.getSourceLogName(game, source, card.getId())) + ); + } + result = true; + } + return result; + } + + @Override + public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, Game game, Zone fromZone) { + Set movedCards = new LinkedHashSet<>(); + while (!allCards.isEmpty()) { + // identify cards from one owner + Cards cards = new CardsImpl(); + UUID ownerId = null; + for (Iterator it = allCards.iterator(); it.hasNext();) { + Card card = it.next(); + if (cards.isEmpty()) { + ownerId = card.getOwnerId(); + } + if (card.isOwnedBy(ownerId)) { + it.remove(); + cards.add(card); + } + } + // move cards to graveyard in order the owner decides + if (!cards.isEmpty()) { + Player choosingPlayer = this; + if (!Objects.equals(ownerId, this.getId())) { + choosingPlayer = game.getPlayer(ownerId); + } + if (choosingPlayer == null) { + continue; + } + boolean chooseOrder = false; + if (userData.askMoveToGraveOrder()) { + if (cards.size() > 1) { + chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral, + "Choose the order in which the cards go to the graveyard?", source, game); + } + } + if (chooseOrder) { + TargetCard target = new TargetCard(fromZone, + new FilterCard("card to put on the top of your graveyard (last one chosen will be topmost)")); + target.setRequired(true); + while (choosingPlayer.canRespond() && cards.size() > 1) { + choosingPlayer.chooseTarget(Outcome.Neutral, cards, target, source, game); + UUID targetObjectId = target.getFirstTarget(); + Card card = cards.get(targetObjectId, game); + cards.remove(targetObjectId); + if (card != null) { + fromZone = game.getState().getZone(card.getId()); + if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + target.clearChosen(); + } + if (cards.size() == 1) { + Card card = cards.getCards(game).iterator().next(); + if (card != null && choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + } else { + for (Card card : cards.getCards(game)) { + if (choosingPlayer.moveCardToGraveyardWithInfo(card, source, game, fromZone)) { + movedCards.add(card); + } + } + } + } + } + return movedCards; + } + + @Override + public boolean moveCardToGraveyardWithInfo(Card card, Ability source, Game game, Zone fromZone) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.GRAVEYARD, source, game, false)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(card.getLogName()).append(' ').append(card.isCopy() ? "(Copy) " : "") + .append(fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : ""); + if (card.isOwnedBy(getId())) { + sb.append("into their graveyard"); + } else { + sb.append("it into its owner's graveyard"); + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToLibraryWithInfo(Card card, Ability source, Game game, Zone fromZone, boolean toTop, boolean withName) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.LIBRARY, source, game, toTop)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(withName ? card.getLogName() : "a card").append(' '); + if (fromZone != null) { + sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); + } + sb.append("to the ").append(toTop ? "top" : "bottom"); + if (card.isOwnedBy(getId())) { + sb.append(" of their library"); + } else { + Player player = game.getPlayer(card.getOwnerId()); + if (player != null) { + sb.append(" of ").append(player.getLogName()).append("'s library"); + } + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToCommandWithInfo(Card card, Ability source, Game game, Zone fromZone) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToZone(Zone.COMMAND, source, game, true)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { + card = game.getCard(card.getId()); + } + StringBuilder sb = new StringBuilder(this.getLogName()) + .append(" puts ").append(card.getLogName()).append(' '); + if (fromZone != null) { + sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); + } + if (card.isOwnedBy(getId())) { + sb.append(" to their command zone"); + } else { + Player player = game.getPlayer(card.getOwnerId()); + if (player != null) { + sb.append(" to ").append(player.getLogName()).append("'s command zone"); + } + } + sb.append(CardUtil.getSourceLogName(game, source, card.getId())); + game.informPlayers(sb.toString()); + } + result = true; + } + return result; + } + + @Override + public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, Ability source, Game game, Zone fromZone, boolean withName) { + if (card == null) { + return false; + } + boolean result = false; + if (card.moveToExile(exileId, exileName, source, game)) { + if (!game.isSimulation()) { + if (card instanceof PermanentCard) { + // in case it's face down or name was changed by copying from other permanent + Card basicCard = game.getCard(card.getId()); + if (basicCard != null) { + card = basicCard; + } + } else if (card instanceof Spell) { + final Spell spell = (Spell) card; + if (spell.isCopy()) { + // copied spell, only remove from stack + game.getStack().remove(spell, game); + } + } + if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced + game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); + } + + } + result = true; + } + return result; + } + + @Override + public Cards millCards(int toMill, Ability source, Game game) { + GameEvent event = GameEvent.getEvent(GameEvent.EventType.MILL_CARDS, getId(), source, getId(), toMill); + if (game.replaceEvent(event)) { + return new CardsImpl(); + } + Cards cards = new CardsImpl(this.getLibrary().getTopCards(game, event.getAmount())); + this.moveCards(cards, Zone.GRAVEYARD, source, game); + for (Card card : cards.getCards(game)) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.MILLED_CARD, card.getId(), source, getId())); + } + return cards; + } + + @Override + public boolean hasOpponent(UUID playerToCheckId, Game game) { + return !this.getId().equals(playerToCheckId) + && game.isOpponent(this, playerToCheckId) + && getInRange().contains(playerToCheckId); + } + + @Override + public void cleanUpOnMatchEnd() { + + } + + @Override + public boolean getPassedAllTurns() { + return passedAllTurns; + } + + @Override + public boolean getPassedUntilNextMain() { + return passedUntilNextMain; + } + + @Override + public boolean getPassedUntilEndOfTurn() { + return passedUntilEndOfTurn; + } + + @Override + public boolean getPassedTurn() { + return passedTurn; + } + + @Override + public boolean getPassedUntilStackResolved() { + return passedUntilStackResolved; + } + + @Override + public boolean getPassedUntilEndStepBeforeMyTurn() { + return passedUntilEndStepBeforeMyTurn; + } + + @Override + public AbilityType getJustActivatedType() { + return justActivatedType; + } + + @Override + public void setJustActivatedType(AbilityType justActivatedType + ) { + this.justActivatedType = justActivatedType; + } + + @Override + public void revokePermissionToSeeHandCards() { + usersAllowedToSeeHandCards.clear(); + } + + @Override + public void addPermissionToShowHandCards(UUID watcherUserId + ) { + usersAllowedToSeeHandCards.add(watcherUserId); + } + + @Override + public boolean isPlayerAllowedToRequestHand(UUID gameId, UUID requesterPlayerId) { + return userData.isAllowRequestHandToPlayer(gameId, requesterPlayerId); + } + + @Override + public void addPlayerToRequestedHandList(UUID gameId, UUID requesterPlayerId) { + userData.addPlayerToRequestedHandList(gameId, requesterPlayerId); + } + + @Override + public boolean hasUserPermissionToSeeHand(UUID userId + ) { + return usersAllowedToSeeHandCards.contains(userId); + } + + @Override + public Set getUsersAllowedToSeeHandCards() { + return usersAllowedToSeeHandCards; + } + + @Override + public void setMatchPlayer(MatchPlayer matchPlayer + ) { + this.matchPlayer = matchPlayer; + } + + @Override + public MatchPlayer getMatchPlayer() { + return matchPlayer; + } + + @Override + public void abortReset() { + abort = false; + } + + @Override + public void signalPlayerConcede() { + + } + + @Override + public boolean scry(int value, Ability source, Game game) { + GameEvent event = new GameEvent(GameEvent.EventType.SCRY, getId(), source, getId(), value, true); + if (game.replaceEvent(event)) { + return false; + } + game.informPlayers(getLogName() + " scries " + event.getAmount() + CardUtil.getSourceLogName(game, source)); + Cards cards = new CardsImpl(); + cards.addAll(getLibrary().getTopCards(game, event.getAmount())); + if (!cards.isEmpty()) { + TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, + new FilterCard("card" + (cards.size() == 1 ? "" : "s") + + " to PUT on the BOTTOM of your library (Scry)")); + chooseTarget(Outcome.Benefit, cards, target, source, game); + putCardsOnBottomOfLibrary(new CardsImpl(target.getTargets()), game, source, true); + cards.removeAll(target.getTargets()); + putCardsOnTopOfLibrary(cards, game, source, true); + } + game.fireEvent(new GameEvent(GameEvent.EventType.SCRIED, getId(), source, getId(), event.getAmount(), true)); + return true; + } + + @Override + public boolean surveil(int value, Ability source, Game game) { + GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source, getId(), value, true); + if (game.replaceEvent(event)) { + return false; + } + game.informPlayers(getLogName() + " surveils " + event.getAmount() + CardUtil.getSourceLogName(game, source)); + Cards cards = new CardsImpl(); + cards.addAll(getLibrary().getTopCards(game, event.getAmount())); + if (!cards.isEmpty()) { + TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, + new FilterCard("cards to PUT into your GRAVEYARD (Surveil)")); + chooseTarget(Outcome.Benefit, cards, target, source, game); + moveCards(new CardsImpl(target.getTargets()), Zone.GRAVEYARD, source, game); + cards.removeAll(target.getTargets()); + putCardsOnTopOfLibrary(cards, game, source, true); + } + game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source, getId(), event.getAmount(), true)); + return true; + } + + @Override + public boolean addTargets(Ability ability, Game game + ) { + // only used for TestPlayer to preSet Targets + return true; + } + + @Override + public String getHistory() { + return "no available"; + } + + @Override + public boolean hasDesignation(DesignationType designationName) { + for (Designation designation : designations) { + if (designation.getDesignationType().equals(designationName)) { + return true; + } + } + return false; + } + + @Override + public void addDesignation(Designation designation) { + if (!designation.isUnique() || !this.hasDesignation(designation.getDesignationType())) { + designations.add(designation); + } + } + + @Override + public List getDesignations() { + return designations; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Player obj = (Player) o; + if (this.getId() == null || obj.getId() == null) { + return false; + } + + return this.getId().equals(obj.getId()); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 89 * hash + Objects.hashCode(this.playerId); + return hash; + } + + @Override + public void addPhyrexianToColors(FilterMana colors) { + if (phyrexianColors == null) { + phyrexianColors = colors.copy(); + } else { + if (colors.isWhite()) { + this.phyrexianColors.setWhite(true); + } + if (colors.isBlue()) { + this.phyrexianColors.setBlue(true); + } + if (colors.isBlack()) { + this.phyrexianColors.setBlack(true); + } + if (colors.isRed()) { + this.phyrexianColors.setRed(true); + } + if (colors.isGreen()) { + this.phyrexianColors.setGreen(true); + } + } + } + + @Override + public FilterMana getPhyrexianColors() { + return this.phyrexianColors; + } + + @Override + public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { + return card.getSpellAbility(); + } + + @Override + public String toString() { + return getName() + " (" + super.getClass().getSimpleName() + ")"; + } +}