diff --git a/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java b/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java index ab59754f1c3..8bb2c7eb5a9 100644 --- a/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java +++ b/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java @@ -27,15 +27,8 @@ */ package mage.cards.b; -import java.util.ArrayList; import java.util.List; import java.util.UUID; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; -import mage.constants.Zone; import mage.abilities.Ability; import mage.abilities.StaticAbility; import mage.abilities.common.SimpleStaticAbility; @@ -45,6 +38,14 @@ import mage.abilities.effects.common.continuous.BoostEquippedEffect; import mage.abilities.keyword.EquipAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.constants.Zone; import mage.filter.common.FilterArtifactPermanent; import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.CardTypePredicate; @@ -59,8 +60,7 @@ import mage.game.permanent.Permanent; public class BludgeonBrawl extends CardImpl { public BludgeonBrawl(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{2}{R}"); - + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{R}"); // Each noncreature, non-Equipment artifact is an Equipment with equip {X} and "Equipped creature gets +X/+0," where X is that artifact's converted mana cost. this.addAbility(new BludgeonBrawlAbility()); @@ -114,7 +114,7 @@ class BludgeonBrawlAddSubtypeEffect extends ContinuousEffectImpl { filter.add(Predicates.not(new CardTypePredicate(CardType.CREATURE))); filter.add(Predicates.not(new SubtypePredicate("Equipment"))); - List affectedPermanents = new ArrayList(); + Cards affectedPermanents = new CardsImpl(); List permanents = game.getBattlefield().getActivePermanents(filter, source.getControllerId(), game); for (Permanent permanent : permanents) { if (permanent != null) { @@ -149,7 +149,7 @@ class BludgeonBrawlGainAbilityEffect extends ContinuousEffectImpl { @Override public boolean apply(Game game, Ability source) { - List permanents = (List) game.getState().getValue(source.getSourceId() + "BludgeonBrawlAffectedPermanents"); + Cards permanents = (Cards) game.getState().getValue(source.getSourceId() + "BludgeonBrawlAffectedPermanents"); if (permanents != null) { for (UUID permanentId : permanents) { Permanent permanent = game.getPermanent(permanentId); diff --git a/Mage.Sets/src/mage/cards/s/Soulflayer.java b/Mage.Sets/src/mage/cards/s/Soulflayer.java index 5e70c397964..0f530d9dd7f 100644 --- a/Mage.Sets/src/mage/cards/s/Soulflayer.java +++ b/Mage.Sets/src/mage/cards/s/Soulflayer.java @@ -28,7 +28,6 @@ package mage.cards.s; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.UUID; import mage.MageInt; @@ -52,6 +51,7 @@ import mage.abilities.keyword.VigilanceAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.cards.Cards; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.Layer; @@ -69,7 +69,7 @@ import mage.util.CardUtil; public class Soulflayer extends CardImpl { public Soulflayer(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{B}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B}{B}"); this.subtype.add("Demon"); this.power = new MageInt(4); this.toughness = new MageInt(4); @@ -92,7 +92,6 @@ public class Soulflayer extends CardImpl { } } - class SoulflayerEffect extends ContinuousEffectImpl implements SourceEffect { private Set abilitiesToAdd; @@ -126,13 +125,13 @@ class SoulflayerEffect extends ContinuousEffectImpl implements SourceEffect { abilitiesToAdd = new HashSet<>(); this.objectReference = new MageObjectReference(permanent, game); String keyString = CardUtil.getCardZoneString("delvedCards", source.getSourceId(), game, true); - List delvedCards = (List) game.getState().getValue(keyString); + Cards delvedCards = (Cards) game.getState().getValue(keyString); if (delvedCards != null) { - for(Card card: delvedCards) { + for (Card card : delvedCards.getCards(game)) { if (!card.getCardType().contains(CardType.CREATURE)) { continue; } - for (Ability cardAbility: card.getAbilities()) { + for (Ability cardAbility : card.getAbilities()) { if (cardAbility instanceof FlyingAbility) { abilitiesToAdd.add(FlyingAbility.getInstance()); } @@ -170,14 +169,12 @@ class SoulflayerEffect extends ContinuousEffectImpl implements SourceEffect { } } } - for (Ability ability: abilitiesToAdd) { + for (Ability ability : abilitiesToAdd) { permanent.addAbility(ability, source.getSourceId(), game); } return true; - } else { - if (abilitiesToAdd != null) { - abilitiesToAdd = null; - } + } else if (abilitiesToAdd != null) { + abilitiesToAdd = null; } return false; } diff --git a/Mage.Sets/src/mage/cards/u/UbaMask.java b/Mage.Sets/src/mage/cards/u/UbaMask.java index 1913e259745..8c310390549 100644 --- a/Mage.Sets/src/mage/cards/u/UbaMask.java +++ b/Mage.Sets/src/mage/cards/u/UbaMask.java @@ -27,10 +27,7 @@ */ package mage.cards.u; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; -import java.util.Set; import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; @@ -59,7 +56,7 @@ public class UbaMask extends CardImpl { public final static String UBA_MASK_VALUE_KEY = "ubaMaskExiledCards"; public UbaMask(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{4}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); // If a player would draw a card, that player exiles that card face up instead. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new UbaMaskReplacementEffect())); @@ -97,7 +94,9 @@ class UbaMaskReplacementEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { if (event.getType().equals(GameEvent.EventType.PLAY_TURN)) { - game.getState().setValue(UbaMask.UBA_MASK_VALUE_KEY, null); + for (UUID playerId : game.getPlayerList()) { + game.getState().setValue(UbaMask.UBA_MASK_VALUE_KEY + source.getSourceId() + playerId, null); + } return false; } MageObject sourceObject = source.getSourceObject(game); @@ -106,15 +105,10 @@ class UbaMaskReplacementEffect extends ReplacementEffectImpl { Card card = player.getLibrary().getFromTop(game); if (card != null) { player.moveCardsToExile(card, source, game, true, source.getId(), sourceObject.getIdName()); - Map> exiledCards = (Map) game.getState().getValue(UbaMask.UBA_MASK_VALUE_KEY); - if (exiledCards == null) { - exiledCards = new HashMap<>(); - game.getState().setValue(UbaMask.UBA_MASK_VALUE_KEY, exiledCards); - } - HashSet exiledCardsByPlayer = exiledCards.get(event.getPlayerId()); + HashSet exiledCardsByPlayer = (HashSet) game.getState().getValue(UbaMask.UBA_MASK_VALUE_KEY + event.getPlayerId()); if (exiledCardsByPlayer == null) { exiledCardsByPlayer = new HashSet<>(); - exiledCards.put(event.getPlayerId(), exiledCardsByPlayer); + game.getState().setValue(UbaMask.UBA_MASK_VALUE_KEY + event.getPlayerId(), exiledCardsByPlayer); } exiledCardsByPlayer.add(new MageObjectReference(card.getId(), game)); } @@ -158,12 +152,9 @@ class UbaMaskPlayEffect extends AsThoughEffectImpl { public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { Card card = game.getCard(objectId); if (card != null && affectedControllerId.equals(card.getOwnerId()) && game.getState().getZone(card.getId()) == Zone.EXILED) { - Map> exiledCards = (Map) game.getState().getValue(UbaMask.UBA_MASK_VALUE_KEY); - if (exiledCards != null) { - Set exiledCardsByPlayer = exiledCards.get(affectedControllerId); - if (exiledCardsByPlayer != null) { - return exiledCardsByPlayer.contains(new MageObjectReference(card, game)); - } + HashSet exiledCardsByPlayer = (HashSet) game.getState().getValue(UbaMask.UBA_MASK_VALUE_KEY + affectedControllerId); + if (exiledCardsByPlayer != null) { + return exiledCardsByPlayer.contains(new MageObjectReference(card, game)); } } return false; diff --git a/Mage.Tests/src/test/java/org/mage/test/rollback/StateValuesTest.java b/Mage.Tests/src/test/java/org/mage/test/rollback/StateValuesTest.java new file mode 100644 index 00000000000..0eb60c4a695 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/rollback/StateValuesTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package org.mage.test.rollback; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class StateValuesTest extends CardTestPlayerBase { + + @Test + public void testDragonWhelpActivatedFourTimes() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // Flying + // {R}: Dragon Whelp gets +1/+0 until end of turn. If this ability has been activated four or more times this turn, sacrifice Dragon Whelp at the beginning of the next end step. + addCard(Zone.BATTLEFIELD, playerA, "Dragon Whelp", 1); // 2/3 + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: "); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: "); + attack(1, playerA, "Dragon Whelp"); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: "); + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: "); + + attack(3, playerA, "Dragon Whelp"); + + rollbackTurns(3, PhaseStep.BEGIN_COMBAT, playerA, 0); + + setStopAt(4, PhaseStep.UPKEEP); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 12); + + assertPermanentCount(playerA, "Dragon Whelp", 1); + assertGraveyardCount(playerA, "Dragon Whelp", 0); + + } + + @Test + public void testBriarbridgePatrol() { + // Whenever Briarbridge Patrol deals damage to one or more creatures, investigate (Put a colorless Clue artifact token onto the battlefield with "2, Sacrifice this artifact: Draw a card."). + // At the beginning of each end step, if you sacrificed three or more Clues this turn, you may put a creature card from your hand onto the battlefield. + addCard(Zone.BATTLEFIELD, playerA, "Briarbridge Patrol", 1); // 3/3 + + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 1); // 2/2 + + attack(1, playerA, "Briarbridge Patrol"); + block(1, playerB, "Pillarfield Ox", "Briarbridge Patrol"); + + attack(3, playerA, "Briarbridge Patrol"); + block(3, playerB, "Pillarfield Ox", "Briarbridge Patrol"); + rollbackTurns(3, PhaseStep.POSTCOMBAT_MAIN, playerA, 0); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertPermanentCount(playerA, "Clue", 2); + + } +} diff --git a/Mage/src/main/java/mage/abilities/ActivationInfo.java b/Mage/src/main/java/mage/abilities/ActivationInfo.java index 84d6b1b8db0..bf322f03af4 100644 --- a/Mage/src/main/java/mage/abilities/ActivationInfo.java +++ b/Mage/src/main/java/mage/abilities/ActivationInfo.java @@ -40,8 +40,9 @@ import mage.game.Game; */ public class ActivationInfo { - protected int turnNum; - protected int activationCounter; + protected int turnNum = 0; + protected int activationCounter = 0; + protected String key; public static ActivationInfo getInstance(Game game, UUID sourceId) { return ActivationInfo.getInstance(game, sourceId, game.getState().getZoneChangeCounter(sourceId)); @@ -49,17 +50,25 @@ public class ActivationInfo { public static ActivationInfo getInstance(Game game, UUID sourceId, int zoneChangeCounter) { String key = "ActivationInfo" + sourceId.toString() + zoneChangeCounter; - ActivationInfo activationInfo = (ActivationInfo) game.getState().getValue(key); - if (activationInfo == null) { - activationInfo = new ActivationInfo(game); - game.getState().setValue(key, activationInfo); + Integer activations = (Integer) game.getState().getValue(key); + ActivationInfo activationInfo; + if (activations != null) { + Integer turnNum = (Integer) game.getState().getValue(key + "T"); + activationInfo = new ActivationInfo(game, turnNum, activations); + } else { + activationInfo = new ActivationInfo(game, game.getTurnNum(), 0); } + activationInfo.setKey(key); return activationInfo; } - protected ActivationInfo(Game game) { - this.turnNum = game.getTurnNum(); - this.activationCounter = 0; + public void setKey(String key) { + this.key = key; + } + + protected ActivationInfo(Game game, int turnNum, int activationCounter) { + this.turnNum = turnNum; + this.activationCounter = activationCounter; } public void addActivation(Game game) { @@ -69,6 +78,8 @@ public class ActivationInfo { } else { activationCounter++; } + game.getState().setValue(key, activationCounter); + game.getState().setValue(key + "T", turnNum); } public int getActivationCounter() { diff --git a/Mage/src/main/java/mage/abilities/common/DealsDamageToOneOrMoreCreaturesTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealsDamageToOneOrMoreCreaturesTriggeredAbility.java index ef1213967c0..3b410899d99 100644 --- a/Mage/src/main/java/mage/abilities/common/DealsDamageToOneOrMoreCreaturesTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealsDamageToOneOrMoreCreaturesTriggeredAbility.java @@ -9,7 +9,6 @@ import mage.abilities.effects.Effect; import mage.constants.PhaseStep; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.turn.Step; /** * @@ -30,10 +29,10 @@ public class DealsDamageToOneOrMoreCreaturesTriggeredAbility extends DealsDamage if (super.checkTrigger(event, game)) { // check that combat damage does only once trigger also if multiple creatures were damaged because they block or were blocked by source if (game.getTurn().getStepType().equals(PhaseStep.COMBAT_DAMAGE) || game.getTurn().getStepType().equals(PhaseStep.FIRST_COMBAT_DAMAGE)) { - Step step = (Step) game.getState().getValue("damageStep" + getOriginalId()); - if (!game.getStep().equals(step)) { + Integer stepHash = (Integer) game.getState().getValue("damageStep" + getOriginalId()); + if (stepHash == null || game.getStep().hashCode() != stepHash) { // this ability did not trigger during this damage step - game.getState().setValue("damageStep" + getOriginalId(), game.getStep()); + game.getState().setValue("damageStep" + getOriginalId(), game.getStep().hashCode()); return true; } } else { diff --git a/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java b/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java index ebfc146c1ec..fa4ffdf3db5 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java @@ -37,6 +37,8 @@ import mage.abilities.costs.mana.AlternateManaPaymentAbility; import mage.abilities.costs.mana.ManaCost; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; import mage.constants.AbilityType; import mage.constants.ManaType; import mage.constants.Outcome; @@ -56,17 +58,17 @@ import mage.util.CardUtil; * applies only after the total cost of the spell with delve is determined. * 702.65b Multiple instances of delve on the same spell are redundant. * - * The rules for delve have changed slightly since it was last in an - * expansion. Previously, delve reduced the cost to cast a spell. Under the - * current rules, you exile cards from your graveyard at the same time you pay - * the spell’s cost. Exiling a card this way is simply another way to pay that - * cost. * Delve doesn't change a spell’s mana cost or converted mana cost. For - * example, Dead Drop’s converted mana cost is 10 even if you exiled three cards - * to cast it. * You can’t exile cards to pay for the colored mana requirements - * of a spell with delve. * You can’t exile more cards than the generic mana - * requirement of a spell with delve. For example, you can’t exile more than - * nine cards from your graveyard to cast Dead Drop. * Because delve isn't an - * alternative cost, it can be used in conjunction with alternative costs. + * The rules for delve have changed slightly since it was last in an expansion. + * Previously, delve reduced the cost to cast a spell. Under the current rules, + * you exile cards from your graveyard at the same time you pay the spell’s + * cost. Exiling a card this way is simply another way to pay that cost. * Delve + * doesn't change a spell’s mana cost or converted mana cost. For example, Dead + * Drop’s converted mana cost is 10 even if you exiled three cards to cast it. * + * You can’t exile cards to pay for the colored mana requirements of a spell + * with delve. * You can’t exile more cards than the generic mana requirement of + * a spell with delve. For example, you can’t exile more than nine cards from + * your graveyard to cast Dead Drop. * Because delve isn't an alternative cost, + * it can be used in conjunction with alternative costs. * * @author LevelX2 * @@ -155,18 +157,23 @@ class DelveEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { ExileFromGraveCost exileFromGraveCost = (ExileFromGraveCost) source.getCosts().get(0); + List exiledCards = exileFromGraveCost.getExiledCards(); if (exiledCards.size() > 0) { + Cards toDelve = new CardsImpl(); + for (Card card : exiledCards) { + toDelve.add(card); + } ManaPool manaPool = controller.getManaPool(); - manaPool.addMana(new Mana(0, 0, 0, 0, 0, 0, 0, exiledCards.size()), game, source); + manaPool.addMana(new Mana(0, 0, 0, 0, 0, 0, 0, toDelve.size()), game, source); manaPool.unlockManaType(ManaType.COLORLESS); String keyString = CardUtil.getCardZoneString("delvedCards", source.getSourceId(), game); @SuppressWarnings("unchecked") - List delvedCards = (List) game.getState().getValue(keyString); + Cards delvedCards = (Cards) game.getState().getValue(keyString); if (delvedCards == null) { - game.getState().setValue(keyString, exiledCards); + game.getState().setValue(keyString, toDelve); } else { - delvedCards.addAll(exiledCards); + delvedCards.addAll(toDelve); } } return true; diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index eb553340685..661b206e934 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -182,7 +183,11 @@ public class GameState implements Serializable, Copyable { this.turnMods = state.turnMods.copy(); this.watchers = state.watchers.copy(); for (Map.Entry entry : state.values.entrySet()) { - this.values.put(entry.getKey(), entry.getValue()); + if (entry.getValue() instanceof HashSet) { + this.values.put(entry.getKey(), (HashSet) ((HashSet) entry.getValue()).clone()); + } else { + this.values.put(entry.getKey(), entry.getValue()); + } } this.zones.putAll(state.zones); this.simultaneousEvents.addAll(state.simultaneousEvents); @@ -893,8 +898,10 @@ public class GameState implements Serializable, Copyable { /** * Best only use immutable objects, otherwise the states/values of the - * object may be changed by AI simulation, because the Value objects are not - * copied as the state class is copied. + * object may be changed by AI simulation or rollbacks, because the Value + * objects are not copied as the state class is copied. Mutable supported: + * HashSet with immutable entries (e.g. HashSet< UUID > or HashSet< String + * >) * * @param valueId * @param value