diff --git a/Mage.Sets/src/mage/cards/m/MysticBarrier.java b/Mage.Sets/src/mage/cards/m/MysticBarrier.java index 91d724fe046..1ed6fee983c 100644 --- a/Mage.Sets/src/mage/cards/m/MysticBarrier.java +++ b/Mage.Sets/src/mage/cards/m/MysticBarrier.java @@ -1,20 +1,17 @@ package mage.cards.m; -import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.continuous.PlayerCanOnlyAttackInDirectionRestrictionEffect; import mage.abilities.meta.OrTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.players.Player; -import mage.players.PlayerList; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.TargetController; +import mage.constants.Zone; import java.util.UUID; @@ -37,7 +34,12 @@ public final class MysticBarrier extends CardImpl { new BeginningOfUpkeepTriggeredAbility(null, TargetController.YOU, false))); // Each player may attack only the nearest opponent in the last chosen direction and planeswalkers controlled by that player. - this.addAbility(new SimpleStaticAbility(new MysticBarrierReplacementEffect())); + this.addAbility(new SimpleStaticAbility( + new PlayerCanOnlyAttackInDirectionRestrictionEffect( + Duration.WhileOnBattlefield, + "the last chosen direction" + ) + )); } private MysticBarrier(final MysticBarrier card) { @@ -48,80 +50,4 @@ public final class MysticBarrier extends CardImpl { public MysticBarrier copy() { return new MysticBarrier(this); } -} - -class MysticBarrierReplacementEffect extends ReplacementEffectImpl { - - MysticBarrierReplacementEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "Each player may attack only the nearest opponent in the " + - "last chosen direction and planeswalkers controlled by that player."; - } - - private MysticBarrierReplacementEffect(MysticBarrierReplacementEffect effect) { - super(effect); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - return true; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DECLARE_ATTACKER; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - if (game.getPlayers().size() > 2) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { - return false; - } - if (!game.getState().getPlayersInRange(controller.getId(), game).contains(event.getPlayerId())) { - return false; - } - String allowedDirection = (String) game.getState().getValue(source.getSourceId() + "_modeChoice"); - if (allowedDirection == null) { - return false; - } - Player defender = game.getPlayer(event.getTargetId()); - if (defender == null) { - Permanent planeswalker = game.getPermanent(event.getTargetId()); - if (planeswalker != null) { - defender = game.getPlayer(planeswalker.getControllerId()); - } - } - if (defender == null) { - return false; - } - PlayerList playerList = game.getState().getPlayerList(event.getPlayerId()); - if (allowedDirection.equals(MysticBarrier.ALLOW_ATTACKING_LEFT) - && !playerList.getNext().equals(defender.getId())) { - // the defender is not the player to the left - Player attacker = game.getPlayer(event.getPlayerId()); - if (attacker != null) { - game.informPlayer(attacker, "You can only attack to the left!"); - } - return true; - } - if (allowedDirection.equals(MysticBarrier.ALLOW_ATTACKING_RIGHT) - && !playerList.getPrevious().equals(defender.getId())) { - // the defender is not the player to the right - Player attacker = game.getPlayer(event.getPlayerId()); - if (attacker != null) { - game.informPlayer(attacker, "You can only attack to the right!"); - } - return true; - } - } - return false; - } - - @Override - public MysticBarrierReplacementEffect copy() { - return new MysticBarrierReplacementEffect(this); - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PramikonSkyRampart.java b/Mage.Sets/src/mage/cards/p/PramikonSkyRampart.java index 2c585e782c0..dd0dee5e239 100644 --- a/Mage.Sets/src/mage/cards/p/PramikonSkyRampart.java +++ b/Mage.Sets/src/mage/cards/p/PramikonSkyRampart.java @@ -1,21 +1,17 @@ package mage.cards.p; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ReplacementEffectImpl; -import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.continuous.PlayerCanOnlyAttackInDirectionRestrictionEffect; import mage.abilities.keyword.DefenderAbility; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.players.Player; -import mage.players.PlayerList; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; import java.util.UUID; @@ -24,9 +20,6 @@ import java.util.UUID; */ public final class PramikonSkyRampart extends CardImpl { - static final String ALLOW_ATTACKING_LEFT = "Allow attacking left"; - static final String ALLOW_ATTACKING_RIGHT = "Allow attacking right"; - public PramikonSkyRampart(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{R}{W}"); @@ -42,13 +35,15 @@ public final class PramikonSkyRampart extends CardImpl { this.addAbility(DefenderAbility.getInstance()); // As Pramikon, Sky Rampart enters the battlefield, choose left or right. - this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect( - "Choose a direction to allow attacking in.", - ALLOW_ATTACKING_LEFT, ALLOW_ATTACKING_RIGHT - ))); + this.addAbility(new AsEntersBattlefieldAbility(PlayerCanOnlyAttackInDirectionRestrictionEffect.choiceEffect())); // Each player may attack only the nearest opponent in the chosen direction and planeswalkers controlled by that opponent. - this.addAbility(new SimpleStaticAbility(new PramikonSkyRampartReplacementEffect())); + this.addAbility(new SimpleStaticAbility( + new PlayerCanOnlyAttackInDirectionRestrictionEffect( + Duration.WhileOnBattlefield, + "the chosen direction" + ) + )); } private PramikonSkyRampart(final PramikonSkyRampart card) { @@ -59,79 +54,4 @@ public final class PramikonSkyRampart extends CardImpl { public PramikonSkyRampart copy() { return new PramikonSkyRampart(this); } -} - -class PramikonSkyRampartReplacementEffect extends ReplacementEffectImpl { - - PramikonSkyRampartReplacementEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "Each player may attack only the nearest opponent " + - "in the chosen direction and planeswalkers controlled by that opponent."; - } - - private PramikonSkyRampartReplacementEffect(PramikonSkyRampartReplacementEffect effect) { - super(effect); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - return true; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DECLARE_ATTACKER; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - if (game.getPlayers().size() > 2) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { - return false; - } - if (!game.getState().getPlayersInRange(controller.getId(), game).contains(event.getPlayerId())) { - return false; - } - String allowedDirection = (String) game.getState().getValue(source.getSourceId() + "_modeChoice"); - if (allowedDirection == null) { - return false; - } - Player defender = game.getPlayer(event.getTargetId()); - if (defender == null) { - Permanent planeswalker = game.getPermanent(event.getTargetId()); - if (planeswalker != null) { - defender = game.getPlayer(planeswalker.getControllerId()); - } - } - if (defender == null) { - return false; - } - PlayerList playerList = game.getState().getPlayerList(event.getPlayerId()); - if (allowedDirection.equals(PramikonSkyRampart.ALLOW_ATTACKING_LEFT) - && !playerList.getNext().equals(defender.getId())) { - // the defender is not the player to the left - Player attacker = game.getPlayer(event.getPlayerId()); - if (attacker != null) { - game.informPlayer(attacker, "You can only attack to the left!"); - } - return true; - } - if (allowedDirection.equals(PramikonSkyRampart.ALLOW_ATTACKING_RIGHT) - && !playerList.getPrevious().equals(defender.getId())) { - // the defender is not the player to the right - Player attacker = game.getPlayer(event.getPlayerId()); - if (attacker != null) { - game.informPlayer(attacker, "You can only attack to the right!"); - } - return true; - } - } - return false; - } - - @Override - public PramikonSkyRampartReplacementEffect copy() { - return new PramikonSkyRampartReplacementEffect(this); - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/t/TeyoGeometricTactician.java b/Mage.Sets/src/mage/cards/t/TeyoGeometricTactician.java new file mode 100644 index 00000000000..1f1908f78e7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TeyoGeometricTactician.java @@ -0,0 +1,70 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.effects.common.continuous.PlayerCanOnlyAttackInDirectionRestrictionEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.game.permanent.token.WallFlyingToken; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class TeyoGeometricTactician extends CardImpl { + + public TeyoGeometricTactician(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.TEYO); + this.setStartingLoyalty(3); + + // When Teyo, Geometric Tactician enters the battlefield, create a 0/4 white Wall creature token with defender and flying. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new CreateTokenEffect(new WallFlyingToken()) + )); + + // +1: You and target opponent each draw a card. + Ability ability = new LoyaltyAbility( + new DrawCardSourceControllerEffect(1).setText("you"), + 1 + ); + ability.addEffect( + new DrawCardTargetEffect(1) + .setText("and target opponent each draw a card") + ); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability); + + // -2: Choose left or right. Until your next turn, each player may attack only the nearest opponent in the last chosen direction and planeswalkers controlled by that opponent. + ability = new LoyaltyAbility( + PlayerCanOnlyAttackInDirectionRestrictionEffect.choiceEffect(), + -2 + ); + ability.addEffect(new PlayerCanOnlyAttackInDirectionRestrictionEffect( + Duration.UntilYourNextTurn, + "the last chosen direction" + )); + this.addAbility(ability); + } + + private TeyoGeometricTactician(final TeyoGeometricTactician card) { + super(card); + } + + @Override + public TeyoGeometricTactician copy() { + return new TeyoGeometricTactician(this); + } +} diff --git a/Mage.Sets/src/mage/sets/CommanderMasters.java b/Mage.Sets/src/mage/sets/CommanderMasters.java index cce04546a50..5c64998e1dc 100644 --- a/Mage.Sets/src/mage/sets/CommanderMasters.java +++ b/Mage.Sets/src/mage/sets/CommanderMasters.java @@ -655,6 +655,7 @@ public final class CommanderMasters extends ExpansionSet { cards.add(new SetCardInfo("Terramorphic Expanse", 428, Rarity.COMMON, mage.cards.t.TerramorphicExpanse.class)); cards.add(new SetCardInfo("Teshar, Ancestor's Apostle", 65, Rarity.UNCOMMON, mage.cards.t.TesharAncestorsApostle.class)); cards.add(new SetCardInfo("Tetsuko Umezawa, Fugitive", 126, Rarity.UNCOMMON, mage.cards.t.TetsukoUmezawaFugitive.class)); + cards.add(new SetCardInfo("Teyo, Geometric Tactician", 725, Rarity.RARE, mage.cards.t.TeyoGeometricTactician.class)); cards.add(new SetCardInfo("Teysa Karlov", 359, Rarity.RARE, mage.cards.t.TeysaKarlov.class)); cards.add(new SetCardInfo("The Binding of the Titans", 886, Rarity.UNCOMMON, mage.cards.t.TheBindingOfTheTitans.class)); cards.add(new SetCardInfo("The Chain Veil", 943, Rarity.MYTHIC, mage.cards.t.TheChainVeil.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/PramikonSkyRampartTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/PramikonSkyRampartTest.java new file mode 100644 index 00000000000..336873b029a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/PramikonSkyRampartTest.java @@ -0,0 +1,326 @@ +package org.mage.test.cards.single.c19; + +import mage.abilities.effects.common.continuous.PlayerCanOnlyAttackInDirectionRestrictionEffect; +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.mulligan.MulliganType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +/** + * @author Susucr + */ +public class PramikonSkyRampartTest extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException { + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, MulliganType.GAME_DEFAULT.getMulligan(0), 20); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, "PlayerA"); + playerB = createPlayer(game, "PlayerB"); + playerC = createPlayer(game, "PlayerC"); + playerD = createPlayer(game, "PlayerD"); + return game; + } + + /** + * Pramikon, Sky Rampart + * {U}{R}{W} + * Legendary Creature — Wall + *
+ * Flying, defender + *
+ * As Pramikon, Sky Rampart enters the battlefield, choose left or right. + *
+ * Each player may attack only the nearest opponent in the chosen direction and planeswalkers controlled by that opponent. + *
+ * 1/5 + */ + private static String pramikon = "Pramikon, Sky Rampart"; + + private static String ancients = "Indomitable Ancients"; + private static String bogstomper = "Bogstomper"; + private static String crocodile = "Catacomb Crocodile"; + private static String devil = "Hulking Devil"; + + @Test + public void chooseLeft() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, pramikon); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + addCard(Zone.BATTLEFIELD, playerA, ancients); + addCard(Zone.BATTLEFIELD, playerB, bogstomper); + addCard(Zone.BATTLEFIELD, playerC, crocodile); + addCard(Zone.BATTLEFIELD, playerD, devil); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); + setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_LEFT); + + // A has pramikon, and chose left. + // + // D <----> C + // ^ | + // | | + // | v + // A <----- B + + checkMayAttackDefender("Attack left possible", + 1, playerA, ancients, playerD, true); + checkMayAttackDefender("Attack out of range impossible", + 1, playerA, ancients, playerC, false); + checkMayAttackDefender("Attack right impossible", + 1, playerA, ancients, playerB, false); + checkMayAttackDefender("Attack self impossible", + 1, playerA, ancients, playerA, false); + + checkMayAttackDefender("Attack left possible", + 2, playerD, devil, playerC, true); + checkMayAttackDefender("Attack out of range impossible", + 2, playerD, devil, playerB, false); + checkMayAttackDefender("Attack right impossible", + 2, playerD, devil, playerA, false); + checkMayAttackDefender("Attack self impossible", + 2, playerD, devil, playerD, false); + + checkMayAttackDefender("Attack left possible -- not in range of Pramikon", + 3, playerC, crocodile, playerB, true); + checkMayAttackDefender("Attack out of range impossible", + 3, playerC, crocodile, playerA, false); + checkMayAttackDefender("Attack right possible -- not in range of Pramikon", + 3, playerC, crocodile, playerD, true); + checkMayAttackDefender("Attack self impossible", + 3, playerC, crocodile, playerC, false); + + checkMayAttackDefender("Attack left possible", + 4, playerB, bogstomper, playerA, true); + checkMayAttackDefender("Attack out of range impossible", + 4, playerB, bogstomper, playerD, false); + checkMayAttackDefender("Attack right impossible", + 4, playerB, bogstomper, playerC, false); + checkMayAttackDefender("Attack self impossible", + 4, playerB, bogstomper, playerB, false); + + setStopAt(4, PhaseStep.END_TURN); + execute(); + } + + @Test + public void chooseRight() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, pramikon); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + addCard(Zone.BATTLEFIELD, playerA, ancients); + addCard(Zone.BATTLEFIELD, playerB, bogstomper); + addCard(Zone.BATTLEFIELD, playerC, crocodile); + addCard(Zone.BATTLEFIELD, playerD, devil); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); + setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_RIGHT); + + // A has pramikon, and chose right. + // + // D <----- C + // | ^ + // | | + // v v + // A -----> B + + checkMayAttackDefender("Attack left impossible", + 1, playerA, ancients, playerD, false); + checkMayAttackDefender("Attack out of range impossible", + 1, playerA, ancients, playerC, false); + checkMayAttackDefender("Attack right possible", + 1, playerA, ancients, playerB, true); + checkMayAttackDefender("Attack self impossible", + 1, playerA, ancients, playerA, false); + + checkMayAttackDefender("Attack left impossible", + 2, playerD, devil, playerC, false); + checkMayAttackDefender("Attack out of range impossible", + 2, playerD, devil, playerB, false); + checkMayAttackDefender("Attack right possible", + 2, playerD, devil, playerA, true); + checkMayAttackDefender("Attack self impossible", + 2, playerD, devil, playerD, false); + + checkMayAttackDefender("Attack left possible -- not in range of Pramikon", + 3, playerC, crocodile, playerB, true); + checkMayAttackDefender("Attack out of range impossible", + 3, playerC, crocodile, playerA, false); + checkMayAttackDefender("Attack right possible -- not in range of Pramikon", + 3, playerC, crocodile, playerD, true); + checkMayAttackDefender("Attack self impossible", + 3, playerC, crocodile, playerC, false); + + checkMayAttackDefender("Attack left impossible", + 4, playerB, bogstomper, playerA, false); + checkMayAttackDefender("Attack out of range impossible", + 4, playerB, bogstomper, playerD, false); + checkMayAttackDefender("Attack right possible", + 4, playerB, bogstomper, playerC, true); + checkMayAttackDefender("Attack self impossible", + 4, playerB, bogstomper, playerB, false); + + setStopAt(4, PhaseStep.END_TURN); + execute(); + } + + @Test + public void doublePramikon() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, pramikon); + addCard(Zone.HAND, playerD, pramikon); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerD, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerD, "Island", 1); + addCard(Zone.BATTLEFIELD, playerD, "Plains", 1); + + addCard(Zone.BATTLEFIELD, playerA, ancients); + addCard(Zone.BATTLEFIELD, playerB, bogstomper); + addCard(Zone.BATTLEFIELD, playerC, crocodile); + addCard(Zone.BATTLEFIELD, playerD, devil); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); + setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_RIGHT); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, pramikon); + setChoice(playerD, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_LEFT); + + // A has pramikon, and chose right. + // D has pramikon, and chose left. + // + // D x----x C + // x ^ + // | | + // x v + // A x----x B + + checkMayAttackDefender("Attack left impossible", + 2, playerD, devil, playerC, false); + checkMayAttackDefender("Attack out of range impossible", + 2, playerD, devil, playerB, false); + checkMayAttackDefender("Attack right impossible", + 2, playerD, devil, playerA, false); + checkMayAttackDefender("Attack self impossible", + 2, playerD, devil, playerD, false); + + checkMayAttackDefender("Attack left possible -- not in range of A's Pramikon", + 3, playerC, crocodile, playerB, true); + checkMayAttackDefender("Attack out of range impossible", + 3, playerC, crocodile, playerA, false); + checkMayAttackDefender("Attack right impossible", + 3, playerC, crocodile, playerD, false); + checkMayAttackDefender("Attack self impossible", + 3, playerC, crocodile, playerC, false); + + checkMayAttackDefender("Attack left impossible", + 4, playerB, bogstomper, playerA, false); + checkMayAttackDefender("Attack out of range impossible", + 4, playerB, bogstomper, playerD, false); + checkMayAttackDefender("Attack right possible -- not in range of D's Pramikon", + 4, playerB, bogstomper, playerC, true); + checkMayAttackDefender("Attack self impossible", + 4, playerB, bogstomper, playerB, false); + + checkMayAttackDefender("Attack left impossible", + 5, playerA, ancients, playerD, false); + checkMayAttackDefender("Attack out of range impossible", + 5, playerA, ancients, playerC, false); + checkMayAttackDefender("Attack right impossible", + 5, playerA, ancients, playerB, false); + checkMayAttackDefender("Attack self impossible", + 5, playerA, ancients, playerA, false); + + setStopAt(5, PhaseStep.END_TURN); + execute(); + } + + @Test + public void doublePramikonOther() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, pramikon); + addCard(Zone.HAND, playerD, pramikon); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerD, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerD, "Island", 1); + addCard(Zone.BATTLEFIELD, playerD, "Plains", 1); + + addCard(Zone.BATTLEFIELD, playerA, ancients); + addCard(Zone.BATTLEFIELD, playerB, bogstomper); + addCard(Zone.BATTLEFIELD, playerC, crocodile); + addCard(Zone.BATTLEFIELD, playerD, devil); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); + setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_LEFT); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, pramikon); + setChoice(playerD, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_RIGHT); + + // A has pramikon, and chose left. + // D has pramikon, and chose right. + // + // D <----x C + // x x + // | | + // x x + // A <----x B + + checkMayAttackDefender("Attack left impossible", + 2, playerD, devil, playerC, false); + checkMayAttackDefender("Attack out of range impossible", + 2, playerD, devil, playerB, false); + checkMayAttackDefender("Attack right impossible", + 2, playerD, devil, playerA, false); + checkMayAttackDefender("Attack self impossible", + 2, playerD, devil, playerD, false); + + checkMayAttackDefender("Attack left impossible", + 3, playerC, crocodile, playerB, false); + checkMayAttackDefender("Attack out of range impossible", + 3, playerC, crocodile, playerA, false); + checkMayAttackDefender("Attack right possible -- not in range of A's Pramikon", + 3, playerC, crocodile, playerD, true); + checkMayAttackDefender("Attack self impossible", + 3, playerC, crocodile, playerC, false); + + checkMayAttackDefender("Attack left possible -- not in range of D's Pramikon", + 4, playerB, bogstomper, playerA, true); + checkMayAttackDefender("Attack out of range impossible", + 4, playerB, bogstomper, playerD, false); + checkMayAttackDefender("Attack right impossible", + 4, playerB, bogstomper, playerC, false); + checkMayAttackDefender("Attack self impossible", + 4, playerB, bogstomper, playerB, false); + + checkMayAttackDefender("Attack left impossible", + 5, playerA, ancients, playerD, false); + checkMayAttackDefender("Attack out of range impossible", + 5, playerA, ancients, playerC, false); + checkMayAttackDefender("Attack right impossible", + 5, playerA, ancients, playerB, false); + checkMayAttackDefender("Attack self impossible", + 5, playerA, ancients, playerA, false); + + setStopAt(5, PhaseStep.END_TURN); + execute(); + } +} \ No newline at end of file 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 09185b14f26..fd708a96011 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 @@ -816,6 +816,13 @@ public class TestPlayer implements Player { wasProccessed = true; } + // check may attack ability: + if (params[0].equals(CHECK_COMMAND_MAY_ATTACK_DEFENDER) && params.length == 4) { + assertMayAttackDefender(action, game, computerPlayer, params[1], game.getPlayer(UUID.fromString(params[2])), Boolean.parseBoolean(params[3])); + actions.remove(action); + wasProccessed = true; + } + // check battlefield count: target player, card name, count if (params[0].equals(CHECK_COMMAND_PERMANENT_COUNT) && params.length == 4) { assertPermanentCount(action, game, game.getPlayer(UUID.fromString(params[1])), params[2], Integer.parseInt(params[3])); @@ -1135,7 +1142,7 @@ public class TestPlayer implements Player { // all fine return perm; } - Assert.fail(action.getActionName() + " - can''t find permanent to check: " + cardName); + Assert.fail(action.getActionName() + " - can't find permanent to check: " + cardName); return null; } @@ -1373,6 +1380,29 @@ public class TestPlayer implements Player { } } + private void assertMayAttackDefender(PlayerAction action, Game game, Player controller, String permanentName, Player defender, boolean expectedMayAttack) { + Permanent attackingPermanent = findPermanentWithAssert(action, game, controller, permanentName); + + // Is the defender in range of the controller? + boolean mayAttack = game.getState().getPlayersInRange(controller.getId(), game).contains(defender.getId()); + // Can the attacking permanent attack the defender? + mayAttack &= attackingPermanent.canAttack(defender.getId(), game); + // Not allowed to attack self. + mayAttack &= !controller.getId().equals(defender.getId()); + + if (expectedMayAttack && !mayAttack) { + printStart(game, action.getActionName()); + printEnd(); + Assert.fail(permanentName + " was expected to be able to attack " + defender.getName() + " but is not able to."); + } + + if (!expectedMayAttack && mayAttack) { + printStart(game, action.getActionName()); + printEnd(); + Assert.fail(permanentName + " was not expected to be able to attack " + defender.getName() + " but is able to."); + } + } + private void assertPermanentCount(PlayerAction action, Game game, Player player, String permanentName, int count) { int foundCount = 0; for (Permanent perm : game.getBattlefield().getAllPermanents()) { 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 7600ac60137..0e6a10c74f4 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 @@ -31,6 +31,7 @@ import mage.server.util.SystemUtil; import mage.util.CardUtil; import mage.view.GameView; import org.junit.Assert; +import static org.junit.Assert.assertTrue; import org.junit.Before; import org.mage.test.player.PlayerAction; import org.mage.test.player.TestPlayer; @@ -44,8 +45,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import static org.junit.Assert.assertTrue; - /** * API for test initialization and asserting the test results. * @@ -85,6 +84,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String CHECK_COMMAND_LIFE = "LIFE"; public static final String CHECK_COMMAND_ABILITY = "ABILITY"; public static final String CHECK_COMMAND_PLAYABLE_ABILITY = "PLAYABLE_ABILITY"; + public static final String CHECK_COMMAND_MAY_ATTACK_DEFENDER = "MAY_ATTACK_DEFENDER"; public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT"; public static final String CHECK_COMMAND_PERMANENT_TAPPED = "PERMANENT_TAPPED"; public static final String CHECK_COMMAND_PERMANENT_COUNTERS = "PERMANENT_COUNTERS"; @@ -392,6 +392,20 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement check(checkName, turnNum, step, player, CHECK_COMMAND_PLAYABLE_ABILITY, abilityStartText, mustHave.toString()); } + /** + * Checks whether or not a creature can attack on a given turn a defender (player only for now, could be extended to permanents) + * + * @param checkName String to show up if the check fails, for display purposes only. + * @param turnNum The turn number to check on. + * @param player The player to be checked for the ability. + * @param attackerName The attacker permanent to check. + * @param defendingPlayer The defending player. + * @param mayAttack Whether the attack is possible or not. + */ + public void checkMayAttackDefender(String checkName, int turnNum, TestPlayer player, String attackerName, TestPlayer defendingPlayer, Boolean mayAttack) { + check(checkName, turnNum, PhaseStep.BEGIN_COMBAT, player, CHECK_COMMAND_MAY_ATTACK_DEFENDER, attackerName, defendingPlayer.getId().toString(), mayAttack.toString()); + } + public void checkPermanentCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer count) { //Assert.assertNotEquals("", permanentName); checkPermanentCount(checkName, turnNum, step, player, player, permanentName, count); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java new file mode 100644 index 00000000000..b74ab65717c --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java @@ -0,0 +1,100 @@ +package mage.abilities.effects.common.continuous; + +import mage.abilities.Ability; +import mage.abilities.effects.Effect; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.players.PlayerList; + +import java.util.UUID; + +/** + * @author TheElk801, Susucr + */ +public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends RestrictionEffect { + + public static final String ALLOW_ATTACKING_LEFT = "Allow attacking left"; + public static final String ALLOW_ATTACKING_RIGHT = "Allow attacking right"; + + public PlayerCanOnlyAttackInDirectionRestrictionEffect(Duration duration, String directionText) { + super(duration, Outcome.Neutral); + staticText = duration + (duration.toString().isEmpty() ? "" : ", ") + + "each player may attack only the nearest opponent " + + "in " + directionText + " and planeswalkers controlled by that opponent"; + } + + private PlayerCanOnlyAttackInDirectionRestrictionEffect(PlayerCanOnlyAttackInDirectionRestrictionEffect effect) { + super(effect); + } + + @Override + public PlayerCanOnlyAttackInDirectionRestrictionEffect copy() { + return new PlayerCanOnlyAttackInDirectionRestrictionEffect(this); + } + + public static Effect choiceEffect() { + return new ChooseModeEffect( + "Choose a direction to allow attacking in.", + ALLOW_ATTACKING_LEFT, ALLOW_ATTACKING_RIGHT + ).setText("choose left or right"); + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return true; + } + + @Override + public boolean canAttack(Permanent attacker, UUID defenderId, Ability source, Game game, boolean canUseChooseDialogs) { + if (defenderId == null) { + return true; + } + + String allowedDirection = (String) game.getState().getValue(source.getSourceId() + "_modeChoice"); + if (allowedDirection == null) { + return true; // If no choice was made, the ability has no effect. + } + + Player playerAttacking = game.getPlayer(attacker.getControllerId()); + if (playerAttacking == null) { + return true; + } + + // The attacking player should be in range of the source's controller + if (!game.getState().getPlayersInRange(source.getControllerId(), game).contains(playerAttacking.getId())) { + return true; + } + + Player playerDefending = game.getPlayer(defenderId); + if (playerDefending == null) { + Permanent planeswalker = game.getPermanent(defenderId); + if (planeswalker != null && planeswalker.getCardType(game).contains(CardType.PLANESWALKER)) { + playerDefending = game.getPlayer(planeswalker.getControllerId()); + } + } + if (playerDefending == null) { + return false; // not a planeswalker, either a battle or not on battelfield/game anymore. + } + + PlayerList playerList = game.getState().getPlayerList(playerAttacking.getId()); + if (allowedDirection.equals(ALLOW_ATTACKING_LEFT) + && !playerList.getNext().equals(playerDefending.getId())) { + // the defender is not the player to the left + return false; + } + if (allowedDirection.equals(ALLOW_ATTACKING_RIGHT) + && !playerList.getPrevious().equals(playerDefending.getId())) { + // the defender is not the player to the right + return false; + } + + return true; + } + +} diff --git a/Mage/src/main/java/mage/game/permanent/token/WallFlyingToken.java b/Mage/src/main/java/mage/game/permanent/token/WallFlyingToken.java new file mode 100644 index 00000000000..2fdb0a4d96c --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/WallFlyingToken.java @@ -0,0 +1,29 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.keyword.DefenderAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.constants.CardType; +import mage.constants.SubType; + +public final class WallFlyingToken extends TokenImpl { + + public WallFlyingToken() { + super("Wall Token", "0/4 white Wall creature token with defender and flying"); + cardType.add(CardType.CREATURE); + subtype.add(SubType.WALL); + power = new MageInt(0); + toughness = new MageInt(4); + + addAbility(DefenderAbility.getInstance()); + addAbility(FlyingAbility.getInstance()); + } + + private WallFlyingToken(final WallFlyingToken token) { + super(token); + } + + public WallFlyingToken copy() { + return new WallFlyingToken(this); + } +}