From 29e5230469c2dc0ac8c740ae2920c138560b528d Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Tue, 23 Jun 2020 09:18:40 +0200 Subject: [PATCH] * Oathbreaker format - Fixed that signate spell didn't return to command zone. Added unit test for oathbreaker format (fixes #6695). --- Mage.Tests/Oathbreaker_UR.dck | 48 +++++++++++++ .../cards/abilities/keywords/BolsterTest.java | 37 ++++++++++ .../BasicSaheekiSublimeArtificerTest.java | 69 +++++++++++++++++++ .../base/CardTestOathbreaker3PlayersFFA.java | 38 ++++++++++ .../OathbreakerOnBattlefieldCondition.java | 12 ++-- .../CommanderReplacementEffect.java | 35 ++++++---- 6 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 Mage.Tests/Oathbreaker_UR.dck create mode 100644 Mage.Tests/src/test/java/org/mage/test/oathbreaker/FFA3/BasicSaheekiSublimeArtificerTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestOathbreaker3PlayersFFA.java diff --git a/Mage.Tests/Oathbreaker_UR.dck b/Mage.Tests/Oathbreaker_UR.dck new file mode 100644 index 0000000000..9574902d44 --- /dev/null +++ b/Mage.Tests/Oathbreaker_UR.dck @@ -0,0 +1,48 @@ +NAME:Oathbreaker_UR +1 [M15:44] Aetherspouts +1 [ISD:130] Blasphemous Act +1 [M21:46] Cancel +1 [C20:146] Chaos Warp +1 [GRN:233] Chromatic Lantern +1 [7ED:67] Counterspell +1 [M15:242] Darksteel Citadel +1 [EMN:55] Displace +1 [RAV:46] Drift of Phantasms +1 [JMP:313] Dualcaster Mage +1 [NPH:35] Gitaxian Probe +1 [MRD:282] Great Furnace +1 [M19:145] Guttersnipe +1 [LRW:175] Heat Shimmer +1 [DTK:140] Impact Tremors +1 [VIS:34] Impulse +9 [M21:263] Island +1 [GRN:251] Izzet Guildgate +1 [GRN:238] Izzet Locket +1 [M12:63] Mana Leak +1 [SOI:73] Manic Scribe +7 [M21:269] Mountain +1 [M20:69] Negate +1 [M21:59] Opt +1 [AER:167] Ornithopter +1 [ELD:60] Overwhelmed Apprentice +1 [M11:70] Preordain +1 [M19:68] Psychic Corrosion +1 [THS:135] Purphoros, God of the Forge +1 [KLD:126] Reckless Fireweaver +1 [M19:254] Reliquary Tower +1 [MRD:283] Seat of the Synod +1 [CHK:268] Sensei's Divining Top +1 [M15:161] Shrapnel Blast +1 [5DN:150] Silent Arbiter +1 [XLN:81] Spell Pierce +1 [M10:220] Spellbook +1 [SOM:46] Stoic Rebuttal +1 [MH1:231] Talisman of Creativity +1 [M11:229] Terramorphic Expanse +1 [KTK:59] Treasure Cruise +1 [JOU:115] Twinflame +1 [DOM:72] Unwind +1 [M14:163] Young Pyromancer +SB: 1 [WAR:234] Saheeli, Sublime Artificer +SB: 1 [MRD:54] Thoughtcast + diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BolsterTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BolsterTest.java index c9eb93a740..c83bc8fa8d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BolsterTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BolsterTest.java @@ -91,4 +91,41 @@ public class BolsterTest extends CardTestPlayerBase { assertLife(playerB, 16); } + + + @Test + public void EliteScaleguardTriggerTwiceTest() { + // When Elite Scaleguard enters the battlefield, bolster 2. + // Whenever a creature you control with a +1/+1 counter on it attacks, tap target creature defending player controls. + addCard(Zone.HAND, playerA, "Elite Scaleguard"); // Creature 2/3 {4}{W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Elite Scaleguard"); + + attack(3, playerA, "Silvercoat Lion"); + addTarget(playerA, "Pillarfield Ox"); // Tap from triggered ability of Elite Scaleguard + + attack(5, playerA, "Silvercoat Lion"); + addTarget(playerA, "Pillarfield Ox"); // Tap from triggered ability of Elite Scaleguard + + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Elite Scaleguard", 2, 3); + assertCounterCount("Silvercoat Lion", CounterType.P1P1, 2); + assertPowerToughness(playerA, "Silvercoat Lion", 4, 4); + assertTapped("Silvercoat Lion", true); + + assertPermanentCount(playerB, "Pillarfield Ox", 1); + assertTapped("Pillarfield Ox", true); + + assertLife(playerB, 12); + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/oathbreaker/FFA3/BasicSaheekiSublimeArtificerTest.java b/Mage.Tests/src/test/java/org/mage/test/oathbreaker/FFA3/BasicSaheekiSublimeArtificerTest.java new file mode 100644 index 0000000000..118983824d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/oathbreaker/FFA3/BasicSaheekiSublimeArtificerTest.java @@ -0,0 +1,69 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.oathbreaker.FFA3; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestOathbreaker3PlayersFFA; + +/** + * + * @author LevelX2 + */ + +public class BasicSaheekiSublimeArtificerTest extends CardTestOathbreaker3PlayersFFA { + + /** + * Check that if a player left the game, it's commander is also removed + */ + @Test + public void commanderRemovedTest() { + + concede(1, PhaseStep.PRECOMBAT_MAIN, playerA); + setStopAt(1, PhaseStep.DECLARE_ATTACKERS); + execute(); + + assertCommandZoneCount(playerB, "Saheeli, Sublime Artificer", 1); + assertCommandZoneCount(playerB, "Thoughtcast", 1); + assertCommandZoneCount(playerC, "Saheeli, Sublime Artificer", 1); + assertCommandZoneCount(playerC, "Thoughtcast", 1); + assertCommandZoneCount(playerA, "Saheeli, Sublime Artificer", 0); + assertCommandZoneCount(playerA, "Thoughtcast", 0); + + } + + @Test + public void castCommanderTest() { + + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + addCard(Zone.BATTLEFIELD, playerA, "Ornithopter", 4); + + // Planeswalker 5 Loyality Counter + // Whenever you cast a noncreature spell, create a 1/1 colorless Servo artifact creature token. + // -2: Target artifact you control becomes a copy of another target artifact or creature you control until end of turn, + // except it's an artifact in addition to its other types. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Saheeli, Sublime Artificer"); // Planeswalker 5 {1}{U/R}{U/R} + + // Affinity for artifacts + // Draw two cards. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thoughtcast"); // Sorcery {4}{U} + + setStopAt(1, PhaseStep.DECLARE_ATTACKERS); + execute(); + + + assertCommandZoneCount(playerB, "Saheeli, Sublime Artificer", 1); + assertCommandZoneCount(playerB, "Thoughtcast", 1); + assertCommandZoneCount(playerC, "Saheeli, Sublime Artificer", 1); + assertCommandZoneCount(playerC, "Thoughtcast", 1); + + assertHandCount(playerA, 3); + assertPermanentCount(playerA, "Saheeli, Sublime Artificer", 1); + assertCommandZoneCount(playerA, "Thoughtcast", 1); + + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestOathbreaker3PlayersFFA.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestOathbreaker3PlayersFFA.java new file mode 100644 index 0000000000..003b9bcb08 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestOathbreaker3PlayersFFA.java @@ -0,0 +1,38 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.serverside.base; + +import java.io.FileNotFoundException; +import mage.constants.MultiplayerAttackOption; +import mage.constants.RangeOfInfluence; +import mage.game.Game; +import mage.game.GameException; +import mage.game.OathbreakerFreeForAll; +import mage.game.mulligan.MulliganType; +import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; + +/** + * @author LevelX2 + */ +public abstract class CardTestOathbreaker3PlayersFFA extends CardTestPlayerAPIImpl { + + public CardTestOathbreaker3PlayersFFA() { + super(); + this.deckNameA = "Oathbreaker_UR.dck"; // PW: Saheeli, Sublime Artificer SS: Thoughtcast + this.deckNameB = "Oathbreaker_UR.dck"; + this.deckNameC = "Oathbreaker_UR.dck"; + } + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + Game game = new OathbreakerFreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); + playerA = createPlayer(game, playerA, "PlayerA", deckNameA); + playerB = createPlayer(game, playerB, "PlayerB", deckNameB); + playerC = createPlayer(game, playerC, "PlayerC", deckNameC); + return game; + } + +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/condition/common/OathbreakerOnBattlefieldCondition.java b/Mage/src/main/java/mage/abilities/condition/common/OathbreakerOnBattlefieldCondition.java index ad26f8a79d..ba27bdfbbc 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/OathbreakerOnBattlefieldCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/OathbreakerOnBattlefieldCondition.java @@ -22,9 +22,9 @@ import java.util.UUID; */ public class OathbreakerOnBattlefieldCondition implements Condition { - private UUID playerId; - private FilterControlledPermanent filter; - private String compatibleNames; + private final UUID playerId; + private final FilterControlledPermanent filter; + private final String compatibleNames; public OathbreakerOnBattlefieldCondition(Game game, UUID playerId, UUID signatureSpellId, Set oathbreakersToSearch) { this.playerId = playerId; @@ -35,17 +35,17 @@ public class OathbreakerOnBattlefieldCondition implements Condition { // spell can be casted by any compatible oathbreakers List compatibleList = new ArrayList<>(); - List compatibleNames = new ArrayList<>(); + List compatibleNamesList = new ArrayList<>(); if (oathbreakersToSearch != null && !oathbreakersToSearch.isEmpty()) { for (UUID id : oathbreakersToSearch) { Card commander = game.getCard(id); if (commander != null && ManaUtil.isColorIdentityCompatible(commander.getColorIdentity(), spellColors)) { compatibleList.add(new PermanentIdPredicate(id)); - compatibleNames.add(commander.getName()); + compatibleNamesList.add(commander.getName()); } } } - this.compatibleNames = String.join("; ", compatibleNames); + this.compatibleNames = String.join("; ", compatibleNamesList); if (compatibleList.isEmpty()) { // random id to disable condition diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java index 5ccacd8711..94aa319796 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java @@ -28,9 +28,7 @@ import java.util.UUID; that permanent and the card representing it that isn’t a commander are put into the appropriate zone, and the card that represents it and is a commander is put into the command zone. */ - // Oathbreaker mode: If your Oathbreaker changes zones, you may return it to the Command Zone. The Signature Spell must return to the Command Zone. - public class CommanderReplacementEffect extends ReplacementEffectImpl { private final UUID commanderId; @@ -39,6 +37,17 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl { private final boolean forceToMove; private final String commanderTypeName; + /** + * + * @param commanderId + * @param alsoHand is the replacement effect also applied if commander + * object goes to hand zone + * @param alsoLibrary is the replacement effect also applied if commander + * object goes to library zone + * @param forceToMove used for signature spell of Oathbreaker format (spell + * is mandatory moved to command zone instead) + * @param commanderTypeName type of commander object to set the correct text + */ public CommanderReplacementEffect(UUID commanderId, boolean alsoHand, boolean alsoLibrary, boolean forceToMove, String commanderTypeName) { super(Duration.WhileOnBattlefield, Outcome.Benefit); String mayStr = forceToMove ? " " : " may "; @@ -89,11 +98,11 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { - ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (!commanderId.equals(event.getTargetId())) { + return false; + } -// if (!game.isSimulation() && commanderId.equals(zEvent.getTargetId())) { -// System.out.println("applies " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId())); -// } + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; if (zEvent.getToZone().equals(Zone.HAND) && !alsoHand) { return false; @@ -106,10 +115,14 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl { switch (zEvent.getToZone()) { case LIBRARY: case HAND: - if (commanderId.equals(zEvent.getTargetId())) { + return true; + } + if (forceToMove) { + switch (zEvent.getToZone()) { + case BATTLEFIELD: + case GRAVEYARD: return true; - } - break; + } } return false; } @@ -119,10 +132,6 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl { ZoneChangeEvent zEvent = (ZoneChangeEvent) event; String originToZone = zEvent.getToZone().toString().toLowerCase(Locale.ENGLISH); - if (!game.isSimulation()) { - //System.out.println("replace " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId())); - } - if (zEvent.getFromZone() == Zone.BATTLEFIELD) { Permanent permanent = zEvent.getTarget(); if (permanent != null) {