From 24fc597fe55a0162f3be25a5afc5c053b6e116a8 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Mon, 3 Oct 2016 19:41:46 +0200 Subject: [PATCH] Fixed mode handling for modes that can be selected multiple times. --- .../cards/modal/SameModeMoreThanOnceTest.java | 99 +++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 15 +-- .../base/impl/CardTestPlayerAPIImpl.java | 3 +- .../main/java/mage/abilities/AbilityImpl.java | 5 +- Mage/src/main/java/mage/abilities/Mode.java | 14 ++- Mage/src/main/java/mage/abilities/Modes.java | 87 ++++++++++++---- Mage/src/main/java/mage/game/stack/Spell.java | 5 +- 7 files changed, 188 insertions(+), 40 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/modal/SameModeMoreThanOnceTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/modal/SameModeMoreThanOnceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/modal/SameModeMoreThanOnceTest.java new file mode 100644 index 00000000000..4bdaf851531 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/modal/SameModeMoreThanOnceTest.java @@ -0,0 +1,99 @@ +/* + * 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.cards.modal; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class SameModeMoreThanOnceTest extends CardTestPlayerBase { + + @Test + public void testEachModeOnce() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + // Choose three. You may choose the same mode more than once. + // - Target player draws a card and loses 1 life; + // - Target creature gets -2/-2 until end of turn; + // - Return target creature card from your graveyard to your hand. + addCard(Zone.HAND, playerA, "Wretched Confluence"); // Instant {3}{B}{B} + + addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wretched Confluence", "mode=1targetPlayer=PlayerA^mode=2Pillarfield Ox^mode=3Silvercoat Lion"); + setModeChoice(playerA, "1"); + setModeChoice(playerA, "2"); + setModeChoice(playerA, "3"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Wretched Confluence", 1); + assertLife(playerA, 19); + assertLife(playerB, 20); + assertHandCount(playerA, 2); + assertPowerToughness(playerB, "Pillarfield Ox", 0, 2); + assertGraveyardCount(playerA, "Silvercoat Lion", 0); + + } + + @Test + public void testSecondModeTwiceThridModeOnce() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + // Choose three. You may choose the same mode more than once. + // - Target player draws a card and loses 1 life; + // - Target creature gets -2/-2 until end of turn; + // - Return target creature card from your graveyard to your hand. + addCard(Zone.HAND, playerA, "Wretched Confluence"); // Instant {3}{B}{B} + + addCard(Zone.BATTLEFIELD, playerB, "Wall of Air"); + addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox"); + addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wretched Confluence", "mode=1Pillarfield Ox^mode=2Wall of Air^mode=3Silvercoat Lion"); + setModeChoice(playerA, "2"); + setModeChoice(playerA, "2"); + setModeChoice(playerA, "3"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Wretched Confluence", 1); + assertLife(playerA, 20); + assertLife(playerB, 20); + assertPowerToughness(playerB, "Wall of Air", -1, 3); + assertPowerToughness(playerB, "Pillarfield Ox", 0, 2); + assertGraveyardCount(playerA, "Silvercoat Lion", 0); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 4c7fd6698fd..7c554025fd0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -282,19 +282,14 @@ public class TestPlayer implements Player { Mode selectedMode = null; if (targetName.startsWith("mode=")) { int modeNr = Integer.parseInt(targetName.substring(5, 6)); - if (modeNr == 0 || modeNr > ability.getModes().size()) { + if (modeNr == 0 || modeNr > (ability.getModes().isEachModeMoreThanOnce() ? ability.getModes().getSelectedModes().size() : ability.getModes().size())) { throw new UnsupportedOperationException("Given mode number (" + modeNr + ") not available for " + ability.toString()); } UUID modeId = ability.getModes().getModeId(modeNr); - - for (UUID currentModeId : ability.getModes().getSelectedModes()) { - Mode mode = ability.getModes().get(currentModeId); - if (mode.getId().equals(modeId)) { - selectedMode = mode; - ability.getModes().setActiveMode(mode); - index = 0; // reset target index if mode changes - break; - } + selectedMode = ability.getModes().get(modeId); + if (modeId != ability.getModes().getMode().getId()) { + ability.getModes().setActiveMode(modeId); + index = 0; // reset target index if mode changes } targetName = targetName.substring(6); } else { diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index fc92814a8b6..acba31dad0e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1131,7 +1131,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * additional by setcode e.g. "creatureName-M15" you can add [no copy] to * the end of the target name to prohibite targets that are copied you can * add [only copy] to the end of the target name to allow only targets that - * are copies + * are copies For modal spells use a prefix with the mode number: + * mode=1Lightning Bolt^mode=2Silvercoat Lion */ public void addTarget(TestPlayer player, String target) { player.addTarget(target); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index c79e37f28bc..cf82795eff0 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -307,8 +307,7 @@ public abstract class AbilityImpl implements Ability { return false; } for (UUID modeId : this.getModes().getSelectedModes()) { - Mode mode = this.getModes().get(modeId); - this.getModes().setActiveMode(mode); + this.getModes().setActiveMode(modeId); //20121001 - 601.2c // 601.2c The player announces his or her choice of an appropriate player, object, or zone for // each target the spell requires. A spell may require some targets only if an alternative or @@ -330,7 +329,7 @@ public abstract class AbilityImpl implements Ability { sourceObject.adjustTargets(this, game); } // Flashback abilities haven't made the choices the underlying spell might need for targetting. - if (!(this instanceof FlashbackAbility) && mode.getTargets().size() > 0 && mode.getTargets().chooseTargets( + if (!(this instanceof FlashbackAbility) && getTargets().size() > 0 && getTargets().chooseTargets( getEffects().get(0).getOutcome(), this.controllerId, this, noMana, game) == false) { if ((variableManaCost != null || announceString != null) && !game.isSimulation()) { game.informPlayer(controller, (sourceObject != null ? sourceObject.getIdName() : "") + ": no valid targets with this value of X"); diff --git a/Mage/src/main/java/mage/abilities/Mode.java b/Mage/src/main/java/mage/abilities/Mode.java index 26942355841..113907d70d7 100644 --- a/Mage/src/main/java/mage/abilities/Mode.java +++ b/Mage/src/main/java/mage/abilities/Mode.java @@ -1,16 +1,16 @@ /* * Copyright 2011 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 @@ -20,7 +20,7 @@ * 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. @@ -54,6 +54,10 @@ public class Mode implements Serializable { this.effects = mode.effects.copy(); } + public UUID setRandomId() { + return this.id = UUID.randomUUID(); + } + public Mode copy() { return new Mode(this); } diff --git a/Mage/src/main/java/mage/abilities/Modes.java b/Mage/src/main/java/mage/abilities/Modes.java index dad2af68c02..a10edeba125 100644 --- a/Mage/src/main/java/mage/abilities/Modes.java +++ b/Mage/src/main/java/mage/abilities/Modes.java @@ -55,13 +55,14 @@ public class Modes extends LinkedHashMap { private TargetController modeChooser; private boolean eachModeMoreThanOnce; // each mode can be selected multiple times during one choice private boolean eachModeOnlyOnce; // state if each mode can be chosen only once as long as the source object exists + private final LinkedHashMap duplicateModes = new LinkedHashMap<>(); public Modes() { this.currentMode = new Mode(); this.put(currentMode.getId(), currentMode); this.minModes = 1; this.maxModes = 1; - this.selectedModes.add(currentMode.getId()); + this.addSelectedMode(currentMode.getId()); this.modeChooser = TargetController.YOU; this.eachModeOnlyOnce = false; this.eachModeMoreThanOnce = false; @@ -71,6 +72,9 @@ public class Modes extends LinkedHashMap { for (Map.Entry entry : modes.entrySet()) { this.put(entry.getKey(), entry.getValue().copy()); } + for (Map.Entry entry : modes.duplicateModes.entrySet()) { + this.put(entry.getKey(), entry.getValue().copy()); + } this.minModes = modes.minModes; this.maxModes = modes.maxModes; this.selectedModes.addAll(modes.getSelectedModes()); @@ -78,7 +82,7 @@ public class Modes extends LinkedHashMap { if (modes.getSelectedModes().isEmpty()) { this.currentMode = values().iterator().next(); } else { - this.currentMode = get(selectedModes.get(0)); + this.currentMode = get(modes.getMode().getId()); } this.modeChooser = modes.modeChooser; this.eachModeOnlyOnce = modes.eachModeOnlyOnce; @@ -89,16 +93,41 @@ public class Modes extends LinkedHashMap { return new Modes(this); } + @Override + public Mode get(Object key) { + Mode modeToGet = super.get(key); + if (modeToGet == null && eachModeMoreThanOnce) { + modeToGet = duplicateModes.get(key); + } + return modeToGet; + } + public Mode getMode() { return currentMode; } + /** + * Returns the mode by index. For modal spells with eachModeMoreThanOnce, + * the index returns the n selected mode + * + * @param index + * @return + */ public UUID getModeId(int index) { int idx = 0; - for (Mode mode : this.values()) { - idx++; - if (idx == index) { - return mode.getId(); + if (eachModeMoreThanOnce) { + for (UUID modeId : this.selectedModes) { + idx++; + if (idx == index) { + return modeId; + } + } + } else { + for (Mode mode : this.values()) { + idx++; + if (idx == index) { + return mode.getId(); + } } } return null; @@ -138,6 +167,12 @@ public class Modes extends LinkedHashMap { } } + public void setActiveMode(UUID modeId) { + if (selectedModes.contains(modeId)) { + this.currentMode = get(modeId); + } + } + public void addMode(Mode mode) { this.put(mode.getId(), mode); } @@ -145,6 +180,7 @@ public class Modes extends LinkedHashMap { public boolean choose(Game game, Ability source) { if (this.size() > 1) { this.selectedModes.clear(); + this.duplicateModes.clear(); // check if mode modifying abilities exist Card card = game.getCard(source.getSourceId()); if (card != null) { @@ -163,7 +199,7 @@ public class Modes extends LinkedHashMap { for (Mode mode : this.values()) { if ((!isEachModeOnlyOnce() || onceSelectedModes == null || !onceSelectedModes.contains(mode.getId())) && mode.getTargets().canChoose(source.getSourceId(), source.getControllerId(), game)) { - this.selectedModes.add(mode.getId()); + this.addSelectedMode(mode.getId()); } } if (isEachModeOnlyOnce()) { @@ -200,7 +236,7 @@ public class Modes extends LinkedHashMap { } return this.selectedModes.size() >= this.getMinModes(); } - this.selectedModes.add(choice.getId()); + this.addSelectedMode(choice.getId()); if (currentMode == null) { currentMode = choice; } @@ -209,17 +245,15 @@ public class Modes extends LinkedHashMap { setAlreadySelectedModes(selectedModes, source, game); } return true; + } else { // only one mode + if (currentMode == null) { + this.selectedModes.clear(); + Mode mode = this.values().iterator().next(); + this.addSelectedMode(mode.getId()); + this.setActiveMode(mode); + } + return true; } - if (currentMode == null) { - this.selectedModes.clear(); - Mode copiedMode = this.values().iterator().next().copy(); - this.selectedModes.add(copiedMode.getId()); - this.setActiveMode(copiedMode); - } - if (isEachModeOnlyOnce()) { - setAlreadySelectedModes(selectedModes, source, game); - } - return true; } /** @@ -236,6 +270,23 @@ public class Modes extends LinkedHashMap { } } + /** + * Adds a mode as selected. If the mode is already selected, it copies the + * mode and adds it to the duplicate modes + * + * @param modeId + */ + private void addSelectedMode(UUID modeId) { + if (selectedModes.contains(modeId) && eachModeMoreThanOnce) { + Mode duplicateMode = get(modeId).copy(); + duplicateMode.setRandomId(); + modeId = duplicateMode.getId(); + duplicateModes.put(modeId, duplicateMode); + + } + this.selectedModes.add(modeId); + } + // The already once selected modes for a modal card are stored as a state value // That's important for modal abilities with modes that can only selected once while the object stays in its zone @SuppressWarnings("unchecked") diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index bffef3e5ba2..69dfb83d4be 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -211,9 +211,8 @@ public class Spell extends StackObjImpl implements Card { for (SpellAbility spellAbility : this.spellAbilities) { if (spellAbilityHasLegalParts(spellAbility, game)) { for (UUID modeId : spellAbility.getModes().getSelectedModes()) { - Mode mode = spellAbility.getModes().get(modeId); - spellAbility.getModes().setActiveMode(mode); - if (mode.getTargets().stillLegal(spellAbility, game)) { + spellAbility.getModes().setActiveMode(modeId); + if (spellAbility.getTargets().stillLegal(spellAbility, game)) { if (!spellAbility.getSpellAbilityType().equals(SpellAbilityType.SPLICE)) { updateOptionalCosts(index); }