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