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 18c40b0c9bc..87a675e3241 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 @@ -19,6 +19,7 @@ import mage.cards.decks.Deck; import mage.choices.Choice; import mage.constants.*; import mage.counters.Counter; +import mage.counters.CounterType; import mage.counters.Counters; import mage.designations.Designation; import mage.designations.DesignationType; @@ -639,6 +640,13 @@ public class TestPlayer implements Player { wasProccessed = true; } + // check permanent counters: card name, counter type, count + if (params[0].equals(CHECK_COMMAND_PERMANENT_COUNTERS) && params.length == 4) { + assertPermanentCounters(action, game, computerPlayer, params[1], CounterType.findByName(params[2]), Integer.parseInt(params[3])); + actions.remove(action); + wasProccessed = true; + } + // check exile count: card name, count if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) { assertExileCount(action, game, computerPlayer, params[1], Integer.parseInt(params[2])); @@ -836,8 +844,8 @@ public class TestPlayer implements Player { List data = cards.stream() .map(c -> (c.getIdName() - + " - " + c.getPower().getValue() - + "/" + c.getToughness().getValue() + + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() + + (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") + ", " + (c.isTapped() ? "Tapped" : "Untapped") + (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName()) )) @@ -949,6 +957,17 @@ public class TestPlayer implements Player { Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must exists in " + count + " instances", count, foundedCount); } + private void assertPermanentCounters(PlayerAction action, Game game, Player player, String permanentName, CounterType counterType, int count) { + int foundedCount = 0; + for (Permanent perm : game.getBattlefield().getAllPermanents()) { + if (perm.getName().equals(permanentName) && perm.getControllerId().equals(player.getId())) { + foundedCount = perm.getCounters(game).getCount(counterType); + } + } + + Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must have " + count + " " + counterType.toString(), count, foundedCount); + } + private void assertExileCount(PlayerAction action, Game game, Player player, String permanentName, int count) { int foundedCount = 0; for (Card card : game.getExile().getAllCards(game)) { @@ -1151,10 +1170,12 @@ public class TestPlayer implements Player { public void selectAttackers(Game game, UUID attackingPlayerId) { // Loop through players and validate can attack/block this turn UUID defenderId = null; - //List + boolean mustAttackByAction = false; + boolean madeAttackByAction = false; for (Iterator it = actions.iterator(); it.hasNext(); ) { PlayerAction action = it.next(); if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("attack:")) { + mustAttackByAction = true; String command = action.getAction(); command = command.substring(command.indexOf("attack:") + 7); String[] groups = command.split("\\$"); @@ -1198,9 +1219,13 @@ public class TestPlayer implements Player { if (attacker != null && attacker.canAttack(defenderId, game)) { computerPlayer.declareAttacker(attacker.getId(), defenderId, game, false); it.remove(); + madeAttackByAction = true; } } + } + if (mustAttackByAction && !madeAttackByAction) { + this.chooseStrictModeFailed(game, "select attackers must use attack command but don't"); } } @@ -1212,10 +1237,12 @@ public class TestPlayer implements Player { @Override public void selectBlockers(Game game, UUID defendingPlayerId) { + List tempActions = new ArrayList<>(actions); + UUID opponentId = game.getOpponents(computerPlayer.getId()).iterator().next(); // Map of Blocker reference -> list of creatures blocked Map> blockedCreaturesByCreature = new HashMap<>(); - for (PlayerAction action : actions) { + for (PlayerAction action : tempActions) { if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("block:")) { String command = action.getAction(); command = command.substring(command.indexOf("block:") + 6); @@ -1226,6 +1253,7 @@ public class TestPlayer implements Player { Permanent blocker = findPermanent(new FilterControlledPermanent(), blockerName, computerPlayer.getId(), game); if (canBlockAnother(game, blocker, attacker, blockedCreaturesByCreature)) { computerPlayer.declareBlocker(defendingPlayerId, blocker.getId(), attacker.getId(), game); + actions.remove(action); } else { throw new UnsupportedOperationException(blockerName + " cannot block " + attackerName + " it is already blocking the maximum amount of creatures."); } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestMultiPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestMultiPlayerBase.java index 804e28fef92..e2ea6d4ad67 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestMultiPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestMultiPlayerBase.java @@ -1,6 +1,5 @@ package org.mage.test.serverside.base; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; import mage.game.FreeForAll; @@ -9,13 +8,14 @@ import mage.game.GameException; import mage.game.mulligan.VancouverMulligan; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; +import java.io.FileNotFoundException; + /** * Base class for testing single cards and effects in multiplayer game. For PvP * games { * - * @see CardTestPlayerBase} - * * @author magenoxx_at_gmail.com + * @see CardTestPlayerBase} */ public abstract class CardTestMultiPlayerBase extends CardTestPlayerAPIImpl { @@ -29,5 +29,4 @@ public abstract class CardTestMultiPlayerBase extends CardTestPlayerAPIImpl { playerD = createPlayer(game, playerD, "PlayerD"); return game; } - } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestMultiPlayerBaseWithRangeAll.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestMultiPlayerBaseWithRangeAll.java new file mode 100644 index 00000000000..28a88ccf913 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestMultiPlayerBaseWithRangeAll.java @@ -0,0 +1,28 @@ +package org.mage.test.serverside.base; + +import mage.constants.MultiplayerAttackOption; +import mage.constants.RangeOfInfluence; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.mulligan.VancouverMulligan; +import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; + +import java.io.FileNotFoundException; + +/** + * @author JayDi82 + */ +public abstract class CardTestMultiPlayerBaseWithRangeAll extends CardTestPlayerAPIImpl { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, playerA, "PlayerA"); + playerB = createPlayer(game, playerB, "PlayerB"); + playerC = createPlayer(game, playerC, "PlayerC"); + playerD = createPlayer(game, playerD, "PlayerD"); + return game; + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java index 25bf0512107..5d64d3d2460 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java @@ -1,12 +1,15 @@ package org.mage.test.serverside.base; +import mage.abilities.Abilities; +import mage.abilities.AbilitiesImpl; +import mage.abilities.Ability; import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; import mage.cards.decks.DeckCardLists; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; -import mage.constants.PhaseStep; -import mage.constants.RangeOfInfluence; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; import mage.game.match.MatchType; import mage.game.permanent.PermanentCard; @@ -33,7 +36,7 @@ import java.util.regex.Pattern; /** * Base class for all tests. * - * @author ayratn + * @author ayratn, JayDi85 */ public abstract class MageTestPlayerBase { @@ -333,10 +336,63 @@ public abstract class MageTestPlayerBase { return new TestPlayer(new TestComputerPlayer(name, rangeOfInfluence)); } - public void setStrictChooseMode(boolean enable) { + protected void setStrictChooseMode(boolean enable) { if (playerA != null) playerA.setChooseStrictMode(enable); if (playerB != null) playerB.setChooseStrictMode(enable); if (playerC != null) playerC.setChooseStrictMode(enable); if (playerD != null) playerD.setChooseStrictMode(enable); } + + protected void addCustomCardWithAbility(String customName, TestPlayer controllerPlayer, Ability ability) { + // add custom card with selected ability to battlefield + CustomTestCard.clearCustomAbilities(customName); + CustomTestCard.addCustomAbility(customName, ability); + CardSetInfo testSet = new CardSetInfo(customName, "custom", "123", Rarity.COMMON); + PermanentCard card = new PermanentCard(new CustomTestCard(controllerPlayer.getId(), testSet), controllerPlayer.getId(), currentGame); + getBattlefieldCards(controllerPlayer).add(card); + } +} + +// custom card with global abilities list to init (can contains abilities per card name) +class CustomTestCard extends CardImpl { + + static private Map> abilitiesList = new HashMap<>(); // card name -> abilities + + static protected void addCustomAbility(String cardName, Ability ability) { + if (!abilitiesList.containsKey(cardName)) { + abilitiesList.put(cardName, new AbilitiesImpl<>()); + } + Abilities oldAbilities = abilitiesList.get(cardName); + oldAbilities.add(ability); + } + + static protected void clearCustomAbilities(String cardName) { + abilitiesList.remove(cardName); + } + + static public void clearCustomAbilities() { + abilitiesList.clear(); + } + + + public CustomTestCard(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, ""); + + // load dynamic abilities by card name + Abilities extraAbitilies = abilitiesList.get(setInfo.getName()); + if (extraAbitilies != null) { + for (Ability ability : extraAbitilies) { + this.addAbility(ability.copy()); + } + } + } + + private CustomTestCard(final CustomTestCard card) { + super(card); + } + + @Override + public CustomTestCard copy() { + return new CustomTestCard(this); + } } 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 c7ecc49a0a1..34962ecf00c 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 @@ -55,6 +55,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String CHECK_COMMAND_LIFE = "LIFE"; public static final String CHECK_COMMAND_ABILITY = "ABILITY"; public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT"; + public static final String CHECK_COMMAND_PERMANENT_COUNTERS = "PERMANENT_COUNTERS"; public static final String CHECK_COMMAND_EXILE_COUNT = "EXILE_COUNT"; public static final String CHECK_COMMAND_HAND_COUNT = "HAND_COUNT"; public static final String CHECK_COMMAND_HAND_CARD_COUNT = "HAND_CARD_COUNT"; @@ -232,10 +233,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement + " (found actions after stop on " + maxTurn + " / " + maxPhase + ")", (maxTurn > this.stopOnTurn) || (maxTurn == this.stopOnTurn && maxPhase > this.stopAtStep.getIndex())); + for (Player player : currentGame.getPlayers().values()) { TestPlayer testPlayer = (TestPlayer) player; currentGame.cheat(player.getId(), getCommands(testPlayer)); - currentGame.cheat(player.getId(), getLibraryCards(testPlayer), getHandCards(testPlayer), + currentGame.cheat(player.getId(), activePlayer.getId(), getLibraryCards(testPlayer), getHandCards(testPlayer), getBattlefieldCards(testPlayer), getGraveCards(testPlayer)); } @@ -308,6 +310,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNT, permanentName, count.toString()); } + public void checkPermanentCounters(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, CounterType counterType, Integer count) { + check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNTERS, permanentName, counterType.toString(), count.toString()); + } + public void checkExileCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer count) { //Assert.assertNotEquals("", permanentName); check(checkName, turnNum, step, player, CHECK_COMMAND_EXILE_COUNT, permanentName, count.toString()); @@ -1231,6 +1237,16 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement + "])", count, player.getActions().size()); } + public void assertActionsMustBeEmpty(TestPlayer player) throws AssertionError { + if (!player.getActions().isEmpty()) { + System.out.println("Remaining actions for " + player.getName() + " (" + player.getActions().size() + "):"); + player.getActions().stream().forEach(a -> { + System.out.println("* turn " + a.getTurnNum() + " - " + a.getStep() + ": " + a.getActionName()); + }); + Assert.fail("Player " + player.getName() + " must have 0 actions but found " + player.getActions().size()); + } + } + public void assertChoicesCount(TestPlayer player, int count) throws AssertionError { Assert.assertEquals("(Choices of " + player.getName() + ") Count are not equal (found " + player.getChoices() + ")", count, player.getChoices().size()); } @@ -1242,7 +1258,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void assertAllCommandsUsed() throws AssertionError { for (Player player : currentGame.getPlayers().values()) { TestPlayer testPlayer = (TestPlayer) player; - assertActionsCount(testPlayer, 0); + assertActionsMustBeEmpty(testPlayer); assertChoicesCount(testPlayer, 0); assertTargetsCount(testPlayer, 0); } diff --git a/Mage/src/main/java/mage/counters/CounterType.java b/Mage/src/main/java/mage/counters/CounterType.java index 89ce5320cc0..1478bd84c5a 100644 --- a/Mage/src/main/java/mage/counters/CounterType.java +++ b/Mage/src/main/java/mage/counters/CounterType.java @@ -194,6 +194,11 @@ public enum CounterType { } } + @Override + public String toString() { + return name; + } + public static CounterType findByName(String name) { for (CounterType counterType : values()) { if (counterType.getName().equals(name)) { diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 17342a8fb72..7ee0bfdcc03 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -25,7 +25,6 @@ import mage.game.events.GameEvent; import mage.game.events.Listener; import mage.game.events.PlayerQueryEvent; import mage.game.events.TableEvent; -import mage.game.match.Match; import mage.game.match.MatchType; import mage.game.mulligan.Mulligan; import mage.game.permanent.Battlefield; @@ -433,7 +432,7 @@ public interface Game extends MageItem, Serializable { // game cheats (for tests only) void cheat(UUID ownerId, Map commands); - void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard); + void cheat(UUID ownerId, UUID activePlayerId, List library, List hand, List battlefield, List graveyard); // controlling the behaviour of replacement effects while permanents entering the battlefield void setScopeRelevant(boolean scopeRelevant); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 8d2589f68f4..3594cf3062f 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -40,7 +40,6 @@ import mage.game.command.Emblem; import mage.game.command.Plane; import mage.game.events.*; import mage.game.events.TableEvent.EventType; -import mage.game.mulligan.LondonMulligan; import mage.game.mulligan.Mulligan; import mage.game.permanent.Battlefield; import mage.game.permanent.Permanent; @@ -128,7 +127,7 @@ public abstract class GameImpl implements Game, Serializable { private int priorityTime; private final int startLife; - protected PlayerList playerList; + protected PlayerList playerList; // auto-generated from state, don't copy // infinite loop check (no copy of this attributes neccessary) private int infiniteLoopCounter; // used to check if the game is in an infinite loop @@ -138,8 +137,9 @@ public abstract class GameImpl implements Game, Serializable { // used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist) protected Map enterWithCounters = new HashMap<>(); - // used to proceed player conceding requests - private final LinkedList concedingPlayers = new LinkedList<>(); // used to handle asynchronous request of a player to leave the game + + // temporary store for income concede commands, don't copy + private final LinkedList concedingPlayers = new LinkedList<>(); public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife) { this.id = UUID.randomUUID(); @@ -2845,7 +2845,7 @@ public abstract class GameImpl implements Game, Serializable { } @Override - public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard) { + public void cheat(UUID ownerId, UUID activePlayerId, List library, List hand, List battlefield, List graveyard) { Player player = getPlayer(ownerId); if (player != null) { loadCards(ownerId, library); @@ -2864,6 +2864,8 @@ public abstract class GameImpl implements Game, Serializable { card.setZone(Zone.GRAVEYARD, this); player.getGraveyard().add(card); } + + // warning, permanents go to battlefield without resolve, continuus effects must be init for (PermanentCard permanentCard : battlefield) { permanentCard.setZone(Zone.BATTLEFIELD, this); permanentCard.setOwnerId(ownerId); @@ -2876,6 +2878,14 @@ public abstract class GameImpl implements Game, Serializable { if (permanentCard.isTapped()) { newPermanent.setTapped(true); } + + // init effects on static abilities (init continuous effects, warning, game state contains copy) + for (ContinuousEffect effect : this.getState().getContinuousEffects().getLayeredEffects(this)) { + Optional ability = this.getState().getContinuousEffects().getLayeredEffectAbilities(effect).stream().findFirst(); + if (ability.isPresent() && newPermanent.getId().equals(ability.get().getSourceId())) { + effect.init(ability.get(), this, activePlayerId); // game is not setup yet, game.activePlayer is null -- need direct id + } + } } applyEffects(); }