diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index 50cc2a4ba2..77f7ecfdf9 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -118,7 +118,12 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } Player player = game.getPlayer(playerId); - logger.info(new StringBuilder("[").append(game.getPlayer(playerId).getName()).append("], life = ").append(player.getLife()).toString()); + GameStateEvaluator2.PlayerEvaluateScore score = GameStateEvaluator2.evaluate(playerId, game); + logger.info(new StringBuilder("[").append(game.getPlayer(playerId).getName()).append("]") + .append(", life = ").append(player.getLife()) + .append(", score = ").append(score.getTotalScore()) + .append(" (").append(score.getPlayerInfoFull()).append(")") + .toString()); StringBuilder sb = new StringBuilder("-> Hand: ["); for (Card card : player.getHand().getCards(game)) { sb.append(card.getName()).append(';'); @@ -135,6 +140,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { if (permanent.isAttacking()) { sb.append("(attacking)"); } + sb.append(':' + String.valueOf(GameStateEvaluator2.evaluatePermanent(permanent, game))); sb.append(';'); } } @@ -200,13 +206,13 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { && Thread.interrupted()) { Thread.currentThread().interrupt(); logger.debug("interrupted"); - return GameStateEvaluator2.evaluate(playerId, game); + return GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); } // Condition to stop deeper simulation if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.checkIfGameIsOver()) { - val = GameStateEvaluator2.evaluate(playerId, game); + val = GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); if (logger.isTraceEnabled()) { StringBuilder sb = new StringBuilder("Add Actions -- reached end state <").append(val).append('>'); SimulationNode2 logNode = node; @@ -240,20 +246,20 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } if (game.checkIfGameIsOver()) { - val = GameStateEvaluator2.evaluate(playerId, game); + val = GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); } else if (stepFinished) { logger.debug("Step finished"); - int testScore = GameStateEvaluator2.evaluate(playerId, game); + int testScore = GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); if (game.isActivePlayer(playerId)) { if (testScore < currentScore) { // if score at end of step is worse than original score don't check further //logger.debug("Add Action -- abandoning check, no immediate benefit"); val = testScore; } else { - val = GameStateEvaluator2.evaluate(playerId, game); + val = GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); } } else { - val = GameStateEvaluator2.evaluate(playerId, game); + val = GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); } } else if (!node.getChildren().isEmpty()) { if (logger.isDebugEnabled()) { @@ -445,7 +451,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { && Thread.interrupted()) { Thread.currentThread().interrupt(); logger.info("interrupted"); - return GameStateEvaluator2.evaluate(playerId, game); + return GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); } node.setGameValue(game.getState().getValue(true).hashCode()); SimulatedPlayer2 currentPlayer = (SimulatedPlayer2) game.getPlayer(game.getPlayerList().get()); @@ -490,7 +496,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { int val; if (action instanceof PassAbility && sim.getStack().isEmpty()) { // Stop to simulate deeper if PassAbility and stack is empty - val = GameStateEvaluator2.evaluate(this.getId(), sim); + val = GameStateEvaluator2.evaluate(this.getId(), sim).getTotalScore(); } else { val = addActions(newNode, depth - 1, alpha, beta); } @@ -533,7 +539,9 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { bestNode.setCombat(newNode.getChildren().get(0).getCombat()); } if (depth == maxDepth) { - logger.info("Sim Prio [" + depth + "] -- Saved best node yet <" + bestNode.getScore() + "> " + bestNode.getAbilities().toString()); + GameStateEvaluator2.PlayerEvaluateScore score = GameStateEvaluator2.evaluate(this.getId(), bestNode.game); + String scoreInfo = " [" + score.getPlayerInfoShort() + "-" + score.getOpponentInfoShort() + "]"; + logger.info("Sim Prio [" + depth + "] -- Saved best node yet <" + bestNode.getScore() + scoreInfo + "> " + bestNode.getAbilities().toString()); node.children.clear(); node.children.add(bestNode); node.setScore(bestNode.getScore()); @@ -933,7 +941,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { if (action instanceof PassAbility || action instanceof SpellAbility || action.getAbilityType() == AbilityType.MANA) { return false; } - int newVal = GameStateEvaluator2.evaluate(playerId, sim); + int newVal = GameStateEvaluator2.evaluate(playerId, sim).getTotalScore(); SimulationNode2 test = node.getParent(); while (test != null) { if (test.getPlayerId().equals(playerId)) { @@ -942,7 +950,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { if (test.getParent() != null) { Game prevGame = node.getGame(); if (prevGame != null) { - int oldVal = GameStateEvaluator2.evaluate(playerId, prevGame); + int oldVal = GameStateEvaluator2.evaluate(playerId, prevGame).getTotalScore(); if (oldVal >= newVal) { return true; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java index 7d2ea07761..84dfc55a9e 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java @@ -108,7 +108,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { protected void calculateActions(Game game) { if (!getNextAction(game)) { Date startTime = new Date(); - currentScore = GameStateEvaluator2.evaluate(playerId, game); + currentScore = GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); Game sim = createSimulation(game); SimulationNode2.resetCount(); root = new SimulationNode2(null, sim, maxDepth, playerId); diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java index 77a0705001..0997159e82 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java @@ -4,19 +4,18 @@ */ package mage.player.ai; -import java.util.UUID; import mage.game.Game; import mage.game.permanent.Permanent; import mage.player.ai.ma.ArtificialScoringSystem; import mage.players.Player; import org.apache.log4j.Logger; +import java.util.UUID; + /** - * * @author nantuko - * + *

