From 9fc0e2f25a8b4906ea1cd3d5cdb9e93e1b7287d0 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Wed, 13 Jan 2021 09:14:29 +0100 Subject: [PATCH] * Added some trace output for continous effects and triggered abilities. Changed duration of AffinityEffect to WhileOnStack to prevent wrong handling for removement of the effect. --- .../src/mage/cards/l/LithoformBlight.java | 17 ++-- ...sEffectsLastingAfterCreatorsDeathTest.java | 78 ++++++++++++++++ .../main/java/mage/abilities/AbilityImpl.java | 11 +-- .../abilities/effects/ContinuousEffects.java | 92 +++++++++++++++++-- .../effects/common/AffinityEffect.java | 8 +- ...SpellCostReductionForEachSourceEffect.java | 4 +- Mage/src/main/java/mage/game/turn/Turn.java | 29 +++--- Mage/src/main/java/mage/util/CardUtil.java | 15 ++- .../main/java/mage/util/trace/TraceInfo.java | 85 +++++++++++++++++ .../main/java/mage/util/trace/TraceUtil.java | 32 ++++++- 10 files changed, 323 insertions(+), 48 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/multiplayer/ContinuousEffectsLastingAfterCreatorsDeathTest.java create mode 100644 Mage/src/main/java/mage/util/trace/TraceInfo.java diff --git a/Mage.Sets/src/mage/cards/l/LithoformBlight.java b/Mage.Sets/src/mage/cards/l/LithoformBlight.java index eeb49c5e836..1ec4d1b8877 100644 --- a/Mage.Sets/src/mage/cards/l/LithoformBlight.java +++ b/Mage.Sets/src/mage/cards/l/LithoformBlight.java @@ -1,5 +1,6 @@ package mage.cards.l; +import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; @@ -18,8 +19,6 @@ import mage.game.permanent.Permanent; import mage.target.TargetPermanent; import mage.target.common.TargetLandPermanent; -import java.util.UUID; - /** * @author TheElk801 */ @@ -41,7 +40,7 @@ public final class LithoformBlight extends CardImpl { this.addAbility(new EntersBattlefieldTriggeredAbility(new DrawCardSourceControllerEffect(1))); // Enchanted land loses all land types and abilities and has "{T}: Add {C}" and "{T}, Pay 1 life: Add one mana of any color." - this.addAbility(new SimpleStaticAbility(new BecomesCreatureAttachedEffect())); + this.addAbility(new SimpleStaticAbility(new ChangeLandAttachedEffect())); } private LithoformBlight(final LithoformBlight card) { @@ -54,21 +53,21 @@ public final class LithoformBlight extends CardImpl { } } -class BecomesCreatureAttachedEffect extends ContinuousEffectImpl { +class ChangeLandAttachedEffect extends ContinuousEffectImpl { - BecomesCreatureAttachedEffect() { - super(Duration.WhileOnBattlefield, Outcome.LoseAbility); + ChangeLandAttachedEffect() { + super(Duration.WhileOnBattlefield, Outcome.AddAbility); staticText = "Enchanted land loses all land types and abilities " + "and has \"{T}: Add {C}\" and \"{T}, Pay 1 life: Add one mana of any color.\""; } - private BecomesCreatureAttachedEffect(final BecomesCreatureAttachedEffect effect) { + private ChangeLandAttachedEffect(final ChangeLandAttachedEffect effect) { super(effect); } @Override - public BecomesCreatureAttachedEffect copy() { - return new BecomesCreatureAttachedEffect(this); + public ChangeLandAttachedEffect copy() { + return new ChangeLandAttachedEffect(this); } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/ContinuousEffectsLastingAfterCreatorsDeathTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/ContinuousEffectsLastingAfterCreatorsDeathTest.java new file mode 100644 index 00000000000..f4a5cf13896 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/ContinuousEffectsLastingAfterCreatorsDeathTest.java @@ -0,0 +1,78 @@ +package org.mage.test.multiplayer; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +/** + * + * @author LevelX2 + */ +public class ContinuousEffectsLastingAfterCreatorsDeathTest extends CardTestMultiPlayerBase { + + // Player order: A -> D -> C -> B + @Test + public void testDontUntapNormal() { + // Trample + // Whenever you tap a land for mana, add one mana of any type that land produced. + // Whenever an opponent taps a land for mana, that land doesn't untap during its controller's next untap step. + addCard(Zone.BATTLEFIELD, playerA, "Vorinclex, Voice of Hunger"); // Creature {6}{G}{G} 7/6 + + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + + setStopAt(6, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Vorinclex, Voice of Hunger", 1); + assertPermanentCount(playerD, "Silvercoat Lion", 1); + + Assert.assertTrue("Active player is player D", currentGame.getActivePlayerId().equals(playerD.getId())); + assertTappedCount("Plains", true, 2); + } + + @Test + public void testDontUntap() { + /** + * https://github.com/magefree/mage/issues/6997 Some continuous effects + * should stay in play even after the player that set them leaves the + * game. Example: + * + * Player A: Casts Vorinclex, Voice of Hunger Player B: Taps all lands + * and do stuff (lands shouldn't untap during his next untap step) + * Player C: Kills Player A Player B: Lands untapped normally, though + * they shouldn't + * + * This happened playing commander against 3 AIs. One of the AIs played + * Vorinclex, I tapped all my lands during my turn to do stuff. Next AI + * killed the one that had Vorinclex. When the game got to my turn, my + * lands untapped normally. + */ + + // Trample + // Whenever you tap a land for mana, add one mana of any type that land produced. + // Whenever an opponent taps a land for mana, that land doesn't untap during its controller's next untap step. + addCard(Zone.BATTLEFIELD, playerA, "Vorinclex, Voice of Hunger"); // Creature {6}{G}{G} 7/6 + + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + + concede(2, PhaseStep.POSTCOMBAT_MAIN, playerA); + + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Vorinclex, Voice of Hunger", 0); + assertPermanentCount(playerD, "Silvercoat Lion", 1); + + Assert.assertTrue("Active player is player D", currentGame.getActivePlayerId().equals(playerD.getId())); + assertTappedCount("Plains", true, 2); + } + +} diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index b26d8c63b60..e05f0b362ce 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1,5 +1,9 @@ package mage.abilities; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; import mage.MageIdentifier; import mage.MageObject; import mage.abilities.costs.*; @@ -32,11 +36,6 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import org.apache.log4j.Logger; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; - /** * @author BetaSteward_at_googlemail.com */ @@ -365,7 +364,7 @@ public abstract class AbilityImpl implements Ability { //20101001 - 601.2e if (needCostModification && sourceObject != null) { - sourceObject.adjustCosts(this, game); // still needed + sourceObject.adjustCosts(this, game); // still needed for CostAdjuster objects (to handle some types of dynamic costs) game.getContinuousEffects().costModification(this, game); } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index a5cd2a688e2..b6ae54aff7e 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1,5 +1,9 @@ package mage.abilities.effects; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; import mage.ApprovingObject; import mage.MageObject; import mage.abilities.Ability; @@ -17,7 +21,6 @@ import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.CardIdPredicate; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; @@ -26,13 +29,9 @@ import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.common.TargetCardInHand; import mage.util.CardUtil; +import mage.util.trace.TraceInfo; import org.apache.log4j.Logger; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - /** * @author BetaSteward_at_googlemail.com */ @@ -1391,4 +1390,85 @@ public class ContinuousEffects implements Serializable { } return controllerFound; } + + /** + * Prints out a status of the currently existing continuous effects + * @param game + */ + public void traceContinuousEffects(Game game) { + game.getContinuousEffects().getLayeredEffects(game); + logger.info("-------------------------------------------------------------------------------------------------"); + int numberEffects = 0; + for(ContinuousEffectsList list: allEffectsLists) { + numberEffects += list.size(); + } + logger.info("Turn: " + game.getTurnNum() + " - currently existing continuous effects: " + numberEffects); + logger.info("layeredEffects ...................: " + layeredEffects.size()); + logger.info("continuousRuleModifyingEffects ...: " + continuousRuleModifyingEffects.size()); + logger.info("replacementEffects ...............: " + replacementEffects.size()); + logger.info("preventionEffects ................: " + preventionEffects.size()); + logger.info("requirementEffects ...............: " + requirementEffects.size()); + logger.info("restrictionEffects ...............: " + restrictionEffects.size()); + logger.info("restrictionUntapNotMoreThanEffects: " + restrictionUntapNotMoreThanEffects.size()); + logger.info("costModificationEffects ..........: " + costModificationEffects.size()); + logger.info("spliceCardEffects ................: " + spliceCardEffects.size()); + logger.info("asThoughEffects:"); + for (Map.Entry> entry : asThoughEffectsMap.entrySet()) { + logger.info("... " + entry.getKey().toString() + ": " + entry.getValue().size()); + } + logger.info("applyCounters ....................: " + (applyCounters != null ? "exists":"null")); + logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists":"null")); + Map orderedEffects = new TreeMap<>(); + traceAddContinuousEffects(orderedEffects, layeredEffects, game, "layeredEffects................"); + traceAddContinuousEffects(orderedEffects, continuousRuleModifyingEffects, game, "continuousRuleModifyingEffects"); + traceAddContinuousEffects(orderedEffects, replacementEffects, game, "replacementEffects............"); + traceAddContinuousEffects(orderedEffects, preventionEffects, game, "preventionEffects............."); + traceAddContinuousEffects(orderedEffects, requirementEffects, game, "requirementEffects............"); + traceAddContinuousEffects(orderedEffects, restrictionEffects, game, "restrictionEffects............"); + traceAddContinuousEffects(orderedEffects, restrictionUntapNotMoreThanEffects, game, "restrictionUntapNotMore..."); + traceAddContinuousEffects(orderedEffects, costModificationEffects, game, "costModificationEffects......."); + traceAddContinuousEffects(orderedEffects, spliceCardEffects, game, "spliceCardEffects............."); + for (Map.Entry> entry : asThoughEffectsMap.entrySet()) { + traceAddContinuousEffects(orderedEffects, entry.getValue(), game, entry.getKey().toString()); + } + String playerName = ""; + for (Map.Entry entry : orderedEffects.entrySet()) { + if (!entry.getValue().getPlayerName().equals(playerName)) { + playerName = entry.getValue().getPlayerName(); + logger.info("--- Player: " + playerName + " --------------------------------"); + } + logger.info(entry.getValue().getInfo() + + " " + entry.getValue().getSourceName() + + " " + entry.getValue().getDuration().name() + + " " + entry.getValue().getRule() + + " (Order: "+entry.getValue().getOrder() +")" + ); + } + logger.info("---- End trace Continuous effects --------------------------------------------------------------------------"); + } + public static void traceAddContinuousEffects(Map orderedEffects, ContinuousEffectsList cel, Game game, String listName) { + for (ContinuousEffect effect : cel) { + Set abilities = cel.getAbility(effect.getId()); + for (Ability ability : abilities) { + Player controller = game.getPlayer(ability.getControllerId()); + MageObject source = game.getObject(ability.getSourceId()); + TraceInfo traceInfo = new TraceInfo(); + traceInfo.setInfo(listName); + traceInfo.setOrder(effect.getOrder()); + if (ability instanceof MageSingleton) { + traceInfo.setPlayerName("Mage Singleton"); + traceInfo.setSourceName("Mage Singleton"); + } else { + traceInfo.setPlayerName(controller == null ? "no controller": controller.getName()); + traceInfo.setSourceName(source == null ? "no source": source.getIdName()); + } + traceInfo.setRule(ability.getRule()); + traceInfo.setAbilityId(ability.getId()); + traceInfo.setEffectId(effect.getId()); + traceInfo.setDuration(effect.getDuration()); + orderedEffects.put(traceInfo.getPlayerName() + traceInfo.getSourceName() + effect.getId() + ability.getId(), traceInfo); + } + } + } + } diff --git a/Mage/src/main/java/mage/abilities/effects/common/AffinityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/AffinityEffect.java index f44dfcf5450..cf332b95df1 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/AffinityEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/AffinityEffect.java @@ -10,12 +10,18 @@ import mage.filter.common.FilterControlledPermanent; import mage.game.Game; import mage.util.CardUtil; +/** + * 702.40. Affinity + 702.40a Affinity is a static ability that functions while the spell with affinity is on the stack. + “Affinity for [text]” means “This spell costs you {1} less to cast for each [text] you control.” + 702.40b If a spell has multiple instances of affinity, each of them applies. + */ public class AffinityEffect extends CostModificationEffectImpl { private final FilterControlledPermanent filter; public AffinityEffect(FilterControlledPermanent affinityFilter) { - super(Duration.Custom, Outcome.Benefit, CostModificationType.REDUCE_COST); + super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST); this.filter = affinityFilter; staticText = "Affinity for " + filter.getMessage(); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java index f7c1b9e95df..6bae62051d7 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostReductionForEachSourceEffect.java @@ -22,7 +22,7 @@ public class SpellCostReductionForEachSourceEffect extends CostModificationEffec private final int reduceGenericMana; public SpellCostReductionForEachSourceEffect(int reduceGenericMana, DynamicValue eachAmount) { - super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.REDUCE_COST); + super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST); this.eachAmount = eachAmount; this.reduceManaCosts = null; this.reduceGenericMana = reduceGenericMana; @@ -36,7 +36,7 @@ public class SpellCostReductionForEachSourceEffect extends CostModificationEffec } public SpellCostReductionForEachSourceEffect(ManaCosts reduceManaCosts, DynamicValue eachAmount) { - super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.REDUCE_COST); + super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST); this.eachAmount = eachAmount; this.reduceManaCosts = reduceManaCosts; this.reduceGenericMana = 0; diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index 681c36669f5..80410f1dba2 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -1,23 +1,21 @@ package mage.game.turn; -import mage.abilities.Ability; -import mage.constants.PhaseStep; -import mage.constants.TurnPhase; -import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.PhaseChangedEvent; -import mage.game.permanent.Permanent; -import mage.game.stack.Spell; -import mage.game.stack.StackObject; -import mage.players.Player; -import mage.util.ThreadLocalStringBuilder; - import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.UUID; +import mage.abilities.Ability; +import mage.constants.PhaseStep; +import mage.constants.TurnPhase; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.PhaseChangedEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.players.Player; +import mage.util.ThreadLocalStringBuilder; /** * @author BetaSteward_at_googlemail.com @@ -97,7 +95,10 @@ public class Turn implements Serializable { * @param activePlayer * @return true if turn is skipped */ - public boolean play(Game game, Player activePlayer) { + public boolean play(Game game, Player activePlayer) { + // uncomment this to trace triggered abilities and/or continous effects + // TraceUtil.traceTriggeredAbilities(game); + // game.getState().getContinuousEffects().traceContinuousEffects(game); activePlayer.becomesActivePlayer(); this.setDeclareAttackersStepStarted(false); if (game.isPaused() || game.checkIfGameIsOver()) { diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 90d2e36ca73..e944c4be845 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1,6 +1,12 @@ package mage.util; import com.google.common.collect.ImmutableList; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; import mage.MageObject; import mage.Mana; import mage.abilities.Abilities; @@ -36,13 +42,6 @@ import mage.target.targetpointer.FixedTarget; import mage.util.functions.CopyTokenFunction; import org.apache.log4j.Logger; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.stream.Collectors; - /** * @author nantuko */ @@ -818,7 +817,7 @@ public final class CardUtil { } public static boolean isFusedPartAbility(Ability ability, Game game) { - // TODO: is works fine with copies of spells on stack? + // TODO: does it work fine with copies of spells on stack? if (ability instanceof SpellAbility) { Spell mainSpell = game.getSpell(ability.getId()); if (mainSpell == null) { diff --git a/Mage/src/main/java/mage/util/trace/TraceInfo.java b/Mage/src/main/java/mage/util/trace/TraceInfo.java new file mode 100644 index 00000000000..fd282f6f76c --- /dev/null +++ b/Mage/src/main/java/mage/util/trace/TraceInfo.java @@ -0,0 +1,85 @@ +package mage.util.trace; + +import java.util.UUID; +import mage.constants.Duration; + +/** + * + * @author LevelX2 + */ +public class TraceInfo { + public String info; + public String playerName; + public String sourceName; + public String rule; + public UUID abilityId; + public UUID effectId; + public Duration duration; + public long order; + + public String getPlayerName() { + return playerName; + } + + public void setPlayerName(String playerName) { + this.playerName = playerName; + } + + public String getSourceName() { + return sourceName; + } + + public void setSourceName(String sourceName) { + this.sourceName = sourceName; + } + + public String getRule() { + return rule; + } + + public void setRule(String rule) { + this.rule = rule; + } + + public UUID getAbilityId() { + return abilityId; + } + + public void setAbilityId(UUID abilityId) { + this.abilityId = abilityId; + } + + public UUID getEffectId() { + return effectId; + } + + public void setEffectId(UUID effectId) { + this.effectId = effectId; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public long getOrder() { + return order; + } + + public void setOrder(long order) { + this.order = order; + } + + +} diff --git a/Mage/src/main/java/mage/util/trace/TraceUtil.java b/Mage/src/main/java/mage/util/trace/TraceUtil.java index 8b20c8cccbc..d9360a11ae6 100644 --- a/Mage/src/main/java/mage/util/trace/TraceUtil.java +++ b/Mage/src/main/java/mage/util/trace/TraceUtil.java @@ -1,8 +1,10 @@ package mage.util.trace; +import java.util.*; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.StaticAbility; +import mage.abilities.TriggeredAbility; import mage.abilities.effects.ContinuousEffectsList; import mage.abilities.effects.RestrictionEffect; import mage.abilities.keyword.CantBeBlockedSourceAbility; @@ -14,10 +16,9 @@ import mage.game.Game; import mage.game.combat.Combat; import mage.game.combat.CombatGroup; import mage.game.permanent.Permanent; +import mage.players.Player; import org.apache.log4j.Logger; -import java.util.*; - /** * @author magenoxx_at_gmail.com */ @@ -210,4 +211,31 @@ public final class TraceUtil { public static void trace(String msg) { log.info(msg); } + + /** + * Prints out a status of the currently existing triggered abilities + * @param game + */ + public static void traceTriggeredAbilities(Game game) { + log.info("-------------------------------------------------------------------------------------------------"); + log.info("Turn: " + game.getTurnNum() + " - currently existing triggered abilities: " + game.getState().getTriggers().size()); + Map orderedAbilities = new TreeMap<>(); + for (Map.Entry entry : game.getState().getTriggers().entrySet()) { + Player controller = game.getPlayer(entry.getValue().getControllerId()); + MageObject source = game.getObject(entry.getValue().getSourceId()); + orderedAbilities.put((controller == null ? "no controller": controller.getName()) + (source == null ? "no source": source.getIdName())+ entry.getKey(), entry.getKey()); + } + String playerName = ""; + for (Map.Entry entry : orderedAbilities.entrySet()) { + TriggeredAbility trAbility = game.getState().getTriggers().get(entry.getValue()); + Player controller = game.getPlayer(trAbility.getControllerId()); + MageObject source = game.getObject(trAbility.getSourceId()); + if (!controller.getName().equals(playerName)) { + playerName = controller.getName(); + log.info("--- Player: " + playerName + " --------------------------------"); + } + log.info((source == null ? "no source": source.getIdName()) + " -> " + + trAbility.getRule()); + } + } }