* Fixed a problem with check playable methods causing e.g. endless loop if Shared Fate was on the battlefield.

This commit is contained in:
LevelX2 2019-12-25 16:01:02 +01:00
parent 32bd88a6c0
commit 133cc7342d
19 changed files with 227 additions and 230 deletions

View file

@ -1,7 +1,28 @@
package mage.player.human; package mage.player.human;
import java.awt.Color;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import mage.MageObject; import mage.MageObject;
import mage.abilities.*; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.abilities.Mode;
import mage.abilities.Modes;
import mage.abilities.PlayLandAbility;
import mage.abilities.SpecialAction;
import mage.abilities.SpellAbility;
import mage.abilities.TriggeredAbility;
import mage.abilities.costs.VariableCost; import mage.abilities.costs.VariableCost;
import mage.abilities.costs.common.SacrificeSourceCost; import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.common.TapSourceCost;
@ -16,6 +37,11 @@ import mage.cards.decks.Deck;
import mage.choices.Choice; import mage.choices.Choice;
import mage.choices.ChoiceImpl; import mage.choices.ChoiceImpl;
import mage.constants.*; import mage.constants.*;
import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL;
import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL;
import static mage.constants.SpellAbilityType.SPLIT;
import static mage.constants.SpellAbilityType.SPLIT_AFTERMATH;
import static mage.constants.SpellAbilityType.SPLIT_FUSED;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.filter.common.FilterAttackingCreature; import mage.filter.common.FilterAttackingCreature;
import mage.filter.common.FilterBlockingCreature; import mage.filter.common.FilterBlockingCreature;
@ -46,16 +72,6 @@ import mage.util.ManaUtil;
import mage.util.MessageToClient; import mage.util.MessageToClient;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.awt.*;
import java.io.Serializable;
import java.util.List;
import java.util.Queue;
import java.util.*;
import java.util.stream.Collectors;
import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL;
import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -268,6 +284,9 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) { public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) {
if (game.inCheckPlayableState()) {
return true;
}
MessageToClient messageToClient = new MessageToClient(message, secondMessage); MessageToClient messageToClient = new MessageToClient(message, secondMessage);
Map<String, Serializable> options = new HashMap<>(2); Map<String, Serializable> options = new HashMap<>(2);
if (trueText != null) { if (trueText != null) {
@ -334,6 +353,14 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public int chooseReplacementEffect(Map<String, String> rEffects, Game game) { public int chooseReplacementEffect(Map<String, String> rEffects, Game game) {
if (game.inCheckPlayableState()) {
logger.warn("player interaction in checkPlayableState.");
if (rEffects.size() <= 1) {
return 0;
} else {
return 1;
}
}
updateGameStatePriority("chooseEffect", game); updateGameStatePriority("chooseEffect", game);
if (rEffects.size() <= 1) { if (rEffects.size() <= 1) {
return 0; return 0;
@ -394,6 +421,11 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public boolean choose(Outcome outcome, Choice choice, Game game) { public boolean choose(Outcome outcome, Choice choice, Game game) {
if (game.inCheckPlayableState()) {
logger.warn("player interaction in checkPlayableState. Choice: " + choice.getMessage());
choice.setChoice(choice.getChoices().iterator().next());
return true;
}
if (Outcome.PutManaInPool == outcome) { if (Outcome.PutManaInPool == outcome) {
if (currentlyUnpaidMana != null if (currentlyUnpaidMana != null
&& ManaUtil.tryToAutoSelectAManaColor(choice, currentlyUnpaidMana)) { && ManaUtil.tryToAutoSelectAManaColor(choice, currentlyUnpaidMana)) {
@ -429,6 +461,11 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map<String, Serializable> options) { public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map<String, Serializable> options) {
if (game.inCheckPlayableState()) {
logger.warn("player interaction in checkPlayableState. Target: " + target.getMessage());
// TODO: set default choice
return true;
}
// choose one or multiple permanents // choose one or multiple permanents
updateGameStatePriority("choose(5)", game); updateGameStatePriority("choose(5)", game);
UUID abilityControllerId = playerId; UUID abilityControllerId = playerId;
@ -520,6 +557,11 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) {
if (game.inCheckPlayableState()) {
logger.warn("player interaction in checkPlayableState. Target: " + target.getMessage());
// TODO: set default choice
return true;
}
// choose one or multiple targets // choose one or multiple targets
updateGameStatePriority("chooseTarget", game); updateGameStatePriority("chooseTarget", game);
UUID abilityControllerId = playerId; UUID abilityControllerId = playerId;
@ -582,6 +624,11 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public boolean choose(Outcome outcome, Cards cards, TargetCard target, Game game) { public boolean choose(Outcome outcome, Cards cards, TargetCard target, Game game) {
if (game.inCheckPlayableState()) {
logger.warn("player interaction in checkPlayableState. Target: " + target.getMessage());
// TODO: set default choice
return true;
}
// choose one or multiple cards // choose one or multiple cards
if (cards == null) { if (cards == null) {
return false; return false;

View file

@ -52,7 +52,7 @@ class ExpropriateDilemmaEffect extends CouncilsDilemmaVoteEffect {
public ExpropriateDilemmaEffect() { public ExpropriateDilemmaEffect() {
super(Outcome.Benefit); super(Outcome.Benefit);
this.staticText = "<i>Council's dilemma</i> — Starting with you, each player votes for time or money. For each time vote, take an extra turn after this one. For each money vote, choose a permanent owned by the voter and gain control of it."; this.staticText = "<i>Council's dilemma</i> — Starting with you, each player votes for time or money. For each time vote, take an extra turn after this one. For each money vote, choose a permanent owned by the voter and gain control of it";
} }
public ExpropriateDilemmaEffect(final ExpropriateDilemmaEffect effect) { public ExpropriateDilemmaEffect(final ExpropriateDilemmaEffect effect) {
@ -110,7 +110,7 @@ class ExpropriateDilemmaEffect extends CouncilsDilemmaVoteEffect {
target.setNotTarget(true); target.setNotTarget(true);
if (controller != null if (controller != null
&& controller.choose(Outcome.GainControl, target, source.getSourceId(), game)) { && controller.chooseTarget(Outcome.GainControl, target, source, game)) {
Permanent targetPermanent = game.getPermanent(target.getFirstTarget()); Permanent targetPermanent = game.getPermanent(target.getFirstTarget());
if (targetPermanent != null) { if (targetPermanent != null) {
@ -132,9 +132,7 @@ class ExpropriateDilemmaEffect extends CouncilsDilemmaVoteEffect {
protected void vote(String choiceOne, String choiceTwo, Player controller, Game game, Ability source) { protected void vote(String choiceOne, String choiceTwo, Player controller, Game game, Ability source) {
for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) {
Player player = game.getPlayer(playerId); Player player = game.getPlayer(playerId);
if (player != null if (player != null) {
&& player.canRespond()
&& player.isInGame()) {
if (player.chooseUse(Outcome.Vote, "Choose " + choiceOne + '?', source, game)) { if (player.chooseUse(Outcome.Vote, "Choose " + choiceOne + '?', source, game)) {
voteOneCount++; voteOneCount++;
game.informPlayers(player.getName() + " has voted for " + choiceOne); game.informPlayers(player.getName() + " has voted for " + choiceOne);

View file

@ -1,4 +1,3 @@
package mage.cards.f; package mage.cards.f;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -6,7 +5,6 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import mage.Mana; import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.abilities.effects.common.cost.CostModificationEffectImpl;
import mage.abilities.keyword.CyclingAbility; import mage.abilities.keyword.CyclingAbility;
@ -70,9 +68,8 @@ class FluctuatorEffect extends CostModificationEffectImpl {
reduceMax = 2; reduceMax = 2;
} }
if (reduceMax > 0) { if (reduceMax > 0) {
int reduce = 0; int reduce;
if (abilityToModify.getAbilityType() == AbilityType.ACTIVATED if (game.inCheckPlayableState()) {
&& ((ActivatedAbility) abilityToModify).isCheckPlayableMode()) {
reduce = reduceMax; reduce = reduceMax;
} else { } else {
ChoiceImpl choice = new ChoiceImpl(true); ChoiceImpl choice = new ChoiceImpl(true);

View file

@ -1,4 +1,3 @@
package mage.cards.m; package mage.cards.m;
import java.util.UUID; import java.util.UUID;
@ -111,7 +110,7 @@ class MizzixOfTheIzmagnusCostReductionEffect extends CostModificationEffectImpl
Spell spell = (Spell) game.getStack().getStackObject(abilityToModify.getId()); Spell spell = (Spell) game.getStack().getStackObject(abilityToModify.getId());
if (spell != null) { if (spell != null) {
return StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY.match(spell, source.getSourceId(), source.getControllerId(), game); return StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY.match(spell, source.getSourceId(), source.getControllerId(), game);
} else if (((SpellAbility) abilityToModify).isCheckPlayableMode()) { } else if (game.inCheckPlayableState()) {
// Spell is not on the stack yet, but possible playable spells are determined // Spell is not on the stack yet, but possible playable spells are determined
Card sourceCard = game.getCard(abilityToModify.getSourceId()); Card sourceCard = game.getCard(abilityToModify.getSourceId());
return sourceCard != null && StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY.match(sourceCard, source.getSourceId(), source.getControllerId(), game); return sourceCard != null && StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY.match(sourceCard, source.getSourceId(), source.getControllerId(), game);

View file

@ -1,9 +1,7 @@
package mage.cards.n; package mage.cards.n;
import java.util.UUID; import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.CyclingDiscardCost; import mage.abilities.costs.common.CyclingDiscardCost;
@ -70,7 +68,8 @@ class NewPerspectivesCostModificationEffect extends CostModificationEffectImpl {
public boolean apply(Game game, Ability source, Ability abilityToModify) { public boolean apply(Game game, Ability source, Ability abilityToModify) {
Player controller = game.getPlayer(abilityToModify.getControllerId()); Player controller = game.getPlayer(abilityToModify.getControllerId());
if (controller != null) { if (controller != null) {
if ((abilityToModify instanceof ActivatedAbility && ((ActivatedAbility) abilityToModify).isCheckPlayableMode()) || controller.chooseUse(Outcome.PlayForFree, "Pay {0} to cycle?", source, game)) { if (game.inCheckPlayableState()
|| controller.chooseUse(Outcome.PlayForFree, "Pay {0} to cycle?", source, game)) {
abilityToModify.getCosts().clear(); abilityToModify.getCosts().clear();
abilityToModify.getManaCostsToPay().clear(); abilityToModify.getManaCostsToPay().clear();
abilityToModify.getCosts().add(new CyclingDiscardCost()); abilityToModify.getCosts().add(new CyclingDiscardCost());

View file

@ -1,4 +1,3 @@
package mage.cards.s; package mage.cards.s;
import java.util.UUID; import java.util.UUID;
@ -78,12 +77,9 @@ class SharedFateReplacementEffect extends ReplacementEffectImpl {
Card card = chosenPlayer.getLibrary().getFromTop(game); Card card = chosenPlayer.getLibrary().getFromTop(game);
if (card != null) { if (card != null) {
playerToDraw.moveCardsToExile( playerToDraw.moveCardsToExile(
card, card, source, game, false,
source,
game,
false,
CardUtil.getExileZoneId(source.getSourceId().toString() + sourcePermanent.getZoneChangeCounter(game) + playerToDraw.getId().toString(), game), CardUtil.getExileZoneId(source.getSourceId().toString() + sourcePermanent.getZoneChangeCounter(game) + playerToDraw.getId().toString(), game),
"Shared Fate (" + playerToDraw.getName() + ')'); sourcePermanent.getIdName() + " (" + playerToDraw.getName() + ')');
card.setFaceDown(true, game); card.setFaceDown(true, game);
} }
} }

View file

@ -15,7 +15,6 @@ public class ThousandYearStormTest extends CardTestPlayerBase {
Enchantment Enchantment
Whenever you cast an instant or sorcery spell, copy it for each other instant and sorcery spell youve cast before it this turn. You may choose new targets for the copies. Whenever you cast an instant or sorcery spell, copy it for each other instant and sorcery spell youve cast before it this turn. You may choose new targets for the copies.
*/ */
@Test @Test
public void test_CalcBeforeStorm() { public void test_CalcBeforeStorm() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
@ -233,7 +232,6 @@ public class ThousandYearStormTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerB, "Augmenting Automaton"); addCard(Zone.BATTLEFIELD, playerB, "Augmenting Automaton");
// turn 1 // turn 1
// 1a // 1a
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
checkLife("0x copy", 1, PhaseStep.BEGIN_COMBAT, playerB, 20 - 3); checkLife("0x copy", 1, PhaseStep.BEGIN_COMBAT, playerB, 20 - 3);
@ -255,7 +253,6 @@ public class ThousandYearStormTest extends CardTestPlayerBase {
assertAllCommandsUsed(); assertAllCommandsUsed();
} }
@Test @Test
public void test_WaitStackResolvedWithBolts() { public void test_WaitStackResolvedWithBolts() {
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 5); addCard(Zone.BATTLEFIELD, playerB, "Mountain", 5);
@ -282,6 +279,8 @@ public class ThousandYearStormTest extends CardTestPlayerBase {
You control enchanted permanent. You control enchanted permanent.
Cycling {2} ({2}, Discard this card: Draw a card.) Cycling {2} ({2}, Discard this card: Draw a card.)
*/ */
// Test fails sometimes with the following message:
// java.lang.AssertionError: b 0x copy after control - PlayerA have wrong life: 20 <> 17 expected:<17> but was:<20>
@Test @Test
public void test_GetControlNotCounts() { public void test_GetControlNotCounts() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
@ -296,7 +295,6 @@ public class ThousandYearStormTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerB, "Lay Claim"); addCard(Zone.HAND, playerB, "Lay Claim");
// turn 2 // turn 2
// pump card for A // pump card for A
// 1 // 1
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
@ -313,7 +311,6 @@ public class ThousandYearStormTest extends CardTestPlayerBase {
checkLife("b 0x copy after control", 3, PhaseStep.UPKEEP, playerA, 20 - 3); checkLife("b 0x copy after control", 3, PhaseStep.UPKEEP, playerA, 20 - 3);
// turn 4 // turn 4
// pump for B // pump for B
// 1 // 1
castSpell(4, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA); castSpell(4, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA);

View file

@ -1,4 +1,3 @@
package mage.abilities; package mage.abilities;
import java.util.UUID; import java.util.UUID;
@ -62,14 +61,6 @@ public interface ActivatedAbility extends Ability {
@Override @Override
ActivatedAbility copy(); ActivatedAbility copy();
/**
* Set a flag to know, that the ability is only created adn used to check
* what's playbable for the player.
*/
void setCheckPlayableMode();
boolean isCheckPlayableMode();
void setMaxActivationsPerTurn(int maxActivationsPerTurn); void setMaxActivationsPerTurn(int maxActivationsPerTurn);
int getMaxActivationsPerTurn(Game game); int getMaxActivationsPerTurn(Game game);

View file

@ -46,11 +46,9 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
protected TimingRule timing = TimingRule.INSTANT; protected TimingRule timing = TimingRule.INSTANT;
protected TargetController mayActivate = TargetController.YOU; protected TargetController mayActivate = TargetController.YOU;
protected UUID activatorId; protected UUID activatorId;
protected boolean checkPlayableMode;
protected ActivatedAbilityImpl(AbilityType abilityType, Zone zone) { protected ActivatedAbilityImpl(AbilityType abilityType, Zone zone) {
super(abilityType, zone); super(abilityType, zone);
this.checkPlayableMode = false;
} }
public ActivatedAbilityImpl(final ActivatedAbilityImpl ability) { public ActivatedAbilityImpl(final ActivatedAbilityImpl ability) {
@ -58,7 +56,6 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
timing = ability.timing; timing = ability.timing;
mayActivate = ability.mayActivate; mayActivate = ability.mayActivate;
activatorId = ability.activatorId; activatorId = ability.activatorId;
checkPlayableMode = ability.checkPlayableMode;
maxActivationsPerTurn = ability.maxActivationsPerTurn; maxActivationsPerTurn = ability.maxActivationsPerTurn;
condition = ability.condition; condition = ability.condition;
} }
@ -262,16 +259,6 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
this.timing = timing; this.timing = timing;
} }
@Override
public void setCheckPlayableMode() {
checkPlayableMode = true;
}
@Override
public boolean isCheckPlayableMode() {
return checkPlayableMode;
}
protected boolean hasMoreActivationsThisTurn(Game game) { protected boolean hasMoreActivationsThisTurn(Game game) {
if (getMaxActivationsPerTurn(game) == Integer.MAX_VALUE) { if (getMaxActivationsPerTurn(game) == Integer.MAX_VALUE) {
return true; return true;

View file

@ -1,5 +1,7 @@
package mage.abilities; package mage.abilities;
import java.util.Optional;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
@ -15,9 +17,6 @@ import mage.game.events.GameEvent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import java.util.Optional;
import java.util.UUID;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -91,7 +90,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
} }
} }
// Check if rule modifying events prevent to cast the spell in check playable mode // Check if rule modifying events prevent to cast the spell in check playable mode
if (this.isCheckPlayableMode()) { if (game.inCheckPlayableState()) {
if (game.getContinuousEffects().preventedByRuleModification( if (game.getContinuousEffects().preventedByRuleModification(
GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, this.getId(), this.getSourceId(), playerId), this, game, true)) { GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, this.getId(), this.getSourceId(), playerId), this, game, true)) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();

View file

@ -1,5 +1,9 @@
package mage.abilities.effects; package mage.abilities.effects;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.*; import mage.abilities.*;
@ -23,11 +27,6 @@ import mage.players.Player;
import mage.target.common.TargetCardInHand; import mage.target.common.TargetCardInHand;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -737,10 +736,7 @@ public class ContinuousEffects implements Serializable {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
effect.setValue("targetAbility", targetAbility); effect.setValue("targetAbility", targetAbility);
if (effect.applies(event, sourceAbility, game)) { if (effect.applies(event, sourceAbility, game)) {
if (targetAbility instanceof ActivatedAbility && ((ActivatedAbility) targetAbility).isCheckPlayableMode()) { if (!game.inCheckPlayableState()) {
checkPlayableMode = true;
}
if (!checkPlayableMode) {
String message = effect.getInfoMessage(sourceAbility, event, game); String message = effect.getInfoMessage(sourceAbility, event, game);
if (message != null && !message.isEmpty()) { if (message != null && !message.isEmpty()) {
if (effect.sendMessageToUser()) { if (effect.sendMessageToUser()) {

View file

@ -1,4 +1,3 @@
package mage.abilities.effects.common.cost; package mage.abilities.effects.common.cost;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -6,7 +5,6 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import mage.Mana; import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.choices.ChoiceImpl; import mage.choices.ChoiceImpl;
@ -70,12 +68,10 @@ public class SpellsCostReductionAllEffect extends CostModificationEffectImpl {
@Override @Override
public boolean apply(Game game, Ability source, Ability abilityToModify) { public boolean apply(Game game, Ability source, Ability abilityToModify) {
if (upTo) { if (upTo) {
if (abilityToModify instanceof ActivatedAbility) { if (game.inCheckPlayableState()) {
if (((ActivatedAbility) abilityToModify).isCheckPlayableMode()) {
CardUtil.reduceCost(abilityToModify, this.amount); CardUtil.reduceCost(abilityToModify, this.amount);
return true; return true;
} }
}
Mana mana = abilityToModify.getManaCostsToPay().getMana(); Mana mana = abilityToModify.getManaCostsToPay().getMana();
int reduceMax = mana.getGeneric(); int reduceMax = mana.getGeneric();
if (reduceMax > 2) { if (reduceMax > 2) {

View file

@ -1,4 +1,3 @@
package mage.abilities.effects.common.cost; package mage.abilities.effects.common.cost;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -6,7 +5,6 @@ import java.util.Set;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCosts;
@ -85,7 +83,7 @@ public class SpellsCostReductionControllerEffect extends CostModificationEffectI
return false; return false;
} }
int reduce = reduceMax; int reduce = reduceMax;
if (!(abilityToModify instanceof ActivatedAbility) || !((ActivatedAbility) abilityToModify).isCheckPlayableMode()) { if (!game.inCheckPlayableState()) {
ChoiceImpl choice = new ChoiceImpl(false); ChoiceImpl choice = new ChoiceImpl(false);
Set<String> set = new LinkedHashSet<>(); Set<String> set = new LinkedHashSet<>();
for (int i = 0; i <= amount; i++) { for (int i = 0; i <= amount; i++) {

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.StaticAbility; import mage.abilities.StaticAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
@ -23,8 +24,6 @@ import mage.players.Player;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.UUID;
/** /**
* @author LevelX2 * @author LevelX2
* <p> * <p>
@ -122,7 +121,7 @@ class HideawayLookAtFaceDownCardEffect extends AsThoughEffectImpl {
public HideawayLookAtFaceDownCardEffect() { public HideawayLookAtFaceDownCardEffect() {
super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.Benefit); super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.Benefit);
staticText = "You may look at cards exiled with {this}"; staticText = "You may look at the cards exiled with {this}";
} }
private HideawayLookAtFaceDownCardEffect(final HideawayLookAtFaceDownCardEffect effect) { private HideawayLookAtFaceDownCardEffect(final HideawayLookAtFaceDownCardEffect effect) {

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import java.util.UUID;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
@ -17,8 +18,6 @@ import mage.target.Target;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledCreaturePermanent;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.UUID;
/** /**
* 702.46. Offering # 702.46a Offering is a static ability of a card that * 702.46. Offering # 702.46a Offering is a static ability of a card that
* functions in any zone from which the card can be cast. "[Subtype] offering" * functions in any zone from which the card can be cast. "[Subtype] offering"
@ -121,7 +120,7 @@ class OfferingAsThoughEffect extends AsThoughEffectImpl {
if (game.getBattlefield().count(((OfferingAbility) source).getFilter(), source.getSourceId(), source.getControllerId(), game) > 0) { if (game.getBattlefield().count(((OfferingAbility) source).getFilter(), source.getSourceId(), source.getControllerId(), game) > 0) {
if (CardUtil.isCheckPlayableMode(affectedAbility)) { if (game.inCheckPlayableState()) {
return true; return true;
} }
FilterControlledCreaturePermanent filter = ((OfferingAbility) source).getFilter(); FilterControlledCreaturePermanent filter = ((OfferingAbility) source).getFilter();
@ -130,7 +129,7 @@ class OfferingAsThoughEffect extends AsThoughEffectImpl {
return false; return false;
} }
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
if (player != null && !CardUtil.isCheckPlayableMode(affectedAbility) if (player != null && !game.inCheckPlayableState()
&& player.chooseUse(Outcome.Benefit, "Offer a " + filter.getMessage() + " to cast " + spellToCast.getName() + '?', source, game)) { && player.chooseUse(Outcome.Benefit, "Offer a " + filter.getMessage() + " to cast " + spellToCast.getName() + '?', source, game)) {
Target target = new TargetControlledCreaturePermanent(1, 1, filter, true); Target target = new TargetControlledCreaturePermanent(1, 1, filter, true);
player.chooseTarget(Outcome.Sacrifice, target, source, game); player.chooseTarget(Outcome.Sacrifice, target, source, game);
@ -193,7 +192,7 @@ class OfferingCostReductionEffect extends CostModificationEffectImpl {
@Override @Override
public boolean applies(Ability abilityToModify, Ability source, Game game) { public boolean applies(Ability abilityToModify, Ability source, Game game) {
if (CardUtil.isCheckPlayableMode(abilityToModify)) { // Cost modifaction does not work correctly for checking available spells if (game.inCheckPlayableState()) { // Cost modifaction does not work correctly for checking available spells
return false; return false;
} }
if (abilityToModify.getId().equals(spellAbilityId) && abilityToModify instanceof SpellAbility) { if (abilityToModify.getId().equals(spellAbilityId) && abilityToModify instanceof SpellAbility) {

View file

@ -1,5 +1,8 @@
package mage.game; package mage.game;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import mage.MageItem; import mage.MageItem;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -41,10 +44,6 @@ import mage.players.Players;
import mage.util.MessageToClient; import mage.util.MessageToClient;
import mage.util.functions.ApplyToPermanent; import mage.util.functions.ApplyToPermanent;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
public interface Game extends MageItem, Serializable { public interface Game extends MageItem, Serializable {
MatchType getGameType(); MatchType getGameType();
@ -131,7 +130,6 @@ public interface Game extends MageItem, Serializable {
} }
default boolean isActivePlayer(UUID playerId) { default boolean isActivePlayer(UUID playerId) {
return getActivePlayerId() != null && getActivePlayerId().equals(playerId); return getActivePlayerId() != null && getActivePlayerId().equals(playerId);
} }
@ -202,7 +200,11 @@ public interface Game extends MageItem, Serializable {
boolean isSimulation(); boolean isSimulation();
void setSimulation(boolean simulation); void setSimulation(boolean checkPlayableState);
boolean inCheckPlayableState();
void setCheckPlayableState(boolean checkPlayableState);
MageObject getLastKnownInformation(UUID objectId, Zone zone); MageObject getLastKnownInformation(UUID objectId, Zone zone);

View file

@ -1,5 +1,9 @@
package mage.game; package mage.game;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import mage.MageException; import mage.MageException;
import mage.MageObject; import mage.MageObject;
import mage.abilities.*; import mage.abilities.*;
@ -65,11 +69,6 @@ import mage.util.functions.ApplyToPermanent;
import mage.watchers.common.*; import mage.watchers.common.*;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
public abstract class GameImpl implements Game, Serializable { public abstract class GameImpl implements Game, Serializable {
private static final int ROLLBACK_TURNS_MAX = 4; private static final int ROLLBACK_TURNS_MAX = 4;
@ -77,7 +76,9 @@ public abstract class GameImpl implements Game, Serializable {
private static final Logger logger = Logger.getLogger(GameImpl.class); private static final Logger logger = Logger.getLogger(GameImpl.class);
private transient Object customData; private transient Object customData;
protected boolean simulation = false; protected boolean simulation = false;
protected boolean checkPlayableState = false;
protected final UUID id; protected final UUID id;
@ -163,6 +164,7 @@ public abstract class GameImpl implements Game, Serializable {
this.state = game.state.copy(); this.state = game.state.copy();
this.gameCards = game.gameCards; this.gameCards = game.gameCards;
this.simulation = game.simulation; this.simulation = game.simulation;
this.checkPlayableState = game.checkPlayableState;
this.gameOptions = game.gameOptions; this.gameOptions = game.gameOptions;
this.lki.putAll(game.lki); this.lki.putAll(game.lki);
this.lkiExtended.putAll(game.lkiExtended); this.lkiExtended.putAll(game.lkiExtended);
@ -188,6 +190,16 @@ public abstract class GameImpl implements Game, Serializable {
this.simulation = simulation; this.simulation = simulation;
} }
@Override
public void setCheckPlayableState(boolean checkPlayableState) {
this.checkPlayableState = checkPlayableState;
}
@Override
public boolean inCheckPlayableState() {
return checkPlayableState;
}
@Override @Override
public UUID getId() { public UUID getId() {
return id; return id;
@ -2444,8 +2456,8 @@ public abstract class GameImpl implements Game, Serializable {
* exist. Then, if there are any objects still controlled by that player, * exist. Then, if there are any objects still controlled by that player,
* those objects are exiled. This is not a state-based action. It happens as * those objects are exiled. This is not a state-based action. It happens as
* soon as the player leaves the game. If the player who left the game had * soon as the player leaves the game. If the player who left the game had
* priority at the time they left, priority passes to the next player * priority at the time they left, priority passes to the next player in
* in turn order who's still in the game. # * turn order who's still in the game. #
* *
* @param playerId * @param playerId
*/ */

View file

@ -1,6 +1,9 @@
package mage.players; package mage.players;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import mage.ConditionalMana; import mage.ConditionalMana;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
@ -65,10 +68,6 @@ import mage.util.GameLog;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
public abstract class PlayerImpl implements Player, Serializable { public abstract class PlayerImpl implements Player, Serializable {
private static final Logger logger = Logger.getLogger(PlayerImpl.class); private static final Logger logger = Logger.getLogger(PlayerImpl.class);
@ -1589,6 +1588,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public LinkedHashMap<UUID, ActivatedAbility> getUseableActivatedAbilities(MageObject object, Zone zone, Game game) { public LinkedHashMap<UUID, ActivatedAbility> getUseableActivatedAbilities(MageObject object, Zone zone, Game game) {
game.setCheckPlayableState(true);
LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>(); LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>();
if (object instanceof StackAbility) { // It may not be possible to activate abilities of stack abilities if (object instanceof StackAbility) { // It may not be possible to activate abilities of stack abilities
return useable; return useable;
@ -1606,7 +1606,7 @@ public abstract class PlayerImpl implements Player, Serializable {
zone, game, object.getAbilities(), useable); zone, game, object.getAbilities(), useable);
} }
getOtherUseableActivatedAbilities(object, zone, game, useable); getOtherUseableActivatedAbilities(object, zone, game, useable);
game.setCheckPlayableState(false);
return useable; return useable;
} }
@ -3049,8 +3049,7 @@ public abstract class PlayerImpl implements Player, Serializable {
*/ */
protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) { protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) {
if (!(ability instanceof ActivatedManaAbilityImpl)) { if (!(ability instanceof ActivatedManaAbilityImpl)) {
ActivatedAbility copy = ability.copy(); ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability
copy.setCheckPlayableMode(); // prevents from endless loops for asking player to use effects by checking this mode
if (!copy.canActivate(playerId, game).canActivate()) { if (!copy.canActivate(playerId, game).canActivate()) {
return false; return false;
} }
@ -3330,7 +3329,7 @@ public abstract class PlayerImpl implements Player, Serializable {
public List<Ability> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { public List<Ability> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) {
List<Ability> playable = new ArrayList<>(); List<Ability> playable = new ArrayList<>();
game.setCheckPlayableState(true);
if (!shouldSkipGettingPlayable(game)) { if (!shouldSkipGettingPlayable(game)) {
ManaOptions availableMana = getManaAvailable(game); ManaOptions availableMana = getManaAvailable(game);
availableMana.addMana(manaPool.getMana()); availableMana.addMana(manaPool.getMana());
@ -3455,17 +3454,19 @@ public abstract class PlayerImpl implements Player, Serializable {
playable.addAll(activatedAll); playable.addAll(activatedAll);
} }
} }
game.setCheckPlayableState(false);
return playable; return playable;
} }
/** /**
* Creates a list of card ids that are currently playable.<br> * Creates a list of card ids that are currently playable.<br>
* Used to mark the playable cards in GameView * Used to mark the playable cards in GameView Also contains number of
* Also contains number of playable abilities for that object (it's just info, server decides to show choose dialog or not) * playable abilities for that object (it's just info, server decides to
* show choose dialog or not)
* *
* @param game * @param game
* @return A Set of cardIds that are playable and amount of playable abilities * @return A Set of cardIds that are playable and amount of playable
* abilities
*/ */
@Override @Override
public Map<UUID, Integer> getPlayableObjects(Game game, Zone zone) { public Map<UUID, Integer> getPlayableObjects(Game game, Zone zone) {
@ -3747,11 +3748,10 @@ public abstract class PlayerImpl implements Player, Serializable {
public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) {
if (null != game.getContinuousEffects().asThough(card.getId(), if (null != game.getContinuousEffects().asThough(card.getId(),
AsThoughEffectType.LOOK_AT_FACE_DOWN, card.getSpellAbility(), this.getId(), game)) { AsThoughEffectType.LOOK_AT_FACE_DOWN, card.getSpellAbility(), this.getId(), game)) {
// two modes: look at card or not to look and activate other abilities // two modes: look at the card or do not look and activate other abilities
String lookMessage = abilitiesToActivate > 0 ? "Look at that card (it's have " String lookMessage = "Look at " + card.getIdName();
+ abilitiesToActivate + " abilities to activate)?" : "Look at that card?"; String lookYes = "Yes, look at the card";
String lookYes = "Yes, look at card"; String lookNo = "No, play/activate the card/ability";
String lookNo = abilitiesToActivate > 0 ? "No, activate ability" : "No";
if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) { if (chooseUse(Outcome.Benefit, lookMessage, "", lookYes, lookNo, null, game)) {
Cards cards = new CardsImpl(card); Cards cards = new CardsImpl(card);
this.lookAtCards(getName() + " - " + card.getIdName() + " - " this.lookAtCards(getName() + " - " + card.getIdName() + " - "

View file

@ -1,9 +1,11 @@
package mage.util; package mage.util;
import java.text.SimpleDateFormat;
import java.util.Objects;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.costs.VariableCost; import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.*; import mage.abilities.costs.mana.*;
@ -15,10 +17,6 @@ import mage.game.permanent.Permanent;
import mage.game.permanent.token.Token; import mage.game.permanent.token.Token;
import mage.util.functions.CopyTokenFunction; import mage.util.functions.CopyTokenFunction;
import java.text.SimpleDateFormat;
import java.util.Objects;
import java.util.UUID;
/** /**
* @author nantuko * @author nantuko
*/ */
@ -489,20 +487,6 @@ public final class CardUtil {
return uniqueString.toString(); return uniqueString.toString();
} }
/**
* Returns if the ability is used to check which cards are playable on hand.
* (Issue #457)
*
* @param ability - ability to check
* @return
*/
public static boolean isCheckPlayableMode(Ability ability) {
if (ability instanceof ActivatedAbility) {
return ((ActivatedAbility) ability).isCheckPlayableMode();
}
return false;
}
/** /**
* Adds tags to mark the additional info of a card (e.g. blue font color) * Adds tags to mark the additional info of a card (e.g. blue font color)
* *
@ -560,7 +544,8 @@ public final class CardUtil {
} }
/** /**
* Face down cards and their copy tokens don't have names and that's "empty" names is not equals * Face down cards and their copy tokens don't have names and that's "empty"
* names is not equals
*/ */
public static boolean haveSameNames(String name1, String name2, Boolean ignoreMtgRuleForEmptyNames) { public static boolean haveSameNames(String name1, String name2, Boolean ignoreMtgRuleForEmptyNames) {
if (ignoreMtgRuleForEmptyNames) { if (ignoreMtgRuleForEmptyNames) {