unfinished = Arrays.asList("Aether Syphon", "Amonkhet Raceway", "Avishkar Raceway", "Burnout Bashtronaut", "Embalmed Ascendant", "Endrider Catalyzer", "Endrider Spikespitter", "Far Fortune, End Boss", "Gas Guzzler", "Gastal Raider", "Gastal Thrillseeker", "Glitch Ghost Surveyor", "Goblin Surveyor", "Hazoret, Godseeker", "Hour of Victory", "Howlsquad Heavy", "Kickoff Celebrations", "Leonin Surveyor", "Lightwheel Enhancements", "Loxodon Surveyor", "Mendicant Core, Guidelight", "Momentum Breaker", "Muraganda Raceway", "Mutant Surveyor", "Nesting Bot", "Outpace Oblivion", "Perilous Snare", "Point the Way", "Pride of the Road", "Racers' Scoreboard", "Risen Necroregent", "Samut, the Driving Force", "Slick Imitator", "Starting Column", "Streaking Oilgorger", "Swiftwing Assailant", "The Speed Demon", "Vnwxt, Verbose Host", "Walking Sarcophagus", "Zahur, Glory's Past");
private static final Aetherdrift instance = new Aetherdrift();
public static Aetherdrift getInstance() {
@@ -22,10 +18,11 @@ public final class Aetherdrift extends ExpansionSet {
private Aetherdrift() {
super("Aetherdrift", "DFT", ExpansionSet.buildDate(2025, 2, 14), SetType.EXPANSION);
this.blockName = "Aetherdrift"; // for sorting in GUI
- this.hasBasicLands = true;
- this.hasBoosters = false; // temporary
+
+ this.enablePlayBooster(Integer.MAX_VALUE);
cards.add(new SetCardInfo("Aatchik, Emerald Radian", 187, Rarity.RARE, mage.cards.a.AatchikEmeraldRadian.class));
+ cards.add(new SetCardInfo("Adrenaline Jockey", 112, Rarity.UNCOMMON, mage.cards.a.AdrenalineJockey.class));
cards.add(new SetCardInfo("Aether Syphon", 38, Rarity.UNCOMMON, mage.cards.a.AetherSyphon.class));
cards.add(new SetCardInfo("Aetherjacket", 230, Rarity.COMMON, mage.cards.a.Aetherjacket.class));
cards.add(new SetCardInfo("Afterburner Expert", 150, Rarity.RARE, mage.cards.a.AfterburnerExpert.class));
@@ -34,6 +31,7 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Alacrian Jaguar", 152, Rarity.COMMON, mage.cards.a.AlacrianJaguar.class));
cards.add(new SetCardInfo("Amonkhet Raceway", 248, Rarity.UNCOMMON, mage.cards.a.AmonkhetRaceway.class));
cards.add(new SetCardInfo("Apocalypse Runner", 188, Rarity.UNCOMMON, mage.cards.a.ApocalypseRunner.class));
+ cards.add(new SetCardInfo("Autarch Mammoth", 153, Rarity.UNCOMMON, mage.cards.a.AutarchMammoth.class));
cards.add(new SetCardInfo("Avishkar Raceway", 249, Rarity.COMMON, mage.cards.a.AvishkarRaceway.class));
cards.add(new SetCardInfo("Back on Track", 76, Rarity.UNCOMMON, mage.cards.b.BackOnTrack.class));
cards.add(new SetCardInfo("Basri, Tomorrow's Champion", 3, Rarity.RARE, mage.cards.b.BasriTomorrowsChampion.class));
@@ -43,6 +41,10 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Bloodfell Caves", 251, Rarity.COMMON, mage.cards.b.BloodfellCaves.class));
cards.add(new SetCardInfo("Bloodghast", 77, Rarity.RARE, mage.cards.b.Bloodghast.class));
cards.add(new SetCardInfo("Blossoming Sands", 252, Rarity.COMMON, mage.cards.b.BlossomingSands.class));
+ cards.add(new SetCardInfo("Boommobile", 113, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Boommobile", 310, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Boommobile", 454, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Boommobile", 526, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Boom Scholar", 189, Rarity.UNCOMMON, mage.cards.b.BoomScholar.class));
cards.add(new SetCardInfo("Boosted Sloop", 190, Rarity.UNCOMMON, mage.cards.b.BoostedSloop.class));
cards.add(new SetCardInfo("Bounce Off", 39, Rarity.COMMON, mage.cards.b.BounceOff.class));
@@ -52,17 +54,24 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Broadcast Rambler", 6, Rarity.COMMON, mage.cards.b.BroadcastRambler.class));
cards.add(new SetCardInfo("Broadside Barrage", 192, Rarity.UNCOMMON, mage.cards.b.BroadsideBarrage.class));
cards.add(new SetCardInfo("Broken Wings", 156, Rarity.COMMON, mage.cards.b.BrokenWings.class));
+ cards.add(new SetCardInfo("Broodheart Engine", 193, Rarity.UNCOMMON, mage.cards.b.BroodheartEngine.class));
cards.add(new SetCardInfo("Bulwark Ox", 7, Rarity.RARE, mage.cards.b.BulwarkOx.class));
+ cards.add(new SetCardInfo("Burner Rocket", 114, Rarity.COMMON, mage.cards.b.BurnerRocket.class));
cards.add(new SetCardInfo("Burnout Bashtronaut", 115, Rarity.RARE, mage.cards.b.BurnoutBashtronaut.class));
cards.add(new SetCardInfo("Caelorna, Coral Tyrant", 40, Rarity.UNCOMMON, mage.cards.c.CaelornaCoralTyrant.class));
cards.add(new SetCardInfo("Camera Launcher", 232, Rarity.COMMON, mage.cards.c.CameraLauncher.class));
cards.add(new SetCardInfo("Caradora, Heart of Alacria", 195, Rarity.RARE, mage.cards.c.CaradoraHeartOfAlacria.class));
+ cards.add(new SetCardInfo("Carrion Cruiser", 78, Rarity.UNCOMMON, mage.cards.c.CarrionCruiser.class));
+ cards.add(new SetCardInfo("Chandra, Spark Hunter", 116, Rarity.MYTHIC, mage.cards.c.ChandraSparkHunter.class));
+ cards.add(new SetCardInfo("Chitin Gravestalker", 79, Rarity.COMMON, mage.cards.c.ChitinGravestalker.class));
cards.add(new SetCardInfo("Clamorous Ironclad", 117, Rarity.COMMON, mage.cards.c.ClamorousIronclad.class));
cards.add(new SetCardInfo("Cloudspire Captain", 9, Rarity.UNCOMMON, mage.cards.c.CloudspireCaptain.class));
+ cards.add(new SetCardInfo("Cloudspire Coordinator", 196, Rarity.UNCOMMON, mage.cards.c.CloudspireCoordinator.class));
cards.add(new SetCardInfo("Collision Course", 10, Rarity.COMMON, mage.cards.c.CollisionCourse.class));
cards.add(new SetCardInfo("Count on Luck", 118, Rarity.RARE, mage.cards.c.CountOnLuck.class));
cards.add(new SetCardInfo("Country Roads", 253, Rarity.UNCOMMON, mage.cards.c.CountryRoads.class));
cards.add(new SetCardInfo("Crash and Burn", 119, Rarity.COMMON, mage.cards.c.CrashAndBurn.class));
+ cards.add(new SetCardInfo("Cryptcaller Chariot", 80, Rarity.RARE, mage.cards.c.CryptcallerChariot.class));
cards.add(new SetCardInfo("Daretti, Rocketeer Engineer", 120, Rarity.RARE, mage.cards.d.DarettiRocketeerEngineer.class));
cards.add(new SetCardInfo("Daring Mechanic", 11, Rarity.COMMON, mage.cards.d.DaringMechanic.class));
cards.add(new SetCardInfo("Deathless Pilot", 82, Rarity.COMMON, mage.cards.d.DeathlessPilot.class));
@@ -81,13 +90,19 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Endrider Catalyzer", 124, Rarity.COMMON, mage.cards.e.EndriderCatalyzer.class));
cards.add(new SetCardInfo("Endrider Spikespitter", 125, Rarity.UNCOMMON, mage.cards.e.EndriderSpikespitter.class));
cards.add(new SetCardInfo("Engine Rat", 84, Rarity.COMMON, mage.cards.e.EngineRat.class));
+ cards.add(new SetCardInfo("Fang Guardian", 162, Rarity.UNCOMMON, mage.cards.f.FangGuardian.class));
+ cards.add(new SetCardInfo("Far Fortune, End Boss", 203, Rarity.RARE, mage.cards.f.FarFortuneEndBoss.class));
cards.add(new SetCardInfo("Fearless Swashbuckler", 204, Rarity.RARE, mage.cards.f.FearlessSwashbuckler.class));
+ cards.add(new SetCardInfo("Flood the Engine", 42, Rarity.COMMON, mage.cards.f.FloodTheEngine.class));
cards.add(new SetCardInfo("Forest", 289, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Foul Roads", 255, Rarity.UNCOMMON, mage.cards.f.FoulRoads.class));
cards.add(new SetCardInfo("Fuel the Flames", 126, Rarity.UNCOMMON, mage.cards.f.FuelTheFlames.class));
cards.add(new SetCardInfo("Gallant Strike", 13, Rarity.UNCOMMON, mage.cards.g.GallantStrike.class));
cards.add(new SetCardInfo("Gas Guzzler", 85, Rarity.RARE, mage.cards.g.GasGuzzler.class));
+ cards.add(new SetCardInfo("Gastal Blockbuster", 128, Rarity.COMMON, mage.cards.g.GastalBlockbuster.class));
+ cards.add(new SetCardInfo("Gastal Raider", 86, Rarity.UNCOMMON, mage.cards.g.GastalRaider.class));
cards.add(new SetCardInfo("Gastal Thrillroller", 129, Rarity.RARE, mage.cards.g.GastalThrillroller.class));
+ cards.add(new SetCardInfo("Gastal Thrillseeker", 205, Rarity.UNCOMMON, mage.cards.g.GastalThrillseeker.class));
cards.add(new SetCardInfo("Gearseeker Serpent", 43, Rarity.COMMON, mage.cards.g.GearseekerSerpent.class));
cards.add(new SetCardInfo("Gilded Ghoda", 130, Rarity.COMMON, mage.cards.g.GildedGhoda.class));
cards.add(new SetCardInfo("Glitch Ghost Surveyor", 44, Rarity.COMMON, mage.cards.g.GlitchGhostSurveyor.class));
@@ -96,12 +111,17 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Greasewrench Goblin", 132, Rarity.UNCOMMON, mage.cards.g.GreasewrenchGoblin.class));
cards.add(new SetCardInfo("Greenbelt Guardian", 164, Rarity.UNCOMMON, mage.cards.g.GreenbeltGuardian.class));
cards.add(new SetCardInfo("Grim Bauble", 88, Rarity.COMMON, mage.cards.g.GrimBauble.class));
+ cards.add(new SetCardInfo("Grim Javelineer", 89, Rarity.COMMON, mage.cards.g.GrimJavelineer.class));
cards.add(new SetCardInfo("Guardian Sunmare", 15, Rarity.RARE, mage.cards.g.GuardianSunmare.class));
cards.add(new SetCardInfo("Guidelight Pathmaker", 206, Rarity.UNCOMMON, mage.cards.g.GuidelightPathmaker.class));
+ cards.add(new SetCardInfo("Guidelight Synergist", 16, Rarity.UNCOMMON, mage.cards.g.GuidelightSynergist.class));
+ cards.add(new SetCardInfo("Haunt the Network", 207, Rarity.UNCOMMON, mage.cards.h.HauntTheNetwork.class));
cards.add(new SetCardInfo("Haunted Hellride", 208, Rarity.UNCOMMON, mage.cards.h.HauntedHellride.class));
cards.add(new SetCardInfo("Hazard of the Dunes", 165, Rarity.COMMON, mage.cards.h.HazardOfTheDunes.class));
cards.add(new SetCardInfo("Hazoret, Godseeker", 133, Rarity.MYTHIC, mage.cards.h.HazoretGodseeker.class));
+ cards.add(new SetCardInfo("Hour of Victory", 91, Rarity.UNCOMMON, mage.cards.h.HourOfVictory.class));
cards.add(new SetCardInfo("Howler's Heavy", 46, Rarity.COMMON, mage.cards.h.HowlersHeavy.class));
+ cards.add(new SetCardInfo("Howlsquad Heavy", 134, Rarity.RARE, mage.cards.h.HowlsquadHeavy.class));
cards.add(new SetCardInfo("Hulldrifter", 47, Rarity.COMMON, mage.cards.h.Hulldrifter.class));
cards.add(new SetCardInfo("Interface Ace", 17, Rarity.COMMON, mage.cards.i.InterfaceAce.class));
cards.add(new SetCardInfo("Intimidation Tactics", 92, Rarity.UNCOMMON, mage.cards.i.IntimidationTactics.class));
@@ -110,12 +130,19 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Jungle Hollow", 256, Rarity.COMMON, mage.cards.j.JungleHollow.class));
cards.add(new SetCardInfo("Kalakscion, Hunger Tyrant", 93, Rarity.UNCOMMON, mage.cards.k.KalakscionHungerTyrant.class));
cards.add(new SetCardInfo("Keen Buccaneer", 48, Rarity.COMMON, mage.cards.k.KeenBuccaneer.class));
+ cards.add(new SetCardInfo("Kickoff Celebrations", 135, Rarity.COMMON, mage.cards.k.KickoffCelebrations.class));
cards.add(new SetCardInfo("Lagorin, Soul of Alacria", 211, Rarity.UNCOMMON, mage.cards.l.LagorinSoulOfAlacria.class));
cards.add(new SetCardInfo("Leonin Surveyor", 18, Rarity.COMMON, mage.cards.l.LeoninSurveyor.class));
+ cards.add(new SetCardInfo("Lifecraft Engine", 234, Rarity.RARE, mage.cards.l.LifecraftEngine.class));
cards.add(new SetCardInfo("Lightning Strike", 136, Rarity.COMMON, mage.cards.l.LightningStrike.class));
cards.add(new SetCardInfo("Lightshield Parry", 19, Rarity.COMMON, mage.cards.l.LightshieldParry.class));
+ cards.add(new SetCardInfo("Lightwheel Enhancements", 20, Rarity.COMMON, mage.cards.l.LightwheelEnhancements.class));
cards.add(new SetCardInfo("Locust Spray", 95, Rarity.UNCOMMON, mage.cards.l.LocustSpray.class));
+ cards.add(new SetCardInfo("Lotusguard Disciple", 21, Rarity.COMMON, mage.cards.l.LotusguardDisciple.class));
cards.add(new SetCardInfo("Loxodon Surveyor", 167, Rarity.COMMON, mage.cards.l.LoxodonSurveyor.class));
+ cards.add(new SetCardInfo("Lumbering Worldwagon", 168, Rarity.RARE, mage.cards.l.LumberingWorldwagon.class));
+ cards.add(new SetCardInfo("Magmakin Artillerist", 137, Rarity.COMMON, mage.cards.m.MagmakinArtillerist.class));
+ cards.add(new SetCardInfo("Marauding Mako", 138, Rarity.UNCOMMON, mage.cards.m.MaraudingMako.class));
cards.add(new SetCardInfo("Marketback Walker", 235, Rarity.RARE, mage.cards.m.MarketbackWalker.class));
cards.add(new SetCardInfo("Marshals' Pathcruiser", 236, Rarity.UNCOMMON, mage.cards.m.MarshalsPathcruiser.class));
cards.add(new SetCardInfo("Maximum Overdrive", 96, Rarity.COMMON, mage.cards.m.MaximumOverdrive.class));
@@ -126,16 +153,25 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Molt Tender", 171, Rarity.UNCOMMON, mage.cards.m.MoltTender.class));
cards.add(new SetCardInfo("Monument to Endurance", 237, Rarity.RARE, mage.cards.m.MonumentToEndurance.class));
cards.add(new SetCardInfo("Mountain", 286, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Mu Yanling, Wind Rider", 52, Rarity.MYTHIC, mage.cards.m.MuYanlingWindRider.class));
cards.add(new SetCardInfo("Muraganda Raceway", 257, Rarity.RARE, mage.cards.m.MuragandaRaceway.class));
cards.add(new SetCardInfo("Mutant Surveyor", 98, Rarity.COMMON, mage.cards.m.MutantSurveyor.class));
cards.add(new SetCardInfo("Nesting Bot", 22, Rarity.UNCOMMON, mage.cards.n.NestingBot.class));
cards.add(new SetCardInfo("Night Market", 258, Rarity.COMMON, mage.cards.n.NightMarket.class));
cards.add(new SetCardInfo("Nimble Thopterist", 53, Rarity.COMMON, mage.cards.n.NimbleThopterist.class));
+ cards.add(new SetCardInfo("Ooze Patrol", 172, Rarity.UNCOMMON, mage.cards.o.OozePatrol.class));
+ cards.add(new SetCardInfo("Outpace Oblivion", 139, Rarity.UNCOMMON, mage.cards.o.OutpaceOblivion.class));
+ cards.add(new SetCardInfo("Oviya, Automech Artisan", 173, Rarity.RARE, mage.cards.o.OviyaAutomechArtisan.class));
cards.add(new SetCardInfo("Pacesetter Paragon", 140, Rarity.UNCOMMON, mage.cards.p.PacesetterParagon.class));
+ cards.add(new SetCardInfo("Pactdoll Terror", 99, Rarity.COMMON, mage.cards.p.PactdollTerror.class));
cards.add(new SetCardInfo("Pedal to the Metal", 141, Rarity.COMMON, mage.cards.p.PedalToTheMetal.class));
+ cards.add(new SetCardInfo("Perilous Snare", 23, Rarity.RARE, mage.cards.p.PerilousSnare.class));
+ cards.add(new SetCardInfo("Pit Automaton", 238, Rarity.UNCOMMON, mage.cards.p.PitAutomaton.class));
cards.add(new SetCardInfo("Plains", 272, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Plow Through", 174, Rarity.UNCOMMON, mage.cards.p.PlowThrough.class));
+ cards.add(new SetCardInfo("Point the Way", 175, Rarity.UNCOMMON, mage.cards.p.PointTheWay.class));
cards.add(new SetCardInfo("Possession Engine", 54, Rarity.RARE, mage.cards.p.PossessionEngine.class));
+ cards.add(new SetCardInfo("Pothole Mole", 176, Rarity.COMMON, mage.cards.p.PotholeMole.class));
cards.add(new SetCardInfo("Pride of the Road", 24, Rarity.UNCOMMON, mage.cards.p.PrideOfTheRoad.class));
cards.add(new SetCardInfo("Prowcatcher Specialist", 142, Rarity.COMMON, mage.cards.p.ProwcatcherSpecialist.class));
cards.add(new SetCardInfo("Pyrewood Gearhulk", 216, Rarity.MYTHIC, mage.cards.p.PyrewoodGearhulk.class));
@@ -143,31 +179,44 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Racers' Scoreboard", 239, Rarity.UNCOMMON, mage.cards.r.RacersScoreboard.class));
cards.add(new SetCardInfo("Rangers' Aetherhive", 217, Rarity.UNCOMMON, mage.cards.r.RangersAetherhive.class));
cards.add(new SetCardInfo("Rangers' Refueler", 55, Rarity.UNCOMMON, mage.cards.r.RangersRefueler.class));
+ cards.add(new SetCardInfo("Reckless Velocitaur", 144, Rarity.UNCOMMON, mage.cards.r.RecklessVelocitaur.class));
+ cards.add(new SetCardInfo("Redshift, Rocketeer Chief", 218, Rarity.RARE, mage.cards.r.RedshiftRocketeerChief.class));
cards.add(new SetCardInfo("Reef Roads", 259, Rarity.UNCOMMON, mage.cards.r.ReefRoads.class));
cards.add(new SetCardInfo("Regal Imperiosaur", 177, Rarity.RARE, mage.cards.r.RegalImperiosaur.class));
+ cards.add(new SetCardInfo("Repurposing Bay", 56, Rarity.RARE, mage.cards.r.RepurposingBay.class));
cards.add(new SetCardInfo("Ride's End", 25, Rarity.COMMON, mage.cards.r.RidesEnd.class));
cards.add(new SetCardInfo("Ripclaw Wrangler", 101, Rarity.COMMON, mage.cards.r.RipclawWrangler.class));
+ cards.add(new SetCardInfo("Rise from the Wreck", 178, Rarity.UNCOMMON, mage.cards.r.RiseFromTheWreck.class));
cards.add(new SetCardInfo("Risen Necroregent", 102, Rarity.UNCOMMON, mage.cards.r.RisenNecroregent.class));
cards.add(new SetCardInfo("Risky Shortcut", 103, Rarity.COMMON, mage.cards.r.RiskyShortcut.class));
cards.add(new SetCardInfo("Riverpyre Verge", 260, Rarity.RARE, mage.cards.r.RiverpyreVerge.class));
+ cards.add(new SetCardInfo("Road Rage", 145, Rarity.UNCOMMON, mage.cards.r.RoadRage.class));
cards.add(new SetCardInfo("Roadside Assistance", 26, Rarity.UNCOMMON, mage.cards.r.RoadsideAssistance.class));
cards.add(new SetCardInfo("Roadside Blowout", 58, Rarity.UNCOMMON, mage.cards.r.RoadsideBlowout.class));
cards.add(new SetCardInfo("Rocketeer Boostbuggy", 220, Rarity.UNCOMMON, mage.cards.r.RocketeerBoostbuggy.class));
cards.add(new SetCardInfo("Rocky Roads", 261, Rarity.UNCOMMON, mage.cards.r.RockyRoads.class));
cards.add(new SetCardInfo("Rover Blades", 241, Rarity.UNCOMMON, mage.cards.r.RoverBlades.class));
cards.add(new SetCardInfo("Rugged Highlands", 262, Rarity.COMMON, mage.cards.r.RuggedHighlands.class));
+ cards.add(new SetCardInfo("Run Over", 179, Rarity.COMMON, mage.cards.r.RunOver.class));
cards.add(new SetCardInfo("Sab-Sunen, Luxa Embodied", 221, Rarity.MYTHIC, mage.cards.s.SabSunenLuxaEmbodied.class));
+ cards.add(new SetCardInfo("Sabotage Strategist", 59, Rarity.UNCOMMON, mage.cards.s.SabotageStrategist.class));
cards.add(new SetCardInfo("Salvation Engine", 27, Rarity.MYTHIC, mage.cards.s.SalvationEngine.class));
+ cards.add(new SetCardInfo("Samut, the Driving Force", 222, Rarity.RARE, mage.cards.s.SamutTheDrivingForce.class));
cards.add(new SetCardInfo("Scoured Barrens", 263, Rarity.COMMON, mage.cards.s.ScouredBarrens.class));
+ cards.add(new SetCardInfo("Scrap Compactor", 242, Rarity.COMMON, mage.cards.s.ScrapCompactor.class));
+ cards.add(new SetCardInfo("Scrounging Skyray", 60, Rarity.UNCOMMON, mage.cards.s.ScroungingSkyray.class));
cards.add(new SetCardInfo("Shefet Archfiend", 104, Rarity.UNCOMMON, mage.cards.s.ShefetArchfiend.class));
cards.add(new SetCardInfo("Silken Strength", 180, Rarity.COMMON, mage.cards.s.SilkenStrength.class));
cards.add(new SetCardInfo("Skybox Ferry", 243, Rarity.COMMON, mage.cards.s.SkyboxFerry.class));
cards.add(new SetCardInfo("Skycrash", 146, Rarity.UNCOMMON, mage.cards.s.Skycrash.class));
cards.add(new SetCardInfo("Skystreak Engineer", 61, Rarity.COMMON, mage.cards.s.SkystreakEngineer.class));
+ cards.add(new SetCardInfo("Slick Imitator", 62, Rarity.UNCOMMON, mage.cards.s.SlickImitator.class));
cards.add(new SetCardInfo("Spectral Interference", 63, Rarity.COMMON, mage.cards.s.SpectralInterference.class));
cards.add(new SetCardInfo("Spell Pierce", 64, Rarity.UNCOMMON, mage.cards.s.SpellPierce.class));
+ cards.add(new SetCardInfo("Spikeshell Harrier", 65, Rarity.UNCOMMON, mage.cards.s.SpikeshellHarrier.class));
cards.add(new SetCardInfo("Spin Out", 106, Rarity.COMMON, mage.cards.s.SpinOut.class));
cards.add(new SetCardInfo("Spotcycle Scouter", 30, Rarity.COMMON, mage.cards.s.SpotcycleScouter.class));
+ cards.add(new SetCardInfo("Stall Out", 66, Rarity.COMMON, mage.cards.s.StallOut.class));
cards.add(new SetCardInfo("Stampeding Scurryfoot", 181, Rarity.COMMON, mage.cards.s.StampedingScurryfoot.class));
cards.add(new SetCardInfo("Starting Column", 244, Rarity.COMMON, mage.cards.s.StartingColumn.class));
cards.add(new SetCardInfo("Stock Up", 67, Rarity.UNCOMMON, mage.cards.s.StockUp.class));
@@ -179,13 +228,18 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Swiftwing Assailant", 32, Rarity.COMMON, mage.cards.s.SwiftwingAssailant.class));
cards.add(new SetCardInfo("Syphon Fuel", 108, Rarity.COMMON, mage.cards.s.SyphonFuel.class));
cards.add(new SetCardInfo("Terrian, World Tyrant", 182, Rarity.UNCOMMON, mage.cards.t.TerrianWorldTyrant.class));
+ cards.add(new SetCardInfo("The Aetherspark", 231, Rarity.MYTHIC, mage.cards.t.TheAetherspark.class));
cards.add(new SetCardInfo("The Last Ride", 94, Rarity.MYTHIC, mage.cards.t.TheLastRide.class));
cards.add(new SetCardInfo("The Speed Demon", 105, Rarity.MYTHIC, mage.cards.t.TheSpeedDemon.class));
cards.add(new SetCardInfo("Thopter Fabricator", 68, Rarity.RARE, mage.cards.t.ThopterFabricator.class));
cards.add(new SetCardInfo("Thornwood Falls", 266, Rarity.COMMON, mage.cards.t.ThornwoodFalls.class));
+ cards.add(new SetCardInfo("Thunderhead Gunner", 148, Rarity.COMMON, mage.cards.t.ThunderheadGunner.class));
cards.add(new SetCardInfo("Thundering Broodwagon", 225, Rarity.UNCOMMON, mage.cards.t.ThunderingBroodwagon.class));
+ cards.add(new SetCardInfo("Ticket Tortoise", 245, Rarity.COMMON, mage.cards.t.TicketTortoise.class));
+ cards.add(new SetCardInfo("Trade the Helm", 69, Rarity.UNCOMMON, mage.cards.t.TradeTheHelm.class));
cards.add(new SetCardInfo("Tranquil Cove", 267, Rarity.COMMON, mage.cards.t.TranquilCove.class));
cards.add(new SetCardInfo("Transit Mage", 70, Rarity.UNCOMMON, mage.cards.t.TransitMage.class));
+ cards.add(new SetCardInfo("Trip Up", 71, Rarity.COMMON, mage.cards.t.TripUp.class));
cards.add(new SetCardInfo("Tyrox, Saurid Tyrant", 149, Rarity.UNCOMMON, mage.cards.t.TyroxSauridTyrant.class));
cards.add(new SetCardInfo("Unstoppable Plan", 72, Rarity.RARE, mage.cards.u.UnstoppablePlan.class));
cards.add(new SetCardInfo("Unswerving Sloth", 34, Rarity.UNCOMMON, mage.cards.u.UnswervingSloth.class));
@@ -193,18 +247,20 @@ public final class Aetherdrift extends ExpansionSet {
cards.add(new SetCardInfo("Veloheart Bike", 184, Rarity.COMMON, mage.cards.v.VeloheartBike.class));
cards.add(new SetCardInfo("Venomsac Lagac", 185, Rarity.COMMON, mage.cards.v.VenomsacLagac.class));
cards.add(new SetCardInfo("Veteran Beastrider", 226, Rarity.UNCOMMON, mage.cards.v.VeteranBeastrider.class));
+ cards.add(new SetCardInfo("Vnwxt, Verbose Host", 73, Rarity.RARE, mage.cards.v.VnwxtVerboseHost.class));
cards.add(new SetCardInfo("Voyage Home", 227, Rarity.UNCOMMON, mage.cards.v.VoyageHome.class));
+ cards.add(new SetCardInfo("Voyager Glidecar", 36, Rarity.RARE, mage.cards.v.VoyagerGlidecar.class));
cards.add(new SetCardInfo("Voyager Quickwelder", 37, Rarity.COMMON, mage.cards.v.VoyagerQuickwelder.class));
cards.add(new SetCardInfo("Walking Sarcophagus", 246, Rarity.COMMON, mage.cards.w.WalkingSarcophagus.class));
cards.add(new SetCardInfo("Wastewood Verge", 268, Rarity.RARE, mage.cards.w.WastewoodVerge.class));
cards.add(new SetCardInfo("Waxen Shapethief", 74, Rarity.RARE, mage.cards.w.WaxenShapethief.class));
+ cards.add(new SetCardInfo("Webstrike Elite", 186, Rarity.RARE, mage.cards.w.WebstrikeElite.class));
cards.add(new SetCardInfo("Wild Roads", 269, Rarity.UNCOMMON, mage.cards.w.WildRoads.class));
cards.add(new SetCardInfo("Willowrush Verge", 270, Rarity.RARE, mage.cards.w.WillowrushVerge.class));
cards.add(new SetCardInfo("Wind-Scarred Crag", 271, Rarity.COMMON, mage.cards.w.WindScarredCrag.class));
cards.add(new SetCardInfo("Wreck Remover", 247, Rarity.COMMON, mage.cards.w.WreckRemover.class));
cards.add(new SetCardInfo("Wreckage Wickerfolk", 110, Rarity.COMMON, mage.cards.w.WreckageWickerfolk.class));
cards.add(new SetCardInfo("Wretched Doll", 111, Rarity.UNCOMMON, mage.cards.w.WretchedDoll.class));
-
- cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName()));
+ cards.add(new SetCardInfo("Zahur, Glory's Past", 229, Rarity.RARE, mage.cards.z.ZahurGlorysPast.class));
}
}
diff --git a/Mage.Sets/src/mage/sets/AetherdriftCommander.java b/Mage.Sets/src/mage/sets/AetherdriftCommander.java
index 3444bbd6e67..24dcf213775 100644
--- a/Mage.Sets/src/mage/sets/AetherdriftCommander.java
+++ b/Mage.Sets/src/mage/sets/AetherdriftCommander.java
@@ -20,8 +20,12 @@ public final class AetherdriftCommander extends ExpansionSet {
this.hasBasicLands = false;
cards.add(new SetCardInfo("Academy Ruins", 58, Rarity.RARE, mage.cards.a.AcademyRuins.class));
+ cards.add(new SetCardInfo("Accursed Duneyard", 20, Rarity.RARE, mage.cards.a.AccursedDuneyard.class));
+ cards.add(new SetCardInfo("Adaptive Omnitool", 16, Rarity.RARE, mage.cards.a.AdaptiveOmnitool.class));
cards.add(new SetCardInfo("Adarkar Wastes", 144, Rarity.RARE, mage.cards.a.AdarkarWastes.class));
cards.add(new SetCardInfo("Aether Hub", 145, Rarity.UNCOMMON, mage.cards.a.AetherHub.class));
+ cards.add(new SetCardInfo("Aetherflux Conduit", 17, Rarity.RARE, mage.cards.a.AetherfluxConduit.class));
+ cards.add(new SetCardInfo("Aetheric Amplifier", 18, Rarity.RARE, mage.cards.a.AethericAmplifier.class));
cards.add(new SetCardInfo("Aethersquall Ancient", 68, Rarity.RARE, mage.cards.a.AethersquallAncient.class));
cards.add(new SetCardInfo("Aethertide Whale", 69, Rarity.RARE, mage.cards.a.AethertideWhale.class));
cards.add(new SetCardInfo("Aetherwind Basker", 107, Rarity.MYTHIC, mage.cards.a.AetherwindBasker.class));
@@ -115,6 +119,7 @@ public final class AetherdriftCommander extends ExpansionSet {
cards.add(new SetCardInfo("Panharmonicon", 135, Rarity.RARE, mage.cards.p.Panharmonicon.class));
cards.add(new SetCardInfo("Path of Ancestry", 61, Rarity.COMMON, mage.cards.p.PathOfAncestry.class));
cards.add(new SetCardInfo("Peema Aether-Seer", 113, Rarity.UNCOMMON, mage.cards.p.PeemaAetherSeer.class));
+ cards.add(new SetCardInfo("Peema Trailblazer", 14, Rarity.RARE, mage.cards.p.PeemaTrailblazer.class));
cards.add(new SetCardInfo("Pia and Kiran Nalaar", 105, Rarity.RARE, mage.cards.p.PiaAndKiranNalaar.class));
cards.add(new SetCardInfo("Plague Belcher", 97, Rarity.RARE, mage.cards.p.PlagueBelcher.class));
cards.add(new SetCardInfo("Prairie Stream", 167, Rarity.RARE, mage.cards.p.PrairieStream.class));
@@ -123,6 +128,7 @@ public final class AetherdriftCommander extends ExpansionSet {
cards.add(new SetCardInfo("Reality Shift", 39, Rarity.UNCOMMON, mage.cards.r.RealityShift.class));
cards.add(new SetCardInfo("Reckless Fireweaver", 106, Rarity.COMMON, mage.cards.r.RecklessFireweaver.class));
cards.add(new SetCardInfo("Retrofitter Foundry", 136, Rarity.RARE, mage.cards.r.RetrofitterFoundry.class));
+ cards.add(new SetCardInfo("Rhet-Tomb Mystic", 10, Rarity.RARE, mage.cards.r.RhetTombMystic.class));
cards.add(new SetCardInfo("Rogue Refiner", 118, Rarity.UNCOMMON, mage.cards.r.RogueRefiner.class));
cards.add(new SetCardInfo("Rootbound Crag", 168, Rarity.RARE, mage.cards.r.RootboundCrag.class));
cards.add(new SetCardInfo("Rot Hulk", 98, Rarity.MYTHIC, mage.cards.r.RotHulk.class));
diff --git a/Mage.Sets/src/mage/sets/BloomburrowCommander.java b/Mage.Sets/src/mage/sets/BloomburrowCommander.java
index daf45fc4390..cd669f6f14a 100644
--- a/Mage.Sets/src/mage/sets/BloomburrowCommander.java
+++ b/Mage.Sets/src/mage/sets/BloomburrowCommander.java
@@ -300,6 +300,8 @@ public final class BloomburrowCommander extends ExpansionSet {
cards.add(new SetCardInfo("Tetsuko Umezawa, Fugitive", 177, Rarity.UNCOMMON, mage.cards.t.TetsukoUmezawaFugitive.class));
cards.add(new SetCardInfo("The Gitrog Monster", 88, Rarity.MYTHIC, mage.cards.t.TheGitrogMonster.class));
cards.add(new SetCardInfo("The Odd Acorn Gang", 7, Rarity.MYTHIC, mage.cards.t.TheOddAcornGang.class));
+ cards.add(new SetCardInfo("Thickest in the Thicket", 34, Rarity.RARE, mage.cards.t.ThickestInTheThicket.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Thickest in the Thicket", 67, Rarity.RARE, mage.cards.t.ThickestInTheThicket.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Thopter Engineer", 204, Rarity.UNCOMMON, mage.cards.t.ThopterEngineer.class));
cards.add(new SetCardInfo("Thought Vessel", 289, Rarity.COMMON, mage.cards.t.ThoughtVessel.class));
cards.add(new SetCardInfo("Thran Dynamo", 290, Rarity.UNCOMMON, mage.cards.t.ThranDynamo.class));
diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java
index 2c095c0831a..a9ff63f658b 100644
--- a/Mage.Sets/src/mage/sets/DoctorWho.java
+++ b/Mage.Sets/src/mage/sets/DoctorWho.java
@@ -920,13 +920,13 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("The Fifth Doctor", 413, Rarity.RARE, mage.cards.t.TheFifthDoctor.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Fifth Doctor", 556, Rarity.RARE, mage.cards.t.TheFifthDoctor.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Fifth Doctor", 732, Rarity.RARE, mage.cards.t.TheFifthDoctor.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("The First Doctor", "552z", Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("The First Doctor", 1005, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("The First Doctor", 1143, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("The First Doctor", 128, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("The First Doctor", 414, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("The First Doctor", 552, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("The First Doctor", 733, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("The First Doctor", "552z", Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("The First Doctor", 1005, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("The First Doctor", 1143, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("The First Doctor", 128, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("The First Doctor", 414, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("The First Doctor", 552, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("The First Doctor", 733, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("The Five Doctors", 101, Rarity.RARE, mage.cards.t.TheFiveDoctors.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("The Five Doctors", 394, Rarity.RARE, mage.cards.t.TheFiveDoctors.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("The Five Doctors", 706, Rarity.RARE, mage.cards.t.TheFiveDoctors.class, NON_FULL_USE_VARIOUS));
@@ -1180,10 +1180,10 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("Weeping Angel", 773, Rarity.RARE, mage.cards.w.WeepingAngel.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Wibbly-wobbly, Timey-wimey", 62, Rarity.COMMON, mage.cards.w.WibblyWobblyTimeyWimey.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Wibbly-wobbly, Timey-wimey", 667, Rarity.COMMON, mage.cards.w.WibblyWobblyTimeyWimey.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Wilfred Mott", 32, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Wilfred Mott", 350, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Wilfred Mott", 637, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Wilfred Mott", 941, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Wilfred Mott", 32, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Wilfred Mott", 350, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Wilfred Mott", 637, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Wilfred Mott", 941, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Wound Reflection", 1062, Rarity.RARE, mage.cards.w.WoundReflection.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Wound Reflection", 223, Rarity.RARE, mage.cards.w.WoundReflection.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Wound Reflection", 471, Rarity.RARE, mage.cards.w.WoundReflection.class, NON_FULL_USE_VARIOUS));
diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java
index d1b61be1f1e..1e66450e904 100644
--- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java
+++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java
@@ -180,8 +180,8 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Hand That Feeds", 139, Rarity.COMMON, mage.cards.h.HandThatFeeds.class));
cards.add(new SetCardInfo("Hardened Escort", 16, Rarity.COMMON, mage.cards.h.HardenedEscort.class));
cards.add(new SetCardInfo("Haunted Screen", 250, Rarity.UNCOMMON, mage.cards.h.HauntedScreen.class));
- //cards.add(new SetCardInfo("Hauntwoods Shrieker", 182, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Hauntwoods Shrieker", 349, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Hauntwoods Shrieker", 182, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Hauntwoods Shrieker", 349, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Hedge Shredder", 183, Rarity.RARE, mage.cards.h.HedgeShredder.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Hedge Shredder", 320, Rarity.RARE, mage.cards.h.HedgeShredder.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Horrid Vigor", 184, Rarity.COMMON, mage.cards.h.HorridVigor.class));
diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java
index ac626b3e735..8a977e570c8 100644
--- a/Mage.Sets/src/mage/sets/Fallout.java
+++ b/Mage.Sets/src/mage/sets/Fallout.java
@@ -839,10 +839,10 @@ public final class Fallout extends ExpansionSet {
//cards.add(new SetCardInfo("Strong, the Brutish Thespian", 612, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("Strong, the Brutish Thespian", 84, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS));
//cards.add(new SetCardInfo("Strong, the Brutish Thespian", 931, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Struggle for Project Purity", 380, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Struggle for Project Purity", 39, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Struggle for Project Purity", 567, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
- //cards.add(new SetCardInfo("Struggle for Project Purity", 908, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Struggle for Project Purity", 380, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Struggle for Project Purity", 39, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Struggle for Project Purity", 567, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Struggle for Project Purity", 908, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Sulfur Falls", 1040, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Sulfur Falls", 294, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Sulfur Falls", 512, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS));
diff --git a/Mage.Sets/src/mage/sets/GameDayPromos.java b/Mage.Sets/src/mage/sets/GameDayPromos.java
new file mode 100644
index 00000000000..6792560c183
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/GameDayPromos.java
@@ -0,0 +1,33 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/gdy
+ */
+public final class GameDayPromos extends ExpansionSet {
+
+ private static final GameDayPromos instance = new GameDayPromos();
+
+ public static GameDayPromos getInstance() {
+ return instance;
+ }
+
+ private GameDayPromos() {
+ super("Game Day Promos", "GDY", ExpansionSet.buildDate(2022, 4, 8), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("All-Seeing Arbiter", 3, Rarity.MYTHIC, mage.cards.a.AllSeeingArbiter.class));
+ cards.add(new SetCardInfo("Braids, Arisen Nightmare", 8, Rarity.RARE, mage.cards.b.BraidsArisenNightmare.class));
+ cards.add(new SetCardInfo("Power Word Kill", 1, Rarity.RARE, mage.cards.p.PowerWordKill.class));
+ cards.add(new SetCardInfo("Recruitment Officer", 7, Rarity.RARE, mage.cards.r.RecruitmentOfficer.class));
+ cards.add(new SetCardInfo("Shivan Devastator", 6, Rarity.MYTHIC, mage.cards.s.ShivanDevastator.class));
+ cards.add(new SetCardInfo("Skyclave Apparition", 2, Rarity.RARE, mage.cards.s.SkyclaveApparition.class));
+ cards.add(new SetCardInfo("Surge Engine", 9, Rarity.MYTHIC, mage.cards.s.SurgeEngine.class));
+ cards.add(new SetCardInfo("Touch the Spirit Realm", 4, Rarity.RARE, mage.cards.t.TouchTheSpiritRealm.class));
+ cards.add(new SetCardInfo("Workshop Warchief", 5, Rarity.RARE, mage.cards.w.WorkshopWarchief.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java
index d409a1c4709..1227997eb3f 100644
--- a/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java
+++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java
@@ -21,10 +21,14 @@ public class JudgeGiftCards2020 extends ExpansionSet {
this.hasBasicLands = false;
cards.add(new SetCardInfo("Arena Rector", 1, Rarity.RARE, mage.cards.a.ArenaRector.class));
+ cards.add(new SetCardInfo("Birthing Pod", 7, Rarity.RARE, mage.cards.b.BirthingPod.class));
cards.add(new SetCardInfo("Demonic Tutor", 4, Rarity.RARE, mage.cards.d.DemonicTutor.class));
cards.add(new SetCardInfo("Enlightened Tutor", 2, Rarity.RARE, mage.cards.e.EnlightenedTutor.class));
+ cards.add(new SetCardInfo("Eye of Ugin", 10, Rarity.RARE, mage.cards.e.EyeOfUgin.class));
cards.add(new SetCardInfo("Gamble", 6, Rarity.RARE, mage.cards.g.Gamble.class));
+ cards.add(new SetCardInfo("Infernal Tutor", 5, Rarity.RARE, mage.cards.i.InfernalTutor.class));
cards.add(new SetCardInfo("Spellseeker", 3, Rarity.RARE, mage.cards.s.Spellseeker.class));
+ cards.add(new SetCardInfo("Sterling Grove", 9, Rarity.RARE, mage.cards.s.SterlingGrove.class));
cards.add(new SetCardInfo("Sylvan Tutor", 8, Rarity.RARE, mage.cards.s.SylvanTutor.class));
}
}
diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2021.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2021.java
new file mode 100644
index 00000000000..efe4cd6675d
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2021.java
@@ -0,0 +1,35 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/pj21
+ */
+public class JudgeGiftCards2021 extends ExpansionSet {
+
+ private static final JudgeGiftCards2021 instance = new JudgeGiftCards2021();
+
+ public static JudgeGiftCards2021 getInstance() {
+ return instance;
+ }
+
+ private JudgeGiftCards2021() {
+ super("Judge Gift Cards 2021", "PJ21", ExpansionSet.buildDate(2021, 1, 1), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("Edgar Markov", 3, Rarity.MYTHIC, mage.cards.e.EdgarMarkov.class));
+ cards.add(new SetCardInfo("Ezuri, Claw of Progress", 4, Rarity.MYTHIC, mage.cards.e.EzuriClawOfProgress.class));
+ cards.add(new SetCardInfo("Grand Arbiter Augustin IV", 6, Rarity.RARE, mage.cards.g.GrandArbiterAugustinIV.class));
+ cards.add(new SetCardInfo("Karlov of the Ghost Council", 7, Rarity.MYTHIC, mage.cards.k.KarlovOfTheGhostCouncil.class));
+ cards.add(new SetCardInfo("K'rrik, Son of Yawgmoth", 2, Rarity.RARE, mage.cards.k.KrrikSonOfYawgmoth.class));
+ cards.add(new SetCardInfo("Mizzix of the Izmagnus", 8, Rarity.MYTHIC, mage.cards.m.MizzixOfTheIzmagnus.class));
+ cards.add(new SetCardInfo("Morophon, the Boundless", 1, Rarity.MYTHIC, mage.cards.m.MorophonTheBoundless.class));
+ cards.add(new SetCardInfo("Nicol Bolas, the Arisen", 9, Rarity.MYTHIC, mage.cards.n.NicolBolasTheArisen.class));
+ cards.add(new SetCardInfo("Nicol Bolas, the Ravager", 9, Rarity.MYTHIC, mage.cards.n.NicolBolasTheRavager.class));
+ cards.add(new SetCardInfo("The Gitrog Monster", 5, Rarity.MYTHIC, mage.cards.t.TheGitrogMonster.class));
+ cards.add(new SetCardInfo("Zacama, Primal Calamity", 10, Rarity.MYTHIC, mage.cards.z.ZacamaPrimalCalamity.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2022.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2022.java
new file mode 100644
index 00000000000..75275088946
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2022.java
@@ -0,0 +1,35 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/p22
+ */
+public class JudgeGiftCards2022 extends ExpansionSet {
+
+ private static final JudgeGiftCards2022 instance = new JudgeGiftCards2022();
+
+ public static JudgeGiftCards2022 getInstance() {
+ return instance;
+ }
+
+ private JudgeGiftCards2022() {
+ super("Judge Gift Cards 2022", "P22", ExpansionSet.buildDate(2022, 1, 1), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("Animate Dead", 7, Rarity.RARE, mage.cards.a.AnimateDead.class));
+ cards.add(new SetCardInfo("Greater Auramancy", 1, Rarity.RARE, mage.cards.g.GreaterAuramancy.class));
+ cards.add(new SetCardInfo("Growing Rites of Itlimoc", 10, Rarity.RARE, mage.cards.g.GrowingRitesOfItlimoc.class));
+ cards.add(new SetCardInfo("Itlimoc, Cradle of the Sun", 10, Rarity.RARE, mage.cards.i.ItlimocCradleOfTheSun.class));
+ cards.add(new SetCardInfo("No Mercy", 9, Rarity.RARE, mage.cards.n.NoMercy.class));
+ cards.add(new SetCardInfo("Omniscience", 2, Rarity.RARE, mage.cards.o.Omniscience.class));
+ cards.add(new SetCardInfo("Parallel Lives", 3, Rarity.RARE, mage.cards.p.ParallelLives.class));
+ cards.add(new SetCardInfo("Purphoros, God of the Forge", 8, Rarity.RARE, mage.cards.p.PurphorosGodOfTheForge.class));
+ cards.add(new SetCardInfo("Smothering Tithe", 5, Rarity.RARE, mage.cards.s.SmotheringTithe.class));
+ cards.add(new SetCardInfo("Stranglehold", 4, Rarity.RARE, mage.cards.s.Stranglehold.class));
+ cards.add(new SetCardInfo("Training Grounds", 6, Rarity.RARE, mage.cards.t.TrainingGrounds.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2023.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2023.java
new file mode 100644
index 00000000000..133474c56f6
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2023.java
@@ -0,0 +1,33 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/p23
+ */
+public class JudgeGiftCards2023 extends ExpansionSet {
+
+ private static final JudgeGiftCards2023 instance = new JudgeGiftCards2023();
+
+ public static JudgeGiftCards2023 getInstance() {
+ return instance;
+ }
+
+ private JudgeGiftCards2023() {
+ super("Judge Gift Cards 2023", "P23", ExpansionSet.buildDate(2023, 1, 1), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+
+ cards.add(new SetCardInfo("Forest", 10, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS));
+ cards.add(new SetCardInfo("Grindstone", 2, Rarity.RARE, mage.cards.g.Grindstone.class));
+ cards.add(new SetCardInfo("Island", 7, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS));
+ cards.add(new SetCardInfo("Mountain", 9, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS));
+ cards.add(new SetCardInfo("Mycosynth Lattice", 3, Rarity.RARE, mage.cards.m.MycosynthLattice.class));
+ cards.add(new SetCardInfo("Painter's Servant", 1, Rarity.RARE, mage.cards.p.PaintersServant.class));
+ cards.add(new SetCardInfo("Plains", 6, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS));
+ cards.add(new SetCardInfo("Retrofitter Foundry", 4, Rarity.RARE, mage.cards.r.RetrofitterFoundry.class));
+ cards.add(new SetCardInfo("Swamp", 8, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS));
+ cards.add(new SetCardInfo("Sword of War and Peace", 5, Rarity.MYTHIC, mage.cards.s.SwordOfWarAndPeace.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/SpecialGuests.java b/Mage.Sets/src/mage/sets/SpecialGuests.java
index 55dbe9fbb6c..fd069857867 100644
--- a/Mage.Sets/src/mage/sets/SpecialGuests.java
+++ b/Mage.Sets/src/mage/sets/SpecialGuests.java
@@ -21,35 +21,58 @@ public final class SpecialGuests extends ExpansionSet {
this.hasBoosters = false;
this.hasBasicLands = false;
+ cards.add(new SetCardInfo("Akroma's Memorial", 81, Rarity.MYTHIC, mage.cards.a.AkromasMemorial.class));
+ cards.add(new SetCardInfo("Bloom Tender", 79, Rarity.MYTHIC, mage.cards.b.BloomTender.class));
+ cards.add(new SetCardInfo("Bone Miser", 87, Rarity.MYTHIC, mage.cards.b.BoneMiser.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Bone Miser", 97, Rarity.MYTHIC, mage.cards.b.BoneMiser.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Brazen Borrower", 30, Rarity.MYTHIC, mage.cards.b.BrazenBorrower.class));
cards.add(new SetCardInfo("Breeches, Brazen Plunderer", 6, Rarity.UNCOMMON, mage.cards.b.BreechesBrazenPlunderer.class));
cards.add(new SetCardInfo("Bridge from Below", 3, Rarity.RARE, mage.cards.b.BridgeFromBelow.class));
cards.add(new SetCardInfo("Carnage Tyrant", 10, Rarity.MYTHIC, mage.cards.c.CarnageTyrant.class));
+ cards.add(new SetCardInfo("Cavalier of Dawn", 84, Rarity.MYTHIC, mage.cards.c.CavalierOfDawn.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Cavalier of Dawn", 94, Rarity.MYTHIC, mage.cards.c.CavalierOfDawn.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Chandra's Ignition", 89, Rarity.MYTHIC, mage.cards.c.ChandrasIgnition.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Chandra's Ignition", 99, Rarity.MYTHIC, mage.cards.c.ChandrasIgnition.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Chrome Mox", 102, Rarity.MYTHIC, mage.cards.c.ChromeMox.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Chrome Mox", 92, Rarity.MYTHIC, mage.cards.c.ChromeMox.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Collected Company", 72, Rarity.MYTHIC, mage.cards.c.CollectedCompany.class));
+ cards.add(new SetCardInfo("Condemn", 74, Rarity.MYTHIC, mage.cards.c.Condemn.class));
cards.add(new SetCardInfo("Crashing Footfalls", 25, Rarity.MYTHIC, mage.cards.c.CrashingFootfalls.class));
+ cards.add(new SetCardInfo("Damnation", 68, Rarity.MYTHIC, mage.cards.d.Damnation.class));
cards.add(new SetCardInfo("Dargo, the Shipwrecker", 7, Rarity.UNCOMMON, mage.cards.d.DargoTheShipwrecker.class));
cards.add(new SetCardInfo("Desert", 37, Rarity.MYTHIC, mage.cards.d.Desert.class));
cards.add(new SetCardInfo("Desertion", 31, Rarity.MYTHIC, mage.cards.d.Desertion.class));
cards.add(new SetCardInfo("Dismember", 41, Rarity.MYTHIC, mage.cards.d.Dismember.class));
cards.add(new SetCardInfo("Drown in the Loch", 27, Rarity.MYTHIC, mage.cards.d.DrownInTheLoch.class));
+ cards.add(new SetCardInfo("Embercleave", 77, Rarity.MYTHIC, mage.cards.e.Embercleave.class));
cards.add(new SetCardInfo("Endurance", 48, Rarity.MYTHIC, mage.cards.e.Endurance.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Endurance", 53, Rarity.MYTHIC, mage.cards.e.Endurance.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Expressive Iteration", 43, Rarity.MYTHIC, mage.cards.e.ExpressiveIteration.class));
+ cards.add(new SetCardInfo("Expropriate", 66, Rarity.MYTHIC, mage.cards.e.Expropriate.class));
cards.add(new SetCardInfo("Fabricate", 20, Rarity.MYTHIC, mage.cards.f.Fabricate.class));
cards.add(new SetCardInfo("Field of the Dead", 28, Rarity.MYTHIC, mage.cards.f.FieldOfTheDead.class));
+ cards.add(new SetCardInfo("Fiend Artisan", 83, Rarity.MYTHIC, mage.cards.f.FiendArtisan.class));
cards.add(new SetCardInfo("Frogmite", 61, Rarity.MYTHIC, mage.cards.f.Frogmite.class));
cards.add(new SetCardInfo("Fury", 47, Rarity.MYTHIC, mage.cards.f.Fury.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Fury", 52, Rarity.MYTHIC, mage.cards.f.Fury.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Galvanic Blast", 100, Rarity.MYTHIC, mage.cards.g.GalvanicBlast.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Galvanic Blast", 90, Rarity.MYTHIC, mage.cards.g.GalvanicBlast.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Gamble", 24, Rarity.MYTHIC, mage.cards.g.Gamble.class));
cards.add(new SetCardInfo("Ghalta, Primal Hunger", 11, Rarity.RARE, mage.cards.g.GhaltaPrimalHunger.class));
cards.add(new SetCardInfo("Ghostly Prison", 19, Rarity.MYTHIC, mage.cards.g.GhostlyPrison.class));
+ cards.add(new SetCardInfo("Goblin Bushwhacker", 78, Rarity.MYTHIC, mage.cards.g.GoblinBushwhacker.class));
cards.add(new SetCardInfo("Grief", 46, Rarity.MYTHIC, mage.cards.g.Grief.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Grief", 51, Rarity.MYTHIC, mage.cards.g.Grief.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Grim Tutor", 76, Rarity.MYTHIC, mage.cards.g.GrimTutor.class));
+ cards.add(new SetCardInfo("Hallowed Haunting", 64, Rarity.MYTHIC, mage.cards.h.HallowedHaunting.class));
cards.add(new SetCardInfo("Kalamax, the Stormsire", 13, Rarity.MYTHIC, mage.cards.k.KalamaxTheStormsire.class));
cards.add(new SetCardInfo("Kindred Charge", 58, Rarity.MYTHIC, mage.cards.k.KindredCharge.class));
cards.add(new SetCardInfo("Ledger Shredder", 55, Rarity.MYTHIC, mage.cards.l.LedgerShredder.class));
cards.add(new SetCardInfo("Lord Windgrace", 14, Rarity.MYTHIC, mage.cards.l.LordWindgrace.class));
cards.add(new SetCardInfo("Lord of Atlantis", 1, Rarity.RARE, mage.cards.l.LordOfAtlantis.class));
+ cards.add(new SetCardInfo("Lord of the Undead", 88, Rarity.MYTHIC, mage.cards.l.LordOfTheUndead.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Lord of the Undead", 98, Rarity.MYTHIC, mage.cards.l.LordOfTheUndead.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Maddening Hex", 70, Rarity.MYTHIC, mage.cards.m.MaddeningHex.class));
cards.add(new SetCardInfo("Malcolm, Keen-Eyed Navigator", 2, Rarity.UNCOMMON, mage.cards.m.MalcolmKeenEyedNavigator.class));
cards.add(new SetCardInfo("Mana Crypt", "17a", Rarity.MYTHIC, mage.cards.m.ManaCrypt.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Mana Crypt", "17b", Rarity.MYTHIC, mage.cards.m.ManaCrypt.class, NON_FULL_USE_VARIOUS));
@@ -63,7 +86,12 @@ public final class SpecialGuests extends ExpansionSet {
cards.add(new SetCardInfo("Morbid Opportunist", 32, Rarity.MYTHIC, mage.cards.m.MorbidOpportunist.class));
cards.add(new SetCardInfo("Mystic Snake", 35, Rarity.MYTHIC, mage.cards.m.MysticSnake.class));
cards.add(new SetCardInfo("Notion Thief", 36, Rarity.MYTHIC, mage.cards.n.NotionThief.class));
+ cards.add(new SetCardInfo("Noxious Revival", 73, Rarity.MYTHIC, mage.cards.n.NoxiousRevival.class));
+ cards.add(new SetCardInfo("Paradise Druid", 80, Rarity.MYTHIC, mage.cards.p.ParadiseDruid.class));
+ cards.add(new SetCardInfo("Pathbreaker Ibex", 101, Rarity.MYTHIC, mage.cards.p.PathbreakerIbex.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Pathbreaker Ibex", 91, Rarity.MYTHIC, mage.cards.p.PathbreakerIbex.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Persist", 42, Rarity.MYTHIC, mage.cards.p.Persist.class));
+ cards.add(new SetCardInfo("Phantasmal Image", 67, Rarity.MYTHIC, mage.cards.p.PhantasmalImage.class));
cards.add(new SetCardInfo("Pitiless Plunderer", 5, Rarity.UNCOMMON, mage.cards.p.PitilessPlunderer.class));
cards.add(new SetCardInfo("Polyraptor", 12, Rarity.MYTHIC, mage.cards.p.Polyraptor.class));
cards.add(new SetCardInfo("Port Razer", 33, Rarity.MYTHIC, mage.cards.p.PortRazer.class));
@@ -72,11 +100,16 @@ public final class SpecialGuests extends ExpansionSet {
cards.add(new SetCardInfo("Rampaging Ferocidon", 8, Rarity.RARE, mage.cards.r.RampagingFerocidon.class));
cards.add(new SetCardInfo("Rat Colony", 56, Rarity.MYTHIC, mage.cards.r.RatColony.class));
cards.add(new SetCardInfo("Relentless Rats", 57, Rarity.MYTHIC, mage.cards.r.RelentlessRats.class));
+ cards.add(new SetCardInfo("Sacrifice", 69, Rarity.MYTHIC, mage.cards.s.Sacrifice.class));
cards.add(new SetCardInfo("Scapeshift", 34, Rarity.MYTHIC, mage.cards.s.Scapeshift.class));
cards.add(new SetCardInfo("Secluded Courtyard", 63, Rarity.MYTHIC, mage.cards.s.SecludedCourtyard.class));
cards.add(new SetCardInfo("Show and Tell", 21, Rarity.MYTHIC, mage.cards.s.ShowAndTell.class));
+ cards.add(new SetCardInfo("Skysovereign, Consul Flagship", 103, Rarity.MYTHIC, mage.cards.s.SkysovereignConsulFlagship.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Skysovereign, Consul Flagship", 93, Rarity.MYTHIC, mage.cards.s.SkysovereignConsulFlagship.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Solitude", 44, Rarity.MYTHIC, mage.cards.s.Solitude.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Solitude", 49, Rarity.MYTHIC, mage.cards.s.Solitude.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Soul Warden", 65, Rarity.MYTHIC, mage.cards.s.SoulWarden.class));
+ cards.add(new SetCardInfo("Sphinx's Tutelage", 75, Rarity.MYTHIC, mage.cards.s.SphinxsTutelage.class));
cards.add(new SetCardInfo("Star Compass", 18, Rarity.UNCOMMON, mage.cards.s.StarCompass.class));
cards.add(new SetCardInfo("Stoneforge Mystic", 29, Rarity.MYTHIC, mage.cards.s.StoneforgeMystic.class));
cards.add(new SetCardInfo("Subtlety", 45, Rarity.MYTHIC, mage.cards.s.Subtlety.class, NON_FULL_USE_VARIOUS));
@@ -84,12 +117,18 @@ public final class SpecialGuests extends ExpansionSet {
cards.add(new SetCardInfo("Sword of Fire and Ice", 62, Rarity.MYTHIC, mage.cards.s.SwordOfFireAndIce.class));
cards.add(new SetCardInfo("Swords to Plowshares", 54, Rarity.MYTHIC, mage.cards.s.SwordsToPlowshares.class));
cards.add(new SetCardInfo("Sylvan Tutor", 59, Rarity.MYTHIC, mage.cards.s.SylvanTutor.class));
+ cards.add(new SetCardInfo("Temporal Manipulation", 82, Rarity.MYTHIC, mage.cards.t.TemporalManipulation.class));
cards.add(new SetCardInfo("Thought-Knot Seer", 39, Rarity.MYTHIC, mage.cards.t.ThoughtKnotSeer.class));
+ cards.add(new SetCardInfo("Thoughtcast", 85, Rarity.MYTHIC, mage.cards.t.Thoughtcast.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Thoughtcast", 95, Rarity.MYTHIC, mage.cards.t.Thoughtcast.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Thrasios, Triton Hero", 16, Rarity.RARE, mage.cards.t.ThrasiosTritonHero.class));
cards.add(new SetCardInfo("Tireless Tracker", 26, Rarity.MYTHIC, mage.cards.t.TirelessTracker.class));
cards.add(new SetCardInfo("Toski, Bearer of Secrets", 60, Rarity.MYTHIC, mage.cards.t.ToskiBearerOfSecrets.class));
cards.add(new SetCardInfo("Tragic Slip", 22, Rarity.MYTHIC, mage.cards.t.TragicSlip.class));
cards.add(new SetCardInfo("Underworld Breach", 9, Rarity.RARE, mage.cards.u.UnderworldBreach.class));
+ cards.add(new SetCardInfo("Unholy Heat", 71, Rarity.MYTHIC, mage.cards.u.UnholyHeat.class));
cards.add(new SetCardInfo("Victimize", 23, Rarity.MYTHIC, mage.cards.v.Victimize.class));
+ cards.add(new SetCardInfo("Whir of Invention", 86, Rarity.MYTHIC, mage.cards.w.WhirOfInvention.class, NON_FULL_USE_VARIOUS));
+ cards.add(new SetCardInfo("Whir of Invention", 96, Rarity.MYTHIC, mage.cards.w.WhirOfInvention.class, NON_FULL_USE_VARIOUS));
}
}
diff --git a/Mage.Sets/src/mage/sets/Unfinity.java b/Mage.Sets/src/mage/sets/Unfinity.java
index d64beec852e..48366459648 100644
--- a/Mage.Sets/src/mage/sets/Unfinity.java
+++ b/Mage.Sets/src/mage/sets/Unfinity.java
@@ -16,10 +16,14 @@ public final class Unfinity extends ExpansionSet {
}
private Unfinity() {
- super("Unfinity", "UNF", ExpansionSet.buildDate(2022, 4, 1), SetType.JOKE_SET);
+ super("Unfinity", "UNF", ExpansionSet.buildDate(2022, 4, 1), SetType.SUPPLEMENTAL);
this.hasBasicLands = true;
this.hasBoosters = false; // un-set, low implemented cards
+ // set contains both legal and joke cards, so must use SetType.SUPPLEMENTAL:
+ // https://mtg.fandom.com/wiki/Unfinity
+ // The set is the first Un-set to include a mix of eternal-legal cards and acorn cards.
+
cards.add(new SetCardInfo("\"Name Sticker\" Goblin", "107m", Rarity.COMMON, mage.cards.n.NameStickerGoblin.class));
cards.add(new SetCardInfo("Atomwheel Acrobats", 130, Rarity.COMMON, mage.cards.a.AtomwheelAcrobats.class));
cards.add(new SetCardInfo("Attempted Murder", 66, Rarity.UNCOMMON, mage.cards.a.AttemptedMurder.class));
@@ -50,6 +54,7 @@ public final class Unfinity extends ExpansionSet {
cards.add(new SetCardInfo("Pair o' Dice Lost", 149, Rarity.UNCOMMON, mage.cards.p.PairODiceLost.class));
cards.add(new SetCardInfo("Plains", 235, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_UST_VARIOUS));
cards.add(new SetCardInfo("Plains", 240, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_UST_VARIOUS));
+ cards.add(new SetCardInfo("Priority Boarding", 119, Rarity.UNCOMMON, mage.cards.p.PriorityBoarding.class));
cards.add(new SetCardInfo("Sacred Foundry", 285, Rarity.RARE, mage.cards.s.SacredFoundry.class));
cards.add(new SetCardInfo("Saw in Half", 88, Rarity.RARE, mage.cards.s.SawInHalf.class));
cards.add(new SetCardInfo("Six-Sided Die", 92, Rarity.COMMON, mage.cards.s.SixSidedDie.class));
diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2021.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2021.java
new file mode 100644
index 00000000000..485b3300e04
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2021.java
@@ -0,0 +1,30 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/pw21
+ */
+public class WizardsPlayNetwork2021 extends ExpansionSet {
+
+ private static final WizardsPlayNetwork2021 instance = new WizardsPlayNetwork2021();
+
+ public static WizardsPlayNetwork2021 getInstance() {
+ return instance;
+ }
+
+ private WizardsPlayNetwork2021() {
+ super("Wizards Play Network 2021", "PW21", ExpansionSet.buildDate(2021, 6, 18), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("Arbor Elf", 1, Rarity.RARE, mage.cards.a.ArborElf.class));
+ cards.add(new SetCardInfo("Collected Company", 2, Rarity.RARE, mage.cards.c.CollectedCompany.class));
+ cards.add(new SetCardInfo("Conjurer's Closet", 6, Rarity.RARE, mage.cards.c.ConjurersCloset.class));
+ cards.add(new SetCardInfo("Fabled Passage", 4, Rarity.RARE, mage.cards.f.FabledPassage.class));
+ cards.add(new SetCardInfo("Mind Stone", 5, Rarity.RARE, mage.cards.m.MindStone.class));
+ cards.add(new SetCardInfo("Wurmcoil Engine", 3, Rarity.MYTHIC, mage.cards.w.WurmcoilEngine.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2022.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2022.java
new file mode 100644
index 00000000000..30ef4953e93
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2022.java
@@ -0,0 +1,30 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/pw22
+ */
+public class WizardsPlayNetwork2022 extends ExpansionSet {
+
+ private static final WizardsPlayNetwork2022 instance = new WizardsPlayNetwork2022();
+
+ public static WizardsPlayNetwork2022 getInstance() {
+ return instance;
+ }
+
+ private WizardsPlayNetwork2022() {
+ super("Wizards Play Network 2022", "PW22", ExpansionSet.buildDate(2022, 3, 5), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("Atsushi, the Blazing Sky", 3, Rarity.MYTHIC, mage.cards.a.AtsushiTheBlazingSky.class));
+ cards.add(new SetCardInfo("Consider", 1, Rarity.RARE, mage.cards.c.Consider.class));
+ cards.add(new SetCardInfo("Dismember", 5, Rarity.RARE, mage.cards.d.Dismember.class));
+ cards.add(new SetCardInfo("Fateful Absence", 2, Rarity.RARE, mage.cards.f.FatefulAbsence.class));
+ cards.add(new SetCardInfo("Psychosis Crawler", 6, Rarity.RARE, mage.cards.p.PsychosisCrawler.class));
+ cards.add(new SetCardInfo("Swiftfoot Boots", 4, Rarity.RARE, mage.cards.s.SwiftfootBoots.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2023.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2023.java
new file mode 100644
index 00000000000..58ff15b30b0
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2023.java
@@ -0,0 +1,35 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/pw23
+ */
+public class WizardsPlayNetwork2023 extends ExpansionSet {
+
+ private static final WizardsPlayNetwork2023 instance = new WizardsPlayNetwork2023();
+
+ public static WizardsPlayNetwork2023 getInstance() {
+ return instance;
+ }
+
+ private WizardsPlayNetwork2023() {
+ super("Wizards Play Network 2023", "PW23", ExpansionSet.buildDate(2023, 1, 1), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("Beast Within", 3, Rarity.RARE, mage.cards.b.BeastWithin.class));
+ cards.add(new SetCardInfo("Cultivate", 6, Rarity.RARE, mage.cards.c.Cultivate.class));
+ cards.add(new SetCardInfo("Drown in the Loch", 4, Rarity.RARE, mage.cards.d.DrownInTheLoch.class));
+ cards.add(new SetCardInfo("Ice Out", 7, Rarity.RARE, mage.cards.i.IceOut.class));
+ cards.add(new SetCardInfo("Lifecrafter's Bestiary", 2, Rarity.RARE, mage.cards.l.LifecraftersBestiary.class));
+ cards.add(new SetCardInfo("Norn's Annex", 1, Rarity.RARE, mage.cards.n.NornsAnnex.class));
+ cards.add(new SetCardInfo("Pyroblast", 8, Rarity.RARE, mage.cards.p.Pyroblast.class));
+ cards.add(new SetCardInfo("Rampant Growth", 9, Rarity.RARE, mage.cards.r.RampantGrowth.class));
+ cards.add(new SetCardInfo("Ravenous Chupacabra", 10, Rarity.RARE, mage.cards.r.RavenousChupacabra.class));
+ cards.add(new SetCardInfo("Syr Konrad, the Grim", 5, Rarity.RARE, mage.cards.s.SyrKonradTheGrim.class));
+ cards.add(new SetCardInfo("Unclaimed Territory", 11, Rarity.RARE, mage.cards.u.UnclaimedTerritory.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2024.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2024.java
new file mode 100644
index 00000000000..0d89ceb1de5
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2024.java
@@ -0,0 +1,42 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/pw24
+ */
+public class WizardsPlayNetwork2024 extends ExpansionSet {
+
+ private static final WizardsPlayNetwork2024 instance = new WizardsPlayNetwork2024();
+
+ public static WizardsPlayNetwork2024 getInstance() {
+ return instance;
+ }
+
+ private WizardsPlayNetwork2024() {
+ super("Wizards Play Network 2024", "PW24", ExpansionSet.buildDate(2024, 1, 1), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("Chaos Warp", 7, Rarity.RARE, mage.cards.c.ChaosWarp.class));
+ cards.add(new SetCardInfo("Commander's Sphere", 8, Rarity.RARE, mage.cards.c.CommandersSphere.class));
+ cards.add(new SetCardInfo("Costly Plunder", 14, Rarity.RARE, mage.cards.c.CostlyPlunder.class));
+ cards.add(new SetCardInfo("Crippling Fear", 17, Rarity.RARE, mage.cards.c.CripplingFear.class));
+ cards.add(new SetCardInfo("Darksteel Colossus", 19, Rarity.MYTHIC, mage.cards.d.DarksteelColossus.class));
+ cards.add(new SetCardInfo("Diabolic Tutor", 13, Rarity.RARE, mage.cards.d.DiabolicTutor.class));
+ cards.add(new SetCardInfo("Gaea's Liege", 5, Rarity.RARE, mage.cards.g.GaeasLiege.class));
+ cards.add(new SetCardInfo("Goblin King", 4, Rarity.RARE, mage.cards.g.GoblinKing.class));
+ cards.add(new SetCardInfo("Heirloom Blade", 16, Rarity.RARE, mage.cards.h.HeirloomBlade.class));
+ cards.add(new SetCardInfo("Lord of Atlantis", 2, Rarity.RARE, mage.cards.l.LordOfAtlantis.class));
+ cards.add(new SetCardInfo("Night's Whisper", 18, Rarity.RARE, mage.cards.n.NightsWhisper.class));
+ cards.add(new SetCardInfo("Oltec Matterweaver", 12, Rarity.MYTHIC, mage.cards.o.OltecMatterweaver.class));
+ cards.add(new SetCardInfo("Ravenous Squirrel", 15, Rarity.RARE, mage.cards.r.RavenousSquirrel.class));
+ cards.add(new SetCardInfo("Rogue's Passage", 10, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class));
+ cards.add(new SetCardInfo("Serra Angel", 1, Rarity.RARE, mage.cards.s.SerraAngel.class));
+ cards.add(new SetCardInfo("Transmutation Font", 11, Rarity.MYTHIC, mage.cards.t.TransmutationFont.class));
+ cards.add(new SetCardInfo("Underworld Connections", 9, Rarity.RARE, mage.cards.u.UnderworldConnections.class));
+ cards.add(new SetCardInfo("Zombie Master", 3, Rarity.RARE, mage.cards.z.ZombieMaster.class));
+ }
+}
diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java
new file mode 100644
index 00000000000..5d762e01f6c
--- /dev/null
+++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java
@@ -0,0 +1,25 @@
+package mage.sets;
+
+import mage.cards.ExpansionSet;
+import mage.constants.Rarity;
+import mage.constants.SetType;
+
+/**
+ * https://scryfall.com/sets/pw25
+ */
+public class WizardsPlayNetwork2025 extends ExpansionSet {
+
+ private static final WizardsPlayNetwork2025 instance = new WizardsPlayNetwork2025();
+
+ public static WizardsPlayNetwork2025 getInstance() {
+ return instance;
+ }
+
+ private WizardsPlayNetwork2025() {
+ super("Wizards Play Network 2025", "PW25", ExpansionSet.buildDate(2025, 1, 1), SetType.PROMOTIONAL);
+ this.hasBoosters = false;
+ this.hasBasicLands = false;
+
+ cards.add(new SetCardInfo("Rishkar's Expertise", 1, Rarity.RARE, mage.cards.r.RishkarsExpertise.class));
+ }
+}
diff --git a/Mage.Tests/pom.xml b/Mage.Tests/pom.xml
index 76334a3fa9e..feba3cf97ca 100644
--- a/Mage.Tests/pom.xml
+++ b/Mage.Tests/pom.xml
@@ -6,7 +6,7 @@
org.mage
mage-root
- 1.4.55
+ 1.4.56
mage-tests
diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java
index 624a26a3341..af1e99a689e 100644
--- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java
@@ -6,6 +6,22 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
/**
+ * Combat's blocking tests
+ *
+ * TODO: add tests with multi blocker requirement effects
+ *
+ * Supported abilities:
+ * - DeathtouchAbility - supported, has tests
+ * - FirstStrikeAbility - supported, has tests
+ * - DoubleStrikeAbility - ?
+ * - IndestructibleAbility - supported, need tests
+ * - FlyingAbility - ?
+ * - ReachAbility - ?
+ * - ExaltedAbility - ?
+ * - Trample + Deathtouch
+ * - combat damage and die triggers - need to implement full combat simulation with stack resolve, see CombatUtil->willItSurviveSimple
+ * - other use cases, see https://github.com/magefree/mage/issues/4485
+ *
* @author JayDi85
*/
public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
@@ -52,7 +68,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
}
@Test
- public void test_Block_1_small_attacker_vs_1_small_blocker() {
+ public void test_Block_1_small_attacker_vs_1_small_blocker_same() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
@@ -72,6 +88,54 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
assertGraveyardCount(playerB, "Arbor Elf", 1);
}
+ @Test
+ public void test_Block_1_small_attacker_vs_1_small_blocker_better() {
+ addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
+ //addCard(Zone.BATTLEFIELD, playerB, "Elvish Archers"); // 2/1 first strike
+ addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1
+
+ attack(1, playerA, "Arbor Elf");
+
+ // ai must ignore block to keep better creature alive
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkBlockers("no blockers", 1, playerB, "");
+
+ setStopAt(1, PhaseStep.END_TURN);
+ setStrictChooseMode(true);
+ execute();
+
+ assertLife(playerA, 20);
+ assertLife(playerB, 20 - 1);
+ assertPermanentCount(playerA, "Arbor Elf", 1);
+ assertPermanentCount(playerB, "Dregscape Zombie", 1);
+ }
+
+ @Test
+ public void test_Block_1_small_attacker_vs_1_small_blocker_better_but_player_die() {
+ addCustomEffect_TargetDamage(playerA, 19);
+
+ addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
+ addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1
+
+ // prepare 1 life
+ activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target damage 19", playerB);
+
+ attack(1, playerA, "Arbor Elf");
+
+ // ai must keep better blocker in normal case, but now it must protect from lose and sacrifice it
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkBlockers("x1 blocker", 1, playerB, "Dregscape Zombie");
+
+ setStopAt(1, PhaseStep.END_TURN);
+ setStrictChooseMode(true);
+ execute();
+
+ assertLife(playerA, 20);
+ assertLife(playerB, 1);
+ assertGraveyardCount(playerA, "Arbor Elf", 1);
+ assertGraveyardCount(playerB, "Dregscape Zombie", 1);
+ }
+
@Test
public void test_Block_1_big_attacker_vs_1_small_blocker() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
@@ -194,12 +258,55 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
assertDamageReceived(playerB, "Spectral Bears", 2);
}
- // TODO: add tests with multi blocker requirement effects
- // TODO: add tests for DeathtouchAbility
- // TODO: add tests for FirstStrikeAbility
- // TODO: add tests for DoubleStrikeAbility
- // TODO: add tests for IndestructibleAbility
- // TODO: add tests for FlyingAbility
- // TODO: add tests for ReachAbility
- // TODO: add tests for ExaltedAbility???
+ @Test
+ public void test_Block_1_attacker_vs_first_strike() {
+ addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
+ addCard(Zone.BATTLEFIELD, playerB, "White Knight", 1); // 2/2 with first strike
+ addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
+ addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
+ addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6
+
+ attack(1, playerA, "Balduvian Bears");
+
+ // ai must use smaller blocker and survive (2/2 with first strike must block 2/2)
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkBlockers("x1 optimal blocker", 1, playerB, "White Knight");
+
+ setStopAt(1, PhaseStep.END_TURN);
+ setStrictChooseMode(true);
+ execute();
+
+ assertLife(playerA, 20);
+ assertLife(playerB, 20);
+ assertGraveyardCount(playerA, "Balduvian Bears", 1);
+ assertDamageReceived(playerB, "White Knight", 0); // due first strike
+ }
+
+ @Test
+ public void test_Block_1_attacker_vs_deathtouch() {
+ addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
+ addCard(Zone.BATTLEFIELD, playerB, "Arashin Cleric", 1); // 1/3
+ addCard(Zone.BATTLEFIELD, playerB, "Graveblade Marauder", 1); // 1/4 with deathtouch
+ addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
+ addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6
+
+ attack(1, playerA, "Balduvian Bears");
+
+ // ai must use smaller blocker to kill and survive (1/4 with deathtouch must block 2/2 -- not a smaller 1/3)
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkBlockers("x1 optimal blocker", 1, playerB, "Graveblade Marauder");
+
+ setStopAt(1, PhaseStep.END_TURN);
+ setStrictChooseMode(true);
+ execute();
+
+ assertLife(playerA, 20);
+ assertLife(playerB, 20);
+ assertGraveyardCount(playerA, "Balduvian Bears", 1);
+ assertDamageReceived(playerB, "Graveblade Marauder", 2);
+ }
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java
index 45db260750a..c8ce253a707 100644
--- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java
@@ -1,8 +1,12 @@
package org.mage.test.AI.basic;
+import mage.abilities.Ability;
+import mage.abilities.common.SimpleStaticAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
+import mage.game.permanent.Permanent;
import org.junit.Assert;
+import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseAI;
@@ -10,6 +14,15 @@ import java.util.Arrays;
import java.util.List;
/**
+ * Possible problems:
+ * - big memory consumption on sims prepare (memory overflow)
+ * - too many sims to calculate (AI fail on time out and do nothing)
+ *
+ * TODO: add tests and implement best choice selection on timeout
+ * (AI must make any good/bad choice on timeout with game log - not a skip)
+ *
+ * TODO: AI do not support game simulations for target options in triggered
+ *
* @author JayDi85
*/
public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
@@ -20,7 +33,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
}
@Test
- public void test_AIvsAI_Simple() {
+ public void test_Simple_ShortGame() {
// both must kill x2 bears by x2 bolts
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2);
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 2);
@@ -38,7 +51,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
}
@Test
- public void test_AIvsAI_LongGame() {
+ public void test_Simple_LongGame() {
// many bears and bolts must help to end game fast
int maxTurn = 50;
removeAllCardsFromLibrary(playerA);
@@ -61,4 +74,143 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
Assert.assertTrue("One of player must won a game before turn " + maxTurn + ", but it ends on " + currentGame, currentGame.hasEnded());
}
+
+ private void runManyTargetOptionsInTrigger(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) {
+ // When Bogardan Hellkite enters, it deals 5 damage divided as you choose among any number of targets.
+ addCard(Zone.HAND, playerA, "Bogardan Hellkite", 1); // {6}{R}{R}
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures);
+
+ if (isDamageRandomCreature) {
+ runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> {
+ Permanent creature = game.getBattlefield().getAllPermanents().stream()
+ .filter(p -> p.getName().equals("Balduvian Bears"))
+ .findAny()
+ .orElse(null);
+ Assert.assertNotNull(creature);
+ Ability fakeAbility = new SimpleStaticAbility(null);
+ fakeAbility.setControllerId(player.getId());
+ fakeAbility.setSourceId(creature.getId());
+ creature.damage(1, fakeAbility, game);
+ });
+ }
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.BEGIN_COMBAT);
+ execute();
+
+ assertPermanentCount(playerA, "Bogardan Hellkite", 1); // if fail then AI stops before all sims ends
+ assertGraveyardCount(playerB, "Balduvian Bears", needDiedCreatures);
+ assertLife(playerB, needPlayerLife);
+ }
+
+ @Test
+ @Ignore // enable after triggered supported or need performance test
+ public void test_ManyTargetOptions_Triggered_Single() {
+ // 2 damage to bear and 3 damage to player B
+ runManyTargetOptionsInTrigger("1 target creature", 1, 1, false, 20 - 3);
+ }
+
+ @Test
+ @Ignore // enable after triggered supported or need performance test
+ public void test_ManyTargetOptions_Triggered_Few() {
+ // 4 damage to x2 bears and 1 damage to player B
+ runManyTargetOptionsInTrigger("2 target creatures", 2, 2, false, 20 - 1);
+ }
+
+ @Test
+ @Ignore // enable after triggered supported or need performance test
+ public void test_ManyTargetOptions_Triggered_Many() {
+ // 4 damage to x2 bears and 1 damage to player B
+ runManyTargetOptionsInTrigger("5 target creatures", 5, 2, false, 20 - 1);
+ }
+
+ @Test
+ @Ignore // enable after triggered supported or need performance test
+ public void test_ManyTargetOptions_Triggered_TooMuch() {
+ // warning, can be slow
+
+ // make sure targets optimization works
+ // (must ignore same targets for faster calc)
+
+ // 4 damage to x2 bears and 1 damage to player B
+ runManyTargetOptionsInTrigger("50 target creatures", 50, 2, false, 20 - 1);
+ }
+
+ @Test
+ @Ignore // enable after triggered supported or need performance test
+ public void test_ManyTargetOptions_Triggered_TargetGroups() {
+ // make sure targets optimization can find unique creatures, e.g. damaged
+
+ // 4 damage to x2 bears and 1 damage to damaged bear
+ runManyTargetOptionsInTrigger("5 target creatures with one damaged", 5, 3, true, 20);
+ }
+
+ private void runManyTargetOptionsInActivate(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) {
+ // Boulderfall deals 5 damage divided as you choose among any number of targets.
+ addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R}
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures);
+
+ if (isDamageRandomCreature) {
+ runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> {
+ Permanent creature = game.getBattlefield().getAllPermanents().stream()
+ .filter(p -> p.getName().equals("Balduvian Bears"))
+ .findAny()
+ .orElse(null);
+ Assert.assertNotNull(creature);
+ Ability fakeAbility = new SimpleStaticAbility(null);
+ fakeAbility.setControllerId(player.getId());
+ fakeAbility.setSourceId(creature.getId());
+ creature.damage(1, fakeAbility, game);
+ });
+ }
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.BEGIN_COMBAT);
+ execute();
+
+ assertGraveyardCount(playerA, "Boulderfall", 1); // if fail then AI stops before all sims ends
+ assertGraveyardCount(playerB, "Balduvian Bears", needDiedCreatures);
+ assertLife(playerB, needPlayerLife);
+ }
+
+ @Test
+ public void test_ManyTargetOptions_Activated_Single() {
+ // 2 damage to bear and 3 damage to player B
+ runManyTargetOptionsInActivate("1 target creature", 1, 1, false, 20 - 3);
+ }
+
+ @Test
+ public void test_ManyTargetOptions_Activated_Few() {
+ // 4 damage to x2 bears and 1 damage to player B
+ runManyTargetOptionsInActivate("2 target creatures", 2, 2, false, 20 - 1);
+ }
+
+ @Test
+ public void test_ManyTargetOptions_Activated_Many() {
+ // 4 damage to x2 bears and 1 damage to player B
+ runManyTargetOptionsInActivate("5 target creatures", 5, 2, false, 20 - 1);
+ }
+
+ @Test
+ public void test_ManyTargetOptions_Activated_TooMuch() {
+ // warning, can be slow
+
+ // make sure targets optimization works
+ // (must ignore same targets for faster calc)
+
+ // 4 damage to x2 bears and 1 damage to player B
+ runManyTargetOptionsInActivate("50 target creatures", 50, 2, false, 20 - 1);
+ }
+
+ @Test
+ public void test_ManyTargetOptions_Activated_TargetGroups() {
+ // make sure targets optimization can find unique creatures, e.g. damaged
+
+ // 4 damage to x2 bears and 1 damage to damaged bear
+ runManyTargetOptionsInActivate("5 target creatures with one damaged", 5, 3, true, 20);
+ }
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java
index cb24db3af3d..51b5efea6c6 100644
--- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java
@@ -31,6 +31,8 @@ public class TargetAmountAITest extends CardTestPlayerBaseWithAIHelps {
@Test
public void test_AI_SimulateTargets() {
+ // warning, test depends on targets list optimization by AI
+
// Distribute four +1/+1 counters among any number of target creatures.
addCard(Zone.HAND, playerA, "Blessings of Nature", 1); // {4}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java
new file mode 100644
index 00000000000..2c4f6caf823
--- /dev/null
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java
@@ -0,0 +1,202 @@
+package org.mage.test.cards.designations;
+
+import mage.constants.PhaseStep;
+import mage.constants.Zone;
+import mage.players.Player;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mage.test.serverside.base.CardTestPlayerBase;
+
+/**
+ * @author TheElk801
+ */
+public class StartYourEnginesTest extends CardTestPlayerBase {
+
+ private static final String sarcophagus = "Walking Sarcophagus";
+
+ private void assertSpeed(Player player, int speed) {
+ Assert.assertEquals(player.getName() + " speed should be " + speed, speed, player.getSpeed());
+ }
+
+ @Test
+ public void testRegular() {
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 0);
+ assertSpeed(playerB, 0);
+ }
+
+ @Test
+ public void testSpeed1() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
+ addCard(Zone.HAND, playerA, sarcophagus);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus);
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 1);
+ assertSpeed(playerB, 0);
+ assertPowerToughness(playerA, sarcophagus, 2, 1);
+ }
+
+ private static final String goblet = "Onyx Goblet";
+
+ @Test
+ public void testSpeed2() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
+ addCard(Zone.BATTLEFIELD, playerA, goblet);
+ addCard(Zone.HAND, playerA, sarcophagus);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus);
+
+ activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 2);
+ assertSpeed(playerB, 0);
+ assertPowerToughness(playerA, sarcophagus, 2, 1);
+ }
+
+ @Test
+ public void testSpeed1OppTurn() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
+ addCard(Zone.BATTLEFIELD, playerA, goblet);
+ addCard(Zone.HAND, playerA, sarcophagus);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus);
+
+ activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ setStrictChooseMode(true);
+ setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 1);
+ assertSpeed(playerB, 0);
+ assertPowerToughness(playerA, sarcophagus, 2, 1);
+ }
+
+ @Test
+ public void testSpeed3() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
+ addCard(Zone.BATTLEFIELD, playerA, goblet);
+ addCard(Zone.HAND, playerA, sarcophagus);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus);
+
+ activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ setStrictChooseMode(true);
+ setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 3);
+ assertSpeed(playerB, 0);
+ assertPowerToughness(playerA, sarcophagus, 2, 1);
+ }
+
+ @Test
+ public void testSpeed4() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
+ addCard(Zone.BATTLEFIELD, playerA, goblet);
+ addCard(Zone.HAND, playerA, sarcophagus);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus);
+
+ activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ setStrictChooseMode(true);
+ setStopAt(5, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 4);
+ assertSpeed(playerB, 0);
+ assertPowerToughness(playerA, sarcophagus, 2 + 1, 1 + 2);
+ }
+
+ @Test
+ public void testSpeed5() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
+ addCard(Zone.BATTLEFIELD, playerA, goblet);
+ addCard(Zone.HAND, playerA, sarcophagus);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus);
+
+ activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ activateAbility(7, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB);
+
+ setStrictChooseMode(true);
+ setStopAt(7, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 4);
+ assertSpeed(playerB, 0);
+ assertPowerToughness(playerA, sarcophagus, 2 + 1, 1 + 2);
+ }
+
+ private static final String surveyor = "Loxodon Surveyor";
+
+ @Test
+ public void testSpeed4Graveyard() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 3);
+ addCard(Zone.GRAVEYARD, playerA, surveyor);
+
+ runCode("Increase player speed", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
+ player.initSpeed(game);
+ player.increaseSpeed(game);
+ player.increaseSpeed(game);
+ player.increaseSpeed(game);
+ });
+ activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{3}");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 4);
+ assertSpeed(playerB, 0);
+ assertGraveyardCount(playerA, surveyor, 0);
+ assertExileCount(playerA, surveyor, 1);
+ }
+
+ private static final String mindControl = "Mind Control";
+
+ @Test
+ public void testSpeedChangeControl() {
+ addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2);
+ addCard(Zone.BATTLEFIELD, playerB, "Island", 5);
+ addCard(Zone.HAND, playerA, sarcophagus);
+ addCard(Zone.HAND, playerB, mindControl);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus);
+
+ castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, mindControl, sarcophagus);
+
+ setStrictChooseMode(true);
+ setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertSpeed(playerA, 1);
+ assertSpeed(playerB, 1);
+ assertPowerToughness(playerB, sarcophagus, 2, 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 52f24ce1acf..b8cb151d5e8 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
@@ -20,31 +20,31 @@ 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");
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite");
setChoice(playerA, "X=5");
-
+
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
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);
+ assertManaOptions("{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}[{ActivatedAbilityManaCondition}]", 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.
@@ -54,54 +54,54 @@ public class CrypticTrilobiteTest extends CardTestPlayerBase {
// 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", true);
setChoice(playerA, "X=5");
-
+
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:");
-
+
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
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);
- }
-
+ assertManaOptions("{C}{C}{C}{C}{C}{C}[{ActivatedAbilityManaCondition}]", 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();
assertPermanentCount(playerA, "Cryptic Trilobite", 1);
- assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5);
- }
-
-
-}
\ No newline at end of file
+ assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5);
+ }
+
+
+}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java
index 90b80d04a13..39c9bb6886a 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java
@@ -151,4 +151,24 @@ public class ShiftingWoodlandTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 3 - 1);
assertGraveyardCount(playerA, woodland, 1);
}
+
+ @Test
+ public void test_Copy_MDFC() {
+ setStrictChooseMode(true);
+
+ addCard(Zone.BATTLEFIELD, playerA, "Yavimaya Coast", 4); // to be sure not to activate Woodland
+ addCard(Zone.BATTLEFIELD, playerA, woodland);
+ addCard(Zone.GRAVEYARD, playerA, "Drowner of Truth");
+ addCard(Zone.GRAVEYARD, playerA, "Plains");
+ addCard(Zone.GRAVEYARD, playerA, "Memnite");
+ addCard(Zone.GRAVEYARD, playerA, "Divination");
+
+ activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}. {this} deals 1 damage to you.", 4);
+ activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Delirium — {2}{G}{G}:", "Drowner of Truth");
+
+ setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
+ execute();
+
+ assertPermanentCount(playerA, "Drowner of Truth", 1);
+ }
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java
new file mode 100644
index 00000000000..115fcb99474
--- /dev/null
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java
@@ -0,0 +1,127 @@
+package org.mage.test.cards.single.pip;
+
+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.CardTestCommander4Players;
+
+/**
+ * @author JayDi85
+ */
+public class StruggleForProjectPurityTest extends CardTestCommander4Players {
+
+ /**
+ * {@link mage.cards.s.StruggleForProjectPurity Struggle for Project Purity}
+ * {3}{U}
+ * Enchantment
+ * As Struggle for Project Purity enters, choose Brotherhood or Enclave.
+ * • Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way.
+ * • Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters.
+ */
+ private static final String struggle = "Struggle for Project Purity";
+
+ private void checkRadCounters(String info, int needA, int needB, int needC, int needD) {
+ Assert.assertEquals(info + ", rad counter on playerA", needA, playerA.getCountersCount(CounterType.RAD));
+ Assert.assertEquals(info + ", rad counter on playerB", needB, playerB.getCountersCount(CounterType.RAD));
+ Assert.assertEquals(info + ", rad counter on playerC", needC, playerC.getCountersCount(CounterType.RAD));
+ Assert.assertEquals(info + ", rad counter on playerD", needD, playerD.getCountersCount(CounterType.RAD));
+ }
+
+ @Test
+ public void test_Brotherhood() {
+ // Player order: A -> D -> C -> B
+
+ // Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way.
+ addCard(Zone.HAND, playerA, struggle);
+ addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
+
+ checkHandCount("before cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // struggle + starting draw
+
+ // turn 1 - A - prepare brotherhood, no triggers
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, struggle);
+ setChoice(playerA, "Brotherhood");
+ waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
+
+ // turn 2 - D - no triggers
+ checkHandCount("no draws on turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerA, 1);
+
+ // turn 3 - C - no triggers
+ checkHandCount("no draws on turn 3", 3, PhaseStep.PRECOMBAT_MAIN, playerA, 1);
+
+ // turn 4 - B - no triggers
+ checkHandCount("no draws on turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerA, 1);
+
+ // turn 5 - A - trigger
+ // opponent draw: +1
+ // you draw: +3
+ checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerA, 1 + 1 + 3); // draw 1 + draw 5 + draw trigger
+ checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerB, 2); // opponent turn + trigger
+ checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerC, 2); // opponent turn + trigger
+ checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerD, 2); // opponent turn + trigger
+
+ setStrictChooseMode(true);
+ setStopAt(5, PhaseStep.BEGIN_COMBAT);
+ execute();
+
+ assertPermanentCount(playerA, struggle, 1);
+ }
+
+ @Test
+ public void test_Enclave() {
+ // Player order: A -> D -> C -> B
+
+ // Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters.
+ addCard(Zone.HAND, playerA, struggle);
+ addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
+ //
+ addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2);
+ addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 2);
+ addCard(Zone.BATTLEFIELD, playerC, "Grizzly Bears", 2);
+ addCard(Zone.BATTLEFIELD, playerD, "Grizzly Bears", 2);
+
+ checkHandCount("before cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // struggle + starting draw
+ runCode("rad count playerA turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 0));
+
+ // turn 1
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, struggle);
+ setChoice(playerA, "Enclave");
+ waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
+
+ // turn 1
+ attack(1, playerA, "Grizzly Bears", playerD);
+ attack(1, playerA, "Grizzly Bears", playerD);
+ runCode("A attacked D on turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 0));
+
+ // turn 2
+ attack(2, playerD, "Grizzly Bears", playerA); // <<< trigger for D
+ attack(2, playerD, "Grizzly Bears", playerA);
+ runCode("D attacked A on turn 2", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 2 * 2));
+
+ // turn 3
+ attack(3, playerC, "Grizzly Bears", playerB);
+ attack(3, playerC, "Grizzly Bears", playerB);
+ runCode("B attacked B on turn 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 2 * 2));
+
+ // turn 4
+ attack(4, playerB, "Grizzly Bears", playerA); // <<< trigger for B
+ attack(4, playerB, "Grizzly Bears", playerA);
+ runCode("B attacked A on turn 4", 4, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 2 * 2, 0, 2 * 2));
+
+ // turn 5
+ attack(5, playerA, "Grizzly Bears", playerD);
+ attack(5, playerA, "Grizzly Bears", playerD);
+ runCode("A attacked D on turn 5", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 2 * 2, 0, 2 * 2));
+
+ setStrictChooseMode(true);
+ setStopAt(5, PhaseStep.END_TURN);
+ execute();
+
+ assertPermanentCount(playerA, struggle, 1);
+ assertLife(playerA, 20 - 2 * 2 - 2 * 2); // from D and B
+ assertLife(playerB, 20 - 2 * 2); // from C
+ assertLife(playerC, 20); // no attackers
+ assertLife(playerD, 20 - 2 * 2 - 2 * 2); // from A and A
+ }
+}
diff --git a/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java b/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java
index e60aa31d41c..a43818f1c90 100644
--- a/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java
@@ -7,7 +7,7 @@ import mage.game.permanent.Permanent;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
-import org.mage.test.serverside.base.CardTestPlayerBase;
+import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -17,7 +17,7 @@ import static org.junit.Assert.fail;
*
* @author noxx, JayDi85
*/
-public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
+public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
@Test
public void testFlyingVsNonFlying() {
@@ -571,7 +571,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
}
@Test
- public void test_MustBeBlocked_nothing() {
+ public void test_MustBeBlocked_nothing_human() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
@@ -587,12 +587,30 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
}
@Test
- public void test_MustBeBlocked_1_blocker() {
+ public void test_MustBeBlocked_nothing_AI() {
+ // Fear of Being Hunted must be blocked if able.
+ addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
+
+ attack(1, playerA, "Fear of Being Hunted");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
+ checkBlockers("no blocker", 1, playerB, "");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertLife(playerB, 20 - 4);
+ }
+
+ @Test
+ public void test_MustBeBlocked_1_blocker_human() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
+ // auto-choose blocker
attack(1, playerA, "Fear of Being Hunted");
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
@@ -606,15 +624,36 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
}
@Test
- public void test_MustBeBlocked_many_blockers_good() {
+ public void test_MustBeBlocked_1_blocker_AI() {
+ // Fear of Being Hunted must be blocked if able.
+ addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
+
+ // auto-choose blocker with AI
+ attack(1, playerA, "Fear of Being Hunted");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
+ checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertLife(playerB, 20);
+ assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
+ }
+
+ @Test
+ public void test_MustBeBlocked_many_blockers_good_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 10); // 3/3
- // TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
- // take first available blocker
+ // ai must choose any bear
attack(1, playerA, "Fear of Being Hunted");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears");
@@ -627,15 +666,15 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
}
@Test
- public void test_MustBeBlocked_many_blockers_bad() {
+ public void test_MustBeBlocked_many_blockers_bad_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
- // TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
- // take first available blocker
+ // ai don't want but must choose any bad memnite
attack(1, playerA, "Fear of Being Hunted");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Memnite");
@@ -645,12 +684,11 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
assertLife(playerB, 20);
assertPermanentCount(playerA, "Fear of Being Hunted", 1);
+ assertGraveyardCount(playerB, "Memnite", 1); // x1 blocker die
}
@Test
- @Ignore
- // TODO: enable and duplicate for AI -- after implement choose blocker logic and isHuman replace by ~isComputer
- public void test_MustBeBlocked_many_blockers_optimal() {
+ public void test_MustBeBlocked_many_blockers_optimal_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
@@ -659,7 +697,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
+ // ai must choose optimal creature to kill but survive
attack(1, playerA, "Fear of Being Hunted");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Deadbridge Goliath");
@@ -669,6 +709,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
+ assertGraveyardCount(playerB, 0);
}
@Test
@@ -697,7 +738,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
}
@Test
- public void test_MustBlocking_full_blockers() {
+ public void test_MustBlocking_full_blockers_human() {
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
@@ -725,7 +766,37 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
}
@Test
- public void test_MustBlocking_many_blockers() {
+ public void test_MustBlocking_full_blockers_AI() {
+ // All creatures able to block target creature this turn do so.
+ addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
+ //
+ // Menace
+ // Each creature you control with menace can't be blocked except by three or more creatures.
+ addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Memnite", 3); // 1/1
+
+ // prepare
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
+
+ // ai must choose all blockers anyway
+ attack(1, playerA, "Sonorous Howlbonder");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
+ checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
+ checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertLife(playerB, 20);
+ assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
+ }
+
+ @Test
+ public void test_MustBlocking_many_blockers_human() {
// possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
// All creatures able to block target creature this turn do so.
@@ -754,6 +825,38 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
}
+ @Test
+ public void test_MustBlocking_many_blockers_AI() {
+ // possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
+
+ // All creatures able to block target creature this turn do so.
+ addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
+ //
+ // Menace
+ // Each creature you control with menace can't be blocked except by three or more creatures.
+ addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Memnite", 5); // 1/1
+
+ // prepare
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
+
+ // ai must choose all blockers
+ attack(1, playerA, "Sonorous Howlbonder");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
+ checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
+ checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertLife(playerB, 20);
+ assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
+ }
+
@Test
@Ignore
// TODO: need exception fix - java.lang.UnsupportedOperationException: Sonorous Howlbonder is blocked by 1 creature(s). It has to be blocked by 3 or more.
@@ -814,6 +917,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
@Test
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
+ // looks like it's impossible for auto-fix (it's can remove blocker, but not add)
public void test_MustBeBlockedWithMenace_all_blockers() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
// power until end of turn. That creature must be blocked this combat if able.
@@ -844,7 +948,8 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
@Test
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
- public void test_MustBeBlockedWithMenace_many_blockers() {
+ // looks like it's impossible for auto-fix (it's can remove blocker, but not add)
+ public void test_MustBeBlockedWithMenace_many_low_blockers_human() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
@@ -872,6 +977,42 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
+ @Test
+ @Ignore // TODO: blockWithGoodTrade2 must support additional restrictions
+ public void test_MustBeBlockedWithMenace_many_low_blockers_AI() {
+ // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
+ // power until end of turn. That creature must be blocked this combat if able.
+ addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
+ //
+ // Menace
+ addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
+
+ // If the target creature has menace, two creatures must block it if able.
+ // (2020-06-23)
+
+ // AI must be forced to choose min x2 low blockers (it's possible to fail here after AI logic improve someday)
+
+ addTarget(playerA, "Alley Strangler"); // boost target
+ setChoice(playerA, true); // boost target
+ attack(1, playerA, "Alley Strangler");
+ setChoiceAmount(playerA, 1); // assign damage to 1 of 2 blocking memnites
+ setChoiceAmount(playerA, 1); // assign damage to 2 of 2 blocking memnites
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
+ checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertLife(playerB, 20);
+ assertGraveyardCount(playerA, "Alley Strangler", 0);
+ assertGraveyardCount(playerB, "Memnite", 2); // x2 blockers must die
+ }
+
@Test
public void test_MustBeBlockedWithMenace_low_blockers_auto() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
@@ -946,4 +1087,80 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 4);
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
+
+ @Test
+ public void test_MustBeBlockedWithMenace_low_small_blockers_AI() {
+ // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
+ // power until end of turn. That creature must be blocked this combat if able.
+ addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
+ //
+ // Menace
+ addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // 1/1
+
+ // If the target creature has menace, two creatures must block it if able.
+ // (2020-06-23)
+ //
+ // If a creature is required to block a creature with menace, another creature must also block that creature
+ // if able. If none can, the creature that’s required to block can block another creature or not block at all.
+ // (2020-04-17)
+
+ // auto-fix block config inside
+ // AI must ignore such use case
+
+ addTarget(playerA, "Alley Strangler"); // boost target
+ setChoice(playerA, true); // boost target
+ attack(1, playerA, "Alley Strangler");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
+ checkBlockers("no blockers", 1, playerB, "");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertLife(playerB, 20 - 4);
+ assertGraveyardCount(playerA, "Alley Strangler", 0);
+ }
+
+ @Test
+ public void test_MustBeBlockedWithMenace_low_big_blockers_AI() {
+ // bug: #13290, AI can try to use bigger creature to block
+
+ // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
+ // power until end of turn. That creature must be blocked this combat if able.
+ addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
+ //
+ // Menace
+ addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
+
+ // If the target creature has menace, two creatures must block it if able.
+ // (2020-06-23)
+ //
+ // If a creature is required to block a creature with menace, another creature must also block that creature
+ // if able. If none can, the creature that’s required to block can block another creature or not block at all.
+ // (2020-04-17)
+
+ // auto-fix block config inside
+ // AI must ignore BIG creature to wrongly block
+
+ addTarget(playerA, "Alley Strangler"); // boost target
+ setChoice(playerA, true); // boost target
+ attack(1, playerA, "Alley Strangler");
+ aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
+ checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
+ checkBlockers("no blockers", 1, playerB, "");
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertLife(playerB, 20 - 4);
+ assertGraveyardCount(playerA, "Alley Strangler", 0);
+ }
}
\ No newline at end of file
diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java
index e69ee664e21..35046809512 100644
--- a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java
+++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java
@@ -7,6 +7,7 @@ import mage.remote.Session;
import mage.view.*;
import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
+import org.junit.Assert;
import java.util.List;
import java.util.UUID;
@@ -33,6 +34,7 @@ public class LoadCallbackClient implements CallbackClient {
private final String logsPrefix;
private final Boolean showLogsAsHtml; // original game logs in HTML, but it can be converted to txt for more readable console
+ private String globalProgress = ""; // example: progress 33% [20.cd, 21.__, 17.__], AI game #09: ---
public LoadCallbackClient(boolean joinGameChat, String logsPrefix, Boolean showLogsAsHtml) {
this.joinGameChat = joinGameChat;
@@ -40,6 +42,10 @@ public class LoadCallbackClient implements CallbackClient {
this.showLogsAsHtml = showLogsAsHtml;
}
+ protected void updateGlobalProgress(String globalProgress) {
+ this.globalProgress = globalProgress;
+ }
+
@Override
public void onNewConnection() {
// nothing to do, only one time connection for LoadClient
@@ -69,6 +75,12 @@ public class LoadCallbackClient implements CallbackClient {
}
break;
+ case GAME_UPDATE:
+ GameView newGameView = (GameView) callback.getData();
+ Assert.assertNotNull("game update event must return game view object", newGameView);
+ this.gameView = newGameView;
+ break;
+
case CHATMESSAGE: {
ChatMessage message = (ChatMessage) callback.getData();
String mes = this.showLogsAsHtml ? message.getMessage() : Jsoup.parse(message.getMessage()).text();
@@ -89,7 +101,7 @@ public class LoadCallbackClient implements CallbackClient {
case GAME_UPDATE_AND_INFORM:
case GAME_INFORM_PERSONAL: {
GameClientMessage message = (GameClientMessage) callback.getData();
- gameView = message.getGameView();
+ this.gameView = message.getGameView();
// ignore play priority log
break;
}
@@ -169,7 +181,6 @@ public class LoadCallbackClient implements CallbackClient {
break;
// skip callbacks (no need to react)
- case GAME_UPDATE:
case JOINED_TABLE:
break;
@@ -199,7 +210,7 @@ public class LoadCallbackClient implements CallbackClient {
mes += "T" + this.gameView.getTurn() + "-" + this.gameView.getStep().getIndex() + ", L:" + p.getLibraryCount() + ", H:" + getPlayer().getHandCount() + ": ";
}
- return logsPrefix + ": " + mes;
+ return globalProgress + ", " + logsPrefix + ": " + mes;
}
public void setSession(Session session) {
diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java
index 9319ab03b16..8fad6bac7ef 100644
--- a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java
@@ -51,7 +51,7 @@ public class LoadTest {
private static final Boolean TEST_SHOW_GAME_LOGS_AS_HTML = false; // html is original format with full data, but can be too bloated
private static final String TEST_AI_GAME_MODE = "Freeform Commander Free For All";
private static final String TEST_AI_DECK_TYPE = "Variant Magic - Freeform Commander";
- private static final String TEST_AI_RANDOM_DECK_SETS = "LCI,LCC,WHO"; // set for random generated decks (empty for all sets usage, PELP for lands only - communication test)
+ private static final String TEST_AI_RANDOM_DECK_SETS = ""; // sets list for random generated decks (GRN,ACR for specific sets, empty for all sets, PELP for lands only - communication test)
private static final String TEST_AI_RANDOM_DECK_COLORS_FOR_EMPTY_GAME = "GR"; // colors list for deck generation, empty for all colors
private static final String TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME = "WUBRG";
private static final String TEST_AI_CUSTOM_DECK_PATH_1 = ""; // custom deck file instead random for player 1 (empty for random)
@@ -217,7 +217,6 @@ public class LoadTest {
public void playTwoAIGame(String gameName, Integer taskNumber, TasksProgress tasksProgress, long randomSeed, String deckColors, String deckAllowedSets, LoadTestGameResult gameResult) {
Assert.assertFalse("need deck colors", deckColors.isEmpty());
- Assert.assertFalse("need allowed sets", deckAllowedSets.isEmpty());
// monitor and game source
LoadPlayer monitor = new LoadPlayer("mon", true, gameName + ", mon");
@@ -250,8 +249,13 @@ public class LoadTest {
TableView checkGame = monitor.getTable(tableId).orElse(null);
TableState state = (checkGame == null ? null : checkGame.getTableState());
- tasksProgress.update(taskNumber, state == TableState.FINISHED, gameView == null ? 0 : gameView.getTurn());
+ String finishInfo = "";
+ if (state == TableState.FINISHED) {
+ finishInfo = gameView == null ? "??" : gameView.getStep().getStepShortText().toLowerCase(Locale.ENGLISH);
+ }
+ tasksProgress.update(taskNumber, finishInfo, gameView == null ? 0 : gameView.getTurn());
String globalProgress = tasksProgress.getInfo();
+ monitor.client.updateGlobalProgress(globalProgress);
if (gameView != null && checkGame != null) {
logger.info(globalProgress + ", " + checkGame.getTableName() + ": ---");
@@ -283,12 +287,13 @@ public class LoadTest {
if (Objects.equals(gameView.getActivePlayerId(), p.getPlayerId())) {
activeInfo = " (active)";
}
- logger.info(String.format("%s, %s, status: %s - Life=%d; Lib=%d;%s",
+ logger.info(String.format("%s, %s, status: %s - Life=%d; Lib=%d; CRs=%d%s;",
globalProgress,
checkGame.getTableName(),
p.getName(),
p.getLife(),
p.getLibraryCount(),
+ p.getBattlefield().values().stream().filter(CardView::isCreature).mapToInt(x -> 1).sum(),
activeInfo
));
});
@@ -319,7 +324,7 @@ public class LoadTest {
long randomSeed = RandomUtil.nextInt();
LoadTestGameResult gameResult = gameResults.createGame(0, "test game", randomSeed);
TasksProgress tasksProgress = new TasksProgress();
- tasksProgress.update(1, true, 0);
+ tasksProgress.update(1, "", 0);
playTwoAIGame("Single AI game", 1, tasksProgress, randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult);
printGameResults(gameResults);
@@ -331,12 +336,13 @@ public class LoadTest {
// play multiple AI games with CLIENT side code (catch every GameView changes from the server)
int singleGameSID = 0; // set sid for same deck games, set 0 for random decks
- int gamesAmount = 10; // games per run
- boolean isRunParallel = true; // can generate too much logs in test run, so search server logs for possible errors
+
+ int runTotalGames = 10;
+ int runMaxParallelGames = 5; // use 1 to run one by one (warning, it's limited by COMPUTER_MAX_THREADS_FOR_SIMULATIONS)
ExecutorService executerService;
- if (isRunParallel) {
- executerService = Executors.newFixedThreadPool(gamesAmount, new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES));
+ if (runMaxParallelGames > 1) {
+ executerService = Executors.newFixedThreadPool(runMaxParallelGames, new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES));
} else {
executerService = Executors.newSingleThreadExecutor(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES));
}
@@ -344,11 +350,11 @@ public class LoadTest {
// save random seeds for repeated results (in decks generating)
List seedsList = new ArrayList<>();
if (singleGameSID != 0) {
- for (int i = 1; i <= gamesAmount; i++) {
+ for (int i = 1; i <= runTotalGames; i++) {
seedsList.add(singleGameSID);
}
} else {
- for (int i = 1; i <= gamesAmount; i++) {
+ for (int i = 1; i <= runTotalGames; i++) {
seedsList.add(RandomUtil.nextInt());
}
}
@@ -356,26 +362,42 @@ public class LoadTest {
LoadTestGameResultsList gameResults = new LoadTestGameResultsList();
try {
TasksProgress tasksProgress = new TasksProgress();
+ List gameTasks = new ArrayList<>();
for (int i = 0; i < seedsList.size(); i++) {
int gameIndex = i;
- tasksProgress.update(gameIndex + 1, true, 0);
+ tasksProgress.update(gameIndex + 1, "", 0);
long randomSeed = seedsList.get(i);
logger.info("Game " + (i + 1) + " of " + seedsList.size() + ", RANDOM seed: " + randomSeed);
Future gameTask = executerService.submit(() -> {
- String gameName = "AI game #" + (gameIndex + 1);
+ String gameName = String.format("AI game #%02d", gameIndex + 1);
LoadTestGameResult gameResult = gameResults.createGame(gameIndex + 1, gameName, randomSeed);
playTwoAIGame(gameName, gameIndex + 1, tasksProgress, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult);
});
+ gameTasks.add(gameTask);
- if (!isRunParallel) {
+ if (runMaxParallelGames <= 1) {
// run one by one
gameTask.get();
}
}
- if (isRunParallel) {
+ if (runMaxParallelGames > 1) {
// run parallel
executerService.shutdown();
- Assert.assertTrue(executerService.awaitTermination(1, TimeUnit.HOURS));
+ Assert.assertTrue("running too long", executerService.awaitTermination(1, TimeUnit.HOURS));
+ }
+
+ // check errors
+ int errorsCount = 0;
+ for (Future task : gameTasks) {
+ try {
+ task.get();
+ } catch (InterruptedException | ExecutionException e) {
+ errorsCount++;
+ logger.error(e, e);
+ }
+ }
+ if (errorsCount > 0) {
+ Assert.fail(String.format("Found %d critical errors in running games, see logs above", errorsCount));
}
} catch (InterruptedException | ExecutionException e) {
logger.error(e, e);
@@ -583,11 +605,11 @@ public class LoadTest {
private static class TasksProgress {
private String info;
- private final Map finishes = new LinkedHashMap<>();
- private final Map turns = new LinkedHashMap<>();
+ private final Map finishes = new LinkedHashMap<>(); // game number, finish on step
+ private final Map turns = new LinkedHashMap<>(); // game number, current turn
- synchronized public void update(Integer taskNumber, boolean newFinish, Integer newTurn) {
- Boolean oldFinish = this.finishes.getOrDefault(taskNumber, false);
+ synchronized public void update(Integer taskNumber, String newFinish, Integer newTurn) {
+ String oldFinish = this.finishes.getOrDefault(taskNumber, "");
Integer oldTurn = this.turns.getOrDefault(taskNumber, 0);
if (!this.finishes.containsKey(taskNumber)
|| !Objects.equals(oldFinish, newFinish)
@@ -599,14 +621,25 @@ public class LoadTest {
}
private void updateInfo() {
- // example: progress [=00, +01, +01, =12, =15, =01, +61]
+ // example: progress 33% [20.cd, 21.__, 17.__], AI game #09: ---
+
+ int completed = this.finishes.values().stream().mapToInt(x -> x.isEmpty() ? 0 : 1).sum();
+ int completedPercent = this.finishes.size() == 0 ? 0 : completed * 100 / this.finishes.size();
+
String res = this.finishes.keySet().stream()
- .map(taskNumber -> String.format("%s%02d",
- this.finishes.getOrDefault(taskNumber, false) ? "=" : "+",
- this.turns.getOrDefault(taskNumber, 0)
- ))
+ .map(taskNumber -> {
+ String turn = String.format("%02d", this.turns.getOrDefault(taskNumber, 0));
+ String finishInfo = this.finishes.getOrDefault(taskNumber, "");
+ if (finishInfo.isEmpty()) {
+ // active
+ return turn + ".__";
+ } else {
+ // done
+ return turn + "." + finishInfo;
+ }
+ })
.collect(Collectors.joining(", "));
- this.info = String.format("progress [%s]", res);
+ this.info = String.format("progress %d%% [%s]", completedPercent, res);
}
public String getInfo() {
@@ -848,10 +881,32 @@ public class LoadTest {
return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(1).getLife();
}
+ public int getCreaturesCount1() {
+ return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(0).getBattlefield().values()
+ .stream()
+ .filter(CardView::isCreature)
+ .mapToInt(x -> 1)
+ .sum();
+ }
+
+ public int getCreaturesCount2() {
+ return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(1).getBattlefield().values()
+ .stream()
+ .filter(CardView::isCreature)
+ .mapToInt(x -> 1)
+ .sum();
+ }
+
public int getTurn() {
return finalGameView == null ? 0 : this.finalGameView.getTurn();
}
+ public String getTurnInfo() {
+ int turn = finalGameView == null ? 0 : this.finalGameView.getTurn();
+ String stepInfo = finalGameView == null ? "??" : this.finalGameView.getStep().getStepShortText().toLowerCase(Locale.ENGLISH);
+ return String.format("%02d.%s", turn, stepInfo);
+ }
+
public int getDurationMs() {
return finalGameView == null ? 0 : ((int) ((this.timeEnded.getTime() - this.timeStarted.getTime())));
}
@@ -859,12 +914,16 @@ public class LoadTest {
public int getTotalErrorsCount() {
return finalGameView == null ? 0 : this.finalGameView.getTotalErrorsCount();
}
+
+ public int getTotalEffectsCount() {
+ return finalGameView == null ? 0 : this.finalGameView.getTotalEffectsCount();
+ }
}
private static class LoadTestGameResultsList extends HashMap {
- private static final String tableFormatHeader = "|%-10s|%-15s|%-20s|%-10s|%-10s|%-15s|%-15s|%-10s|%-20s|%n";
- private static final String tableFormatData = "|%-10s|%15s|%20s|%10s|%10s|%15s|%15s|%10s|%20s|%n";
+ private static final String tableFormatHeader = "|%-10s|%-15s|%-20s|%-10s|%-10s|%-10s|%-10s|%-10s|%-15s|%-15s|%-10s|%n";
+ private static final String tableFormatData = "|%-10s|%15s|%20s|%10s|%10s|%10s|%10s|%10s|%15s|%15s|%10s|%n";
public LoadTestGameResult createGame(int index, String name, long randomSeed) {
if (this.containsKey(index)) {
@@ -881,9 +940,12 @@ public class LoadTest {
"name",
"random sid",
"errors",
+ "effects",
"turn",
- "player 1",
- "player 2",
+ "life p1",
+ "life p2",
+ "creatures p1",
+ "creatures p2",
"time, sec",
"time per turn, sec"
);
@@ -900,9 +962,12 @@ public class LoadTest {
gameResult.name, //"name",
String.valueOf(gameResult.randomSeed), // "random sid",
String.valueOf(gameResult.getTotalErrorsCount()), // "errors",
- String.valueOf(gameResult.getTurn()), //"turn",
- String.valueOf(gameResult.getLife1()), //"player 1",
- String.valueOf(gameResult.getLife2()), //"player 2",
+ String.valueOf(gameResult.getTotalEffectsCount()), // "effects",
+ gameResult.getTurnInfo(), //"turn",
+ String.valueOf(gameResult.getLife1()), //"life p1",
+ String.valueOf(gameResult.getLife2()), //"life p2",
+ String.valueOf(gameResult.getCreaturesCount1()), //"creatures p1",
+ String.valueOf(gameResult.getCreaturesCount2()), //"creatures p2",
String.format("%.3f", (float) gameResult.getDurationMs() / 1000), //"time, sec",
String.format("%.3f", ((float) gameResult.getDurationMs() / 1000) / gameResult.getTurn()) //"per turn, sec"
);
@@ -915,9 +980,12 @@ public class LoadTest {
String.valueOf(this.size()), //"name",
"total, secs: " + String.format("%.3f", (float) this.getTotalDurationMs() / 1000), // "random sid",
String.valueOf(this.getTotalErrorsCount()), // errors
+ String.valueOf(this.getAvgEffectsCount()), // effects
String.valueOf(this.getAvgTurn()), // turn
- String.valueOf(this.getAvgLife1()), // player 1
- String.valueOf(this.getAvgLife2()), // player 2
+ String.valueOf(this.getAvgLife1()), // life p1
+ String.valueOf(this.getAvgLife2()), // life p2
+ String.valueOf(this.getAvgCreaturesCount1()), // creatures p1
+ String.valueOf(this.getAvgCreaturesCount2()), // creatures p2
String.valueOf(String.format("%.3f", (float) this.getAvgDurationMs() / 1000)), // time, sec
String.valueOf(String.format("%.3f", (float) this.getAvgDurationPerTurnMs() / 1000)) // time per turn, sec
);
@@ -928,6 +996,10 @@ public class LoadTest {
return this.values().stream().mapToInt(LoadTestGameResult::getTotalErrorsCount).sum();
}
+ private int getAvgEffectsCount() {
+ return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTotalEffectsCount).sum() / this.size();
+ }
+
private int getAvgTurn() {
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size();
}
@@ -940,6 +1012,14 @@ public class LoadTest {
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size();
}
+ private int getAvgCreaturesCount1() {
+ return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getCreaturesCount1).sum() / this.size();
+ }
+
+ private int getAvgCreaturesCount2() {
+ return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getCreaturesCount2).sum() / this.size();
+ }
+
private int getTotalDurationMs() {
return this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum();
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java b/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java
index e49c03a0a4b..0ed68a22f05 100644
--- a/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java
+++ b/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java
@@ -28,6 +28,10 @@ public class SimpleMageClient implements MageClient {
callbackClient = new LoadCallbackClient(joinGameChat, logsPrefix, showLogsAsHtml);
}
+ protected void updateGlobalProgress(String globalProgress) {
+ callbackClient.updateGlobalProgress(globalProgress);
+ }
+
@Override
public MageVersion getVersion() {
return version;
diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java
index cef50ca4a8e..15dfcd42146 100644
--- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java
+++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java
@@ -1996,7 +1996,7 @@ public class TestPlayer implements Player {
}
}
}
- checkMultipleBlockers(game, blockedCreaturesList);
+ checkMultipleBlockers(game, blockedCreaturesList); // search wrong block commands
// AI FULL play if no actions available
if (!mustBlockByAction && (this.AIPlayer || this.AIRealGameSimulation)) {
@@ -3977,6 +3977,26 @@ public class TestPlayer implements Player {
return computerPlayer.isDrawsOnOpponentsTurn();
}
+ @Override
+ public int getSpeed() {
+ return computerPlayer.getSpeed();
+ }
+
+ @Override
+ public void initSpeed(Game game) {
+ computerPlayer.initSpeed(game);
+ }
+
+ @Override
+ public void increaseSpeed(Game game) {
+ computerPlayer.increaseSpeed(game);
+ }
+
+ @Override
+ public void decreaseSpeed(Game game) {
+ computerPlayer.decreaseSpeed(game);
+ }
+
@Override
public void setPayManaMode(boolean payManaMode) {
computerPlayer.setPayManaMode(payManaMode);
diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java
index 35c08188042..f7180431d1c 100644
--- a/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java
@@ -148,12 +148,15 @@ public class DeckAutoLandsTest extends MageTestPlayerBase {
Deck deck = prepareDeck(Arrays.asList(
new DeckCardInfo("Amulet of Kroog", "36", "ATQ", 1) // ATQ without lands
));
- // must find 2 random sets
- List possibleSets1 = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()).stream().sorted().collect(Collectors.toList());
- List possibleSets2 = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()).stream().sorted().collect(Collectors.toList());
- Assert.assertEquals("must find 1 random set, try 1", 1, possibleSets1.size());
- Assert.assertEquals("must find 1 random set, try 2", 1, possibleSets2.size());
- Assert.assertNotEquals("must find random sets, try 3", possibleSets1.get(0), possibleSets2.get(0));
+
+ // must find random sets
+ int tries = 3;
+ List possibleSets = new ArrayList<>();
+ for (int i = 0; i < tries; ++i) {
+ possibleSets.addAll(TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()).stream().sorted().collect(Collectors.toList()));
+ }
+ Assert.assertEquals("must find 1 set per request, but get " + possibleSets, tries, possibleSets.size());
+ Assert.assertNotEquals("must find different random sets, but get " + possibleSets, 1, possibleSets.stream().distinct().count());
}
private void assertPossibleSets(
diff --git a/Mage.Verify/pom.xml b/Mage.Verify/pom.xml
index ac62b70e9db..31b9493ede0 100644
--- a/Mage.Verify/pom.xml
+++ b/Mage.Verify/pom.xml
@@ -6,7 +6,7 @@
org.mage
mage-root
- 1.4.55
+ 1.4.56
mage-verify
diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java
index 1da2835fdfc..892bfadafbe 100644
--- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java
+++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java
@@ -70,7 +70,7 @@ public class VerifyCardDataTest {
private static final Logger logger = Logger.getLogger(VerifyCardDataTest.class);
- private static final String FULL_ABILITIES_CHECK_SET_CODES = "MH3;M3C"; // check ability text due mtgjson, can use multiple sets like MAT;CMD or * for all
+ private static final String FULL_ABILITIES_CHECK_SET_CODES = "DFT"; // check ability text due mtgjson, can use multiple sets like MAT;CMD or * for all
private static final boolean CHECK_ONLY_ABILITIES_TEXT = false; // use when checking text locally, suppresses unnecessary checks and output messages
private static final boolean CHECK_COPYABLE_FIELDS = true; // disable for better verify test performance
@@ -102,7 +102,8 @@ public class VerifyCardDataTest {
"shroud", "banding", "flanking", "horsemanship", "legendary landwalk"
);
- private static final List doubleWords = new ArrayList<>();
+ private static final List doubleWords = new ArrayList<>(); // for inner calc
+ private static final List etbTriggerPhrases = new ArrayList<>(); // for inner calc
static {
// numbers
@@ -143,14 +144,12 @@ public class VerifyCardDataTest {
skipListAddName(SKIP_LIST_TYPE, "UNH", "Old Fogey"); // uses summon word as a joke card
skipListAddName(SKIP_LIST_TYPE, "UND", "Old Fogey");
skipListAddName(SKIP_LIST_TYPE, "UST", "capital offense"); // uses "instant" instead "Instant" as a joke card
- skipListAddName(SKIP_LIST_TYPE, "DFT", "Venomsac Lagac"); // temporary
// subtype
// skipListAddName(SKIP_LIST_SUBTYPE, set, cardName);
skipListAddName(SKIP_LIST_SUBTYPE, "UGL", "Miss Demeanor"); // uses multiple types as a joke card: Lady, of, Proper, Etiquette
skipListAddName(SKIP_LIST_SUBTYPE, "UGL", "Elvish Impersonators"); // subtype is "Elves" pun
skipListAddName(SKIP_LIST_SUBTYPE, "UND", "Elvish Impersonators");
- skipListAddName(SKIP_LIST_SUBTYPE, "DFT", "Venomsac Lagac"); // temporary
// number
// skipListAddName(SKIP_LIST_NUMBER, set, cardName);
@@ -1991,6 +1990,7 @@ public class VerifyCardDataTest {
}
String refLowerText = ref.text.toLowerCase(Locale.ENGLISH);
+ String cardLowerText = String.join("\n", card.getRules()).toLowerCase(Locale.ENGLISH);
// special check: kicker ability must be in rules
if (card.getAbilities().containsClass(MultikickerAbility.class) && card.getRules().stream().noneMatch(rule -> rule.contains("Multikicker"))) {
@@ -2050,6 +2050,21 @@ public class VerifyCardDataTest {
fail(card, "abilities", "mutate cards aren't implemented and shouldn't be available");
}
+ // special check: some new creature's ETB must use When this creature enters instead When {this} enters
+ if (EntersBattlefieldTriggeredAbility.ENABLE_TRIGGER_PHRASE_AUTO_FIX) {
+ if (etbTriggerPhrases.isEmpty()) {
+ etbTriggerPhrases.addAll(EntersBattlefieldTriggeredAbility.getPossibleTriggerPhrases());
+ Assert.assertTrue(etbTriggerPhrases.get(0).startsWith("when"));
+ }
+ if (refLowerText.contains("when")) {
+ for (String needTriggerPhrase : etbTriggerPhrases) {
+ if (refLowerText.contains(needTriggerPhrase) && !cardLowerText.contains(needTriggerPhrase)) {
+ fail(card, "abilities", "wrong creature's ETB trigger phrase, must use: " + needTriggerPhrase);
+ }
+ }
+ }
+ }
+
// special check: wrong dies triggers (there are also a runtime check on wrong usage, see isInUseableZoneDiesTrigger)
Set ignoredCards = new HashSet<>();
ignoredCards.add("Caller of the Claw");
@@ -2096,7 +2111,7 @@ public class VerifyCardDataTest {
continue;
}
boolean isDiesAbility = rules.contains("die ")
- || rules.contains("dies ")
+ || (rules.contains("dies ") && !rules.contains("dies this turn"))
|| rules.contains("die,")
|| rules.contains("dies,");
boolean isPutToGraveAbility = rules.contains("put into")
diff --git a/Mage/pom.xml b/Mage/pom.xml
index e5c1184ee14..6713c55dccb 100644
--- a/Mage/pom.xml
+++ b/Mage/pom.xml
@@ -6,7 +6,7 @@
org.mage
mage-root
- 1.4.55
+ 1.4.56
mage
diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbility.java b/Mage/src/main/java/mage/abilities/TriggeredAbility.java
index f948f9ffe4b..24266aeaaad 100644
--- a/Mage/src/main/java/mage/abilities/TriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/TriggeredAbility.java
@@ -104,4 +104,6 @@ public interface TriggeredAbility extends Ability {
GameEvent getTriggerEvent();
TriggeredAbility setTriggerPhrase(String triggerPhrase);
+
+ String getTriggerPhrase();
}
diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
index cb87938acdc..0daef751810 100644
--- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
+++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java
@@ -132,6 +132,11 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return this;
}
+ @Override
+ public String getTriggerPhrase() {
+ return this.triggerPhrase;
+ }
+
@Override
public void setTriggerEvent(GameEvent triggerEvent) {
this.triggerEvent = triggerEvent;
diff --git a/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java b/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java
index e53490ee343..4eb998547f3 100644
--- a/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java
@@ -9,6 +9,8 @@ import mage.constants.Zone;
public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl {
+ private boolean showActivateText = true;
+
public ActivateAsSorceryActivatedAbility(Effect effect, Cost cost) {
this(Zone.BATTLEFIELD, effect, cost);
}
@@ -20,6 +22,7 @@ public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl {
protected ActivateAsSorceryActivatedAbility(final ActivateAsSorceryActivatedAbility ability) {
super(ability);
+ this.showActivateText = ability.showActivateText;
}
@Override
@@ -27,9 +30,18 @@ public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl {
return new ActivateAsSorceryActivatedAbility(this);
}
+ public ActivateAsSorceryActivatedAbility withShowActivateText(boolean showActivateText) {
+ this.showActivateText = showActivateText;
+ return this;
+ }
+
@Override
public String getRule() {
String superRule = super.getRule();
+ if (!showActivateText) {
+ return superRule;
+ }
+
String newText = (mayActivate == TargetController.OPPONENT
? " Only your opponents may activate this ability and only as a sorcery."
: " Activate only as a sorcery.");
diff --git a/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java
index 1ebaf677b9d..0a8aeb373d6 100644
--- a/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java
@@ -2,22 +2,33 @@ package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
+import mage.cards.Card;
+import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
+import java.util.Arrays;
+import java.util.List;
+
/**
* @author BetaSteward_at_googlemail.com
*/
public class EntersBattlefieldTriggeredAbility extends TriggeredAbilityImpl {
+ static public boolean ENABLE_TRIGGER_PHRASE_AUTO_FIX = false;
+
public EntersBattlefieldTriggeredAbility(Effect effect) {
this(effect, false);
}
public EntersBattlefieldTriggeredAbility(Effect effect, boolean optional) {
super(Zone.ALL, effect, optional); // Zone.All because a creature with trigger can be put into play and be sacrificed during the resolution of an effect (discard Obstinate Baloth with Smallpox)
- this.withRuleTextReplacement(true); // default true to replace "{this}" with "it"
+ this.withRuleTextReplacement(true); // default true to replace "{this}" with "it" or "this creature"
+
+ // warning, it's impossible to add text auto-replacement for creatures here (When this creature enters),
+ // so it was implemented in CardImpl.addAbility instead
+ // see https://github.com/magefree/mage/issues/12791
setTriggerPhrase("When {this} enters, ");
}
@@ -43,4 +54,59 @@ public class EntersBattlefieldTriggeredAbility extends TriggeredAbilityImpl {
public EntersBattlefieldTriggeredAbility copy() {
return new EntersBattlefieldTriggeredAbility(this);
}
+
+ @Override
+ public EntersBattlefieldTriggeredAbility setTriggerPhrase(String triggerPhrase) {
+ super.setTriggerPhrase(triggerPhrase);
+ return this;
+ }
+
+ /**
+ * Find description of "{this}" like "this creature"
+ */
+ static public String getThisObjectDescription(Card card) {
+ // prepare {this} description
+
+ // short names like Aatchik for Aatchik, Emerald Radian
+ // except: Mu Yanling, Wind Rider (maybe related to spaces in name)
+ List parts = Arrays.asList(card.getName().split(","));
+ if (parts.size() > 1 && !parts.get(0).contains(" ")) {
+ return parts.get(0);
+ }
+
+ // some types have priority, e.g. Vehicle instead artifact, example: Boommobile
+ if (card.getSubtype().contains(SubType.VEHICLE)) {
+ return "this Vehicle";
+ }
+ if (card.getSubtype().contains(SubType.AURA)) {
+ return "this Aura";
+ }
+
+ // by priority
+ if (card.isCreature()) {
+ return "this creature";
+ } else if (card.isPlaneswalker()) {
+ return "this planeswalker";
+ } else if (card.isLand()) {
+ return "this land";
+ } else if (card.isEnchantment()) {
+ return "this enchantment";
+ } else if (card.isArtifact()) {
+ return "this artifact";
+ } else {
+ return "this permanent";
+ }
+ }
+
+ public static List getPossibleTriggerPhrases() {
+ // for verify tests - must be same list as above (only {this} relates phrases)
+ return Arrays.asList(
+ "when this creature enters",
+ "when this planeswalker enters",
+ "when this land enters",
+ "when this enchantment enters",
+ "when this artifact enters",
+ "when this permanent enters"
+ );
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java b/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java
index aaebd872795..9f25acf57b4 100644
--- a/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java
@@ -13,6 +13,10 @@ import mage.util.CardUtil;
*/
public class LimitedTimesPerTurnActivatedAbility extends ActivatedAbilityImpl {
+ public LimitedTimesPerTurnActivatedAbility(Effect effect, Cost cost) {
+ this(Zone.BATTLEFIELD, effect, cost);
+ }
+
public LimitedTimesPerTurnActivatedAbility(Zone zone, Effect effect, Cost cost) {
this(zone, effect, cost, 1);
}
diff --git a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java
index 4a66080a057..d97c23e9c16 100644
--- a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java
@@ -10,6 +10,7 @@ import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
import mage.game.permanent.Permanent;
+import mage.util.CardUtil;
/**
* @author TheElk801
@@ -38,9 +39,21 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl {
private final Ability ability;
+ private static Duration getDuration(Ability ability) {
+ switch (ability.getZone()) {
+ case BATTLEFIELD:
+ return Duration.WhileOnBattlefield;
+ case GRAVEYARD:
+ return Duration.WhileInGraveyard;
+ default:
+ return Duration.Custom;
+ }
+ }
+
MaxSpeedAbilityEffect(Ability ability) {
- super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
+ super(getDuration(ability), Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.ability = ability;
+ this.ability.setRuleVisible(false);
}
private MaxSpeedAbilityEffect(final MaxSpeedAbilityEffect effect) {
@@ -73,6 +86,6 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl {
@Override
public String getText(Mode mode) {
- return "Max speed — " + ability.getRule();
+ return "Max speed — " + CardUtil.getTextWithFirstCharUpperCase(ability.getRule());
}
}
diff --git a/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java b/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java
index a86ac17e6c6..00f77b14586 100644
--- a/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java
+++ b/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java
@@ -35,7 +35,7 @@ class MayCastFromGraveyardEffect extends AsThoughEffectImpl {
MayCastFromGraveyardEffect() {
super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.PutCreatureInPlay);
- staticText = "you may cast {this} from your graveyard";
+ staticText = "you may cast this card from your graveyard";
}
private MayCastFromGraveyardEffect(final MayCastFromGraveyardEffect effect) {
diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java
index 0a518e03f9f..e3b234cb4e2 100644
--- a/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java
@@ -18,7 +18,6 @@ import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
-import java.util.stream.Collectors;
/**
* @author nantuko
@@ -27,6 +26,7 @@ public class ExileFromGraveCost extends CostImpl {
private final List exiledCards = new ArrayList<>();
private boolean setTargetPointer = false;
+ private boolean useSourceExileZone = true;
public ExileFromGraveCost(TargetCardInYourGraveyard target) {
target.withNotTarget(true);
@@ -73,6 +73,7 @@ public class ExileFromGraveCost extends CostImpl {
super(cost);
this.exiledCards.addAll(cost.getExiledCards());
this.setTargetPointer = cost.setTargetPointer;
+ this.useSourceExileZone = cost.useSourceExileZone;
}
@Override
@@ -90,11 +91,23 @@ public class ExileFromGraveCost extends CostImpl {
}
Cards cardsToExile = new CardsImpl();
cardsToExile.addAllCards(exiledCards);
+
+
+ UUID exileZoneId = null;
+ String exileZoneName = "";
+ if (useSourceExileZone) {
+ exileZoneId = CardUtil.getExileZoneId(game, source);
+ exileZoneName = CardUtil.getSourceName(game, source);
+ }
controller.moveCardsToExile(
- cardsToExile.getCards(game), source, game, true,
- CardUtil.getExileZoneId(game, source),
- CardUtil.getSourceName(game, source)
+ cardsToExile.getCards(game),
+ source,
+ game,
+ true,
+ exileZoneId,
+ exileZoneName
);
+
if (setTargetPointer) {
source.getEffects().setTargetPointer(new FixedTargets(cardsToExile.getCards(game), game));
}
@@ -118,4 +131,12 @@ public class ExileFromGraveCost extends CostImpl {
public List getExiledCards() {
return exiledCards;
}
+
+ /**
+ * Put exiled cards to source zone, so next linked ability can find it
+ */
+ public ExileFromGraveCost withSourceExileZone(boolean useSourceExileZone) {
+ this.useSourceExileZone = useSourceExileZone;
+ return this;
+ }
}
diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java
index 51fad57ba09..4b1ad0b2610 100644
--- a/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java
+++ b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java
@@ -16,7 +16,7 @@ import java.util.UUID;
public class ExileSourceFromGraveCost extends CostImpl {
public ExileSourceFromGraveCost() {
- this.text = "exile {this} from your graveyard";
+ this.text = "exile this card from your graveyard";
}
private ExileSourceFromGraveCost(final ExileSourceFromGraveCost cost) {
diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileSourceWithTimeCountersCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceWithTimeCountersCost.java
new file mode 100644
index 00000000000..4ce88003b7a
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceWithTimeCountersCost.java
@@ -0,0 +1,88 @@
+package mage.abilities.costs.common;
+
+import java.util.Locale;
+import java.util.UUID;
+
+import mage.abilities.Ability;
+import mage.abilities.costs.Cost;
+import mage.abilities.costs.CostImpl;
+import mage.abilities.effects.common.continuous.GainSuspendEffect;
+import mage.abilities.keyword.SuspendAbility;
+import mage.cards.Card;
+import mage.constants.Zone;
+import mage.counters.CounterType;
+import mage.game.Game;
+import mage.MageObjectReference;
+import mage.players.Player;
+
+
+/**
+ * @author padfoot
+ */
+public class ExileSourceWithTimeCountersCost extends CostImpl {
+
+ private final int counters;
+ private final boolean checksSuspend;
+ private final boolean givesSuspend;
+ private final Zone fromZone;
+
+ public ExileSourceWithTimeCountersCost(int counters) {
+ this (counters, true, false, null);
+ }
+
+ public ExileSourceWithTimeCountersCost(int counters, boolean givesSuspend, boolean checksSuspend, Zone fromZone) {
+ this.counters = counters;
+ this.givesSuspend = givesSuspend;
+ this.checksSuspend = checksSuspend;
+ this.fromZone = fromZone;
+ this.text = "exile {this} " +
+ ((fromZone != null) ? " from your " + fromZone.toString().toLowerCase(Locale.ENGLISH) : "") +
+ " and put " + counters + " time counters on it" +
+ (givesSuspend ? ". It gains suspend" : "") +
+ (checksSuspend ? ". If it doesn't have suspend, it gains suspend" : "");
+ }
+
+ private ExileSourceWithTimeCountersCost(final ExileSourceWithTimeCountersCost cost) {
+ super(cost);
+ this.counters = cost.counters;
+ this.givesSuspend = cost.givesSuspend;
+ this.checksSuspend = cost.checksSuspend;
+ this.fromZone = cost.fromZone;
+ }
+
+ @Override
+ public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
+ Player controller = game.getPlayer(controllerId);
+ if (controller == null) {
+ return paid;
+ }
+ Card card = game.getCard(source.getSourceId());
+ boolean hasSuspend = card.getAbilities(game).containsClass(SuspendAbility.class);
+ if (card != null && (fromZone == null || fromZone == game.getState().getZone(source.getSourceId()))) {
+ UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game);
+ if (controller.moveCardsToExile(card, source, game, true, exileId, "Suspended cards of " + controller.getName())) {
+ card.addCounters(CounterType.TIME.createInstance(counters), controller.getId(), source, game);
+ game.informPlayers(controller.getLogName() + " exiles " + card.getLogName() + ((fromZone != null) ? " from their " + fromZone.toString().toLowerCase(Locale.ENGLISH) : "") + " with " + counters + " time counters on it.");
+ if (givesSuspend || (checksSuspend && !hasSuspend)) {
+ game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source);
+ }
+ }
+ // 117.11. The actions performed when paying a cost may be modified by effects.
+ // Even if they are, meaning the actions that are performed don't match the actions
+ // that are called for, the cost has still been paid.
+ // so return state here is not important because the user indended to exile the target anyway
+ paid = true;
+ }
+ return paid;
+ }
+
+ @Override
+ public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
+ return (game.getCard(source.getSourceId()) != null && (fromZone == null || fromZone == game.getState().getZone(source.getSourceId())));
+ }
+
+ @Override
+ public ExileSourceWithTimeCountersCost copy() {
+ return new ExileSourceWithTimeCountersCost(this);
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java
index c307f98adf9..07f2116470e 100644
--- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java
+++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java
@@ -3,14 +3,16 @@ package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
+import mage.abilities.hint.Hint;
+import mage.abilities.hint.ValueHint;
import mage.cards.Card;
+import mage.constants.CardType;
import mage.game.Game;
import mage.game.permanent.PermanentToken;
import mage.players.Player;
-import java.util.Collection;
-import java.util.Objects;
-import java.util.UUID;
+import java.util.*;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -20,16 +22,18 @@ public enum CardTypesInGraveyardCount implements DynamicValue {
YOU("your graveyard"),
ALL("all graveyards"),
OPPONENTS("your opponents' graveyards");
+
private final String message;
+ private final CardTypesInGraveyardHint hint;
CardTypesInGraveyardCount(String message) {
this.message = "the number of card types among cards in " + message;
+ this.hint = new CardTypesInGraveyardHint(this);
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
- return getStream(game, sourceAbility)
- .filter(card -> !card.isCopy() && !(card instanceof PermanentToken))
+ return getGraveyardCards(game, sourceAbility)
.map(card -> card.getCardType(game))
.flatMap(Collection::stream)
.distinct()
@@ -52,16 +56,16 @@ public enum CardTypesInGraveyardCount implements DynamicValue {
return message;
}
- private final Stream getStream(Game game, Ability ability) {
+ public Hint getHint() {
+ return hint;
+ }
+
+ public Stream getGraveyardCards(Game game, Ability ability) {
Collection playerIds;
switch (this) {
case YOU:
- Player player = game.getPlayer(ability.getControllerId());
- return player == null
- ? null : player
- .getGraveyard()
- .getCards(game)
- .stream();
+ playerIds = Collections.singletonList(ability.getControllerId());
+ break;
case OPPONENTS:
playerIds = game.getOpponents(ability.getControllerId());
break;
@@ -69,13 +73,47 @@ public enum CardTypesInGraveyardCount implements DynamicValue {
playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game);
break;
default:
- return null;
+ throw new IllegalArgumentException("Wrong code usage: miss implementation for " + this);
}
return playerIds.stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.map(Player::getGraveyard)
.map(graveyard -> graveyard.getCards(game))
- .flatMap(Collection::stream);
+ .flatMap(Collection::stream)
+ .filter(Objects::nonNull)
+ .filter(card -> !card.isCopy() && !(card instanceof PermanentToken));
}
}
+
+class CardTypesInGraveyardHint implements Hint {
+
+ CardTypesInGraveyardCount value;
+
+ CardTypesInGraveyardHint(CardTypesInGraveyardCount value) {
+ this.value = value;
+ }
+
+ private CardTypesInGraveyardHint(final CardTypesInGraveyardHint hint) {
+ this.value = hint.value;
+ }
+
+ @Override
+ public String getText(Game game, Ability ability) {
+ Stream stream = this.value.getGraveyardCards(game, ability);
+ List types = stream
+ .map(card -> card.getCardType(game))
+ .flatMap(Collection::stream)
+ .distinct()
+ .map(CardType::toString)
+ .sorted()
+ .collect(Collectors.toList());
+ return "Card types in " + this.value.getMessage() + ": " + types.size()
+ + (types.size() > 0 ? " (" + String.join(", ", types) + ')' : "");
+ }
+
+ @Override
+ public CardTypesInGraveyardHint copy() {
+ return new CardTypesInGraveyardHint(this);
+ }
+}
\ No newline at end of file
diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java
index b06aa28ecfe..12ca32d02a9 100644
--- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java
+++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java
@@ -4,6 +4,9 @@ import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
+import mage.players.Player;
+
+import java.util.Optional;
/**
* @author TheElk801
@@ -13,8 +16,10 @@ public enum ControllerSpeedCount implements DynamicValue {
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
- // TODO: Implement this
- return 0;
+ return Optional
+ .ofNullable(game.getPlayer(sourceAbility.getControllerId()))
+ .map(Player::getSpeed)
+ .orElse(0);
}
@Override
diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedDiscardValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedDiscardValue.java
new file mode 100644
index 00000000000..c41ccdc5d13
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedDiscardValue.java
@@ -0,0 +1,40 @@
+package mage.abilities.dynamicvalue.common;
+
+import mage.abilities.Ability;
+import mage.abilities.dynamicvalue.DynamicValue;
+import mage.abilities.effects.Effect;
+import mage.game.Game;
+
+/**
+ * @author TheElk801
+ */
+public enum SavedDiscardValue implements DynamicValue {
+ MANY("many"),
+ MUCH("much");
+
+ private final String message;
+
+ SavedDiscardValue(String message) {
+ this.message = "that " + message;
+ }
+
+ @Override
+ public int calculate(Game game, Ability sourceAbility, Effect effect) {
+ return (Integer) effect.getValue("discarded");
+ }
+
+ @Override
+ public SavedDiscardValue copy() {
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return message;
+ }
+
+ @Override
+ public String getMessage() {
+ return "";
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
index 5ad74ac0fb1..294ad8831cb 100644
--- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
+++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java
@@ -1487,8 +1487,12 @@ public class ContinuousEffects implements Serializable {
}
}
+ public int getTotalEffectsCount() {
+ return allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum();
+ }
+
@Override
public String toString() {
- return "Effects: " + allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum();
+ return "Effects: " + getTotalEffectsCount();
}
}
diff --git a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java
index d01a7ada574..5ca8e531c68 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java
@@ -13,6 +13,7 @@ import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
+import mage.util.CardUtil;
/**
* @author LevelX2
@@ -49,7 +50,7 @@ public class ChooseModeEffect extends OneShotEffect {
}
if (controller != null && sourcePermanent != null) {
Choice choice = new ChoiceImpl(true);
- choice.setMessage(choiceMessage);
+ choice.setMessage(choiceMessage + CardUtil.getSourceLogName(game, source));
choice.getChoices().addAll(modes);
if (controller.choose(Outcome.Neutral, choice, game)) {
if (!game.isSimulation()) {
diff --git a/Mage/src/main/java/mage/abilities/effects/common/DiscardOneOrMoreCardsTriggeredAbility.java b/Mage/src/main/java/mage/abilities/effects/common/DiscardOneOrMoreCardsTriggeredAbility.java
new file mode 100644
index 00000000000..2f9d60137e9
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/effects/common/DiscardOneOrMoreCardsTriggeredAbility.java
@@ -0,0 +1,49 @@
+package mage.abilities.effects.common;
+
+import mage.abilities.TriggeredAbilityImpl;
+import mage.abilities.effects.Effect;
+import mage.constants.Zone;
+import mage.game.Game;
+import mage.game.events.GameEvent;
+
+/**
+ * @author TheElk801
+ */
+public class DiscardOneOrMoreCardsTriggeredAbility extends TriggeredAbilityImpl {
+
+ public DiscardOneOrMoreCardsTriggeredAbility(Effect effect) {
+ this(effect, false);
+ }
+
+ public DiscardOneOrMoreCardsTriggeredAbility(Effect effect, boolean optional) {
+ this(Zone.BATTLEFIELD, effect, optional);
+ }
+
+ public DiscardOneOrMoreCardsTriggeredAbility(Zone zone, Effect effect, boolean optional) {
+ super(zone, effect, optional);
+ setTriggerPhrase("Whenever you discard one or more cards, ");
+ }
+
+ private DiscardOneOrMoreCardsTriggeredAbility(final DiscardOneOrMoreCardsTriggeredAbility ability) {
+ super(ability);
+ }
+
+ @Override
+ public DiscardOneOrMoreCardsTriggeredAbility copy() {
+ return new DiscardOneOrMoreCardsTriggeredAbility(this);
+ }
+
+ @Override
+ public boolean checkEventType(GameEvent event, Game game) {
+ return event.getType() == GameEvent.EventType.DISCARDED_CARDS;
+ }
+
+ @Override
+ public boolean checkTrigger(GameEvent event, Game game) {
+ if (!isControlledBy(event.getPlayerId())) {
+ return false;
+ }
+ this.getEffects().setValue("discarded", event.getAmount());
+ return true;
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java b/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java
index e390384b404..aecb28a6765 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java
@@ -43,7 +43,7 @@ public class LoseLifeOpponentsYouGainLifeLostEffect extends OneShotEffect {
return true;
}
int totalLifeLost = game
- .getOpponents(source.getControllerId())
+ .getOpponents(source.getControllerId(), true)
.stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java
index c75f26a8aac..03195c6b9e0 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java
@@ -36,8 +36,9 @@ public class GainAbilitySourceEffect extends ContinuousEffectImpl {
super(duration, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.ability = ability;
this.onCard = onCard;
- this.staticText = "{this} gains " + CardUtil.stripReminderText(ability.getRule())
- + (duration.toString().isEmpty() ? "" : ' ' + duration.toString());
+ this.staticText = "{this} " + (duration == Duration.WhileOnBattlefield ? "has" : "gains") +
+ ' ' + CardUtil.stripReminderText(ability.getRule()) +
+ (duration.toString().isEmpty() ? "" : ' ' + duration.toString());
this.generateGainAbilityDependencies(ability, null);
}
diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java
index aba3e39d53c..df1d492475b 100644
--- a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java
@@ -58,7 +58,7 @@ import java.util.Set;
* entering the battlefield, that card isn’t manifested. Its characteristics remain unmodified and it remains in
* its previous zone. If it was face up, it remains face up.
*
- * 701.34g TODO: need support it
+ * 701.34g
* If a manifested permanent that’s represented by an instant or sorcery card would turn face up, its controller
* reveals it and leaves it face down. Abilities that trigger whenever a permanent is turned face up won’t trigger.
*
diff --git a/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java b/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java
index 66c114a8b72..b7172258258 100644
--- a/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java
+++ b/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java
@@ -52,7 +52,7 @@ public class ConditionTrueHint implements Hint {
}
@Override
- public Hint copy() {
+ public ConditionTrueHint copy() {
return new ConditionTrueHint(this);
}
}
diff --git a/Mage/src/main/java/mage/abilities/hint/StaticHint.java b/Mage/src/main/java/mage/abilities/hint/StaticHint.java
index 848b09ac568..72d9f64b1d0 100644
--- a/Mage/src/main/java/mage/abilities/hint/StaticHint.java
+++ b/Mage/src/main/java/mage/abilities/hint/StaticHint.java
@@ -30,7 +30,7 @@ public class StaticHint implements Hint {
}
@Override
- public Hint copy() {
+ public StaticHint copy() {
return new StaticHint(this);
}
}
diff --git a/Mage/src/main/java/mage/abilities/hint/common/CardTypesInGraveyardHint.java b/Mage/src/main/java/mage/abilities/hint/common/CardTypesInGraveyardHint.java
deleted file mode 100644
index 81f920d30f0..00000000000
--- a/Mage/src/main/java/mage/abilities/hint/common/CardTypesInGraveyardHint.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package mage.abilities.hint.common;
-
-import mage.abilities.Ability;
-import mage.abilities.hint.Hint;
-import mage.cards.Card;
-import mage.constants.CardType;
-import mage.game.Game;
-import mage.players.Player;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-import java.util.UUID;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * @author JayDi85
- */
-public enum CardTypesInGraveyardHint implements Hint {
-
- YOU("your graveyard"),
- ALL("all graveyards"),
- OPPONENTS("your opponents' graveyards");
- private final String message;
-
- CardTypesInGraveyardHint(String message) {
- this.message = message;
- }
-
- @Override
- public String getText(Game game, Ability ability) {
- Stream stream = getStream(game, ability);
- if (stream == null) {
- return null;
- }
- List types = stream
- .map(card -> card.getCardType(game))
- .flatMap(Collection::stream)
- .distinct()
- .map(CardType::toString)
- .sorted()
- .collect(Collectors.toList());
- return "Card types in " + this.message + ": " + types.size()
- + (types.size() > 0 ? " (" + String.join(", ", types) + ')' : "");
- }
-
- @Override
- public Hint copy() {
- return this;
- }
-
- private final Stream getStream(Game game, Ability ability) {
- Collection playerIds;
- switch (this) {
- case YOU:
- Player player = game.getPlayer(ability.getControllerId());
- return player == null
- ? null : player
- .getGraveyard()
- .getCards(game)
- .stream();
- case OPPONENTS:
- playerIds = game.getOpponents(ability.getControllerId());
- break;
- case ALL:
- playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game);
- break;
- default:
- return null;
- }
- return playerIds.stream()
- .map(game::getPlayer)
- .filter(Objects::nonNull)
- .map(Player::getGraveyard)
- .map(graveyard -> graveyard.getCards(game))
- .flatMap(Collection::stream);
- }
-}
diff --git a/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java b/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java
index 027cd4d7dba..8b44491811b 100644
--- a/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java
+++ b/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java
@@ -1,19 +1,19 @@
package mage.abilities.hint.common;
import mage.abilities.Ability;
+import mage.abilities.condition.common.CountersOnPermanentsCondition;
import mage.abilities.hint.Hint;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
-import mage.abilities.condition.common.CountersOnPermanentsCondition;
import mage.util.CardUtil;
/**
* A hint which keeps track of how many counters of a specific type there are
* among some type of permanents
- *
+ *
* @author alexander-novo
*/
public class CountersOnPermanentsHint implements Hint {
@@ -23,6 +23,10 @@ public class CountersOnPermanentsHint implements Hint {
// Which counter type to count
public final CounterType counterType;
+ public CountersOnPermanentsHint(CountersOnPermanentsCondition condition) {
+ this(condition.filter, condition.counterType);
+ }
+
/**
* @param filter Which permanents to consider counters on
* @param counterType Which counter type to count
@@ -32,12 +36,9 @@ public class CountersOnPermanentsHint implements Hint {
this.counterType = counterType;
}
- /**
- * Copy parameters from a {@link CountersOnPermanentsCondition}
- */
- public CountersOnPermanentsHint(CountersOnPermanentsCondition condition) {
- this.filter = condition.filter;
- this.counterType = condition.counterType;
+ public CountersOnPermanentsHint(final CountersOnPermanentsHint hint) {
+ this.filter = hint.filter.copy();
+ this.counterType = hint.counterType;
}
@Override
@@ -56,7 +57,7 @@ public class CountersOnPermanentsHint implements Hint {
}
@Override
- public Hint copy() {
- return this;
+ public CountersOnPermanentsHint copy() {
+ return new CountersOnPermanentsHint(this);
}
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java b/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java
index 8e4ea99d7fb..d6baf07740f 100644
--- a/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java
@@ -47,6 +47,7 @@ import java.util.UUID;
public class ChampionAbility extends StaticAbility {
protected final String objectDescription;
+ protected final String etbObjectDescription;
/**
* Champion one or more creature types or if the subtype array is empty
@@ -59,6 +60,8 @@ public class ChampionAbility extends StaticAbility {
public ChampionAbility(Card card, SubType... subtypes) {
super(Zone.BATTLEFIELD, null);
+ this.etbObjectDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(card);
+
List subTypes = Arrays.asList(subtypes);
FilterControlledPermanent filter;
switch (subTypes.size()) {
@@ -105,6 +108,7 @@ public class ChampionAbility extends StaticAbility {
protected ChampionAbility(final ChampionAbility ability) {
super(ability);
this.objectDescription = ability.objectDescription;
+ this.etbObjectDescription = ability.etbObjectDescription;
}
@Override
@@ -115,7 +119,7 @@ public class ChampionAbility extends StaticAbility {
@Override
public String getRule() {
return "Champion " + CardUtil.addArticle(objectDescription)
- + " (When this enters the battlefield, sacrifice it unless you exile another " + objectDescription
+ + " (When " + etbObjectDescription + " enters, sacrifice it unless you exile another " + objectDescription
+ " you control. When this leaves the battlefield, that card returns to the battlefield.)";
}
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java b/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java
index b002f4a8931..835f6a1b5eb 100644
--- a/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java
@@ -18,7 +18,7 @@ public class DecayedAbility extends StaticAbility {
super(Zone.BATTLEFIELD, new CantBlockSourceEffect(Duration.WhileOnBattlefield));
this.addSubAbility(new AttacksTriggeredAbility(new CreateDelayedTriggeredAbilityEffect(
new AtTheEndOfCombatDelayedTriggeredAbility(new SacrificeSourceEffect())
- ).setText("sacrifice it at end of combat")).setTriggerPhrase("When {this} attacks, "));
+ ).setText("sacrifice it at end of combat")).setTriggerPhrase("When {this} attacks, ").setRuleVisible(false));
}
private DecayedAbility(final DecayedAbility ability) {
@@ -32,6 +32,6 @@ public class DecayedAbility extends StaticAbility {
@Override
public String getRule() {
- return "decayed";
+ return "decayed (This creature can't block. When it attacks, sacrifice it at end of combat.)";
}
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java b/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java
index f793e3e4ce8..e43fbecdeaa 100644
--- a/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java
@@ -63,10 +63,16 @@ public class DelveAbility extends SimpleStaticAbility implements AlternateManaPa
private static final DynamicValue cardsInGraveyard = new CardsInControllerGraveyardCount();
- public DelveAbility() {
+ private boolean useSourceExileZone;
+
+ /**
+ * @param useSourceExileZone - keep exiled cards in linked source zone, so next ability can find it
+ */
+ public DelveAbility(boolean useSourceExileZone) {
super(Zone.ALL, null);
this.setRuleAtTheTop(true);
this.addHint(new ValueHint("Cards in your graveyard", cardsInGraveyard));
+ this.useSourceExileZone = useSourceExileZone;
}
protected DelveAbility(final DelveAbility ability) {
@@ -101,8 +107,11 @@ public class DelveAbility extends SimpleStaticAbility implements AlternateManaPa
unpaidAmount = 1;
}
specialAction.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(
- 0, Math.min(controller.getGraveyard().size(), unpaidAmount),
- new FilterCard("cards from your graveyard"), true)));
+ 0,
+ Math.min(controller.getGraveyard().size(), unpaidAmount),
+ new FilterCard("cards from your graveyard"),
+ true
+ )).withSourceExileZone(this.useSourceExileZone));
if (specialAction.canActivate(source.getControllerId(), game).canActivate()) {
game.getState().getSpecialActions().add(specialAction);
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java b/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java
index 5779883b9fe..7e9d239c38e 100644
--- a/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java
@@ -9,7 +9,6 @@ import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
-import mage.game.events.GameEvent.EventType;
import mage.players.Player;
import mage.util.CardUtil;
@@ -63,13 +62,16 @@ class DredgeEffect extends ReplacementEffectImpl {
if (sourceCard == null) {
return false;
}
- Player owner = game.getPlayer(game.getCard(source.getSourceId()).getOwnerId());
- if (owner != null
- && owner.getLibrary().size() >= amount
- && owner.chooseUse(outcome, new StringBuilder("Dredge ").append(sourceCard.getLogName()).
- append("? (").append(amount).append(" cards are milled)").toString(), source, game)) {
+ Player owner = game.getPlayer(sourceCard.getOwnerId());
+ if (owner == null) {
+ return false;
+ }
+
+ String message = "Dredge " + sourceCard.getLogName() + "? (" + amount + " cards are milled)";
+
+ if (owner.getLibrary().size() >= amount && owner.chooseUse(outcome, message, source, game)) {
if (!game.isSimulation()) {
- game.informPlayers(new StringBuilder(owner.getLogName()).append(" dredges ").append(sourceCard.getLogName()).toString());
+ game.informPlayers(owner.getLogName() + " dredges " + sourceCard.getLogName() + CardUtil.getSourceLogName(game, source));
}
owner.millCards(amount, source, game);
owner.moveCards(sourceCard, Zone.HAND, source, game);
@@ -85,9 +87,14 @@ class DredgeEffect extends ReplacementEffectImpl {
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
- Player owner = game.getPlayer(game.getCard(source.getSourceId()).getOwnerId());
- return (owner != null
- && event.getPlayerId().equals(owner.getId())
- && owner.getLibrary().size() >= amount);
+ Card card = game.getCard(source.getSourceId());
+ if (card == null) {
+ return false;
+ }
+ Player owner = game.getPlayer(card.getOwnerId());
+ if (owner == null) {
+ return false;
+ }
+ return event.getPlayerId().equals(owner.getId()) && owner.getLibrary().size() >= amount;
}
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java b/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java
index 5e08f7bef1a..4142d96a8eb 100644
--- a/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java
@@ -36,21 +36,25 @@ import java.util.*;
public class HideawayAbility extends EntersBattlefieldTriggeredAbility {
private final int amount;
+ private final String etbObjectDescription;
- public HideawayAbility(int amount) {
+ public HideawayAbility(Card card, int amount) {
super(new HideawayExileEffect(amount));
this.amount = amount;
this.addWatcher(new HideawayWatcher());
+
+ this.etbObjectDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(card);
}
private HideawayAbility(final HideawayAbility ability) {
super(ability);
this.amount = ability.amount;
+ this.etbObjectDescription = ability.etbObjectDescription;
}
@Override
public String getRule() {
- return "Hideaway " + this.amount + " (When this permanent enters the battlefield, look at the top "
+ return "Hideaway " + this.amount + " (When " + this.etbObjectDescription + " enters, look at the top "
+ CardUtil.numberToText(this.amount) + " cards of your library, exile one face down, " +
"then put the rest on the bottom of your library in a random order.)";
}
diff --git a/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java b/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java
index 1c8d8c5516c..ecbafd688f4 100644
--- a/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java
@@ -1,17 +1,21 @@
package mage.abilities.keyword;
import mage.abilities.StaticAbility;
+import mage.abilities.dynamicvalue.common.ControllerSpeedCount;
+import mage.abilities.hint.Hint;
+import mage.abilities.hint.ValueHint;
import mage.constants.Zone;
/**
- * TODO: Implement this
- *
* @author TheElk801
*/
public class StartYourEnginesAbility extends StaticAbility {
+ private static final Hint hint = new ValueHint("Your current speed", ControllerSpeedCount.instance);
+
public StartYourEnginesAbility() {
super(Zone.BATTLEFIELD, null);
+ this.addHint(hint);
}
private StartYourEnginesAbility(final StartYourEnginesAbility ability) {
@@ -25,6 +29,6 @@ public class StartYourEnginesAbility extends StaticAbility {
@Override
public String getRule() {
- return "Start your engines!";
+ return "start your engines!";
}
}
diff --git a/Mage/src/main/java/mage/abilities/mana/builder/common/ActivatedAbilityManaBuilder.java b/Mage/src/main/java/mage/abilities/mana/builder/common/ActivatedAbilityManaBuilder.java
new file mode 100644
index 00000000000..9de687331b5
--- /dev/null
+++ b/Mage/src/main/java/mage/abilities/mana/builder/common/ActivatedAbilityManaBuilder.java
@@ -0,0 +1,52 @@
+package mage.abilities.mana.builder.common;
+
+import mage.ConditionalMana;
+import mage.Mana;
+import mage.abilities.Ability;
+import mage.abilities.condition.Condition;
+import mage.abilities.costs.Cost;
+import mage.abilities.mana.builder.ConditionalManaBuilder;
+import mage.abilities.mana.conditional.ManaCondition;
+import mage.game.Game;
+
+import java.util.UUID;
+
+/**
+ * @author TheElk801
+ */
+public class ActivatedAbilityManaBuilder extends ConditionalManaBuilder {
+
+ @Override
+ public ConditionalMana build(Object... options) {
+ return new ActivatedAbilityConditionalMana(this.mana);
+ }
+
+ @Override
+ public String getRule() {
+ return "Spend this mana only to activate abilities";
+ }
+}
+
+class ActivatedAbilityConditionalMana extends ConditionalMana {
+
+ public ActivatedAbilityConditionalMana(Mana mana) {
+ super(mana);
+ staticText = "Spend this mana only to activate abilities";
+ addCondition(new ActivatedAbilityManaCondition());
+ }
+}
+
+class ActivatedAbilityManaCondition extends ManaCondition implements Condition {
+
+ @Override
+ public boolean apply(Game game, Ability source) {
+ return source != null
+ && !source.isActivated()
+ && source.isActivatedAbility();
+ }
+
+ @Override
+ public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) {
+ return apply(game, source);
+ }
+}
diff --git a/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java b/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java
index b3d2851fb42..2383d0bbf9a 100644
--- a/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java
+++ b/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java
@@ -42,7 +42,7 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl {
//Remove useless data
ability.getEffects().clear();
- for(Watcher watcher : ability.getWatchers()) {
+ for (Watcher watcher : ability.getWatchers()) {
super.addWatcher(watcher);
}
@@ -51,6 +51,21 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl {
}
}
setTriggerPhrase(generateTriggerPhrase());
+
+ // runtime check: enters and sacrifice must use Zone.ALL, see https://github.com/magefree/mage/issues/12826
+ boolean haveEnters = false;
+ boolean haveSacrifice = false;
+ for (Ability ability : abilities) {
+ if (ability.getRule().toLowerCase(Locale.ENGLISH).contains("enters")) {
+ haveEnters = true;
+ }
+ if (ability.getRule().toLowerCase(Locale.ENGLISH).contains("sacrifice")) {
+ haveSacrifice = true;
+ }
+ }
+ if (zone != Zone.ALL && haveEnters && haveSacrifice) {
+ throw new IllegalArgumentException("Wrong code usage: on enters and sacrifice OrTriggeredAbility must use Zone.ALL");
+ }
}
public OrTriggeredAbility(OrTriggeredAbility ability) {
diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java
index fa2462a09ed..e6c7aa4b9ae 100644
--- a/Mage/src/main/java/mage/cards/CardImpl.java
+++ b/Mage/src/main/java/mage/cards/CardImpl.java
@@ -4,6 +4,7 @@ import mage.MageObject;
import mage.MageObjectImpl;
import mage.Mana;
import mage.abilities.*;
+import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.continuous.HasSubtypesSourceEffect;
import mage.abilities.keyword.ChangelingAbility;
@@ -343,6 +344,19 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
}
}
}
+
+ // rules fix: workaround to fix "When {this} enters" into "When this xxx enters"
+ if (EntersBattlefieldTriggeredAbility.ENABLE_TRIGGER_PHRASE_AUTO_FIX) {
+ if (ability instanceof TriggeredAbility) {
+ TriggeredAbility triggeredAbility = ((TriggeredAbility) ability);
+ if (triggeredAbility.getTriggerPhrase() != null && triggeredAbility.getTriggerPhrase().startsWith("When {this} enters")) {
+ // there are old sets with old oracle, but it's ok for newer sets, so keep that rules fix
+ // see https://github.com/magefree/mage/issues/12791
+ String etbDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(this);
+ triggeredAbility.setTriggerPhrase(triggeredAbility.getTriggerPhrase().replace("{this}", etbDescription));
+ }
+ }
+ }
}
protected void addAbility(Ability ability, Watcher watcher) {
diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java
index a8ce11f6408..24484308724 100644
--- a/Mage/src/main/java/mage/cards/repository/CardRepository.java
+++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java
@@ -147,6 +147,9 @@ public enum CardRepository {
if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) {
namesList.add(card.getMeldsToCardName());
}
+ if (card.getAdventureSpellName() != null && !card.getAdventureSpellName().isEmpty()) {
+ namesList.add(card.getAdventureSpellName());
+ }
}
public static Boolean haveSnowLands(String setCode) {
@@ -157,7 +160,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
@@ -173,7 +176,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -190,7 +193,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
Where where = qb.where();
where.and(
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
@@ -211,7 +214,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -228,7 +231,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -245,7 +248,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
List results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
@@ -262,7 +265,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
Where where = qb.where();
where.and(
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
@@ -283,7 +286,7 @@ public enum CardRepository {
Set names = new TreeSet<>();
try {
QueryBuilder qb = cardsDao.queryBuilder();
- qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
+ qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
Where where = qb.where();
where.and(
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
diff --git a/Mage/src/main/java/mage/cards/repository/TokenRepository.java b/Mage/src/main/java/mage/cards/repository/TokenRepository.java
index ed25e535d39..a3a86c78c9e 100644
--- a/Mage/src/main/java/mage/cards/repository/TokenRepository.java
+++ b/Mage/src/main/java/mage/cards/repository/TokenRepository.java
@@ -35,6 +35,7 @@ public enum TokenRepository {
public static final String XMAGE_IMAGE_NAME_RADIATION = "Radiation";
public static final String XMAGE_IMAGE_NAME_THE_RING = "The Ring";
public static final String XMAGE_IMAGE_NAME_HELPER_EMBLEM = "Helper Emblem";
+ public static final String XMAGE_IMAGE_NAME_SPEED = "Speed";
private static final Logger logger = Logger.getLogger(TokenRepository.class);
@@ -310,6 +311,9 @@ public enum TokenRepository {
// The Ring
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_RING, 1, "https://api.scryfall.com/cards/tltr/H13/en?format=image"));
+ // Speed
+ res.add(createXmageToken(XMAGE_IMAGE_NAME_SPEED, 1, "https://api.scryfall.com/cards/tdft/14/en?format=image&&face=back"));
+
// Helper emblem (for global card hints)
// use backface for it
res.add(createXmageToken(XMAGE_IMAGE_NAME_HELPER_EMBLEM, 1, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"));
diff --git a/Mage/src/main/java/mage/constants/TargetController.java b/Mage/src/main/java/mage/constants/TargetController.java
index 2737b52adba..819f621c78f 100644
--- a/Mage/src/main/java/mage/constants/TargetController.java
+++ b/Mage/src/main/java/mage/constants/TargetController.java
@@ -16,6 +16,7 @@ import java.util.UUID;
public enum TargetController {
ACTIVE,
+ INACTIVE,
ANY,
YOU,
NOT_YOU,
@@ -85,6 +86,8 @@ public enum TargetController {
return card.isOwnedBy(input.getSource().getFirstTarget());
case ACTIVE:
return card.isOwnedBy(game.getActivePlayerId());
+ case INACTIVE:
+ return !card.isOwnedBy(game.getActivePlayerId());
case MONARCH:
return card.isOwnedBy(game.getMonarchId());
case ANY:
@@ -130,6 +133,8 @@ public enum TargetController {
return player.getId().equals(input.getSource().getFirstTarget());
case ACTIVE:
return game.isActivePlayer(player.getId());
+ case INACTIVE:
+ return !game.isActivePlayer(player.getId());
case MONARCH:
return player.getId().equals(game.getMonarchId());
default:
@@ -168,6 +173,8 @@ public enum TargetController {
return !object.isControlledBy(playerId);
case ACTIVE:
return object.isControlledBy(game.getActivePlayerId());
+ case INACTIVE:
+ return !object.isControlledBy(game.getActivePlayerId());
case ENCHANTED:
Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game);
return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo());
diff --git a/Mage/src/main/java/mage/designations/DesignationType.java b/Mage/src/main/java/mage/designations/DesignationType.java
index 62b77b28c61..dd95ff9808f 100644
--- a/Mage/src/main/java/mage/designations/DesignationType.java
+++ b/Mage/src/main/java/mage/designations/DesignationType.java
@@ -6,8 +6,8 @@ package mage.designations;
public enum DesignationType {
THE_MONARCH("The Monarch"), // global
CITYS_BLESSING("City's Blessing"), // per player
- THE_INITIATIVE("The Initiative"); // global
-
+ THE_INITIATIVE("The Initiative"), // global
+ SPEED("Speed"); // per player
private final String text;
DesignationType(String text) {
@@ -18,5 +18,4 @@ public enum DesignationType {
public String toString() {
return text;
}
-
}
diff --git a/Mage/src/main/java/mage/designations/Speed.java b/Mage/src/main/java/mage/designations/Speed.java
new file mode 100644
index 00000000000..8781ffe847e
--- /dev/null
+++ b/Mage/src/main/java/mage/designations/Speed.java
@@ -0,0 +1,122 @@
+package mage.designations;
+
+import mage.MageObject;
+import mage.abilities.Ability;
+import mage.abilities.TriggeredAbilityImpl;
+import mage.abilities.effects.OneShotEffect;
+import mage.cards.repository.TokenInfo;
+import mage.cards.repository.TokenRepository;
+import mage.constants.Outcome;
+import mage.constants.Zone;
+import mage.game.Game;
+import mage.game.events.GameEvent;
+import mage.players.Player;
+
+import java.util.Optional;
+
+/**
+ * @author TheElk801
+ */
+public class Speed extends Designation {
+
+ public Speed() {
+ super(DesignationType.SPEED);
+ addAbility(new SpeedTriggeredAbility());
+
+ TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_SPEED, null);
+ if (foundInfo != null) {
+ this.setExpansionSetCode(foundInfo.getSetCode());
+ this.setUsesVariousArt(true);
+ this.setCardNumber("");
+ this.setImageFileName(""); // use default
+ this.setImageNumber(foundInfo.getImageNumber());
+ } else {
+ // how-to fix: add image to the tokens-database TokenRepository->loadXmageTokens
+ throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_SPEED);
+ }
+ }
+
+ private Speed(final Speed card) {
+ super(card);
+ }
+
+ @Override
+ public Speed copy() {
+ return new Speed(this);
+ }
+}
+
+class SpeedTriggeredAbility extends TriggeredAbilityImpl {
+
+ SpeedTriggeredAbility() {
+ super(Zone.ALL, new SpeedEffect());
+ setTriggersLimitEachTurn(1);
+ }
+
+ private SpeedTriggeredAbility(final SpeedTriggeredAbility ability) {
+ super(ability);
+ }
+
+ @Override
+ public SpeedTriggeredAbility copy() {
+ return new SpeedTriggeredAbility(this);
+ }
+
+ @Override
+ public boolean checkEventType(GameEvent event, Game game) {
+ return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER;
+ }
+
+ @Override
+ public boolean checkTrigger(GameEvent event, Game game) {
+ return game.isActivePlayer(getControllerId())
+ && game
+ .getOpponents(getControllerId())
+ .contains(event.getTargetId());
+ }
+
+ @Override
+ public boolean checkInterveningIfClause(Game game) {
+ return Optional
+ .ofNullable(getControllerId())
+ .map(game::getPlayer)
+ .map(Player::getSpeed)
+ .map(x -> x < 4)
+ .orElse(false);
+ }
+
+ @Override
+ public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
+ return true;
+ }
+
+ @Override
+ public String getRule() {
+ return "Whenever one or more opponents lose life during your turn, if your speed is less than 4, " +
+ "increase your speed by 1. This ability triggers only once each turn.";
+ }
+}
+
+class SpeedEffect extends OneShotEffect {
+
+ SpeedEffect() {
+ super(Outcome.Benefit);
+ }
+
+ private SpeedEffect(final SpeedEffect effect) {
+ super(effect);
+ }
+
+ @Override
+ public SpeedEffect copy() {
+ return new SpeedEffect(this);
+ }
+
+ @Override
+ public boolean apply(Game game, Ability source) {
+ Optional.ofNullable(source.getControllerId())
+ .map(game::getPlayer)
+ .ifPresent(player -> player.increaseSpeed(game));
+ return true;
+ }
+}
diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java
index 57a18bb9a08..dfb7cc6b4c9 100644
--- a/Mage/src/main/java/mage/filter/StaticFilters.java
+++ b/Mage/src/main/java/mage/filter/StaticFilters.java
@@ -754,6 +754,28 @@ public final class StaticFilters {
FILTER_PERMANENT_CREATURE_OR_LAND.setLockedFilter(true);
}
+ public static final FilterPermanent FILTER_PERMANENT_CREATURE_OR_VEHICLE = new FilterPermanent("creature or Vehicle");
+
+ static {
+ FILTER_PERMANENT_CREATURE_OR_VEHICLE.add(
+ Predicates.or(
+ CardType.CREATURE.getPredicate(),
+ SubType.VEHICLE.getPredicate()
+ ));
+ FILTER_PERMANENT_CREATURE_OR_VEHICLE.setLockedFilter(true);
+ }
+
+ public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE = new FilterControlledPermanent("creature or Vehicle you control");
+
+ static {
+ FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE.add(
+ Predicates.or(
+ CardType.CREATURE.getPredicate(),
+ SubType.VEHICLE.getPredicate()
+ ));
+ FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE.setLockedFilter(true);
+ }
+
public static final FilterCreaturePermanent FILTER_PERMANENT_A_CREATURE = new FilterCreaturePermanent("a creature");
static {
diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java
index 0dd53c256e2..2df39b9016d 100644
--- a/Mage/src/main/java/mage/game/Game.java
+++ b/Mage/src/main/java/mage/game/Game.java
@@ -174,7 +174,7 @@ public interface Game extends MageItem, Serializable, Copyable {
*
* Warning, it will return leaved players until end of turn. For dialogs and one shot effects use excludeLeavedPlayers
*/
- // TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers
+ // TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers, see #13289
default Set getOpponents(UUID playerId) {
return getOpponents(playerId, false);
}
@@ -314,7 +314,9 @@ public interface Game extends MageItem, Serializable, Copyable {
Player getLosingPlayer();
- int getTotalErrorsCount();
+ int getTotalErrorsCount(); // debug only
+
+ int getTotalEffectsCount(); // debug only
//client event methods
void addTableEventListener(Listener listener);
diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java
index 1f554dbdd4e..c60f6499165 100644
--- a/Mage/src/main/java/mage/game/GameImpl.java
+++ b/Mage/src/main/java/mage/game/GameImpl.java
@@ -2862,6 +2862,13 @@ public abstract class GameImpl implements Game {
}
}
}
+
+ // Start Your Engines // Max Speed
+ if (perm.getAbilities(this).containsClass(StartYourEnginesAbility.class)) {
+ Optional.ofNullable(perm.getControllerId())
+ .map(this::getPlayer)
+ .ifPresent(player -> player.initSpeed(this));
+ }
}
//201300713 - 704.5k
// If a player controls two or more legendary permanents with the same name, that player
@@ -3169,7 +3176,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.INFO, message, this);
}
@@ -3178,7 +3185,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.STATUS, message, withTime, withTurnInfo, this);
}
@@ -3187,7 +3194,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.UPDATE, null, this);
getState().clearLookedAt();
getState().clearRevealed();
@@ -3198,23 +3205,23 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.END_GAME_INFO, null, this);
}
@Override
public void fireErrorEvent(String message, Exception ex) {
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.ERROR, message, ex, this);
}
- private void makeSureCalledOutsideLayersEffects() {
+ private void makeSureCalledOutsideLayerEffects() {
// very slow, enable/comment it for debug or load/stability tests only
// TODO: enable check and remove/rework all wrong usages
if (true) return;
Arrays.stream(Thread.currentThread().getStackTrace()).forEach(e -> {
if (e.toString().contains("GameState.applyEffects")) {
- throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?");
+ throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?)");
}
});
}
@@ -3542,11 +3549,6 @@ public abstract class GameImpl implements Game {
}
- protected void removeCreaturesFromCombat() {
- //20091005 - 511.3
- getCombat().endCombat(this);
- }
-
@Override
public ContinuousEffects getContinuousEffects() {
return state.getContinuousEffects();
@@ -3689,6 +3691,11 @@ public abstract class GameImpl implements Game {
return this.totalErrorsCount.get();
}
+ @Override
+ public int getTotalEffectsCount() {
+ return this.getContinuousEffects().getTotalEffectsCount();
+ }
+
@Override
public void cheat(UUID ownerId, Map commands) {
if (commands != null) {
@@ -3885,7 +3892,7 @@ public abstract class GameImpl implements Game {
@Override
public void initTimer(UUID playerId) {
if (priorityTime > 0) {
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.INIT_TIMER, playerId, null, this);
}
}
@@ -3893,7 +3900,7 @@ public abstract class GameImpl implements Game {
@Override
public void resumeTimer(UUID playerId) {
if (priorityTime > 0) {
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.RESUME_TIMER, playerId, null, this);
}
}
@@ -3901,7 +3908,7 @@ public abstract class GameImpl implements Game {
@Override
public void pauseTimer(UUID playerId) {
if (priorityTime > 0) {
- makeSureCalledOutsideLayersEffects();
+ makeSureCalledOutsideLayerEffects();
tableEventSource.fireTableEvent(EventType.PAUSE_TIMER, playerId, null, this);
}
}
diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java
index 38c300969ca..5a433b7e04f 100644
--- a/Mage/src/main/java/mage/game/combat/Combat.java
+++ b/Mage/src/main/java/mage/game/combat/Combat.java
@@ -312,7 +312,23 @@ public class Combat implements Serializable, Copyable {
Player player = game.getPlayer(attackingPlayerId);
if (player != null) {
if (groups.size() > 0) {
- game.informPlayers(player.getLogName() + " attacks with " + groups.size() + (groups.size() == 1 ? " creature" : " creatures"));
+ String defendersInfo = groups.stream()
+ .map(g -> g.defenderId)
+ .distinct()
+ .map(id -> {
+ Player defPlayer = game.getPlayer(id);
+ if (defPlayer != null) {
+ return defPlayer.getLogName();
+ }
+ Permanent defPermanent = game.getPermanentOrLKIBattlefield(id);
+ if (defPermanent != null) {
+ return defPermanent.getLogName();
+ }
+ return null;
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.joining(", "));
+ game.informPlayers(player.getLogName() + " attacks " + defendersInfo + " with " + groups.size() + (groups.size() == 1 ? " creature" : " creatures"));
} else {
game.informPlayers(player.getLogName() + " skip attack");
}
@@ -670,9 +686,25 @@ public class Combat implements Serializable, Copyable {
}
// choosing until good block configuration
+ int aiTries = 0;
while (true) {
+ aiTries++;
+
+ if (controller.isComputer() && aiTries > 20) {
+ // TODO: AI must use real attacker/blocker configuration with all possible combination
+ // (current human like logic will fail sometime, e.g. with menace and big/low creatures)
+ // real game: send warning
+ // test: fast fail
+ game.informPlayers(controller.getLogName() + ": WARNING - AI can't find good blocker combination and will skip it - report your battlefield to github - " + game.getCombat());
+ if (controller.isTestsMode()) {
+ // how-to fix: AI code must support failed abilities or use cases
+ throw new IllegalArgumentException("AI can't find good blocker combination");
+ }
+ break;
+ }
+
// declare normal blockers
- // TODO: need reseach - is it possible to concede on bad blocker configuration (e.g. user can't continue)
+ // TODO: need research - is it possible to concede on bad blocker configuration (e.g. user can't continue)
controller.selectBlockers(source, game, defenderId);
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
return;
@@ -776,18 +808,16 @@ public class Combat implements Serializable, Copyable {
/**
* Check the block restrictions
*
- * @param player
- * @param game
* @return false - if block restrictions were not complied
*/
- public boolean checkBlockRestrictions(Player player, Game game) {
+ public boolean checkBlockRestrictions(Player defender, Game game) {
int count = 0;
boolean blockWasLegal = true;
for (CombatGroup group : groups) {
count += group.getBlockers().size();
}
for (CombatGroup group : groups) {
- blockWasLegal &= group.checkBlockRestrictions(game, count);
+ blockWasLegal &= group.checkBlockRestrictions(game, defender, count);
}
return blockWasLegal;
}
diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java
index c5b5a9cb677..c7caca995fa 100644
--- a/Mage/src/main/java/mage/game/combat/CombatGroup.java
+++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java
@@ -243,7 +243,7 @@ public class CombatGroup implements Serializable, Copyable {
* @param first true for first strike damage step, false for normal damage step
* @return true if permanent should deal damage this step
*/
- private boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) {
+ public static boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) {
if (perm == null) {
return false;
}
@@ -773,11 +773,27 @@ public class CombatGroup implements Serializable, Copyable {
}
}
- public boolean checkBlockRestrictions(Game game, int blockersCount) {
+ public boolean checkBlockRestrictions(Game game, Player defender, int blockersCount) {
boolean blockWasLegal = true;
if (attackers.isEmpty()) {
return blockWasLegal;
}
+
+ // collect possible blockers
+ Map> possibleBlockers = new HashMap<>();
+ for (UUID attackerId : attackers) {
+ Permanent attacker = game.getPermanent(attackerId);
+ Set goodBlockers = new HashSet<>();
+ for (Permanent blocker : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, defender.getId(), game)) {
+ if (blocker.canBlock(attackerId, game)) {
+ goodBlockers.add(blocker.getId());
+ }
+ }
+ possibleBlockers.put(attacker.getId(), goodBlockers);
+ }
+
+ // effects: can't block alone
+ // too much blockers
if (blockersCount == 1) {
List toBeRemoved = new ArrayList<>();
for (UUID blockerId : getBlockers()) {
@@ -802,7 +818,8 @@ public class CombatGroup implements Serializable, Copyable {
for (UUID uuid : attackers) {
Permanent attacker = game.getPermanent(uuid);
if (attacker != null && this.blocked) {
- // Check if there are enough blockers to have a legal block
+ // effects: can't be blocked except by xxx or more creatures
+ // too few blockers
if (attacker.getMinBlockedBy() > 1 && !blockers.isEmpty() && blockers.size() < attacker.getMinBlockedBy()) {
for (UUID blockerId : new ArrayList<>(blockers)) {
game.getCombat().removeBlocker(blockerId, game);
@@ -812,9 +829,16 @@ public class CombatGroup implements Serializable, Copyable {
if (!game.isSimulation()) {
game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded.");
}
- blockWasLegal = false;
+
+ // if there aren't any possible blocker configuration then it's legal due mtg rules
+ // warning, it's affect AI related logic like other block auto-fixes does, see https://github.com/magefree/mage/pull/13182
+ if (attacker.getMinBlockedBy() <= possibleBlockers.getOrDefault(attacker.getId(), Collections.emptySet()).size()) {
+ blockWasLegal = false;
+ }
}
- // Check if there are too many blockers (maxBlockedBy = 0 means no restrictions)
+
+ // effects: can't be blocked by more than xxx creature
+ // too much blockers
if (attacker.getMaxBlockedBy() > 0 && attacker.getMaxBlockedBy() < blockers.size()) {
for (UUID blockerId : new ArrayList<>(blockers)) {
game.getCombat().removeBlocker(blockerId, game);
@@ -827,6 +851,7 @@ public class CombatGroup implements Serializable, Copyable {
.append(attacker.getMaxBlockedBy() == 1 ? " creature." : " creatures.")
.append(" Blockers discarded.").toString());
}
+
blockWasLegal = false;
}
}
diff --git a/Mage/src/main/java/mage/game/command/emblems/ChandraSparkHunterEmblem.java b/Mage/src/main/java/mage/game/command/emblems/ChandraSparkHunterEmblem.java
new file mode 100644
index 00000000000..843f2f6bb4f
--- /dev/null
+++ b/Mage/src/main/java/mage/game/command/emblems/ChandraSparkHunterEmblem.java
@@ -0,0 +1,38 @@
+package mage.game.command.emblems;
+
+import mage.abilities.Ability;
+import mage.abilities.common.EntersBattlefieldAllTriggeredAbility;
+import mage.abilities.effects.common.DamageTargetEffect;
+import mage.constants.Zone;
+import mage.filter.StaticFilters;
+import mage.game.command.Emblem;
+import mage.target.common.TargetAnyTarget;
+
+/**
+ * @author TheElk801
+ */
+public final class ChandraSparkHunterEmblem extends Emblem {
+
+ /**
+ * Emblem with "Whenever an artifact you control enters, this emblem deals 3 damage to any target."
+ */
+
+ public ChandraSparkHunterEmblem() {
+ super("Emblem Chandra");
+ Ability ability = new EntersBattlefieldAllTriggeredAbility(
+ Zone.COMMAND, new DamageTargetEffect(3, "this emblem"),
+ StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT, false
+ );
+ ability.addTarget(new TargetAnyTarget());
+ this.getAbilities().add(ability);
+ }
+
+ private ChandraSparkHunterEmblem(final ChandraSparkHunterEmblem card) {
+ super(card);
+ }
+
+ @Override
+ public ChandraSparkHunterEmblem copy() {
+ return new ChandraSparkHunterEmblem(this);
+ }
+}
diff --git a/Mage/src/main/java/mage/game/permanent/PermanentCard.java b/Mage/src/main/java/mage/game/permanent/PermanentCard.java
index 7242b774892..b7ef957b100 100644
--- a/Mage/src/main/java/mage/game/permanent/PermanentCard.java
+++ b/Mage/src/main/java/mage/game/permanent/PermanentCard.java
@@ -7,13 +7,11 @@ import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.keyword.NightboundAbility;
import mage.abilities.keyword.TransformAbility;
-import mage.cards.Card;
-import mage.cards.LevelerCard;
-import mage.cards.ModalDoubleFacedCard;
-import mage.cards.SplitCard;
+import mage.cards.*;
import mage.constants.SpellAbilityType;
import mage.game.Game;
import mage.game.events.ZoneChangeEvent;
+import mage.players.Player;
import java.util.UUID;
@@ -45,7 +43,7 @@ public class PermanentCard extends PermanentImpl {
}
// usage check: you must put to play only real card's part
- // if you use it in test code then call CardUtil.getDefaultCardSideForBattlefield for default side
+ // if you use it in test code or for permanent's copy effects then call CardUtil.getDefaultCardSideForBattlefield for default side
// it's a basic check and still allows to create permanent from instant or sorcery
boolean goodForBattlefield = true;
if (card instanceof ModalDoubleFacedCard) {
@@ -185,7 +183,10 @@ public class PermanentCard extends PermanentImpl {
// 701.34g. If a manifested permanent that's represented by an instant or sorcery card would turn face up,
// its controller reveals it and leaves it face down. Abilities that trigger whenever a permanent
// is turned face up won't trigger.
- // TODO: add reveal effect
+ Player player = game.getPlayer(source.getControllerId());
+ if (player != null) {
+ player.revealCards(source, new CardsImpl(this), game);
+ }
return false;
}
if (super.turnFaceUp(source, game, playerId)) {
diff --git a/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java b/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java
index 1be43c33bcc..707718dc4ca 100644
--- a/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java
+++ b/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java
@@ -66,7 +66,7 @@ class AshiokNightmareMuseTokenEffect extends OneShotEffect {
return false;
}
Set cards = game
- .getOpponents(source.getControllerId())
+ .getOpponents(source.getControllerId(), true)
.stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
diff --git a/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java
index 42a7f067a44..176a4829c59 100644
--- a/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java
+++ b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java
@@ -14,8 +14,6 @@ import mage.constants.Zone;
*/
public final class ConsumingBlobOozeToken extends TokenImpl {
- private static final DynamicValue powerValue = CardTypesInGraveyardCount.YOU;
-
public ConsumingBlobOozeToken() {
super("Ooze Token", "green Ooze creature token with \"This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1.\"");
cardType.add(CardType.CREATURE);
@@ -26,7 +24,9 @@ public final class ConsumingBlobOozeToken extends TokenImpl {
toughness = new MageInt(1);
// This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1.
- this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue)));
+ this.addAbility(new SimpleStaticAbility(Zone.ALL,
+ new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.YOU)
+ ).addHint(CardTypesInGraveyardCount.YOU.getHint()));
}
private ConsumingBlobOozeToken(final ConsumingBlobOozeToken token) {
diff --git a/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java b/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java
index e96c267aae3..e5fe9a90055 100644
--- a/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java
+++ b/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java
@@ -11,7 +11,7 @@ import mage.constants.SubType;
public final class PilotSaddleCrewToken extends TokenImpl {
public PilotSaddleCrewToken() {
- super("Pilot Token", "1/1 colorless Pilot creature token with \"This creature saddles Mounts and crews Vehicles as though its power were 2 greater.\"");
+ super("Pilot Token", "1/1 colorless Pilot creature token with \"This token saddles Mounts and crews Vehicles as though its power were 2 greater.\"");
cardType.add(CardType.CREATURE);
subtype.add(SubType.PILOT);
power = new MageInt(1);
diff --git a/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java b/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java
index c9107b97f5d..4c24a3ff76d 100644
--- a/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java
+++ b/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java
@@ -24,7 +24,9 @@ public final class TarmogoyfToken extends TokenImpl {
toughness = new MageInt(1);
// Tarmogoyf's power is equal to the number of card types among cards in all graveyards and its toughness is equal to that number plus 1.
- this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL)));
+ this.addAbility(new SimpleStaticAbility(Zone.ALL,
+ new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL)
+ ).addHint(CardTypesInGraveyardCount.ALL.getHint()));
}
private TarmogoyfToken(final TarmogoyfToken token) {
diff --git a/Mage/src/main/java/mage/game/permanent/token/VehicleToken.java b/Mage/src/main/java/mage/game/permanent/token/VehicleToken.java
new file mode 100644
index 00000000000..58a5031d0c6
--- /dev/null
+++ b/Mage/src/main/java/mage/game/permanent/token/VehicleToken.java
@@ -0,0 +1,30 @@
+package mage.game.permanent.token;
+
+import mage.MageInt;
+import mage.abilities.keyword.CrewAbility;
+import mage.constants.CardType;
+import mage.constants.SubType;
+
+/**
+ * @author TheElk801
+ */
+public final class VehicleToken extends TokenImpl {
+
+ public VehicleToken() {
+ super("Vehicle Token", "3/2 colorless Vehicle artifact token with crew 1");
+ cardType.add(CardType.ARTIFACT);
+ subtype.add(SubType.VEHICLE);
+ power = new MageInt(3);
+ toughness = new MageInt(2);
+
+ this.addAbility(new CrewAbility(1));
+ }
+
+ private VehicleToken(final VehicleToken token) {
+ super(token);
+ }
+
+ public VehicleToken copy() {
+ return new VehicleToken(this);
+ }
+}
diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java
index 9566348bc7c..5e8286c5b6e 100644
--- a/Mage/src/main/java/mage/players/Player.java
+++ b/Mage/src/main/java/mage/players/Player.java
@@ -216,6 +216,14 @@ public interface Player extends MageItem, Copyable {
boolean isDrawsOnOpponentsTurn();
+ int getSpeed();
+
+ void initSpeed(Game game);
+
+ void increaseSpeed(Game game);
+
+ void decreaseSpeed(Game game);
+
/**
* Returns alternative casting costs a player can cast spells for
*
@@ -620,6 +628,7 @@ public interface Player extends MageItem, Copyable {
*
* Warning, if you use it from continuous effect, then check with extra call
* isCanLookAtNextTopLibraryCard
+ * If you use revealCards with face-down permanents, they will be revealed face up.
*
* @param source
* @param name
diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java
index b1b3434cf55..4db86272845 100644
--- a/Mage/src/main/java/mage/players/PlayerImpl.java
+++ b/Mage/src/main/java/mage/players/PlayerImpl.java
@@ -27,6 +27,7 @@ import mage.counters.CounterType;
import mage.counters.Counters;
import mage.designations.Designation;
import mage.designations.DesignationType;
+import mage.designations.Speed;
import mage.filter.FilterCard;
import mage.filter.FilterMana;
import mage.filter.FilterPermanent;
@@ -153,6 +154,7 @@ public abstract class PlayerImpl implements Player, Serializable {
protected boolean canPlotFromTopOfLibrary = false;
protected boolean drawsFromBottom = false;
protected boolean drawsOnOpponentsTurn = false;
+ protected int speed = 0;
protected FilterPermanent sacrificeCostFilter;
protected List alternativeSourceCosts = new ArrayList<>();
@@ -252,6 +254,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary;
this.drawsFromBottom = player.drawsFromBottom;
this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn;
+ this.speed = player.speed;
this.attachments.addAll(player.attachments);
@@ -367,6 +370,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.drawsFromBottom = player.isDrawsFromBottom();
this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn();
this.alternativeSourceCosts = CardUtil.deepCopyObject(((PlayerImpl) player).alternativeSourceCosts);
+ this.speed = player.getSpeed();
this.topCardRevealed = player.isTopCardRevealed();
@@ -480,6 +484,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.canPlotFromTopOfLibrary = false;
this.drawsFromBottom = false;
this.drawsOnOpponentsTurn = false;
+ this.speed = 0;
this.sacrificeCostFilter = null;
this.alternativeSourceCosts.clear();
@@ -1905,7 +1910,11 @@ public abstract class PlayerImpl implements Player, Serializable {
int last = cards.size();
for (Card card : cards.getCards(game)) {
current++;
- sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!)
+ if (card instanceof PermanentCard && card.isFaceDown(game)) {
+ sb.append(GameLog.getColoredObjectName(card.getMainCard()));
+ } else {
+ sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!)
+ }
if (current < last) {
sb.append(", ");
}
@@ -4452,11 +4461,7 @@ public abstract class PlayerImpl implements Player, Serializable {
}
/**
- * Only used for AIs
- *
- * @param ability
- * @param game
- * @return
+ * AI related code
*/
@Override
public List getPlayableOptions(Ability ability, Game game) {
@@ -4477,6 +4482,9 @@ public abstract class PlayerImpl implements Player, Serializable {
return options;
}
+ /**
+ * AI related code
+ */
private void addModeOptions(List options, Ability option, Game game) {
// TODO: support modal spells with more than one selectable mode (also must use max modes filter)
for (Mode mode : option.getModes().values()) {
@@ -4499,11 +4507,18 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
+ /**
+ * AI related code
+ */
protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) {
addTargetOptions(options, option, targetNum, game);
}
+ /**
+ * AI related code
+ */
protected void addTargetOptions(List options, Ability option, int targetNum, Game game) {
+ // TODO: target options calculated for triggered ability too, but do not used in real game
for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) {
Ability newOption = option.copy();
if (target instanceof TargetAmount) {
@@ -4516,7 +4531,7 @@ public abstract class PlayerImpl implements Player, Serializable {
newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true);
}
}
- if (targetNum < option.getTargets().size() - 2) {
+ if (targetNum < option.getTargets().size() - 2) { // wtf
addTargetOptions(options, newOption, targetNum + 1, game);
} else if (!option.getCosts().getTargets().isEmpty()) {
addCostTargetOptions(options, newOption, 0, game);
@@ -4526,6 +4541,9 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
+ /**
+ * AI related code
+ */
private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) {
for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(playerId, option, game)) {
Ability newOption = option.copy();
@@ -4674,6 +4692,37 @@ public abstract class PlayerImpl implements Player, Serializable {
return drawsOnOpponentsTurn;
}
+ @Override
+ public int getSpeed() {
+ return speed;
+ }
+
+ @Override
+ public void initSpeed(Game game) {
+ if (speed > 0) {
+ return;
+ }
+ speed = 1;
+ game.getState().addDesignation(new Speed(), game, getId());
+ game.informPlayers(this.getLogName() + "'s speed is now 1.");
+ }
+
+ @Override
+ public void increaseSpeed(Game game) {
+ if (speed < 4) {
+ speed++;
+ game.informPlayers(this.getLogName() + "'s speed has increased to " + speed);
+ }
+ }
+
+ @Override
+ public void decreaseSpeed(Game game) {
+ if (speed > 1) {
+ speed--;
+ game.informPlayers(this.getLogName() + "'s speed has decreased to " + speed);
+ }
+ }
+
@Override
public boolean autoLoseGame() {
return false;
diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java
index 8ef2e97d69c..9664a0a10ca 100644
--- a/Mage/src/main/java/mage/target/Target.java
+++ b/Mage/src/main/java/mage/target/Target.java
@@ -79,6 +79,9 @@ public interface Target extends Serializable {
boolean isLegal(Ability source, Game game);
+ /**
+ * AI related code. Returns all possible different target combinations
+ */
List extends Target> getTargetOptions(Ability source, Game game);
boolean canChoose(UUID sourceControllerId, Game game);
diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java
index 68107f825a8..d128d38ec9b 100644
--- a/Mage/src/main/java/mage/target/TargetAmount.java
+++ b/Mage/src/main/java/mage/target/TargetAmount.java
@@ -1,11 +1,16 @@
package mage.target;
+import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
+import mage.cards.Card;
import mage.constants.Outcome;
import mage.game.Game;
+import mage.game.permanent.Permanent;
import mage.players.Player;
+import mage.util.DebugUtil;
+import mage.util.RandomUtil;
import java.util.*;
import java.util.stream.Collectors;
@@ -118,45 +123,229 @@ public abstract class TargetAmount extends TargetImpl {
}
@Override
- public List extends TargetAmount> getTargetOptions(Ability source, Game game) {
+ final public List extends TargetAmount> getTargetOptions(Ability source, Game game) {
+ if (!amountWasSet) {
+ setAmount(source, game);
+ }
+
List options = new ArrayList<>();
Set possibleTargets = possibleTargets(source.getControllerId(), source, game);
- addTargets(this, possibleTargets, options, source, game);
+ // optimizations for less memory/cpu consumptions
+ printTargetsTableAndVariations("before optimize", game, possibleTargets, options, false);
+ optimizePossibleTargets(source, game, possibleTargets);
+ printTargetsTableAndVariations("after optimize", game, possibleTargets, options, false);
- // debug target variations
- //printTargetsVariations(possibleTargets, options);
+ // calc possible amount variations
+ addTargets(this, possibleTargets, options, source, game);
+ printTargetsTableAndVariations("after calc", game, possibleTargets, options, true);
return options;
}
- private void printTargetsVariations(Set possibleTargets, List options) {
- // debug target variations
- // permanent index + amount
- // example: 7 -> 2; 8 -> 3; 9 -> 1
+ /**
+ * AI related, trying to reduce targets for simulations
+ */
+ private void optimizePossibleTargets(Ability source, Game game, Set possibleTargets) {
+ // remove duplicated/same creatures (example: distribute 3 damage between 10+ same tokens)
+
+ // it must have additional threshold to keep more variations for analyse
+ //
+ // bad example:
+ // - Blessings of Nature
+ // - Distribute four +1/+1 counters among any number of target creatures.
+ // on low targets threshold AI can put 1/1 to opponent's creature instead own, see TargetAmountAITest.test_AI_SimulateTargets
+
+ int maxPossibleTargetsToSimulate = this.remainingAmount * 2;
+ if (possibleTargets.size() < maxPossibleTargetsToSimulate) {
+ return;
+ }
+
+ // split targets by groups
+ Map targetGroups = new HashMap<>();
+ possibleTargets.forEach(id -> {
+ String groupKey = "";
+
+ // player
+ Player player = game.getPlayer(id);
+ if (player != null) {
+ groupKey = getTargetGroupKeyAsPlayer(player);
+ }
+
+ // game object
+ MageObject object = game.getObject(id);
+ if (object != null) {
+ groupKey = object.getName();
+ if (object instanceof Permanent) {
+ groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object);
+ } else if (object instanceof Card) {
+ groupKey += getTargetGroupKeyAsCard(game, (Card) object);
+ } else {
+ groupKey += getTargetGroupKeyAsOther(game, object);
+ }
+ }
+
+ // unknown - use all
+ if (groupKey.isEmpty()) {
+ groupKey = id.toString();
+ }
+
+ targetGroups.put(id, groupKey);
+ });
+
+ Map> groups = new HashMap<>();
+ targetGroups.forEach((id, groupKey) -> {
+ groups.computeIfAbsent(groupKey, k -> new ArrayList<>());
+ groups.get(groupKey).add(id);
+ });
+
+ // optimize logic:
+ // - use one target from each target group all the time
+ // - add random target from random group until fill all remainingAmount condition
+
+ // use one target per group
+ Set newPossibleTargets = new HashSet<>();
+ groups.forEach((groupKey, groupTargets) -> {
+ UUID targetId = RandomUtil.randomFromCollection(groupTargets);
+ if (targetId != null) {
+ newPossibleTargets.add(targetId);
+ groupTargets.remove(targetId);
+ }
+ });
+
+ // use random target until fill condition
+ while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
+ String groupKey = RandomUtil.randomFromCollection(groups.keySet());
+ if (groupKey == null) {
+ break;
+ }
+ List groupTargets = groups.getOrDefault(groupKey, null);
+ if (groupTargets == null || groupTargets.isEmpty()) {
+ groups.remove(groupKey);
+ continue;
+ }
+ UUID targetId = RandomUtil.randomFromCollection(groupTargets);
+ if (targetId != null) {
+ newPossibleTargets.add(targetId);
+ groupTargets.remove(targetId);
+ }
+ }
+
+ // keep final result
+ possibleTargets.clear();
+ possibleTargets.addAll(newPossibleTargets);
+ }
+
+ private String getTargetGroupKeyAsPlayer(Player player) {
+ // use all
+ return String.join(";", Arrays.asList(
+ player.getName(),
+ String.valueOf(player.getId().hashCode())
+ ));
+ }
+
+ private String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) {
+ // split by name and stats
+ // TODO: rework and combine with PermanentEvaluator (to use battlefield score)
+
+ // try to use short text/hash for lesser data on debug
+ return String.join(";", Arrays.asList(
+ permanent.getName(),
+ String.valueOf(permanent.getControllerId().hashCode()),
+ String.valueOf(permanent.getOwnerId().hashCode()),
+ String.valueOf(permanent.isTapped()),
+ String.valueOf(permanent.getPower().getValue()),
+ String.valueOf(permanent.getToughness().getValue()),
+ String.valueOf(permanent.getDamage()),
+ String.valueOf(permanent.getCardType(game).toString().hashCode()),
+ String.valueOf(permanent.getSubtype(game).toString().hashCode()),
+ String.valueOf(permanent.getCounters(game).getTotalCount()),
+ String.valueOf(permanent.getAbilities(game).size()),
+ String.valueOf(permanent.getRules(game).toString().hashCode())
+ ));
+ }
+
+ private String getTargetGroupKeyAsCard(Game game, Card card) {
+ // split by name and stats
+ return String.join(";", Arrays.asList(
+ card.getName(),
+ String.valueOf(card.getOwnerId().hashCode()),
+ String.valueOf(card.getCardType(game).toString().hashCode()),
+ String.valueOf(card.getSubtype(game).toString().hashCode()),
+ String.valueOf(card.getCounters(game).getTotalCount()),
+ String.valueOf(card.getAbilities(game).size()),
+ String.valueOf(card.getRules(game).toString().hashCode())
+ ));
+ }
+
+ private String getTargetGroupKeyAsOther(Game game, MageObject item) {
+ // use all
+ return String.join(";", Arrays.asList(
+ item.getName(),
+ String.valueOf(item.getId().hashCode())
+ ));
+ }
+
+ /**
+ * Debug only. Print targets table and variations.
+ */
+ private void printTargetsTableAndVariations(String info, Game game, Set possibleTargets, List options, boolean isPrintOptions) {
+ if (!DebugUtil.AI_SHOW_TARGET_OPTIMIZATION_LOGS) return;
+
+ // output example:
+ //
+ // Targets (after optimize): 5
+ // 0. Balduvian Bears [ac8], C, BalduvianBears, DKM:22::0, 2/2
+ // 1. PlayerA (SimulatedPlayer2)
+ //
+ // Target variations (info): 126
+ // 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 1; 4 -> 1
+ // 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 2
+ // 0 -> 1; 1 -> 1; 2 -> 1; 4 -> 2
+
+ // print table
List list = new ArrayList<>(possibleTargets);
+ Collections.sort(list);
HashMap targetNumbers = new HashMap<>();
+ System.out.println();
+ System.out.println(String.format("Targets (%s): %d", info, list.size()));
for (int i = 0; i < list.size(); i++) {
targetNumbers.put(list.get(i), i);
+ String targetName;
+ Player player = game.getPlayer(list.get(i));
+ if (player != null) {
+ targetName = player.toString();
+ } else {
+ MageObject object = game.getObject(list.get(i));
+ if (object != null) {
+ targetName = object.toString();
+ } else {
+ targetName = "unknown";
+ }
+ }
+ System.out.println(String.format("%d. %s", i, targetName));
}
+ System.out.println();
+
+ if (!isPrintOptions) {
+ return;
+ }
+
+ // print amount variations
List res = options
.stream()
.map(t -> t.getTargets()
.stream()
.map(id -> targetNumbers.get(id) + " -> " + t.getTargetAmount(id))
.sorted()
- .collect(Collectors.joining("; ")))
- .collect(Collectors.toList());
- Collections.sort(res);
+ .collect(Collectors.joining("; "))).sorted().collect(Collectors.toList());
System.out.println();
- System.out.println(res.stream().collect(Collectors.joining("\n")));
+ System.out.println(String.format("Target variations (info): %d", options.size()));
+ System.out.println(String.join("\n", res));
System.out.println();
}
- protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) {
- if (!amountWasSet) {
- setAmount(source, game);
- }
+ final protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) {
Set usedTargets = new HashSet<>();
for (UUID targetId : possibleTargets) {
usedTargets.add(targetId);
diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java
index 1a937a54ef4..0aae5458609 100644
--- a/Mage/src/main/java/mage/target/TargetImpl.java
+++ b/Mage/src/main/java/mage/target/TargetImpl.java
@@ -446,13 +446,6 @@ public abstract class TargetImpl implements Target {
return !targets.isEmpty();
}
- /**
- * Returns all possible different target combinations
- *
- * @param source
- * @param game
- * @return
- */
@Override
public List extends TargetImpl> getTargetOptions(Ability source, Game game) {
List options = new ArrayList<>();
diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java
index 7b81b895d68..7ba17e4036f 100644
--- a/Mage/src/main/java/mage/util/CardUtil.java
+++ b/Mage/src/main/java/mage/util/CardUtil.java
@@ -10,6 +10,7 @@ import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.*;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.SavedDamageValue;
+import mage.abilities.dynamicvalue.common.SavedDiscardValue;
import mage.abilities.dynamicvalue.common.SavedGainedLifeValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.ContinuousEffect;
@@ -967,7 +968,9 @@ public final class CardUtil {
boolean xValue = amount.toString().equals("X");
if (xValue) {
sb.append("X ").append(counter.getName()).append(" counters");
- } else if (amount == SavedDamageValue.MANY || amount == SavedGainedLifeValue.MANY) {
+ } else if (amount == SavedDamageValue.MANY
+ || amount == SavedGainedLifeValue.MANY
+ || amount == SavedDiscardValue.MANY) {
sb.append("that many ").append(counter.getName()).append(" counters");
} else {
sb.append(counter.getDescription());
@@ -1176,7 +1179,7 @@ public final class CardUtil {
.sum();
int remainingValue = maxValue - selectedValue;
Set validTargets = new HashSet<>();
- for (UUID id: possibleTargets) {
+ for (UUID id : possibleTargets) {
MageObject mageObject = game.getObject(id);
if (mageObject != null && valueMapper.applyAsInt(mageObject) <= remainingValue) {
validTargets.add(id);
diff --git a/Mage/src/main/java/mage/util/DebugUtil.java b/Mage/src/main/java/mage/util/DebugUtil.java
index 51f029ca0d9..09ba1e89659 100644
--- a/Mage/src/main/java/mage/util/DebugUtil.java
+++ b/Mage/src/main/java/mage/util/DebugUtil.java
@@ -11,6 +11,12 @@ public class DebugUtil {
public static boolean NETWORK_SHOW_CLIENT_CALLBACK_MESSAGES_LOG = false; // show all callback messages (server commands)
+ // AI
+ // game simulations runs in multiple threads, if you stop code to debug then it will be terminated by timeout
+ // so AI debug mode will make single simulation thread without any timeouts
+ public static boolean AI_ENABLE_DEBUG_MODE = false;
+ public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount
+
// cards basic (card panels)
public static boolean GUI_CARD_DRAW_OUTER_BORDER = false;
public static boolean GUI_CARD_DRAW_INNER_BORDER = false;
diff --git a/Mage/src/main/resources/tokens-database.txt b/Mage/src/main/resources/tokens-database.txt
index 2ca7c5dea43..8b69c37b086 100644
--- a/Mage/src/main/resources/tokens-database.txt
+++ b/Mage/src/main/resources/tokens-database.txt
@@ -139,6 +139,13 @@
|Generate|EMBLEM:DSK|Emblem Kaito|||KaitoBaneOfNightmaresEmblem|
|Generate|EMBLEM:FDN|Emblem Kaito|||KaitoCunningInfiltratorEmblem|
|Generate|EMBLEM:FDN|Emblem Vivien|||VivienReidEmblem|
+|Generate|EMBLEM:INR|Emblem Arlinn|||ArlinnEmbracedByTheMoonEmblem|
+|Generate|EMBLEM:INR|Emblem Chandra|||ChandraDressedToKillEmblem|
+|Generate|EMBLEM:INR|Emblem Jace|||JaceUnravelerOfSecretsEmblem|
+|Generate|EMBLEM:INR|Emblem Tamiyo|||TamiyoFieldResearcherEmblem|
+|Generate|EMBLEM:INR|Emblem Wrenn|||WrennAndSevenEmblem|
+|Generate|EMBLEM:DFT|Emblem Chandra|||ChandraSparkHunterEmblem|
+
# ALL PLANES
# Usage hints:
@@ -2422,3 +2429,27 @@
|Generate|TOK:FDN|Spirit|||SpiritWhiteToken|
|Generate|TOK:FDN|Treasure|||TreasureToken|
|Generate|TOK:FDN|Zombie|||ZombieToken|
+
+# INR
+|Generate|TOK:INR|Blood|||BloodToken|
+|Generate|TOK:INR|Clue|||ClueArtifactToken|
+|Generate|TOK:INR|Demon|||DemonToken|
+|Generate|TOK:INR|Eldrazi Horror|||EldraziHorrorToken|
+|Generate|TOK:INR|Elemental|||SeizeTheStormElementalToken|
+|Generate|TOK:INR|Human|1||RedHumanToken|
+|Generate|TOK:INR|Human|2||HumanToken|
+|Generate|TOK:INR|Human Cleric|||HumanClericToken|
+|Generate|TOK:INR|Human Soldier|1||HumanSoldierToken|
+|Generate|TOK:INR|Human Soldier|2||HumanSoldierTrainingToken|
+|Generate|TOK:INR|Human Wizard|||HumanWizardToken|
+|Generate|TOK:INR|Insect|||InsectToken|
+|Generate|TOK:INR|Spider|||SpiderToken|
+|Generate|TOK:INR|Spirit|||SpiritWhiteToken|
+|Generate|TOK:INR|Treefolk|||WrennAndSevenTreefolkToken|
+|Generate|TOK:INR|Vampire|1||EdgarMarkovToken|
+|Generate|TOK:INR|Vampire|2||VampireToken|
+|Generate|TOK:INR|Wolf|1||WolfTokenWithDeathtouch|
+|Generate|TOK:INR|Wolf|2||WolfToken|
+|Generate|TOK:INR|Zombie|1||ZombieToken2|
+|Generate|TOK:INR|Zombie|2||ZombieToken|
+|Generate|TOK:INR|Zombie|3||ZombieDecayedToken|
diff --git a/Makefile b/Makefile
index c7e26647203..066d4568749 100644
--- a/Makefile
+++ b/Makefile
@@ -6,19 +6,28 @@
# Alternatively, you can set this variable in the .env file
TARGET_DIR ?= deploy/
-# Note that the proper install script is located under ./Utils/build-and-package.pl
-# and that should be used instead. This script is purely for convenience.
-# The perl script bundles the artifacts into a single zip
-.PHONY: install
-install:
- # Building project
- mvn clean install package -DskipTests
+.PHONY: clean
+clean:
+ mvn clean
+
+.PHONY: build
+build:
+ mvn install package -DskipTests
+
+.PHONY: package
+package:
# Packaging Mage.Client to zip
- cd Mage.Client && mvn assembly:assembly
+ cd Mage.Client && mvn assembly:single
# Packaging Mage.Server to zip
- cd Mage.Server && mvn assembly:assembly
+ cd Mage.Server && mvn assembly:single
# Copying the files to the target directory
mkdir -p $(TARGET_DIR)
cp ./Mage.Server/target/mage-server.zip $(TARGET_DIR)
cp ./Mage.Client/target/mage-client.zip $(TARGET_DIR)
+# Note that the proper install script is located under ./Utils/build-and-package.pl
+# and that should be used instead. This script is purely for convenience.
+# The perl script bundles the artifacts into a single zip
+.PHONY: install
+install: clean build package
+
diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt
index 41de19925d7..db57fb79528 100644
--- a/Utils/mtg-cards-data.txt
+++ b/Utils/mtg-cards-data.txt
@@ -56011,7 +56011,7 @@ Bounce Off|Aetherdrift|39|C|{U}|Instant|||Return target creature or Vehicle to i
Caelorna, Coral Tyrant|Aetherdrift|40|U|{1}{U}|Legendary Creature - Octopus|0|8||
Diversion Unit|Aetherdrift|41|U|{1}{U}|Artifact Creature - Robot|2|1|Flying${U}, Sacrifice this creature: Counter target instant or sorcery spell unless its controller pays {3}.|
Flood the Engine|Aetherdrift|42|C|{2}{U}|Enchantment - Aura|||Enchant creature or Vehicle$When this Aura enters, tap enchanted permanent.$Enchanted permanent loses all abilities and doesn't untap during its controller's untap step.|
-Gearseeker Serpent|Aetherdrift|43|C|{5}{U}{U}|Creature - Serpent|5|6|This spell costs {1} less to cast for each artifact you control.${5}{U}: Gearseeker Serpent can't be blocked this turn.|
+Gearseeker Serpent|Aetherdrift|43|C|{5}{U}{U}|Creature - Serpent|5|6|Affinity for artifacts${5}{U}: Gearseeker Serpent can't be blocked this turn.|
Glitch Ghost Surveyor|Aetherdrift|44|C|{2}{U}|Creature - Spirit Scout|2|2|Flying$Start your engines!$Max speed -- {3}, Exile this card from your graveyard: Draw a card.|
Guidelight Optimizer|Aetherdrift|45|C|{1}{U}|Artifact Creature - Robot|2|1|{T}: Add {U}. Spend this mana only to cast an artifact spell or activate an ability.|
Howler's Heavy|Aetherdrift|46|C|{3}{U}|Creature - Seal Pirate|3|4|Cycling {1}{U}$When you cycle this card, target creature or Vehicle an opponent controls gets -3/-0 until end of turn.|
@@ -56029,7 +56029,7 @@ Riverchurn Monument|Aetherdrift|57|R|{1}{U}|Artifact|||{1}, {T}: Any number of t
Roadside Blowout|Aetherdrift|58|U|{2}{U}|Sorcery|||This spell costs {2} less to cast if it targets a permanent with mana value 1.$Return target creature or Vehicle an opponent controls to its owner's hand.$Draw a card.|
Sabotage Strategist|Aetherdrift|59|U|{2}{U}{U}|Creature - Vedalken Ranger|2|2|Flying, vigilance$Whenever one or more creatures attack you, those creatures get -1/-0 until end of turn.$Exhaust -- {5}{U}{U}: Put three +1/+1 counters on this creature.|
Scrounging Skyray|Aetherdrift|60|U|{1}{U}|Creature - Fish Pirate|1|2|Flying$Whenever you discard one or more cards, put that many +1/+1 counters on this creature.$Cycling {2}|
-Skystreak Engineer|Aetherdrift|61|C|{1}{U}|Creature - Human Pilot|1|3|Flying$Exhaust - {4}{U}: Put two +1/+1 counters on this creature.|
+Skystreak Engineer|Aetherdrift|61|C|{1}{U}|Creature - Human Pilot|1|3|Flying$Exhaust -- {4}{U}: Put two +1/+1 counters on this creature.|
Slick Imitator|Aetherdrift|62|U|{1}{U}|Creature - Ooze|1|3|Start your engines!$Max speed -- {1}, Sacrifice this creature: Copy target spell you control. You may choose new targets for the copy.|
Spectral Interference|Aetherdrift|63|C|{1}{U}|Instant|||Counter target artifact or creature spell unless its controller pays {4}.|
Spell Pierce|Aetherdrift|64|U|{U}|Instant|||Counter target noncreature spell unless its controller pays {2}.|
@@ -56042,7 +56042,7 @@ Transit Mage|Aetherdrift|70|U|{2}{U}|Creature - Human Wizard|2|2|When this creat
Trip Up|Aetherdrift|71|C|{3}{U}|Instant|||Target nonland permanent's owner puts it on their choice of the top or bottom of their library.$Cycling {2}|
Unstoppable Plan|Aetherdrift|72|R|{2}{U}|Enchantment|||At the beginning of your end step, untap all nonland permanents you control.|
Vnwxt, Verbose Host|Aetherdrift|73|R|{1}{U}|Legendary Creature - Homunculus|0|4|Start your engines!$You have no maximum hand size.$Max speed -- If you would draw a card, draw two cards instead.|
-Waxen Shapethief|Aetherdrift|74|R|{3}{U}|Creature - Shapeshifter|0|0|Flash$You may have this creature enter as a copy of a creature or artifact you control.$Cycling {2}|
+Waxen Shapethief|Aetherdrift|74|R|{3}{U}|Creature - Shapeshifter|0|0|Flash$You may have this creature enter as a copy of an artifact or creature you control.$Cycling {2}|
Ancient Vendetta|Aetherdrift|75|U|{3}{B}|Sorcery|||Choose a card name. Search target opponent's graveyard, hand, and library for up to four cards with that name and exile them. Then that player shuffles.|
Back on Track|Aetherdrift|76|U|{4}{B}|Sorcery|||Return target creature or Vehicle card from your graveyard to the battlefield. Create a 1/1 colorless Pilot creature token with "This token saddles Mounts and crews Vehicles as though its power were 2 greater."|
Bloodghast|Aetherdrift|77|R|{B}{B}|Creature - Vampire Spirit|2|1|Bloodghast can't block.$Bloodghast has haste as long as an opponent has 10 or less life.$Landfall -- Whenever a land you control enters, you may return Bloodghast from your graveyard to the battlefield.|
@@ -56166,10 +56166,10 @@ Captain Howler, Sea Scourge|Aetherdrift|194|R|{2}{U}{R}|Legendary Creature - Sha
Caradora, Heart of Alacria|Aetherdrift|195|R|{2}{G}{W}|Legendary Creature - Human Knight|4|2|When Caradora enters, you may search your library for a Mount or Vehicle card, reveal it, put it into your hand, then shuffle.$If one or more +1/+1 counters would be put on a creature or Vehicle you control, that many plus one +1/+1 counters are put on it instead.|
Cloudspire Coordinator|Aetherdrift|196|U|{R}{W}|Creature - Human Pilot|3|1|When this creature enters, scry 2.${T}: Create X 1/1 colorless Pilot creature tokens, where X is the number of Mounts and/or Vehicles that entered the battlefield under your control this turn. The tokens have "This token saddles Mounts and crews Vehicles as though its power were 2 greater."|
Cloudspire Skycycle|Aetherdrift|197|U|{2}{R}{W}|Artifact - Vehicle|2|3|Flying$When this Vehicle enters, distribute two +1/+1 counters among one or two other target Vehicles and/or creatures you control.$Crew 1|
-Coalstoke Gearhulk|Aetherdrift|198|M|{1}{B}{B}{R}{R}|Artifact Creature - Construct|5|4|Menace, deathtouch$When this creature enters, put target creature card with mana value 4 or less from a graveyard onto the battlefield under your control with a finality counter on it. It gains menace, deathtouch, and haste. Exile that creature at the beginning of your next end step.|
+Coalstoke Gearhulk|Aetherdrift|198|M|{1}{B}{B}{R}{R}|Artifact Creature - Construct|5|4|Menace, deathtouch$When this creature enters, put target creature card with mana value 4 or less from a graveyard onto the battlefield under your control with a finality counter on it. That creature gains menace, deathtouch, and haste. At the beginnning of your next end step, exile that creature.|
Debris Beetle|Aetherdrift|199|R|{2}{B}{G}|Artifact - Vehicle|6|6|Trample$When this Vehicle enters, each opponent loses 3 life and you gain 3 life.$Crew 2|
Dune Drifter|Aetherdrift|200|U|{X}{W}{B}|Artifact - Vehicle|3|3|When this Vehicle enters, return target artifact or creature card with mana value X or less from your graveyard to the battlefield.$Crew 2|
-Embalmed Ascendant|Aetherdrift|201|U|{1}{W}{B}|Creature - Zombie|1|2|Start your engines!$When this creature enters, create a 2/2 black Zombie creature token.$Max speed--Whenever a creature you control dies, each opponent loses 1 life and you gain 1 life.|
+Embalmed Ascendant|Aetherdrift|201|U|{1}{W}{B}|Creature - Zombie|1|2|Start your engines!$When this creature enters, create a 2/2 black Zombie creature token.$Max speed -- Whenever a creature you control dies, each opponent loses 1 life and you gain 1 life.|
Explosive Getaway|Aetherdrift|202|R|{3}{R}{W}|Sorcery|||Exile up to one target artifact or creature. Return it to the battlefield under its owner's control at the beginning of the next end step.$Explosive Getaway deals 4 damage to each creature.|
Far Fortune, End Boss|Aetherdrift|203|R|{2}{B}{R}|Legendary Creature - Human Mercenary|4|5|Start your engines!$Whenever you attack, Far Fortune deals 1 damage to each opponent.$Max speed -- If a source you control would deal damage to an opponent or a permanent an opponent controls, it deals that much damage plus 1 instead.|
Fearless Swashbuckler|Aetherdrift|204|R|{1}{U}{R}|Creature - Fish Pirate|3|3|Haste$Vehicles you control have haste.$Whenever you attack, if a Pirate and a Vehicle attacked this combat, draw three cards, then discard two cards.|
@@ -56201,11 +56201,11 @@ Zahur, Glory's Past|Aetherdrift|229|R|{W}{B}|Legendary Creature - Zombie Cat War
Aetherjacket|Aetherdrift|230|C|{3}|Artifact Creature - Thopter|2|1|Flying, vigilance${2}, {T}, Sacrifice this creature: Destroy another target artifact. Activate only as a sorcery.|
The Aetherspark|Aetherdrift|231|M|{4}|Legendary Artifact Planeswalker - Equipment|4|As long as The Aetherspark is attached to a creature, The Aetherspark can't be attacked and has "Whenever equipped creature deals combat damage during your turn, put that many loyalty counters on The Aetherspark."$+1: Attach The Aetherspark to up to one target creature you control. Put a +1/+1 counter on that creature.$-5: Draw two cards.$-10: Add ten mana of any one color.|
Camera Launcher|Aetherdrift|232|C|{3}|Artifact Creature - Construct|2|2|Exhaust -- {3}: Put a +1/+1 counter on this creature. Create a 1/1 colorless Thopter artifact creature token with flying.|
-Guidelight Matrix|Aetherdrift|233|C|{2}|Artifact|||When this artifact enters, draw a card.${2}, {T}: Target Mount you control becomes saddled until end of turn. Activate only as a sorcery.${2}, {t}: Target Vehicle you control becomes an artifact creature until end of turn.|
+Guidelight Matrix|Aetherdrift|233|C|{2}|Artifact|||When this artifact enters, draw a card.${2}, {T}: Target Mount you control becomes saddled until end of turn. Activate only as a sorcery.${2}, {T}: Target Vehicle you control becomes an artifact creature until end of turn.|
Lifecraft Engine|Aetherdrift|234|R|{3}|Artifact - Vehicle|4|4|As this Vehicle enters, choose a creature type.$Vehicle creatures you control are the chosen creature type in addition to their other types.$Each creature you control of the chosen type other than this Vehicle gets +1/+1.$Crew 3|
Marketback Walker|Aetherdrift|235|R|{X}{X}|Artifact Creature - Construct|0|0|This creature enters with X +1/+1 counters on it.${4}: Put a +1/+1 counter on this creature.$When this creature dies, draw a card for each +1/+1 counter on it.|
Marshals' Pathcruiser|Aetherdrift|236|U|{3}|Artifact - Vehicle|6|5|When this Vehicle enters, search your library for a basic land card, reveal it, put it into your hand, then shuffle.$Exhaust -- {W}{U}{B}{R}{G}: This Vehicle becomes an artifact creature. Put two +1/+1 counters on it.$Crew 5|
-Monument to Endurance|Aetherdrift|237|R|{3}|Artifact|||Whenever you discard a card, choose one that hasn't been chosen this turn--$* Draw a card.$* Create a Treasure token.$* Each opponent loses 3 life.|
+Monument to Endurance|Aetherdrift|237|R|{3}|Artifact|||Whenever you discard a card, choose one that hasn't been chosen this turn --$* Draw a card.$* Create a Treasure token.$* Each opponent loses 3 life.|
Pit Automaton|Aetherdrift|238|U|{2}|Artifact Creature - Construct|0|4|Defender${T}: Add {C}{C}. Spend this mana only to activate abilities.${2}, {T}: When you next activate an exhaust ability this turn, copy it. You may choose new targets for the copy.|
Racers' Scoreboard|Aetherdrift|239|U|{4}|Artifact|||Start your engines!$When this artifact enters, draw two cards, then discard a card.$Max speed -- Spells you cast cost {1} less to cast.|
Radiant Lotus|Aetherdrift|240|M|{6}|Artifact|||{T}, Sacrifice one or more artifacts: Choose a color. Target player adds three mana of the chosen color for each artifact sacrificed this way.|
diff --git a/pom.xml b/pom.xml
index 34f0f0a8109..feb6a458482 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.mage
mage-root
- 1.4.55
+ 1.4.56
pom
Mage Root
Mage Root POM
@@ -16,7 +16,7 @@
${project.basedir}
- 1.4.55
+ 1.4.56
-Dfile.encoding=UTF-8
UTF-8
yyyy-MM-dd'T'HH:mm:ss'Z'