* This evaluator is only good for two player games - * */ public final class GameStateEvaluator2 { @@ -25,43 +24,47 @@ public final class GameStateEvaluator2 { public static final int WIN_GAME_SCORE = 100000000; public static final int LOSE_GAME_SCORE = -WIN_GAME_SCORE; - public static int evaluate(UUID playerId, Game game) { + public static PlayerEvaluateScore evaluate(UUID playerId, Game game) { Player player = game.getPlayer(playerId); - Player opponent = game.getPlayer(game.getOpponents(playerId).iterator().next()); + Player opponent = game.getPlayer(game.getOpponents(playerId).iterator().next()); // TODO: add multi opponent support? if (game.checkIfGameIsOver()) { - if (player.hasLost() + if (player.hasLost() || opponent.hasWon()) { - return LOSE_GAME_SCORE; + return new PlayerEvaluateScore(LOSE_GAME_SCORE); } - if (opponent.hasLost() + if (opponent.hasLost() || player.hasWon()) { - return WIN_GAME_SCORE; + return new PlayerEvaluateScore(WIN_GAME_SCORE); } } - int lifeScore = 0; + + int playerLifeScore = 0; + int opponentLifeScore = 0; if (player.getLife() <= 0) { // we don't want a tie - lifeScore = ArtificialScoringSystem.LOSE_GAME_SCORE; + playerLifeScore = ArtificialScoringSystem.LOSE_GAME_SCORE; } else if (opponent.getLife() <= 0) { - lifeScore = ArtificialScoringSystem.WIN_GAME_SCORE; + playerLifeScore = ArtificialScoringSystem.WIN_GAME_SCORE; } else { - lifeScore = ArtificialScoringSystem.getLifeScore(player.getLife()) - ArtificialScoringSystem.getLifeScore(opponent.getLife()); + playerLifeScore = ArtificialScoringSystem.getLifeScore(player.getLife()); + opponentLifeScore = ArtificialScoringSystem.getLifeScore(opponent.getLife()); // TODO: minus } - int permanentScore = 0; - int playerScore = 0; - int opponentScore = 0; + + int playerPermanentsScore = 0; + int opponentPermanentsScore = 0; try { StringBuilder sbPlayer = new StringBuilder(); StringBuilder sbOpponent = new StringBuilder(); + // add values of player for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { int onePermScore = evaluatePermanent(permanent, game); - playerScore += onePermScore; + playerPermanentsScore += onePermScore; if (logger.isDebugEnabled()) { sbPlayer.append(permanent.getName()).append('[').append(onePermScore).append("] "); } } if (logger.isDebugEnabled()) { - sbPlayer.insert(0, playerScore + " - "); + sbPlayer.insert(0, playerPermanentsScore + " - "); sbPlayer.insert(0, "Player..: "); logger.debug(sbPlayer); } @@ -69,27 +72,32 @@ public final class GameStateEvaluator2 { // add values of opponent for (Permanent permanent : game.getBattlefield().getAllActivePermanents(opponent.getId())) { int onePermScore = evaluatePermanent(permanent, game); - opponentScore += onePermScore; + opponentPermanentsScore += onePermScore; if (logger.isDebugEnabled()) { sbOpponent.append(permanent.getName()).append('[').append(onePermScore).append("] "); } } if (logger.isDebugEnabled()) { - sbOpponent.insert(0, opponentScore + " - "); + sbOpponent.insert(0, opponentPermanentsScore + " - "); sbOpponent.insert(0, "Opponent: "); - logger.debug(sbOpponent); } - permanentScore = playerScore - opponentScore; } catch (Throwable t) { } - int handScore; - handScore = player.getHand().size() - opponent.getHand().size(); - handScore *= 5; - int score = lifeScore + permanentScore + handScore; - logger.debug(score + " total Score (life:" + lifeScore + " permanents:" + permanentScore + " hand:" + handScore + ')'); - return score; + int playerHandScore = player.getHand().size() * 5; + int opponentHandScore = opponent.getHand().size() * 5; + + int score = (playerLifeScore - opponentLifeScore) + + (playerPermanentsScore - opponentPermanentsScore) + + (playerHandScore - opponentHandScore); + logger.debug(score + + " total Score (life:" + (playerLifeScore - opponentLifeScore) + + " permanents:" + (playerPermanentsScore - opponentPermanentsScore) + + " hand:" + (playerHandScore - opponentHandScore) + ')'); + return new PlayerEvaluateScore( + playerLifeScore, playerHandScore, playerPermanentsScore, + opponentLifeScore, opponentHandScore, opponentPermanentsScore); } public static int evaluatePermanent(Permanent permanent, Game game) { @@ -104,4 +112,81 @@ public final class GameStateEvaluator2 { return value; } + public static class PlayerEvaluateScore { + private int playerLifeScore = 0; + private int playerHandScore = 0; + private int playerPermanentsScore = 0; + + private int opponentLifeScore = 0; + private int opponentHandScore = 0; + private int opponentPermanentsScore = 0; + + private int specialScore = 0; // special score (ignore all other) + + public PlayerEvaluateScore(int specialScore) { + this.specialScore = specialScore; + } + + public PlayerEvaluateScore(int playerLifeScore, int playerHandScore, int playerPermanentsScore, + int opponentLifeScore, int opponentHandScore, int opponentPermanentsScore) { + this.playerLifeScore = playerLifeScore; + this.playerHandScore = playerHandScore; + this.playerPermanentsScore = playerPermanentsScore; + this.opponentLifeScore = opponentLifeScore; + this.opponentHandScore = opponentHandScore; + this.opponentPermanentsScore = opponentPermanentsScore; + } + + public int getPlayerScore() { + return playerLifeScore + playerHandScore + playerPermanentsScore; + } + + public int getOpponentScore() { + return opponentLifeScore + opponentHandScore + opponentPermanentsScore; + } + + public int getTotalScore() { + if (specialScore != 0) { + return specialScore; + } else { + return getPlayerScore() - getOpponentScore(); + } + } + + public int getPlayerLifeScore() { + return playerLifeScore; + } + + public int getPlayerHandScore() { + return playerHandScore; + } + + public int getPlayerPermanentsScore() { + return playerPermanentsScore; + } + + public String getPlayerInfoFull() { + return "Life:" + playerLifeScore + + ", Hand:" + playerHandScore + + ", Perm:" + playerPermanentsScore; + } + + public String getPlayerInfoShort() { + return "L:" + playerLifeScore + + ",H:" + playerHandScore + + ",P:" + playerPermanentsScore; + } + + public String getOpponentInfoFull() { + return "Life:" + opponentLifeScore + + ", Hand:" + opponentHandScore + + ", Perm:" + opponentPermanentsScore; + } + + public String getOpponentInfoShort() { + return "L:" + opponentLifeScore + + ",H:" + opponentHandScore + + ",P:" + opponentPermanentsScore; + } + } } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 4ee2440d33..d8ee5251a5 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -264,6 +264,8 @@ public class SimulatedPlayer2 extends ComputerPlayer { Iterator iterator = options.iterator(); boolean bad = true; boolean good = true; + + // TODO: add custom outcome from ability? for (Effect effect : ability.getEffects()) { if (effect.getOutcome().isGood()) { bad = false; diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java index 379dadee6a..f9f4103705 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java @@ -1,17 +1,17 @@ package mage.player.ai.ma; -import java.util.UUID; +import mage.MageObject; import mage.abilities.Ability; -import mage.abilities.effects.Effect; import mage.abilities.keyword.HasteAbility; import mage.cards.Card; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SubType; import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; +import java.util.UUID; + /** * @author ubeefx, nantuko */ @@ -20,7 +20,7 @@ public final class ArtificialScoringSystem { public static final int WIN_GAME_SCORE = 100000000; public static final int LOSE_GAME_SCORE = -WIN_GAME_SCORE; - private static final int LIFE_SCORES[] = {0, 1000, 2000, 3000, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 7400, 7800, 8200, 8600, 9000, 9200, 9400, 9600, 9800, 10000}; + private static final int[] LIFE_SCORES = {0, 1000, 2000, 3000, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 7400, 7800, 8200, 8600, 9000, 9200, 9400, 9600, 9800, 10000}; private static final int MAX_LIFE = LIFE_SCORES.length - 1; private static final int UNKNOWN_CARD_SCORE = 300; private static final int PERMANENT_SCORE = 300; @@ -79,29 +79,21 @@ public final class ArtificialScoringSystem { abilityScore += MagicAbility.getAbilityScore(ability); } score += power * 300 + getPositive(toughness) * 200 + abilityScore * (getPositive(power) + 1) / 2; - //TODO: it can be improved - //score += permanent.getEquipmentPermanents().size() * 50 + permanent.getAuraPermanents().size() * 100; int enchantments = 0; int equipments = 0; for (UUID uuid : permanent.getAttachments()) { - Card card = game.getCard(uuid); - if (card != null) { + MageObject object = game.getObject(uuid); + if (object instanceof Card) { + Card card = (Card) object; + int outcomeScore = object.getAbilities().getOutcomeTotal(); if (card.getCardType().contains(CardType.ENCHANTMENT)) { - Effect effect = card.getSpellAbility().getEffects().get(0); - if (effect != null) { - Outcome outcome = effect.getOutcome(); - if (outcome.isGood()) { - enchantments++; - } else if (outcome != Outcome.Detriment) { - enchantments--; - } - } + enchantments = enchantments + outcomeScore * 100; } else { - equipments++; + equipments = equipments + outcomeScore * 50; } } } - score += equipments * 50 + enchantments * 100; + score += equipments + enchantments; if (!permanent.canAttack(null, game)) { score -= 100; diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/MagicAbility.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/MagicAbility.java index bb854bf370..43fae122d1 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/MagicAbility.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/MagicAbility.java @@ -38,13 +38,13 @@ public final class MagicAbility { // gatecrash put(new EvolveAbility().getRule(), 50); put(new ExtortAbility().getRule(), 30); - + }}; public static int getAbilityScore(Ability ability) { if (!scores.containsKey(ability.getRule())) { - //System.err.println("Couldn't find ability score: " + ability.getRule()); + //System.err.println("Couldn't find ability score: " + ability.getClass().getSimpleName() + " - " + ability.toString()); //TODO: add handling protection from ..., levelup, kicker, etc. abilities return 0; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java index c80f585cc7..c6aed09cfa 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java @@ -6,24 +6,25 @@ package mage.player.ai.ma.optimizers.impl; -import java.util.List; import mage.abilities.Ability; import mage.abilities.effects.Effect; import mage.constants.Outcome; import mage.game.Game; +import java.util.List; + /** * Removes abilities that require only discard a card for activation. * - * @author LevelX2 + * @author LevelX2 */ public class OutcomeOptimizer extends BaseTreeOptimizer { @Override public void filter(Game game, List actions) { for (Ability ability : actions) { - for (Effect effect: ability.getEffects()) { - if (effect.getOutcome() == Outcome.AIDontUseIt) { + for (Effect effect : ability.getEffects()) { + if (ability.getCustomOutcome() == Outcome.AIDontUseIt || effect.getOutcome() == Outcome.AIDontUseIt) { removeAbility(ability); break; } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 4b27f6bfd8..bae4133820 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -1160,12 +1160,12 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (!playableAbilities.isEmpty()) { for (ActivatedAbility ability : playableAbilities) { if (ability.canActivate(playerId, game).canActivate()) { - if (ability.getEffects().hasOutcome(Outcome.PutLandInPlay)) { + if (ability.getEffects().hasOutcome(ability, Outcome.PutLandInPlay)) { if (this.activateAbility(ability, game)) { return true; } } - if (ability.getEffects().hasOutcome(Outcome.PutCreatureInPlay)) { + if (ability.getEffects().hasOutcome(ability, Outcome.PutCreatureInPlay)) { if (getOpponentBlockers(opponentId, game).size() <= 1) { if (this.activateAbility(ability, game)) { return true; diff --git a/Mage.Server/src/main/java/mage/server/util/SystemUtil.java b/Mage.Server/src/main/java/mage/server/util/SystemUtil.java index 73ecbb7b1b..9cc3efdf8c 100644 --- a/Mage.Server/src/main/java/mage/server/util/SystemUtil.java +++ b/Mage.Server/src/main/java/mage/server/util/SystemUtil.java @@ -1,15 +1,8 @@ package mage.server.util; -import java.io.File; -import java.lang.reflect.Constructor; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; +import mage.MageObject; import mage.abilities.Ability; +import mage.abilities.ActivatedAbility; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.cards.Card; @@ -30,6 +23,16 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.RandomUtil; +import java.io.File; +import java.lang.reflect.Constructor; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + /** * @author JayDi85 */ @@ -43,25 +46,28 @@ public final class SystemUtil { private static final String INIT_FILE_PATH = "config" + File.separator + "init.txt"; private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(SystemUtil.class); - private static final String COMMAND_MANA_ADD = "@mana add"; - private static final String COMMAND_LANDS_ADD = "@lands add"; - private static final String COMMAND_RUN_CUSTOM_CODE = "@run custom code"; - private static final String COMMAND_CLEAR_BATTLEFIELD = "@clear battlefield"; + // transform special group names from init.txt to special commands in choose dialog: + // [@mana add] -> MANA ADD + private static final String COMMAND_MANA_ADD = "@mana add"; // TODO: not implemented + private static final String COMMAND_LANDS_ADD = "@lands add"; // TODO: not implemented + private static final String COMMAND_RUN_CUSTOM_CODE = "@run custom code"; // TODO: not implemented private static final String COMMAND_SHOW_OPPONENT_HAND = "@show opponent hand"; private static final String COMMAND_SHOW_OPPONENT_LIBRARY = "@show opponent library"; private static final String COMMAND_SHOW_MY_HAND = "@show my hand"; private static final String COMMAND_SHOW_MY_LIBRARY = "@show my library"; + private static final String COMMAND_ACTIVATE_OPPONENT_ABILITY = "@activate opponent ability"; private static final Map supportedCommands = new HashMap<>(); static { + // special commands names in choose dialog supportedCommands.put(COMMAND_MANA_ADD, "MANA ADD"); supportedCommands.put(COMMAND_LANDS_ADD, "LANDS ADD"); supportedCommands.put(COMMAND_RUN_CUSTOM_CODE, "RUN CUSTOM CODE"); - supportedCommands.put(COMMAND_CLEAR_BATTLEFIELD, "CLAR BATTLEFIELD"); supportedCommands.put(COMMAND_SHOW_OPPONENT_HAND, "SHOW OPPONENT HAND"); supportedCommands.put(COMMAND_SHOW_OPPONENT_LIBRARY, "SHOW OPPONENT LIBRARY"); supportedCommands.put(COMMAND_SHOW_MY_HAND, "SHOW MY HAND"); supportedCommands.put(COMMAND_SHOW_MY_LIBRARY, "SHOW MY LIBRARY"); + supportedCommands.put(COMMAND_ACTIVATE_OPPONENT_ABILITY, "ACTIVATE OPPONENT ABILITY"); } private static final Pattern patternGroup = Pattern.compile("\\[(.+)\\]"); // [test new card] @@ -392,6 +398,54 @@ public final class SystemUtil { game.informPlayer(feedbackPlayer, info); } break; + + case COMMAND_ACTIVATE_OPPONENT_ABILITY: + // WARNING, maybe very bugged if called in wrong priority + // uses choose triggered ability dialog to select it + if (feedbackPlayer != null) { + UUID savedPriorityPlayer = null; + if (game.getActivePlayerId() != opponent.getId()) { + savedPriorityPlayer = game.getActivePlayerId(); + } + + // change active player to find and play selected abilities (it's danger and buggy code) + if (savedPriorityPlayer != null) { + game.getState().setPriorityPlayerId(opponent.getId()); + game.firePriorityEvent(opponent.getId()); + } + + List abilities = opponent.getPlayable(game, true); + Map choices = new HashMap<>(); + abilities.forEach(ability -> { + MageObject object = ability.getSourceObject(game); + choices.put(ability.getId().toString(), object.getName() + ": " + ability.toString()); + }); + // TODO: set priority for us? + Choice choice = new ChoiceImpl(); + choice.setMessage("Choose playable ability to active by opponent " + opponent.getName()); + choice.setKeyChoices(choices); + if (feedbackPlayer.choose(Outcome.Detriment, choice, game) && choice.getChoiceKey() != null) { + String needId = choice.getChoiceKey(); + Optional ability = abilities.stream().filter(a -> a.getId().toString().equals(needId)).findFirst(); + if (ability.isPresent()) { + // TODO: set priority for player? + ActivatedAbility activatedAbility = (ActivatedAbility) ability.get(); + game.informPlayers(feedbackPlayer.getLogName() + " as another player " + opponent.getLogName() + + " trying to force an activate ability: " + activatedAbility.getGameLogMessage(game)); + if (opponent.activateAbility(activatedAbility, game)) { + game.informPlayers("Force to activate ability: DONE"); + } else { + game.informPlayers("Force to activate ability: FAIL"); + } + } + } + // restore original priority player + if (savedPriorityPlayer != null) { + game.getState().setPriorityPlayerId(savedPriorityPlayer); + game.firePriorityEvent(savedPriorityPlayer); + } + } + break; } return; @@ -438,8 +492,8 @@ public final class SystemUtil { // Steps: 1) Remove the last plane and set its effects to discarded for (CommandObject cobject : game.getState().getCommand()) { if (cobject instanceof Plane) { - if (((Plane) cobject).getAbilities() != null) { - for (Ability ability : ((Plane) cobject).getAbilities()) { + if (cobject.getAbilities() != null) { + for (Ability ability : cobject.getAbilities()) { for (Effect effect : ability.getEffects()) { if (effect instanceof ContinuousEffect) { ((ContinuousEffect) effect).discard(); @@ -447,7 +501,7 @@ public final class SystemUtil { } } } - game.getState().removeTriggersOfSourceId(((Plane) cobject).getId()); + game.getState().removeTriggersOfSourceId(cobject.getId()); game.getState().getCommand().remove(cobject); break; } @@ -629,8 +683,8 @@ public final class SystemUtil { /** * Get a diff between two dates * - * @param date1 the oldest date - * @param date2 the newest date + * @param date1 the oldest date + * @param date2 the newest date * @param timeUnit the unit in which you want the diff * @return the diff value, in the provided unit */ diff --git a/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java b/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java index 3615a8eeba..e9d90de7c4 100644 --- a/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java +++ b/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java @@ -1,7 +1,5 @@ - package mage.cards.k; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; @@ -20,8 +18,9 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInHand; +import java.util.UUID; + /** - * * @author Backfir3 */ public final class KaaliaOfTheVast extends CardImpl { @@ -73,9 +72,7 @@ class KaaliaOfTheVastAttacksAbility extends TriggeredAbilityImpl { public boolean checkTrigger(GameEvent event, Game game) { if (event.getSourceId().equals(this.getSourceId())) { Player opponent = game.getPlayer(event.getTargetId()); - if (opponent != null) { - return true; - } + return opponent != null; } return false; } @@ -123,7 +120,7 @@ class KaaliaOfTheVastEffect extends OneShotEffect { return false; } TargetCardInHand target = new TargetCardInHand(filter); - if (target.canChoose(controller.getId(), game) && target.choose(getOutcome(), controller.getId(), source.getSourceId(), game)) { + if (target.canChoose(controller.getId(), game) && target.choose(outcome, controller.getId(), source.getSourceId(), game)) { if (!target.getTargets().isEmpty()) { UUID cardId = target.getFirstTarget(); Card card = game.getCard(cardId); diff --git a/Mage.Sets/src/mage/cards/m/MiresGrasp.java b/Mage.Sets/src/mage/cards/m/MiresGrasp.java index 0205aea176..9c76c6658a 100644 --- a/Mage.Sets/src/mage/cards/m/MiresGrasp.java +++ b/Mage.Sets/src/mage/cards/m/MiresGrasp.java @@ -28,7 +28,7 @@ public final class MiresGrasp extends CardImpl { // Enchant creature TargetPermanent auraTarget = new TargetCreaturePermanent(); this.getSpellAbility().addTarget(auraTarget); - this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.UnboostCreature)); Ability ability = new EnchantAbility(auraTarget.getTargetName()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java b/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java index c17a7dc82a..421c4d5ca1 100644 --- a/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java +++ b/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java @@ -1,6 +1,5 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; @@ -19,8 +18,9 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInHand; +import java.util.UUID; + /** - * * @author Rafbill */ public final class PreeminentCaptain extends CardImpl { @@ -71,7 +71,7 @@ class PreeminentCaptainEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); TargetCardInHand target = new TargetCardInHand(filter); if (controller != null && target.canChoose(controller.getId(), game) - && target.choose(getOutcome(), controller.getId(), source.getSourceId(), game)) { + && target.choose(outcome, controller.getId(), source.getSourceId(), game)) { if (!target.getTargets().isEmpty()) { UUID cardId = target.getFirstTarget(); Card card = controller.getHand().get(cardId, game); diff --git a/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java b/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java index 1a8f126c8d..b5041281c0 100644 --- a/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java +++ b/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java @@ -1,7 +1,5 @@ - package mage.cards.r; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.Mode; @@ -29,34 +27,34 @@ import mage.target.common.TargetCardInGraveyard; import mage.target.common.TargetCardInYourGraveyard; import mage.target.common.TargetControlledCreaturePermanent; +import java.util.UUID; + /** - * * Once you announce you're casting Rescue from the Underworld, no player may * attempt to stop you from casting the spell by removing the creature you want * to sacrifice. - * + *

* If you sacrifice a creature token to cast Rescue from the Underworld, it * won't return to the battlefield, although the target creature card will. - * + *

* If either the sacrificed creature or the target creature card leaves the * graveyard before the delayed triggered ability resolves during your next * upkeep, it won't return. - * + *

* However, if the sacrificed creature is put into another public zone instead * of the graveyard, perhaps because it's your commander or because of another * replacement effect, it will return to the battlefield from the zone it went * to. - * + *

* Rescue from the Underworld is exiled as it resolves, not later as its delayed * trigger resolves. * - * * @author LevelX2 */ public final class RescueFromTheUnderworld extends CardImpl { public RescueFromTheUnderworld(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{4}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{B}"); // As an additional cost to cast Rescue from the Underworld, sacrifice a creature. this.getSpellAbility().addCost(new SacrificeTargetCost(new TargetControlledCreaturePermanent(1, 1, new FilterControlledCreaturePermanent("a creature"), false))); @@ -106,7 +104,7 @@ class RescueFromTheUnderworldCreateDelayedTriggeredAbilityEffect extends OneShot protected DelayedTriggeredAbility ability; public RescueFromTheUnderworldCreateDelayedTriggeredAbilityEffect(DelayedTriggeredAbility ability) { - super(ability.getEffects().get(0).getOutcome()); + super(ability.getEffects().getOutcome(ability)); this.ability = ability; } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TestFrameworkCanPlayAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TestFrameworkCanPlayAITest.java new file mode 100644 index 0000000000..64c863ba4a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TestFrameworkCanPlayAITest.java @@ -0,0 +1,77 @@ +package org.mage.test.AI.basic; + +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class TestFrameworkCanPlayAITest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_AI_PlayOnePriorityAction() { + addCard(Zone.HAND, playerA, "Lightning Bolt", 3); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 5); + + // AI must play one time + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertPermanentCount(playerB, "Balduvian Bears", 5 - 1); + } + + @Test + public void test_AI_PlayManyActionsInOneStep() { + addCard(Zone.HAND, playerA, "Lightning Bolt", 3); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 5); + + // AI must play all step actions + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Lightning Bolt", 3); + assertPermanentCount(playerB, "Balduvian Bears", 5 - 3); + } + + @Test + @Ignore // AI can't play blade cause score system give priority for boost instead restriction effects like goad + public void test_AI_GoadedByBloodthirstyBlade_Normal() { + // Equipped creature gets +2/+0 and is goaded + // {1}: Attach Bloodthirsty Blade to target creature an opponent controls. Activate this ability only any time you could cast a sorcery. + addCard(Zone.BATTLEFIELD, playerA, "Bloodthirsty Blade", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + + // AI must play + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + Assert.assertEquals(1, currentGame.getBattlefield().getAllActivePermanents(CardType.CREATURE).size()); + Permanent permanent = currentGame.getBattlefield().getAllActivePermanents(CardType.CREATURE).get(0); + Assert.assertEquals(1, permanent.getAttachments().size()); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/OutcomesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/OutcomesTest.java new file mode 100644 index 0000000000..1e142b2e5f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/OutcomesTest.java @@ -0,0 +1,99 @@ +package org.mage.test.cards.abilities; + +import mage.abilities.Ability; +import mage.abilities.common.LeavesBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.constants.Duration; +import mage.constants.Outcome; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class OutcomesTest extends CardTestPlayerBaseWithAIHelps { + + /** + * Normal outcome from effects + */ + + @Test + public void test_FromEffects_Single() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + Assert.assertEquals(1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + Assert.assertEquals(-1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + } + + @Test + public void test_FromEffects_Multi() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + abilityGood.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + Assert.assertEquals(1 + 1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + abilityBad.addEffect(new ExileTargetEffect()); + Assert.assertEquals(-1 + -1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + } + + @Test + public void test_FromEffects_MultiCombine() { + Ability ability = new SimpleStaticAbility(new GainLifeEffect(10)); + ability.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + ability.addEffect(new ExileTargetEffect()); + Assert.assertEquals(1 + 1 + -1, ability.getEffects().getOutcomeScore(ability)); + } + + @Test + public void test_FromEffects_Default() { + Ability ability = new LeavesBattlefieldTriggeredAbility(null, false); + Assert.assertEquals(0, ability.getEffects().getOutcomeScore(ability)); + Assert.assertEquals(Outcome.Detriment, ability.getEffects().getOutcome(ability)); + Assert.assertEquals(Outcome.BoostCreature, ability.getEffects().getOutcome(ability, Outcome.BoostCreature)); + } + + /** + * Special outcome from ability (AI activates only good abilities) + */ + + @Test + public void test_FromAbility_Single() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + abilityGood.addCustomOutcome(Outcome.Detriment); + Assert.assertEquals(-1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + Assert.assertEquals(Outcome.Detriment, abilityGood.getEffects().getOutcome(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + abilityBad.addCustomOutcome(Outcome.Neutral); + Assert.assertEquals(1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + Assert.assertEquals(Outcome.Neutral, abilityBad.getEffects().getOutcome(abilityBad)); + } + + @Test + public void test_FromAbility_Multi() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + abilityGood.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + abilityGood.addCustomOutcome(Outcome.Detriment); + Assert.assertEquals(-1 + -1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + abilityBad.addEffect(new ExileTargetEffect()); + abilityBad.addCustomOutcome(Outcome.Neutral); + Assert.assertEquals(1 + 1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + } + + @Test + public void test_FromAbility_MultiCombine() { + Ability ability = new SimpleStaticAbility(new GainLifeEffect(10)); + ability.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + ability.addEffect(new ExileTargetEffect()); + ability.addCustomOutcome(Outcome.Neutral); // must "convert" all effects to good + Assert.assertEquals(1 + 1 + 1, ability.getEffects().getOutcomeScore(ability)); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 97094b56a7..1c2a68c241 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -84,6 +84,7 @@ public class TestPlayer implements Player { private int foundNoAction = 0; private boolean AIPlayer; private final List actions = new ArrayList<>(); + private final Map actionsToRemovesLater = new HashMap<>(); // remove actions later, on next step (e.g. for AI commands) private final List choices = new ArrayList<>(); // choices stack for choice private final List targets = new ArrayList<>(); // targets stack for choose (it's uses on empty direct target by cast command) private final Map aliases = new HashMap<>(); // aliases for game objects/players (use it for cards with same name to save and use) @@ -499,6 +500,18 @@ public class TestPlayer implements Player { @Override public boolean priority(Game game) { + // later remove actions (ai commands related) + if (actionsToRemovesLater.size() > 0) { + List removed = new ArrayList<>(); + actionsToRemovesLater.forEach((action, step) -> { + if (game.getStep().getType() != step) { + actions.remove(action); + removed.add(action); + } + }); + removed.forEach(actionsToRemovesLater::remove); + } + int numberOfActions = actions.size(); List tempActions = new ArrayList<>(); tempActions.addAll(actions); @@ -622,6 +635,25 @@ public class TestPlayer implements Player { actions.remove(action); } } + } else if (action.getAction().startsWith(AI_PREFIX)) { + String command = action.getAction(); + command = command.substring(command.indexOf(AI_PREFIX) + AI_PREFIX.length()); + + // play priority + if (command.equals(AI_COMMAND_PLAY_PRIORITY)) { + computerPlayer.priority(game); + actions.remove(action); + return true; + } + + // play step + if (command.equals(AI_COMMAND_PLAY_STEP)) { + actionsToRemovesLater.put(action, game.getStep().getType()); + computerPlayer.priority(game); + return true; + } + + Assert.fail("Unknow ai command: " + command); } else if (action.getAction().startsWith(CHECK_PREFIX)) { String command = action.getAction(); command = command.substring(command.indexOf(CHECK_PREFIX) + CHECK_PREFIX.length()); @@ -752,9 +784,9 @@ public class TestPlayer implements Player { if (!wasProccessed) { Assert.fail("Unknow check command or params: " + command); } - } else if (action.getAction().startsWith("show:")) { + } else if (action.getAction().startsWith(SHOW_PREFIX)) { String command = action.getAction(); - command = command.substring(command.indexOf("show:") + "show:".length()); + command = command.substring(command.indexOf(SHOW_PREFIX) + SHOW_PREFIX.length()); String[] params = command.split(CHECK_PARAM_DELIMETER); boolean wasProccessed = false; @@ -3640,4 +3672,8 @@ public class TestPlayer implements Player { this.chooseStrictModeFailed("choice", game, getInfo(card) + " - can't select ability to cast.\n" + "Card's abilities:\n" + allInfo); return computerPlayer.chooseAbilityForCast(card, game, noMana); } + + public ComputerPlayer getComputerPlayer() { + return computerPlayer; + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseWithAIHelps.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseWithAIHelps.java new file mode 100644 index 0000000000..47091ce582 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseWithAIHelps.java @@ -0,0 +1,22 @@ +package org.mage.test.serverside.base; + +import mage.constants.RangeOfInfluence; +import org.mage.test.player.TestComputerPlayer7; +import org.mage.test.player.TestPlayer; + +/** + * Base class but with latest computer player to test single AI commands (it's different from full AI simulation from CardTestPlayerBaseAI): + * 1. AI don't play normal priorities (you must use ai*** commands to play it); + * 2. AI will choose in non strict mode (it's simulated ComputerPlayer7, not simple ComputerPlayer from basic tests) + * + * @author JayDi85 + */ +public abstract class CardTestPlayerBaseWithAIHelps extends CardTestPlayerBase { + + @Override + protected TestPlayer createPlayer(String name, RangeOfInfluence rangeOfInfluence) { + TestPlayer testPlayer = new TestPlayer(new TestComputerPlayer7(name, RangeOfInfluence.ONE, 6)); + testPlayer.setAIPlayer(false); // AI can't play it by itself, use AI commands + return testPlayer; + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index b9aad30df3..62f0f603df 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -22,6 +22,7 @@ import mage.game.GameOptions; import mage.game.command.CommandObject; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; +import mage.player.ai.ComputerPlayer7; import mage.players.ManaPool; import mage.players.Player; import mage.util.CardUtil; @@ -53,6 +54,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String ALIAS_PREFIX = "@"; // don't change -- it uses in user's tests public static final String CHECK_PARAM_DELIMETER = "#"; public static final String CHECK_PREFIX = "check:"; // prefix for all check commands + public static final String SHOW_PREFIX = "show:"; // prefix for all show commands + public static final String AI_PREFIX = "ai:"; // prefix for all ai commands static { // aliases can be used in check commands, so all prefixes and delimeters must be unique @@ -67,6 +70,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String ACTIVATE_PLAY = "activate:Play "; public static final String ACTIVATE_CAST = "activate:Cast "; + // commands for AI + public static final String AI_COMMAND_PLAY_PRIORITY = "play priority"; + public static final String AI_COMMAND_PLAY_STEP = "play step"; + static { // cards can be played/casted by activate ability command too Assert.assertTrue("musts contains activate ability part", ACTIVATE_PLAY.startsWith(ACTIVATE_ABILITY)); @@ -1391,6 +1398,28 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement player.addAction(turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName() + "$manaInPool=" + manaInPool); } + /** + * AI play one PRIORITY with multi game simulations (calcs and play ONE best action, can be called with stack) + */ + public void aiPlayPriority(int turnNum, PhaseStep step, TestPlayer player) { + assertAiPlayAndGameCompatible(player); + player.addAction(turnNum, step, AI_PREFIX + AI_COMMAND_PLAY_PRIORITY); + } + + /** + * AI play STEP to the end with multi game simulations (calcs and play best actions until step ends, can be called in the middle of the step) + */ + public void aiPlayStep(int turnNum, PhaseStep step, TestPlayer player) { + assertAiPlayAndGameCompatible(player); + player.addAction(turnNum, step, AI_PREFIX + AI_COMMAND_PLAY_STEP); + } + + private void assertAiPlayAndGameCompatible(TestPlayer player) { + if (player.isAIPlayer() || !(player.getComputerPlayer() instanceof ComputerPlayer7)) { + Assert.fail("AI commands supported by CardTestPlayerBaseWithAIHelps only"); + } + } + public void waitStackResolved(int turnNum, PhaseStep step, TestPlayer player) { player.addAction(turnNum, step, "waitStackResolved"); } diff --git a/Mage/src/main/java/mage/abilities/AbilitiesImpl.java b/Mage/src/main/java/mage/abilities/AbilitiesImpl.java index 16ddbfbeac..d389a5ac15 100644 --- a/Mage/src/main/java/mage/abilities/AbilitiesImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilitiesImpl.java @@ -1,8 +1,5 @@ - package mage.abilities; -import java.util.*; -import java.util.stream.Collectors; import mage.abilities.common.ZoneChangeTriggeredAbility; import mage.abilities.costs.Cost; import mage.abilities.keyword.ProtectionAbility; @@ -13,6 +10,9 @@ import mage.game.Game; import mage.util.ThreadLocalStringBuilder; import org.apache.log4j.Logger; +import java.util.*; +import java.util.stream.Collectors; + /** * @param * @author BetaSteward_at_googlemail.com @@ -220,7 +220,7 @@ public class AbilitiesImpl extends ArrayList implements Ab @Override public boolean contains(T ability) { - for (Iterator iterator = this.iterator(); iterator.hasNext();) { // simple loop can cause java.util.ConcurrentModificationException + for (Iterator iterator = this.iterator(); iterator.hasNext(); ) { // simple loop can cause java.util.ConcurrentModificationException T test = iterator.next(); // Checking also by getRule() without other restrictions is a problem when a triggered ability will be copied to a permanent that had the same ability // already before the copy. Because then it keeps the triggered ability twice and it triggers twice. @@ -273,7 +273,7 @@ public class AbilitiesImpl extends ArrayList implements Ab @Override public int getOutcomeTotal() { - return stream().mapToInt(ability -> ability.getEffects().getOutcomeTotal()).sum(); + return stream().mapToInt(ability -> ability.getEffects().getOutcomeScore(ability)).sum(); } @Override diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index f14c95b634..dd5b811f84 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -1,8 +1,5 @@ package mage.abilities; -import java.io.Serializable; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.abilities.costs.Cost; import mage.abilities.costs.CostAdjuster; @@ -12,10 +9,7 @@ import mage.abilities.costs.mana.ManaCosts; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; import mage.abilities.hint.Hint; -import mage.constants.AbilityType; -import mage.constants.AbilityWord; -import mage.constants.EffectType; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Controllable; import mage.game.Game; import mage.game.events.GameEvent; @@ -26,6 +20,10 @@ import mage.target.Targets; import mage.target.targetadjustment.TargetAdjuster; import mage.watchers.Watcher; +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + /** * Practically everything in the game is started from an Ability. This interface * describes what an Ability is composed of at the highest level. @@ -46,10 +44,8 @@ public interface Ability extends Controllable, Serializable { * * @see mage.players.PlayerImpl#playAbility(mage.abilities.ActivatedAbility, * mage.game.Game) - * @see - * mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) - * @see - * mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) + * @see mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) + * @see mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) */ void newId(); @@ -58,10 +54,8 @@ public interface Ability extends Controllable, Serializable { * * @see mage.players.PlayerImpl#playAbility(mage.abilities.ActivatedAbility, * mage.game.Game) - * @see - * mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) - * @see - * mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) + * @see mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) + * @see mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) */ void newOriginalId(); @@ -267,16 +261,15 @@ public interface Ability extends Controllable, Serializable { /** * Activates this ability prompting the controller to pay any mandatory * - * @param game A reference the {@link Game} for which this ability should be - * activated within. + * @param game A reference the {@link Game} for which this ability should be + * activated within. * @param noMana Whether or not {@link ManaCosts} have to be paid. * @return True if this ability was successfully activated. * @see mage.players.PlayerImpl#cast(mage.abilities.SpellAbility, * mage.game.Game, boolean) * @see mage.players.PlayerImpl#playAbility(mage.abilities.ActivatedAbility, * mage.game.Game) - * @see - * mage.players.PlayerImpl#triggerAbility(mage.abilities.TriggeredAbility, + * @see mage.players.PlayerImpl#triggerAbility(mage.abilities.TriggeredAbility, * mage.game.Game) */ boolean activate(Game game, boolean noMana); @@ -290,8 +283,7 @@ public interface Ability extends Controllable, Serializable { * * @param game The {@link Game} for which this ability resolves within. * @return Whether or not this ability successfully resolved. - * @see - * mage.players.PlayerImpl#playManaAbility(mage.abilities.mana.ManaAbility, + * @see mage.players.PlayerImpl#playManaAbility(mage.abilities.mana.ManaAbility, * mage.game.Game) * @see mage.players.PlayerImpl#specialAction(mage.abilities.SpecialAction, * mage.game.Game) @@ -526,4 +518,8 @@ public interface Ability extends Controllable, Serializable { List getHints(); Ability addHint(Hint hint); + + Ability addCustomOutcome(Outcome customOutcome); + + Outcome getCustomOutcome(); } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 43c53e8820..7fcb21e585 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1,9 +1,5 @@ package mage.abilities; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.abilities.costs.*; import mage.abilities.costs.common.PayLifeCost; @@ -33,6 +29,11 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import org.apache.log4j.Logger; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -68,6 +69,7 @@ public abstract class AbilityImpl implements Ability { protected TargetAdjuster targetAdjuster = null; protected CostAdjuster costAdjuster = null; protected List hints = new ArrayList<>(); + protected Outcome customOutcome = null; // uses for AI decisions instead effects public AbilityImpl(AbilityType abilityType, Zone zone) { this.id = UUID.randomUUID(); @@ -117,6 +119,7 @@ public abstract class AbilityImpl implements Ability { for (Hint hint : ability.getHints()) { this.hints.add(hint.copy()); } + this.customOutcome = ability.customOutcome; } @Override @@ -321,7 +324,7 @@ public abstract class AbilityImpl implements Ability { sourceObject.adjustTargets(this, game); } if (!getTargets().isEmpty()) { - Outcome outcome = getEffects().isEmpty() ? Outcome.Detriment : getEffects().get(0).getOutcome(); + Outcome outcome = getEffects().getOutcome(this); // only activated abilities can be canceled by user (not triggered) if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, this instanceof ActivatedAbility)) { // was canceled during targer selection @@ -948,10 +951,7 @@ public abstract class AbilityImpl implements Ability { } return ((Permanent) object).isPhasedIn(); } else if (object instanceof Card) { - if (!((Card) object).getAbilities(game).contains(this)) { - return false; - } - return true; + return ((Card) object).getAbilities(game).contains(this); } else if (!object.getAbilities().contains(this)) { // not sure which object it can still be // check if it's an ability that is temporary gained to a card Abilities otherAbilities = game.getState().getAllOtherAbilities(this.getSourceId()); @@ -1250,4 +1250,15 @@ public abstract class AbilityImpl implements Ability { this.hints.add(hint); return this; } + + @Override + public Ability addCustomOutcome(Outcome customOutcome) { + this.customOutcome = customOutcome; + return this; + } + + @Override + public Outcome getCustomOutcome() { + return this.customOutcome; + } } diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java index 62cf4f0ea3..e037461998 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java @@ -3,7 +3,6 @@ package mage.abilities; import mage.MageObject; import mage.abilities.effects.Effect; import mage.constants.AbilityType; -import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; @@ -61,7 +60,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge MageObject object = game.getObject(getSourceId()); Player player = game.getPlayer(this.getControllerId()); if (player != null && object != null) { - if (!player.chooseUse(getEffects().isEmpty() ? Outcome.Detriment : getEffects().get(0).getOutcome(), this.getRule(object.getLogName()), this, game)) { + if (!player.chooseUse(getEffects().getOutcome(this), this.getRule(object.getLogName()), this, game)) { return false; } } else { diff --git a/Mage/src/main/java/mage/abilities/effects/Effects.java b/Mage/src/main/java/mage/abilities/effects/Effects.java index 3eaf378be5..8d3d81db54 100644 --- a/Mage/src/main/java/mage/abilities/effects/Effects.java +++ b/Mage/src/main/java/mage/abilities/effects/Effects.java @@ -1,13 +1,11 @@ package mage.abilities.effects; +import mage.abilities.Ability; import mage.abilities.Mode; import mage.constants.Outcome; import mage.target.targetpointer.TargetPointer; import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; /** * @author BetaSteward_at_googlemail.com @@ -99,7 +97,11 @@ public class Effects extends ArrayList { return sbText.toString(); } - public boolean hasOutcome(Outcome outcome) { + public boolean hasOutcome(Ability source, Outcome outcome) { + Outcome realOutcome = (source == null ? null : source.getCustomOutcome()); + if (realOutcome != null) { + return realOutcome == outcome; + } for (Effect effect : this) { if (effect.getOutcome() == outcome) { return true; @@ -108,18 +110,40 @@ public class Effects extends ArrayList { return false; } - public List getOutcomes() { - Set outcomes = new HashSet<>(); - for (Effect effect : this) { - outcomes.add(effect.getOutcome()); - } - return new ArrayList<>(outcomes); + /** + * @param source source ability for effects + * @return real outcome of ability + */ + public Outcome getOutcome(Ability source) { + return getOutcome(source, Outcome.Detriment); } - public int getOutcomeTotal() { + public Outcome getOutcome(Ability source, Outcome defaultOutcome) { + Outcome realOutcome = (source == null ? null : source.getCustomOutcome()); + if (realOutcome != null) { + return realOutcome; + } + + if (!this.isEmpty()) { + return this.get(0).getOutcome(); + } + + return defaultOutcome; + } + + /** + * @param source source ability for effects + * @return total score of outcome effects (plus/minus) + */ + public int getOutcomeScore(Ability source) { int total = 0; for (Effect effect : this) { - if (effect.getOutcome().isGood()) { + // custom ability outcome must "rewrite" effect's outcome (it uses for AI desisions and card score... hmm, getOutcomeTotal used on 28.01.2020) + Outcome realOutcome = (source == null ? null : source.getCustomOutcome()); + if (realOutcome == null) { + realOutcome = effect.getOutcome(); + } + if (realOutcome.isGood()) { total++; } else { total--; diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java index c8aeb347db..3485c7c4f9 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java @@ -1,4 +1,3 @@ - package mage.abilities.effects.common; import mage.MageObject; @@ -103,11 +102,11 @@ public class CopyPermanentEffect extends OneShotEffect { // attach - search effect in spell ability (example: cast Utopia Sprawl, cast Estrid's Invocation on it) for (Ability ability : bluePrintPermanent.getAbilities()) { if (ability instanceof SpellAbility) { + auraOutcome = ability.getEffects().getOutcome(ability); for (Effect effect : ability.getEffects()) { if (effect instanceof AttachEffect) { if (bluePrintPermanent.getSpellAbility().getTargets().size() > 0) { auraTarget = bluePrintPermanent.getSpellAbility().getTargets().get(0); - auraOutcome = effect.getOutcome(); } } } @@ -118,12 +117,9 @@ public class CopyPermanentEffect extends OneShotEffect { if (auraTarget == null) { for (Ability ability : bluePrintPermanent.getAbilities()) { if (ability instanceof EnchantAbility) { + auraOutcome = ability.getEffects().getOutcome(ability); if (ability.getTargets().size() > 0) { // Animate Dead don't have targets auraTarget = ability.getTargets().get(0); - for (Effect effect : ability.getEffects()) { - // first outcome - auraOutcome = effect.getOutcome(); - } } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java index 646e3bebfa..179a59f736 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java @@ -1,4 +1,3 @@ - package mage.abilities.effects.common; import mage.abilities.Ability; @@ -6,11 +5,9 @@ import mage.abilities.DelayedTriggeredAbility; import mage.abilities.Mode; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; -import mage.constants.Outcome; import mage.game.Game; /** - * * @author BetaSteward_at_googlemail.com */ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect { @@ -28,7 +25,7 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect { } public CreateDelayedTriggeredAbilityEffect(DelayedTriggeredAbility ability, boolean copyTargets, boolean initAbility) { - super(ability.getEffects().isEmpty() ? Outcome.Detriment : ability.getEffects().get(0).getOutcome()); + super(ability.getEffects().getOutcome(ability)); this.ability = ability; this.copyTargets = copyTargets; this.initAbility = initAbility; diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java index 26266d0406..3d760dcee4 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java @@ -1,15 +1,12 @@ - package mage.abilities.effects.common; import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.SpecialAction; import mage.abilities.effects.OneShotEffect; -import mage.constants.Outcome; import mage.game.Game; /** - * * @author BetaSteward_at_googlemail.com */ public class CreateSpecialActionEffect extends OneShotEffect { @@ -17,7 +14,7 @@ public class CreateSpecialActionEffect extends OneShotEffect { protected SpecialAction action; public CreateSpecialActionEffect(SpecialAction action) { - super(action.getEffects().isEmpty() ? Outcome.Detriment : action.getEffects().get(0).getOutcome()); + super(action.getEffects().getOutcome(action)); this.action = action; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java index 7277831f43..d231598bf0 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java +++ b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java @@ -91,7 +91,7 @@ public class DoIfCostPaid extends OneShotEffect { } message = CardUtil.replaceSourceName(message, mageObject.getLogName()); boolean result = true; - Outcome payOutcome = executingEffects.size() > 0 ? executingEffects.get(0).getOutcome() : this.outcome; + Outcome payOutcome = executingEffects.getOutcome(source, this.outcome); if (cost.canPay(source, source.getSourceId(), player.getId(), game) && (!optional || player.chooseUse(payOutcome, message, source, game))) { cost.clearPaid(); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java index b98e86f8bb..5647423c5d 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java @@ -39,8 +39,7 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl { } public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, boolean onCard, Layer layer, SubLayer subLayer) { - super(duration, layer, subLayer, - !ability.getEffects().isEmpty() ? ability.getEffects().get(0).getOutcome() : Outcome.AddAbility); + super(duration, layer, subLayer, ability.getEffects().getOutcome(ability, Outcome.AddAbility)); this.ability = ability; staticText = rule; this.onCard = onCard; diff --git a/Mage/src/main/java/mage/game/draft/RateCard.java b/Mage/src/main/java/mage/game/draft/RateCard.java index 87676a9210..d2d3608a85 100644 --- a/Mage/src/main/java/mage/game/draft/RateCard.java +++ b/Mage/src/main/java/mage/game/draft/RateCard.java @@ -132,6 +132,7 @@ public final class RateCard { } private static int isEffectRemoval(Card card, Ability ability, Effect effect) { + // it's effect relates score, do not use custom outcome from ability if (effect.getOutcome() == Outcome.Removal) { // found removal return 1; diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index e6436d7eae..bd37c6436f 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -1,9 +1,5 @@ package mage.game.stack; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.ObjectColor; @@ -34,6 +30,11 @@ import mage.util.GameLog; import mage.util.SubTypeList; import mage.watchers.Watcher; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -578,7 +579,7 @@ public class StackAbility extends StackObjImpl implements Ability { game.getStack().push(newStackAbility); if (chooseNewTargets && !newAbility.getTargets().isEmpty()) { Player controller = game.getPlayer(newControllerId); - Outcome outcome = newAbility.getEffects().isEmpty() ? Outcome.Detriment : newAbility.getEffects().get(0).getOutcome(); + Outcome outcome = newAbility.getEffects().getOutcome(newAbility); if (controller.chooseUse(outcome, "Choose new targets?", source, game)) { newAbility.getTargets().clearChosen(); newAbility.getTargets().chooseTargets(outcome, newControllerId, newAbility, false, game, false); @@ -648,7 +649,16 @@ public class StackAbility extends StackObjImpl implements Ability { @Override public Ability addHint(Hint hint) { - // only abilities supports addhint - return null; + throw new IllegalArgumentException("Stack ability is not supports hint adding"); + } + + @Override + public Ability addCustomOutcome(Outcome customOutcome) { + throw new IllegalArgumentException("Stack ability is not supports custom outcome adding"); + } + + @Override + public Outcome getCustomOutcome() { + return this.ability.getCustomOutcome(); } } diff --git a/Mage/src/main/java/mage/game/stack/StackObjImpl.java b/Mage/src/main/java/mage/game/stack/StackObjImpl.java index 48c32191a9..6b38b76acb 100644 --- a/Mage/src/main/java/mage/game/stack/StackObjImpl.java +++ b/Mage/src/main/java/mage/game/stack/StackObjImpl.java @@ -163,7 +163,7 @@ public abstract class StackObjImpl implements StackObject { targetAmount = " (amount: " + target.getTargetAmount(targetId) + ")"; } // change the target? - Outcome outcome = mode.getEffects().isEmpty() ? Outcome.Detriment : mode.getEffects().get(0).getOutcome(); + Outcome outcome = mode.getEffects().getOutcome(ability); if (targetNames != null && (forceChange || targetController.chooseUse(outcome, "Change this target: " + targetNames + targetAmount + '?', ability, game))) {