* WARNING, if you add new fields then sync it with constructor, restore, reset and init methods
*/
public abstract class PlayerImpl implements Player, Serializable {
private static final Logger logger = Logger.getLogger(PlayerImpl.class);
/**
* During some steps we can't play anything
*/
static final Map SILENT_PHASES_STEPS = ImmutableMap.builder().
put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build();
/**
* 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 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;
protected int bufferTimeLeft = 0;
// 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 due rules)
protected boolean isTestMode = false;
protected boolean canGainLife = true;
protected boolean canLoseLife = true;
protected PayLifeCostLevel payLifeCostLevel = PayLifeCostLevel.allAbilities;
protected boolean loseByZeroOrLessLife = true;
protected boolean canPlotFromTopOfLibrary = false;
protected boolean drawsFromBottom = false;
protected boolean drawsOnOpponentsTurn = false;
protected int speed = 0;
protected FilterPermanent sacrificeCostFilter;
protected List alternativeSourceCosts = new ArrayList<>();
// TODO: rework turn controller to use single list (see other todos)
//protected Stack allTurnControllers = new Stack<>();
protected boolean isGameUnderControl = true; // TODO: replace with allTurnControllers.isEmpty
protected UUID turnController; // null on own control TODO: replace with allTurnControllers.last
protected List turnControllers = new ArrayList<>(); // TODO: remove
protected Set playersUnderYourControl = new HashSet<>(); // TODO: replace with game method and search in allTurnControllers
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
//
// A card may be able to cast multiple way with multiple methods.
// The specific MageIdentifier should be checked, before checking null as a fallback.
// TODO: must rework playable methods to static
protected Map> castSourceIdWithAlternateMana = new HashMap<>();
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<>();
// mana colors the player can handle like Phyrexian mana
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<>();
protected 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 = null;
}
protected PlayerImpl(UUID id) {
this.playerId = id; // TODO: miss another fields init?
}
protected 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.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.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary;
this.drawsFromBottom = player.drawsFromBottom;
this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn;
this.speed = player.speed;
this.attachments.addAll(player.attachments);
this.inRange.addAll(player.inRange);
this.userData = player.userData;
this.matchPlayer = player.matchPlayer;
this.payLifeCostLevel = player.payLifeCostLevel;
this.sacrificeCostFilter = player.sacrificeCostFilter;
this.alternativeSourceCosts = CardUtil.deepCopyObject(player.alternativeSourceCosts);
this.storedBookmark = player.storedBookmark;
this.topCardRevealed = player.topCardRevealed;
this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards);
this.isTestMode = player.isTestMode;
this.isGameUnderControl = player.isGameUnderControl;
this.turnController = player.turnController;
this.turnControllers.addAll(player.turnControllers);
this.playersUnderYourControl.addAll(player.playersUnderYourControl);
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.bufferTimeLeft = player.getBufferTimeLeft();
this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving;
this.castSourceIdWithAlternateMana = CardUtil.deepCopyObject(player.castSourceIdWithAlternateMana);
this.castSourceIdManaCosts = CardUtil.deepCopyObject(player.castSourceIdManaCosts);
this.castSourceIdCosts = CardUtil.deepCopyObject(player.castSourceIdCosts);
this.payManaMode = player.payManaMode;
this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null;
this.designations = CardUtil.deepCopyObject(player.designations);
}
/**
* Restore on rollback
*
* @param player
*/
@Override
public void restore(Player player) {
if (!(player instanceof PlayerImpl)) {
throw new IllegalArgumentException("Wrong code usage: can't restore from player class " + player.getClass().getName());
}
this.name = player.getName();
this.human = player.isHuman();
this.life = player.getLife();
this.passed = player.isPassed();
// 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.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 shouldn't 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();
//noinspection deprecation - it's ok to use it in inner methods
this.commandersIds = new HashSet<>(player.getCommandersIds());
this.abilities = player.getAbilities().copy();
this.counters = player.getCountersAsCopy();
this.landsPlayed = player.getLandsPlayed();
this.landsPerTurn = player.getLandsPerTurn();
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(((PlayerImpl) player).inRange);
this.payLifeCostLevel = player.getPayLifeCostLevel();
this.sacrificeCostFilter = player.getSacrificeCostFilter() != null
? player.getSacrificeCostFilter().copy() : null;
this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife();
this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary();
this.drawsFromBottom = player.isDrawsFromBottom();
this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn();
this.alternativeSourceCosts = CardUtil.deepCopyObject(((PlayerImpl) player).alternativeSourceCosts);
this.speed = player.getSpeed();
this.topCardRevealed = player.isTopCardRevealed();
this.isGameUnderControl = player.isGameUnderControl();
this.turnController = this.getId().equals(player.getTurnControlledBy()) ? null : player.getTurnControlledBy();
this.turnControllers.clear();
this.turnControllers.addAll(player.getTurnControllers());
this.playersUnderYourControl.clear();
this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl());
this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving();
this.clearCastSourceIdManaCosts();
for (Entry> entry : player.getCastSourceIdWithAlternateMana().entrySet()) {
this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue())));
}
for (Entry>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>());
for (Entry> subEntry : entry.getValue().entrySet()) {
this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
for (Entry>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), new HashMap<>());
for (Entry> subEntry : entry.getValue().entrySet()) {
this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null;
this.designations.clear();
for (Designation object : player.getDesignations()) {
this.designations.add(object.copy());
}
// Don't restore!
// this.storedBookmark
// this.usersAllowedToSeeHandCards
}
@Override
public void useDeck(Deck deck, Game game) {
library.clear();
library.addAll(deck.getMaindeckCards(), game);
sideboard.clear();
for (Card card : deck.getSideboard()) {
sideboard.add(card);
}
}
@Override
public void init(Game game) {
this.abort = false;
// keep old
//this.playerId;
//this.name;
//this.human;
this.life = game.getStartingLife();
this.wins = false;
this.draws = false;
this.loses = false;
this.library.reset();
this.sideboard.clear();
this.hand.clear();
this.graveyard.clear();
this.commandersIds.clear();
this.abilities.clear();
this.counters.clear();
this.landsPlayed = 0;
this.landsPerTurn = 1;
this.maxHandSize = 7;
this.maxAttackedBy = Integer.MAX_VALUE;
this.getManaPool().init(); // needed to remove mana that not empties on step change from previous game if left
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.turns = 0;
this.storedBookmark = -1;
this.priorityTimeLeft = Integer.MAX_VALUE;
this.bufferTimeLeft = 0;
// reset is necessary because in tournament player will be used for each round
this.left = false;
this.quit = false;
this.timerTimeout = false;
this.idleTimeout = false;
//this.range; // must keep
this.inRange.clear();
//this.isTestMode // must keep
this.canGainLife = true;
this.canLoseLife = true;
this.payLifeCostLevel = PayLifeCostLevel.allAbilities;
this.loseByZeroOrLessLife = true;
this.canPlotFromTopOfLibrary = false;
this.drawsFromBottom = false;
this.drawsOnOpponentsTurn = false;
this.speed = 0;
this.sacrificeCostFilter = null;
this.alternativeSourceCosts.clear();
this.isGameUnderControl = true;
this.turnController = null;
this.turnControllers.clear();
this.playersUnderYourControl.clear();
//this.usersAllowedToSeeHandCards; // must keep
this.attachments.clear();
this.topCardRevealed = false;
this.reachedNextTurnAfterLeaving = false;
this.clearCastSourceIdManaCosts();
this.payManaMode = false;
// must keep
//this.userData;
//this.matchPlayer;
this.designations.clear();
this.phyrexianColors = null;
this.availableTriggeredManaList.clear();
}
/**
* called before apply effects
*/
@Override
public void reset() {
this.abilities.clear();
this.landsPerTurn = 1;
this.maxHandSize = 7;
this.maxAttackedBy = Integer.MAX_VALUE;
this.canGainLife = true;
this.canLoseLife = true;
this.payLifeCostLevel = PayLifeCostLevel.allAbilities;
this.sacrificeCostFilter = null;
this.loseByZeroOrLessLife = true;
this.canPlotFromTopOfLibrary = false;
this.drawsFromBottom = false;
this.drawsOnOpponentsTurn = false;
this.topCardRevealed = false;
this.alternativeSourceCosts.clear();
this.clearCastSourceIdManaCosts();
this.getManaPool().clearEmptyManaPoolRules();
this.phyrexianColors = null;
}
@Override
public Counters getCountersAsCopy() {
return counters.copy();
}
@Override
public void beginTurn(Game game) {
resetLandsPlayed();
updateRange(game);
game.getState().removeTurnStartEffect(game);
}
@Override
public RangeOfInfluence getRange() {
return range;
}
@Override
public void updateRange(Game game) {
// 20100423 - 801.2c
// 801.2c The particular players within each player’s range of influence are determined as each turn begins.
// BUT it also uses before game start to fill game and card data in starting game events
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 boolean hasPlayerInRange(UUID checkingPlayerId) {
if (inRange.isEmpty()) {
// runtime check: inRange filled on beginTurn, but unit tests adds cards by cheat engine before game starting,
// so inRange will be empty and some ETB effects can be broken (example: Spark Double puts direct to battlefield).
// Cheat engine already have a workaround, so that error must not be visible in normal situation.
// TODO: that's error possible on GameView call before real game start (too laggy players on starting???)
throw new IllegalStateException("Wrong code usage (game is not started, but you call hasPlayerInRange in some effects).");
}
return inRange.contains(checkingPlayerId);
}
@Override
public Set getPlayersUnderYourControl() {
return this.playersUnderYourControl;
}
@Override
public boolean controlPlayersTurn(Game game, UUID playerUnderControlId, String info) {
Player playerUnderControl = game.getPlayer(playerUnderControlId);
// TODO: add support computer over computer
// TODO: add support computer over human
if (this.isComputer()) {
// not supported yet
game.informPlayers(getLogName() + " is AI and can't take control over " + playerUnderControl.getLogName() + info);
return false;
}
playerUnderControl.setTurnControlledBy(this.getId());
game.informPlayers(getLogName() + " taken turn control of " + playerUnderControl.getLogName() + info);
if (!playerUnderControlId.equals(this.getId())) {
this.playersUnderYourControl.add(playerUnderControlId);
if (!playerUnderControl.hasLeft() && !playerUnderControl.hasLost()) {
playerUnderControl.setGameUnderYourControl(false);
}
// control will reset on start of the turn
}
return true;
}
@Override
public void setTurnControlledBy(UUID playerId) {
if (playerId == null) {
throw new IllegalArgumentException("Can't add unknown player to turn controllers: " + playerId);
}
this.turnController = playerId;
this.turnControllers.add(playerId);
}
@Override
public List getTurnControllers() {
return this.turnControllers;
}
@Override
public UUID getTurnControlledBy() {
return this.turnController == null ? this.getId() : 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) {
// to own
this.turnControllers.clear();
this.turnController = null;
this.isGameUnderControl = true;
} else {
// to prev player
if (!turnControllers.isEmpty()) {
this.turnControllers.remove(turnControllers.size() - 1);
}
if (turnControllers.isEmpty()) {
this.turnController = null;
this.isGameUnderControl = true;
} 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 sourceObject, UUID sourceControllerId, Ability source, Game game) {
if (this.hasLost() || this.hasLeft()) {
return false;
}
if (sourceObject != null) {
if (abilities.containsKey(ShroudAbility.getInstance().getId())
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) {
return false;
}
if (sourceControllerId != null
&& this.hasOpponent(sourceControllerId, game)
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty()
&& abilities.stream()
.filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast)
.anyMatch(ability -> ability.checkObject(sourceObject, source, game))) {
return false;
}
if (hasProtectionFrom(sourceObject, game)) {
return false;
}
// example: Peace Talks
return !game.getContinuousEffects().preventedByRuleModification(
new TargetEvent(this, sourceObject.getId(), sourceControllerId),
null,
game,
true
);
}
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, Ability source, Game game) {
return drawCards(num, source, game, null);
}
/*
* 614.11. Some effects replace card draws. These effects are applied even if no cards could be drawn because
* there are no cards in the affected player's library.
* 614.11a. If an effect replaces a draw within a sequence of card draws, all actions required by the replacement
* are completed, if possible, before resuming the sequence.
* 614.11b. If an effect would have a player both draw a card and perform an additional action on that card, and
* the draw is replaced, the additional action is not performed on any cards that are drawn as a result of that
* replacement effect.
*/
@Override
public int drawCards(int num, Ability source, Game game, GameEvent event) {
if (num == 0) {
return 0;
}
if (num >= 2) {
// Event for replacement effects that only apply when two or more cards are drawn
DrawTwoOrMoreCardsEvent multiDrawEvent = new DrawTwoOrMoreCardsEvent(getId(), source, event, num);
if (game.replaceEvent(multiDrawEvent)) {
return multiDrawEvent.getCardsDrawn();
}
num = multiDrawEvent.getAmount();
}
int numDrawn = 0;
for (int i = 0; i < num; i++) {
DrawCardEvent drawCardEvent = new DrawCardEvent(getId(), source, event);
if (game.replaceEvent(drawCardEvent)) {
numDrawn += drawCardEvent.getCardsDrawn();
continue;
}
Card card = isDrawsFromBottom() ? getLibrary().drawFromBottom(game) : getLibrary().drawFromTop(game);
if (card != null) {
card.moveToZone(Zone.HAND, source, game, false); // if you want to use event.getSourceId() here then thinks x10 times
if (isTopCardRevealed()) {
game.fireInformEvent(getLogName() + " draws a revealed card (" + card.getLogName() + ')');
}
game.fireEvent(new DrewCardEvent(card.getId(), getId(), source, event));
numDrawn++;
}
}
if (!isTopCardRevealed() && numDrawn > 0) {
game.fireInformEvent(getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : ""));
}
// if this method was called from a replacement event, pass the number of cards back through
// (uncomment conditions if correct ruling is to only count cards drawn by the same player)
if (event instanceof DrawCardEvent /* && event.getPlayerId().equals(getId()) */) {
((DrawCardEvent) event).incrementCardsDrawn(numDrawn);
}
if (event instanceof DrawTwoOrMoreCardsEvent /* && event.getPlayerId().equals(getId()) */) {
((DrawTwoOrMoreCardsEvent) event).incrementCardsDrawn(numDrawn);
}
return numDrawn;
}
@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, false, null, game);
}
}
@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, boolean payForCost, Ability source, Game game) {
return discard(1, random, payForCost, source, game).getRandom(game);
}
@Override
public Cards discard(int amount, boolean random, boolean payForCost, Ability source, Game game) {
if (random) {
return discard(getRandomToDiscard(amount, source, game), payForCost, source, game);
}
return discard(amount, amount, payForCost, source, game);
}
@Override
public Cards discard(int minAmount, int maxAmount, boolean payForCost, Ability source, Game game) {
return discard(getToDiscard(minAmount, maxAmount, source, game), payForCost, source, game);
}
@Override
public Cards discard(Cards cards, boolean payForCost, Ability source, Game game) {
Cards discardedCards = new CardsImpl();
if (cards == null) {
return discardedCards;
}
for (Card card : cards.getCards(game)) {
if (doDiscard(card, source, game, payForCost, false)) {
discardedCards.add(card);
}
}
if (!discardedCards.isEmpty()) {
game.fireEvent(new DiscardedCardsEvent(source, playerId, discardedCards.size(), discardedCards));
}
return discardedCards;
}
@Override
public boolean discard(Card card, boolean payForCost, Ability source, Game game) {
return doDiscard(card, source, game, payForCost, true);
}
private Cards getToDiscard(int minAmount, int maxAmount, Ability source, Game game) {
Cards toDiscard = new CardsImpl();
if (minAmount > maxAmount) {
return getToDiscard(maxAmount, minAmount, source, game);
}
if (maxAmount < 1) {
return toDiscard;
}
if (getHand().size() <= minAmount) {
toDiscard.addAll(getHand());
return toDiscard;
}
TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId());
choose(Outcome.Discard, target, source, game);
toDiscard.addAll(target.getTargets());
return toDiscard;
}
private Cards getRandomToDiscard(int amount, Ability source, Game game) {
Cards toDiscard = new CardsImpl();
Cards theHand = getHand().copy();
for (int i = 0; i < amount; i++) {
if (theHand.isEmpty()) {
break;
}
Card card = theHand.getRandom(game);
theHand.remove(card);
toDiscard.add(card);
}
return toDiscard;
}
private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) {
//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, playerId);
gameEvent.setFlag(!payForCost); // event from effect (1) or from cost (0)
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() + CardUtil.getSourceLogName(game, source));
}
/* 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, game, false);
// So discard is also successful if card is moved to another zone by replacement effect!
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DISCARDED_CARD, card.getId(), source, playerId));
if (fireFinalEvent) {
game.fireEvent(new DiscardedCardsEvent(source, playerId, 1, new CardsImpl(card)));
}
return true;
}
@Override
public List getAttachments() {
return attachments;
}
@Override
public boolean addAttachment(UUID permanentId, Ability source, Game game) {
if (!this.attachments.contains(permanentId)) {
Permanent aura = game.getPermanent(permanentId);
if (aura == null) {
aura = game.getPermanentEntering(permanentId);
}
if (aura != null && !game.replaceEvent(new EnchantPlayerEvent(playerId, aura, source))) {
this.attachments.add(permanentId);
aura.attachTo(playerId, source, game);
game.fireEvent(new EnchantedPlayerEvent(playerId, aura, source));
return true;
}
}
return false;
}
@Override
public boolean removeAttachment(Permanent attachment, Ability source, Game game) {
if (this.attachments.contains(attachment.getId())
&& !game.replaceEvent(new UnattachEvent(playerId, attachment.getId(), attachment, source))) {
this.attachments.remove(attachment.getId());
attachment.attachTo(null, source, game);
game.fireEvent(new UnattachedEvent(playerId, attachment.getId(), attachment, source));
return true;
}
return false;
}
@Override
public boolean removeFromBattlefield(Permanent permanent, Ability source, 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(), source, game);
} else {
Player attachedToPlayer = game.getPlayer(permanent.getAttachedTo());
if (attachedToPlayer != null) {
attachedToPlayer.removeAttachment(permanent, source, game);
} else {
Card attachedToCard = game.getCard(permanent.getAttachedTo());
if (attachedToCard != null) {
attachedToCard.removeAttachment(permanent.getId(), source, 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, game, false);
}
} else {
// user defined order
UUID cardOwner = cards.getRandom(game).getOwnerId();
TargetCard target = new TargetCard(Zone.ALL,
new FilterCard("card ORDER to put on the BOTTOM of " +
(cardOwner.equals(playerId) ? "your" : game.getPlayer(cardOwner).getName() + "'s") +
" library (last one chosen will be bottommost)"));
target.setRequired(true);
while (cards.size() > 1 && this.canRespond()
&& this.choose(Outcome.Neutral, cards, target, source, game)) {
UUID targetObjectId = target.getFirstTarget();
if (targetObjectId == null) {
break;
}
cards.remove(targetObjectId);
moveObjectToLibrary(targetObjectId, source, game, false);
target.clearChosen();
}
for (UUID c : cards) {
moveObjectToLibrary(c, source, game, false);
}
}
}
return true;
}
@Override
public boolean shuffleCardsToLibrary(Cards cards, Game game, Ability source) {
if (cards.isEmpty()) {
return true;
}
game.informPlayers(getLogName() + " shuffles " + CardUtil.numberToText(cards.size(), "a")
+ " card" + (cards.size() == 1 ? "" : "s")
+ " into their library" + CardUtil.getSourceLogName(game, source));
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, boolean withName) {
if (card.isOwnedBy(getId())) {
if (library.size() + 1 < xFromTheTop) {
putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, true);
} else {
if (card.moveToZone(Zone.LIBRARY, source, game, true)
&& !(card instanceof PermanentToken) && !card.isCopy()) {
Card cardInLib = getLibrary().getFromTop(game);
if (cardInLib != null && cardInLib.getId().equals(card.getMainCard().getId())) { // check needed because e.g. commander can go to command zone
cardInLib = getLibrary().removeFromTop(game);
getLibrary().putCardToTopXPos(cardInLib, xFromTheTop, game);
game.informPlayers((withName ? cardInLib.getLogName() : "A card")
+ " is put into "
+ getLogName()
+ "'s library "
+ CardUtil.numberToOrdinalText(xFromTheTop)
+ " from the top" + CardUtil.getSourceLogName(game, source, cardInLib.getId()));
}
} else {
return false;
}
}
} else {
return game.getPlayer(card.getOwnerId()).putCardOnTopXOfLibrary(card, game, source, xFromTheTop, withName);
}
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, game, true);
}
} else {
// user defined order
UUID cardOwner = cards.getRandom(game).getOwnerId();
TargetCard target = new TargetCard(Zone.ALL,
new FilterCard("card ORDER to put on the TOP of " +
(cardOwner.equals(playerId) ? "your" : game.getPlayer(cardOwner).getName() + "'s") +
" library (last one chosen will be topmost)"));
target.setRequired(true);
while (cards.size() > 1
&& this.canRespond()
&& this.choose(Outcome.Neutral, cards, target, source, game)) {
UUID targetObjectId = target.getFirstTarget();
if (targetObjectId == null) {
break;
}
cards.remove(targetObjectId);
moveObjectToLibrary(targetObjectId, source, game, true);
target.clearChosen();
}
for (UUID c : cards) {
moveObjectToLibrary(c, source, game, true);
}
}
}
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 void moveObjectToLibrary(UUID objectId, Ability source, Game game, boolean toTop) {
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 spell 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(source.getSourceId(), false);
if (spellNoCopy != null) {
mageObject = spellNoCopy;
}
}
if (mageObject != null) {
Zone fromZone = game.getState().getZone(objectId);
if ((mageObject instanceof Permanent)) {
this.moveCardToLibraryWithInfo((Permanent) mageObject, source, game, fromZone, toTop, false);
} else if (mageObject instanceof Card) {
this.moveCardToLibraryWithInfo((Card) mageObject, source, game, fromZone, toTop, false);
}
}
}
@Override
public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs, MageIdentifier identifier) {
// cost must be copied for data consistence between game simulations
castSourceIdWithAlternateMana
.computeIfAbsent(sourceId, k -> new HashSet<>())
.add(identifier);
castSourceIdManaCosts
.computeIfAbsent(sourceId, k -> new HashMap<>())
.put(identifier, manaCosts != null ? manaCosts.copy() : null);
castSourceIdCosts
.computeIfAbsent(sourceId, k -> new HashMap<>())
.put(identifier, costs != null ? costs.copy() : null);
}
@Override
public Map> 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, ApprovingObject approvingObject) {
if (card == null) {
return false;
}
// play without timing and from any zone
boolean result;
if (card.isLand(game)) {
result = playLand(card, game, true);
} else {
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
result = cast(this.chooseAbilityForCast(card, game, noMana), game, noMana, approvingObject);
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
}
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 approvingObject which object approved the cast
* @return
*/
@Override
public boolean cast(SpellAbility originalAbility, Game game, boolean noMana, ApprovingObject approvingObject) {
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();
Set allowedIdentifiers = originalAbility.spellCanBeActivatedNow(getId(), game);
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) {
Zone fromZone = game.getState().getZone(card.getMainCard().getId());
GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL,
ability.getId(), ability, playerId, approvingObject);
castEvent.setZone(fromZone);
if (!game.replaceEvent(castEvent, ability)) {
int bookmark = game.bookmarkState();
setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it)
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;
}
if (card.isCopy()) {
spell.setCopy(true, null);
}
// 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
MageIdentifier identifier = approvingObject == null
? MageIdentifier.Default
: approvingObject.getApprovingAbility().getIdentifier();
if (!getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) {
// identifier has no alternate cast entry for that sourceId, using Default instead.
identifier = MageIdentifier.Default;
}
if (getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) {
Ability spellAbility = spell.getSpellAbility();
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()).get(identifier);
Costs costs = getCastSourceIdCosts().get(ability.getSourceId()).get(identifier);
if (alternateCosts == null) {
noMana = true;
} else {
spellAbility.clearManaCosts();
spellAbility.clearManaCostsToPay();
spellAbility.addCost(alternateCosts.copy());
}
spellAbility.clearCosts();
spellAbility.addCost(costs);
}
clearCastSourceIdManaCosts(); // TODO: test multiple alternative cost for different cards as same time
castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL,
spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject);
castEvent.setZone(fromZone);
game.fireEvent(castEvent);
if (spell.activate(game, allowedIdentifiers, noMana)) {
GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST,
ability.getId(), ability, playerId, approvingObject);
castedEvent.setZone(fromZone);
game.fireEvent(castedEvent);
if (!game.isSimulation()) {
game.informPlayers(getLogName() + spell.getActivatedMessage(game, fromZone));
}
game.removeBookmark(bookmark);
resetStoredBookmark(game);
return true;
}
restoreState(bookmark, ability.getRule(), game);
}
}
return false;
}
@Override
public boolean playLand(Card card, Game game, boolean ignoreTiming) {
if (card == null) {
return false;
}
ActivatedAbility playLandAbility = null;
for (Ability ability : card.getAbilities(game)) {
if (ability instanceof PlayLandAbility) {
playLandAbility = (ActivatedAbility) ability;
}
}
if (playLandAbility == null) {
return false;
}
//20091005 - 114.2a
ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game);
if (ignoreTiming) {
if (!canPlayLand() || !isActivePlayer(game)) {
// ignore timing does not mean that more lands than normal can be played
// it also has to be your turn
return false;
}
} else {
if (!activationStatus.canActivate()) {
return false;
}
}
ApprovingObjectResult approvingResult = chooseApprovingObject(
game,
activationStatus.getApprovingObjects().stream().collect(Collectors.toList()),
false
);
if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) {
return false; // canceled choice of approving object.
}
//20091005 - 305.1
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND,
card.getId(), playLandAbility, playerId, approvingResult.approvingObject))) {
// 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(), playLandAbility, playerId, approvingResult.approvingObject);
landEventBefore.setZone(cardZoneBefore);
game.fireEvent(landEventBefore);
if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) {
incrementLandsPlayed();
GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED,
card.getId(), playLandAbility, playerId, approvingResult.approvingObject);
landEventAfter.setZone(cardZoneBefore);
game.fireEvent(landEventAfter);
String playText = getLogName() + " plays " + card.getLogName();
if (card instanceof ModalDoubleFacedCardHalf) {
ModalDoubleFacedCard mdfCard = (ModalDoubleFacedCard) card.getMainCard();
playText = getLogName() + " plays " + GameLog.replaceNameByColoredName(card, card.getName(), mdfCard)
+ " as MDF side of " + GameLog.getColoredObjectIdName(mdfCard);
}
game.fireInformEvent(playText);
// 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;
}
private enum ApprovingObjectResultStatus {
CHOSEN,
NO_POSSIBLE_CHOICE,
NOT_REQUIRED_NO_CHOICE,
}
private static class ApprovingObjectResult {
public final ApprovingObjectResultStatus status;
public final ApprovingObject approvingObject; // not null iff status is CHOSEN
private ApprovingObjectResult(ApprovingObjectResultStatus status, ApprovingObject approvingObject) {
this.status = status;
this.approvingObject = approvingObject;
}
}
private ApprovingObjectResult chooseApprovingObject(Game game, List possibleApprovingObjects, boolean required) {
// Choosing
if (possibleApprovingObjects.isEmpty()) {
return new ApprovingObjectResult(ApprovingObjectResultStatus.NO_POSSIBLE_CHOICE, null);
} else {
// Select the ability that you use to permit the action
Map keyChoices = new HashMap<>();
int i = 0;
for (ApprovingObject possibleApprovingObject : possibleApprovingObjects) {
MageObject mageObject = game.getObject(possibleApprovingObject.getApprovingAbility().getSourceId());
String choiceValue = "";
MageIdentifier identifier = possibleApprovingObject.getApprovingAbility().getIdentifier();
if (!identifier.getAdditionalText().isEmpty()) {
choiceValue += identifier.getAdditionalText() + ": ";
}
if (mageObject == null) {
choiceValue += possibleApprovingObject.getApprovingAbility().getRule();
} else {
choiceValue += mageObject.getIdName() + ": ";
String moreDetails = possibleApprovingObject.getApprovingAbility().getRule(mageObject.getName());
choiceValue += moreDetails.isEmpty() ? "Cast normally" : moreDetails;
}
keyChoices.put((i++) + "", choiceValue);
}
int choice = 0;
if (!game.inCheckPlayableState() && keyChoices.size() > 1) {
Choice choicePermitting = new ChoiceImpl(required);
choicePermitting.setMessage("Choose the permitting object");
choicePermitting.setKeyChoices(keyChoices);
if (canRespond()) {
if (choose(Outcome.Neutral, choicePermitting, game)) {
String choiceKey = choicePermitting.getChoiceKey();
if (choiceKey != null) {
choice = Integer.parseInt(choiceKey);
}
} else {
return new ApprovingObjectResult(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE, null);
}
}
}
return new ApprovingObjectResult(ApprovingObjectResultStatus.CHOSEN, possibleApprovingObjects.get(choice));
}
}
protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) {
int bookmark = game.bookmarkState();
if (ability.activate(game, false) && 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
int bookmark = game.bookmarkState();
if (ability.isUsesStack()) {
// put to stack
setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it)
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, playerId));
if (!game.isSimulation()) {
game.informPlayers(getLogName() + ability.getGameLogMessage(game));
}
game.removeBookmark(bookmark);
resetStoredBookmark(game);
return true;
}
restoreState(bookmark, ability.getRule(), game);
} else {
// resolve without stack
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.TAKE_SPECIAL_ACTION,
action.getId(), action, getId()))) {
int bookmark = game.bookmarkState();
if (action.activate(game, false)) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_ACTION,
action.getId(), action, 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 boolean specialManaPayment(SpecialAction action, Game game) {
//20091005 - 114
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TAKE_SPECIAL_MANA_PAYMENT,
action.getId(), action, getId()))) {
int bookmark = game.bookmarkState();
if (action.activate(game, false)) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TAKEN_SPECIAL_MANA_PAYMENT,
action.getId(), action, 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;
}
@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;
}
// Copy, and try to pay for and apply effects
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 SPECIAL_MANA_PAYMENT:
result = specialManaPayment((SpecialAction) ability.copy(), game);
break;
case ACTIVATED_MANA:
result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game);
break;
case SPELL:
ApprovingObjectResult approvingResult = chooseApprovingObject(
game,
activationStatus.getApprovingObjects().stream().collect(Collectors.toList()),
false
);
if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) {
return false; // chosen to not approve any AsThough.
}
result = cast((SpellAbility) ability, game, false, approvingResult.approvingObject);
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().isActivatedAbility())) {
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();
ability.adjustTargets(game);
if (ability.canChooseTarget(game, playerId)) {
if (ability.isUsesStack()) {
game.getStack().push(new StackAbility(ability, playerId));
}
if (ability.activate(game, false)) {
if ((ability.isUsesStack()
|| ability.getRuleVisible())
&& !game.isSimulation()) {
game.informPlayers(getLogName() + " - " + ability.getGameLogMessage(game));
}
if (!ability.isUsesStack()) {
ability.resolve(game);
} else {
game.fireEvent(new GameEvent(
GameEvent.EventType.TRIGGERED_ABILITY,
ability.getId(), ability, ability.getControllerId()
));
}
game.removeBookmark_v2(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;
}
/**
* Return spells for possible cast Uses in GUI to show only playable spells
* for choosing from the card (example: effect allow to cast card and player
* must choose the spell ability)
*
* @param game
* @param playerId
* @param object
* @param zone
* @param noMana
* @return
*/
public static Map getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) {
// it uses simple check from spellCanBeActivatedNow
// reason: no approved info here (e.g. forced to choose spell ability from cast card)
LinkedHashMap useable = new LinkedHashMap<>();
Abilities allAbilities;
if (object instanceof Card) {
allAbilities = ((Card) object).getAbilities(game);
} else {
allAbilities = object.getAbilities();
}
for (SpellAbility spellAbility : allAbilities
.stream()
.filter(SpellAbility.class::isInstance)
.map(SpellAbility.class::cast)
.collect(Collectors.toList())) {
switch (spellAbility.getSpellAbilityType()) {
case BASE_ALTERNATE:
// rules:
// If you cast a spell “without paying its mana cost,” you can’t choose to cast it for
// any alternative costs. You can, however, pay additional costs, such as kicker costs.
// If the card has any mandatory additional costs, those must be paid to cast the spell.
// (2021-02-05)
if (!noMana) {
Set allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game);
if (allowedToBeCastNow.contains(MageIdentifier.Default) || allowedToBeCastNow.contains(spellAbility.getIdentifier())) {
useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability
}
return useable;
}
break;
case SPLIT_FUSED:
// rules:
// If you cast a split card with fuse from your hand without paying its mana cost,
// you can choose to use its fuse ability and cast both halves without paying their mana costs.
if (zone == Zone.HAND && spellAbility.canChooseTarget(game, playerId)) {
useable.put(spellAbility.getId(), spellAbility);
}
case SPLIT:
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(
((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility()
);
}
if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
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, playerId)) {
useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(),
((SplitCard) object).getRightHalfCard().getSpellAbility());
}
} else {
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility());
}
}
return useable;
default: {
Set allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game);
if (allowedToBeCastNow.contains(MageIdentifier.Default) || allowedToBeCastNow.contains(spellAbility.getIdentifier())) {
useable.put(spellAbility.getId(), spellAbility);
}
}
}
}
return useable;
}
@Override
public Map getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) {
LinkedHashMap useable = new LinkedHashMap<>();
// stack abilities - can't activate anything
// spell ability - can activate additional abilities (example: "Lightning Storm")
if (object instanceof StackAbility || object == null) {
return useable;
}
// collect and filter playable activated abilities
// GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right)
Set needIds = CardUtil.getObjectParts(object);
// workaround to find all abilities first and filter it for one object
List allPlayable = getPlayable(game, true, zone, false);
for (ActivatedAbility ability : allPlayable) {
if (needIds.contains(ability.getSourceId())) {
useable.putIfAbsent(ability.getId(), ability);
}
}
return useable;
}
protected Map 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 void incrementLandsPlayed() {
landsPlayed++;
}
@Override
public void resetLandsPlayed() {
landsPlayed = 0;
}
@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, source, playerId))) {
this.library.shuffle();
if (!game.isSimulation()) {
game.informPlayers(getLogName() + "'s library is shuffled" + CardUtil.getSourceLogName(game, source));
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SHUFFLED, playerId, source, 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.createObjectRelatedWindowTitle(source, game, titleSuffix), cards);
} else {
game.getState().getRevealed().update(CardUtil.createObjectRelatedWindowTitle(source, game, titleSuffix), cards);
}
if (postToLog && !game.isSimulation()) {
StringBuilder sb = new StringBuilder(getLogName()).append(" reveals ");
int current = 0;
int last = cards.size();
for (Card card : cards.getCards(game)) {
current++;
sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!)
if (current < last) {
sb.append(", ");
}
}
sb.append(CardUtil.getSourceLogName(game, source));
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.createObjectRelatedWindowTitle(source, game, titleSuffix), cards);
game.fireUpdatePlayersEvent();
}
@Override
public void phasing(Game game) {
//20091005 - 502.1
List phasedOut = game.getBattlefield().getPhasedOut(playerId);
for (Permanent permanent : game.getBattlefield().getPhasingOut(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 a 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) {
// rule 118.5
if (life > this.life) {
gainLife(life - this.life, game, source);
} else if (life < this.life) {
loseLife(this.life - life, game, source, 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, Ability source, boolean atCombat, UUID attackerId) {
if (!canLoseLife || !this.isInGame()) {
return 0;
}
GameEvent event = new GameEvent(GameEvent.EventType.LOSE_LIFE,
playerId, source, playerId, amount, atCombat);
if (!game.replaceEvent(event)) {
this.life = CardUtil.overflowDec(this.life, event.getAmount());
if (!game.isSimulation()) {
UUID needId = attackerId;
if (needId == null) {
needId = source == null ? null : source.getSourceId();
}
game.informPlayers(this.getLogName() + " loses " + event.getAmount() + " life"
+ (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", ""));
}
if (event.getAmount() > 0) {
LifeLostEvent lifeLostEvent = new LifeLostEvent(playerId, source, event.getAmount(), atCombat);
game.fireEvent(lifeLostEvent);
game.getState().addSimultaneousLifeLossToBatch(lifeLostEvent, game);
}
return event.getAmount();
}
return 0;
}
@Override
public int loseLife(int amount, Game game, Ability source, boolean atCombat) {
return loseLife(amount, game, source, atCombat, null);
}
@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) {
if (!canGainLife || amount <= 0) {
return 0;
}
GameEvent event = new GameEvent(GameEvent.EventType.GAIN_LIFE,
playerId, source, 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.overflowInc(this.life, event.getAmount());
if (!game.isSimulation()) {
game.informPlayers(this.getLogName() + " gains " + event.getAmount() + " life" + CardUtil.getSourceLogName(game, source));
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.GAINED_LIFE,
playerId, source, playerId, event.getAmount()));
return event.getAmount();
}
return 0;
}
@Override
public void exchangeLife(Player player, Ability source, Game game) {
int lifePlayer1 = getLife();
int lifePlayer2 = player.getLife();
if ((lifePlayer1 != lifePlayer2 && this.isLifeTotalCanChange() && player.isLifeTotalCanChange())
&& (lifePlayer1 >= lifePlayer2 || (this.isCanGainLife() && player.isCanLoseLife()))
&& (lifePlayer1 <= lifePlayer2 || (this.isCanLoseLife() && player.isCanGainLife()))) {
this.setLife(lifePlayer2, game, source);
player.setLife(lifePlayer1, game, source);
}
}
@Override
public int damage(int damage, Ability source, Game game) {
return doDamage(damage, source.getSourceId(), source, game, false, true, null);
}
@Override
public int damage(int damage, UUID attackerId, Ability source, Game game) {
return doDamage(damage, attackerId, source, game, false, true, null);
}
@Override
public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) {
return doDamage(damage, attackerId, source, game, combatDamage, preventable, null);
}
@Override
public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) {
return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects);
}
private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combat, boolean preventable, List appliedEffects) {
if (!this.isInGame()) {
return 0;
}
if (damage < 1) {
return 0;
}
DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combat);
event.setAppliedEffects(appliedEffects);
// Even if no damage was dealt, some watchers would need a reset next time actions are processed.
// For instance PhantomPreventionWatcher used by the [[Phantom Wurm]] type of replacement effect.
game.getState().addBatchDamageCouldHaveBeenFired(combat, game);
if (game.replaceEvent(event)) {
return 0;
}
int actualDamage = checkProtectionAbilities(event, attackerId, source, game);
if (actualDamage < 1) {
return 0;
}
UUID sourceControllerId = null;
Abilities sourceAbilities = null;
MageObject attacker = game.getPermanentOrLKIBattlefield(attackerId);
if (attacker == null) {
StackObject stackObject = game.getStack().getStackObject(attackerId);
if (stackObject != null) {
attacker = stackObject.getStackAbility().getSourceObject(game);
} else {
attacker = game.getObject(attackerId);
}
if (attacker instanceof Spell) {
sourceAbilities = ((Spell) attacker).getAbilities(game);
sourceControllerId = ((Spell) attacker).getControllerId();
} else if (attacker instanceof Card) {
sourceAbilities = ((Card) attacker).getAbilities(game);
sourceControllerId = ((Card) attacker).getOwnerId();
} else if (attacker instanceof CommandObject) {
sourceControllerId = ((CommandObject) attacker).getControllerId();
sourceAbilities = attacker.getAbilities();
}
} else {
sourceAbilities = ((Permanent) attacker).getAbilities(game);
sourceControllerId = ((Permanent) attacker).getControllerId();
}
if (event.isAsThoughInfect() || (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId()))) {
addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game);
} else {
GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS,
playerId, source, playerId, actualDamage, combat);
if (!game.replaceEvent(damageToLifeLossEvent)) {
this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combat, attackerId);
}
}
if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) {
if (combat) {
game.getPermanent(attackerId).markLifelink(actualDamage);
} else {
Player player = game.getPlayer(sourceControllerId);
player.gainLife(actualDamage, game, source);
}
}
if (combat && sourceAbilities != null && sourceAbilities.containsClass(ToxicAbility.class)) {
int countersToAdd = CardUtil
.castStream(sourceAbilities.stream(), ToxicAbility.class)
.mapToInt(ToxicAbility::getAmount)
.sum();
addCounters(CounterType.POISON.createInstance(countersToAdd), sourceControllerId, source, game);
}
// Unstable ability - Earl of Squirrel
if (sourceAbilities != null && sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) {
Player player = game.getPlayer(sourceControllerId);
new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId());
}
DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combat);
game.fireEvent(damagedEvent);
game.getState().addSimultaneousDamage(damagedEvent, game);
return actualDamage;
}
private int checkProtectionAbilities(GameEvent event, UUID attackerId, Ability source, Game game) {
MageObject attacker = game.getObject(attackerId);
if (attacker != null && hasProtectionFrom(attacker, game)) {
GameEvent preventEvent = new PreventDamageEvent(playerId, attackerId, source, playerId, event.getAmount(), ((DamageEvent) event).isCombatDamage());
if (!game.replaceEvent(preventEvent)) {
int preventedDamage = event.getAmount();
event.setAmount(0);
game.fireEvent(new PreventedDamageEvent(playerId, attackerId, source, playerId, preventedDamage));
game.informPlayers(preventedDamage + " damage from " + attacker.getLogName() + " to " + getLogName()
+ (preventedDamage > 1 ? " were" : "was") + " prevented because of protection");
return 0;
}
}
return event.getAmount();
}
@Override
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game) {
boolean returnCode = true;
GameEvent addingAllEvent = GameEvent.getEvent(
GameEvent.EventType.ADD_COUNTERS, playerId, source,
playerAddingCounters, 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(
GameEvent.EventType.ADD_COUNTER, playerId, source,
playerAddingCounters, counter.getName(), 1
);
addingOneEvent.setFlag(isEffectFlag);
if (!game.replaceEvent(addingOneEvent)) {
counters.addCounter(eventCounter);
GameEvent addedOneEvent = GameEvent.getEvent(
GameEvent.EventType.COUNTER_ADDED, playerId, source,
playerAddingCounters, counter.getName(), 1
);
addedOneEvent.setFlag(addingOneEvent.getFlag());
game.fireEvent(addedOneEvent);
} else {
finalAmount--;
returnCode = false;
}
}
if (finalAmount > 0) {
GameEvent addedAllEvent = GameEvent.getEvent(
GameEvent.EventType.COUNTERS_ADDED, playerId, source,
playerAddingCounters, counter.getName(), amount
);
addedAllEvent.setFlag(addingAllEvent.getFlag());
game.fireEvent(addedAllEvent);
}
} else {
returnCode = false;
}
return returnCode;
}
@Override
public void loseCounters(String counterName, int amount, Ability source, Game game) {
GameEvent removeCountersEvent = new RemoveCountersEvent(counterName, this, source, amount, false);
if (game.replaceEvent(removeCountersEvent)) {
return;
}
int finalAmount = 0;
for (int i = 0; i < amount; i++) {
GameEvent event = new RemoveCounterEvent(counterName, this, source, false);
if (game.replaceEvent(event)) {
continue;
}
if (!counters.removeCounter(counterName, 1)) {
break;
}
event = new CounterRemovedEvent(counterName, this, source, false);
game.fireEvent(event);
finalAmount++;
}
GameEvent event = new CountersRemovedEvent(counterName, this, source, finalAmount, false);
game.fireEvent(event);
}
@Override
public int loseAllCounters(Ability source, Game game) {
int amountBefore = getCountersTotalCount();
for (Counter counter : getCountersAsCopy().values()) {
loseCounters(counter.getName(), counter.getCount(), source, game);
}
int amountAfter = getCountersTotalCount();
return Math.max(0, amountBefore - amountAfter);
}
@Override
public int loseAllCounters(String counterName, Ability source, Game game) {
int amountBefore = getCountersCount(counterName);
loseCounters(counterName, amountBefore, source, game);
int amountAfter = getCountersCount(counterName);
return Math.max(0, amountBefore - amountAfter);
}
@Override
public int getCountersCount(CounterType counterType) {
return counters.getCount(counterType);
}
@Override
public int getCountersCount(String counterName) {
return counters.getCount(counterName);
}
@Override
public int getCountersTotalCount() {
return counters.getTotalCount();
}
@Override
public Abilities getAbilities() {
return this.abilities;
}
@Override
public void addAbility(Ability ability) {
ability.setSourceId(playerId);
this.abilities.add(ability);
this.abilities.addAll(ability.getSubAbilities());
}
@Override
public int getLandsPerTurn() {
return this.landsPerTurn;
}
@Override
public void setLandsPerTurn(int landsPerTurn) {
this.landsPerTurn = landsPerTurn;
}
@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); // it's ok to be ignored by "can't lose abilities" here (setConcedingPlayer done all work above)
}
@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.getTurnStepType();
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.getTurnStepType() == PhaseStep.POSTCOMBAT_MAIN
|| game.getTurnStepType() == 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);
}
@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 (independent 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) {
// TODO: allowUndo is smells bad, must be researched (what will be restored on allowUndo = false and why there are so diff usage), 2024-01-16
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)
&& !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);
}
@Override
public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) {
//20091005 - 701.14c
// searching control can be intercepted by another player, see Opposition Agent
SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source, playerId, Integer.MAX_VALUE);
if (game.replaceEvent(searchEvent)) {
return false;
}
Player targetPlayer = game.getPlayer(targetPlayerId);
Player searchingPlayer = this;
Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId());
if (targetPlayer == null || searchingController == null) {
return false;
}
String searchInfo = searchingPlayer.getLogName();
if (!searchingPlayer.getId().equals(searchingController.getId())) {
searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName();
}
if (targetPlayer.getId().equals(searchingPlayer.getId())) {
searchInfo = searchInfo + " searches their library";
} else {
searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName();
}
if (!game.isSimulation()) {
game.informPlayers(searchInfo + CardUtil.getSourceLogName(game, source));
}
// https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/
// You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect:
// * While you’re searching your library, you may cast Panglacial Wurm from your library.
// So use here same code as Word of Command
// P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest
boolean takeControl = false;
if (!searchingPlayer.getId().equals(searchingController.getId())) {
// game logs added in child's call
CardUtil.takeControlUnderPlayerStart(game, source, searchingController, searchingPlayer, true);
takeControl = true;
}
Library searchingLibrary = targetPlayer.getLibrary();
TargetCardInLibrary newTarget = target.copy();
int count;
int librarySearchLimit = searchEvent.getAmount();
List cardsFromTop = null;
do {
// TODO: prevent shuffling from moving the visualized cards
if (librarySearchLimit == Integer.MAX_VALUE) {
count = searchingLibrary.count(target.getFilter(), game);
} else {
if (cardsFromTop == null) {
cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit));
} else {
cardsFromTop.retainAll(searchingLibrary.getCards(game));
}
newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size()));
count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit);
}
if (count < target.getNumberOfTargets()) {
newTarget.setMinNumberOfTargets(count);
}
// handling Panglacial Wurm - cast cards while searching from own library
if (targetPlayer.getId().equals(searchingPlayer.getId())) {
if (handleCastableCardsWhileLibrarySearching(library, targetPlayer, source, game)) {
// clear all choices to start from scratch (casted cards must be removed from library)
newTarget.clearChosen();
continue;
}
}
if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), source, game)) {
target.getTargets().clear();
for (UUID targetId : newTarget.getTargets()) {
target.add(targetId, game);
}
}
// END SEARCH
if (takeControl) {
// game logs added in child's call
CardUtil.takeControlUnderPlayerEnd(game, source, searchingController, searchingPlayer);
}
LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source, searchingPlayer.getId(), target);
if (!game.replaceEvent(searchedEvent)) {
game.fireEvent(searchedEvent);
}
break;
} while (true);
return true;
}
@Override
public boolean seekCard(FilterCard filter, Ability source, Game game) {
Set cards = this.getLibrary()
.getCards(game)
.stream()
.filter(card -> filter.match(card, getId(), source, game))
.collect(Collectors.toSet());
Card card = RandomUtil.randomFromCollection(cards);
if (card == null) {
return false;
}
game.informPlayers(this.getLogName() + " seeks a card from their library");
this.moveCards(card, Zone.HAND, source, game);
return true;
}
@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 handleCastableCardsWhileLibrarySearching(Library library, Player targetPlayer, Ability source, Game game) {
// must return true after cast try (to restart searching process without casted cards)
// uses for handling Panglacial Wurm:
// * While you're searching your library, you may cast Panglacial Wurm from your library.
List castableCards = library.getCards(game).stream()
.filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class))
.map(MageItem::getId)
.collect(Collectors.toList());
if (castableCards.isEmpty()) {
return false;
}
// only humans can use it
if (targetPlayer.isComputer()) {
return false;
}
if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "There are " + castableCards.size() + " cards you can cast while searching your library. Cast any of them?", null, game)) {
return false;
}
boolean casted = false;
TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD);
targetCard.withTargetName("card to cast from library");
targetCard.withNotTarget(true);
while (!castableCards.isEmpty()) {
targetCard.clearChosen();
if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, source, game)) {
break;
}
Card card = game.getCard(targetCard.getFirstTarget());
if (card == null) {
break;
}
// AI NOTICE: if you want AI implement here then remove selected card from castable after each
// choice (otherwise you catch infinite freeze on uncastable use case)
// casting selected card
// TODO: fix costs (why is Panglacial Wurm automatically accepting payment?)
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null);
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
castableCards.remove(card.getId());
casted = true;
}
return casted;
}
/**
* @param source
* @param game
* @param winnable
* @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) {
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 = this.flipCoinResult(game);
FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable);
game.replaceEvent(event);
game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult())
+ CardUtil.getSourceLogName(game, source));
if (event.getFlipCount() > 1) {
boolean canChooseHeads = event.getResult();
boolean canChooseTails = !event.getResult();
for (int i = 1; i < event.getFlipCount(); i++) {
boolean tempFlip = this.flipCoinResult(game);
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"
+ CardUtil.getSourceLogName(game, source));
}
game.fireEvent(event.createFlippedEvent());
if (event.isWinnable()) {
return event.getResult() == event.getChosen();
}
return event.getResult();
}
/**
* Return result for next flip coint try (can be contolled in tests)
*
* @return
*/
@Override
public boolean flipCoinResult(Game game) {
return RandomUtil.nextBoolean();
}
private static final class RollDieResult {
// 706.2.
// After the roll, the number indicated on the top face of the die before any modifiers is
// the natural result. The instruction may include modifiers to the roll which add to or
// subtract from the natural result. Modifiers may also come from other sources. After
// considering all applicable modifiers, the final number is the result of the die roll.
private final int naturalResult;
private final int modifier;
private final PlanarDieRollResult planarResult;
RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) {
this.naturalResult = naturalResult;
this.modifier = modifier;
this.planarResult = planarResult;
}
public int getResult() {
return this.naturalResult + this.modifier;
}
public PlanarDieRollResult getPlanarResult() {
return this.planarResult;
}
}
@Override
public int rollDieResult(int sides, Game game) {
return RandomUtil.nextInt(sides) + 1;
}
/**
* Roll single die. Support both die types: planar and numerical.
*
* @param outcome
* @param game
* @param source
* @param rollDieType
* @param sidesAmount
* @param chaosSidesAmount
* @param planarSidesAmount
* @param rollsAmount
* @return
*/
private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType,
int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) {
if (rollsAmount == 1) {
return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount);
}
Set