From a2ae232b43ca79c34eda04575641a320f6c477f1 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Sat, 18 Jul 2020 12:23:19 +0200 Subject: [PATCH] * Fixed some corner cases for Worl Enchantment State-Based actions (704.5k). --- .../mage/cards/l/LieutenantsOfTheGuard.java | 17 ++-- .../rules/WorldEnchantmentsRuleTest.java | 98 +++++++++++++++++-- .../effects/AuraReplacementEffect.java | 11 +-- Mage/src/main/java/mage/game/Game.java | 11 ++- Mage/src/main/java/mage/game/GameImpl.java | 40 +++++--- .../src/main/java/mage/game/ZonesHandler.java | 22 +++-- .../mage/game/permanent/token/TokenImpl.java | 16 +-- 7 files changed, 161 insertions(+), 54 deletions(-) diff --git a/Mage.Sets/src/mage/cards/l/LieutenantsOfTheGuard.java b/Mage.Sets/src/mage/cards/l/LieutenantsOfTheGuard.java index b9c327a785..db5983ad7a 100644 --- a/Mage.Sets/src/mage/cards/l/LieutenantsOfTheGuard.java +++ b/Mage.Sets/src/mage/cards/l/LieutenantsOfTheGuard.java @@ -1,4 +1,3 @@ - package mage.cards.l; import java.util.UUID; @@ -11,8 +10,8 @@ import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; @@ -27,13 +26,15 @@ public final class LieutenantsOfTheGuard extends CardImpl { public LieutenantsOfTheGuard(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{W}"); - + this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.SOLDIER); this.power = new MageInt(2); this.toughness = new MageInt(2); - // Council's dilemma — When Lieutenants of the Guard enters the battlefield, starting with you, each player votes for strength or numbers. Put a +1/+1 counter on Lieutenants of the Guard for each strength vote and put a 1/1 white Soldier creature token onto the battlefield for each numbers vote. + // Council's dilemma — When Lieutenants of the Guard enters the battlefield, starting with you, + // each player votes for strength or numbers. Put a +1/+1 counter on Lieutenants of the Guard for each + // strength vote and put a 1/1 white Soldier creature token onto the battlefield for each numbers vote. this.addAbility(new EntersBattlefieldTriggeredAbility(new LieutenantsOfTheGuardDilemmaEffect(), false, "Council's dilemma — ")); } @@ -48,6 +49,7 @@ public final class LieutenantsOfTheGuard extends CardImpl { } class LieutenantsOfTheGuardDilemmaEffect extends CouncilsDilemmaVoteEffect { + public LieutenantsOfTheGuardDilemmaEffect() { super(Outcome.Benefit); this.staticText = "starting with you, each player votes for strength or numbers. Put a +1/+1 counter on {this} for each strength vote and put a 1/1 white Soldier creature token onto the battlefield for each numbers vote."; @@ -62,7 +64,9 @@ class LieutenantsOfTheGuardDilemmaEffect extends CouncilsDilemmaVoteEffect { Player controller = game.getPlayer(source.getControllerId()); //If no controller, exit out here and do not vote. - if (controller == null) return false; + if (controller == null) { + return false; + } this.vote("strength", "numbers", controller, game, source); @@ -70,8 +74,9 @@ class LieutenantsOfTheGuardDilemmaEffect extends CouncilsDilemmaVoteEffect { //Strength Votes //If strength received zero votes or the permanent is no longer on the battlefield, do not attempt to put P1P1 counters on it. - if (voteOneCount > 0 && permanent != null) + if (voteOneCount > 0 && permanent != null) { permanent.addCounters(CounterType.P1P1.createInstance(voteOneCount), source, game); + } //Numbers Votes if (voteTwoCount > 0) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/rules/WorldEnchantmentsRuleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/rules/WorldEnchantmentsRuleTest.java index 6a5258b212..e2866affcc 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/rules/WorldEnchantmentsRuleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/rules/WorldEnchantmentsRuleTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.rules; import mage.constants.PhaseStep; @@ -10,15 +9,15 @@ import org.mage.test.serverside.base.CardTestMultiPlayerBase; * * @author LevelX2 */ - public class WorldEnchantmentsRuleTest extends CardTestMultiPlayerBase { /** - * 704.5m If two or more permanents have the supertype world, all except the one that has had - * the world supertype for the shortest amount of time are put into their owners' graveyards. - * In the event of a tie for the shortest amount of time, all are put into their owners' graveyards. - * This is called the “world rule. - * + * 704.5m If two or more permanents have the supertype world, all except the + * one that has had the world supertype for the shortest amount of time are + * put into their owners' graveyards. In the event of a tie for the shortest + * amount of time, all are put into their owners' graveyards. This is called + * the “world rule. + * */ @Test public void TestTwoWorldEnchantments() { @@ -26,14 +25,14 @@ public class WorldEnchantmentsRuleTest extends CardTestMultiPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); addCard(Zone.HAND, playerA, "Nether Void", 1); addCard(Zone.HAND, playerA, "Silvercoat Lion", 1); - + addCard(Zone.BATTLEFIELD, playerD, "Swamp", 7); addCard(Zone.HAND, playerD, "Nether Void", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nether Void"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silvercoat Lion"); // just needed to get different craete time to second Nether Void castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Nether Void"); - + setStopAt(2, PhaseStep.END_TURN); execute(); assertAllCommandsUsed(); @@ -64,4 +63,83 @@ public class WorldEnchantmentsRuleTest extends CardTestMultiPlayerBase { assertPermanentCount(playerA, "Nether Void", 1); assertPermanentCount(playerC, "Nether Void", 1); } -} \ No newline at end of file + + // 704.5 In the event of a tie for the shortest amount of time, all are put into their owners’ graveyards. This is called the “world rule.” + // In this example the execution order of the leaves the battlefield triggers of the two Oblivion Rings decide, which World Enchnatment may stay + // Player order: A -> D -> C -> B + @Test + public void TestTwoWorldEnchantmentsFromTriggers() { + setStrictChooseMode(true); + // When Oblivion Ring enters the battlefield, exile another target nonland permanent. + // When Oblivion Ring leaves the battlefield, return the exiled card to the battlefield under its owner's control. + addCard(Zone.HAND, playerA, "Oblivion Ring", 1); + // All creatures have haste. + addCard(Zone.HAND, playerA, "Concordant Crossroads", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + // When Oblivion Ring enters the battlefield, exile another target nonland permanent. + // When Oblivion Ring leaves the battlefield, return the exiled card to the battlefield under its owner's control. + addCard(Zone.HAND, playerD, "Oblivion Ring", 1); // Enchantment {2}{W} + // Destroy all white permanents. + addCard(Zone.HAND, playerD, "Anarchy", 1); // Sorcery {2}{R}{R} + + // All creatures have haste. + addCard(Zone.BATTLEFIELD, playerD, "Concordant Crossroads", 1); // World Enchantment {G} + addCard(Zone.BATTLEFIELD, playerD, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerD, "Mountain", 6); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Oblivion Ring"); + addTarget(playerA, "Concordant Crossroads"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Concordant Crossroads"); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Oblivion Ring"); + addTarget(playerD, "Concordant Crossroads"); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Anarchy"); // Both World Enchantments return at the same time and go to grave + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerD, "Concordant Crossroads", 0); + assertPermanentCount(playerA, "Concordant Crossroads", 1); + + assertGraveyardCount(playerA, "Oblivion Ring", 1); + assertGraveyardCount(playerA, "Concordant Crossroads", 0); + assertGraveyardCount(playerD, "Oblivion Ring", 1); + assertGraveyardCount(playerD, "Concordant Crossroads", 1); + assertGraveyardCount(playerD, "Anarchy", 1); + } + + @Test + public void TestTwoWorldEnchantmentsWithSameOrder() { + setStrictChooseMode(true); + // When Charmed Griffin enters the battlefield, each other player may put an artifact or enchantment card onto the battlefield from their hand. + addCard(Zone.HAND, playerA, "Charmed Griffin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + + // All creatures have haste. + addCard(Zone.HAND, playerD, "Concordant Crossroads", 1); + addCard(Zone.HAND, playerB, "Concordant Crossroads", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Charmed Griffin"); + + setChoice(playerD, "Yes"); // Put an artifact or enchantment card from your hand onto the battlefield? + setChoice(playerD, "Concordant Crossroads"); + + setChoice(playerB, "Yes"); // Put an artifact or enchantment card from your hand onto the battlefield? + setChoice(playerB, "Concordant Crossroads"); + + concede(1, PhaseStep.PRECOMBAT_MAIN, playerC); // World Enchantments come into range + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Charmed Griffin", 1); + assertPermanentCount(playerD, "Concordant Crossroads", 0); + assertPermanentCount(playerB, "Concordant Crossroads", 0); + + assertGraveyardCount(playerB, "Concordant Crossroads", 1); + assertGraveyardCount(playerD, "Concordant Crossroads", 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/AuraReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/AuraReplacementEffect.java index 2369835049..8ad5ee3acd 100644 --- a/Mage/src/main/java/mage/abilities/effects/AuraReplacementEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/AuraReplacementEffect.java @@ -1,5 +1,6 @@ package mage.abilities.effects; +import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.SpellAbility; @@ -17,8 +18,6 @@ import mage.players.Player; import mage.target.Target; import mage.target.common.TargetCardInGraveyard; -import java.util.UUID; - /** * Cards with the Aura subtype don't change the zone they are in, if there is no * valid target on the battlefield. Also, when entering the battlefield and it @@ -163,7 +162,7 @@ public class AuraReplacementEffect extends ReplacementEffectImpl { PermanentCard permanent = new PermanentCard(card, (controllingPlayer == null ? card.getOwnerId() : controllingPlayer.getId()), game); ZoneChangeEvent zoneChangeEvent = new ZoneChangeEvent(permanent, controllerId, fromZone, Zone.BATTLEFIELD); permanent.updateZoneChangeCounter(game, zoneChangeEvent); - game.getBattlefield().addPermanent(permanent); + game.addPermanent(permanent, 0); card.setZone(Zone.BATTLEFIELD, game); if (permanent.entersBattlefield(event.getSourceId(), game, fromZone, true)) { if (targetCard != null) { @@ -196,9 +195,9 @@ public class AuraReplacementEffect extends ReplacementEffectImpl { return card != null && (card.isEnchantment() && card.hasSubtype(SubType.AURA, game) || // in case of transformable enchantments (game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()) != null - && card.getSecondCardFace() != null - && card.getSecondCardFace().isEnchantment() - && card.getSecondCardFace().hasSubtype(SubType.AURA, game))); + && card.getSecondCardFace() != null + && card.getSecondCardFace().isEnchantment() + && card.getSecondCardFace().hasSubtype(SubType.AURA, game))); } return false; } diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 106a68ece5..8a3a983ea4 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -373,7 +373,16 @@ public interface Game extends MageItem, Serializable { void addCommander(Commander commander); - void addPermanent(Permanent permanent); + /** + * Adds a permanent to the battlefield + * + * @param permanent + * @param createOrder upcounting number from state about the create order of + * all permanents. Can equal for multiple permanents, if + * they go to battlefield at the same time. If the value + * is set to 0, a next number will be set automatically. + */ + void addPermanent(Permanent permanent, int createOrder); // priority method void sendPlayerAction(PlayerAction playerAction, UUID playerId, Object data); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 05341b64b4..48604e957e 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -187,6 +187,7 @@ public abstract class GameImpl implements Game, Serializable { this.startLife = game.startLife; this.enterWithCounters.putAll(game.enterWithCounters); this.startingSize = game.startingSize; + this.gameStopped = game.gameStopped; } @Override @@ -1419,7 +1420,7 @@ public abstract class GameImpl implements Game, Serializable { if (spell != null) { if (spell.getCommandedBy() != null) { UUID commandedBy = spell.getCommandedBy(); - UUID spellControllerId = null; + UUID spellControllerId; if (commandedBy.equals(spell.getControllerId())) { spellControllerId = spell.getSpellAbility().getFirstTarget(); // i.e. resolved spell is Word of Command } else { @@ -1605,9 +1606,12 @@ public abstract class GameImpl implements Game, Serializable { } @Override - public void addPermanent(Permanent permanent) { + public void addPermanent(Permanent permanent, int createOrder) { + if (createOrder == 0) { + createOrder = getState().getNextPermanentOrderNumber(); + } + permanent.setCreateOrder(createOrder); getBattlefield().addPermanent(permanent); - permanent.setCreateOrder(getState().getNextPermanentOrderNumber()); } @Override @@ -2254,30 +2258,38 @@ public abstract class GameImpl implements Game, Serializable { } } } - //704.5m - World Enchantments + //704.5k - World Enchantments if (worldEnchantment.size() > 1) { int newestCard = -1; + Set controllerIdOfNewest = new HashSet<>(); Permanent newestPermanent = null; for (Permanent permanent : worldEnchantment) { if (newestCard == -1) { newestCard = permanent.getCreateOrder(); newestPermanent = permanent; + controllerIdOfNewest.clear(); + controllerIdOfNewest.add(permanent.getControllerId()); } else if (newestCard < permanent.getCreateOrder()) { newestCard = permanent.getCreateOrder(); newestPermanent = permanent; + controllerIdOfNewest.clear(); + controllerIdOfNewest.add(permanent.getControllerId()); } else if (newestCard == permanent.getCreateOrder()) { + // In the event of a tie for the shortest amount of time, all are put into their owners’ graveyards. This is called the “world rule.” newestPermanent = null; + controllerIdOfNewest.add(permanent.getControllerId()); } } + for (UUID controllerId : controllerIdOfNewest) { + PlayerList newestPermanentControllerRange = state.getPlayersInRange(controllerId, this); - PlayerList newestPermanentControllerRange = state.getPlayersInRange(newestPermanent.getControllerId(), this); - - // 801.12 The "world rule" applies to a permanent only if other world permanents are within its controller's range of influence. - for (Permanent permanent : worldEnchantment) { - if (newestPermanentControllerRange.contains(permanent.getControllerId()) - && !Objects.equals(newestPermanent, permanent)) { - movePermanentToGraveyardWithInfo(permanent); - somethingHappened = true; + // 801.12 The "world rule" applies to a permanent only if other world permanents are within its controller's range of influence. + for (Permanent permanent : worldEnchantment) { + if (newestPermanentControllerRange.contains(permanent.getControllerId()) + && !Objects.equals(newestPermanent, permanent)) { + movePermanentToGraveyardWithInfo(permanent); + somethingHappened = true; + } } } } @@ -2788,7 +2800,7 @@ public abstract class GameImpl implements Game, Serializable { } if (amountToPrevent != Integer.MAX_VALUE) { // set remaining amount - result.setRemainingAmount(amountToPrevent -= result.getPreventedDamage()); + result.setRemainingAmount(amountToPrevent - result.getPreventedDamage()); } MageObject damageSource = game.getObject(damageEvent.getSourceId()); MageObject preventionSource = game.getObject(source.getSourceId()); @@ -3058,7 +3070,7 @@ public abstract class GameImpl implements Game, Serializable { PermanentCard newPermanent = new PermanentCard(permanentCard.getCard(), ownerId, this); getPermanentsEntering().put(newPermanent.getId(), newPermanent); newPermanent.entersBattlefield(newPermanent.getId(), this, Zone.OUTSIDE, false); - getBattlefield().addPermanent(newPermanent); + addPermanent(newPermanent, getState().getNextPermanentOrderNumber()); getPermanentsEntering().remove(newPermanent.getId()); newPermanent.removeSummoningSickness(); if (permanentCard.isTapped()) { diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index cb993563d3..9a2289ffa6 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -1,5 +1,6 @@ package mage.game; +import java.util.*; import mage.cards.Card; import mage.cards.Cards; import mage.cards.CardsImpl; @@ -18,8 +19,6 @@ import mage.game.stack.Spell; import mage.players.Player; import mage.target.TargetCard; -import java.util.*; - /** * Created by samuelsandeen on 9/6/16. */ @@ -27,7 +26,7 @@ public final class ZonesHandler { public static boolean cast(ZoneChangeInfo info, Game game) { if (maybeRemoveFromSourceZone(info, game)) { - placeInDestinationZone(info, game); + placeInDestinationZone(info, game, 0); // create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx)) Set cards = new HashSet<>(); Set tokens = new HashSet<>(); @@ -53,7 +52,7 @@ public final class ZonesHandler { public static List moveCards(List zoneChangeInfos, Game game) { // Handle Unmelded Meld Cards - for (ListIterator itr = zoneChangeInfos.listIterator(); itr.hasNext(); ) { + for (ListIterator itr = zoneChangeInfos.listIterator(); itr.hasNext();) { ZoneChangeInfo info = itr.next(); MeldCard card = game.getMeldCard(info.event.getTargetId()); // Copies should be handled as normal cards. @@ -67,8 +66,13 @@ public final class ZonesHandler { } } zoneChangeInfos.removeIf(zoneChangeInfo -> !maybeRemoveFromSourceZone(zoneChangeInfo, game)); + int createOrder = 0; for (ZoneChangeInfo zoneChangeInfo : zoneChangeInfos) { - placeInDestinationZone(zoneChangeInfo, game); + if (createOrder == 0 && Zone.BATTLEFIELD.equals(zoneChangeInfo.event.getToZone())) { + // All permanents go to battlefield at the same time (=create order) + createOrder = game.getState().getNextPermanentOrderNumber(); + } + placeInDestinationZone(zoneChangeInfo, game, createOrder); if (game.getPhase() != null) { // moving cards to zones before game started does not need events game.addSimultaneousEvent(zoneChangeInfo.event); } @@ -76,14 +80,14 @@ public final class ZonesHandler { return zoneChangeInfos; } - private static void placeInDestinationZone(ZoneChangeInfo info, Game game) { + private static void placeInDestinationZone(ZoneChangeInfo info, Game game, int createOrder) { // Handle unmelded cards if (info instanceof ZoneChangeInfo.Unmelded) { ZoneChangeInfo.Unmelded unmelded = (ZoneChangeInfo.Unmelded) info; Zone toZone = null; for (ZoneChangeInfo subInfo : unmelded.subInfo) { toZone = subInfo.event.getToZone(); - placeInDestinationZone(subInfo, game); + placeInDestinationZone(subInfo, game, createOrder); } // We arbitrarily prefer the bottom half card. This should never be relevant. if (toZone != null) { @@ -161,7 +165,7 @@ public final class ZonesHandler { break; case BATTLEFIELD: Permanent permanent = event.getTarget(); - game.addPermanent(permanent); + game.addPermanent(permanent, createOrder); game.getPermanentsEntering().remove(permanent.getId()); break; default: @@ -197,7 +201,7 @@ public final class ZonesHandler { if (info instanceof ZoneChangeInfo.Unmelded) { ZoneChangeInfo.Unmelded unmelded = (ZoneChangeInfo.Unmelded) info; MeldCard meld = game.getMeldCard(info.event.getTargetId()); - for (Iterator itr = unmelded.subInfo.iterator(); itr.hasNext(); ) { + for (Iterator itr = unmelded.subInfo.iterator(); itr.hasNext();) { ZoneChangeInfo subInfo = itr.next(); if (!maybeRemoveFromSourceZone(subInfo, game)) { itr.remove(); diff --git a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java index 30919e478f..0ba43809a5 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java +++ b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java @@ -1,5 +1,9 @@ package mage.game.permanent.token; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.UUID; import mage.MageObject; import mage.MageObjectImpl; import mage.abilities.Ability; @@ -14,11 +18,6 @@ import mage.game.permanent.PermanentToken; import mage.players.Player; import mage.util.RandomUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.UUID; - public abstract class TokenImpl extends MageObjectImpl implements Token { protected String description; @@ -202,8 +201,9 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { } } game.setScopeRelevant(false); + int createOrder = game.getState().getNextPermanentOrderNumber(); for (Permanent permanent : permanentsEntered) { - game.addPermanent(permanent); + game.addPermanent(permanent, createOrder); permanent.setZone(Zone.BATTLEFIELD, game); game.getPermanentsEntering().remove(permanent.getId()); @@ -242,8 +242,8 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { } /** - * Set token index to search in card-pictures-tok.txt (if set have multiple tokens with same name) - * Default is 1 + * Set token index to search in card-pictures-tok.txt (if set have multiple + * tokens with same name) Default is 1 */ @Override public void setTokenType(int tokenType) {