From 18a4a98f1acee327ce9bef252fe7e23ae23ab894 Mon Sep 17 00:00:00 2001 From: betasteward Date: Fri, 3 Apr 2015 13:37:11 -0400 Subject: [PATCH] moved copied cards to GameState --- .../test/cards/copy/IsochronScepterTest.java | 114 ++++++++++++++++++ .../cards/copy/ReversalOfFortuneTest.java | 90 ++++++++++++++ .../mage/test/cards/copy/SpelltwineTest.java | 72 +++++++++++ Mage/src/mage/game/Game.java | 1 - Mage/src/mage/game/GameImpl.java | 103 +++------------- Mage/src/mage/game/GameState.java | 43 ++++++- 6 files changed, 328 insertions(+), 95 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/copy/IsochronScepterTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/copy/ReversalOfFortuneTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/copy/SpelltwineTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/IsochronScepterTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/IsochronScepterTest.java new file mode 100644 index 00000000000..8ca4ed754fa --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/IsochronScepterTest.java @@ -0,0 +1,114 @@ +/* + * 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.copy; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author BetaSteward + */ +public class IsochronScepterTest extends CardTestPlayerBase { + + + /** + * Isochron Scepter + * Artifact, 2 (2) + * Imprint — When Isochron Scepter enters the battlefield, you may exile an + * instant card with converted mana cost 2 or less from your hand. + * {2}, {T}: You may copy the exiled card. If you do, you may cast the copy + * without paying its mana cost. + * + */ + @Test + public void testImprint() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Isochron Scepter"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Isochron Scepter"); + addTarget(playerA, "Lightning Bolt"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Isochron Scepter", 1); + assertExileCount("Lightning Bolt", 1); + assertLife(playerB, 20); + + } + + @Test + public void testCopyCard() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Isochron Scepter"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Isochron Scepter"); + addTarget(playerA, "Lightning Bolt"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{2},{T}:"); + setChoice(playerA, "Yes"); + setChoice(playerA, "Yes"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Isochron Scepter", 1); + assertExileCount("Lightning Bolt", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 0); + assertLife(playerB, 17); + + } + + @Test + public void testCopyCardButDontCast() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Isochron Scepter"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Isochron Scepter"); + addTarget(playerA, "Lightning Bolt"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{2},{T}:"); + setChoice(playerA, "Yes"); + setChoice(playerA, "No"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Isochron Scepter", 1); + assertExileCount("Lightning Bolt", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 0); + assertLife(playerB, 20); + + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/ReversalOfFortuneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/ReversalOfFortuneTest.java new file mode 100644 index 00000000000..31d1197c014 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/ReversalOfFortuneTest.java @@ -0,0 +1,90 @@ +/* + * 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.copy; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author BetaSteward + */ +public class ReversalOfFortuneTest extends CardTestPlayerBase { + + + /** + * Reversal of Fortune + * Sorcery, 4RR (6) + * Target opponent reveals his or her hand. You may copy an instant or sorcery + * card in it. If you do, you may cast the copy without paying its mana cost. + * + */ + @Test + public void testCopyCard() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.HAND, playerA, "Reversal of Fortune"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reversal of Fortune", playerB); + addTarget(playerA, "Lightning Bolt"); + setChoice(playerA, "Yes"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Reversal of Fortune", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 0); + assertGraveyardCount(playerB, "Lightning Bolt", 0); + assertLife(playerB, 17); + + } + + @Test + public void testCopyCardButDontCast() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.HAND, playerA, "Reversal of Fortune"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reversal of Fortune", playerB); + addTarget(playerA, "Lightning Bolt"); + setChoice(playerA, "No"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Reversal of Fortune", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 0); + assertGraveyardCount(playerB, "Lightning Bolt", 0); + assertLife(playerB, 20); + + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/SpelltwineTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/SpelltwineTest.java new file mode 100644 index 00000000000..dc1ed92799c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/SpelltwineTest.java @@ -0,0 +1,72 @@ +/* + * 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.copy; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author BetaSteward + */ +public class SpelltwineTest extends CardTestPlayerBase { + + + /** + * Spelltwine + * Sorcery, 5U (6) + * Exile target instant or sorcery card from your graveyard and target instant + * or sorcery card from an opponent's graveyard. Copy those cards. Cast the + * copies if able without paying their mana costs. Exile Spelltwine. + * + */ + @Test + public void testCopyCards() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + addCard(Zone.HAND, playerA, "Spelltwine"); + addCard(Zone.GRAVEYARD, playerA, "Lightning Bolt"); + addCard(Zone.GRAVEYARD, playerB, "Shock"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spelltwine"); + addTarget(playerA, "Lightning Bolt"); + addTarget(playerA, "Shock"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertExileCount("Spelltwine", 1); + assertExileCount("Lightning Bolt", 1); + assertExileCount("Shock", 1); + assertLife(playerB, 15); + + } + +} diff --git a/Mage/src/mage/game/Game.java b/Mage/src/mage/game/Game.java index 6694959ddf8..0d59775657a 100644 --- a/Mage/src/mage/game/Game.java +++ b/Mage/src/mage/game/Game.java @@ -80,7 +80,6 @@ public interface Game extends MageItem, Serializable { //game data methods void loadCards(Set cards, UUID ownerId); - void unloadCard(Card card); Collection getCards(); Object getCustomData(); void setCustomData(Object data); diff --git a/Mage/src/mage/game/GameImpl.java b/Mage/src/mage/game/GameImpl.java index 84e3cc1fc00..f73f0d97864 100644 --- a/Mage/src/mage/game/GameImpl.java +++ b/Mage/src/mage/game/GameImpl.java @@ -220,10 +220,7 @@ public abstract class GameImpl implements Game, Serializable { this.freeMulligans = game.freeMulligans; this.attackOption = game.attackOption; this.state = game.state.copy(); - // Ai simulation modifies e.g. zoneChangeCounter so copy is needed if AI active - for (Map.Entry entry: game.gameCards.entrySet()) { - this.gameCards.put(entry.getKey(), entry.getValue().copy()); - } + this.gameCards = game.gameCards; this.simulation = game.simulation; this.gameOptions = game.gameOptions; this.lki.putAll(game.lki); @@ -294,20 +291,6 @@ public abstract class GameImpl implements Game, Serializable { } } - @Override - public void unloadCard(Card card) { - gameCards.remove(card.getId()); - state.removeCard(card); - if (card.isSplitCard()) { - Card leftCard = ((SplitCard)card).getLeftHalfCard(); - gameCards.remove(leftCard.getId()); - state.removeCard(leftCard); - Card rightCard = ((SplitCard)card).getRightHalfCard(); - gameCards.remove(rightCard.getId()); - state.removeCard(rightCard); - } - } - @Override public Collection getCards() { return gameCards.values(); @@ -428,7 +411,11 @@ public abstract class GameImpl implements Game, Serializable { if (cardId == null) { return null; } - return gameCards.get(cardId); + Card card = gameCards.get(cardId); + if (card == null) { + return state.getCopiedCard(cardId); + } + return card; } @Override @@ -1328,14 +1315,7 @@ public abstract class GameImpl implements Game, Serializable { @Override public Card copyCard(Card cardToCopy, Ability source, UUID newController) { - Card copiedCard = cardToCopy.copy(); - copiedCard.assignNewId(); - copiedCard.setCopy(true); - Set cards = new HashSet<>(); - cards.add(copiedCard); - loadCards(cards, source.getControllerId()); - - return copiedCard; + return state.copyCard(cardToCopy, source, this); } @Override @@ -1437,30 +1417,13 @@ public abstract class GameImpl implements Game, Serializable { } } - // 704.5e - for (Player player: getPlayers().values()) { - for (Card card: player.getHand().getCards(this)) { - if (card.isCopy()) { - player.getHand().remove(card); - this.unloadCard(card); - } - } - for (Card card: player.getGraveyard().getCards(this)) { - if (card.isCopy()) { - player.getGraveyard().remove(card); - this.unloadCard(card); - } - } - } + // 704.5e If a copy of a spell is in a zone other than the stack, it ceases to exist. If a copy of a card is in any zone other than the stack or the battlefield, it ceases to exist. // (Isochron Scepter) 12/1/2004: If you don't want to cast the copy, you can choose not to; the copy ceases to exist the next time state-based actions are checked. - for (Card card: this.getState().getExile().getAllCards(this)) { - if (card.isCopy()) { - this.getState().getExile().removeCard(card, this); - this.unloadCard(card); - } - } - // TODO Library + graveyard - + for (Card card: this.getState().getCopiedCards()) { + Zone zone = state.getZone(card.getId()); + if (zone != Zone.BATTLEFIELD && zone != Zone.STACK) + state.removeCopiedCard(card); + } List planeswalkers = new ArrayList<>(); List legendary = new ArrayList<>(); @@ -2277,14 +2240,11 @@ public abstract class GameImpl implements Game, Serializable { switch (command.getKey()) { case HAND: if (command.getValue().equals("clear")) { - removeCards(player.getHand()); + player.getHand().clear(); } break; case LIBRARY: if (command.getValue().equals("clear")) { - for (UUID card : player.getLibrary().getCardList()) { - removeCard(card); - } player.getLibrary().clear(); } break; @@ -2315,22 +2275,6 @@ public abstract class GameImpl implements Game, Serializable { return lki; } - private void removeCards(Cards cards) { - for (UUID card : cards) { - removeCard(card); - } - cards.clear(); - } - - private void removeCard(UUID cardId) { - Card card = this.getCard(cardId); - if(card != null && card.isSplitCard()) { - gameCards.remove(((SplitCard)card).getLeftHalfCard().getId()); - gameCards.remove(((SplitCard)card).getRightHalfCard().getId()); - } - gameCards.remove(cardId); - } - @Override public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard) { Player player = getPlayer(ownerId); @@ -2374,25 +2318,6 @@ public abstract class GameImpl implements Game, Serializable { loadCards(set, ownerId); } - public void replaceLibrary(List cardsDownToTop, UUID ownerId) { - Player player = getPlayer(ownerId); - if (player != null) { - for (UUID card : player.getLibrary().getCardList()) { - removeCard(card); - } - player.getLibrary().clear(); - Set cards = new HashSet<>(); - for (Card card : cardsDownToTop) { - cards.add(card); - } - loadCards(cards, ownerId); - - for (Card card : cards) { - player.getLibrary().putOnTop(card, this); - } - } - } - @Override public boolean endTurn() { getTurn().endTurn(this, getActivePlayerId()); diff --git a/Mage/src/mage/game/GameState.java b/Mage/src/mage/game/GameState.java index 5240c08e1b1..6a5b3d98dc3 100644 --- a/Mage/src/mage/game/GameState.java +++ b/Mage/src/mage/game/GameState.java @@ -105,7 +105,8 @@ public class GameState implements Serializable, Copyable { private List simultaneousEvents = new ArrayList<>(); private Map cardState = new HashMap<>(); private Map zoneChangeCounter = new HashMap<>(); - + private Map copiedCards = new HashMap<>(); + public GameState() { players = new Players(); playerList = new PlayerList(); @@ -161,6 +162,7 @@ public class GameState implements Serializable, Copyable { cardState.put(entry.getKey(), entry.getValue().copy()); } this.zoneChangeCounter.putAll(state.zoneChangeCounter); + this.copiedCards.putAll(state.copiedCards); } @Override @@ -499,6 +501,7 @@ public class GameState implements Serializable, Copyable { this.simultaneousEvents = state.simultaneousEvents; this.cardState = state.cardState; this.zoneChangeCounter = state.zoneChangeCounter; + this.copiedCards = state.copiedCards; } public void addSimultaneousEvent(GameEvent event, Game game) { @@ -540,13 +543,18 @@ public class GameState implements Serializable, Copyable { } } - public void removeCard(Card card) { - zones.remove(card.getId()); + public void removeCopiedCard(Card card) { + if (copiedCards.containsKey(card.getId())) { + copiedCards.remove(card.getId()); + cardState.remove(card.getId()); + zones.remove(card.getId()); + zoneChangeCounter.remove(card.getId()); + } // TODO Watchers? // TODO Abilities? if (card.isSplitCard()) { - removeCard( ((SplitCard)card).getLeftHalfCard()); - removeCard( ((SplitCard)card).getRightHalfCard()); + removeCopiedCard( ((SplitCard)card).getLeftHalfCard()); + removeCopiedCard( ((SplitCard)card).getRightHalfCard()); } } @@ -819,4 +827,29 @@ public class GameState implements Serializable, Copyable { this.zoneChangeCounter.put(objectId, value); } + public Card getCopiedCard(UUID cardId) { + return copiedCards.get(cardId); + } + + public Collection getCopiedCards() { + return copiedCards.values(); + } + + public Card copyCard(Card cardToCopy, Ability source, Game game) { + Card copiedCard = cardToCopy.copy(); + copiedCard.assignNewId(); + copiedCard.setOwnerId(source.getControllerId()); + copiedCard.setCopy(true); + copiedCards.put(copiedCard.getId(), copiedCard); + addCard(copiedCard); + if (copiedCard.isSplitCard()) { + Card leftCard = ((SplitCard)copiedCard).getLeftHalfCard(); + copiedCards.put(leftCard.getId(), leftCard); + addCard(leftCard); + Card rightCard = ((SplitCard)copiedCard).getRightHalfCard(); + copiedCards.put(rightCard.getId(), rightCard); + addCard(rightCard); + } + return copiedCard; + } }