package mage.players; import com.google.common.collect.ImmutableMap; import java.io.Serializable; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; import mage.ConditionalMana; import mage.MageObject; import mage.MageObjectReference; import mage.Mana; import mage.abilities.*; import mage.abilities.ActivatedAbility.ActivationStatus; import mage.abilities.common.PassAbility; import mage.abilities.common.PlayLandAsCommanderAbility; import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; import mage.abilities.costs.*; import mage.abilities.costs.mana.AlternateManaPaymentAbility; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.RestrictionEffect; import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; import mage.abilities.keyword.*; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.abilities.mana.ManaOptions; import mage.actions.MageDrawAction; import mage.cards.*; import mage.cards.decks.Deck; import mage.choices.ChoiceImpl; import mage.constants.*; import mage.counters.Counter; import mage.counters.CounterType; import mage.counters.Counters; import mage.designations.Designation; import mage.designations.DesignationType; import mage.filter.FilterCard; import mage.filter.FilterMana; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreatureForCombat; import mage.filter.common.FilterCreatureForCombatBlock; import mage.filter.predicate.Predicates; import mage.filter.predicate.permanent.PermanentIdPredicate; import mage.game.*; import mage.game.combat.CombatGroup; import mage.game.command.CommandObject; import mage.game.events.*; import mage.game.events.GameEvent.EventType; import mage.game.match.MatchPlayer; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; import mage.game.permanent.PermanentToken; import mage.game.permanent.token.SquirrelToken; import mage.game.stack.Spell; import mage.game.stack.StackAbility; import mage.game.stack.StackObject; import mage.game.turn.Step; import mage.players.net.UserData; import mage.target.Target; import mage.target.TargetAmount; import mage.target.TargetCard; import mage.target.TargetPermanent; import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetDiscard; import mage.util.CardUtil; import mage.util.GameLog; import mage.util.RandomUtil; import org.apache.log4j.Logger; public abstract class PlayerImpl implements Player, Serializable { private static final Logger logger = Logger.getLogger(PlayerImpl.class); /** * Used to cancel waiting requests send to the player */ protected boolean abort; protected final UUID playerId; protected String name; protected boolean human; protected int life; protected boolean wins; protected boolean draws; protected boolean loses; protected Library library; protected Cards sideboard; protected Cards hand; protected Graveyard graveyard; protected Set commandersIds = new HashSet<>(0); protected Abilities abilities; protected Counters counters; protected int landsPlayed; protected int landsPerTurn = 1; protected int loyaltyUsePerTurn = 1; protected int maxHandSize = 7; protected int maxAttackedBy = Integer.MAX_VALUE; protected ManaPool manaPool; // priority control protected boolean passed; // player passed priority protected boolean passedTurn; // F4 protected boolean passedTurnSkipStack; // F6 // TODO: research protected boolean passedUntilEndOfTurn; // F5 protected boolean passedUntilNextMain; // F7 protected boolean passedUntilStackResolved; // F10 protected Date dateLastAddedToStack; protected boolean passedUntilEndStepBeforeMyTurn; // F11 protected boolean skippedAtLeastOnce; // used to track if passed started in specific phase /** * This indicates that player passed all turns until their own turn starts * (F9). Note! This differs from passedTurn as it doesn't care about spells * and abilities in the stack and will pass them as well. */ protected boolean passedAllTurns; // F9 protected AbilityType justActivatedType; // used to check if priority can be passed automatically protected int turns; protected int storedBookmark = -1; protected int priorityTimeLeft = Integer.MAX_VALUE; // conceded or connection lost game protected boolean left; // set if the player quits the complete match protected boolean quit; // set if the player lost match because of priority timeout protected boolean timerTimeout; // set if the player lost match because of idle timeout protected boolean idleTimeout; protected RangeOfInfluence range; protected Set inRange = new HashSet<>(); // players list in current range of influence (updates each turn) protected boolean isTestMode = false; protected boolean canGainLife = true; protected boolean canLoseLife = true; protected boolean canPayLifeCost = true; protected boolean loseByZeroOrLessLife = true; protected boolean canPlayCardsFromGraveyard = true; protected FilterPermanent sacrificeCostFilter; protected final List alternativeSourceCosts = new ArrayList<>(); protected boolean isGameUnderControl = true; protected UUID turnController; protected List turnControllers = new ArrayList<>(); protected Set playersUnderYourControl = new HashSet<>(); protected Set usersAllowedToSeeHandCards = new HashSet<>(); protected List attachments = new ArrayList<>(); protected boolean topCardRevealed = false; // 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn // or until a specific point in that turn will last until that turn would have begun. // They neither expire immediately nor last indefinitely. protected boolean reachedNextTurnAfterLeaving = false; // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) // support multiple cards with alternative mana cost protected Set castSourceIdWithAlternateMana = new HashSet<>(); protected Map> castSourceIdManaCosts = new HashMap<>(); protected Map> castSourceIdCosts = new HashMap<>(); // indicates that the player is in mana payment phase protected boolean payManaMode = false; protected UserData userData; protected MatchPlayer matchPlayer; protected List designations = new ArrayList<>(); protected FilterMana phyrexianColors; // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) protected final List> availableTriggeredManaList = new ArrayList<>(); /** * During some steps we can't play anything */ protected final Map silentPhaseSteps = ImmutableMap.builder(). put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build(); public PlayerImpl(String name, RangeOfInfluence range) { this(UUID.randomUUID()); this.name = name; this.range = range; hand = new CardsImpl(); graveyard = new Graveyard(); abilities = new AbilitiesImpl<>(); counters = new Counters(); manaPool = new ManaPool(playerId); library = new Library(playerId); sideboard = new CardsImpl(); phyrexianColors = new FilterMana(); } protected PlayerImpl(UUID id) { this.playerId = id; } public PlayerImpl(final PlayerImpl player) { this.abort = player.abort; this.playerId = player.playerId; this.name = player.name; this.human = player.human; this.life = player.life; this.wins = player.wins; this.draws = player.draws; this.loses = player.loses; this.library = player.library.copy(); this.sideboard = player.sideboard.copy(); this.hand = player.hand.copy(); this.graveyard = player.graveyard.copy(); this.commandersIds = player.commandersIds; this.abilities = player.abilities.copy(); this.counters = player.counters.copy(); this.landsPlayed = player.landsPlayed; this.landsPerTurn = player.landsPerTurn; this.loyaltyUsePerTurn = player.loyaltyUsePerTurn; this.maxHandSize = player.maxHandSize; this.maxAttackedBy = player.maxAttackedBy; this.manaPool = player.manaPool.copy(); this.turns = player.turns; this.left = player.left; this.quit = player.quit; this.timerTimeout = player.timerTimeout; this.idleTimeout = player.idleTimeout; this.range = player.range; this.canGainLife = player.canGainLife; this.canLoseLife = player.canLoseLife; this.loseByZeroOrLessLife = player.loseByZeroOrLessLife; this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard; this.attachments.addAll(player.attachments); this.inRange.addAll(player.inRange); this.userData = player.userData; this.canPayLifeCost = player.canPayLifeCost; this.sacrificeCostFilter = player.sacrificeCostFilter; this.alternativeSourceCosts.addAll(player.alternativeSourceCosts); this.storedBookmark = player.storedBookmark; this.topCardRevealed = player.topCardRevealed; this.playersUnderYourControl.addAll(player.playersUnderYourControl); this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); this.isTestMode = player.isTestMode; this.isGameUnderControl = player.isGameUnderControl; this.turnController = player.turnController; this.turnControllers.addAll(player.turnControllers); this.passed = player.passed; this.passedTurn = player.passedTurn; this.passedTurnSkipStack = player.passedTurnSkipStack; this.passedUntilEndOfTurn = player.passedUntilEndOfTurn; this.passedUntilNextMain = player.passedUntilNextMain; this.passedUntilStackResolved = player.passedUntilStackResolved; this.dateLastAddedToStack = player.dateLastAddedToStack; this.passedUntilEndStepBeforeMyTurn = player.passedUntilEndStepBeforeMyTurn; this.skippedAtLeastOnce = player.skippedAtLeastOnce; this.passedAllTurns = player.passedAllTurns; this.justActivatedType = player.justActivatedType; this.priorityTimeLeft = player.getPriorityTimeLeft(); this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; this.castSourceIdWithAlternateMana.addAll(player.castSourceIdWithAlternateMana); this.castSourceIdManaCosts.putAll(player.castSourceIdManaCosts); this.castSourceIdCosts.putAll(player.castSourceIdCosts); this.payManaMode = player.payManaMode; this.phyrexianColors = player.phyrexianColors.copy(); this.designations.addAll(player.designations); } @Override public void restore(Player player) { this.name = player.getName(); this.human = player.isHuman(); this.life = player.getLife(); // Don't restore more global states. If restored they are probably cause for unintended draws (https://github.com/magefree/mage/issues/1205). // this.wins = player.hasWon(); // this.loses = player.hasLost(); // this.left = player.hasLeft(); // this.quit = player.hasQuit(); // Makes no sense to restore // this.passed = player.isPassed(); // this.priorityTimeLeft = player.getPriorityTimeLeft(); // this.idleTimeout = player.hasIdleTimeout(); // this.timerTimeout = player.hasTimerTimeout(); // can't change so no need to restore // this.isTestMode = player.isTestMode(); // This is meta data and should'nt be restored by rollback // this.userData = player.getUserData(); this.library = player.getLibrary().copy(); this.sideboard = player.getSideboard().copy(); this.hand = player.getHand().copy(); this.graveyard = player.getGraveyard().copy(); this.commandersIds = new HashSet<>(player.getCommandersIds()); this.abilities = player.getAbilities().copy(); this.counters = player.getCounters().copy(); this.landsPlayed = player.getLandsPlayed(); this.landsPerTurn = player.getLandsPerTurn(); this.loyaltyUsePerTurn = player.getLoyaltyUsePerTurn(); this.maxHandSize = player.getMaxHandSize(); this.maxAttackedBy = player.getMaxAttackedBy(); this.manaPool = player.getManaPool().copy(); // Restore user specific settings in case changed since state save this.manaPool.setAutoPayment(this.getUserData().isManaPoolAutomatic()); this.manaPool.setAutoPaymentRestricted(this.getUserData().isManaPoolAutomaticRestricted()); this.turns = player.getTurns(); this.range = player.getRange(); this.canGainLife = player.isCanGainLife(); this.canLoseLife = player.isCanLoseLife(); this.attachments.clear(); this.attachments.addAll(player.getAttachments()); this.inRange.clear(); this.inRange.addAll(player.getInRange()); this.canPayLifeCost = player.canPayLifeCost(); this.sacrificeCostFilter = player.getSacrificeCostFilter() != null ? player.getSacrificeCostFilter().copy() : null; this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard(); this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); this.topCardRevealed = player.isTopCardRevealed(); this.playersUnderYourControl.clear(); this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); this.isGameUnderControl = player.isGameUnderControl(); this.turnController = player.getTurnControlledBy(); this.turnControllers.clear(); this.turnControllers.addAll(player.getTurnControllers()); this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); this.clearCastSourceIdManaCosts(); this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { this.castSourceIdManaCosts.put(entry.getKey(), entry.getValue().copy()); } for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { this.castSourceIdCosts.put(entry.getKey(), entry.getValue().copy()); } this.phyrexianColors = player.getPhyrexianColors().copy(); this.designations.clear(); this.designations.addAll(player.getDesignations()); // Don't restore! // this.storedBookmark // this.usersAllowedToSeeHandCards } @Override public void useDeck(Deck deck, Game game) { library.clear(); library.addAll(deck.getCards(), game); sideboard.clear(); for (Card card : deck.getSideboard()) { sideboard.add(card); } } /** * Cast e.g. from Karn Liberated to restart the current game * * @param game */ @Override public void init(Game game) { init(game, false); } @Override public void init(Game game, boolean testMode) { this.abort = false; if (!testMode) { this.hand.clear(); this.graveyard.clear(); } this.library.reset(); this.abilities.clear(); this.counters.clear(); this.wins = false; this.draws = false; this.loses = false; this.left = false; // reset is necessary because in tournament player will be used for each round this.quit = false; this.timerTimeout = false; this.idleTimeout = false; this.turns = 0; this.isGameUnderControl = true; this.turnController = this.getId(); this.turnControllers.clear(); this.playersUnderYourControl.clear(); this.passed = false; this.passedTurn = false; this.passedTurnSkipStack = false; this.passedUntilEndOfTurn = false; this.passedUntilNextMain = false; this.passedUntilStackResolved = false; this.dateLastAddedToStack = null; this.passedUntilEndStepBeforeMyTurn = false; this.skippedAtLeastOnce = false; this.passedAllTurns = false; this.justActivatedType = null; this.canGainLife = true; this.canLoseLife = true; this.topCardRevealed = false; this.payManaMode = false; this.setLife(game.getLife(), game, (UUID) null); this.setReachedNextTurnAfterLeaving(false); this.clearCastSourceIdManaCosts(); this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left this.phyrexianColors = new FilterMana(); this.designations.clear(); } /** * called before apply effects */ @Override public void reset() { this.abilities.clear(); this.landsPerTurn = 1; this.loyaltyUsePerTurn = 1; this.maxHandSize = 7; this.maxAttackedBy = Integer.MAX_VALUE; this.canGainLife = true; this.canLoseLife = true; this.canPayLifeCost = true; this.sacrificeCostFilter = null; this.loseByZeroOrLessLife = true; this.canPlayCardsFromGraveyard = false; this.topCardRevealed = false; this.alternativeSourceCosts.clear(); this.clearCastSourceIdManaCosts(); this.getManaPool().clearEmptyManaPoolRules(); this.phyrexianColors = new FilterMana(); } @Override public Counters getCounters() { return counters; } @Override public void beginTurn(Game game) { this.landsPlayed = 0; updateRangeOfInfluence(game); } @Override public RangeOfInfluence getRange() { return range; } protected void updateRangeOfInfluence(Game game) { // 20100423 - 801.2c // 801.2c The particular players within each player’s range of influence are determined as each turn begins. inRange.clear(); inRange.add(this.playerId); inRange.addAll(getAllNearPlayers(game, true)); inRange.addAll(getAllNearPlayers(game, false)); } private Set getAllNearPlayers(Game game, boolean needPrevious) { // find all near players (search from current player position) Set foundedList = new HashSet<>(); PlayerList players = game.getState().getPlayerList(this.playerId); int needAmount = this.getRange().getRange(); // distance to search (0 - ALL range) int foundedAmount = 0; while (needAmount == 0 || foundedAmount < needAmount) { Player foundedPlayer = needPrevious ? players.getPrevious(game) : players.getNext(game, false); // PlayerList is inifine, so stops on repeats if (foundedPlayer == null || foundedPlayer.getId().equals(this.playerId) || foundedList.contains(foundedPlayer.getId())) { break; } // skip leaved player (no needs cause next/previous code already checks it) foundedList.add(foundedPlayer.getId()); foundedAmount++; } return foundedList; } @Override public Set getInRange() { return inRange; } @Override public Set getPlayersUnderYourControl() { return this.playersUnderYourControl; } @Override public void controlPlayersTurn(Game game, UUID playerId) { Player player = game.getPlayer(playerId); player.setTurnControlledBy(this.getId()); game.informPlayers(getLogName() + " controls the turn of " + player.getLogName()); if (!playerId.equals(this.getId())) { this.playersUnderYourControl.add(playerId); if (!player.hasLeft() && !player.hasLost()) { player.setGameUnderYourControl(false); } DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), player.getLogName())); ability.setSourceId(getId()); ability.setControllerId(getId()); game.addDelayedTriggeredAbility(ability); } } @Override public void setTurnControlledBy(UUID playerId) { this.turnController = playerId; this.turnControllers.add(playerId); } @Override public List getTurnControllers() { return this.turnControllers; } @Override public UUID getTurnControlledBy() { return this.turnController; } @Override public void resetOtherTurnsControlled() { playersUnderYourControl.clear(); } /** * returns true if the player has the control itself - false if the player * is controlled by another player * * @return */ @Override public boolean isGameUnderControl() { return isGameUnderControl; } @Override public void setGameUnderYourControl(boolean value) { setGameUnderYourControl(value, true); } @Override public void setGameUnderYourControl(boolean value, boolean fullRestore) { this.isGameUnderControl = value; if (isGameUnderControl) { if (fullRestore) { this.turnControllers.clear(); this.turnController = getId(); } else { if (turnControllers.size() > 0) { this.turnControllers.remove(turnControllers.size() - 1); } if (turnControllers.isEmpty()) { this.turnController = getId(); } else { this.turnController = turnControllers.get(turnControllers.size() - 1); isGameUnderControl = false; } } } } @Override public void endOfTurn(Game game) { this.passedTurn = false; this.passedTurnSkipStack = false; } @Override public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { if (this.hasLost() || this.hasLeft()) { return false; } if (source != null) { if (abilities.containsKey(ShroudAbility.getInstance().getId())) { return false; } if (sourceControllerId != null && this.hasOpponent(sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && abilities.stream() .filter(HexproofBaseAbility.class::isInstance) .map(HexproofBaseAbility.class::cast) .anyMatch(ability -> ability.checkObject(source, game))) { return false; } return !hasProtectionFrom(source, game); } return true; } @Override public boolean hasProtectionFrom(MageObject source, Game game) { for (ProtectionAbility ability : abilities.getProtectionAbilities()) { if (!ability.canTarget(source, game)) { return true; } } return false; } @Override public int drawCards(int num, UUID sourceId, Game game) { if (num > 0) { return game.doAction(new MageDrawAction(this, num, null), sourceId); } return 0; } @Override public int drawCards(int num, UUID sourceId, Game game, List appliedEffects) { return game.doAction(new MageDrawAction(this, num, appliedEffects), sourceId); } @Override public void discardToMax(Game game) { if (hand.size() > this.maxHandSize) { if (!game.isSimulation()) { game.informPlayers(getLogName() + " discards down to " + this.maxHandSize + (this.maxHandSize == 1 ? " hand card" : " hand cards")); } discard(hand.size() - this.maxHandSize, false, null, game); } } /** * Don't use this in normal card code, it's for more internal use. Always * use the [Player].moveCards methods if possible for card movement of card * code. * * @param card * @param game * @return */ @Override public boolean putInHand(Card card, Game game) { if (card.isOwnedBy(playerId)) { card.setZone(Zone.HAND, game); this.hand.add(card); } else { return game.getPlayer(card.getOwnerId()).putInHand(card, game); } return true; } @Override public boolean removeFromHand(Card card, Game game) { return hand.remove(card.getId()); } @Override public boolean removeFromLibrary(Card card, Game game) { if (card == null) { return false; } library.remove(card.getId(), game); // must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop) // TODO: replace removeFromTop logic to normal with moveToZone return true; } @Override public Card discardOne(boolean random, Ability source, Game game) { Cards cards = discard(1, random, source, game); if (cards.isEmpty()) { return null; } return cards.getRandom(game); } @Override public Cards discard(int amount, boolean random, Ability source, Game game) { Cards discardedCards = doDiscard(amount, random, source, game); if (!discardedCards.isEmpty()) { UUID sourceId = source == null ? null : source.getSourceId(); game.fireEvent(GameEvent.getEvent( GameEvent.EventType.DISCARDED_CARDS, sourceId, sourceId, playerId, discardedCards.size() )); } return discardedCards; } @Override public Cards discard(Cards cards, Ability source, Game game) { Cards discardedCards = new CardsImpl(); if (cards == null) { return discardedCards; } for (Card card : cards.getCards(game)) { if (doDiscard(card, source, game, false)) { discardedCards.add(card); } } if (!discardedCards.isEmpty()) { UUID sourceId = source == null ? null : source.getSourceId(); game.fireEvent(GameEvent.getEvent( GameEvent.EventType.DISCARDED_CARDS, sourceId, sourceId, playerId, discardedCards.size() )); } return discardedCards; } private Cards doDiscard(int amount, boolean random, Ability source, Game game) { Cards discardedCards = new CardsImpl(); if (amount <= 0) { return discardedCards; } // all without dialogs if (this.getHand().size() == 1 || this.getHand().size() == amount) { List cardsToDiscard = new ArrayList<>(this.getHand()); for (UUID id : cardsToDiscard) { if (doDiscard(this.getHand().get(id, game), source, game, false)) { discardedCards.add(id); } } return discardedCards; } if (random) { for (int i = 0; i < amount; i++) { Card card = this.getHand().getRandom(game); if (doDiscard(card, source, game, false)) { discardedCards.add(card); } } } else { int possibleAmount = Math.min(getHand().size(), amount); TargetDiscard target = new TargetDiscard(possibleAmount, possibleAmount, new FilterCard(CardUtil.numberToText(possibleAmount, "a") + " card" + (possibleAmount > 1 ? "s" : "")), playerId); choose(Outcome.Discard, target, source == null ? null : source.getSourceId(), game); for (UUID cardId : target.getTargets()) { if (doDiscard(this.getHand().get(cardId, game), source, game, false)) { discardedCards.add(cardId); } } } return discardedCards; } @Override public boolean discard(Card card, Ability source, Game game) { return doDiscard(card, source, game, true); } private boolean doDiscard(Card card, Ability source, Game game, boolean fireEvent) { //20100716 - 701.7 /* 701.7. Discard # 701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. 701.7b By default, effects that cause a player to discard a card allow the affected player to choose which card to discard. Some effects, however, require a random discard or allow another player to choose which card is discarded. 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone instead of into its owner’s graveyard without being revealed, all values of that card’s characteristics are considered to be undefined. TODO: If a card is discarded this way to pay a cost that specifies a characteristic about the discarded card, that cost payment is illegal; the game returns to the moment before the cost was paid (see rule 717, "Handling Illegal Actions"). */ if (card == null) { return false; } GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, card.getId(), source == null ? null : source.getSourceId(), playerId); gameEvent.setFlag(source != null); // event from effect or from cost (source == null) if (game.replaceEvent(gameEvent, source)) { return false; } // write info to game log first so game log infos from triggered or replacement effects follow in the game log if (!game.isSimulation()) { game.informPlayers(getLogName() + " discards " + card.getLogName()); } /* If a card is discarded while Rest in Peace is on the battlefield, abilities that function * when a card is discarded (such as madness) still work, even though that card never reaches * a graveyard. In addition, spells or abilities that check the characteristics of a discarded * card (such as Chandra Ablaze's first ability) can find that card in exile. */ card.moveToZone(Zone.GRAVEYARD, source == null ? null : source.getSourceId(), game, false); // So discard is also successful if card is moved to another zone by replacement effect! UUID sourceId = source == null ? null : source.getSourceId(); game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), sourceId, playerId)); if (fireEvent) { game.fireEvent(GameEvent.getEvent( GameEvent.EventType.DISCARDED_CARDS, sourceId, sourceId, playerId, 1 )); } return true; } @Override public List getAttachments() { return attachments; } @Override public boolean addAttachment(UUID permanentId, Game game) { if (!this.attachments.contains(permanentId)) { Permanent aura = game.getPermanent(permanentId); if (aura == null) { aura = game.getPermanentEntering(permanentId); } if (aura != null) { if (!game.replaceEvent(new GameEvent(GameEvent.EventType.ENCHANT_PLAYER, playerId, permanentId, aura.getControllerId()))) { this.attachments.add(permanentId); aura.attachTo(playerId, game); game.fireEvent(new GameEvent(GameEvent.EventType.ENCHANTED_PLAYER, playerId, permanentId, aura.getControllerId())); return true; } } } return false; } @Override public boolean removeAttachment(Permanent attachment, Game game) { if (this.attachments.contains(attachment.getId())) { if (!game.replaceEvent(new GameEvent(GameEvent.EventType.UNATTACH, playerId, attachment.getId(), attachment.getControllerId()))) { this.attachments.remove(attachment.getId()); attachment.attachTo(null, game); game.fireEvent(new GameEvent(GameEvent.EventType.UNATTACHED, playerId, attachment.getId(), attachment.getControllerId())); return true; } } return false; } @Override public boolean removeFromBattlefield(Permanent permanent, Game game) { permanent.removeFromCombat(game, false); game.getBattlefield().removePermanent(permanent.getId()); if (permanent.getAttachedTo() != null) { Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); if (attachedTo != null) { attachedTo.removeAttachment(permanent.getId(), game); } else { Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo()); if (attachedToPlayer != null) { attachedToPlayer.removeAttachment(permanent, game); } else { Card attachedToCard = game.getCard(permanent.getAttachedTo()); if (attachedToCard != null) { attachedToCard.removeAttachment(permanent.getId(), game); } } } } if (permanent.getPairedCard() != null) { Permanent pairedCard = permanent.getPairedCard().getPermanent(game); if (pairedCard != null) { pairedCard.clearPairedCard(); } } if (permanent.getBandedCards() != null && !permanent.getBandedCards().isEmpty()) { for (UUID bandedId : permanent.getBandedCards()) { Permanent banded = game.getPermanent(bandedId); if (banded != null) { banded.removeBandedCard(permanent.getId()); } } } return true; } @Override public boolean putInGraveyard(Card card, Game game) { if (card.isOwnedBy(playerId)) { this.graveyard.add(card); } else { return game.getPlayer(card.getOwnerId()).putInGraveyard(card, game); } return true; } @Override public boolean removeFromGraveyard(Card card, Game game) { return this.graveyard.remove(card); } @Override public boolean putCardsOnBottomOfLibrary(Card card, Game game, Ability source, boolean anyOrder) { return putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, anyOrder); } @Override public boolean putCardsOnBottomOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { if (!cardsToLibrary.isEmpty()) { Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException if (!anyOrder) { // random order List ids = new ArrayList<>(cards); Collections.shuffle(ids); for (UUID id : ids) { moveObjectToLibrary(id, source == null ? null : source.getSourceId(), game, false, false); } } else { // user defined order TargetCard target = new TargetCard(Zone.ALL, new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)")); target.setRequired(true); while (cards.size() > 1 && this.canRespond() && this.choose(Outcome.Neutral, cards, target, game)) { UUID targetObjectId = target.getFirstTarget(); if (targetObjectId == null) { break; } cards.remove(targetObjectId); moveObjectToLibrary(targetObjectId, source == null ? null : source.getSourceId(), game, false, false); target.clearChosen(); } for (UUID c : cards) { moveObjectToLibrary(c, source == null ? null : source.getSourceId(), game, false, false); } } } return true; } @Override public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) { if (cards.isEmpty()) { return true; } game.informPlayers(getLogName() + " shuffels " + CardUtil.numberToText(cards.size(), "a") + " card" + (cards.size() == 1 ? "" : "s") + " into their library."); boolean status = moveCards(cards, Zone.LIBRARY, source, game); shuffleLibrary(source, game); return status; } @Override public boolean shuffleCardsToLibrary(Card card, Game game, Ability source) { if (card == null) { return true; } return shuffleCardsToLibrary(new CardsImpl(card), game, source); } @Override public boolean putCardOnTopXOfLibrary(Card card, Game game, Ability source, int xFromTheTop) { if (card.isOwnedBy(getId())) { if (library.size() + 1 < xFromTheTop) { putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true); } else { if (card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true) && !(card instanceof PermanentToken) && !card.isCopy()) { card = getLibrary().removeFromTop(game); getLibrary().putCardToTopXPos(card, xFromTheTop, game); game.informPlayers(card.getLogName() + " is put into " + getLogName() + "'s library " + CardUtil.numberToOrdinalText(xFromTheTop) + " from the top"); } else { return false; } } } else { return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop); } return true; } /** * Can be cards or permanents that go to library * * @param cardsToLibrary * @param game * @param source * @param anyOrder * @return */ @Override public boolean putCardsOnTopOfLibrary(Cards cardsToLibrary, Game game, Ability source, boolean anyOrder) { if (cardsToLibrary != null && !cardsToLibrary.isEmpty()) { Cards cards = new CardsImpl(cardsToLibrary); // prevent possible ConcurrentModificationException if (!anyOrder) { // random order List ids = new ArrayList<>(cards); Collections.shuffle(ids); for (UUID id : ids) { moveObjectToLibrary(id, source == null ? null : source.getSourceId(), game, true, false); } } else { // user defined order TargetCard target = new TargetCard(Zone.ALL, new FilterCard("card ORDER to put on the TOP of your library (last one chosen will be topmost)")); target.setRequired(true); while (cards.size() > 1 && this.canRespond() && this.choose(Outcome.Neutral, cards, target, game)) { UUID targetObjectId = target.getFirstTarget(); if (targetObjectId == null) { break; } cards.remove(targetObjectId); moveObjectToLibrary(targetObjectId, source == null ? null : source.getSourceId(), game, true, false); target.clearChosen(); } for (UUID c : cards) { moveObjectToLibrary(c, source == null ? null : source.getSourceId(), game, true, false); } } } return true; } @Override public boolean putCardsOnTopOfLibrary(Card cardToLibrary, Game game, Ability source, boolean anyOrder) { if (cardToLibrary != null) { return putCardsOnTopOfLibrary(new CardsImpl(cardToLibrary), game, source, anyOrder); } return true; } private boolean moveObjectToLibrary(UUID objectId, UUID sourceId, Game game, boolean toTop, boolean withName) { MageObject mageObject = game.getObject(objectId); if (mageObject instanceof Spell && mageObject.isCopy()) { // Spell copies are not moved as cards, so here the no copy spell has to be selected to move // (but because copy and original have the same objectId the wrong sepell can be selected from stack). // So let's check if the original spell is on the stack and has to be selected. // TODO: Better handling so each spell could be selected by a unique id Spell spellNoCopy = game.getStack().getSpell(sourceId, false); if (spellNoCopy != null) { mageObject = spellNoCopy; } } if (mageObject != null) { Zone fromZone = game.getState().getZone(objectId); if ((mageObject instanceof Permanent)) { return this.moveCardToLibraryWithInfo((Permanent) mageObject, sourceId, game, fromZone, toTop, withName); } else if (mageObject instanceof Card) { return this.moveCardToLibraryWithInfo((Card) mageObject, sourceId, game, fromZone, toTop, withName); } } return false; } @Override public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { // cost must be copied for data consistence between game simulations castSourceIdWithAlternateMana.add(sourceId); castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); } @Override public Set getCastSourceIdWithAlternateMana() { return castSourceIdWithAlternateMana; } @Override public Map> getCastSourceIdCosts() { return castSourceIdCosts; } @Override public Map> getCastSourceIdManaCosts() { return castSourceIdManaCosts; } @Override public void clearCastSourceIdManaCosts() { this.castSourceIdCosts.clear(); this.castSourceIdManaCosts.clear(); this.castSourceIdWithAlternateMana.clear(); } @Override public void setPayManaMode(boolean payManaMode) { this.payManaMode = payManaMode; } @Override public boolean isInPayManaMode() { return payManaMode; } @Override public boolean playCard(Card card, Game game, boolean noMana, boolean ignoreTiming, MageObjectReference reference) { if (card == null) { return false; } boolean result; if (card.isLand()) { result = playLand(card, game, ignoreTiming); } else { result = cast(card.getSpellAbility(), game, noMana, reference); } if (!result) { game.informPlayer(this, "You can't play " + card.getIdName() + '.'); } return result; } /** * * @param originalAbility * @param game * @param noMana cast it without paying mana costs * @param permittingObject which object permitted the cast * @return */ @Override public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, MageObjectReference permittingObject) { if (game == null || originalAbility == null) { return false; } // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). SpellAbility ability = originalAbility.copy(); ability.setControllerId(getId()); ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); //20091005 - 601.2a if (ability.getSourceId() == null) { logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule()); return false; } Card card = game.getCard(ability.getSourceId()); if (card != null) { if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability.getSourceId(), playerId, permittingObject), ability)) { int bookmark = game.bookmarkState(); Zone fromZone = game.getState().getZone(card.getMainCard().getId()); card.cast(game, fromZone, ability, playerId); Spell spell = game.getStack().getSpell(ability.getId()); if (spell == null) { logger.error("Got no spell from stack. ability: " + ability.getRule()); return false; } // Update the zcc to the stack ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); // ALTERNATIVE COST from dynamic effects // some effects set sourceId to cast without paying mana costs or other costs if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { Ability spellAbility = spell.getSpellAbility(); ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); if (alternateCosts == null) { noMana = true; } else { spellAbility.getManaCosts().clear(); spellAbility.getManaCostsToPay().clear(); spellAbility.getManaCosts().add(alternateCosts.copy()); spellAbility.getManaCostsToPay().add(alternateCosts.copy()); } spellAbility.getCosts().clear(); if (costs != null) { spellAbility.getCosts().addAll(costs); } } clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time GameEvent event = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, spell.getSpellAbility().getId(), spell.getSpellAbility().getSourceId(), playerId, permittingObject); game.fireEvent(event); if (spell.activate(game, noMana)) { event = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, spell.getSpellAbility().getId(), spell.getSpellAbility().getSourceId(), playerId, permittingObject); event.setZone(fromZone); game.fireEvent(event); if (!game.isSimulation()) { game.informPlayers(getLogName() + spell.getActivatedMessage(game)); } game.removeBookmark(bookmark); resetStoredBookmark(game); return true; } restoreState(bookmark, ability.getRule(), game); } } return false; } @Override public boolean playLand(Card card, Game game, boolean ignoreTiming) { // Check for alternate casting possibilities: e.g. land with Morph if (card == null) { return false; } ActivatedAbility playLandAbility = null; boolean foundAlternative = false; for (Ability ability : card.getAbilities()) { // if cast for noMana no Alternative costs are allowed if ((ability instanceof AlternativeSourceCosts) || (ability instanceof OptionalAdditionalSourceCosts)) { foundAlternative = true; } if (ability instanceof PlayLandAbility) { playLandAbility = (ActivatedAbility) ability; } } // try alternative cast (face down) if (foundAlternative) { SpellAbility spellAbility = new SpellAbility(null, "", game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE); spellAbility.setControllerId(this.getId()); spellAbility.setSourceId(card.getId()); if (cast(spellAbility, game, false, null)) { return true; } } if (playLandAbility == null) { return false; } //20091005 - 114.2a ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game); if (ignoreTiming) { if (!canPlayLand()) { return false; // ignore timing does not mean that more lands than normal can be played } } else { if (!activationStatus.canActivate()) { return false; } } //20091005 - 305.1 if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject()))) { // int bookmark = game.bookmarkState(); // 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++; 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 return true; } // putOntoBattlefield returned false if putOntoBattlefield was replaced by replacement effect (e.g. Kjeldoran Outpost). // But that would undo the effect completely, // what makes no real sense. So it makes no sense to generally do a restoreState here. // restoreState(bookmark, card.getName(), game); } // if the to play the land is replaced (e.g. Kjeldoran Outpost and don't sacrificing a Plains) it's a valid state so returning true here return true; } protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, ability.getId(), ability.getSourceId(), playerId))) { int bookmark = game.bookmarkState(); if (ability.activate(game, false)) { if (ability.resolve(game)) { if (ability.isUndoPossible()) { if (storedBookmark == -1 || storedBookmark > bookmark) { // e.g. useful for undo Nykthos, Shrine to Nyx setStoredBookmark(bookmark); } } else { resetStoredBookmark(game); } return true; } } restoreState(bookmark, ability.getRule(), game); } return false; } protected boolean playAbility(ActivatedAbility ability, Game game) { //20091005 - 602.2a if (ability.isUsesStack()) { if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, ability.getId(), ability.getSourceId(), playerId))) { int bookmark = game.bookmarkState(); ability.newId(); ability.setControllerId(playerId); game.getStack().push(new StackAbility(ability, playerId)); if (ability.activate(game, false)) { game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, ability.getId(), ability.getSourceId(), playerId)); if (!game.isSimulation()) { game.informPlayers(getLogName() + ability.getGameLogMessage(game)); } game.removeBookmark(bookmark); resetStoredBookmark(game); return true; } restoreState(bookmark, ability.getRule(), game); } } else { int bookmark = game.bookmarkState(); if (ability.activate(game, false)) { ability.resolve(game); game.removeBookmark(bookmark); resetStoredBookmark(game); return true; } restoreState(bookmark, ability.getRule(), game); } return false; } protected boolean specialAction(SpecialAction action, Game game) { //20091005 - 114 if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, action.getId(), action.getSourceId(), getId()))) { int bookmark = game.bookmarkState(); if (action.activate(game, false)) { game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, action.getId(), action.getSourceId(), getId())); if (!game.isSimulation()) { game.informPlayers(getLogName() + action.getGameLogMessage(game)); } if (action.resolve(game)) { game.removeBookmark(bookmark); resetStoredBookmark(game); return true; } } restoreState(bookmark, action.getRule(), game); } return false; } protected void restoreState(int bookmark, String text, Game game) { game.restoreState(bookmark, text); if (storedBookmark >= bookmark) { resetStoredBookmark(game); } } @Override public boolean activateAbility(ActivatedAbility ability, Game game) { if (ability == null) { return false; } boolean result; if (ability instanceof PassAbility) { pass(game); return true; } Card card = game.getCard(ability.getSourceId()); if (ability instanceof PlayLandAsCommanderAbility) { // LAND as commander: play land with cost, but without stack ActivationStatus activationStatus = ability.canActivate(this.playerId, game); if (!activationStatus.canActivate() || !this.canPlayLand()) { return false; } if (card == null) { return false; } // as copy, tries to applie cost effects and pays Ability activatingAbility = ability.copy(); if (activatingAbility.activate(game, false)) { result = playLand(card, game, false); } else { result = false; } } else if (ability instanceof PlayLandAbility) { // LAND as normal card: without cost and stack result = playLand(card, game, false); } else { // ABILITY ActivationStatus activationStatus = ability.canActivate(this.playerId, game); if (!activationStatus.canActivate()) { return false; } switch (ability.getAbilityType()) { case SPECIAL_ACTION: result = specialAction((SpecialAction) ability.copy(), game); break; case MANA: result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); break; case SPELL: result = cast((SpellAbility) ability, game, false, activationStatus.getPermittingObject()); break; default: result = playAbility(ability.copy(), game); break; } } //if player has taken an action then reset all player passed flags justActivatedType = null; if (result) { if (isHuman() && (ability.getAbilityType() == AbilityType.SPELL || ability.getAbilityType() == AbilityType.ACTIVATED)) { if (ability.isUsesStack()) { // if the ability does not use the stack (e.g. Suspend) auto pass would go to next phase unintended setJustActivatedType(ability.getAbilityType()); } } game.getPlayers().resetPassed(); } return result; } @Override public boolean triggerAbility(TriggeredAbility triggeredAbility, Game game) { if (triggeredAbility == null) { logger.warn("Null source in triggerAbility method"); throw new IllegalArgumentException("source TriggeredAbility must not be null"); } //20091005 - 603.3c, 603.3d int bookmark = game.bookmarkState(); TriggeredAbility ability = triggeredAbility.copy(); MageObject sourceObject = ability.getSourceObject(game); if (sourceObject != null) { sourceObject.adjustTargets(ability, game); } if (ability.canChooseTarget(game)) { if (ability.isUsesStack()) { game.getStack().push(new StackAbility(ability, playerId)); } if (ability.activate(game, false)) { if ((ability.isUsesStack() || ability.getRuleVisible()) && !game.isSimulation()) { game.informPlayers(ability.getGameLogMessage(game)); } if (!ability.isUsesStack()) { ability.resolve(game); } else { game.fireEvent(new GameEvent(EventType.TRIGGERED_ABILITY, ability.getId(), ability.getSourceId(), ability.getControllerId())); } game.removeBookmark(bookmark); return true; } } restoreState(bookmark, triggeredAbility.getRule(), game); // why restore is needed here? (to remove the triggered ability from the stack because of no possible targets) return false; } public static LinkedHashMap getSpellAbilities(UUID playerId, MageObject object, Zone zone, Game game) { LinkedHashMap useable = new LinkedHashMap<>(); for (Ability ability : object.getAbilities()) { if (ability instanceof SpellAbility) { switch (((SpellAbility) ability).getSpellAbilityType()) { case BASE_ALTERNATE: ActivationStatus as = ((SpellAbility) ability).canActivate(playerId, game); if (as.canActivate()) { useable.put(ability.getId(), (SpellAbility) ability); // example: Chandra, Torch of Defiance +1 loyal ability } return useable; case SPLIT_FUSED: if (zone == Zone.HAND) { if (ability.canChooseTarget(game)) { useable.put(ability.getId(), (SpellAbility) ability); } } case SPLIT: if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game)) { useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), ((SplitCard) object).getLeftHalfCard().getSpellAbility()); } if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game)) { useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), ((SplitCard) object).getRightHalfCard().getSpellAbility()); } return useable; case SPLIT_AFTERMATH: if (zone == Zone.GRAVEYARD) { if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game)) { useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), ((SplitCard) object).getRightHalfCard().getSpellAbility()); } } else { if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game)) { useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), ((SplitCard) object).getLeftHalfCard().getSpellAbility()); } } return useable; default: useable.put(ability.getId(), (SpellAbility) ability); } } } return useable; } @Override public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { LinkedHashMap useable = new LinkedHashMap<>(); // It may not be possible to activate abilities of stack abilities if (object instanceof StackAbility || object == null) { return useable; } boolean previousState = game.inCheckPlayableState(); game.setCheckPlayableState(true); try { // collect and filter playable activated abilities // GUI: user clicks on card, but it must activate ability from any card's parts (main, left, right) UUID needId1, needId2, needId3; if (object instanceof SplitCard) { needId1 = object.getId(); needId2 = ((SplitCard) object).getLeftHalfCard().getId(); needId3 = ((SplitCard) object).getRightHalfCard().getId(); } else if (object instanceof AdventureCard) { needId1 = object.getId(); needId2 = ((AdventureCard) object).getMainCard().getId(); needId3 = ((AdventureCard) object).getSpellCard().getId(); } else if (object instanceof AdventureCardSpell) { needId1 = object.getId(); needId2 = ((AdventureCardSpell) object).getParentCard().getId(); needId3 = object.getId(); } else { needId1 = object.getId(); needId2 = object.getId(); needId3 = object.getId(); } // workaround to find all abilities first and filter it for one object List allPlayable = getPlayable(game, true, zone, false); for (ActivatedAbility ability : allPlayable) { if (Objects.equals(ability.getSourceId(), needId1) || Objects.equals(ability.getSourceId(), needId2) || Objects.equals(ability.getSourceId(), needId3)) { useable.putIfAbsent(ability.getId(), ability); } } } finally { game.setCheckPlayableState(previousState); } return useable; } protected LinkedHashMap getUseableManaAbilities(MageObject object, Zone zone, Game game) { LinkedHashMap useable = new LinkedHashMap<>(); boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); for (ActivatedManaAbilityImpl ability : object.getAbilities().getActivatedManaAbilities(zone)) { if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { if (ability.canActivate(playerId, game).canActivate()) { useable.put(ability.getId(), ability); } } } return useable; } @Override public int getLandsPlayed() { return landsPlayed; } @Override public boolean canPlayLand() { //20091005 - 114.2a return landsPlayed < landsPerTurn; } protected boolean isActivePlayer(Game game) { return game.isActivePlayer(this.playerId); } @Override public void shuffleLibrary(Ability source, Game game) { if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SHUFFLE_LIBRARY, playerId, playerId))) { this.library.shuffle(); if (!game.isSimulation()) { game.informPlayers(getLogName() + "'s library is shuffled"); } game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, (source == null ? null : source.getSourceId()), playerId)); } } @Override public void revealCards(Ability source, Cards cards, Game game) { revealCards(source, null, cards, game, true); } @Override public void revealCards(String titleSuffix, Cards cards, Game game) { revealCards(titleSuffix, cards, game, true); } @Override public void revealCards(String titleSuffix, Cards cards, Game game, boolean postToLog) { revealCards(null, titleSuffix, cards, game, postToLog); } @Override public void revealCards(Ability source, String titleSuffix, Cards cards, Game game) { revealCards(source, titleSuffix, cards, game, true); } @Override public void revealCards(Ability source, String titleSuffix, Cards cards, Game game, boolean postToLog) { if (cards == null || cards.isEmpty()) { return; } if (postToLog) { game.getState().getRevealed().add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); } else { game.getState().getRevealed().update(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); } if (postToLog && !game.isSimulation()) { StringBuilder sb = new StringBuilder(getLogName()).append(" reveals "); int current = 0, last = cards.size(); for (Card card : cards.getCards(game)) { current++; sb.append(GameLog.getColoredObjectName(card)); if (current < last) { sb.append(", "); } } game.informPlayers(sb.toString()); } } @Override public void lookAtCards(String titleSuffix, Card card, Game game) { game.getState().getLookedAt(this.playerId).add(titleSuffix, card); game.fireUpdatePlayersEvent(); } @Override public void lookAtCards(String titleSuffix, Cards cards, Game game) { this.lookAtCards(null, titleSuffix, cards, game); } @Override public void lookAtCards(Ability source, String titleSuffix, Cards cards, Game game) { game.getState().getLookedAt(this.playerId).add(CardUtil.createObjectRealtedWindowTitle(source, game, titleSuffix), cards); game.fireUpdatePlayersEvent(); } @Override public void phasing(Game game) { //20091005 - 502.1 List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { // 502.15i When a permanent phases out, any local enchantments or Equipment // attached to that permanent phase out at the same time. This alternate way of // phasing out is known as phasing out "indirectly." An enchantment or Equipment // that phased out indirectly won't phase in by itself, but instead phases in // along with the card it's attached to. Permanent attachedTo = game.getPermanent(permanent.getAttachedTo()); if (!(attachedTo != null && attachedTo.isControlledBy(this.getId()))) { permanent.phaseOut(game, false); } } for (Permanent permanent : phasedOut) { if (!permanent.isPhasedOutIndirectly()) { permanent.phaseIn(game); } } } @Override public void untap(Game game) { // create list of all "notMoreThan" effects to track which one are consumed Map>, Integer> notMoreThanEffectsUsage = new HashMap<>(); for (Entry> restrictionEffect : game.getContinuousEffects().getApplicableRestrictionUntapNotMoreThanEffects(this, game).entrySet()) { notMoreThanEffectsUsage.put(restrictionEffect, restrictionEffect.getKey().getNumber()); } if (!notMoreThanEffectsUsage.isEmpty()) { // create list of all permanents that can be untapped generally List canBeUntapped = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { boolean untap = true; for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { untap &= effect.canBeUntapped(permanent, null, game, true); } if (untap) { canBeUntapped.add(permanent); } } // selected permanents to untap List selectedToUntap = new ArrayList<>(); // player can cancel the selection of an effect to use a preferred order of restriction effects boolean playerCanceledSelection; do { playerCanceledSelection = false; // select permanents to untap to consume the "notMoreThan" effects for (Map.Entry>, Integer> handledEntry : notMoreThanEffectsUsage.entrySet()) { // select a permanent to untap for this entry int numberToUntap = handledEntry.getValue(); if (numberToUntap > 0) { List leftForUntap = getPermanentsThatCanBeUntapped(game, canBeUntapped, handledEntry.getKey().getKey(), notMoreThanEffectsUsage); FilterControlledPermanent filter = handledEntry.getKey().getKey().getFilter().copy(); String message = filter.getMessage(); // omit already from other untap effects selected permanents for (Permanent permanent : selectedToUntap) { filter.add(Predicates.not(new PermanentIdPredicate(permanent.getId()))); } // while targets left and there is still allowed to untap while (canRespond() && !leftForUntap.isEmpty() && numberToUntap > 0) { // player has to select the permanent they want to untap for this restriction Ability ability = handledEntry.getKey().getValue().iterator().next(); if (ability != null) { StringBuilder sb = new StringBuilder(message).append(" to untap").append(" (").append(Math.min(leftForUntap.size(), numberToUntap)).append(" in total"); MageObject effectSource = game.getObject(ability.getSourceId()); if (effectSource != null) { sb.append(" from ").append(effectSource.getLogName()); } sb.append(')'); filter.setMessage(sb.toString()); Target target = new TargetPermanent(1, 1, filter, true); if (!this.chooseTarget(Outcome.Untap, target, ability, game)) { // player canceled, go on with the next effect (if no other effect available, this effect will be active again) playerCanceledSelection = true; break; } Permanent selectedPermanent = game.getPermanent(target.getFirstTarget()); if (leftForUntap.contains(selectedPermanent)) { selectedToUntap.add(selectedPermanent); numberToUntap--; // don't allow to select same permanent twice filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); // reduce available untap numbers from other "UntapNotMoreThan" effects if selected permanent applies to their filter too for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { if (notMoreThanEffect.getValue() > 0 && notMoreThanEffect.getKey().getKey().getFilter().match(selectedPermanent, game)) { notMoreThanEffect.setValue(notMoreThanEffect.getValue() - 1); } } // update the left for untap list leftForUntap = getPermanentsThatCanBeUntapped(game, canBeUntapped, handledEntry.getKey().getKey(), notMoreThanEffectsUsage); // remove already selected permanents for (Permanent permanent : selectedToUntap) { leftForUntap.remove(permanent); } } else { // player selected an permanent that is restricted by another effect, disallow it (so AI can select another one) filter.add(Predicates.not(new PermanentIdPredicate(selectedPermanent.getId()))); if (this.isHuman() && !game.isSimulation()) { game.informPlayer(this, "This permanent can't be untapped because of other restricting effect."); } } } } } } } while (canRespond() && playerCanceledSelection); if (!game.isSimulation()) { // show in log which permanents were selected to untap for (Permanent permanent : selectedToUntap) { game.informPlayers(this.getLogName() + " untapped " + permanent.getLogName()); } } // untap if permanent is not concerned by notMoreThan effects or is included in the selectedToUntapList for (Permanent permanent : canBeUntapped) { boolean doUntap = true; if (!selectedToUntap.contains(permanent)) { // if the permanent is covered by one of the restriction effects, don't untap it for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game)) { doUntap = false; break; } } } if (permanent != null && doUntap) { permanent.untap(game); } } } else { //20091005 - 502.2 for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { boolean untap = true; for (RestrictionEffect effect : game.getContinuousEffects().getApplicableRestrictionEffects(permanent, game).keySet()) { untap &= effect.canBeUntapped(permanent, null, game, true); } if (untap) { permanent.untap(game); } } } } private List getPermanentsThatCanBeUntapped(Game game, List canBeUntapped, RestrictionUntapNotMoreThanEffect handledEffect, Map>, Integer> notMoreThanEffectsUsage) { List leftForUntap = new ArrayList<>(); // select permanents that can still be untapped for (Permanent permanent : canBeUntapped) { if (handledEffect.getFilter().match(permanent, game)) { // matches the restricted permanents of handled entry boolean canBeSelected = true; // check if the permanent is restricted by another restriction that has left no permanent for (Entry>, Integer> notMoreThanEffect : notMoreThanEffectsUsage.entrySet()) { if (notMoreThanEffect.getKey().getKey().getFilter().match(permanent, game) && notMoreThanEffect.getValue() == 0) { canBeSelected = false; break; } } if (canBeSelected) { leftForUntap.add(permanent); } } } return leftForUntap; } @Override public UUID getId() { return playerId; } @Override public Cards getHand() { return hand; } @Override public Graveyard getGraveyard() { return graveyard; } @Override public ManaPool getManaPool() { return this.manaPool; } @Override public String getName() { return name; } @Override public String getLogName() { return GameLog.getColoredPlayerName(name); } @Override public boolean isHuman() { return human; } @Override public Library getLibrary() { return library; } @Override public Cards getSideboard() { return sideboard; } @Override public int getLife() { return life; } @Override public void initLife(int life) { this.life = life; } @Override public void setLife(int life, Game game, Ability source) { setLife(life, game, source.getSourceId()); } @Override public void setLife(int life, Game game, UUID sourceId) { // rule 118.5 if (life > this.life) { gainLife(life - this.life, game, sourceId); } else if (life < this.life) { loseLife(this.life - life, game, false); } } @Override public void setLifeTotalCanChange(boolean lifeTotalCanChange) { this.canGainLife = lifeTotalCanChange; this.canLoseLife = lifeTotalCanChange; } @Override public boolean isLifeTotalCanChange() { return canGainLife || canLoseLife; } @Override public List getAlternativeSourceCosts() { return alternativeSourceCosts; } @Override public boolean isCanLoseLife() { return canLoseLife; } @Override public void setCanLoseLife(boolean canLoseLife) { this.canLoseLife = canLoseLife; } @Override public int loseLife(int amount, Game game, boolean atCombat) { if (!canLoseLife || !this.isInGame()) { return 0; } GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE, playerId, playerId, playerId, amount, atCombat); if (!game.replaceEvent(event)) { // this.life -= event.getAmount(); this.life = CardUtil.subtractWithOverflowCheck(this.life, event.getAmount()); if (!game.isSimulation()) { game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life"); } if (amount > 0) { game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, playerId, playerId, playerId, amount, atCombat)); } return amount; } return 0; } @Override public boolean isCanGainLife() { return canGainLife; } @Override public void setCanGainLife(boolean canGainLife) { this.canGainLife = canGainLife; } @Override public int gainLife(int amount, Game game, Ability source) { return gainLife(amount, game, source.getSourceId()); } @Override public int gainLife(int amount, Game game, UUID sourceId) { if (!canGainLife || amount <= 0) { return 0; } GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE, playerId, playerId, playerId, amount, false); if (!game.replaceEvent(event)) { // TODO: lock life at Integer.MAX_VALUE if reached, until it's set to a different amount // (https://magic.wizards.com/en/articles/archive/news/unstable-faqawaslfaqpaftidawabiajtbt-2017-12-06 - "infinite" life total stays infinite no matter how much is gained or lost) // this.life += event.getAmount(); this.life = CardUtil.addWithOverflowCheck(this.life, event.getAmount()); if (!game.isSimulation()) { game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life"); } game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE, playerId, sourceId, playerId, event.getAmount())); return event.getAmount(); } return 0; } @Override public int damage(int damage, UUID sourceId, Game game) { return doDamage(damage, sourceId, game, false, true, null); } @Override public int damage(int damage, UUID sourceId, Game game, boolean combatDamage, boolean preventable) { return doDamage(damage, sourceId, game, combatDamage, preventable, null); } @Override public int damage(int damage, UUID sourceId, Game game, boolean combatDamage, boolean preventable, List< UUID> appliedEffects) { return doDamage(damage, sourceId, game, combatDamage, preventable, appliedEffects); } @SuppressWarnings({"null", "ConstantConditions"}) private int doDamage(int damage, UUID sourceId, Game game, boolean combatDamage, boolean preventable, List< UUID> appliedEffects) { if (!this.isInGame()) { return 0; } if (damage > 0) { if (canDamage(game.getObject(sourceId), game)) { GameEvent event = new DamagePlayerEvent(playerId, sourceId, playerId, damage, preventable, combatDamage); event.setAppliedEffects(appliedEffects); if (!game.replaceEvent(event)) { int actualDamage = event.getAmount(); if (actualDamage > 0) { UUID sourceControllerId = null; Abilities sourceAbilities = null; MageObject source = game.getPermanentOrLKIBattlefield(sourceId); if (source == null) { StackObject stackObject = game.getStack().getStackObject(sourceId); if (stackObject != null) { source = stackObject.getStackAbility().getSourceObject(game); } else { source = game.getObject(sourceId); } if (source instanceof Spell) { sourceAbilities = ((Spell) source).getAbilities(game); sourceControllerId = ((Spell) source).getControllerId(); } else if (source instanceof Card) { sourceAbilities = ((Card) source).getAbilities(game); sourceControllerId = ((Card) source).getOwnerId(); } else if (source instanceof CommandObject) { sourceControllerId = ((CommandObject) source).getControllerId(); sourceAbilities = source.getAbilities(); } } else { sourceAbilities = ((Permanent) source).getAbilities(game); sourceControllerId = ((Permanent) source).getControllerId(); } if (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId())) { addCounters(CounterType.POISON.createInstance(actualDamage), game); } else { GameEvent damageToLifeLossEvent = new GameEvent(EventType.DAMAGE_CAUSES_LIFE_LOSS, playerId, sourceId, playerId, actualDamage, combatDamage); if (!game.replaceEvent(damageToLifeLossEvent)) { this.loseLife(damageToLifeLossEvent.getAmount(), game, combatDamage); } } if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { if (combatDamage) { game.getPermanent(sourceId).markLifelink(actualDamage); } else { Player player = game.getPlayer(sourceControllerId); player.gainLife(actualDamage, game, sourceId); } } // Unstable ability - Earl of Squirrel if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) { Player player = game.getPlayer(sourceControllerId); new SquirrelToken().putOntoBattlefield(actualDamage, game, sourceId, player.getId()); } game.fireEvent(new DamagedPlayerEvent(playerId, sourceId, playerId, actualDamage, combatDamage)); return actualDamage; } } } else { MageObject sourceObject = game.getObject(sourceId); game.informPlayers(damage + " damage " + (sourceObject == null ? "" : "from " + sourceObject.getLogName()) + " to " + getLogName() + (damage > 1 ? " were" : "was") + " prevented because of protection."); } } return 0; } @Override public boolean addCounters(Counter counter, Game game) { boolean returnCode = true; GameEvent addingAllEvent = GameEvent.getEvent(EventType.ADD_COUNTERS, playerId, null, playerId, counter.getName(), counter.getCount()); if (!game.replaceEvent(addingAllEvent)) { int amount = addingAllEvent.getAmount(); int finalAmount = amount; boolean isEffectFlag = addingAllEvent.getFlag(); for (int i = 0; i < amount; i++) { Counter eventCounter = counter.copy(); eventCounter.remove(eventCounter.getCount() - 1); GameEvent addingOneEvent = GameEvent.getEvent(EventType.ADD_COUNTER, playerId, null, playerId, counter.getName(), 1); addingOneEvent.setFlag(isEffectFlag); if (!game.replaceEvent(addingOneEvent)) { getCounters().addCounter(eventCounter); GameEvent addedOneEvent = GameEvent.getEvent(EventType.COUNTER_ADDED, playerId, null, playerId, counter.getName(), 1); addedOneEvent.setFlag(addingOneEvent.getFlag()); game.fireEvent(addedOneEvent); } else { finalAmount--; returnCode = false; } } if (finalAmount > 0) { GameEvent addedAllEvent = GameEvent.getEvent(EventType.COUNTERS_ADDED, playerId, null, playerId, counter.getName(), amount); addedAllEvent.setFlag(addingAllEvent.getFlag()); game.fireEvent(addedAllEvent); } } else { returnCode = false; } return returnCode; } @Override public void removeCounters(String name, int amount, Ability source, Game game) { int finalAmount = 0; for (int i = 0; i < amount; i++) { if (!counters.removeCounter(name, 1)) { break; } GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, getId(), (source == null ? null : source.getSourceId()), (source == null ? null : source.getControllerId())); event.setData(name); event.setAmount(1); game.fireEvent(event); finalAmount++; } GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, getId(), (source == null ? null : source.getSourceId()), (source == null ? null : source.getControllerId())); event.setData(name); event.setAmount(finalAmount); game.fireEvent(event); } protected boolean canDamage(MageObject source, Game game) { for (ProtectionAbility ability : abilities.getProtectionAbilities()) { if (!ability.canTarget(source, game)) { return false; } } return true; } @Override public Abilities getAbilities() { return this.abilities; } @Override public void addAbility(Ability ability) { ability.setSourceId(playerId); this.abilities.add(ability); } @Override public int getLandsPerTurn() { return this.landsPerTurn; } @Override public void setLandsPerTurn(int landsPerTurn) { this.landsPerTurn = landsPerTurn; } @Override public int getLoyaltyUsePerTurn() { return this.loyaltyUsePerTurn; } @Override public void setLoyaltyUsePerTurn(int loyaltyUsePerTurn) { this.loyaltyUsePerTurn = loyaltyUsePerTurn; } @Override public int getMaxHandSize() { return maxHandSize; } @Override public void setMaxHandSize(int maxHandSize) { this.maxHandSize = maxHandSize; } @Override public void setMaxAttackedBy(int maxAttackedBy) { this.maxAttackedBy = maxAttackedBy; } @Override public int getMaxAttackedBy() { return maxAttackedBy; } @Override public void setResponseString(String responseString) { } @Override public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) { } @Override public void setResponseUUID(UUID responseUUID) { } @Override public void setResponseBoolean(Boolean responseBoolean) { } @Override public void setResponseInteger(Integer responseInteger) { } @Override public boolean isPassed() { return passed; } @Override public void pass(Game game) { this.passed = true; resetStoredBookmark(game); } @Override public void resetPassed() { this.passed = this.loses || this.hasLeft(); } @Override public void resetPlayerPassedActions() { this.passed = false; this.passedTurn = false; this.passedTurnSkipStack = false; this.passedUntilEndOfTurn = false; this.passedUntilNextMain = false; this.passedUntilStackResolved = false; this.dateLastAddedToStack = null; this.passedUntilEndStepBeforeMyTurn = false; this.skippedAtLeastOnce = false; this.passedAllTurns = false; this.justActivatedType = null; } @Override public void quit(Game game) { quit = true; this.concede(game); logger.debug(getName() + " quits the match."); game.informPlayers(getLogName() + " quits the match."); } @Override public void timerTimeout(Game game) { quit = true; timerTimeout = true; this.concede(game); game.informPlayers(getLogName() + " has run out of time, losing the match."); } @Override public void idleTimeout(Game game) { quit = true; idleTimeout = true; this.concede(game); game.informPlayers(getLogName() + " was idle for too long, losing the Match."); } @Override public void concede(Game game) { game.setConcedingPlayer(playerId); lost(game); // this.left = true; } @Override public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { switch (playerAction) { case PASS_PRIORITY_UNTIL_MY_NEXT_TURN: // F9 resetPlayerPassedActions(); passedAllTurns = true; this.skip(); break; case PASS_PRIORITY_UNTIL_TURN_END_STEP: // F5 resetPlayerPassedActions(); passedUntilEndOfTurn = true; skippedAtLeastOnce = PhaseStep.END_TURN != game.getTurn().getStepType(); this.skip(); break; case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4 resetPlayerPassedActions(); passedTurn = true; this.skip(); break; case PASS_PRIORITY_UNTIL_NEXT_TURN_SKIP_STACK: // F6 resetPlayerPassedActions(); passedTurnSkipStack = true; this.skip(); break; case PASS_PRIORITY_UNTIL_NEXT_MAIN_PHASE: //F7 resetPlayerPassedActions(); passedUntilNextMain = true; skippedAtLeastOnce = !(game.getTurn().getStepType() == PhaseStep.POSTCOMBAT_MAIN || game.getTurn().getStepType() == PhaseStep.PRECOMBAT_MAIN); this.skip(); break; case PASS_PRIORITY_UNTIL_STACK_RESOLVED: // Default F10 - Skips until the current stack is resolved if (!game.getStack().isEmpty()) { // If stack is empty do nothing resetPlayerPassedActions(); passedUntilStackResolved = true; dateLastAddedToStack = game.getStack().getDateLastAdded(); this.skip(); } break; case PASS_PRIORITY_UNTIL_END_STEP_BEFORE_MY_NEXT_TURN: //F11 resetPlayerPassedActions(); passedUntilEndStepBeforeMyTurn = true; this.skip(); break; case PASS_PRIORITY_CANCEL_ALL_ACTIONS: resetPlayerPassedActions(); break; case PERMISSION_REQUESTS_ALLOWED_OFF: userData.setAllowRequestShowHandCards(false); break; case PERMISSION_REQUESTS_ALLOWED_ON: userData.setAllowRequestShowHandCards(true); userData.resetRequestedHandPlayersList(game.getId()); // users can send request again break; } logger.trace("PASS Priority: " + playerAction.toString()); } @Override public void leave() { this.passed = true; this.loses = true; this.left = true; this.abort(); //20100423 - 800.4a this.hand.clear(); this.graveyard.clear(); this.library.clear(); } @Override public boolean hasLeft() { return this.left; } @Override public void lost(Game game) { if (canLose(game)) { lostForced(game); } } @Override public void lostForced(Game game) { logger.debug(this.getName() + " has lost gameId: " + game.getId()); //20100423 - 603.9 if (!this.wins) { this.loses = true; game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LOST, null, null, playerId)); game.informPlayers(this.getLogName() + " has lost the game."); } else { logger.debug(this.getName() + " has already won - stop lost"); } // for draw - first all players that have lost have to be set to lost if (!hasLeft()) { logger.debug("Game over playerId: " + playerId); game.setConcedingPlayer(playerId); } } @Override public boolean canLose(Game game) { return hasLeft() // If a player concedes or has left the match they lose also if effect would say otherwise || !game.replaceEvent(new GameEvent(GameEvent.EventType.LOSES, null, null, playerId)); } @Override public void won(Game game) { boolean opponentInGame = false; for (UUID opponentId : game.getOpponents(playerId)) { Player opponent = game.getPlayer(opponentId); if (opponent != null && opponent.isInGame()) { opponentInGame = true; break; } } if (!opponentInGame || // if no more opponent is in game the wins event may no longer be replaced !game.replaceEvent(new GameEvent(GameEvent.EventType.WINS, null, null, playerId))) { logger.debug("player won -> start: " + this.getName()); if (!this.loses) { //20130501 - 800.7, 801.16 // all opponents in range loose the game for (UUID opponentId : game.getOpponents(playerId)) { Player opponent = game.getPlayer(opponentId); if (opponent != null && !opponent.hasLost()) { logger.debug("player won -> calling opponent lost: " + this.getName() + " opponent: " + opponent.getName()); opponent.lostForced(game); } } // if no more opponents alive (independant from range), you win and the game ends int opponentsAlive = 0; for (UUID playerIdToCheck : game.getPlayerList()) { if (game.isOpponent(this, playerIdToCheck)) { // Check without range Player opponent = game.getPlayer(playerIdToCheck); if (opponent != null && !opponent.hasLost()) { opponentsAlive++; } } } if (opponentsAlive == 0 && !hasWon()) { logger.debug("player won -> No more opponents alive game won: " + this.getName()); game.informPlayers(this.getLogName() + " has won the game"); this.wins = true; game.end(); } } else { logger.debug("player won -> but already lost before or other players still alive: " + this.getName()); } } } @Override public void drew(Game game) { if (!hasLost()) { this.draws = true; game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); game.informPlayers("For " + this.getLogName() + " the game is a draw."); game.setConcedingPlayer(playerId); } } @Override public boolean hasLost() { return this.loses; } @Override public boolean isInGame() { return !hasQuit() && !hasLost() && !hasWon() && !hasDrew() && !hasLeft(); } @Override public boolean canRespond() { // abort is checked here to get out of player requests (as example: after disconnect) return isInGame() && !abort; } @Override public boolean hasWon() { return !this.loses && this.wins; } @Override public boolean hasDrew() { return this.draws; } @Override public void declareAttacker(UUID attackerId, UUID defenderId, Game game, boolean allowUndo) { if (allowUndo) { setStoredBookmark(game.bookmarkState()); // makes it possible to UNDO a declared attacker with costs from e.g. Propaganda } Permanent attacker = game.getPermanent(attackerId); if (attacker != null && attacker.canAttack(defenderId, game) && attacker.isControlledBy(playerId)) { if (!game.getCombat().declareAttacker(attackerId, defenderId, playerId, game)) { game.undo(playerId); } } } @Override public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game) { declareBlocker(defenderId, blockerId, attackerId, game, true); } @Override public void declareBlocker(UUID defenderId, UUID blockerId, UUID attackerId, Game game, boolean allowUndo) { if (isHuman() && allowUndo) { setStoredBookmark(game.bookmarkState()); } Permanent blocker = game.getPermanent(blockerId); CombatGroup group = game.getCombat().findGroup(attackerId); if (blocker != null && group != null && group.canBlock(blocker, game)) { group.addBlocker(blockerId, playerId, game); game.getCombat().addBlockingGroup(blockerId, attackerId, playerId, game); } else if (this.isHuman() && !game.isSimulation()) { game.informPlayer(this, "You can't block this creature."); } } @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game) { return searchLibrary(target, source, game, playerId, true); } @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, boolean triggerEvents) { return searchLibrary(target, source, game, playerId, triggerEvents); } @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { return searchLibrary(target, source, game, targetPlayerId, true); } @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId, boolean triggerEvents) { //20091005 - 701.14c Library searchedLibrary = null; String searchInfo = null; if (targetPlayerId.equals(playerId)) { searchInfo = getLogName() + " searches their library"; searchedLibrary = library; } else { Player targetPlayer = game.getPlayer(targetPlayerId); if (targetPlayer != null) { searchInfo = getLogName() + " searches the library of " + targetPlayer.getLogName(); searchedLibrary = targetPlayer.getLibrary(); } } if (searchedLibrary == null) { return false; } GameEvent event = GameEvent.getEvent(GameEvent.EventType.SEARCH_LIBRARY, targetPlayerId, source.getSourceId(), playerId, Integer.MAX_VALUE); if (!game.replaceEvent(event)) { if (!game.isSimulation()) { game.informPlayers(searchInfo); } TargetCardInLibrary newTarget = target.copy(); int count; int librarySearchLimit = event.getAmount(); List cardsFromTop = null; do { // TODO: prevent shuffling from moving the visualized cards if (librarySearchLimit == Integer.MAX_VALUE) { count = searchedLibrary.count(target.getFilter(), game); } else { Player targetPlayer = game.getPlayer(targetPlayerId); if (targetPlayer == null) { return false; } if (cardsFromTop == null) { cardsFromTop = new ArrayList<>(targetPlayer.getLibrary().getTopCards(game, librarySearchLimit)); } else { cardsFromTop.retainAll(targetPlayer.getLibrary().getCards(game)); } newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); count = Math.min(searchedLibrary.count(target.getFilter(), game), librarySearchLimit); } if (count < target.getNumberOfTargets()) { newTarget.setMinNumberOfTargets(count); } if (newTarget.choose(Outcome.Neutral, playerId, targetPlayerId, game)) { if (targetPlayerId.equals(playerId) && handleLibraryCastableCards(library, game, targetPlayerId)) { // for handling Panglacial Wurm newTarget.clearChosen(); continue; } target.getTargets().clear(); for (UUID targetId : newTarget.getTargets()) { target.add(targetId, game); } } else if (targetPlayerId.equals(playerId) && handleLibraryCastableCards(library, game, targetPlayerId)) { // for handling Panglacial Wurm newTarget.clearChosen(); continue; } if (triggerEvents) { game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SEARCHED, targetPlayerId, playerId)); } break; } while (true); return true; } return false; } @Override public void lookAtAllLibraries(Ability source, Game game) { for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { Player player = game.getPlayer(playerId); String playerName = this.getName().equals(player.getName()) ? "Your " : player.getName() + "'s "; playerName += "library"; Cards cardsInLibrary = new CardsImpl(player.getLibrary().getTopCards(game, player.getLibrary().size())); lookAtCards(playerName, cardsInLibrary, game); } } private boolean handleLibraryCastableCards(Library library, Game game, UUID targetPlayerId) { // for handling Panglacial Wurm boolean alreadyChosenUse = false; Map libraryCastableCardTracker = new HashMap<>(); searchForCards: do { for (Card card : library.getCards(game)) { for (Ability ability : card.getAbilities()) { if (ability.getClass() == WhileSearchingPlayFromLibraryAbility.class) { libraryCastableCardTracker.put(card.getId(), card.getIdName()); } } } if (!libraryCastableCardTracker.isEmpty()) { Player player = game.getPlayer(targetPlayerId); if (player != null) { if (player.isHuman() && (alreadyChosenUse || player.chooseUse(Outcome.AIDontUseIt, "Cast a creature card from your library? (choose \"No\" to finish search)", null, game))) { ChoiceImpl chooseCard = new ChoiceImpl(); chooseCard.setMessage("Which creature do you wish to cast from your library?"); Set choice = new LinkedHashSet<>(); for (Entry entry : libraryCastableCardTracker.entrySet()) { choice.add(new AbstractMap.SimpleEntry<>(entry).getValue()); } chooseCard.setChoices(choice); while (!choice.isEmpty()) { if (player.choose(Outcome.AIDontUseIt, chooseCard, game)) { String chosenCard = chooseCard.getChoice(); for (Entry entry : libraryCastableCardTracker.entrySet()) { if (chosenCard.equals(entry.getValue())) { Card card = game.getCard(entry.getKey()); if (card != null) { // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) player.cast(card.getSpellAbility(), game, false, null); } chooseCard.clearChoice(); libraryCastableCardTracker.clear(); alreadyChosenUse = true; continue searchForCards; } } continue; } break; } return true; } } } break; } while (alreadyChosenUse); return alreadyChosenUse; } @Override public boolean flipCoin(Ability source, Game game, boolean winnable) { return this.flipCoin(source, game, winnable, null); } /** * @param source * @param game * @param winnable * @param appliedEffects * @return if winnable, true if player won the toss, if not winnable, true * for heads and false for tails */ @Override public boolean flipCoin(Ability source, Game game, boolean winnable, List appliedEffects) { boolean chosen = false; if (winnable) { chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); game.informPlayers(getLogName() + " chose " + CardUtil.booleanToFlipName(chosen)); } boolean result = RandomUtil.nextBoolean(); FlipCoinEvent event = new FlipCoinEvent(playerId, source.getSourceId(), result, chosen, winnable); event.addAppliedEffects(appliedEffects); game.replaceEvent(event); game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult())); if (event.getFlipCount() > 1) { boolean canChooseHeads = event.getResult(); boolean canChooseTails = !event.getResult(); for (int i = 1; i < event.getFlipCount(); i++) { boolean tempFlip = RandomUtil.nextBoolean(); canChooseHeads = canChooseHeads || tempFlip; canChooseTails = canChooseTails || !tempFlip; game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(tempFlip)); } if (canChooseHeads && canChooseTails) { event.setResult(chooseUse(Outcome.Benefit, "Choose which flip to keep", (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), "Heads", "Tails", source, game )); } else { event.setResult(canChooseHeads); } game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); } if (event.isWinnable()) { game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip"); } event.setAppliedEffects(appliedEffects); game.fireEvent(event.getFlippedEvent()); if (event.isWinnable()) { return event.getResult() == event.getChosen(); } return event.getResult(); } @Override public int rollDice(Game game, int numSides) { return this.rollDice(game, null, numSides); } /** * @param game * @param appliedEffects * @param numSides Number of sides the dice has * @return the number that the player rolled */ @Override public int rollDice(Game game, List appliedEffects, int numSides) { int result = RandomUtil.nextInt(numSides) + 1; if (!game.isSimulation()) { game.informPlayers("[Roll a die] " + getLogName() + " rolled a " + result + " on a " + numSides + " sided die"); } GameEvent event = new GameEvent(GameEvent.EventType.ROLL_DICE, playerId, null, playerId, result, true); event.setAppliedEffects(appliedEffects); event.setAmount(result); event.setData(numSides + ""); if (!game.replaceEvent(event)) { GameEvent ge = new GameEvent(GameEvent.EventType.DICE_ROLLED, playerId, null, playerId, event.getAmount(), event.getFlag()); ge.setData(numSides + ""); game.fireEvent(ge); } return event.getAmount(); } @Override public PlanarDieRoll rollPlanarDie(Game game) { return this.rollPlanarDie(game, null); } @Override public PlanarDieRoll rollPlanarDie(Game game, List appliedEffects) { return rollPlanarDie(game, appliedEffects, 2, 2); } /** * @param game * @param appliedEffects * @param numberChaosSides The number of chaos sides the planar die * currently has (normally 1 but can be 5) * @param numberPlanarSides The number of chaos sides the planar die * currently has (normally 1) * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll * or NilRoll */ @Override public PlanarDieRoll rollPlanarDie(Game game, List appliedEffects, int numberChaosSides, int numberPlanarSides) { int result = RandomUtil.nextInt(9) + 1; PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; if (numberChaosSides + numberPlanarSides > 9) { numberChaosSides = 2; numberPlanarSides = 2; } if (result <= numberChaosSides) { roll = PlanarDieRoll.CHAOS_ROLL; } else if (result > 9 - numberPlanarSides) { roll = PlanarDieRoll.PLANAR_ROLL; } if (!game.isSimulation()) { game.informPlayers("[Roll the planar die] " + getLogName() + " rolled a " + roll + " on the planar die"); } GameEvent event = new GameEvent(GameEvent.EventType.ROLL_PLANAR_DIE, playerId, null, playerId, result, true); event.setAppliedEffects(appliedEffects); event.setData(roll + ""); if (!game.replaceEvent(event)) { GameEvent ge = new GameEvent(GameEvent.EventType.PLANAR_DIE_ROLLED, playerId, null, playerId, event.getAmount(), event.getFlag()); ge.setData(roll + ""); game.fireEvent(ge); } return roll; } @Override public List getAvailableAttackers(Game game) { // TODO: get available opponents and their planeswalkers, check for each if permanent can attack one return getAvailableAttackers(null, game); } @Override public List getAvailableAttackers(UUID defenderId, Game game) { FilterCreatureForCombat filter = new FilterCreatureForCombat(); List attackers = game.getBattlefield().getAllActivePermanents(filter, playerId, game); attackers.removeIf(entry -> !entry.canAttack(defenderId, game)); return attackers; } @Override public List getAvailableBlockers(Game game) { FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); return game.getBattlefield().getAllActivePermanents(blockFilter, playerId, game); } /** * Returns the mana options the player currently has. That means which * combinations of mana are available to cast spells or activate abilities * etc. * * @param game * @return */ @Override public ManaOptions getManaAvailable(Game game) { boolean oldState = game.inCheckPlayableState(); game.setCheckPlayableState(true); ManaOptions availableMana = new ManaOptions(); availableMana.addMana(manaPool.getMana()); // conditional mana for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { availableMana.addMana(conditionalMana); } List> sourceWithoutManaCosts = new ArrayList<>(); List> sourceWithCosts = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { Boolean canUse = null; boolean canAdd = false; boolean withCost = false; Abilities manaAbilities = permanent.getAbilities().getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, game); for (Iterator it = manaAbilities.iterator(); it.hasNext();) { ActivatedManaAbilityImpl ability = it.next(); if (canUse == null) { canUse = permanent.canUseActivatedAbilities(game); } if (canUse && ability.canActivate(playerId, game).canActivate()) { // abilities without Tap costs have to be handled as separate sources, because they can be used also if (!availableMana.hasTapCost(ability)) { it.remove(); Abilities noTapAbilities = new AbilitiesImpl<>(ability); if (ability.getManaCosts().isEmpty()) { sourceWithoutManaCosts.add(noTapAbilities); } else { sourceWithCosts.add(noTapAbilities); } continue; } canAdd = true; if (!ability.getManaCosts().isEmpty()) { withCost = true; break; } } } if (canAdd) { if (withCost) { sourceWithCosts.add(manaAbilities); } else { sourceWithoutManaCosts.add(manaAbilities); } } } for (Abilities manaAbilities : sourceWithoutManaCosts) { availableMana.addMana(manaAbilities, game); } for (Abilities manaAbilities : sourceWithCosts) { availableMana.removeDuplicated(); availableMana.addManaWithCost(manaAbilities, game); } // remove duplicated variants (see ManaOptionsTest for info - when that rises) availableMana.removeDuplicated(); game.setCheckPlayableState(oldState); return availableMana; } /** * Used during calculation of available mana to gather the amount of * producable triggered mana caused by using mana sources. So the set value * is only used during the calculation of the mana produced by one source * and cleared thereafter * * @param netManaAvailable the net mana produced by the triggered mana * abaility */ @Override public void addAvailableTriggeredMana(List netManaAvailable) { this.availableTriggeredManaList.add(netManaAvailable); } /** * Used during calculation of available mana to get the amount of producable * triggered mana caused by using mana sources. The list is cleared as soon * the value is retrieved during available mana calculation. * * @return */ @Override public List> getAvailableTriggeredMana() { return availableTriggeredManaList; } // returns only mana producers that don't require mana payment protected List getAvailableManaProducers(Game game) { List result = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { Boolean canUse = null; boolean canAdd = false; for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { if (!ability.getManaCosts().isEmpty()) { canAdd = false; break; } if (canUse == null) { canUse = permanent.canUseActivatedAbilities(game); } if (canUse && ability.canActivate(playerId, game).canActivate()) { canAdd = true; } } if (canAdd) { result.add(permanent); } } for (Card card : getHand().getCards(game)) { boolean canAdd = false; for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.HAND)) { if (!ability.getManaCosts().isEmpty()) { canAdd = false; break; } if (ability.canActivate(playerId, game).canActivate()) { canAdd = true; } } if (canAdd) { result.add(card); } } return result; } // returns only mana producers that require mana payment public List getAvailableManaProducersWithCost(Game game) { List result = new ArrayList<>(); for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { Boolean canUse = null; for (ActivatedManaAbilityImpl ability : permanent.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { if (canUse == null) { canUse = permanent.canUseActivatedAbilities(game); } if (canUse && ability.canActivate(playerId, game).canActivate() && !ability.getManaCosts().isEmpty()) { result.add(permanent); break; } } } return result; } /** * @param ability * @param availableMana if null, it won't be checked if enough mana is * available * @param sourceObject * @param game * @return */ protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { if (!(ability instanceof ActivatedManaAbilityImpl)) { ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability if (!copy.canActivate(playerId, game).canActivate()) { return false; } if (availableMana != null) { sourceObject.adjustCosts(copy, game); game.getContinuousEffects().costModification(copy, game); } boolean canBeCastRegularly = true; if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... // 117.6a (...) If an alternative cost is applied to an unpayable cost, // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. canBeCastRegularly = false; } if (canBeCastRegularly) { if (canPayMinimumManaCost(copy, availableMana, game)) { return true; } } // ALTERNATIVE COST from source card (AlternativeCostSourceAbility) for (Ability objectAbility : sourceObject.getAbilities()) { if (objectAbility instanceof AlternativeCostSourceAbility) { if (objectAbility.getCosts().canPay(copy, copy.getSourceId(), playerId, game)) { return true; } } } // ALTERNATIVE COST FROM dynamic effects if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); boolean canPutToPlay = true; if (alternateCosts != null && !alternateCosts.canPay(copy, copy.getSourceId(), playerId, game)) { canPutToPlay = false; } if (costs != null && !costs.canPay(copy, copy.getSourceId(), playerId, game)) { canPutToPlay = false; } if (canPutToPlay) { return true; } } // ALTERNATIVE COST from source card (any AlternativeSourceCosts) return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); } return false; } protected boolean canPayMinimumManaCost(ActivatedAbility ability, ManaOptions availableMana, Game game) { ManaOptions abilityOptions = ability.getMinimumCostToActivate(playerId, game); if (abilityOptions.isEmpty()) { return true; } else { if (availableMana == null) { return true; } MageObjectReference permittingObject = game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); for (Mana mana : abilityOptions) { for (Mana avail : availableMana) { // TODO: SPEND_OTHER_MANA effects with getAsThoughManaType can change mana type to pay, // but that code processing it as any color, need to test and fix another use cases // (example: Sunglasses of Urza - may spend white mana as though it were red mana) // // add tests for non any color like Sunglasses of Urza if (permittingObject != null && mana.count() <= avail.count()) { return true; } if (mana.enough(avail)) { // here we need to check if spend mana as though allow to pay the mana cost return true; } } } } return false; } protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { if (sourceObject != null && !(sourceObject instanceof Permanent)) { Ability copyAbility; // for alternative cost and reduce tries for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) { // if cast for noMana no Alternative costs are allowed if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { if (alternateSourceCostsAbility.getCosts().canPay(ability, playerId, playerId, game)) { ManaCostsImpl manaCosts = new ManaCostsImpl(); for (Cost cost : alternateSourceCostsAbility.getCosts()) { if (cost instanceof ManaCost) { manaCosts.add((ManaCost) cost); } } if (manaCosts.isEmpty()) { return true; } else { if (availableMana == null) { return true; } // alternative cost reduce copyAbility = ability.copy(); copyAbility.getManaCostsToPay().clear(); copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); sourceObject.adjustCosts(copyAbility, game); game.getContinuousEffects().costModification(copyAbility, game); for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { if (availableMana.enough(mana)) { return true; } } } } } } } // controller specific alternate spell costs for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { if (alternateSourceCosts instanceof Ability) { if (alternateSourceCosts.isAvailable(ability, game)) { if (((Ability) alternateSourceCosts).getCosts().canPay(ability, playerId, playerId, game)) { ManaCostsImpl manaCosts = new ManaCostsImpl(); for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { if (cost instanceof ManaCost) { manaCosts.add((ManaCost) cost); } } if (manaCosts.isEmpty()) { return true; } else { if (availableMana == null) { return true; } // alternative cost reduce copyAbility = ability.copy(); copyAbility.getManaCostsToPay().clear(); copyAbility.getManaCostsToPay().addAll(manaCosts.copy()); sourceObject.adjustCosts(copyAbility, game); game.getContinuousEffects().costModification(copyAbility, game); for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { if (availableMana.enough(mana)) { return true; } } } } } } } } return false; } protected ActivatedAbility findActivatedAbilityFromPlayable(MageObject object, ManaOptions availableMana, Ability ability, Game game) { // special mana to pay spell cost ManaOptions manaFull = availableMana.copy(); if (ability instanceof SpellAbility) { for (AlternateManaPaymentAbility altAbility : CardUtil.getAbilities(object, game).stream() .filter(a -> a instanceof AlternateManaPaymentAbility) .map(a -> (AlternateManaPaymentAbility) a) .collect(Collectors.toList())) { ManaOptions manaSpecial = altAbility.getManaOptions(ability, game, ability.getManaCostsToPay()); manaFull.addMana(manaSpecial); } } // replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land) if (ability instanceof ActivatedManaAbilityImpl) { // mana ability if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) { return (ActivatedManaAbilityImpl) ability; } } else if (ability instanceof AlternativeSourceCosts) { // alternative cost must be replaced by real play ability return findActivatedAbilityFromAlternativeSourceCost(object, manaFull, ability, game); } else if (ability instanceof ActivatedAbility) { // all other activated ability if (canPlay((ActivatedAbility) ability, manaFull, object, game)) { return (ActivatedAbility) ability; } } // non playable abilities like static return null; } protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions availableMana, Ability ability, Game game) { // return play ability that can activate AlternativeSourceCosts if (ability instanceof AlternativeSourceCosts && object != null && !(object instanceof Permanent)) { ActivatedAbility playAbility = null; if (object.isLand()) { playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); } else if (object instanceof Card) { playAbility = ((Card) object).getSpellAbility(); } if (playAbility == null) { return null; } // 707.4.Objects that are cast face down are turned face down before they are put onto the stack // E.g. no lands per turn limit, no cast restrictions, cost reduce, etc // Even mana cost can't be checked here without lookahead // So make it available all the time boolean canUse; if (ability instanceof MorphAbility && object instanceof Card && game.canPlaySorcery(getId())) { canUse = canPlayCardByAlternateCost((Card) object, availableMana, ability, game); } else { canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions } if (canUse) { return playAbility; } } return null; } private void getPlayableFromObjectAll(Game game, Zone fromZone, MageObject object, ManaOptions availableMana, List output) { if (fromZone == null || object == null) { return; } // BASIC abilities if (object instanceof SplitCard) { SplitCard splitCard = (SplitCard) object; getPlayableFromObjectSingle(game, fromZone, splitCard.getLeftHalfCard(), splitCard.getLeftHalfCard().getAbilities(game), availableMana, output); getPlayableFromObjectSingle(game, fromZone, splitCard.getRightHalfCard(), splitCard.getRightHalfCard().getAbilities(game), availableMana, output); getPlayableFromObjectSingle(game, fromZone, splitCard, splitCard.getSharedAbilities(game), availableMana, output); } else if (object instanceof AdventureCard) { // adventure must use different card characteristics for different spells (main or adventure) AdventureCard adventureCard = (AdventureCard) object; getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); } else if (object instanceof Card) { getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); } else { // other things like StackObject or CommandObject getPlayableFromObjectSingle(game, fromZone, object, object.getAbilities(), availableMana, output); } // DYNAMIC ADDED abilities are adds in getAbilities(game) } private void getPlayableFromObjectSingle(Game game, Zone fromZone, MageObject object, Abilities candidateAbilities, ManaOptions availableMana, List output) { // check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller) // must check all abilities, not activated only for (Ability ability : candidateAbilities) { if (!(ability instanceof ActivatedAbility)) { continue; } boolean isPlaySpell = (ability instanceof SpellAbility); boolean isPlayLand = (ability instanceof PlayLandAbility); // as original controller // play land restrictions if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), ability.getSourceId(), this.getId()), ability, game, true)) { continue; } // cast spell restrictions 1 if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( GameEvent.getEvent(EventType.CAST_SPELL, ability.getSourceId(), ability.getSourceId(), this.getId()), ability, game, true)) { continue; } // cast spell restrictions 2 if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( GameEvent.getEvent(EventType.CAST_SPELL_LATE, ability.getSourceId(), ability.getSourceId(), this.getId()), ability, game, true)) { continue; } MageObjectReference permittingObject; if (isPlaySpell || isPlayLand) { // play hand from non hand zone permittingObject = game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); } else { // other abilities from direct zones permittingObject = null; } boolean canActivateAsHandZone = permittingObject != null || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); boolean possibleToPlay = false; // spell/hand abilities (play from all zones) // need permitingObject or canPlayCardsFromGraveyard if (canActivateAsHandZone && ability.getZone().match(Zone.HAND) && (isPlaySpell || isPlayLand)) { possibleToPlay = true; } // zone's abilities (play from specific zone) // no need in permitingObject if (fromZone != Zone.ALL && ability.getZone().match(fromZone)) { possibleToPlay = true; } if (!possibleToPlay) { continue; } // direct mode (with original controller) ActivatedAbility playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); if (playAbility != null && !output.contains(playAbility)) { output.add(playAbility); continue; } // from non hand mode (with affected controller) if (canActivateAsHandZone && ability.getControllerId() != this.getId()) { UUID savedControllerId = ability.getControllerId(); ability.setControllerId(this.getId()); try { playAbility = findActivatedAbilityFromPlayable(object, availableMana, ability, game); if (playAbility != null && !output.contains(playAbility)) { output.add(playAbility); } } finally { ability.setControllerId(savedControllerId); } } } } @Override public List getPlayable(Game game, boolean hidden) { return getPlayable(game, hidden, Zone.ALL, true); } /** * Returns a list of all available spells and abilities the player can * currently cast/activate with his available ressources * * @param game * @param hidden also from hidden objects (e.g. turned face * down cards ?) * @param fromZone of objects from which zone (ALL = from all * zones) * @param hideDuplicatedAbilities if equal abilities exist return only the * first instance * @return */ public List getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { List playable = new ArrayList<>(); if (shouldSkipGettingPlayable(game)) { return playable; } boolean previousState = game.inCheckPlayableState(); game.setCheckPlayableState(true); try { // basic mana ManaOptions availableMana = getManaAvailable(game); // availableMana.addMana(manaPool.getMana()); // conditional mana // for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { // availableMana.addMana(conditionalMana); // } boolean fromAll = fromZone.equals(Zone.ALL); if (hidden && (fromAll || fromZone == Zone.HAND)) { for (Card card : hand.getCards(game)) { for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) if (ability.getZone().match(Zone.HAND)) { boolean isPlaySpell = (ability instanceof SpellAbility); boolean isPlayLand = (ability instanceof PlayLandAbility); // play land restrictions if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), ability.getSourceId(), this.getId()), ability, game, true)) { continue; } // cast spell restrictions 1 if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( GameEvent.getEvent(EventType.CAST_SPELL, ability.getSourceId(), ability.getSourceId(), this.getId()), ability, game, true)) { continue; } // cast spell restrictions 2 if (isPlaySpell && game.getContinuousEffects().preventedByRuleModification( GameEvent.getEvent(EventType.CAST_SPELL_LATE, ability.getSourceId(), ability.getSourceId(), this.getId()), ability, game, true)) { continue; } ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game); if (playAbility != null && !playable.contains(playAbility)) { playable.add(playAbility); } } } } } if (fromAll || fromZone == Zone.GRAVEYARD) { for (Card card : graveyard.getCards(game)) { getPlayableFromObjectAll(game, Zone.GRAVEYARD, card, availableMana, playable); } } if (fromAll || fromZone == Zone.EXILED) { for (ExileZone exile : game.getExile().getExileZones()) { for (Card card : exile.getCards(game)) { getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); } } } // check to play revealed cards if (fromAll) { for (Cards revealedCards : game.getState().getRevealed().values()) { for (Card card : revealedCards.getCards(game)) { // revealed cards can be from any zones getPlayableFromObjectAll(game, game.getState().getZone(card.getId()), card, availableMana, playable); } } } // check to play companion cards if (fromAll || fromZone == Zone.OUTSIDE) { for (Cards companionCards : game.getState().getCompanion().values()) { for (Card card : companionCards.getCards(game)) { getPlayableFromObjectAll(game, Zone.OUTSIDE, card, availableMana, playable); } } } // check if it's possible to play the top card of a library if (fromAll || fromZone == Zone.LIBRARY) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { Player player = game.getPlayer(playerInRangeId); if (player != null && player.getLibrary().hasCards()) { Card card = player.getLibrary().getFromTop(game); if (card != null) { getPlayableFromObjectAll(game, Zone.LIBRARY, card, availableMana, playable); } } } } // eliminate duplicate activated abilities (uses for AI plays) Map activatedUnique = new HashMap<>(); List activatedAll = new ArrayList<>(); // activated abilities from battlefield objects if (fromAll || fromZone == Zone.BATTLEFIELD) { for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { boolean canUseActivated = permanent.canUseActivatedAbilities(game); List currentPlayable = new ArrayList<>(); getPlayableFromObjectAll(game, Zone.BATTLEFIELD, permanent, availableMana, currentPlayable); for (ActivatedAbility ability : currentPlayable) { if (ability instanceof SpecialAction || canUseActivated) { activatedUnique.putIfAbsent(ability.toString(), ability); activatedAll.add(ability); } } } } // activated abilities from stack objects if (fromAll || fromZone == Zone.STACK) { for (StackObject stackObject : game.getState().getStack()) { List currentPlayable = new ArrayList<>(); getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); for (ActivatedAbility ability : currentPlayable) { activatedUnique.put(ability.toString(), ability); activatedAll.add(ability); } } } // activated abilities from objects in the command zone (emblems or commanders) if (fromAll || fromZone == Zone.COMMAND) { for (CommandObject commandObject : game.getState().getCommand()) { List currentPlayable = new ArrayList<>(); getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); for (ActivatedAbility ability : currentPlayable) { activatedUnique.put(ability.toString(), ability); activatedAll.add(ability); } } } if (hideDuplicatedAbilities) { playable.addAll(activatedUnique.values()); } else { playable.addAll(activatedAll); } } finally { game.setCheckPlayableState(previousState); } return playable; } /** * Creates a list of card ids that are currently playable.
* Used to mark the playable cards in GameView Also contains number of * playable abilities for that object (it's just info, server decides to * show choose dialog or not) * * @param game * @return A Set of cardIds that are playable and amount of playable * abilities */ @Override public Map getPlayableObjects(Game game, Zone zone) { List playableAbilities = getPlayable(game, true, zone, false); // do not hide duplicated abilities/cards Map playableObjects = new HashMap<>(); for (Ability ability : playableAbilities) { if (ability.getSourceId() != null) { playableObjects.put(ability.getSourceId(), playableObjects.getOrDefault(ability.getSourceId(), 0) + 1); // main card must be marked playable in GUI Card card = game.getCard(ability.getSourceId()); if (card != null && card.getMainCard().getId() != card.getId()) { UUID mainCardId = card.getMainCard().getId(); playableObjects.put(mainCardId, playableObjects.getOrDefault(mainCardId, 0) + 1); } } } return playableObjects; } /** * Skip "silent" phase step when players are not allowed to cast anything. * E.g. players can't play or cast anything during declaring attackers. * * @param game * @return */ private boolean shouldSkipGettingPlayable(Game game) { if (game.getStep() == null) { // happens at the start of the game return true; } for (Entry phaseStep : silentPhaseSteps.entrySet()) { if (game.getPhase() != null && game.getPhase().getStep() != null && phaseStep.getKey() == game.getPhase().getStep().getType()) { if (phaseStep.getValue() == null || phaseStep.getValue() == game.getPhase().getStep().getStepPart()) { return true; } } } return false; } /** * Only used for AIs * * @param ability * @param game * @return */ @Override public List getPlayableOptions(Ability ability, Game game) { List options = new ArrayList<>(); if (ability.isModal()) { addModeOptions(options, ability, game); } else if (!ability.getTargets().getUnchosen().isEmpty()) { // TODO: Handle other variable costs than mana costs if (!ability.getManaCosts().getVariableCosts().isEmpty()) { addVariableXOptions(options, ability, 0, game); } else { addTargetOptions(options, ability, 0, game); } } else if (!ability.getCosts().getTargets().getUnchosen().isEmpty()) { addCostTargetOptions(options, ability, 0, game); } return options; } private void addModeOptions(List options, Ability option, Game game) { // TODO: Support modal spells with more than one selectable mode for (Mode mode : option.getModes().values()) { Ability newOption = option.copy(); newOption.getModes().clearSelectedModes(); newOption.getModes().addSelectedMode(mode.getId()); newOption.getModes().setActiveMode(mode); if (!newOption.getTargets().getUnchosen().isEmpty()) { if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { addVariableXOptions(options, newOption, 0, game); } else { addTargetOptions(options, newOption, 0, game); } } else if (!newOption.getCosts().getTargets().getUnchosen().isEmpty()) { addCostTargetOptions(options, newOption, 0, game); } else { options.add(newOption); } } } protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { addTargetOptions(options, option, targetNum, game); } protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { for (Target target : option.getTargets().getUnchosen().get(targetNum).getTargetOptions(option, game)) { Ability newOption = option.copy(); if (target instanceof TargetAmount) { for (UUID targetId : target.getTargets()) { int amount = target.getTargetAmount(targetId); newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); } } else { for (UUID targetId : target.getTargets()) { newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); } } if (targetNum < option.getTargets().size() - 2) { addTargetOptions(options, newOption, targetNum + 1, game); } else if (!option.getCosts().getTargets().isEmpty()) { addCostTargetOptions(options, newOption, 0, game); } else { options.add(newOption); } } } private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(option.getSourceId(), playerId, game)) { Ability newOption = option.copy(); newOption.getCosts().getTargets().get(targetNum).addTarget(targetId, option, game, true); if (targetNum < option.getCosts().getTargets().size() - 1) { addCostTargetOptions(options, newOption, targetNum + 1, game); } else { options.add(newOption); } } } @Override public boolean isTestMode() { return isTestMode; } @Override public void setTestMode(boolean value) { this.isTestMode = value; } @Override public boolean isTopCardRevealed() { return topCardRevealed; } @Override public void setTopCardRevealed(boolean topCardRevealed) { this.topCardRevealed = topCardRevealed; } @Override public UserData getUserData() { return this.userData; } public UserData getControllingPlayersUserData(Game game) { if (!isGameUnderControl()) { Player player = game.getPlayer(getTurnControlledBy()); if (player.isHuman()) { return player.getUserData(); } } return this.userData; } @Override public void setUserData(UserData userData) { this.userData = userData; getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); } @Override public void addAction(String action ) { // do nothing } @Override public int getActionCount() { return 0; } @Override public void setAllowBadMoves(boolean allowBadMoves ) { // do nothing } @Override public boolean canPayLifeCost() { return isLifeTotalCanChange() && canPayLifeCost; } @Override public void setCanPayLifeCost(boolean canPayLifeCost ) { this.canPayLifeCost = canPayLifeCost; } @Override public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, UUID controllerId, Game game ) { return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); } @Override public void setCanPaySacrificeCostFilter(FilterPermanent filter ) { this.sacrificeCostFilter = filter; } @Override public FilterPermanent getSacrificeCostFilter() { return sacrificeCostFilter; } @Override public boolean canLoseByZeroOrLessLife() { return loseByZeroOrLessLife; } @Override public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife ) { this.loseByZeroOrLessLife = loseByZeroOrLessLife; } @Override public boolean canPlayCardsFromGraveyard() { return canPlayCardsFromGraveyard; } @Override public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard ) { this.canPlayCardsFromGraveyard = playCardsFromGraveyard; } @Override public boolean autoLoseGame() { return false; } @Override public void becomesActivePlayer() { this.passedAllTurns = false; this.passedUntilEndStepBeforeMyTurn = false; this.turns++; } @Override public int getTurns() { return turns; } @Override public int getStoredBookmark() { return storedBookmark; } @Override public void setStoredBookmark(int storedBookmark) { this.storedBookmark = storedBookmark; } @Override public synchronized void resetStoredBookmark(Game game ) { if (this.storedBookmark != -1) { game.removeBookmark(this.storedBookmark); } setStoredBookmark(-1); } @Override public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { if (null != game.getContinuousEffects().asThough(card.getId(), AsThoughEffectType.LOOK_AT_FACE_DOWN, card.getSpellAbility(), this.getId(), game)) { // two modes: look at the card or do not look and activate other abilities String lookMessage = "Look at " + card.getIdName(); String lookYes = "Yes, look at the card"; String lookNo = "No, play/activate the card/ability"; if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { Cards cards = new CardsImpl(card); this.lookAtCards(getName() + " - " + card.getIdName() + " - " + CardUtil.sdf.format(System.currentTimeMillis()), cards, game); return true; } } return false; } @Override public void setPriorityTimeLeft(int timeLeft ) { priorityTimeLeft = timeLeft; } @Override public int getPriorityTimeLeft() { return priorityTimeLeft; } @Override public boolean hasQuit() { return quit; } @Override public boolean hasTimerTimeout() { return timerTimeout; } @Override public boolean hasIdleTimeout() { return idleTimeout; } @Override public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving ) { this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; } @Override public boolean hasReachedNextTurnAfterLeaving() { return reachedNextTurnAfterLeaving; } @Override public boolean canJoinTable(Table table ) { return !table.userIsBanned(name); } @Override public void addCommanderId(UUID commanderId ) { this.commandersIds.add(commanderId); } @Override public Set getCommandersIds() { return this.commandersIds; } @Override public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { return moveCards(card, toZone, source, game, false, false, false, null); } @Override public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { Set cardList = new HashSet<>(); if (card != null) { cardList.add(card); } return moveCards(cardList, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); } @Override public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game ) { return moveCards(cards.getCards(game), toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, Ability source, Game game ) { return moveCards(cards, toZone, source, game, false, false, false, null); } @Override public boolean moveCards(Set cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { if (cards.isEmpty()) { return true; } Set successfulMovedCards = new LinkedHashSet<>(); Zone fromZone = null; switch (toZone) { case GRAVEYARD: fromZone = game.getState().getZone(cards.iterator().next().getId()); successfulMovedCards = moveCardsToGraveyardWithInfo(cards, source, game, fromZone); return !successfulMovedCards.isEmpty(); case BATTLEFIELD: // new logic that does not yet add the permanents to battlefield while replacement effects are handled List infoList = new ArrayList<>(); for (Card card : cards) { fromZone = game.getState().getZone(card.getId()); ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source.getSourceId(), byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped)); } infoList = ZonesHandler.moveCards(infoList, game); for (ZoneChangeInfo info : infoList) { Permanent permanent = game.getPermanent(info.event.getTargetId()); if (permanent != null) { successfulMovedCards.add(permanent); if (!game.isSimulation()) { Player eventPlayer = game.getPlayer(info.event.getPlayerId()); if (eventPlayer != null && fromZone != null) { game.informPlayers(eventPlayer.getLogName() + " puts " + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield"); } } } } game.applyEffects(); break; case HAND: for (Card card : cards) { fromZone = game.getState().getZone(card.getId()); boolean hideCard = fromZone == Zone.LIBRARY || (card.isFaceDown(game) && fromZone != Zone.STACK && fromZone != Zone.BATTLEFIELD); if (moveCardToHandWithInfo(card, source == null ? null : source.getSourceId(), game, !hideCard)) { successfulMovedCards.add(card); } } break; case EXILED: for (Card card : cards) { fromZone = game.getState().getZone(card.getId()); boolean withName = (fromZone == Zone.BATTLEFIELD || fromZone == Zone.STACK) || !card.isFaceDown(game); if (moveCardToExileWithInfo(card, null, "", source == null ? null : source.getSourceId(), game, fromZone, withName)) { successfulMovedCards.add(card); } } break; case LIBRARY: for (Card card : cards) { if (card instanceof Spell) { fromZone = game.getState().getZone(((Spell) card).getSourceId()); } else { fromZone = game.getState().getZone(card.getId()); } boolean hideCard = fromZone == Zone.HAND || fromZone == Zone.LIBRARY; if (moveCardToLibraryWithInfo(card, source == null ? null : source.getSourceId(), game, fromZone, true, !hideCard)) { successfulMovedCards.add(card); } } break; case COMMAND: for (Card card : cards) { if (moveCardToCommandWithInfo(card, source == null ? null : source.getSourceId(), game, fromZone)) { successfulMovedCards.add(card); } } break; case OUTSIDE: for (Card card : cards) { if (card instanceof Permanent) { game.getBattlefield().removePermanent(card.getId()); ZoneChangeEvent event = new ZoneChangeEvent((Permanent) card, (source == null ? null : source.getSourceId()), byOwner ? card.getOwnerId() : getId(), Zone.BATTLEFIELD, Zone.OUTSIDE, appliedEffects); game.fireEvent(event); } } break; default: throw new UnsupportedOperationException("to Zone" + toZone.toString() + " not supported yet"); } return !successfulMovedCards.isEmpty(); } @Override public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName ) { Set cards = new HashSet<>(); cards.add(card); return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); } @Override public boolean moveCardsToExile(Set cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName ) { if (cards.isEmpty()) { return true; } boolean result = false; for (Card card : cards) { Zone fromZone = game.getState().getZone(card.getId()); result |= moveCardToExileWithInfo(card, exileId, exileZoneName, source == null ? null : source.getSourceId(), game, fromZone, withName); } return result; } @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, Game game ) { return this.moveCardToHandWithInfo(card, sourceId, game, true); } @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, Game game, boolean withName ) { boolean result = false; Zone fromZone = game.getState().getZone(card.getId()); if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { card = game.getPermanent(card.getId()); } if (card.moveToZone(Zone.HAND, sourceId, game, false)) { if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { card = game.getCard(card.getId()); } if (!game.isSimulation()) { game.informPlayers(getLogName() + " puts " + (withName ? card.getLogName() : (card.isFaceDown(game) ? "a face down card" : "a card")) + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' + (card.isOwnedBy(this.getId()) ? "into their hand" : "into its owner's hand") ); } result = true; } return result; } @Override public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, Game game, Zone fromZone ) { UUID sourceId = source == null ? null : source.getSourceId(); Set movedCards = new LinkedHashSet<>(); while (!allCards.isEmpty()) { // identify cards from one owner Cards cards = new CardsImpl(); UUID ownerId = null; for (Iterator it = allCards.iterator(); it.hasNext();) { Card card = it.next(); if (cards.isEmpty()) { ownerId = card.getOwnerId(); } if (card.isOwnedBy(ownerId)) { it.remove(); cards.add(card); } } // move cards to graveyard in order the owner decides if (!cards.isEmpty()) { Player choosingPlayer = this; if (!Objects.equals(ownerId, this.getId())) { choosingPlayer = game.getPlayer(ownerId); } if (choosingPlayer == null) { continue; } boolean chooseOrder = false; if (userData.askMoveToGraveOrder()) { if (cards.size() > 1) { chooseOrder = choosingPlayer.chooseUse(Outcome.Neutral, "Would you like to choose the order the cards go to graveyard?", source, game); } } if (chooseOrder) { TargetCard target = new TargetCard(fromZone, new FilterCard("card to put on the top of your graveyard (last one chosen will be topmost)")); target.setRequired(true); while (choosingPlayer.canRespond() && cards.size() > 1) { choosingPlayer.chooseTarget(Outcome.Neutral, cards, target, source, game); UUID targetObjectId = target.getFirstTarget(); Card card = cards.get(targetObjectId, game); cards.remove(targetObjectId); if (card != null) { fromZone = game.getState().getZone(card.getId()); if (choosingPlayer.moveCardToGraveyardWithInfo(card, sourceId, game, fromZone)) { movedCards.add(card); } } target.clearChosen(); } if (cards.size() == 1) { Card card = cards.getCards(game).iterator().next(); if (card != null && choosingPlayer.moveCardToGraveyardWithInfo(card, sourceId, game, fromZone)) { movedCards.add(card); } } } else { for (Card card : cards.getCards(game)) { if (choosingPlayer.moveCardToGraveyardWithInfo(card, sourceId, game, fromZone)) { movedCards.add(card); } } } } } return movedCards; } @Override public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, Game game, Zone fromZone ) { if (card == null) { return false; } boolean result = false; // Zone fromZone = game.getState().getZone(card.getId()); if (card.moveToZone(Zone.GRAVEYARD, sourceId, game, fromZone != null && fromZone == Zone.BATTLEFIELD)) { if (!game.isSimulation()) { if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { card = game.getCard(card.getId()); } StringBuilder sb = new StringBuilder(this.getLogName()) .append(" puts ").append(card.getLogName()).append(' ').append(card.isCopy() ? "(Copy) " : "") .append(fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : ""); if (card.isOwnedBy(getId())) { sb.append("into their graveyard"); } else { sb.append("it into its owner's graveyard"); } game.informPlayers(sb.toString()); } result = true; } return result; } @Override public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, Game game, Zone fromZone, boolean toTop, boolean withName ) { if (card == null) { return false; } boolean result = false; if (card.moveToZone(Zone.LIBRARY, sourceId, game, toTop)) { if (!game.isSimulation()) { if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { card = game.getCard(card.getId()); } StringBuilder sb = new StringBuilder(this.getLogName()) .append(" puts ").append(withName ? card.getLogName() : "a card").append(' '); if (fromZone != null) { sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); } sb.append("to the ").append(toTop ? "top" : "bottom"); if (card.isOwnedBy(getId())) { sb.append(" of their library"); } else { Player player = game.getPlayer(card.getOwnerId()); if (player != null) { sb.append(" of ").append(player.getLogName()).append("'s library"); } } game.informPlayers(sb.toString()); } result = true; } return result; } @Override public boolean moveCardToCommandWithInfo(Card card, UUID sourceId, Game game, Zone fromZone) { if (card == null) { return false; } boolean result = false; if (card.moveToZone(Zone.COMMAND, sourceId, game, true)) { if (!game.isSimulation()) { if (card instanceof PermanentCard && game.getCard(card.getId()) != null) { card = game.getCard(card.getId()); } StringBuilder sb = new StringBuilder(this.getLogName()) .append(" puts ").append(card.getLogName()).append(' '); if (fromZone != null) { sb.append("from ").append(fromZone.toString().toLowerCase(Locale.ENGLISH)).append(' '); } if (card.isOwnedBy(getId())) { sb.append(" to his or her command zone"); } else { Player player = game.getPlayer(card.getOwnerId()); if (player != null) { sb.append(" to ").append(player.getLogName()).append("'s command zone"); } } game.informPlayers(sb.toString()); } result = true; } return result; } @Override public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId, Game game, Zone fromZone, boolean withName) { if (card == null) { return false; } boolean result = false; if (card.moveToExile(exileId, exileName, sourceId, game)) { if (!game.isSimulation()) { if (card instanceof PermanentCard) { // in case it's face down or name was changed by copying from other permanent Card basicCard = game.getCard(card.getId()); if (basicCard != null) { card = basicCard; } } else if (card instanceof Spell) { final Spell spell = (Spell) card; if (spell.isCopy()) { // Copied spell, only remove from stack game.getStack().remove(spell, game); } } if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + ' ' : "") + "to the exile zone"); } } result = true; } return result; } @Override public Cards millCards(int toMill, Ability source, Game game) { GameEvent event = GameEvent.getEvent(EventType.MILL_CARDS, getId(), source.getSourceId(), getId(), toMill); if (game.replaceEvent(event)) { return new CardsImpl(); } Cards cards = new CardsImpl(this.getLibrary().getTopCards(game, event.getAmount())); this.moveCards(cards, Zone.GRAVEYARD, source, game); return cards; } @Override public boolean hasOpponent(UUID playerToCheckId, Game game) { return !this.getId().equals(playerToCheckId) && game.isOpponent(this, playerToCheckId) && getInRange().contains(playerToCheckId); } @Override public void cleanUpOnMatchEnd() { } @Override public boolean getPassedAllTurns() { return passedAllTurns; } @Override public boolean getPassedUntilNextMain() { return passedUntilNextMain; } @Override public boolean getPassedUntilEndOfTurn() { return passedUntilEndOfTurn; } @Override public boolean getPassedTurn() { return passedTurn; } @Override public boolean getPassedUntilStackResolved() { return passedUntilStackResolved; } @Override public boolean getPassedUntilEndStepBeforeMyTurn() { return passedUntilEndStepBeforeMyTurn; } @Override public AbilityType getJustActivatedType() { return justActivatedType; } @Override public void setJustActivatedType(AbilityType justActivatedType ) { this.justActivatedType = justActivatedType; } @Override public void revokePermissionToSeeHandCards() { usersAllowedToSeeHandCards.clear(); } @Override public void addPermissionToShowHandCards(UUID watcherUserId ) { usersAllowedToSeeHandCards.add(watcherUserId); } @Override public boolean isPlayerAllowedToRequestHand(UUID gameId, UUID requesterPlayerId) { return userData.isAllowRequestHandToPlayer(gameId, requesterPlayerId); } @Override public void addPlayerToRequestedHandList(UUID gameId, UUID requesterPlayerId) { userData.addPlayerToRequestedHandList(gameId, requesterPlayerId); } @Override public boolean hasUserPermissionToSeeHand(UUID userId ) { return usersAllowedToSeeHandCards.contains(userId); } @Override public Set getUsersAllowedToSeeHandCards() { return usersAllowedToSeeHandCards; } @Override public void setMatchPlayer(MatchPlayer matchPlayer ) { this.matchPlayer = matchPlayer; } @Override public MatchPlayer getMatchPlayer() { return matchPlayer; } @Override public void abortReset() { abort = false; } @Override public void signalPlayerConcede() { } @Override public boolean scry(int value, Ability source, Game game) { game.informPlayers(getLogName() + " scries " + value); Cards cards = new CardsImpl(); cards.addAll(getLibrary().getTopCards(game, value)); if (!cards.isEmpty()) { TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, new FilterCard("card" + (cards.size() == 1 ? "" : "s") + " to PUT on the BOTTOM of your library (Scry)")); chooseTarget(Outcome.Benefit, cards, target, source, game); putCardsOnBottomOfLibrary(new CardsImpl(target.getTargets()), game, source, true); cards.removeAll(target.getTargets()); putCardsOnTopOfLibrary(cards, game, source, true); } game.fireEvent(new GameEvent(GameEvent.EventType.SCRY, getId(), source == null ? null : source.getSourceId(), getId(), value, true)); return true; } @Override public boolean surveil(int value, Ability source, Game game) { GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source == null ? null : source.getSourceId(), getId(), value, true); if (game.replaceEvent(event)) { return false; } game.informPlayers(getLogName() + " surveils " + event.getAmount()); Cards cards = new CardsImpl(); cards.addAll(getLibrary().getTopCards(game, event.getAmount())); if (!cards.isEmpty()) { TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, new FilterCard("cards to PUT into your GRAVEYARD (Surveil)")); chooseTarget(Outcome.Benefit, cards, target, source, game); moveCards(new CardsImpl(target.getTargets()), Zone.GRAVEYARD, source, game); cards.removeAll(target.getTargets()); putCardsOnTopOfLibrary(cards, game, source, true); } game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source == null ? null : source.getSourceId(), getId(), event.getAmount(), true)); return true; } @Override public boolean addTargets(Ability ability, Game game ) { // only used for TestPlayer to preSet Targets return true; } @Override public String getHistory() { return "no available"; } @Override public boolean hasDesignation(DesignationType designationName) { for (Designation designation : designations) { if (designation.getDesignationType().equals(designationName)) { return true; } } return false; } @Override public void addDesignation(Designation designation) { if (!designation.isUnique() || !this.hasDesignation(designation.getDesignationType())) { designations.add(designation); } } @Override public List getDesignations() { return designations; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Player obj = (Player) o; if (this.getId() == null || obj.getId() == null) { return false; } return this.getId().equals(obj.getId()); } @Override public int hashCode() { int hash = 7; hash = 89 * hash + Objects.hashCode(this.playerId); return hash; } @Override public void addPhyrexianToColors(FilterMana colors) { if (colors.isWhite()) { this.phyrexianColors.setWhite(true); } if (colors.isBlue()) { this.phyrexianColors.setBlue(true); } if (colors.isBlack()) { this.phyrexianColors.setBlack(true); } if (colors.isRed()) { this.phyrexianColors.setRed(true); } if (colors.isGreen()) { this.phyrexianColors.setGreen(true); } } @Override public void removePhyrexianFromColors(FilterMana colors) { if (colors.isWhite()) { this.phyrexianColors.setWhite(false); } if (colors.isBlue()) { this.phyrexianColors.setBlue(false); } if (colors.isBlack()) { this.phyrexianColors.setBlack(false); } if (colors.isRed()) { this.phyrexianColors.setRed(false); } if (colors.isGreen()) { this.phyrexianColors.setGreen(false); } } @Override public FilterMana getPhyrexianColors() { return this.phyrexianColors; } @Override public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { return card.getSpellAbility(); } }