This commit is contained in:
Ingmar Goudt 2019-07-14 10:16:35 +02:00
commit 3fa2deaa64
60 changed files with 890 additions and 353 deletions

View file

@ -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);

View file

@ -13,9 +13,9 @@ public class MageVersion implements Serializable, Comparable<MageVersion> {
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;

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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<Permanent> attackersList = super.getAvailableAttackers(defenderId, game);
if (attackersList.isEmpty()) {
return;
}
// TODO: add attack of Planeswalker
List<Permanent> possibleBlockers = defender.getAvailableBlockers(game);
List<Permanent> 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<Permanent> attackersList = super.getAvailableAttackers(defenderId, game);
if (attackersList.isEmpty()) {
continue;
}
List<Permanent> possibleBlockers = defender.getAvailableBlockers(game);
List<Permanent> 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<Permanent> attackersList = super.getAvailableAttackers(defenderId, game);
if (attackersList.isEmpty()) {
continue;
}
List<Permanent> 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);
}
}
}
}

View file

@ -32,7 +32,7 @@ public final class CombatUtil {
public static List<Permanent> canKillOpponent(Game game, List<Permanent> attackersList, List<Permanent> blockersList,
Player defender) {
List<Permanent> blockableAttackers = new ArrayList<>(blockersList);
List<Permanent> blockableAttackers = new ArrayList<>(attackersList);
List<Permanent> 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<Permanent> attackers, List<Permanent> blockers) {
UUID attackerId = game.getCombat().getAttackingPlayerId();
@ -319,7 +319,7 @@ public final class CombatUtil {
return combatInfo;
}
private static List<Permanent> getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
List<Permanent> 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());
}

View file

@ -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);

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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 = " (<b>storm count: " + Math.max(0, stormCount - 1) + "</b>) ";
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";
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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");

View file

@ -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");

View file

@ -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 owners hand instead of into that players 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();
}
}

View file

@ -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 opponents 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);
}
}

View file

@ -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();
}
}

View file

@ -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");

View file

@ -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");

View file

@ -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);
}
}

View file

@ -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");

View file

@ -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");

View file

@ -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");

View file

@ -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.
*
* <p>
* 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.
*
* <p>
* 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.

View file

@ -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.
*
* <p>
* 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

View file

@ -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() {

View file

@ -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");

View file

@ -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<MageObjectReference, List<MageObjectReference>> 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

View file

@ -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");

View file

@ -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();

View file

@ -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);

View file

@ -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");

View file

@ -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);

View file

@ -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");

View file

@ -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");

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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".

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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 owners 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 isnt 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);
}
}
}

View file

@ -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;

View file

@ -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,

View file

@ -23,8 +23,11 @@ public abstract class GameCommanderImpl extends GameImpl {
// private final Map<UUID, Cards> 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) {

View file

@ -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)

View file

@ -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() {

View file

@ -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;

View file

@ -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

View file

@ -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);