From 9364616517dcdf92f94bef725ee02ef223c594bd Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Tue, 6 May 2014 17:51:37 +0200 Subject: [PATCH] * Fix to handle returning effects correct if multiple objects return at the same time (e.g. two creatures with evolve return from exile because two Banisher Priests die by damage to all effect). (not complete finished yet, because Undying test does not run without error). --- .../mage/sets/magic2014/BanisherPriest.java | 2 + .../cards/abilities/keywords/EvolveTest.java | 79 +++++++++++++++++++ .../serverside/base/CardTestPlayerBase.java | 1 + Mage/src/mage/abilities/AbilityImpl.java | 2 - .../mage/abilities/keyword/EvolveAbility.java | 67 ++++++++++------ Mage/src/mage/cards/CardImpl.java | 2 +- Mage/src/mage/game/GameImpl.java | 6 +- Mage/src/mage/game/GameState.java | 10 ++- .../mage/game/permanent/PermanentCard.java | 2 +- 9 files changed, 142 insertions(+), 29 deletions(-) diff --git a/Mage.Sets/src/mage/sets/magic2014/BanisherPriest.java b/Mage.Sets/src/mage/sets/magic2014/BanisherPriest.java index cfd44d060e0..9e103441f3b 100644 --- a/Mage.Sets/src/mage/sets/magic2014/BanisherPriest.java +++ b/Mage.Sets/src/mage/sets/magic2014/BanisherPriest.java @@ -79,6 +79,8 @@ public class BanisherPriest extends CardImpl { // Implemented as triggered effect that doesn't uses the stack (implementation with watcher does not work correctly because if the returned creature // has a DiesTriggeredAll ability it triggers for the dying Banish Priest, what shouldn't happen) this.addAbility(new BanisherPriestReturnExiledAbility()); + + } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvolveTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvolveTest.java index 02f4baece8c..3472d6e139f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvolveTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvolveTest.java @@ -145,4 +145,83 @@ public class EvolveTest extends CardTestPlayerBase { assertPowerToughness(playerA, "Experiment One", 3, 3); } + + @Test + public void testMultipleCreaturesComeIntoPlay() { + + // Cloudfin Raptor gets one +1/+1 because itself and other creatur return from exile + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.BATTLEFIELD, playerA, "Judge's Familiar", 1); + addCard(Zone.BATTLEFIELD, playerA, "Cloudfin Raptor", 1); + addCard(Zone.HAND, playerA, "Mizzium Mortars", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Plains", 6); + addCard(Zone.HAND, playerB, "Banisher Priest", 2); + + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Banisher Priest"); + addTarget(playerB, "Cloudfin Raptor"); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Banisher Priest"); + addTarget(playerB, "Judge's Familiar"); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Mizzium Mortars with overload"); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertPermanentCount(playerB, "Banisher Priest", 0); + + assertGraveyardCount(playerB, 2); + assertGraveyardCount(playerA, 1); + + assertPermanentCount(playerA, "Cloudfin Raptor", 1); + assertPermanentCount(playerA, "Judge's Familiar", 1); + + assertPowerToughness(playerA, "Cloudfin Raptor", 1, 2); + + + } + + @Test + public void testMultipleCreaturesComeIntoPlaySuddenDisappearance() { + + // Sudden Disappearance + // Sorcery {5}{W} + // Exile all nonland permanents target player controls. Return the exiled cards + // to the battlefield under their owner's control at the beginning of the next end step. + + // Battering Krasis (2/1) and Crocanura (1/3) get both a +1/+1 counter each other because they come into play at the same time + + addCard(Zone.BATTLEFIELD, playerA, "Battering Krasis", 1); + addCard(Zone.BATTLEFIELD, playerA, "Crocanura", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Plains", 6); + addCard(Zone.HAND, playerB, "Sudden Disappearance", 2); + + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Sudden Disappearance", playerA); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, 1); + assertGraveyardCount(playerA, 0); + + assertPermanentCount(playerA, "Battering Krasis", 1); + assertPermanentCount(playerA, "Crocanura", 1); + + assertPowerToughness(playerA, "Battering Krasis", 3, 2); + assertPowerToughness(playerA, "Crocanura", 2, 4); + + + } + + } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java index 94e32f095fb..e5da7ed42cd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java @@ -42,6 +42,7 @@ public abstract class CardTestPlayerBase extends CardTestPlayerAPIImpl { public CardTestPlayerBase() { } + @Override protected TestPlayer createNewPlayer(String playerName) { return createPlayer(playerName); } diff --git a/Mage/src/mage/abilities/AbilityImpl.java b/Mage/src/mage/abilities/AbilityImpl.java index 0dc5ed073fb..c4805c97834 100644 --- a/Mage/src/mage/abilities/AbilityImpl.java +++ b/Mage/src/mage/abilities/AbilityImpl.java @@ -178,8 +178,6 @@ public abstract class AbilityImpl> implements Ability { if (effect.applyEffectsAfter()) { game.applyEffects(); } - // effects like entersBattlefield have to trigger simultanously so objects see each other - game.getState().handleSimultaneousEvent(game); } } return result; diff --git a/Mage/src/mage/abilities/keyword/EvolveAbility.java b/Mage/src/mage/abilities/keyword/EvolveAbility.java index 3710d1368be..5265a9f9ed7 100644 --- a/Mage/src/mage/abilities/keyword/EvolveAbility.java +++ b/Mage/src/mage/abilities/keyword/EvolveAbility.java @@ -28,17 +28,17 @@ package mage.abilities.keyword; -import java.util.UUID; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.Zone; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.OneShotEffect; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import mage.target.targetpointer.FixedTarget; /** * FAQ 2013/01/11 @@ -52,6 +52,34 @@ import mage.game.permanent.Permanent; * * 702.98b If a creature has multiple instances of evolve, each triggers separately * + * Rulings + * + * When comparing the stats of the two creatures, you always compare power to power and toughness to toughness. + * Whenever a creature enters the battlefield under your control, check its power and toughness against + * the power and toughness of the creature with evolve. If neither stat of the new creature is greater, + * evolve won't trigger at all. For example, if you control a 2/3 creature with evolve and a 2/2 creature + * enters the battlefield under your control, you won't have the opportunity to cast a spell like Giant Growth + * to make the 2/2 creature large enough to cause evolve to trigger. + * If evolve triggers, the stat comparison will happen again when the ability tries to resolve. If + * neither stat of the new creature is greater, the ability will do nothing. If the creature that + * entered the battlefield leaves the battlefield before evolve tries to resolve, use its last known + * power and toughness to compare the stats. + * If a creature enters the battlefield with +1/+1 counters on it, consider those counters when determining + * if evolve will trigger. For example, a 1/1 creature that enters the battlefield with two +1/+1 counters + * on it will cause the evolve ability of a 2/2 creature to trigger. + * If multiple creatures enter the battlefield at the same time, evolve may trigger multiple times, although the stat + * comparison will take place each time one of those abilities tries to resolve. For example, if you control a 2/2 + * creature with evolve and two 3/3 creatures enter the battlefield, evolve will trigger twice. The first ability + * will resolve and put a +1/+1 counter on the creature with evolve. When the second ability tries to resolve, + * neither the power nor the toughness of the new creature is greater than that of the creature with evolve, + * so that ability does nothing. + * When comparing the stats as the evolve ability resolves, it's possible that the stat that's greater changes + * from power to toughness or vice versa. If this happens, the ability will still resolve and you'll put a +1/+1 + * counter on the creature with evolve. For example, if you control a 2/2 creature with evolve and a 1/3 creature + * enters the battlefield under your control, it toughness is greater so evolve will trigger. In response, the 1/3 + * creature gets +2/-2. When the evolve trigger tries to resolve, its power is greater. You'll put a +1/+1 + * counter on the creature with evolve. + * * @author LevelX2 */ @@ -68,15 +96,17 @@ public class EvolveAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD && !event.getTargetId().equals(this.getSourceId())) { - Permanent triggeringCreature = game.getPermanent(event.getTargetId()); - if (triggeringCreature != null - && triggeringCreature.getCardType().contains(CardType.CREATURE) - && triggeringCreature.getControllerId().equals(this.controllerId)) { - Permanent sourceCreature = game.getPermanent(sourceId); - if (sourceCreature != null && isPowerOrThoughnessGreater(sourceCreature, triggeringCreature)) { - this.getEffects().get(0).setValue("triggeringCreature", event.getTargetId()); - return true; + if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD) { + if (!event.getTargetId().equals(this.getSourceId())) { + Permanent triggeringCreature = game.getPermanent(event.getTargetId()); + if (triggeringCreature != null + && triggeringCreature.getCardType().contains(CardType.CREATURE) + && triggeringCreature.getControllerId().equals(this.controllerId)) { + Permanent sourceCreature = game.getPermanent(sourceId); + if (sourceCreature != null && isPowerOrThoughnessGreater(sourceCreature, triggeringCreature)) { + this.getEffects().get(0).setTargetPointer(new FixedTarget(event.getTargetId())); + return true; + } } } } @@ -93,10 +123,7 @@ public class EvolveAbility extends TriggeredAbilityImpl { if (newCreature.getPower().getValue() > sourceCreature.getPower().getValue()) { return true; } - if (newCreature.getToughness().getValue() > sourceCreature.getToughness().getValue()) { - return true; - } - return false; + return newCreature.getToughness().getValue() > sourceCreature.getToughness().getValue(); } @Override @@ -127,11 +154,7 @@ class EvolveEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - UUID triggeringCreatureId = (UUID) getValue("triggeringCreature"); - Permanent triggeringCreature = game.getPermanent(triggeringCreatureId); - if (triggeringCreature == null) { - triggeringCreature = (Permanent) game.getLastKnownInformation(triggeringCreatureId, Zone.BATTLEFIELD); - } + Permanent triggeringCreature = game.getPermanentOrLKIBattlefield(getTargetPointer().getFirst(game, source)); if (triggeringCreature != null) { Permanent sourceCreature = game.getPermanent(source.getSourceId()); if (sourceCreature != null && EvolveAbility.isPowerOrThoughnessGreater(sourceCreature, triggeringCreature)) { diff --git a/Mage/src/mage/cards/CardImpl.java b/Mage/src/mage/cards/CardImpl.java index 9d24423dced..b28c7c31557 100644 --- a/Mage/src/mage/cards/CardImpl.java +++ b/Mage/src/mage/cards/CardImpl.java @@ -376,7 +376,7 @@ public abstract class CardImpl> extends MageObjectImpl } setControllerId(ownerId); game.setZone(objectId, event.getToZone()); - game.fireEvent(event); + game.addSimultaneousEvent(event); return game.getState().getZone(objectId) == toZone; } return false; diff --git a/Mage/src/mage/game/GameImpl.java b/Mage/src/mage/game/GameImpl.java index ef68f7e4695..cda1cff1aaf 100644 --- a/Mage/src/mage/game/GameImpl.java +++ b/Mage/src/mage/game/GameImpl.java @@ -986,7 +986,10 @@ public abstract class GameImpl> implements Game, Serializa while (!player.isPassed() && player.isInGame() && !isPaused() && !gameOver(null)) { if (!resuming) { if (checkStateAndTriggered()) { - applyEffects(); + do { + state.handleSimultaneousEvent(this); + applyEffects(); + } while (state.hasSimultaneousEvents()); } //resetLKI(); applyEffects(); @@ -1057,6 +1060,7 @@ public abstract class GameImpl> implements Game, Serializa } finally { if (top != null) { state.getStack().remove(top); + state.handleSimultaneousEvent(this); } } } diff --git a/Mage/src/mage/game/GameState.java b/Mage/src/mage/game/GameState.java index 015c3b06575..03f0da33952 100644 --- a/Mage/src/mage/game/GameState.java +++ b/Mage/src/mage/game/GameState.java @@ -494,12 +494,18 @@ public class GameState implements Serializable, Copyable { public void handleSimultaneousEvent(Game game) { if (!simultaneousEvents.isEmpty()) { - for (GameEvent event:simultaneousEvents) { + // it can happen, that the events add new simultaneous events, so copy the list before + List eventsToHandle = new ArrayList<>(); + eventsToHandle.addAll(simultaneousEvents); + simultaneousEvents.clear(); + for (GameEvent event:eventsToHandle) { this.handleEvent(event, game); } - simultaneousEvents.clear(); } } + public boolean hasSimultaneousEvents() { + return !simultaneousEvents.isEmpty(); + } public void handleEvent(GameEvent event, Game game) { watchers.watch(event, game); diff --git a/Mage/src/mage/game/permanent/PermanentCard.java b/Mage/src/mage/game/permanent/PermanentCard.java index bad546062d6..335a64f68a4 100644 --- a/Mage/src/mage/game/permanent/PermanentCard.java +++ b/Mage/src/mage/game/permanent/PermanentCard.java @@ -161,7 +161,7 @@ public class PermanentCard extends PermanentImpl { break; } game.setZone(objectId, event.getToZone()); - game.fireEvent(event); + game.addSimultaneousEvent(event); if (event.getFromZone().equals(Zone.BATTLEFIELD)) { game.resetForSourceId(getId()); game.applyEffects(); // LevelX2: needed to execute isInactive for of custom duration copy effect if source returns directly (e.g. cloudshifted clone)