diff --git a/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java b/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java index 29b37987a6..0ec31e57d6 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java @@ -22,7 +22,7 @@ import mage.game.command.emblems.AjaniAdversaryOfTyrantsEmblem; import mage.game.command.planes.AkoumPlane; import mage.game.match.MatchType; import mage.game.mulligan.Mulligan; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import mage.game.permanent.PermanentCard; import mage.players.Player; import mage.players.StubPlayer; @@ -113,7 +113,7 @@ public class TestCardRenderDialog extends MageDialog { cardsPanel.cleanUp(); cardsPanel.setCustomRenderMode(comboRenderMode.getSelectedIndex()); - Game game = new TestGame(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new TestGame(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); Player player = new StubPlayer("player1", RangeOfInfluence.ALL); Deck deck = new Deck(); game.addPlayer(player, deck); diff --git a/Mage.Common/src/main/java/mage/utils/MageVersion.java b/Mage.Common/src/main/java/mage/utils/MageVersion.java index a72ef00ec8..330229ae5f 100644 --- a/Mage.Common/src/main/java/mage/utils/MageVersion.java +++ b/Mage.Common/src/main/java/mage/utils/MageVersion.java @@ -13,9 +13,9 @@ public class MageVersion implements Serializable, Comparable { public static final int MAGE_VERSION_MINOR = 4; public static final int MAGE_VERSION_PATCH = 37; public static final String MAGE_EDITION_INFO = ""; // set "-beta" for 1.4.32-betaV0 - public static final String MAGE_VERSION_MINOR_PATCH = "V3"; // default + public static final String MAGE_VERSION_MINOR_PATCH = "V4"; // default // strict mode - private static final boolean MAGE_VERSION_MINOR_PATCH_MUST_BE_SAME = false; // set true on uncompatible github changes, set false after new major release (after MAGE_VERSION_PATCH changes) + private static final boolean MAGE_VERSION_MINOR_PATCH_MUST_BE_SAME = true; // set true on uncompatible github changes, set false after new major release (after MAGE_VERSION_PATCH changes) public static final boolean MAGE_VERSION_SHOW_BUILD_TIME = true; private final int major; diff --git a/Mage.Server.Plugins/Mage.Game.BrawlDuel/src/mage/game/BrawlDuelMatch.java b/Mage.Server.Plugins/Mage.Game.BrawlDuel/src/mage/game/BrawlDuelMatch.java index 4e3437879b..a3d977a797 100644 --- a/Mage.Server.Plugins/Mage.Game.BrawlDuel/src/mage/game/BrawlDuelMatch.java +++ b/Mage.Server.Plugins/Mage.Game.BrawlDuel/src/mage/game/BrawlDuelMatch.java @@ -1,4 +1,3 @@ - package mage.game; import mage.game.match.MatchImpl; @@ -6,7 +5,6 @@ import mage.game.match.MatchOptions; import mage.game.mulligan.Mulligan; /** - * * @author spjspj */ public class BrawlDuelMatch extends MatchImpl { @@ -22,8 +20,6 @@ public class BrawlDuelMatch extends MatchImpl { BrawlDuel game = new BrawlDuel(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setCheckCommanderDamage(false); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(true); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/src/mage/game/BrawlFreeForAllMatch.java b/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/src/mage/game/BrawlFreeForAllMatch.java index 0e00a6ab9d..dab8c4dd52 100644 --- a/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/src/mage/game/BrawlFreeForAllMatch.java +++ b/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/src/mage/game/BrawlFreeForAllMatch.java @@ -1,4 +1,3 @@ - package mage.game; import mage.game.match.MatchImpl; @@ -6,7 +5,6 @@ import mage.game.match.MatchOptions; import mage.game.mulligan.Mulligan; /** - * * @author spjspj */ public class BrawlFreeForAllMatch extends MatchImpl { @@ -21,9 +19,7 @@ public class BrawlFreeForAllMatch extends MatchImpl { Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); BrawlFreeForAll game = new BrawlFreeForAll(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(true); game.setCheckCommanderDamage(false); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.CommanderDuel/src/mage/game/CommanderDuelMatch.java b/Mage.Server.Plugins/Mage.Game.CommanderDuel/src/mage/game/CommanderDuelMatch.java index 654930db5b..f03473c6e2 100644 --- a/Mage.Server.Plugins/Mage.Game.CommanderDuel/src/mage/game/CommanderDuelMatch.java +++ b/Mage.Server.Plugins/Mage.Game.CommanderDuel/src/mage/game/CommanderDuelMatch.java @@ -1,4 +1,3 @@ - package mage.game; import mage.game.match.MatchImpl; @@ -6,7 +5,6 @@ import mage.game.match.MatchOptions; import mage.game.mulligan.Mulligan; /** - * * @author BetaSteward_at_googlemail.com */ public class CommanderDuelMatch extends MatchImpl { @@ -18,24 +16,19 @@ public class CommanderDuelMatch extends MatchImpl { @Override public void startGame() throws GameException { int startLife = 40; - boolean alsoHand = true; // Don't like it to compare but seems like it's complicated to do it in another way boolean checkCommanderDamage = true; if (options.getDeckType().equals("Variant Magic - Duel Commander")) { startLife = 20; // Starting with the Commander 2016 update (on November 11th, 2016), Duel Commander will be played with 20 life points instead of 30. - alsoHand = true; // commander going to hand allowed to go to command zone effective July 17, 2015 checkCommanderDamage = false; // since nov 16 duel commander uses no longer commander damage rule } if (options.getDeckType().equals("Variant Magic - MTGO 1v1 Commander")) { startLife = 30; - alsoHand = true; // commander going to hand allowed to go to command zone effective July 17, 2015 } Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); CommanderDuel game = new CommanderDuel(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setCheckCommanderDamage(checkCommanderDamage); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(alsoHand); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/src/mage/game/CommanderFreeForAllMatch.java b/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/src/mage/game/CommanderFreeForAllMatch.java index efdc7aa305..fb151a8a40 100644 --- a/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/src/mage/game/CommanderFreeForAllMatch.java +++ b/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/src/mage/game/CommanderFreeForAllMatch.java @@ -1,5 +1,3 @@ - - package mage.game; import mage.game.match.MatchImpl; @@ -7,7 +5,6 @@ import mage.game.match.MatchOptions; import mage.game.mulligan.Mulligan; /** - * * @author LevelX2 */ public class CommanderFreeForAllMatch extends MatchImpl { @@ -19,16 +16,12 @@ public class CommanderFreeForAllMatch extends MatchImpl { @Override public void startGame() throws GameException { int startLife = 40; - boolean alsoHand = true; if (options.getDeckType().equals("Variant Magic - Duel Commander")) { startLife = 30; - alsoHand = true; // commander going to hand allowed to go to command zone effective July 17, 2015 } Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); CommanderFreeForAll game = new CommanderFreeForAll(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(alsoHand); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/src/mage/game/FreeformCommanderDuelMatch.java b/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/src/mage/game/FreeformCommanderDuelMatch.java index 1f3e141929..57422d41be 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/src/mage/game/FreeformCommanderDuelMatch.java +++ b/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/src/mage/game/FreeformCommanderDuelMatch.java @@ -16,15 +16,11 @@ public class FreeformCommanderDuelMatch extends MatchImpl { @Override public void startGame() throws GameException { int startLife = 20; - boolean alsoHand = true; - boolean checkCommanderDamage = true; Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); FreeformCommanderDuel game = new FreeformCommanderDuel(options.getAttackOption(), options.getRange(), mulligan, startLife); - game.setCheckCommanderDamage(checkCommanderDamage); + game.setCheckCommanderDamage(true); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(alsoHand); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/src/mage/game/FreeformCommanderFreeForAllMatch.java b/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/src/mage/game/FreeformCommanderFreeForAllMatch.java index 5c62374042..47440a3f51 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/src/mage/game/FreeformCommanderFreeForAllMatch.java +++ b/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/src/mage/game/FreeformCommanderFreeForAllMatch.java @@ -1,4 +1,3 @@ - package mage.game; import mage.game.match.MatchImpl; @@ -6,7 +5,6 @@ import mage.game.match.MatchOptions; import mage.game.mulligan.Mulligan; /** - * * @author spjspj */ public class FreeformCommanderFreeForAllMatch extends MatchImpl { @@ -18,12 +16,9 @@ public class FreeformCommanderFreeForAllMatch extends MatchImpl { @Override public void startGame() throws GameException { int startLife = 40; - boolean alsoHand = true; Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); FreeformCommanderFreeForAll game = new FreeformCommanderFreeForAll(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(alsoHand); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuel.java b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuel.java index 6979861ee8..48c40e90ad 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuel.java +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuel.java @@ -5,6 +5,8 @@ import mage.constants.RangeOfInfluence; import mage.game.match.MatchType; import mage.game.mulligan.Mulligan; +import java.util.UUID; + /** * @author JayDi85 */ @@ -12,6 +14,7 @@ public class OathbreakerDuel extends OathbreakerFreeForAll { public OathbreakerDuel(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife) { super(attackOption, range, mulligan, startLife); + this.startingPlayerSkipsDraw = true; } public OathbreakerDuel(final OathbreakerDuel game) { @@ -33,4 +36,11 @@ public class OathbreakerDuel extends OathbreakerFreeForAll { return new OathbreakerDuel(this); } + @Override + protected void init(UUID choosingPlayerId) { + super.init(choosingPlayerId); + + startingPlayerSkipsDraw = false; + } + } diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuelMatch.java b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuelMatch.java index 2d2491e2a1..8fc1fffa4c 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuelMatch.java +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/src/mage/game/OathbreakerDuelMatch.java @@ -16,13 +16,10 @@ public class OathbreakerDuelMatch extends MatchImpl { @Override public void startGame() throws GameException { int startLife = 20; - boolean alsoHand = true; Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); OathbreakerDuel game = new OathbreakerDuel(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setCheckCommanderDamage(false); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(alsoHand); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAll.java b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAll.java index 4491870a5a..7e068208e2 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAll.java +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAll.java @@ -33,6 +33,7 @@ public class OathbreakerFreeForAll extends GameCommanderImpl { public OathbreakerFreeForAll(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife) { super(attackOption, range, mulligan, startLife); + this.startingPlayerSkipsDraw = false; } public OathbreakerFreeForAll(final OathbreakerFreeForAll game) { @@ -42,13 +43,6 @@ public class OathbreakerFreeForAll extends GameCommanderImpl { game.playerOathbreakers.forEach((key, value) -> this.playerOathbreakers.put(key, new HashSet<>(value))); } - @Override - protected void init(UUID choosingPlayerId) { - // init base commander game - startingPlayerSkipsDraw = false; - super.init(choosingPlayerId); - } - private String getCommanderTypeName(Card commander) { return commander.isInstantOrSorcery() ? COMMANDER_NAME_SIGNATURE_SPELL : COMMANDER_NAME_OATHBREAKER; } diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllMatch.java b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllMatch.java index 2e2d239e28..6b342469b9 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllMatch.java +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllMatch.java @@ -16,13 +16,10 @@ public class OathbreakerFreeForAllMatch extends MatchImpl { @Override public void startGame() throws GameException { int startLife = 20; - boolean alsoHand = true; Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); OathbreakerFreeForAll game = new OathbreakerFreeForAll(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setCheckCommanderDamage(false); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(alsoHand); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/src/mage/game/PennyDreadfulCommanderFreeForAllMatch.java b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/src/mage/game/PennyDreadfulCommanderFreeForAllMatch.java index e9deed1700..f0feac621b 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/src/mage/game/PennyDreadfulCommanderFreeForAllMatch.java +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/src/mage/game/PennyDreadfulCommanderFreeForAllMatch.java @@ -1,5 +1,3 @@ - - package mage.game; import mage.game.match.MatchImpl; @@ -7,7 +5,6 @@ import mage.game.match.MatchOptions; import mage.game.mulligan.Mulligan; /** - * * @author spjspj */ public class PennyDreadfulCommanderFreeForAllMatch extends MatchImpl { @@ -19,16 +16,12 @@ public class PennyDreadfulCommanderFreeForAllMatch extends MatchImpl { @Override public void startGame() throws GameException { int startLife = 40; - boolean alsoHand = true; if (options.getDeckType().equals("Variant Magic - Duel Penny Dreadful Commander")) { startLife = 30; - alsoHand = true; // commander going to hand allowed to go to command zone effective July 17, 2015 } Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); PennyDreadfulCommanderFreeForAll game = new PennyDreadfulCommanderFreeForAll(options.getAttackOption(), options.getRange(), mulligan, startLife); game.setStartMessage(this.createGameStartMessage()); - game.setAlsoHand(alsoHand); - game.setAlsoLibrary(true); initGame(game); games.add(game); } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index 5b19c6ac35..4a3936a1e6 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -3,6 +3,7 @@ package mage.player.ai; import mage.abilities.Ability; import mage.abilities.ActivatedAbility; import mage.abilities.SpellAbility; +import mage.abilities.StaticAbility; import mage.abilities.common.PassAbility; import mage.abilities.effects.Effect; import mage.abilities.effects.SearchEffect; @@ -36,7 +37,6 @@ import org.apache.log4j.Logger; import java.io.File; import java.util.*; import java.util.concurrent.*; -import mage.abilities.StaticAbility; /** * @author nantuko @@ -794,72 +794,100 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { private void declareAttackers(Game game, UUID activePlayerId) { game.fireEvent(new GameEvent(GameEvent.EventType.DECLARE_ATTACKERS_STEP_PRE, null, null, activePlayerId)); if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, activePlayerId, activePlayerId))) { - Player attackingPlayer = game.getPlayer(activePlayerId); - // TODO: this works only in two player game, also no attack of Planeswalker - UUID defenderId = game.getOpponents(playerId).iterator().next(); - Player defender = game.getPlayer(defenderId); - List attackersList = super.getAvailableAttackers(defenderId, game); - if (attackersList.isEmpty()) { - return; - } + // TODO: add attack of Planeswalker - List possibleBlockers = defender.getAvailableBlockers(game); - - List killers = CombatUtil.canKillOpponent(game, attackersList, possibleBlockers, defender); - if (!killers.isEmpty()) { - for (Permanent attacker : killers) { - attackingPlayer.declareAttacker(attacker.getId(), defenderId, game, false); + // 1. check alpha strike first (all in attack to kill) + for (UUID defenderId : game.getOpponents(playerId)) { + Player defender = game.getPlayer(defenderId); + if (!defender.isInGame()) { + continue; + } + + List attackersList = super.getAvailableAttackers(defenderId, game); + if (attackersList.isEmpty()) { + continue; + } + List possibleBlockers = defender.getAvailableBlockers(game); + List killers = CombatUtil.canKillOpponent(game, attackersList, possibleBlockers, defender); + if (!killers.isEmpty()) { + for (Permanent attacker : killers) { + attackingPlayer.declareAttacker(attacker.getId(), defenderId, game, false); + } + return; } - return; } - // The AI will now attack more sanely. Simple, but good enough for now. - // The sim minmax does not work at the moment. - boolean safeToAttack; - CombatEvaluator eval = new CombatEvaluator(); + // 2. check all other actions + for (UUID defenderId : game.getOpponents(playerId)) { + Player defender = game.getPlayer(defenderId); + if (!defender.isInGame()) { + continue; + } + List attackersList = super.getAvailableAttackers(defenderId, game); + if (attackersList.isEmpty()) { + continue; + } + List possibleBlockers = defender.getAvailableBlockers(game); - for (Permanent attacker : attackersList) { - safeToAttack = true; - int attackerValue = eval.evaluate(attacker, game); - for (Permanent blocker : possibleBlockers) { - int blockerValue = eval.evaluate(blocker, game); - if (attacker.getPower().getValue() <= blocker.getToughness().getValue() - && attacker.getToughness().getValue() <= blocker.getPower().getValue()) { - safeToAttack = false; - } - if (attacker.getToughness().getValue() == blocker.getPower().getValue() - && attacker.getPower().getValue() == blocker.getToughness().getValue()) { - if (attackerValue > blockerValue - || blocker.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) - || blocker.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId()) - || blocker.getAbilities().contains(new ExaltedAbility()) - || blocker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId()) - || blocker.getAbilities().containsKey(IndestructibleAbility.getInstance().getId()) - || !attacker.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) - || !attacker.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId()) - || !attacker.getAbilities().contains(new ExaltedAbility())) { + // The AI will now attack more sanely. Simple, but good enough for now. + // The sim minmax does not work at the moment. + boolean safeToAttack; + CombatEvaluator eval = new CombatEvaluator(); + + for (Permanent attacker : attackersList) { + safeToAttack = true; + int attackerValue = eval.evaluate(attacker, game); + for (Permanent blocker : possibleBlockers) { + int blockerValue = eval.evaluate(blocker, game); + + // blocker can kill attacker + if (attacker.getPower().getValue() <= blocker.getToughness().getValue() + && attacker.getToughness().getValue() <= blocker.getPower().getValue()) { safeToAttack = false; } + + // kill each other + if (attacker.getToughness().getValue() == blocker.getPower().getValue() + && attacker.getPower().getValue() == blocker.getToughness().getValue()) { + if (attackerValue > blockerValue + || blocker.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) + || blocker.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId()) + || blocker.getAbilities().contains(new ExaltedAbility()) + || blocker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId()) + || blocker.getAbilities().containsKey(IndestructibleAbility.getInstance().getId()) + || !attacker.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) + || !attacker.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId()) + || !attacker.getAbilities().contains(new ExaltedAbility())) { + safeToAttack = false; + } + } + + // attacker can kill by deathtouch + if (attacker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId()) + || attacker.getAbilities().containsKey(IndestructibleAbility.getInstance().getId())) { + safeToAttack = true; + } + + // attacker can ignore blocker + if (attacker.getAbilities().containsKey(FlyingAbility.getInstance().getId()) + && !blocker.getAbilities().containsKey(FlyingAbility.getInstance().getId()) + && !blocker.getAbilities().containsKey(ReachAbility.getInstance().getId())) { + safeToAttack = true; + } } - if (attacker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId()) - || attacker.getAbilities().containsKey(IndestructibleAbility.getInstance().getId())) { - safeToAttack = true; + + // 0 damage + if (attacker.getPower().getValue() == 0) { + safeToAttack = false; } - if (attacker.getAbilities().containsKey(FlyingAbility.getInstance().getId()) - && !blocker.getAbilities().containsKey(FlyingAbility.getInstance().getId()) - && !blocker.getAbilities().containsKey(ReachAbility.getInstance().getId())) { - safeToAttack = true; + + if (safeToAttack) { + // undo has to be possible e.g. if not able to pay a attack fee (e.g. Ghostly Prison) + attackingPlayer.declareAttacker(attacker.getId(), defenderId, game, true); } } - if (attacker.getPower().getValue() == 0) { - safeToAttack = false; - } - if (safeToAttack) { - // undo has to be possible e.g. if not able to pay a attack fee (e.g. Ghostly Prison) - attackingPlayer.declareAttacker(attacker.getId(), defenderId, game, true); - } } } } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java index 5fab2bcc82..3d0fe4e6bf 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java @@ -32,7 +32,7 @@ public final class CombatUtil { public static List canKillOpponent(Game game, List attackersList, List blockersList, Player defender) { - List blockableAttackers = new ArrayList<>(blockersList); + List blockableAttackers = new ArrayList<>(attackersList); List unblockableAttackers = new ArrayList<>(); for (Permanent attacker : attackersList) { if (!canBeBlocked(game, attacker, blockersList)) { @@ -292,7 +292,7 @@ public final class CombatUtil { } return canBlock; } - + public static CombatInfo blockWithGoodTrade2(Game game, List attackers, List blockers) { UUID attackerId = game.getCombat().getAttackingPlayerId(); @@ -319,7 +319,7 @@ public final class CombatUtil { return combatInfo; } - + private static List getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List possibleBlockers) { List blockers = new ArrayList<>(); for (Permanent blocker : possibleBlockers) { @@ -335,9 +335,9 @@ public final class CombatUtil { } return blockers; } - + public static SurviveInfo willItSurvive2(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) { - + Game sim = game.copy(); Combat combat = sim.getCombat(); @@ -347,7 +347,7 @@ public final class CombatUtil { if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) { return null; } - + if (attacker.getPower().getValue() >= blocker.getToughness().getValue()) { sim.getBattlefield().removePermanent(blocker.getId()); } diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 10d32eb2f9..ca68bdcfd5 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -1993,12 +1993,14 @@ public class HumanPlayer extends PlayerImpl { } break; case PASS_PRIORITY_UNTIL_STACK_RESOLVED: + // stop recording only, real stack processing in PlayerImpl if (recordingMacro) { logger.debug("Adding a resolveStack"); PlayerResponse tResponse = new PlayerResponse(); tResponse.setString("resolveStack"); actionQueueSaved.add(tResponse); } + super.sendPlayerAction(playerAction, game, data); break; default: super.sendPlayerAction(playerAction, game, data); diff --git a/Mage.Sets/src/mage/cards/k/KnowledgePool.java b/Mage.Sets/src/mage/cards/k/KnowledgePool.java index 9f974c3466..b2317ba720 100644 --- a/Mage.Sets/src/mage/cards/k/KnowledgePool.java +++ b/Mage.Sets/src/mage/cards/k/KnowledgePool.java @@ -1,7 +1,5 @@ - package mage.cards.k; -import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; @@ -16,6 +14,8 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.common.FilterNonlandCard; +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; @@ -26,8 +26,9 @@ import mage.target.common.TargetCardInExile; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.UUID; + /** - * * @author BetaSteward_at_googlemail.com */ public final class KnowledgePool extends CardImpl { @@ -148,13 +149,15 @@ class KnowledgePoolEffect2 extends OneShotEffect { if (controller.moveCardsToExile(spell, source, game, true, exileZoneId, sourceObject.getIdName())) { Player player = game.getPlayer(spell.getControllerId()); if (player != null && player.chooseUse(Outcome.PlayForFree, "Cast another nonland card exiled with " + sourceObject.getLogName() + " without paying that card's mana cost?", source, game)) { - TargetCardInExile target = new TargetCardInExile(filter, source.getSourceId()); - while (player.choose(Outcome.PlayForFree, game.getExile().getExileZone(exileZoneId), target, game)) { + FilterNonlandCard realFilter = filter.copy(); + realFilter.add(Predicates.not(new CardIdPredicate(spell.getSourceId()))); + TargetCardInExile target = new TargetCardInExile(0, 1, realFilter, source.getSourceId()); + target.setNotTarget(true); + if (player.choose(Outcome.PlayForFree, game.getExile().getExileZone(exileZoneId), target, game)) { Card card = game.getCard(target.getFirstTarget()); if (card != null && !card.getId().equals(spell.getSourceId())) { - return player.cast(card.getSpellAbility(), game, true, new MageObjectReference(source.getSourceObject(game), game)); + player.cast(card.getSpellAbility(), game, true, new MageObjectReference(source.getSourceObject(game), game)); } - target.clearChosen(); } } return true; diff --git a/Mage.Sets/src/mage/cards/p/PraetorsGrasp.java b/Mage.Sets/src/mage/cards/p/PraetorsGrasp.java index 9f20c8703d..9a9bf1cb69 100644 --- a/Mage.Sets/src/mage/cards/p/PraetorsGrasp.java +++ b/Mage.Sets/src/mage/cards/p/PraetorsGrasp.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.AsThoughEffectImpl; @@ -9,11 +8,7 @@ import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AsThoughEffectType; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.game.ExileZone; import mage.game.Game; import mage.players.Player; @@ -21,14 +16,15 @@ import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetOpponent; import mage.util.CardUtil; +import java.util.UUID; + /** - * * @author BetaSteward */ public final class PraetorsGrasp extends CardImpl { public PraetorsGrasp(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{1}{B}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}{B}"); // Search target opponent's library for a card and exile it face down. Then that player shuffles their library. You may look at and play that card for as long as it remains exiled. this.getSpellAbility().addEffect(new PraetorsGraspEffect()); @@ -120,11 +116,7 @@ class PraetorsGraspPlayEffect extends AsThoughEffectImpl { if (exileId != null && controller != null) { ExileZone exileZone = game.getExile().getExileZone(exileId); if (exileZone != null && exileZone.contains(cardId)) { - if (controller.chooseUse(outcome, "Play the exiled card?", source, game)) { - return true; - } - } else { - discard(); + return true; } } } diff --git a/Mage.Sets/src/mage/cards/t/ThousandYearStorm.java b/Mage.Sets/src/mage/cards/t/ThousandYearStorm.java index 77f82d9ddd..67db063283 100644 --- a/Mage.Sets/src/mage/cards/t/ThousandYearStorm.java +++ b/Mage.Sets/src/mage/cards/t/ThousandYearStorm.java @@ -1,17 +1,23 @@ package mage.cards.t; import mage.abilities.Ability; +import mage.abilities.Mode; import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; +import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.WatcherScope; +import mage.constants.Zone; import mage.filter.common.FilterInstantOrSorcerySpell; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.stack.Spell; +import mage.players.Player; import mage.watchers.Watcher; import java.util.HashMap; @@ -28,9 +34,7 @@ public final class ThousandYearStorm extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{U}{R}"); // Whenever you cast an instant or sorcery spell, copy it for each other instant and sorcery spell you've cast before it this turn. You may choose new targets for the copies. - this.addAbility(new SpellCastControllerTriggeredAbility( - new ThousandYearStormEffect(), new FilterInstantOrSorcerySpell(), false, true - ), new ThousandYearWatcher()); + this.addAbility(new ThousandYearStormAbility()); } public ThousandYearStorm(final ThousandYearStorm card) { @@ -43,11 +47,48 @@ public final class ThousandYearStorm extends CardImpl { } } +class ThousandYearStormAbility extends SpellCastControllerTriggeredAbility { + + String stormCountInfo = null; + + public ThousandYearStormAbility() { + super(Zone.BATTLEFIELD, new ThousandYearStormEffect(), new FilterInstantOrSorcerySpell(), false, true); + this.addHint(new ValueHint("You've cast instant and sorcery this turn", ThousandYearSpellsCastThatTurnValue.instance)); + this.addWatcher(new ThousandYearWatcher()); + } + + public ThousandYearStormAbility(final ThousandYearStormAbility ability) { + super(ability); + this.stormCountInfo = ability.stormCountInfo; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + // save storm count info, real count will be calculated to stack ability in resolve effect only + if (super.checkTrigger(event, game)) { + int stormCount = ThousandYearSpellsCastThatTurnValue.instance.calculate(game, this, null); + stormCountInfo = " (storm count: " + Math.max(0, stormCount - 1) + ") "; + return true; + } + return false; + } + + @Override + public String getRule() { + return "Whenever you cast an instant or sorcery spell, copy it for each other instant and sorcery spell you've cast before it this turn" + + (stormCountInfo != null ? stormCountInfo : "") + ". You may choose new targets for the copies."; + } + + @Override + public ThousandYearStormAbility copy() { + return new ThousandYearStormAbility(this); + } +} + class ThousandYearStormEffect extends OneShotEffect { public ThousandYearStormEffect() { super(Outcome.Benefit); - this.staticText = "copy it for each other instant and sorcery spell you've cast before it this turn. You may choose new targets for the copies"; } public ThousandYearStormEffect(final ThousandYearStormEffect effect) { @@ -62,7 +103,8 @@ class ThousandYearStormEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Spell spell = game.getSpellOrLKIStack(getTargetPointer().getFirst(game, source)); - if (spell != null) { + Player controller = spell != null ? game.getPlayer(spell.getControllerId()) : null; + if (spell != null && controller != null) { ThousandYearWatcher watcher = game.getState().getWatcher(ThousandYearWatcher.class); if (watcher != null) { String stateSearchId = spell.getId().toString() + source.getSourceId().toString(); @@ -81,6 +123,11 @@ class ThousandYearStormEffect extends OneShotEffect { } return false; } + + @Override + public String getText(Mode mode) { + return "copy it for each other instant and sorcery spell you've cast before it this turn. You may choose new targets for the copies"; + } } class ThousandYearWatcher extends Watcher { @@ -131,3 +178,32 @@ class ThousandYearWatcher extends Watcher { } } + +enum ThousandYearSpellsCastThatTurnValue implements DynamicValue { + + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + ThousandYearWatcher watcher = game.getState().getWatcher(ThousandYearWatcher.class); + if (watcher == null) { + return 0; + } + return watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(sourceAbility.getControllerId()); + } + + @Override + public ThousandYearSpellsCastThatTurnValue copy() { + return instance; + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return "You cast an instant or sorcery spell in this turn"; + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java new file mode 100644 index 0000000000..e8495eb60b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java @@ -0,0 +1,222 @@ +package org.mage.test.AI.basic; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseAI; + +/** + * @author JayDi85 + */ +public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { + + // only PlayerA is AI controlled + + // Trove of Temptation + // Each opponent must attack you or a planeswalker you control with at least one creature each combat if able. + + @Test + public void test_Attack_2_big_vs_0() { + // 2 x 2/2 vs 0 - can't lose any attackers + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2 + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2 - 2); + } + + @Test + public void test_Attack_2_big_vs_1_small() { + // 2 x 2/2 vs 1 x 1/1 - can't lose any attackers + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2 - 2); + } + + @Test + public void test_Attack_1_big_vs_2_small() { + // 1 x 2/2 vs 2 x 1/1 - can lose 1 attacker, but will kill more opponents + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 2); // 1/1 + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2); + } + + @Test + public void test_Attack_2_big_vs_2_small() { + // 2 x 2/2 vs 2 x 1/1 - can lose 1 attacker, but will kill more opponents + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 2); // 1/1 + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2 - 2); + } + + @Test + public void test_Attack_1_small_vs_0() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 1); + } + + @Test + public void test_Attack_1_small_vs_1_big() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20); // no attacks + } + + @Test + public void test_Attack_2_small_vs_1_big() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 2); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20); // no attacks + } + + @Test + public void test_Attack_15_small_vs_1_big_kill_stike() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 15); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 + + block(1, playerB, "Ancient Brontodon", "Balduvian Bears"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - ((15 - 1) * 2)); // one will be blocked + } + + @Test + @Ignore // TODO: add massive attack vs small amount of blockers + public void test_Attack_10_small_vs_1_big_massive_strike() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 10); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 + + block(1, playerB, "Ancient Brontodon", "Balduvian Bears"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - ((10 - 1) * 2)); // one will be blocked + } + + @Test + public void test_ForceAttack_1_small_vs_1_big_a() { + addCard(Zone.BATTLEFIELD, playerA, "Goblin Brigand", 1); // 1/1, force to attack + addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 + + block(1, playerB, "Ancient Brontodon", "Goblin Brigand"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Goblin Brigand", 1); + assertLife(playerA, 20); + assertLife(playerB, 20); + } + + @Test + public void test_ForceAttack_2_small_vs_2_big() { + addCard(Zone.BATTLEFIELD, playerA, "Goblin Brigand", 2); // 1/1, force to attack + addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 2); // 9/9 + + block(1, playerB, "Ancient Brontodon:0", "Goblin Brigand:0"); + block(1, playerB, "Ancient Brontodon:1", "Goblin Brigand:1"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Goblin Brigand", 2); + assertLife(playerA, 20); + assertLife(playerB, 20); + } + + @Test + @Ignore // TODO: need to fix Trove of Temptation effect (player must attack by one creature) + public void test_ForceAttack_1_small_vs_1_big_b() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 + // Trove of Temptation + // Each opponent must attack you or a planeswalker you control with at least one creature each combat if able. + addCard(Zone.BATTLEFIELD, playerB, "Trove of Temptation", 1); // 9/9 + + block(1, playerB, "Ancient Brontodon", "Arbor Elf"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Arbor Elf", 1); + assertLife(playerA, 20); + assertLife(playerB, 20); + } + + @Test + public void test_Attack_1_with_counters_vs_1() { + // chainbreaker real stats is 1/1, it's can be saftly attacked + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Chainbreaker", 1); // 3/3, but with 2x -1/-1 counters + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/PreventRepeatedActionsTest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/PreventRepeatedActionsTest.java index 2665ebcd04..9eb405615d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/PreventRepeatedActionsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/PreventRepeatedActionsTest.java @@ -1,4 +1,3 @@ - package org.mage.test.AI.basic; import mage.constants.PhaseStep; @@ -10,14 +9,12 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBaseAI; /** - * * @author LevelX2 */ public class PreventRepeatedActionsTest extends CardTestPlayerBaseAI { /** * Check that an equipment is not switched again an again between creatures - * */ @Test public void testEquipOnlyOnce() { @@ -77,10 +74,13 @@ public class PreventRepeatedActionsTest extends CardTestPlayerBaseAI { attack(2, playerB, "Silvercoat Lion"); attack(2, playerB, "Silvercoat Lion"); + blockSkip(2, playerA); setStopAt(2, PhaseStep.END_TURN); execute(); + assertPermanentCount(playerA, "Kiora's Follower", 2); + assertPermanentCount(playerB, "Silvercoat Lion", 2); assertLife(playerA, 16); assertTapped("Kiora's Follower", false); } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetPriorityTest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetPriorityTest.java index a9b798cb40..72d1166e1a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetPriorityTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetPriorityTest.java @@ -9,6 +9,7 @@ import mage.abilities.effects.common.DamageMultiEffect; import mage.constants.CardType; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; import mage.target.common.TargetCreaturePermanentAmount; import org.junit.Ignore; import org.junit.Test; @@ -96,7 +97,70 @@ public class TargetPriorityTest extends CardTestPlayerBaseAI { assertPermanentCount(playerB, "Battering Sliver", 3); } - // + @Test + @Ignore // TODO: enable it after chooseTarget will be rewrites like choseTargetAmount + public void test_target_KillCreatureNotDamage() { + // https://github.com/magefree/mage/issues/4497 + // choose target for damage selects wrong target + + addCard(Zone.HAND, playerA, "Burning Sun's Avatar"); // 3 damage to target on enter + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1, true); // avatar can be cast on 3 turn + // + addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2, will be +2 counters + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + + // prepare, A can't cast avatar until mana is tapped + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", CounterType.P1P1, 2); + checkPermanentCounters("counters", 1, PhaseStep.BEGIN_COMBAT, playerB, "Balduvian Bears", CounterType.P1P1, 2); + + // AI cast avatar on turn 3 and target 1 creature to kil by 3 damage + + //setStrictChooseMode(true); // AI + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Burning Sun's Avatar", 1); + assertPermanentCount(playerB, "Ancient Brontodon", 1); // can't be killed + assertPermanentCount(playerB, "Balduvian Bears", 1); // 2/2, but with counters is 4/4, can't be killed + assertPermanentCount(playerB, "Arbor Elf", 0); // must be killed + assertGraveyardCount(playerB, "Arbor Elf", 1); + } + + @Test + @Ignore // do not enable it in production, only for devs + public void test_target_Performance() { + int cardsMultiplier = 10; + + addCard(Zone.HAND, playerA, "Lightning Bolt", 1 * cardsMultiplier); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1 * cardsMultiplier); + // + addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1 * cardsMultiplier); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1 * cardsMultiplier); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Ashcoat Bear", 1 * cardsMultiplier); // 2/2 with ability + addCard(Zone.BATTLEFIELD, playerB, "Golden Bear", 1 * cardsMultiplier); // 4/3 + addCard(Zone.BATTLEFIELD, playerB, "Battering Sliver", 1 * cardsMultiplier); // 4/4 with ability + + //castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt"); + + showHand("after", 1, PhaseStep.BEGIN_COMBAT, playerA); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + /* disabled checks cause target is not yet fixed, see comments at the start file + assertPermanentCount(playerB, "Memnite", 1 * cardsMultiplier); + assertPermanentCount(playerB, "Balduvian Bears", 1 * cardsMultiplier); + assertPermanentCount(playerB, "Ashcoat Bear", 1 * cardsMultiplier); + assertPermanentCount(playerB, "Golden Bear", 1 * cardsMultiplier - 1); + assertPermanentCount(playerB, "Battering Sliver", 1 * cardsMultiplier); + */ + } + + + // TARGET AMOUNT @Test public void test_targetAmount_PriorityKillByBigPT() { @@ -228,4 +292,33 @@ public class TargetPriorityTest extends CardTestPlayerBaseAI { assertPermanentCount(playerA, "Golden Bear", 3); assertPermanentCount(playerA, "Battering Sliver", 3); } + + @Test + @Ignore // do not enable it in production, only for devs + public void test_targetAmount_Performance() { + int cardsMultiplier = 3; + + Ability ability = new SimpleActivatedAbility(Zone.ALL, new DamageMultiEffect(3), new ManaCostsImpl("R")); + ability.addTarget(new TargetCreaturePermanentAmount(3)); + addCustomCardWithAbility("damage 3", playerA, ability); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1 * cardsMultiplier); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1 * cardsMultiplier); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Ashcoat Bear", 1 * cardsMultiplier); // 2/2 with ability + addCard(Zone.BATTLEFIELD, playerB, "Golden Bear", 1 * cardsMultiplier); // 4/3 + addCard(Zone.BATTLEFIELD, playerB, "Battering Sliver", 1 * cardsMultiplier); // 4/4 with ability + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: "); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerB, "Memnite", 1 * cardsMultiplier); + assertPermanentCount(playerB, "Balduvian Bears", 1 * cardsMultiplier); + assertPermanentCount(playerB, "Ashcoat Bear", 1 * cardsMultiplier); + assertPermanentCount(playerB, "Golden Bear", 1 * cardsMultiplier - 1); + assertPermanentCount(playerB, "Battering Sliver", 1 * cardsMultiplier); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetsAreChosenTest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetsAreChosenTest.java index edc51839f0..ffc0fa4621 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetsAreChosenTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetsAreChosenTest.java @@ -1,4 +1,3 @@ - package org.mage.test.AI.basic; import mage.constants.PhaseStep; @@ -9,7 +8,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBaseAI; /** - * * @author LevelX2 */ public class TargetsAreChosenTest extends CardTestPlayerBaseAI { @@ -158,6 +156,7 @@ public class TargetsAreChosenTest extends CardTestPlayerBaseAI { // Whenever a creature an opponent controls dies, put a +1/+1 counter on Malakir Cullblade. addCard(Zone.BATTLEFIELD, playerA, "Malakir Cullblade", 5); + attackSkip(1, playerA); attack(3, playerA, "Nefashu"); setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/NaturesWillTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/NaturesWillTest.java index 3b4b2b2f35..5d17aca1fb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/NaturesWillTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/NaturesWillTest.java @@ -7,7 +7,7 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -16,7 +16,7 @@ import java.io.FileNotFoundException; public class NaturesWillTest extends CardTestPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); playerC = createPlayer(game, playerC, "PlayerC"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/StormTheVaultTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/StormTheVaultTest.java index 287f6828c7..f43cc3b6b5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/StormTheVaultTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/StormTheVaultTest.java @@ -7,7 +7,7 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -16,7 +16,7 @@ import java.io.FileNotFoundException; public class StormTheVaultTest extends CardTestPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); playerC = createPlayer(game, playerC, "PlayerC"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java index 77e3a7e53d..0b0ea38011 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java @@ -1,5 +1,6 @@ package org.mage.test.cards.continuous; +import mage.abilities.keyword.FirstStrikeAbility; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; @@ -183,4 +184,91 @@ public class CommandersCastTest extends CardTestCommander4Players { execute(); assertAllCommandsUsed(); } + + @Test + public void test_CastFromHandWithoutTaxIncrease() { + // Player order: A -> D -> C -> B + + addCard(Zone.COMMAND, playerA, "Balduvian Bears", 1); // {1}{G}, 2/2, commander + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // cast from command for {1}{G}, from hand for {1}{G}, from command for {1}{G}{2} + // + // Counter target spell. If that spell is countered this way, put it into its owner’s hand instead of into that player’s graveyard. + // Draw a card. + addCard(Zone.HAND, playerB, "Remand", 2); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); // counter 2 times + + // cast 1 and counter (increase commander tax) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Remand", "Balduvian Bears", "Balduvian Bears"); + setChoice(playerA, "No"); // move to hand + checkCommandCardCount("cast 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkHandCardCount("cast 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Balduvian Bears", 1); + checkPermanentCount("cast 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Balduvian Bears", 0); + + ///* + // cast 2 from hand without tax + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears"); + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerB, "Remand", "Balduvian Bears", "Balduvian Bears"); + setChoice(playerA, "Yes"); // move to command zone + checkCommandCardCount("cast 2", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, "Balduvian Bears", 1); + checkHandCardCount("cast 2", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkPermanentCount("cast 2", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, "Balduvian Bears", 0); + + // cast 3 from command with tax for 1 play + castSpell(9, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears"); + checkCommandCardCount("cast 3", 9, PhaseStep.BEGIN_COMBAT, playerA, "Balduvian Bears", 0); + checkHandCardCount("cast 3", 9, PhaseStep.BEGIN_COMBAT, playerA, "Balduvian Bears", 0); + checkPermanentCount("cast 3", 9, PhaseStep.BEGIN_COMBAT, playerA, "Balduvian Bears", 1); + + setStrictChooseMode(true); + setStopAt(9, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_AlternativeSpellNormal() { + // Player order: A -> D -> C -> B + + // Weapon Surge + // Target creature you control gets +1/+0 and gains first strike until end of turn. + // Overload {1}{R} (You may cast this spell for its overload cost. If you do, change its text by replacing all instances of “target” with “each.”) + addCard(Zone.HAND, playerA, "Weapon Surge", 1); // {R} or {1}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); + + // cast overload + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Weapon Surge with overload"); + checkAbility("after", 1, PhaseStep.BEGIN_COMBAT, playerA, "Balduvian Bears", FirstStrikeAbility.class, true); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_AlternativeSpellCommander() { + // Player order: A -> D -> C -> B + + // Weapon Surge + // Target creature you control gets +1/+0 and gains first strike until end of turn. + // Overload {1}{R} (You may cast this spell for its overload cost. If you do, change its text by replacing all instances of “target” with “each.”) + addCard(Zone.COMMAND, playerA, "Weapon Surge", 1); // {R} or {1}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); + + // cast overload + showAvaileableAbilities("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Weapon Surge with overload"); + setChoice(playerA, "Yes"); // move to command zone + checkAbility("after", 1, PhaseStep.BEGIN_COMBAT, playerA, "Balduvian Bears", FirstStrikeAbility.class, true); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/PraetorsGraspTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/PraetorsGraspTest.java new file mode 100644 index 0000000000..f21418d1c9 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/PraetorsGraspTest.java @@ -0,0 +1,34 @@ +package org.mage.test.cards.continuous; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class PraetorsGraspTest extends CardTestPlayerBase { + + @Test + public void test_SimpleCast() { + // Search target opponent’s library for a card and exile it face down. Then that player shuffles their library. + // You may look at and play that card for as long as it remains exiled. + addCard(Zone.HAND, playerA, "Praetor's Grasp", 1); // {1}{B}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Praetor's Grasp"); + addTarget(playerA, playerB); + addTarget(playerA, "Mountain"); + + showAvaileableAbilities("after", 1, PhaseStep.POSTCOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Praetor's Grasp", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/TroveOfTemptationTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/TroveOfTemptationTest.java deleted file mode 100644 index a980bbd46e..0000000000 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/TroveOfTemptationTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.mage.test.cards.continuous; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import org.junit.Ignore; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * @author JayDi85 - */ -public class TroveOfTemptationTest extends CardTestPlayerBase { - - @Test - @Ignore // TODO: 2019-04-28 - improve and uncomment test after computer player can process playerMustBeAttackedIfAble restriction - public void test_SingleOpponentMustAttack() { - // Each opponent must attack you or a planeswalker you control with at least one creature each combat if able. - addCard(Zone.BATTLEFIELD, playerA, "Trove of Temptation"); - addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 - addCard(Zone.BATTLEFIELD, playerB, "Ashcoat Bear", 1); // 2/2 - - setStopAt(2, PhaseStep.END_TURN); - setStrictChooseMode(true); - execute(); - assertAllCommandsUsed(); - } -} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/RagsRichesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/RagsRichesTest.java index f7acc869de..23683ef317 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/RagsRichesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/RagsRichesTest.java @@ -7,7 +7,7 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; @@ -19,7 +19,7 @@ import java.io.FileNotFoundException; public class RagsRichesTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/BlatantThieveryTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/BlatantThieveryTest.java index 7bf3411dde..7fc682e8b6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/BlatantThieveryTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/BlatantThieveryTest.java @@ -5,7 +5,6 @@ */ package org.mage.test.multiplayer; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -13,19 +12,20 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public class BlatantThieveryTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/CreepingDreadTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/CreepingDreadTest.java index 6be02becc8..dae63cf06d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/CreepingDreadTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/CreepingDreadTest.java @@ -1,6 +1,5 @@ package org.mage.test.multiplayer; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -8,23 +7,25 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; +import java.io.FileNotFoundException; + /** * Enchantment {3}{B} - * At the beginning of your upkeep, each player discards a card. - * Each opponent who discarded a card that shares a card type with the card you discarded loses 3 life. + * At the beginning of your upkeep, each player discards a card. + * Each opponent who discarded a card that shares a card type with the card you discarded loses 3 life. * (Players reveal the discarded cards simultaneously.) - * + * * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com */ public class CreepingDreadTest extends CardTestMultiPlayerBase { - + @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 40); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); @@ -32,27 +33,27 @@ public class CreepingDreadTest extends CardTestMultiPlayerBase { playerD = createPlayer(game, playerD, "PlayerD"); return game; } - + /** * Discard creature and all opponents who discard creature lose 3 life */ @Test public void basicTest() { - + addCard(Zone.BATTLEFIELD, playerA, "Creeping Dread"); addCard(Zone.HAND, playerA, "Merfolk Looter"); addCard(Zone.HAND, playerB, "Hill Giant"); addCard(Zone.HAND, playerC, "Elite Vanguard"); addCard(Zone.HAND, playerD, "Bone Saw"); - + setChoice(playerA, "Merfolk Looter"); setChoice(playerB, "Hill Giant"); setChoice(playerC, "Elite Vanguard"); setChoice(playerD, "Bone Saw"); - + setStopAt(1, PhaseStep.DRAW); execute(); - + assertGraveyardCount(playerA, "Merfolk Looter", 1); assertGraveyardCount(playerB, "Hill Giant", 1); assertGraveyardCount(playerC, "Elite Vanguard", 1); @@ -63,27 +64,27 @@ public class CreepingDreadTest extends CardTestMultiPlayerBase { assertLife(playerC, 37); assertLife(playerD, 40); // no match } - + /** * Discard Artifact Creature and all opponents who discard either an Artifact or Creature lose 3 life */ @Test public void twoTypesTest() { - + addCard(Zone.BATTLEFIELD, playerA, "Creeping Dread"); addCard(Zone.HAND, playerA, "Bronze Sable"); addCard(Zone.HAND, playerB, "Hill Giant"); addCard(Zone.HAND, playerC, "Swamp"); addCard(Zone.HAND, playerD, "Bone Saw"); - + setChoice(playerA, "Bronze Sable"); setChoice(playerB, "Hill Giant"); setChoice(playerC, "Swamp"); setChoice(playerD, "Bone Saw"); - + setStopAt(1, PhaseStep.DRAW); execute(); - + assertGraveyardCount(playerA, "Bronze Sable", 1); assertGraveyardCount(playerB, "Hill Giant", 1); assertGraveyardCount(playerC, "Swamp", 1); @@ -94,63 +95,63 @@ public class CreepingDreadTest extends CardTestMultiPlayerBase { assertLife(playerC, 40); // neither assertLife(playerD, 37); // artifact } - + /** * Discard enchantment and no opponents discard an enchantment, so no one loses life */ @Test public void noMatchesTest() { - + addCard(Zone.BATTLEFIELD, playerA, "Creeping Dread"); addCard(Zone.HAND, playerA, "Moat"); // enchantment addCard(Zone.HAND, playerB, "Hill Giant"); addCard(Zone.HAND, playerC, "Swamp"); addCard(Zone.HAND, playerD, "Bone Saw"); - + setChoice(playerA, "Moat"); setChoice(playerB, "Hill Giant"); setChoice(playerC, "Swamp"); setChoice(playerD, "Bone Saw"); - + setStopAt(1, PhaseStep.DRAW); execute(); - + assertGraveyardCount(playerA, "Moat", 1); assertGraveyardCount(playerB, "Hill Giant", 1); assertGraveyardCount(playerC, "Swamp", 1); assertGraveyardCount(playerD, "Bone Saw", 1); assertLife(playerA, 40); // no matches - assertLife(playerB, 40); + assertLife(playerB, 40); assertLife(playerC, 40); - assertLife(playerD, 40); + assertLife(playerD, 40); } - + /** * Upkeep player has no cards to discard, so no matches */ @Test public void noDiscardNoMatches() { - + addCard(Zone.BATTLEFIELD, playerA, "Creeping Dread"); addCard(Zone.HAND, playerB, "Hill Giant"); addCard(Zone.HAND, playerC, "Swamp"); addCard(Zone.HAND, playerD, "Bone Saw"); - + setChoice(playerB, "Hill Giant"); setChoice(playerC, "Swamp"); setChoice(playerD, "Bone Saw"); - + setStopAt(1, PhaseStep.DRAW); execute(); - + assertGraveyardCount(playerB, "Hill Giant", 1); assertGraveyardCount(playerC, "Swamp", 1); assertGraveyardCount(playerD, "Bone Saw", 1); assertLife(playerA, 40); // no matches - assertLife(playerB, 40); + assertLife(playerB, 40); assertLife(playerC, 40); - assertLife(playerD, 40); + assertLife(playerD, 40); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/MultiplayerTriggerTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/MultiplayerTriggerTest.java index fa693669c2..6c650b44da 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/MultiplayerTriggerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/MultiplayerTriggerTest.java @@ -1,6 +1,5 @@ package org.mage.test.multiplayer; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -8,15 +7,17 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; +import java.io.FileNotFoundException; + public class MultiplayerTriggerTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 40); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/MyriadTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/MyriadTest.java index e4e420112a..3a4f61bd0e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/MyriadTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/MyriadTest.java @@ -7,7 +7,7 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; @@ -20,7 +20,7 @@ public class MyriadTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 40); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerDiedStackTargetHandlingTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerDiedStackTargetHandlingTest.java index 505351f83b..1e0d42917c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerDiedStackTargetHandlingTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerDiedStackTargetHandlingTest.java @@ -5,7 +5,6 @@ */ package org.mage.test.multiplayer; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -13,13 +12,14 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public class PlayerDiedStackTargetHandlingTest extends CardTestMultiPlayerBase { @@ -27,7 +27,7 @@ public class PlayerDiedStackTargetHandlingTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { // Start Life = 2 - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, new VancouverMulligan(0), 3); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 3); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java index df74f38875..4f8a550c43 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java @@ -1,7 +1,5 @@ - package org.mage.test.multiplayer; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -10,14 +8,15 @@ import mage.counters.CounterType; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import mage.game.permanent.Permanent; import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { @@ -25,7 +24,7 @@ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { // Start Life = 2 - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, new VancouverMulligan(0), 2); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 2); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); @@ -131,7 +130,7 @@ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { * player concedes the game and removes the Planeswalker. Once it becomes an * opponent of the original player's turn and that opponent plays a spell, * Xmage throws an error and rollsback the turn. - * + *

* I don't have the actual error report on my due to negligence, but what I * can recollect is that the error message was along the lines of "The * emblem cannot find the original source. This turn will be rolled back". @@ -278,7 +277,7 @@ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { /** * Pithing Needle keeps the named card's abilities disabled even after the * player controlling the Needle loses the game. - * + *

* I saw it happen during a Commander game. A player cast Pithing Needle * targeting my Proteus Staff. After I killed him, I still couldn't activate * the Staff. diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRangeAllTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRangeAllTest.java index c1c1e1d11f..caef3748b3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRangeAllTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRangeAllTest.java @@ -1,6 +1,5 @@ package org.mage.test.multiplayer; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -9,13 +8,14 @@ import mage.counters.CounterType; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public class PlayerLeftGameRangeAllTest extends CardTestMultiPlayerBase { @@ -23,7 +23,7 @@ public class PlayerLeftGameRangeAllTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { // Start Life = 2 - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 2); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 2); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); @@ -129,7 +129,7 @@ public class PlayerLeftGameRangeAllTest extends CardTestMultiPlayerBase { * player concedes the game and removes the Planeswalker. Once it becomes an * opponent of the original player's turn and that opponent plays a spell, * Xmage throws an error and rollsback the turn. - * + *

* I don't have the actual error report on my due to negligence, but what I * can recollect is that the error message was along the lines of "The * emblem cannot find the original source. This turn will be rolled back". @@ -283,7 +283,7 @@ public class PlayerLeftGameRangeAllTest extends CardTestMultiPlayerBase { } /** - * * 11/4/2015: In a multiplayer game, if Grasp of Fate's owner leaves the + * * 11/4/2015: In a multiplayer game, if Grasp of Fate's owner leaves the * game, the exiled cards will return to the battlefield. Because the * one-shot effect that returns the cards isn't an ability that goes on the * stack, it won't cease to exist along with the leaving player's spells and diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PrivilegedPositionTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PrivilegedPositionTest.java index c404a37a38..ccd36d380c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PrivilegedPositionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PrivilegedPositionTest.java @@ -9,7 +9,7 @@ import mage.counters.CounterType; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; @@ -22,7 +22,7 @@ public class PrivilegedPositionTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 40); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); @@ -32,9 +32,9 @@ public class PrivilegedPositionTest extends CardTestMultiPlayerBase { } /* - * Reported bug: see issue #3328 - * Players unable to attack Planeswalker with Privileged Position on battlefield. - */ + * Reported bug: see issue #3328 + * Players unable to attack Planeswalker with Privileged Position on battlefield. + */ @Test public void testAttackPlaneswalkerWithHexproofPrivilegedPosition() { diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/VindictiveLichTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/VindictiveLichTest.java index ae115daaa2..36c8948973 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/VindictiveLichTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/VindictiveLichTest.java @@ -1,7 +1,5 @@ - package org.mage.test.multiplayer; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; import mage.constants.RangeOfInfluence; @@ -9,19 +7,20 @@ import mage.constants.Zone; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.junit.Test; import org.mage.test.serverside.base.CardTestMultiPlayerBase; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public class VindictiveLichTest extends CardTestMultiPlayerBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 40); + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); 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 593df67fbd..7b5cc3185e 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 @@ -72,7 +72,9 @@ public class TestPlayer implements Player { private static final Logger logger = Logger.getLogger(TestPlayer.class); - public static final String TARGET_SKIP = "[skip]"; + public static final String TARGET_SKIP = "[target_skip]"; + public static final String BLOCK_SKIP = "[block_skip]"; + public static final String ATTACK_SKIP = "[attack_skip]"; private int maxCallsWithoutAction = 100; private int foundNoAction = 0; @@ -574,7 +576,8 @@ public class TestPlayer implements Player { if (permanent.getName().equals(groups[0])) { Counter counter = new Counter(groups[1], Integer.parseInt(groups[2])); permanent.addCounters(counter, null, game); - break; + actions.remove(action); + return true; } } } else if (action.getAction().startsWith("waitStackResolved")) { @@ -901,8 +904,8 @@ public class TestPlayer implements Player { .map(a -> ( a.getZone() + " -> " + a.getSourceObject(game).getIdName() + " -> " - + (a.getRule().length() > 0 - ? a.getRule().substring(0, Math.min(20, a.getRule().length()) - 1) + + (a.toString().length() > 0 + ? a.toString().substring(0, Math.min(20, a.toString().length()) - 1) : a.getClass().getSimpleName()) + "..." )) @@ -1242,6 +1245,14 @@ public class TestPlayer implements Player { mustAttackByAction = true; String command = action.getAction(); command = command.substring(command.indexOf("attack:") + 7); + + // skip attack + if (command.startsWith(ATTACK_SKIP)) { + it.remove(); + madeAttackByAction = true; + break; + } + String[] groups = command.split("\\$"); for (int i = 1; i < groups.length; i++) { String group = groups[i]; @@ -1291,6 +1302,11 @@ public class TestPlayer implements Player { if (mustAttackByAction && !madeAttackByAction) { this.chooseStrictModeFailed(game, "select attackers must use attack command but don't"); } + + // AI play if no actions available + if (!mustAttackByAction && this.AIPlayer) { + this.computerPlayer.selectAttackers(game, attackingPlayerId); + } } @Override @@ -1306,10 +1322,19 @@ public class TestPlayer implements Player { UUID opponentId = game.getOpponents(computerPlayer.getId()).iterator().next(); // Map of Blocker reference -> list of creatures blocked Map> blockedCreaturesByCreature = new HashMap<>(); + boolean mustBlockByAction = false; for (PlayerAction action : tempActions) { if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("block:")) { + mustBlockByAction = true; String command = action.getAction(); command = command.substring(command.indexOf("block:") + 6); + + // skip block + if (command.startsWith(BLOCK_SKIP)) { + actions.remove(action); + break; + } + String[] groups = command.split("\\$"); String blockerName = groups[0]; String attackerName = groups[1]; @@ -1324,6 +1349,11 @@ public class TestPlayer implements Player { } } checkMultipleBlockers(game, blockedCreaturesByCreature); + + // AI play if no actions available + if (!mustBlockByAction && this.AIPlayer) { + this.computerPlayer.selectBlockers(game, defendingPlayerId); + } } // Checks if a creature can block at least one more creature diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java index 77f8e4aaad..519e8861b5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java @@ -10,7 +10,7 @@ import mage.game.Game; import mage.game.GameException; import mage.game.GameOptions; import mage.game.TwoPlayerDuel; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import mage.player.ai.ComputerPlayer; import mage.players.Player; import mage.players.PlayerType; @@ -36,7 +36,7 @@ public class PlayGameTest extends MageTestBase { @Ignore @Test public void playOneGame() throws GameException, FileNotFoundException, IllegalArgumentException { - Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); Player computerA = createPlayer("ComputerA", PlayerType.COMPUTER_MINIMAX_HYBRID); // Player playerA = createPlayer("ComputerA", "Computer - mad"); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/TestPlayRandomGame.java b/Mage.Tests/src/test/java/org/mage/test/serverside/TestPlayRandomGame.java index 3d51dff3bb..dee4439759 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/TestPlayRandomGame.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/TestPlayRandomGame.java @@ -10,7 +10,7 @@ import mage.game.Game; import mage.game.GameException; import mage.game.GameOptions; import mage.game.TwoPlayerDuel; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import mage.player.ai.ComputerPlayer; import mage.players.Player; import mage.util.RandomUtil; @@ -42,7 +42,7 @@ public class TestPlayRandomGame extends MageTestBase { } private void playOneGame() throws GameException, FileNotFoundException, IllegalArgumentException { - Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); Player computerA = createRandomPlayer("ComputerA"); Deck deck = generateRandomDeck(); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander3PlayersFFA.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander3PlayersFFA.java index d259dd05ac..d06c9c2789 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander3PlayersFFA.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander3PlayersFFA.java @@ -1,16 +1,16 @@ package org.mage.test.serverside.base; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; import mage.game.CommanderFreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public abstract class CardTestCommander3PlayersFFA extends CardTestPlayerAPIImpl { @@ -24,7 +24,7 @@ public abstract class CardTestCommander3PlayersFFA extends CardTestPlayerAPIImpl @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new CommanderFreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, new VancouverMulligan(0), 40); + Game game = new CommanderFreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 40); playerA = createPlayer(game, playerA, "PlayerA", deckNameA); playerB = createPlayer(game, playerB, "PlayerB", deckNameB); playerC = createPlayer(game, playerC, "PlayerC", deckNameC); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander4Players.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander4Players.java index a122945927..ec0230fc65 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander4Players.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommander4Players.java @@ -5,7 +5,7 @@ import mage.constants.RangeOfInfluence; import mage.game.CommanderFreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; import java.io.FileNotFoundException; @@ -17,7 +17,7 @@ public abstract class CardTestCommander4Players extends CardTestPlayerAPIImpl { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new CommanderFreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new CommanderFreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommanderDuelBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommanderDuelBase.java index 3a9847db68..6631e73d5e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommanderDuelBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestCommanderDuelBase.java @@ -1,17 +1,16 @@ - package org.mage.test.serverside.base; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; import mage.game.CommanderDuel; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public abstract class CardTestCommanderDuelBase extends CardTestPlayerAPIImpl { @@ -24,7 +23,7 @@ public abstract class CardTestCommanderDuelBase extends CardTestPlayerAPIImpl { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new CommanderDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, new VancouverMulligan(0), 40); + Game game = new CommanderDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 40); playerA = createPlayer(game, playerA, "PlayerA", deckNameA); playerB = createPlayer(game, playerB, "PlayerB", deckNameB); 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 e2ea6d4ad6..87b2a3d8e0 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 @@ -5,7 +5,7 @@ import mage.constants.RangeOfInfluence; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; import java.io.FileNotFoundException; @@ -21,7 +21,7 @@ public abstract class CardTestMultiPlayerBase extends CardTestPlayerAPIImpl { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, new VancouverMulligan(0), 20); + Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); 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 index abf2e9059f..24ec672d38 100644 --- 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 @@ -5,7 +5,7 @@ import mage.constants.RangeOfInfluence; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; import java.io.FileNotFoundException; @@ -17,7 +17,7 @@ public abstract class CardTestMultiPlayerBaseWithRangeAll extends CardTestPlayer @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, new VancouverMulligan(0), 20); + Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20); // Player order: A -> D -> C -> B playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java index 6abbbf6dc7..484c6e9065 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBase.java @@ -1,33 +1,34 @@ package org.mage.test.serverside.base; -import java.io.FileNotFoundException; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; import mage.game.Game; import mage.game.GameException; import mage.game.TwoPlayerDuel; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; +import java.io.FileNotFoundException; + /** * Base class for testing single cards and effects. * * @author ayratn */ public abstract class CardTestPlayerBase extends CardTestPlayerAPIImpl { - + public CardTestPlayerBase() { deckNameA = "RB Aggro.dck"; deckNameB = "RB Aggro.dck"; } - + @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, new VancouverMulligan(0), 20); + Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); playerA = createPlayer(game, playerA, "PlayerA", deckNameA); playerB = createPlayer(game, playerB, "PlayerB", deckNameB); return game; - } + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java index d3da9e21c8..5f1e9d2f9d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java @@ -5,7 +5,7 @@ import mage.constants.RangeOfInfluence; import mage.game.Game; import mage.game.GameException; import mage.game.TwoPlayerDuel; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import org.mage.test.player.TestComputerPlayer7; import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; @@ -21,7 +21,7 @@ public abstract class CardTestPlayerBaseAI extends CardTestPlayerAPIImpl { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, new VancouverMulligan(0), 20); + Game game = new TwoPlayerDuel(MultiplayerAttackOption.LEFT, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); playerA = createPlayer(game, playerA, "PlayerA"); playerB = createPlayer(game, playerB, "PlayerB"); @@ -37,8 +37,4 @@ public abstract class CardTestPlayerBaseAI extends CardTestPlayerAPIImpl { } return super.createPlayer(name, rangeOfInfluence); } - - public void setAISkill(int skill) { - this.skill = skill; - } } 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 c1edd19c0b..858e3e8135 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 @@ -1527,12 +1527,20 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement player.addAction(turnNum, PhaseStep.DECLARE_ATTACKERS, new StringBuilder("attack:").append(attacker).append("$planeswalker=").append(planeswalker).toString()); } + public void attackSkip(int turnNum, TestPlayer player) { + attack(turnNum, player, TestPlayer.ATTACK_SKIP); + } + public void block(int turnNum, TestPlayer player, String blocker, String attacker) { //Assert.assertNotEquals("", blocker); //Assert.assertNotEquals("", attacker); player.addAction(turnNum, PhaseStep.DECLARE_BLOCKERS, "block:" + blocker + '$' + attacker); } + public void blockSkip(int turnNum, TestPlayer player) { + block(turnNum, player, TestPlayer.BLOCK_SKIP, ""); + } + /** * For use choices set "Yes" or "No" the the choice string. For X values set * "X=[xValue]" example: for X=3 set choice string to "X=3". diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java index 6667339b6a..0f98b73aa0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java @@ -7,7 +7,7 @@ import mage.constants.PlanarDieRoll; import mage.constants.RangeOfInfluence; import mage.game.Game; import mage.game.TwoPlayerDuel; -import mage.game.mulligan.VancouverMulligan; +import mage.game.mulligan.MulliganType; import mage.player.human.HumanPlayer; import mage.players.Player; import mage.util.RandomUtil; @@ -93,7 +93,7 @@ public class RandomTest { String dest = "f:/test/xmage/"; //RandomUtil.setSeed(123); Player player = new HumanPlayer("random", RangeOfInfluence.ALL, 1); - Game game = new TwoPlayerDuel(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 50); + Game game = new TwoPlayerDuel(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 50); int height = 512; int weight = 512; @@ -116,7 +116,7 @@ public class RandomTest { String dest = "f:/test/xmage/"; //RandomUtil.setSeed(123); Player player = new HumanPlayer("random", RangeOfInfluence.ALL, 1); - Game game = new TwoPlayerDuel(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 50); + Game game = new TwoPlayerDuel(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 50); int height = 512; int weight = 512; @@ -141,7 +141,7 @@ public class RandomTest { String dest = "f:/test/xmage/"; //RandomUtil.setSeed(123); Player player = new HumanPlayer("random", RangeOfInfluence.ALL, 1); - Game game = new TwoPlayerDuel(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, new VancouverMulligan(0), 50); + Game game = new TwoPlayerDuel(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 50); Deck deck = DeckTestUtils.buildRandomDeck("WGUBR", false, "GRN"); player.getLibrary().addAll(deck.getCards(), game); diff --git a/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java b/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java index b369830e70..68ceca8ea6 100644 --- a/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java @@ -2,7 +2,6 @@ package mage.abilities.common; import mage.abilities.SpellAbility; import mage.cards.Card; -import mage.constants.SpellAbilityType; import mage.constants.Zone; /** @@ -10,16 +9,20 @@ import mage.constants.Zone; */ public class CastCommanderAbility extends SpellAbility { - public CastCommanderAbility(Card card) { - super(card.getSpellAbility()); + private String ruleText; + + public CastCommanderAbility(Card card, SpellAbility spellTemplate) { + super(spellTemplate); this.newId(); - this.setCardName(card.getName()); - zone = Zone.COMMAND; - spellAbilityType = SpellAbilityType.BASE; + this.setCardName(spellTemplate.getCardName()); + this.zone = Zone.COMMAND; + this.spellAbilityType = spellTemplate.getSpellAbilityType(); + this.ruleText = spellTemplate.getRule(); // need to support custom rule texts like OverloadAbility } public CastCommanderAbility(final CastCommanderAbility ability) { super(ability); + this.ruleText = ability.ruleText; } @Override @@ -27,4 +30,9 @@ public class CastCommanderAbility extends SpellAbility { return new CastCommanderAbility(this); } + @Override + public String getRule() { + return ruleText; + } + } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java index 227e938df8..399cd4c72a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java @@ -13,18 +13,20 @@ import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.players.Player; +import java.util.Locale; import java.util.UUID; /** - * @author Plopman + * @author Plopman, JayDi85 */ -//20130711 +// 2019-07-12 /* - * 903.11. If a commander would be put into its owner's graveyard from anywhere, that player may put it into the command zone instead. - * 903.12. If a commander would be put into the exile zone from anywhere, its owner may put it into the command zone instead. - * 903.9. If a commander would be exiled from anywhere or put into its owner's hand, graveyard, or -library from anywhere, its owner may put it into the command zone instead. This replacement effect -may apply more than once to the same event. This is an exception to rule 614.5. + 903.9. If a commander would be exiled from anywhere or put into its owner’s hand, graveyard, or library from anywhere, + its owner may put it into the command zone instead. This replacement effect may apply more than once to the same event. + This is an exception to rule 614.5. + 903.9a If a commander is a melded permanent and its owner chooses to put it into the command zone this way, + that permanent and the card representing it that isn’t a commander are put into the appropriate zone, and the card + that represents it and is a commander is put into the command zone. */ // Oathbreaker mode: If your Oathbreaker changes zones, you may return it to the Command Zone. The Signature Spell must return to the Command Zone. @@ -32,8 +34,8 @@ may apply more than once to the same event. This is an exception to rule 614.5. public class CommanderReplacementEffect extends ReplacementEffectImpl { private final UUID commanderId; - private final boolean alsoHand; - private final boolean alsoLibrary; + private final boolean alsoHand; // return from hand to command zone + private final boolean alsoLibrary; // return from library to command zone private final boolean forceToMove; private final String commanderTypeName; @@ -87,56 +89,56 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { - switch (((ZoneChangeEvent) event).getToZone()) { - case HAND: - if (!alsoHand && ((ZoneChangeEvent) event).getToZone() == Zone.HAND) { - return false; - } + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + + if (!game.isSimulation() && commanderId.equals(zEvent.getTargetId())) { + //System.out.println("applies " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId())); + } + + if (zEvent.getToZone().equals(Zone.HAND) && !alsoHand) { + return false; + } + if (zEvent.getToZone().equals(Zone.LIBRARY) && !alsoLibrary) { + return false; + } + + // return to command zone + switch (zEvent.getToZone()) { case LIBRARY: - if (!alsoLibrary && ((ZoneChangeEvent) event).getToZone() == Zone.LIBRARY) { - return false; - } + case HAND: case GRAVEYARD: case EXILED: - if (((ZoneChangeEvent) event).getFromZone() == Zone.STACK) { - Spell spell = game.getStack().getSpell(event.getTargetId()); - if (spell != null && commanderId.equals(spell.getSourceId())) { - return true; - } - } - if (commanderId.equals(event.getTargetId())) { + if (commanderId.equals(zEvent.getTargetId())) { return true; } break; - case STACK: - Spell spell = game.getStack().getSpell(event.getTargetId()); - if (spell != null) { - if (commanderId.equals(spell.getSourceId())) { - return true; - } - } - break; - } return false; } @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - if (((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD) { - Permanent permanent = ((ZoneChangeEvent) event).getTarget(); + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + String originToZone = zEvent.getToZone().toString().toLowerCase(Locale.ENGLISH); + + if (!game.isSimulation()) { + //System.out.println("replace " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId())); + } + + if (zEvent.getFromZone() == Zone.BATTLEFIELD) { + Permanent permanent = zEvent.getTarget(); if (permanent != null) { Player player = game.getPlayer(permanent.getOwnerId()); - if (player != null && (forceToMove || player.chooseUse(Outcome.Benefit, "Move " + commanderTypeName + " to command zone?", source, game))) { - ((ZoneChangeEvent) event).setToZone(Zone.COMMAND); + if (player != null && (forceToMove || player.chooseUse(Outcome.Benefit, "Move " + commanderTypeName + " to command zone instead " + originToZone + "?", source, game))) { + zEvent.setToZone(Zone.COMMAND); if (!game.isSimulation()) { - game.informPlayers(player.getLogName() + " has moved their " + commanderTypeName + " to the command zone"); + game.informPlayers(player.getLogName() + " has moved their " + commanderTypeName + " to the command zone instead " + originToZone); } } } } else { Card card = null; - if (((ZoneChangeEvent) event).getFromZone() == Zone.STACK) { + if (zEvent.getFromZone() == Zone.STACK) { Spell spell = game.getStack().getSpell(event.getTargetId()); if (spell != null) { card = game.getCard(spell.getSourceId()); @@ -147,10 +149,10 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl { } if (card != null) { Player player = game.getPlayer(card.getOwnerId()); - if (player != null && (forceToMove || player.chooseUse(Outcome.Benefit, "Move " + commanderTypeName + " to command zone?", source, game))) { + if (player != null && (forceToMove || player.chooseUse(Outcome.Benefit, "Move " + commanderTypeName + " to command zone instead " + originToZone + "?", source, game))) { ((ZoneChangeEvent) event).setToZone(Zone.COMMAND); if (!game.isSimulation()) { - game.informPlayers(player.getLogName() + " has moved their " + commanderTypeName + " to the command zone"); + game.informPlayers(player.getLogName() + " has moved their " + commanderTypeName + " to the command zone instead " + originToZone); } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java b/Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java index eb7c672a06..7e6f87daf2 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java @@ -20,6 +20,9 @@ import java.util.UUID; command zone costs an additional {2} for each previous time the player casting it has cast it from the command zone that game. This additional cost is informally known as the “commander tax.” */ + +// cast from hand like Remand do not increase commander tax + public class CommanderCostModification extends CostModificationEffectImpl { private final UUID commanderId; diff --git a/Mage/src/main/java/mage/constants/AsThoughEffectType.java b/Mage/src/main/java/mage/constants/AsThoughEffectType.java index f2357a77e1..de81133a05 100644 --- a/Mage/src/main/java/mage/constants/AsThoughEffectType.java +++ b/Mage/src/main/java/mage/constants/AsThoughEffectType.java @@ -5,7 +5,6 @@ package mage.constants; * @author North */ public enum AsThoughEffectType { - ATTACK, ATTACK_AS_HASTE, ACTIVATE_HASTE, @@ -20,7 +19,7 @@ public enum AsThoughEffectType { BLOCK_FORESTWALK, DAMAGE_NOT_BLOCKED, BE_BLOCKED, - PLAY_FROM_NOT_OWN_HAND_ZONE, + PLAY_FROM_NOT_OWN_HAND_ZONE, // do not use dialogs in "applies" method for that type of effect (it calls multiple times) CAST_AS_INSTANT, ACTIVATE_AS_INSTANT, DAMAGE, diff --git a/Mage/src/main/java/mage/game/GameCommanderImpl.java b/Mage/src/main/java/mage/game/GameCommanderImpl.java index 03d50ee66f..68ab22c919 100644 --- a/Mage/src/main/java/mage/game/GameCommanderImpl.java +++ b/Mage/src/main/java/mage/game/GameCommanderImpl.java @@ -23,8 +23,11 @@ public abstract class GameCommanderImpl extends GameImpl { // private final Map mulliganedCards = new HashMap<>(); protected boolean checkCommanderDamage = true; - protected boolean alsoHand; // replace commander going to hand - protected boolean alsoLibrary; // replace commander going to library + + // old commander's versions (before 2017) restrict return from hand or library to command zone + protected boolean alsoHand = true; // replace commander going to hand + protected boolean alsoLibrary = true; // replace commander going to library + protected boolean startingPlayerSkipsDraw = true; public GameCommanderImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife) { diff --git a/Mage/src/main/java/mage/game/command/Commander.java b/Mage/src/main/java/mage/game/command/Commander.java index 96c25d0c4e..e8a34c3a7d 100644 --- a/Mage/src/main/java/mage/game/command/Commander.java +++ b/Mage/src/main/java/mage/game/command/Commander.java @@ -12,6 +12,7 @@ import mage.abilities.text.TextPart; import mage.cards.Card; import mage.cards.FrameStyle; import mage.constants.CardType; +import mage.constants.SpellAbilityType; import mage.constants.SubType; import mage.constants.SuperType; import mage.game.Game; @@ -19,7 +20,6 @@ import mage.game.events.ZoneChangeEvent; import mage.util.GameLog; import mage.util.SubTypeList; -import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.UUID; @@ -36,7 +36,17 @@ public class Commander implements CommandObject { // replace spell ability by commander cast spell (to cast from command zone) if (card.getSpellAbility() != null) { - abilities.add(new CastCommanderAbility(card)); + abilities.add(new CastCommanderAbility(card, card.getSpellAbility())); + } + + // replace alternative spell abilities by commander cast spell (to cast from command zone) + for (Ability ability : card.getAbilities()) { + if (ability instanceof SpellAbility) { + SpellAbility spellAbility = (SpellAbility) ability; + if (spellAbility.getSpellAbilityType() == SpellAbilityType.BASE_ALTERNATE) { + abilities.add(new CastCommanderAbility(card, spellAbility)); + } + } } // replace play land with commander play land (to play from command zone) diff --git a/Mage/src/main/java/mage/game/mulligan/LondonMulligan.java b/Mage/src/main/java/mage/game/mulligan/LondonMulligan.java index 5c9e85a4d6..4304c5da67 100644 --- a/Mage/src/main/java/mage/game/mulligan/LondonMulligan.java +++ b/Mage/src/main/java/mage/game/mulligan/LondonMulligan.java @@ -91,6 +91,7 @@ public class LondonMulligan extends Mulligan { } } openingHandSizes.put(playerId, openingHandSizes.get(playerId) - deduction); + int newHandSize = openingHandSizes.get(player.getId()); if (deduction == 0) { game.fireInformEvent(new StringBuilder(player.getLogName()) .append(" mulligans for free.") @@ -99,14 +100,13 @@ public class LondonMulligan extends Mulligan { game.fireInformEvent(new StringBuilder(player.getLogName()) .append(" mulligans") .append(" down to ") - .append((numCards - deduction)) - .append(numCards - deduction == 1 ? " card" : " cards").toString()); + .append(newHandSize) + .append(newHandSize == 1 ? " card" : " cards").toString()); } player.drawCards(numCards, game); - int handSize = openingHandSizes.get(player.getId()); - if (player.getHand().size() > handSize) { - int cardsToDiscard = player.getHand().size() - handSize; + if (player.getHand().size() > newHandSize) { + int cardsToDiscard = player.getHand().size() - newHandSize; Cards cards = new CardsImpl(); cards.addAll(player.getHand()); TargetCard target = new TargetCard(cardsToDiscard, cardsToDiscard, Zone.HAND, @@ -118,7 +118,8 @@ public class LondonMulligan extends Mulligan { } @Override - public void endMulligan(Game game, UUID playerId) {} + public void endMulligan(Game game, UUID playerId) { + } @Override public LondonMulligan copy() { diff --git a/Mage/src/main/java/mage/game/mulligan/MulliganType.java b/Mage/src/main/java/mage/game/mulligan/MulliganType.java index 568abff45e..a22394d394 100644 --- a/Mage/src/main/java/mage/game/mulligan/MulliganType.java +++ b/Mage/src/main/java/mage/game/mulligan/MulliganType.java @@ -22,11 +22,11 @@ public enum MulliganType { return new ParisMulligan(freeMulligans); case CANADIAN_HIGHLANDER: return new CanadianHighlanderMulligan(freeMulligans); - case LONDON: - return new LondonMulligan(freeMulligans); - default: case VANCOUVER: return new VancouverMulligan(freeMulligans); + default: + case LONDON: + return new LondonMulligan(freeMulligans); } } @@ -48,7 +48,6 @@ public enum MulliganType { return res; } - public MulliganType orDefault(MulliganType defaultMulligan) { if (this == GAME_DEFAULT) { return defaultMulligan; diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 0057e1dbc7..50440978e7 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1189,11 +1189,17 @@ public abstract class PlayerImpl implements Player, Serializable { //20091005 - 305.1 if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject()))) { // int bookmark = game.bookmarkState(); - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject())); + // land events must return original zone (uses for commander watcher) + Zone cardZoneBefore = game.getState().getZone(card.getId()); + GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject()); + landEventBefore.setZone(cardZoneBefore); + game.fireEvent(landEventBefore); if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { landsPlayed++; - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject())); + GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject()); + landEventAfter.setZone(cardZoneBefore); + game.fireEvent(landEventAfter); game.fireInformEvent(getLogName() + " plays " + card.getLogName()); // game.removeBookmark(bookmark); resetStoredBookmark(game); // prevent undo after playing a land diff --git a/Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java b/Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java index 43c7a19b8d..d354aadb44 100644 --- a/Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java @@ -1,6 +1,7 @@ package mage.watchers.common; import mage.constants.WatcherScope; +import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.GameEvent.EventType; @@ -12,7 +13,8 @@ import java.util.Map; import java.util.UUID; /** - * Calcs commanders play count (spell or land) + * Calcs commanders play count only from command zone (spell or land) + * Cards like Remand can put command to hand and cast it without commander tax increase * * @author JayDi85 */ @@ -49,7 +51,7 @@ public class CommanderPlaysCountWatcher extends Watcher { } } - if (isCommanderObject) { + if (isCommanderObject && event.getZone() == Zone.COMMAND) { int count = playsCount.getOrDefault(possibleCommanderId, 0); count++; playsCount.put(possibleCommanderId, count);