diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java
index d99d2a9c526..017aa055b27 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java
@@ -103,11 +103,11 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
for (boolean isYou : isYous) {
for (boolean isTargetChoice : isTargetChoices) {
for (boolean isPlayerChoice : isPlayerChoices) {
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0 e.g. X=0", createAnyTarget(0, 0)).aiMustChoose(true, 0)); // simulate X=0
+ runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0 e.g. X=0", createAnyTarget(0, 0)).aiMustChoose(false, 0)); // simulate X=0
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1", createAnyTarget(1, 1)).aiMustChoose(true, 1));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3", createAnyTarget(3, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 5", createAnyTarget(5, 5)).aiMustChoose(true, 5));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any max", createAnyTarget(0, Integer.MAX_VALUE)).aiMustChoose(true, 0));
+ runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any max", createAnyTarget(0, Integer.MAX_VALUE)).aiMustChoose(true, 6 + 1)); // 6 own cards + 1 own player
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-1", createAnyTarget(0, 1)).aiMustChoose(true, 1));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-3", createAnyTarget(0, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-5", createAnyTarget(0, 5)).aiMustChoose(true, 5));
@@ -116,7 +116,7 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-5", createAnyTarget(1, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-5", createAnyTarget(2, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3-5", createAnyTarget(3, 5)).aiMustChoose(true, 5));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 4-5", createAnyTarget(4, 5)).aiMustChoose(true, 5)); // impossible on 3 targets
+ runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 4-5", createAnyTarget(4, 5)).aiMustChoose(true, 5));
//
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0, e.g. X=0", createImpossibleTarget(0, 0)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1", createImpossibleTarget(1, 1)).aiMustChoose(false, 0));
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 14ba9d0ea2b..4f189da96b7 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
@@ -23,6 +23,7 @@ import mage.game.stack.StackAbility;
import mage.game.stack.StackObject;
import mage.player.ai.ma.optimizers.TreeOptimizer;
import mage.player.ai.ma.optimizers.impl.*;
+import mage.player.ai.score.GameStateEvaluator2;
import mage.player.ai.util.CombatInfo;
import mage.player.ai.util.CombatUtil;
import mage.players.Player;
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 f78cceee8f1..1e30e995570 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
@@ -3,6 +3,7 @@ package mage.player.ai;
import mage.abilities.Ability;
import mage.constants.RangeOfInfluence;
import mage.game.Game;
+import mage.player.ai.score.GameStateEvaluator2;
import org.apache.log4j.Logger;
import java.util.Date;
diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java
index 873b4c5bbcd..32bc78c75b7 100644
--- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java
+++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java
@@ -13,7 +13,7 @@ import mage.game.permanent.Permanent;
import mage.game.turn.CombatDamageStep;
import mage.game.turn.EndOfCombatStep;
import mage.game.turn.Step;
-import mage.player.ai.GameStateEvaluator2;
+import mage.player.ai.score.GameStateEvaluator2;
import mage.players.Player;
import org.apache.log4j.Logger;
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 22e188722dc..96d4202f9cf 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
@@ -1,9 +1,6 @@
package mage.player.ai;
-import mage.ApprovingObject;
-import mage.ConditionalMana;
-import mage.MageObject;
-import mage.Mana;
+import mage.*;
import mage.abilities.*;
import mage.abilities.costs.mana.*;
import mage.abilities.effects.Effect;
@@ -14,7 +11,6 @@ import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.abilities.mana.ManaOptions;
import mage.cards.Card;
import mage.cards.Cards;
-import mage.cards.CardsImpl;
import mage.cards.RateCard;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidator;
@@ -25,10 +21,11 @@ import mage.cards.repository.CardRepository;
import mage.choices.Choice;
import mage.constants.*;
import mage.counters.CounterType;
-import mage.filter.FilterCard;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
-import mage.filter.common.*;
+import mage.filter.common.FilterCreatureForCombatBlock;
+import mage.filter.common.FilterLandCard;
+import mage.filter.common.FilterNonlandCard;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.Game;
import mage.game.combat.CombatGroup;
@@ -36,8 +33,6 @@ import mage.game.draft.Draft;
import mage.game.events.GameEvent;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
-import mage.game.stack.Spell;
-import mage.game.stack.StackObject;
import mage.game.tournament.Tournament;
import mage.player.ai.simulators.CombatGroupSimulator;
import mage.player.ai.simulators.CombatSimulator;
@@ -47,8 +42,9 @@ import mage.players.Player;
import mage.players.PlayerImpl;
import mage.players.net.UserData;
import mage.players.net.UserGroup;
-import mage.target.*;
-import mage.target.common.*;
+import mage.target.Target;
+import mage.target.TargetAmount;
+import mage.target.TargetCard;
import mage.util.*;
import org.apache.log4j.Logger;
@@ -59,19 +55,17 @@ import java.util.Map.Entry;
/**
* AI: basic server side bot with simple actions support (game, draft, construction/sideboarding)
*
- * TODO: combine choose and chooseTarget to single logic to use shared code
*
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public class ComputerPlayer extends PlayerImpl {
private static final Logger log = Logger.getLogger(ComputerPlayer.class);
- private long lastThinkTime = 0; // msecs for last AI actions calc
- protected int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
+ protected static final int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
// debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout)
- protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = true; // DebugUtil.AI_ENABLE_DEBUG_MODE;
+ protected static final boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = true; // DebugUtil.AI_ENABLE_DEBUG_MODE;
// AI agents uses game simulation thread for all calcs and it's high CPU consumption
// More AI threads - more parallel AI games can be calculate
@@ -80,12 +74,13 @@ public class ComputerPlayer extends PlayerImpl {
// How-to use:
// * 1 for debug or stable
// * 5 for good performance on average computer
- // * use your's CPU cores for best performance
+ // * use yours CPU cores for best performance
// TODO: add server config to control max AI threads (with CPU cores by default)
// TODO: rework AI implementation to use multiple sims calculation instead one by one
final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 1;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5;
+ // TODO: delete after target rework
private final transient Map unplayable = new TreeMap<>();
private final transient List playableNonInstant = new ArrayList<>();
private final transient List playableInstant = new ArrayList<>();
@@ -101,6 +96,8 @@ public class ComputerPlayer extends PlayerImpl {
// For stopping infinite loops when trying to pay Phyrexian mana when the player can't spend life and no other sources are available
private transient boolean alreadyTryingToPayPhyrexian;
+ private transient long lastThinkTime = 0; // time in ms for last AI actions calc
+
public ComputerPlayer(String name, RangeOfInfluence range) {
super(name, range);
human = false;
@@ -125,7 +122,6 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseMulligan(Game game) {
- log.debug("chooseMulligan");
if (hand.size() < 6
|| isTestsMode() // ignore mulligan in tests
|| game.getClass().getName().contains("Momir") // ignore mulligan in Momir games
@@ -144,17 +140,23 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) {
- if (log.isDebugEnabled()) {
- log.debug("choose: " + outcome.toString() + ':' + target.toString());
- }
+ return makeChoice(outcome, target, source, game, null);
+ }
+ /**
+ * Default choice logic for any choose dialogs due effect's outcome and possible target priority
+ */
+ private boolean makeChoice(Outcome outcome, Target target, Ability source, Game game, Cards fromCards) {
// choose itself for starting player all the time
if (target.getMessage(game).equals("Select a starting player")) {
target.add(this.getId(), game);
return true;
}
- boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
+ // nothing to choose
+ if (fromCards != null && fromCards.isEmpty()) {
+ return false;
+ }
// controller hints:
// - target.getTargetController(), this.getId() -- player that must makes choices (must be same with this.getId)
@@ -166,1067 +168,84 @@ public class ComputerPlayer extends PlayerImpl {
&& target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
}
- UUID sourceId = source != null ? source.getSourceId() : null;
- boolean required = target.isRequired(sourceId, game);
- Set possibleTargets = target.possibleTargets(abilityControllerId, source, game);
- if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getMinNumberOfTargets()) {
- required = false;
+ // nothing to choose, e.g. X=0
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return false;
}
- UUID randomOpponentId = getRandomOpponent(game);
-
- if (target.getOriginalTarget() instanceof TargetPlayer) {
- return selectPlayer(outcome, target, abilityControllerId, randomOpponentId, game, required);
- }
-
- if (target.getOriginalTarget() instanceof TargetDiscard) {
- findPlayables(game);
- // discard not playable first
- if (!unplayable.isEmpty()) {
- for (int i = unplayable.size() - 1; i >= 0; i--) {
- UUID targetId = unplayable.values().toArray(new Card[0])[i].getId();
- if (target.canTarget(abilityControllerId, targetId, source, game) && !target.contains(targetId)) {
- target.add(targetId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- }
- if (!hand.isEmpty()) {
- for (int i = 0; i < hand.size(); i++) {
- UUID targetId = hand.toArray(new UUID[0])[i];
- if (target.canTarget(abilityControllerId, targetId, source, game) && !target.contains(targetId)) {
- target.add(targetId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetControlledPermanent
- || target.getOriginalTarget() instanceof TargetSacrifice) {
- List targets;
- TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget();
- targets = threats(abilityControllerId, source, origTarget.getFilter(), game, target.getTargets());
- if (!outcome.isGood()) {
- Collections.reverse(targets);
- }
- for (Permanent permanent : targets) {
- if (origTarget.canTarget(abilityControllerId, permanent.getId(), source, game, false) && !target.contains(permanent.getId())) {
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanent) {
- FilterPermanent filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) {
- filter = (FilterPermanent) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported permanent filter in computer's choose method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- List targets;
- if (outcome.isCanTargetAll()) {
- targets = threats(null, source, filter, game, target.getTargets());
- } else {
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, filter, game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, filter, game, target.getTargets());
- }
- if (targets.isEmpty() && target.isRequired()) {
- if (!outcome.isGood()) {
- targets = threats(abilityControllerId, source, filter, game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, filter, game, target.getTargets());
- }
- }
- }
-
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- // AI workaround to stop adding more targets in "up to" on bad outcome for itself
- if (target.isChosen(game) && target.getMinNumberOfTargets() == target.getTargets().size()) {
- if (outcome.isGood() && hasOpponent(permanent.getControllerId(), game)) {
- return isAddedSomething;
- }
- if (!outcome.isGood() && !hasOpponent(permanent.getControllerId(), game)) {
- return isAddedSomething;
- }
- }
- // add the target
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInHand
- || (target.getZone() == Zone.HAND && (target.getOriginalTarget() instanceof TargetCard))) {
- List cards = new ArrayList<>();
- for (UUID cardId : target.possibleTargets(this.getId(), source, game)) {
- Card card = game.getCard(cardId);
- if (card != null) {
- cards.add(card);
- }
- }
- while ((outcome.isGood() ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game))
- && !cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game); // TODO: why it add as much as possible instead go to isChosen check like above?
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetAnyTarget) {
- List targets;
- TargetAnyTarget origTarget = (TargetAnyTarget) target.getOriginalTarget();
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- target.add(getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (!required) {
- return isAddedSomething;
+ // default logic for any targets
+ boolean isAddedSomething = false;
+ PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game);
+ possibleTargetsSelector.findNewTargets(fromCards);
+ // good targets -- choose as much as possible
+ for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
+ target.add(item.getId(), game);
+ isAddedSomething = true;
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
}
}
+ // bad targets -- choose as low as possible
+ for (MageItem item : possibleTargetsSelector.getBadTargets()) {
+ if (target.isChosen(game)) {
+ break;
+ }
+ target.add(item.getId(), game);
+ isAddedSomething = true;
+ }
+ return isAddedSomething;
+ }
- if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
- List targets;
- TargetPermanentOrPlayer origTarget = (TargetPermanentOrPlayer) target.getOriginalTarget();
- List ownedTargets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- List opponentTargets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- if (outcome.isGood()) {
- targets = ownedTargets;
- } else {
- targets = opponentTargets;
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- isAddedSomething = true;
- target.add(permanent.getId(), game);
- return true;
- }
- }
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- target.add(getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (!target.isRequired(sourceId, game) || target.getMinNumberOfTargets() == 0) {
- return isAddedSomething; // TODO: need research why it here (between diff type of targets)
- }
- if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- target.add(getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (outcome.isGood()) { // no other valid targets so use a permanent
- targets = opponentTargets;
- } else {
- targets = ownedTargets;
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
+ /**
+ * Default choice logic for X or amount values
+ */
+ private int makeChoiceAmount(int min, int max, Game game, Ability source, boolean isManaPay) {
+ // fast calc on nothing to choose
+ if (min >= max) {
+ return min;
}
- if (target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- for (Card card : player.getGraveyard().getCards(game)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game)) {
- cards.add(card);
- }
- }
- }
+ // TODO: add good/bad effects support
+ // TODO: add simple game simulations like declare blocker (need to find only workable payment)?
+ // TODO: remove random logic or make it more stable (e.g. use same value in same game cycle)
- // exile cost workaround: exile is bad, but exile from graveyard in most cases is good (more exiled -- more good things you get, e.g. delve's pay)
- boolean isRealGood = outcome.isGood() || outcome == Outcome.Exile;
- while ((isRealGood ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game))
- && !cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
+ // protection from too big values
+ int realMin = min;
+ int realMax = max;
+ if (max == Integer.MAX_VALUE) {
+ realMax = Math.max(realMin, 10); // AI don't need huge values for X, cause can't use infinite combos
}
- if (target.getOriginalTarget() instanceof TargetCardInGraveyard
- || (target.getZone() == Zone.GRAVEYARD && (target.getOriginalTarget() instanceof TargetCard))) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- for (Card card : player.getGraveyard().getCards(game)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game)) {
- cards.add(card);
- }
- }
- }
-
- // exile cost workaround: exile is bad, but exile from graveyard in most cases is good (more exiled -- more good things you get, e.g. delve's pay)
- boolean isRealGood = outcome.isGood() || outcome == Outcome.Exile;
- while ((isRealGood ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game))
- && !cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
-
- return isAddedSomething;
+ int xValue;
+ if (isManaPay) {
+ // as X mana payment - due available mana
+ xValue = Math.max(0, getAvailableManaProducers(game).size() - source.getManaCostsToPay().getUnpaid().manaValue());
+ } else {
+ // as X actions
+ xValue = RandomUtil.nextInt(realMax + 1);
}
- if (target.getOriginalTarget() instanceof TargetCardInYourGraveyard
- || target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) {
- TargetCard originalTarget = (TargetCard) target.getOriginalTarget();
- List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards(originalTarget.getFilter(), game));
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null && !target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
+ if (xValue > realMax) {
+ xValue = realMax;
+ }
+ if (xValue < realMin) {
+ xValue = realMin;
}
- if (target.getOriginalTarget() instanceof TargetCardInExile) {
- FilterCard filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterCard) {
- filter = (FilterCard) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported exile target filter in computer's choose method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- List cards = game.getExile().getCards(filter, game);
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null && !target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetSource) {
- Set targets;
- targets = target.possibleTargets(abilityControllerId, source, game);
- for (UUID targetId : targets) {
- MageObject targetObject = game.getObject(targetId);
- if (targetObject != null) {
- if (target.canTarget(abilityControllerId, targetObject.getId(), source, game)) {
- if (!target.contains(targetObject.getId())) {
- target.add(targetObject.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- }
- }
- if (!required) {
- return isAddedSomething;
- }
- throw new IllegalStateException("TargetSource wasn't handled in computer's choose method: " + target.getClass().getCanonicalName());
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) {
- List cards = new ArrayList<>(new CardsImpl(possibleTargets).getCards(game));
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCard
- && (target.getZone() == Zone.COMMAND)) { // Hellkite Courser
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- for (Card card : game.getCommanderCardsFromCommandZone(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game)) {
- cards.add(card);
- }
- }
- }
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card);
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- throw new IllegalStateException("Target wasn't handled in computer's choose method: " + target.getClass().getCanonicalName());
- } //end of choose method
+ return xValue;
+ }
@Override
public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) {
- if (log.isDebugEnabled()) {
- log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString());
- }
-
- boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
-
- // target - real target, make all changes and add targets to it
- // target.getOriginalTarget() - copy spell effect replaces original target with TargetWithAdditionalFilter
- // use originalTarget to get filters and target class info
- // source can be null (as example: legendary rule permanent selection)
- UUID sourceId = source != null ? source.getSourceId() : null;
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- boolean required = target.isRequired(sourceId, game);
- Set possibleTargets = target.possibleTargets(abilityControllerId, source, game);
- if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getMinNumberOfTargets()) {
- required = false;
- }
-
- List goodList = new ArrayList<>();
- List badList = new ArrayList<>();
- List allList = new ArrayList<>();
-
- UUID randomOpponentId = getRandomOpponent(game);
-
- if (target.getOriginalTarget() instanceof TargetPlayer) {
- return selectPlayerTarget(outcome, target, source, abilityControllerId, randomOpponentId, game, required);
- }
-
- // Angel of Serenity trigger
- if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) {
- List cards = new ArrayList<>(new CardsImpl(possibleTargets).getCards(game));
- isAddedSomething = false;
- for (Card card : cards) {
- // check permanents first; they have more intrinsic worth
- if (card instanceof Permanent) {
- Permanent p = ((Permanent) card);
- if (outcome.isGood()
- && p.isControlledBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, p.getId(), source, game) && !target.contains(p.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(p.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- if (!outcome.isGood()
- && !p.isControlledBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, p.getId(), source, game) && !target.contains(p.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(p.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- }
- // check the graveyards last
- if (game.getState().getZone(card.getId()) == Zone.GRAVEYARD) {
- if (outcome.isGood()
- && card.isOwnedBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- if (!outcome.isGood()
- && !card.isOwnedBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetDiscard
- || target.getOriginalTarget() instanceof TargetCardInHand) {
- isAddedSomething = false;
- if (outcome.isGood()) {
- // good - choose max possible
- Cards cards = new CardsImpl(possibleTargets);
- List cardsInHand = new ArrayList<>(cards.getCards(game));
- while (!target.isChosen(game)
- && !cardsInHand.isEmpty()
- && target.getMaxNumberOfTargets() > target.getTargets().size()) {
- Card card = selectBestCardTarget(cardsInHand, Collections.emptyList(), target, source, game);
- if (card != null) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- cardsInHand.remove(card);
- }
- }
- } else {
- // bad - choose the lowest possible
- findPlayables(game);
- for (Card card : unplayable.values()) {
- if (target.isChosen(game)) {
- return isAddedSomething;
- }
- if (possibleTargets.contains(card.getId())
- && target.canTarget(abilityControllerId, card.getId(), source, game)
- && !target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return isAddedSomething;
- }
- }
- }
- if (!hand.isEmpty()) {
- for (Card card : hand.getCards(game)) {
- if (target.isChosen(game)) {
- return isAddedSomething;
- }
- if (possibleTargets.contains(card.getId())
- && target.canTarget(abilityControllerId, card.getId(), source, game)
- && !target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return isAddedSomething;
- }
- }
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetControlledPermanent
- || target.getOriginalTarget() instanceof TargetSacrifice) {
- TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget();
- List targets;
- targets = threats(abilityControllerId, source, origTarget.getFilter(), game, target.getTargets());
- if (!outcome.isGood()) {
- Collections.reverse(targets);
- }
- isAddedSomething = false;
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.addTarget(permanent.getId(), source, game);
- isAddedSomething = true;
- if (target.getMinNumberOfTargets() <= target.getTargets().size() && (!outcome.isGood() || target.getMaxNumberOfTargets() <= target.getTargets().size())) {
- return true; // TODO: need research - is it good optimization for good/bad effects?
- }
- }
- }
- return isAddedSomething;
-
- }
-
- // TODO: implemented findBestPlayerTargets
- // TODO: add findBest*Targets for all target types
- // TODO: Much of this code needs to be re-written to move code into Target.possibleTargets
- // A) Having it here makes this function ridiculously long
- // B) Each time a new target type is added, people must remember to add it here
- if (target.getOriginalTarget() instanceof TargetPermanent) {
- FilterPermanent filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) {
- filter = (FilterPermanent) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported permanent filter in computer's chooseTarget method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- findBestPermanentTargets(outcome, abilityControllerId, sourceId, source, filter,
- game, target, goodList, badList, allList);
-
- // use good list all the time and add maximum targets
- isAddedSomething = false;
- for (Permanent permanent : goodList) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(permanent.getId(), source, game);
- isAddedSomething = true;
- }
- }
-
- // use bad list only on required target and add minimum targets
- if (required) {
- for (Permanent permanent : badList) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- if (target.getTargets().size() >= target.getMinNumberOfTargets()) {
- break;
- }
- target.addTarget(permanent.getId(), source, game);
- isAddedSomething = true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetAnyTarget) {
- List targets;
- TargetAnyTarget origTarget = ((TargetAnyTarget) target.getOriginalTarget());
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- }
-
- if (targets.isEmpty()) {
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
- }
-
- if (targets.isEmpty() && required) {
- targets = game.getBattlefield().getActivePermanents(((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), playerId, game);
- }
- for (Permanent permanent : targets) {
- List alreadyTargeted = target.getTargets();
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
- tryAddTarget(target, permanent.getId(), source, game);
- }
- }
- }
-
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
-
- //if (!target.isRequired())
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
- List targets;
- TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget());
-
- // TODO: in multiplayer game there many opponents - if random opponents don't have targets then AI must use next opponent, but it skips
- // (e.g. you randomOpponentId must be replaced by List randomOpponents)
- // normal cycle (good for you, bad for opponents)
- // possible good/bad permanents
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) target.getFilter()).getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) target.getFilter()).getPermanentFilter(), game, target.getTargets());
- }
-
- // possible good/bad players
- if (targets.isEmpty()) {
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
- }
-
- // can't find targets (e.g. effect is bad, but you need take targets from yourself)
- if (targets.isEmpty() && required) {
- targets = game.getBattlefield().getActivePermanents(origTarget.getFilterPermanent(), playerId, game);
- }
-
- // try target permanent
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- return tryAddTarget(target, permanent.getId(), source, game);
- }
- }
-
- // try target player as normal
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
-
- // try target player as bad (bad on itself, good on opponent)
- for (UUID opponentId : game.getOpponents(getId(), true)) {
- if (target.canTarget(abilityControllerId, opponentId, source, game) && !target.contains(opponentId)) {
- return tryAddTarget(target, opponentId, source, game);
- }
- }
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
-
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInGraveyard) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- cards.addAll(player.getGraveyard().getCards(game));
- }
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null && !target.contains(card.getId())) {
- return tryAddTarget(target, card.getId(), source, game);
- }
- //if (!target.isRequired())
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInLibrary) {
- List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getLibrary().getCards(game));
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null && !target.contains(card.getId())) {
- return tryAddTarget(target, card.getId(), source, game);
- }
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInYourGraveyard) {
- List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards((FilterCard) target.getFilter(), game));
- isAddedSomething = false;
- while (!target.isChosen(game) && !cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game); // TODO: why it add as much as possible instead go to isChosen check like above in choose?
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetSpell
- || target.getOriginalTarget() instanceof TargetStackObject) {
- if (!game.getStack().isEmpty()) {
- for (StackObject o : game.getStack()) {
- if (o instanceof Spell
- && !source.getId().equals(o.getStackAbility().getId())
- && target.canTarget(abilityControllerId, o.getStackAbility().getId(), source, game)
- && !target.contains(o.getId())) {
- return tryAddTarget(target, o.getId(), source, game);
- }
- }
- }
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetSpellOrPermanent) {
- // TODO: Also check if a spell should be selected
- TargetSpellOrPermanent origTarget = (TargetSpellOrPermanent) target.getOriginalTarget();
- List targets;
- boolean outcomeTargets = true;
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, origTarget.getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, origTarget.getPermanentFilter(), game, target.getTargets());
- }
- if (targets.isEmpty() && required) {
- targets = threats(null, source, origTarget.getPermanentFilter(), game, target.getTargets());
- Collections.reverse(targets);
- outcomeTargets = false;
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.addTarget(permanent.getId(), source, game);
- if (!outcomeTargets || target.getMaxNumberOfTargets() <= target.getTargets().size()) {
- return true; // TODO: need logic research (e.g. select as much as possible on good outcome?)
- }
- }
- }
- if (!game.getStack().isEmpty()) {
- for (StackObject stackObject : game.getStack()) {
- if (stackObject instanceof Spell && source != null && !source.getId().equals(stackObject.getStackAbility().getId())) {
- if (target.getFilter().match(stackObject, game) && !target.contains(stackObject.getId())) {
- return tryAddTarget(target, stackObject.getId(), source, game);
- }
- }
- }
- }
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInOpponentsGraveyard) {
- List cards = new ArrayList<>();
- for (UUID uuid : game.getOpponents(getId(), true)) {
- Player player = game.getPlayer(uuid);
- if (player != null) {
- cards.addAll(player.getGraveyard().getCards(game));
- }
- }
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- //if (!target.isRequired())
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetDefender) {
- List targets = new ArrayList<>(possibleTargets);
- isAddedSomething = false;
- while (!targets.isEmpty()) {
- UUID randomDefender = RandomUtil.randomFromCollection(possibleTargets);
- if (randomDefender != null) {
- targets.remove(randomDefender);
- if (!target.contains(randomDefender)) {
- isAddedSomething = true;
- target.addTarget(randomDefender, source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- cards.addAll(player.getGraveyard().getCards(game));
- }
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInExile) {
- FilterCard filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterCard) {
- filter = (FilterCard) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported exile target filter in computer's chooseTarget method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- List cards = new ArrayList<>();
- for (UUID uuid : target.possibleTargets(source.getControllerId(), source, game)) {
- Card card = game.getCard(uuid);
- if (card != null && game.getState().getZone(card.getId()) == Zone.EXILED) {
- cards.add(card);
- }
- }
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetActivatedAbility) {
- List stackObjects = new ArrayList<>();
- for (UUID uuid : target.possibleTargets(source.getControllerId(), source, game)) {
- StackObject stackObject = game.getStack().getStackObject(uuid);
- if (stackObject != null) {
- stackObjects.add(stackObject);
- }
- }
- while (!stackObjects.isEmpty()) {
- StackObject pick = stackObjects.get(0);
- if (pick != null) {
- stackObjects.remove(0);
- if (!target.contains(pick.getId())) {
- isAddedSomething = true;
- target.addTarget(pick.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetActivatedOrTriggeredAbility) {
- List targets = new ArrayList<>(target.possibleTargets(source.getControllerId(), source, game));
- isAddedSomething = false;
- while (!targets.isEmpty()) {
- UUID id = targets.get(0);
- if (id != null) {
- targets.remove(0);
- if (!target.contains(id)) {
- isAddedSomething = true;
- target.addTarget(id, source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- cards.addAll(player.getGraveyard().getCards(game));
- cards.addAll(game.getBattlefield().getAllActivePermanents(new FilterPermanent(), player.getId(), game));
- }
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) {
- List cards = new ArrayList<>(new CardsImpl(possibleTargets).getCards(game));
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName());
- } //end of chooseTarget method
+ return makeChoice(outcome, target, source, game, null);
+ }
@Deprecated // TODO: replace by source only version
protected Card selectCard(UUID abilityControllerId, List cards, Outcome outcome, Target target, Game game) {
return selectCardInner(abilityControllerId, cards, outcome, target, null, game);
}
- protected Card selectCardTarget(UUID abilityControllerId, List cards, Outcome outcome, Target target, Ability targetingSource, Game game) {
- return selectCardInner(abilityControllerId, cards, outcome, target, targetingSource, game);
- }
-
/**
* @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
*/
@@ -1255,9 +274,6 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
// TODO: make same code for chooseTarget (without filter and target type dependence)
- if (log.isDebugEnabled()) {
- log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString());
- }
if (target.getAmountRemaining() <= 0) {
return false;
@@ -1360,7 +376,7 @@ public class ComputerPlayer extends PlayerImpl {
targets = threats(opponentId, source, StaticFilters.FILTER_PERMANENT, game, target.getTargets(), false);
}
- // creatures - non killable (TODO: add extra skill checks like undestructeable)
+ // creatures - non killable (TODO: add extra skill checks like indestructible)
for (Permanent permanent : targets) {
if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
int safeDamage = Math.min(permanent.getToughness().getValue() - 1, target.getAmountRemaining());
@@ -1409,6 +425,7 @@ public class ComputerPlayer extends PlayerImpl {
}
private boolean priorityPlay(Game game) {
+ // TODO: simplify and delete, never called in real game due ComputerPlayer7's simulations usage
UUID opponentId = getRandomOpponent(game);
if (game.isActivePlayer(playerId)) {
if (game.isMainPhase() && game.getStack().isEmpty()) {
@@ -1499,7 +516,6 @@ public class ComputerPlayer extends PlayerImpl {
} // end priorityPlay method
protected void playLand(Game game) {
- log.debug("playLand");
Set lands = new LinkedHashSet<>();
for (Card landCard : hand.getCards(new FilterLandCard(), game)) {
// remove lands that can not be played
@@ -1525,7 +541,6 @@ public class ComputerPlayer extends PlayerImpl {
}
protected void playALand(Set lands, Game game) {
- log.debug("playALand");
//play a land that will allow us to play an unplayable
for (Mana mana : unplayable.keySet()) {
for (Card card : lands) {
@@ -1559,6 +574,7 @@ public class ComputerPlayer extends PlayerImpl {
lands.remove(lands.iterator().next());
}
+ @Deprecated // TODO: delete after target rework
protected void findPlayables(Game game) {
playableInstant.clear();
playableNonInstant.clear();
@@ -1643,9 +659,6 @@ public class ComputerPlayer extends PlayerImpl {
}
}
}
- if (log.isDebugEnabled()) {
- log.debug("findPlayables: " + playableInstant.toString() + "---" + playableNonInstant.toString() + "---" + playableAbilities.toString());
- }
}
@Override
@@ -1686,7 +699,7 @@ public class ComputerPlayer extends PlayerImpl {
for (Mana mana : manaAbility.getNetMana(game)) {
// if mana ability can produce non-useful mana then ignore whole ability here (example: {R} or {G})
// (AI can't choose a good mana option, so make sure any selection option will be compatible with cost)
- // AI support {Any} choice by lastUnpaidMana, so it can safly used in includesMana
+ // AI support {Any} choice by lastUnpaidMana, so it can safety used in includesMana
if (!unpaid.getMana().includesMana(mana)) {
continue ManaAbility;
} else if (mana.getAny() > 0) {
@@ -1791,7 +804,7 @@ public class ComputerPlayer extends PlayerImpl {
}
}
}
- // then pay colorless hybrid - more restrictive than mono hybrid
+ // then pay colorless hybrid - more restrictive than monohybrid
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof ColorlessHybridManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) {
@@ -1809,7 +822,7 @@ public class ComputerPlayer extends PlayerImpl {
}
}
}
- // then pay mono hybrid
+ // then pay monohybrid
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof MonoHybridManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) {
@@ -1913,7 +926,7 @@ public class ComputerPlayer extends PlayerImpl {
);
}
- // cost can contains multiple mana types, must check each type (is it possible to pay a cost)
+ // cost can contain multiple mana types, must check each type (is it possible to pay a cost)
for (ManaType checkType : ManaUtil.getManaTypesInCost(checkCost)) {
// affected asThoughMana effect must fit a checkType with pool mana
ManaType possibleAsThoughPoolManaType = game.getContinuousEffects().asThoughMana(checkType, possiblePoolItem, abilityToPay.getSourceId(), abilityToPay, abilityToPay.getControllerId(), game);
@@ -1922,10 +935,10 @@ public class ComputerPlayer extends PlayerImpl {
}
boolean canPay;
if (possibleAsThoughPoolManaType == ManaType.COLORLESS) {
- // colorless can be payed by any color from the pool
+ // colorless can be paid by any color from the pool
canPay = possiblePoolItem.count() > 0;
} else {
- // colored must be payed by specific color from the pool (AsThough already changed it to fit with mana pool)
+ // colored must be paid by specific color from the pool (AsThough already changed it to fit with mana pool)
canPay = possiblePoolItem.get(possibleAsThoughPoolManaType) > 0;
}
if (canPay) {
@@ -1940,23 +953,20 @@ public class ComputerPlayer extends PlayerImpl {
Abilities manaAbilities = mageObject.getAbilities().getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game);
if (manaAbilities.size() > 1) {
// Sort mana abilities by number of produced manas, to use ability first that produces most mana (maybe also conditional if possible)
- Collections.sort(manaAbilities, new Comparator() {
- @Override
- public int compare(ActivatedManaAbilityImpl a1, ActivatedManaAbilityImpl a2) {
- int a1Max = 0;
- for (Mana netMana : a1.getNetMana(game)) {
- if (netMana.count() > a1Max) {
- a1Max = netMana.count();
- }
+ Collections.sort(manaAbilities, (a1, a2) -> {
+ int a1Max = 0;
+ for (Mana netMana : a1.getNetMana(game)) {
+ if (netMana.count() > a1Max) {
+ a1Max = netMana.count();
}
- int a2Max = 0;
- for (Mana netMana : a2.getNetMana(game)) {
- if (netMana.count() > a2Max) {
- a2Max = netMana.count();
- }
- }
- return CardUtil.overflowDec(a2Max, a1Max);
}
+ int a2Max = 0;
+ for (Mana netMana : a2.getNetMana(game)) {
+ if (netMana.count() > a2Max) {
+ a2Max = netMana.count();
+ }
+ }
+ return CardUtil.overflowDec(a2Max, a1Max);
});
}
return manaAbilities;
@@ -1972,7 +982,6 @@ public class ComputerPlayer extends PlayerImpl {
* costs that can't be paid by any other producers
*
* @param unpaid - the amount of unpaid mana costs
- * @param game
* @return List
*/
private List getSortedProducers(ManaCosts unpaid, Game game) {
@@ -2008,12 +1017,7 @@ public class ComputerPlayer extends PlayerImpl {
private List sortByValue(Map map) {
List> list = new LinkedList<>(map.entrySet());
- Collections.sort(list, new Comparator>() {
- @Override
- public int compare(Entry o1, Entry o2) {
- return (o1.getValue().compareTo(o2.getValue()));
- }
- });
+ Collections.sort(list, Comparator.comparing(Entry::getValue));
List result = new ArrayList<>();
for (Entry entry : list) {
result.add(entry.getKey());
@@ -2023,39 +1027,7 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public int announceX(int min, int max, String message, Game game, Ability source, boolean isManaPay) {
- // fast calc on nothing to choose
- if (min >= max) {
- return min;
- }
-
- // TODO: add good/bad effects support
- // TODO: add simple game simulations like declare blocker (need to find only workable payment)?
- // TODO: remove random logic or make it more stable (e.g. use same value in same game cycle)
-
- // protection from too big values
- int realMin = min;
- int realMax = max;
- if (max == Integer.MAX_VALUE) {
- realMax = Math.max(realMin, 10); // AI don't need huge values for X, cause can't use infinite combos
- }
-
- int xValue;
- if (isManaPay) {
- // as X mana payment - due available mana
- xValue = Math.max(0, getAvailableManaProducers(game).size() - source.getManaCostsToPay().getUnpaid().manaValue());
- } else {
- // as X actions
- xValue = RandomUtil.nextInt(realMax + 1);
- }
-
- if (xValue > realMax) {
- xValue = realMax;
- }
- if (xValue < realMin) {
- xValue = realMin;
- }
-
- return xValue;
+ return makeChoiceAmount(min, max, game, source, isManaPay);
}
@Override
@@ -2069,12 +1041,11 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseUse(Outcome outcome, String message, Ability source, Game game) {
- return this.chooseUse(outcome, message, null, null, null, source, game);
+ return chooseUse(outcome, message, null, null, null, source, game);
}
@Override
public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) {
- log.debug("chooseUse: " + outcome.isGood());
// Be proactive! Always use abilities, the evaluation function will decide if it's good or not
// Otherwise some abilities won't be used by AI like LoseTargetEffect that has "bad" outcome
// but still is good when targets opponent
@@ -2083,7 +1054,6 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean choose(Outcome outcome, Choice choice, Game game) {
- log.debug("choose 3");
//TODO: improve this
// choose creature type
@@ -2190,86 +1160,22 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
- if (cards == null || cards.isEmpty()) {
- return false;
- }
-
- boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getTargetController() != null
- && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- // we still use playerId when getting cards even if they don't control the search
- List cardChoices = new ArrayList<>(cards.getCards(target.getFilter(), playerId, source, game));
- isAddedSomething = false;
- while (!cardChoices.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cardChoices, outcome, target, source, game);
- if (card != null) {
- cardChoices.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
-
- // try to fill as much as possible for good effect (see while end) or half for bad (see if)
- if (target.isChosen(game) && !outcome.isGood() && target.getTargets().size() > target.getMinNumberOfTargets() + (target.getMaxNumberOfTargets() - target.getMinNumberOfTargets()) / 2) {
- return true;
- }
- }
- return isAddedSomething;
+ return makeChoice(outcome, target, source, game, cards);
}
@Override
public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
- log.debug("choose 2");
- if (cards == null || cards.isEmpty()) {
- return true;
- }
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getTargetController() != null
- && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- List cardChoices = new ArrayList<>(cards.getCards(target.getFilter(), abilityControllerId, source, game));
- do {
- Card card = selectCard(abilityControllerId, cardChoices, outcome, target, game);
- if (card != null) {
- target.add(card.getId(), game);
- cardChoices.remove(card); // selectCard don't remove cards (only on second+ tries)
- } else {
- // We don't have any valid target to choose so stop choosing
- return target.isChosen(game);
- }
- // try to fill as much as possible for good effect (see while end) or half for bad (see if)
- if (outcome == Outcome.Neutral && target.getTargets().size() > target.getMinNumberOfTargets() + (target.getMaxNumberOfTargets() - target.getMinNumberOfTargets()) / 2) {
- return target.isChosen(game);
- }
- } while (target.getTargets().size() < target.getMaxNumberOfTargets());
- return true;
+ return makeChoice(outcome, target, source, game, cards);
}
@Override
public boolean choosePile(Outcome outcome, String message, List extends Card> pile1, List extends Card> pile2, Game game) {
//TODO: improve this
- return true;
+ return true; // select left pile all the time
}
@Override
public void selectAttackers(Game game, UUID attackingPlayerId) {
- log.debug("selectAttackers");
UUID opponentId = game.getCombat().getDefenders().iterator().next();
Attackers attackers = getPotentialAttackers(game);
List blockers = getOpponentBlockers(opponentId, game);
@@ -2293,8 +1199,6 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) {
- log.debug("selectBlockers");
-
List blockers = getAvailableBlockers(game);
CombatSimulator sim = simulateBlock(CombatSimulator.load(game), blockers, game);
@@ -2309,14 +1213,12 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public int chooseReplacementEffect(Map effectsMap, Map objectsMap, Game game) {
- log.debug("chooseReplacementEffect");
//TODO: implement this
- return 0;
+ return 0; // select first effect all the time
}
@Override
public Mode chooseMode(Modes modes, Ability source, Game game) {
- log.debug("chooseMode");
if (modes.getMode() != null && modes.getMaxModes(game, source) == modes.getSelectedModes().size()) {
// mode was already set by the AI
return modes.getMode();
@@ -2325,52 +1227,30 @@ public class ComputerPlayer extends PlayerImpl {
// spell modes simulated by AI, see addModeOptions
// trigger modes chooses here
// TODO: add AI support to select best modes, current code uses first valid mode
- AvailableMode:
- for (Mode mode : modes.getAvailableModes(source, game)) {
- for (UUID selectedModeId : modes.getSelectedModes()) {
- Mode selectedMode = modes.get(selectedModeId);
- if (selectedMode.getId().equals(mode.getId())) {
- continue AvailableMode;
- }
- }
- if (mode.getTargets().canChoose(source.getControllerId(), source, game)) { // and where targets are available
- return mode;
- }
- }
- return null;
+ return modes.getAvailableModes(source, game).stream()
+ .filter(mode -> !modes.getSelectedModes().contains(mode.getId()))
+ .filter(mode -> mode.getTargets().canChoose(source.getControllerId(), source, game))
+ .findFirst()
+ .orElse(null);
}
@Override
public TriggeredAbility chooseTriggeredAbility(List abilities, Game game) {
- log.debug("chooseTriggeredAbility: " + abilities.toString());
//TODO: improve this
if (!abilities.isEmpty()) {
- return abilities.get(0);
+ return abilities.get(0); // select first trigger all the time
}
return null;
}
@Override
- // TODO: add AI support with outcome and replace random with min/max
public int getAmount(int min, int max, String message, Ability source, Game game) {
- log.debug("getAmount");
-
- // fast calc on nothing to choose
- if (min >= max) {
- return min;
- }
-
- if (min == 0) {
- return RandomUtil.nextInt(CardUtil.overflowInc(max, 1));
- }
- return min;
+ return makeChoiceAmount(min, max, game, source, false);
}
@Override
public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages,
int totalMin, int totalMax, MultiAmountType type, Game game) {
- log.debug("getMultiAmount");
-
int needCount = messages.size();
List defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0) {
@@ -2385,7 +1265,7 @@ public class ComputerPlayer extends PlayerImpl {
}
// GOOD effect
- // values must be stable, so AI must able to simulate it and choose correct actions
+ // values must be stable, so AI must be able to simulate it and choose correct actions
// fill max values as much as possible
return MultiAmountType.prepareMaxValues(messages, totalMin, totalMax);
}
@@ -2397,8 +1277,8 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public void sideboard(Match match, Deck deck) {
- //TODO: improve this
- match.submitDeck(playerId, deck);
+ // TODO: improve this
+ match.submitDeck(playerId, deck); // do not change a deck
}
private static void addBasicLands(Deck deck, String landName, int number) {
@@ -2406,7 +1286,7 @@ public class ComputerPlayer extends PlayerImpl {
CardCriteria criteria = new CardCriteria();
if (!landSets.isEmpty()) {
- criteria.setCodes(landSets.toArray(new String[landSets.size()]));
+ criteria.setCodes(landSets.toArray(new String[0]));
}
criteria.rarities(Rarity.LAND).name(landName);
List cards = CardRepository.instance.findCards(criteria);
@@ -2462,13 +1342,10 @@ public class ComputerPlayer extends PlayerImpl {
// sort card pool by top score
List sortedCards = new ArrayList<>(cardPool);
- Collections.sort(sortedCards, new Comparator() {
- @Override
- public int compare(Card o1, Card o2) {
- Integer score1 = RateCard.rateCard(o1, colors);
- Integer score2 = RateCard.rateCard(o2, colors);
- return score2.compareTo(score1);
- }
+ Collections.sort(sortedCards, (o1, o2) -> {
+ Integer score1 = RateCard.rateCard(o1, colors);
+ Integer score2 = RateCard.rateCard(o2, colors);
+ return score2.compareTo(score1);
});
// get top cards
@@ -2565,10 +1442,6 @@ public class ComputerPlayer extends PlayerImpl {
return selectBestCardInner(cards, chosenColors, null, null, null);
}
- public Card selectBestCardTarget(List cards, List chosenColors, Target target, Ability targetingSource, Game game) {
- return selectBestCardInner(cards, chosenColors, target, targetingSource, game);
- }
-
/**
* @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
*/
@@ -2662,7 +1535,7 @@ public class ComputerPlayer extends PlayerImpl {
// try to counter pick without any color restriction
Card counterPick = selectBestCard(cards, Collections.emptyList());
int counterPickScore = RateCard.getBaseCardScore(counterPick);
- // card is really good
+ // card is perfect
// take it!
if (counterPickScore >= 80) {
bestCard = counterPick;
@@ -2695,9 +1568,6 @@ public class ComputerPlayer extends PlayerImpl {
/**
* Remember picked card with its score.
- *
- * @param card
- * @param score
*/
protected void rememberPick(Card card, int score) {
pickedCards.add(new PickedCard(card, score));
@@ -2707,22 +1577,17 @@ public class ComputerPlayer extends PlayerImpl {
* Choose 2 deck colors for draft: 1. there should be at least 3 cards in
* card pool 2. at least 2 cards should have different colors 3. get card
* colors as chosen starting from most rated card
- *
- * @return
*/
protected List chooseDeckColorsIfPossible() {
if (pickedCards.size() > 2) {
// sort by score and color mana symbol count in descending order
- pickedCards.sort(new Comparator() {
- @Override
- public int compare(PickedCard o1, PickedCard o2) {
- if (o1.score.equals(o2.score)) {
- Integer i1 = RateCard.getColorManaCount(o1.card);
- Integer i2 = RateCard.getColorManaCount(o2.card);
- return i2.compareTo(i1);
- }
- return o2.score.compareTo(o1.score);
+ pickedCards.sort((o1, o2) -> {
+ if (o1.score.equals(o2.score)) {
+ Integer i1 = RateCard.getColorManaCount(o1.card);
+ Integer i2 = RateCard.getColorManaCount(o2.card);
+ return i2.compareTo(i1);
}
+ return o2.score.compareTo(o1.score);
});
Set chosenSymbols = new HashSet<>();
for (PickedCard picked : pickedCards) {
@@ -2771,7 +1636,6 @@ public class ComputerPlayer extends PlayerImpl {
}
protected Attackers getPotentialAttackers(Game game) {
- log.debug("getAvailableAttackers");
Attackers attackers = new Attackers();
List creatures = super.getAvailableAttackers(game);
for (Permanent creature : creatures) {
@@ -2788,7 +1652,6 @@ public class ComputerPlayer extends PlayerImpl {
}
protected int combatPotential(Permanent creature, Game game) {
- log.debug("combatPotential");
if (!creature.canAttack(null, game)) {
return 0;
}
@@ -2807,7 +1670,6 @@ public class ComputerPlayer extends PlayerImpl {
}
protected CombatSimulator simulateAttack(Attackers attackers, List blockers, UUID opponentId, Game game) {
- log.debug("simulateAttack");
List attackersList = attackers.getAttackers();
CombatSimulator best = new CombatSimulator();
int bestResult = 0;
@@ -2839,8 +1701,6 @@ public class ComputerPlayer extends PlayerImpl {
}
protected CombatSimulator simulateBlock(CombatSimulator combat, List blockers, Game game) {
- log.debug("simulateBlock");
-
TreeNode simulations;
simulations = new TreeNode<>(combat);
@@ -2905,69 +1765,12 @@ public class ComputerPlayer extends PlayerImpl {
return worst;
}
- protected void findBestPermanentTargets(Outcome outcome, UUID abilityControllerId, UUID sourceId, Ability source, FilterPermanent filter, Game game, Target target,
- List goodList, List badList, List allList) {
- // searching for most valuable/powerfull permanents
- goodList.clear();
- badList.clear();
- allList.clear();
- List usedTargets = target.getTargets();
-
- // search all
- for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, abilityControllerId, source, game)) {
- if (usedTargets.contains(permanent.getId())) {
- continue;
- }
-
- if (outcome.isGood()) {
- // good effect
- if (permanent.isControlledBy(abilityControllerId)) {
- goodList.add(permanent);
- } else {
- badList.add(permanent);
- }
- } else {
- // bad effect
- if (permanent.isControlledBy(abilityControllerId)) {
- badList.add(permanent);
- } else {
- goodList.add(permanent);
- }
- }
- }
-
- // sort from tiny to big (more valuable)
- PermanentComparator comparator = new PermanentComparator(game);
- goodList.sort(comparator);
- badList.sort(comparator);
-
- // most valueable goes first in good list
- Collections.reverse(goodList);
- // most weakest goes first in bad list (no need to reverse)
- //Collections.reverse(badList);
-
- allList.addAll(goodList);
- allList.addAll(badList);
-
- // "can target all mode" don't need your/opponent lists -- all targets goes with same value
- if (outcome.isCanTargetAll()) {
- allList.sort(comparator); // bad sort
- if (outcome.isGood()) {
- Collections.reverse(allList); // good sort
- }
- goodList.clear();
- goodList.addAll(allList);
- badList.clear();
- badList.addAll(allList);
- }
- }
-
protected List threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List targets) {
return threats(playerId, source, filter, game, targets, true);
}
- protected List threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List targets, boolean mostValueableGoFirst) {
- // most valuable/powerfull permanents goes at first
+ protected List threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List targets, boolean mostValuableGoFirst) {
+ // most valuable/powerfully permanents goes at first
List threats;
if (playerId == null) {
threats = game.getBattlefield().getActivePermanents(filter, this.getId(), source, game); // all permanents within the range of the player
@@ -2984,7 +1787,7 @@ public class ComputerPlayer extends PlayerImpl {
}
}
Collections.sort(threats, new PermanentComparator(game));
- if (mostValueableGoFirst) {
+ if (mostValuableGoFirst) {
Collections.reverse(threats);
}
return threats;
@@ -2999,15 +1802,6 @@ public class ComputerPlayer extends PlayerImpl {
log.info(sb.toString());
}
- protected void logAbilityList(String message, List list) {
- StringBuilder sb = new StringBuilder();
- sb.append(message).append(": ");
- for (Ability ability : list) {
- sb.append(ability.getRule()).append(',');
- }
- log.debug(sb.toString());
- }
-
private void playRemoval(Set creatures, Game game) {
for (UUID creatureId : creatures) {
for (Card card : this.playableInstant) {
@@ -3057,140 +1851,14 @@ public class ComputerPlayer extends PlayerImpl {
return new ComputerPlayer(this);
}
- @Deprecated // TODO: replace by standard while cycle with cards.isempty and addTarget
- private boolean tryAddTarget(Target target, UUID id, Ability source, Game game) {
- // workaround to to check successfull targets add
- int before = target.getTargets().size();
- target.addTarget(id, source, game);
- int after = target.getTargets().size();
- return before != after;
- }
-
private boolean tryAddTarget(Target target, UUID id, int amount, Ability source, Game game) {
- // workaround to to check successfull targets add
+ // workaround to check successfully targets add
int before = target.getTargets().size();
target.addTarget(id, amount, source, game);
int after = target.getTargets().size();
return before != after;
}
- @Deprecated // TODO: replace by source only version
- private boolean selectPlayer(Outcome outcome, Target target, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) {
- return selectPlayerInner(outcome, target, null, abilityControllerId, randomOpponentId, game, required);
- }
-
- private boolean selectPlayerTarget(Outcome outcome, Target target, Ability targetingSource, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) {
- return selectPlayerInner(outcome, target, targetingSource, abilityControllerId, randomOpponentId, game, required);
- }
-
- /**
- * Sets a possible target player. Depends on bad/good outcome
- *
- * Return false on no more valid targets, e.g. can stop choose dialog
- *
- * @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
- */
- private boolean selectPlayerInner(Outcome outcome, Target target, Ability targetingSource, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) {
- Outcome affectedOutcome;
- if (abilityControllerId == this.playerId) {
- // selects for itself
- affectedOutcome = outcome;
- } else {
- // selects for another player
- affectedOutcome = Outcome.inverse(outcome);
- }
-
- if (target.getOriginalTarget() instanceof TargetOpponent) {
- if (targetingSource == null) {
- if (target.canTarget(randomOpponentId, game)) {
- if (!target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game)) {
- if (!target.contains(randomOpponentId)) {
- target.addTarget(randomOpponentId, targetingSource, game);
- return true;
- }
- }
- for (UUID possibleOpponentId : game.getOpponents(getId(), true)) {
- if (targetingSource == null) {
- if (target.canTarget(possibleOpponentId, game)) {
- if (!target.contains(possibleOpponentId)) {
- target.add(possibleOpponentId, game);
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, possibleOpponentId, targetingSource, game)) {
- if (!target.contains(possibleOpponentId)) {
- target.addTarget(possibleOpponentId, targetingSource, game);
- return true;
- }
- }
- }
- return false;
- }
-
- UUID sourceId = targetingSource != null ? targetingSource.getSourceId() : null;
- if (target.getOriginalTarget() instanceof TargetPlayer) {
- if (affectedOutcome.isGood()) {
- if (targetingSource == null) {
- // good
- if (target.canTarget(getId(), game) && !target.contains(getId())) {
- target.add(getId(), game);
- return true;
- }
- if (target.isRequired(sourceId, game)) {
- if (target.canTarget(randomOpponentId, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- return true;
- }
- }
- } else {
- // good
- if (target.canTarget(abilityControllerId, getId(), targetingSource, game) && !target.contains(getId())) {
- target.addTarget(getId(), targetingSource, game);
- return true;
- }
- if (target.isRequired(sourceId, game)) {
- if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game) && !target.contains(randomOpponentId)) {
- target.addTarget(randomOpponentId, targetingSource, game);
- return true;
- }
- }
- }
- } else if (targetingSource == null) {
- // bad
- if (target.canTarget(randomOpponentId, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- return true;
- }
- if (target.isRequired(sourceId, game)) {
- if (target.canTarget(getId(), game) && !target.contains(getId())) {
- target.add(getId(), game);
- return true;
- }
- }
- } else {
- // bad
- if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game) && !target.contains(randomOpponentId)) {
- target.addTarget(randomOpponentId, targetingSource, game);
- return true;
- }
- if (required) {
- if (target.canTarget(abilityControllerId, getId(), targetingSource, game) && !target.contains(getId())) {
- target.addTarget(getId(), targetingSource, game);
- return true;
- }
- }
- }
- return false;
- }
-
- return false;
- }
-
/**
* Returns an opponent by random
*/
@@ -3201,8 +1869,11 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) {
- Map useable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), card, game.getState().getZone(card.getId()), noMana);
- return useable.values().stream().findFirst().orElse(null);
+ Map usable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), card, game.getState().getZone(card.getId()), noMana);
+ return usable.values().stream()
+ .filter(a -> a.getTargets().canChoose(getId(), a, game))
+ .findFirst()
+ .orElse(null);
}
@Override
@@ -3248,4 +1919,4 @@ public class ComputerPlayer extends PlayerImpl {
public void setLastThinkTime(long lastThinkTime) {
this.lastThinkTime = lastThinkTime;
}
-}
+}
\ No newline at end of file
diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsComparator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsComparator.java
new file mode 100644
index 00000000000..d8d9583208a
--- /dev/null
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsComparator.java
@@ -0,0 +1,156 @@
+package mage.player.ai;
+
+import mage.MageItem;
+import mage.MageObject;
+import mage.cards.Card;
+import mage.constants.Zone;
+import mage.counters.CounterType;
+import mage.game.Game;
+import mage.game.permanent.Permanent;
+import mage.player.ai.score.GameStateEvaluator2;
+import mage.players.PlayableObjectsList;
+import mage.players.Player;
+
+import java.util.Comparator;
+import java.util.UUID;
+
+/**
+ * AI related code - compare and sort possible targets due target/effect type
+ *
+ * @author JayDi85
+ */
+public class PossibleTargetsComparator {
+
+ UUID abilityControllerId;
+ Game game;
+ PlayableObjectsList playableItems = new PlayableObjectsList();
+
+ public PossibleTargetsComparator(UUID abilityControllerId, Game game) {
+ this.abilityControllerId = abilityControllerId;
+ this.game = game;
+ }
+
+ public void findPlayableItems() {
+ this.playableItems = this.game.getPlayer(this.abilityControllerId).getPlayableObjects(this.game, Zone.ALL);
+ }
+
+ private int getScoreFromBattlefield(MageItem item) {
+ if (item instanceof Permanent) {
+ // use battlefield score instead simple life
+ return GameStateEvaluator2.evaluatePermanent((Permanent) item, game, false);
+ } else {
+ return getScoreFromLife(item);
+ }
+ }
+
+ private String getName(MageItem item) {
+ if (item instanceof Player) {
+ return ((Player) item).getName();
+ } else if (item instanceof MageObject) {
+ return ((MageObject) item).getName();
+ } else {
+ return "unknown";
+ }
+ }
+
+ private int getScoreFromLife(MageItem item) {
+ // TODO: replace permanent/card life by battlefield score?
+ int res = 0;
+ if (item instanceof Player) {
+ res = ((Player) item).getLife();
+ } else if (item instanceof Card) {
+ Card card = (Card) item;
+ if (card.isPlaneswalker(game)) {
+ res = card.getCounters(game).getCount(CounterType.LOYALTY);
+ } else if (card.isBattle(game)) {
+ res = card.getCounters(game).getCount(CounterType.DEFENSE);
+ } else {
+ int damage = 0;
+ if (card instanceof Permanent) {
+ damage = ((Permanent) card).getDamage();
+ }
+ res = Math.max(0, card.getToughness().getValue() - damage);
+ }
+ // instant
+ if (res == 0) {
+ res = card.getManaValue();
+ }
+ }
+
+ return res;
+ }
+
+ private boolean isMyItem(MageItem item) {
+ return PossibleTargetsSelector.isMyItem(this.abilityControllerId, item);
+ }
+
+ // sort by name-id at the end, so AI will use same choices in all simulations
+ private final Comparator BY_NAME = (o1, o2) -> getName(o2).compareTo(getName(o1));
+ private final Comparator BY_ID = Comparator.comparing(MageItem::getId);
+
+ private final Comparator BY_ME = (o1, o2) -> Boolean.compare(
+ isMyItem(o2),
+ isMyItem(o1)
+ );
+
+ private final Comparator BY_BIGGER_SCORE = (o1, o2) -> Integer.compare(
+ getScoreFromBattlefield(o2),
+ getScoreFromBattlefield(o1)
+ );
+
+ private final Comparator BY_PLAYABLE = (o1, o2) -> Boolean.compare(
+ this.playableItems.containsObject(o2.getId()),
+ this.playableItems.containsObject(o1.getId())
+ );
+
+ private final Comparator BY_LAND = (o1, o2) -> {
+ boolean isLand1 = o1 instanceof MageObject && ((MageObject) o1).isLand(game);
+ boolean isLand2 = o2 instanceof MageObject && ((MageObject) o2).isLand(game);
+ return Boolean.compare(isLand2, isLand1);
+ };
+
+ private final Comparator BY_TYPE_PLAYER = (o1, o2) -> Boolean.compare(
+ o2 instanceof Player,
+ o1 instanceof Player
+ );
+
+ private final Comparator BY_TYPE_PLANESWALKER = (o1, o2) -> {
+ boolean isPlaneswalker1 = o1 instanceof MageObject && ((MageObject) o1).isPlaneswalker(game);
+ boolean isPlaneswalker2 = o2 instanceof MageObject && ((MageObject) o2).isPlaneswalker(game);
+ return Boolean.compare(isPlaneswalker2, isPlaneswalker1);
+ };
+
+ private final Comparator BY_TYPE_BATTLE = (o1, o2) -> {
+ boolean isBattle1 = o1 instanceof MageObject && ((MageObject) o1).isBattle(game);
+ boolean isBattle2 = o2 instanceof MageObject && ((MageObject) o2).isBattle(game);
+ return Boolean.compare(isBattle2, isBattle1);
+ };
+
+ private final Comparator BY_TYPES = BY_TYPE_PLANESWALKER
+ .thenComparing(BY_TYPE_BATTLE)
+ .thenComparing(BY_TYPE_PLAYER);
+
+ /**
+ * Default sorting for good effects - put the biggest items to the top
+ */
+ public final Comparator ANY_MOST_VALUABLE_FIRST = BY_TYPES
+ .thenComparing(BY_BIGGER_SCORE)
+ .thenComparing(BY_NAME)
+ .thenComparing(BY_ID);
+ public final Comparator ANY_MOST_VALUABLE_LAST = ANY_MOST_VALUABLE_FIRST.reversed();
+
+ /**
+ * Default sorting for good effects - put own and biggest items to the top
+ */
+ public final Comparator MY_MOST_VALUABLE_FIRST = BY_ME
+ .thenComparing(ANY_MOST_VALUABLE_FIRST);
+ public final Comparator MY_MOST_VALUABLE_LAST = MY_MOST_VALUABLE_FIRST.reversed();
+
+ /**
+ * Sorting for discard effects - put the biggest unplayable at the top, lands at the end anyway
+ */
+ public final Comparator ANY_UNPLAYABLE_AND_USELESS = BY_LAND.reversed()
+ .thenComparing(BY_PLAYABLE.reversed())
+ .thenComparing(ANY_MOST_VALUABLE_FIRST);
+
+}
diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java
new file mode 100644
index 00000000000..60a5b0ae0a5
--- /dev/null
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java
@@ -0,0 +1,184 @@
+package mage.player.ai;
+
+import mage.MageItem;
+import mage.abilities.Ability;
+import mage.constants.Outcome;
+import mage.constants.Zone;
+import mage.game.ControllableOrOwnerable;
+import mage.game.Game;
+import mage.players.Player;
+import mage.target.Target;
+import mage.target.common.TargetCardInGraveyardBattlefieldOrStack;
+import mage.target.common.TargetDiscard;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * AI related code - find possible targets and sort it due priority
+ *
+ * @author JayDi85
+ */
+public class PossibleTargetsSelector {
+
+ Outcome outcome;
+ Target target;
+ UUID abilityControllerId;
+ Ability source;
+ Game game;
+
+ PossibleTargetsComparator comparators;
+
+ // possible targets lists
+ List me = new ArrayList<>();
+ List opponents = new ArrayList<>();
+ List any = new ArrayList<>(); // for outcomes with any target like copy
+
+ public PossibleTargetsSelector(Outcome outcome, Target target, UUID abilityControllerId, Ability source, Game game) {
+ this.outcome = outcome;
+ this.target = target;
+ this.abilityControllerId = abilityControllerId;
+ this.source = source;
+ this.game = game;
+ this.comparators = new PossibleTargetsComparator(abilityControllerId, game);
+ }
+
+ public void findNewTargets(Set fromTargetsList) {
+ // collect new valid targets
+ List found = target.possibleTargets(abilityControllerId, source, game).stream()
+ .filter(id -> !target.contains(id))
+ .filter(id -> fromTargetsList == null || fromTargetsList.contains(id))
+ .filter(id -> target.canTarget(abilityControllerId, id, source, game))
+ .map(id -> {
+ Player player = game.getPlayer(id);
+ if (player != null) {
+ return player;
+ } else {
+ return game.getObject(id);
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ // split targets between me and opponents
+ found.forEach(item -> {
+ if (isMyItem(abilityControllerId, item)) {
+ this.me.add(item);
+ } else {
+ this.opponents.add(item);
+ }
+ this.any.add(item);
+ });
+
+ if (target instanceof TargetDiscard) {
+ // sort due unplayable
+ sortByUnplayableAndUseless();
+ } else {
+ // sort due good/bad outcome
+ sortByMostValuableTargets();
+ }
+ }
+
+ /**
+ * Sorting for any good/bad effects
+ */
+ private void sortByMostValuableTargets() {
+ if (isGoodEffect()) {
+ // for good effect must choose the biggest objects
+ this.me.sort(comparators.MY_MOST_VALUABLE_FIRST);
+ this.opponents.sort(comparators.MY_MOST_VALUABLE_LAST);
+ this.any.sort(comparators.ANY_MOST_VALUABLE_FIRST);
+ } else {
+ // for bad effect must choose the smallest objects
+ this.me.sort(comparators.MY_MOST_VALUABLE_LAST);
+ this.opponents.sort(comparators.MY_MOST_VALUABLE_FIRST);
+ this.any.sort(comparators.ANY_MOST_VALUABLE_LAST);
+ }
+ }
+
+ /**
+ * Sorting for discard
+ */
+ private void sortByUnplayableAndUseless() {
+ // used
+ // no good or bad effect - you must choose
+ comparators.findPlayableItems();
+ this.me.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
+ this.opponents.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
+ this.any.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
+ }
+
+ /**
+ * Priority targets. Try to use as much as possible.
+ */
+ public List getGoodTargets() {
+ if (isAnyEffect()) {
+ return this.any;
+ }
+
+ if (isGoodEffect()) {
+ return this.me;
+ } else {
+ return this.opponents;
+ }
+ }
+
+ /**
+ * Optional targets. Try to ignore bad targets (e.g. opponent's creatures for your good effect).
+ */
+ public List getBadTargets() {
+ if (isAnyEffect()) {
+ return Collections.emptyList();
+ }
+
+ if (isGoodEffect()) {
+ return this.opponents;
+ } else {
+ return this.me;
+ }
+ }
+
+ public static boolean isMyItem(UUID abilityControllerId, MageItem item) {
+ if (item instanceof Player) {
+ return item.getId().equals(abilityControllerId);
+ } else if (item instanceof ControllableOrOwnerable) {
+ return ((ControllableOrOwnerable) item).getControllerOrOwnerId().equals(abilityControllerId);
+ }
+ return false;
+ }
+
+ private boolean isAnyEffect() {
+ boolean isAnyEffect = outcome.anyTargetHasSameValue();
+
+ if (hasGoodExile()) {
+ isAnyEffect = true;
+ }
+
+ return isAnyEffect;
+ }
+
+ private boolean isGoodEffect() {
+ boolean isGoodEffect = outcome.isGood();
+
+ if (hasGoodExile()) {
+ isGoodEffect = true;
+ }
+
+ return isGoodEffect;
+ }
+
+ private boolean hasGoodExile() {
+ // exile workaround: exile is bad, but exile from library or graveyard in most cases is good
+ // (more exiled -- more good things you get, e.g. delve's pay or search cards with same name)
+ if (outcome == Outcome.Exile) {
+ if (Zone.GRAVEYARD.match(target.getZone())
+ || Zone.LIBRARY.match(target.getZone())) {
+ // TargetCardInGraveyardBattlefieldOrStack - used for additional payment like Craft, so do not allow big cards for it
+ if (!(target instanceof TargetCardInGraveyardBattlefieldOrStack)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/ArtificialScoringSystem.java
similarity index 99%
rename from Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java
rename to Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/ArtificialScoringSystem.java
index 35ededd0de4..4cef32c7ec8 100644
--- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/ArtificialScoringSystem.java
@@ -1,4 +1,4 @@
-package mage.player.ai.ma;
+package mage.player.ai.score;
import mage.MageObject;
import mage.abilities.Ability;
diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/GameStateEvaluator2.java
similarity index 99%
rename from Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java
rename to Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/GameStateEvaluator2.java
index ebc0dbcdade..9c96a619d10 100644
--- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/GameStateEvaluator2.java
@@ -1,8 +1,7 @@
-package mage.player.ai;
+package mage.player.ai.score;
import mage.game.Game;
import mage.game.permanent.Permanent;
-import mage.player.ai.ma.ArtificialScoringSystem;
import mage.players.Player;
import org.apache.log4j.Logger;
diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/MagicAbility.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/MagicAbility.java
similarity index 95%
rename from Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/MagicAbility.java
rename to Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/MagicAbility.java
index 43fae122d17..31f71009b60 100644
--- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/MagicAbility.java
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/score/MagicAbility.java
@@ -1,4 +1,4 @@
-package mage.player.ai.ma;
+package mage.player.ai.score;
import mage.abilities.Ability;
import mage.abilities.keyword.*;
@@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.Map;
/**
+ * TODO: outdated, replace by edh or commander brackets ability score
* @author nantuko
*/
public final class MagicAbility {
diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java
deleted file mode 100644
index e67d6383710..00000000000
--- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-package mage.player.ai.simulators;
-
-import mage.abilities.ActivatedAbility;
-import mage.cards.Card;
-import mage.game.Game;
-import mage.game.permanent.Permanent;
-import mage.player.ai.ComputerPlayer;
-import mage.player.ai.PermanentEvaluator;
-import mage.players.Player;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- *
- * @author BetaSteward_at_googlemail.com
- */
-public class ActionSimulator {
-
- private ComputerPlayer player;
- private List playableInstants = new ArrayList<>();
- private List playableAbilities = new ArrayList<>();
-
- private Game game;
-
- public ActionSimulator(ComputerPlayer player) {
- this.player = player;
- }
-
- public void simulate(Game game) {
-
- }
-
- public int evaluateState() {
- // must find all leaved opponents
- Player opponent = game.getPlayer(game.getOpponents(player.getId(), false).stream().findFirst().orElse(null));
- if (opponent == null) {
- return Integer.MAX_VALUE;
- }
-
- if (game.checkIfGameIsOver()) {
- if (player.hasLost() || opponent.hasWon()) {
- return Integer.MIN_VALUE;
- }
- if (opponent.hasLost() || player.hasWon()) {
- return Integer.MAX_VALUE;
- }
- }
- int value = player.getLife();
- value -= opponent.getLife();
- PermanentEvaluator evaluator = new PermanentEvaluator();
- for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
- value += evaluator.evaluate(permanent, game);
- }
- for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
- value -= evaluator.evaluate(permanent, game);
- }
- value += player.getHand().size();
- value -= opponent.getHand().size();
- return value;
- }
-
-}
diff --git a/Mage.Sets/src/mage/cards/d/DeadWeight.java b/Mage.Sets/src/mage/cards/d/DeadWeight.java
index 0f7e6378f6f..76d576642b5 100644
--- a/Mage.Sets/src/mage/cards/d/DeadWeight.java
+++ b/Mage.Sets/src/mage/cards/d/DeadWeight.java
@@ -1,7 +1,5 @@
-
package mage.cards.d;
-import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.AttachEffect;
@@ -10,30 +8,30 @@ import mage.abilities.keyword.EnchantAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
-import mage.constants.SubType;
import mage.constants.Duration;
import mage.constants.Outcome;
-import mage.constants.Zone;
+import mage.constants.SubType;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
+import java.util.UUID;
+
/**
- *
* @author Alvin
*/
public final class DeadWeight extends CardImpl {
public DeadWeight(UUID ownerId, CardSetInfo setInfo) {
- super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{B}");
+ super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}");
this.subtype.add(SubType.AURA);
-
// Enchant creature
TargetPermanent auraTarget = new TargetCreaturePermanent();
this.getSpellAbility().addTarget(auraTarget);
this.getSpellAbility().addEffect(new AttachEffect(Outcome.Detriment));
Ability ability = new EnchantAbility(auraTarget);
this.addAbility(ability);
+
// Enchanted creature gets -2/-2.
this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(-2, -2, Duration.WhileOnBattlefield)));
}
diff --git a/Mage.Sets/src/mage/cards/s/SepulchralPrimordial.java b/Mage.Sets/src/mage/cards/s/SepulchralPrimordial.java
index 40f47af3a78..412d01515fa 100644
--- a/Mage.Sets/src/mage/cards/s/SepulchralPrimordial.java
+++ b/Mage.Sets/src/mage/cards/s/SepulchralPrimordial.java
@@ -62,7 +62,7 @@ public final class SepulchralPrimordial extends CardImpl {
class SepulchralPrimordialEffect extends OneShotEffect {
SepulchralPrimordialEffect() {
- super(Outcome.PutCreatureInPlay);
+ super(Outcome.GainControl);
this.staticText = "for each opponent, you may put up to one target creature card from that player's graveyard onto the battlefield under your control";
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/PossibleTargetsSelectorAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/PossibleTargetsSelectorAITest.java
new file mode 100644
index 00000000000..2ac5a37f76c
--- /dev/null
+++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/PossibleTargetsSelectorAITest.java
@@ -0,0 +1,149 @@
+package org.mage.test.AI.basic;
+
+import mage.MageItem;
+import mage.MageObject;
+import mage.abilities.Ability;
+import mage.abilities.common.SimpleStaticAbility;
+import mage.abilities.effects.common.InfoEffect;
+import mage.constants.Outcome;
+import mage.constants.PhaseStep;
+import mage.constants.Zone;
+import mage.player.ai.PossibleTargetsSelector;
+import mage.players.Player;
+import mage.target.Target;
+import mage.target.common.TargetDiscard;
+import mage.target.common.TargetPermanentOrPlayer;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mage.test.serverside.base.CardTestPlayerBase;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author JayDi85
+ */
+public class PossibleTargetsSelectorAITest extends CardTestPlayerBase {
+
+ @Test
+ public void test_SortByOutcome() {
+ addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
+ addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
+ addCard(Zone.BATTLEFIELD, playerA, "Spectral Bears", 1); // 3/3
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); // land
+ addCard(Zone.BATTLEFIELD, playerA, "Gideon, Martial Paragon", 1); // planeswalker
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Forest", 1); // land
+ addCard(Zone.BATTLEFIELD, playerB, "Goblin Brigand", 1); // 2/2
+ addCard(Zone.BATTLEFIELD, playerB, "Battering Sliver", 1); // 4/4
+
+
+ runCode("check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
+ // most valuable (planeswalker -> player -> bigger -> smaller)
+
+ // good effect
+ PossibleTargetsSelector selector = prepareAnyTargetSelector(Outcome.Benefit);
+ selector.findNewTargets(null);
+ assertTargets("good effect must return my most valuable and biggest as priority", selector.getGoodTargets(), Arrays.asList(
+ "Gideon, Martial Paragon", // pw
+ "PlayerA", // p
+ "Spectral Bears", // 3/3
+ "Balduvian Bears", // 2/2
+ "Arbor Elf", // 1/1
+ "Forest" // l
+ ));
+ assertTargets("good effect must return opponent's lowest as optional", selector.getBadTargets(), Arrays.asList(
+ "Forest", // l
+ "Goblin Brigand", // 2/2
+ "Battering Sliver", // 4/4
+ "PlayerB" // p
+ ));
+
+ // bad effect - must be inverted
+ selector = prepareAnyTargetSelector(Outcome.Detriment);
+ selector.findNewTargets(null);
+ assertTargets("bad effect must return opponent's most valuable and biggest as priority", selector.getGoodTargets(), Arrays.asList(
+ "PlayerB", // p
+ "Battering Sliver", // 4/4
+ "Goblin Brigand", // 1/1
+ "Forest" // l
+ ));
+ assertTargets("bad effect must return my lowest as optional", selector.getBadTargets(), Arrays.asList(
+ "Forest", // l
+ "Arbor Elf", // 1/1
+ "Balduvian Bears", // 2/2
+ "Spectral Bears", // 3/3
+ "PlayerA", // p
+ "Gideon, Martial Paragon" // pw
+ ));
+ });
+
+ setStopAt(1, PhaseStep.END_TURN);
+ setStrictChooseMode(true);
+ execute();
+ }
+
+ @Test
+ public void test_SortByPlayable() {
+ addCard(Zone.HAND, playerA, "Balduvian Bears", 1); // 2/2, {1}{G}
+ addCard(Zone.HAND, playerA, "Arbor Elf", 1); // 1/1, {G}, playable
+ addCard(Zone.HAND, playerA, "Spectral Bears", 1); // 3/3, {1}{G}
+ addCard(Zone.HAND, playerA, "Forest", 1); // land
+ addCard(Zone.HAND, playerA, "Gideon, Martial Paragon", 1); // planeswalker, {4}{W}
+ //
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
+
+ runCode("check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
+ // discard logic (remove the biggest unplayable first, land at the end)
+
+ PossibleTargetsSelector selector = prepareDiscardCardSelector();
+ selector.findNewTargets(null);
+ assertTargets("discard must return biggest unplayable first", selector.getGoodTargets(), Arrays.asList(
+ "Gideon, Martial Paragon", // pw
+ "Spectral Bears", // 3/3
+ "Balduvian Bears", // 2/2
+ "Arbor Elf", // 1/1 - playable
+ "Forest" // l
+ ));
+ });
+
+ setStopAt(1, PhaseStep.END_TURN);
+ setStrictChooseMode(true);
+ execute();
+ }
+
+ private PossibleTargetsSelector prepareAnyTargetSelector(Outcome outcome) {
+ Target target = new TargetPermanentOrPlayer();
+ Ability fakeAbility = new SimpleStaticAbility(new InfoEffect("fake"));
+ return new PossibleTargetsSelector(outcome, target, playerA.getId(), fakeAbility, currentGame);
+ }
+
+ private PossibleTargetsSelector prepareDiscardCardSelector() {
+ Target target = new TargetDiscard(playerA.getId());
+ Ability fakeAbility = new SimpleStaticAbility(new InfoEffect("fake"));
+ // discard sorting do not use outcome
+ return new PossibleTargetsSelector(Outcome.Benefit, target, playerA.getId(), fakeAbility, currentGame);
+ }
+
+ private void assertTargets(String info, List targets, List needTargets) {
+ List currentTargets = targets.stream()
+ .map(item -> {
+ if (item instanceof Player) {
+ return ((Player) item).getName();
+ } else if (item instanceof MageObject) {
+ return ((MageObject) item).getName();
+ } else {
+ return "unknown item";
+ }
+ })
+ .collect(Collectors.toList());
+ String current = String.join("\n", currentTargets);
+ String need = String.join("\n", needTargets);
+ if (!current.equals(need)) {
+ Assert.fail(info + "\n\n"
+ + "NEED targets:\n" + need + "\n\n"
+ + "FOUND targets:\n" + current + "\n");
+ }
+ }
+}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/exile/SearchNameExileTests.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/exile/SearchNameExileTests.java
index 71e83ef6543..3a0019ed703 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/exile/SearchNameExileTests.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/exile/SearchNameExileTests.java
@@ -125,9 +125,9 @@ public class SearchNameExileTests extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Test of Talents", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
- addCard(Zone.GRAVEYARD, playerB, "Ready // Willing", 1);
+ addCard(Zone.GRAVEYARD, playerB, "Ready // Willing", 2);
addCard(Zone.HAND, playerB, "Ready // Willing", 2);
- addCard(Zone.LIBRARY, playerB, "Ready // Willing", 1);
+ addCard(Zone.LIBRARY, playerB, "Ready // Willing", 2);
addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2);
@@ -137,7 +137,7 @@ public class SearchNameExileTests extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Test of Talents", "Ready // Willing", "Ready // Willing");
// TODO: a non strict cause a good AI choice test - make strict and duplicate as really AI test?
- // in non strict mode AI must choose as much as possible in good "up to" target and half in bad target
+ // in non strict mode AI must choose as much as possible from grave/library due good exile effect/cost
setStrictChooseMode(false);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
@@ -149,6 +149,6 @@ public class SearchNameExileTests extends CardTestPlayerBase {
assertHandCount(playerB, "Ready // Willing", 0);
assertHandCount(playerB, 1); //add 2, cast 1, last is exiled+redrawn
- assertExileCount(playerB, "Ready // Willing", 4);
+ assertExileCount(playerB, "Ready // Willing", 6);
}
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopyPermanentSpellTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopyPermanentSpellTest.java
index edda38ee472..7c5caeba8ef 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopyPermanentSpellTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopyPermanentSpellTest.java
@@ -63,20 +63,23 @@ public class CopyPermanentSpellTest extends CardTestPlayerBase {
public void testAuraTokenRedirect() {
makeTester();
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
- addCard(Zone.BATTLEFIELD, playerB, "Centaur Courser");
- addCard(Zone.BATTLEFIELD, playerB, "Hill Giant");
+ addCard(Zone.BATTLEFIELD, playerB, "Centaur Courser"); // 3/3
+ addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4
addCard(Zone.HAND, playerA, "Dead Weight");
- setChoice(playerA, true);
+ setChoice(playerA, true); // use new target
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dead Weight", "Centaur Courser");
+ // it's bad/unboost effect
+ // allow AI make a choice for new target of copied spell (it will be angel as a opponent's bigger creature for bad effect)
+ setStrictChooseMode(false);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerB, "Centaur Courser", 1);
- assertPowerToughness(playerB, "Centaur Courser", 1, 1);
- assertPermanentCount(playerB, "Hill Giant", 1);
- assertPowerToughness(playerB, "Hill Giant", 1, 1);
+ assertPowerToughness(playerB, "Centaur Courser", 3 - 2, 3 - 2);
+ assertPermanentCount(playerB, "Serra Angel", 1);
+ assertPowerToughness(playerB, "Serra Angel", 4 - 2, 4 - 2);
assertPermanentCount(playerA, "Dead Weight", 2);
}
@@ -152,23 +155,26 @@ public class CopyPermanentSpellTest extends CardTestPlayerBase {
public void testBestowRedirect() {
makeTester();
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
- addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears");
- addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion");
+ addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // 2/2
+ addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf"); // 1/1
addCard(Zone.HAND, playerA, "Nimbus Naiad");
- setChoice(playerA, true);
+ setChoice(playerA, true); // change target
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nimbus Naiad using bestow", "Grizzly Bears");
+ // it's good/boost effect
+ // allow AI make a choice for new target of copied spell (it will be bear as a bigger creature for good effect)
+ setStrictChooseMode(false);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Grizzly Bears", 1);
- assertPowerToughness(playerA, "Grizzly Bears", 4, 4);
+ assertPowerToughness(playerA, "Grizzly Bears", 2 + 2 + 2, 2 + 2 + 2);
assertAbility(playerA, "Grizzly Bears", FlyingAbility.getInstance(), true);
- assertPermanentCount(playerA, "Silvercoat Lion", 1);
- assertPowerToughness(playerA, "Silvercoat Lion", 4, 4);
- assertAbility(playerA, "Silvercoat Lion", FlyingAbility.getInstance(), true);
+ assertPermanentCount(playerA, "Arbor Elf", 1);
+ assertPowerToughness(playerA, "Arbor Elf", 1, 1);
+ assertAbility(playerA, "Arbor Elf", FlyingAbility.getInstance(), false);
assertPermanentCount(playerA, "Nimbus Naiad", 2);
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java
index 811f814d21c..2007c6082d4 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/JaceTest.java
@@ -67,7 +67,7 @@ public class JaceTest extends CardTestPlayerBase {
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
- assertGraveyardCount(playerA, "Pillarfield Ox", 1);
+ assertGraveyardCount(playerA, "Pillarfield Ox", 1); // must discard a creature and keep land card
assertExileCount("Jace, Vryn's Prodigy", 0);
assertPermanentCount(playerA, "Jace, Telepath Unbound", 1);
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/BeamsplitterMageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/BeamsplitterMageTest.java
index 80cfef86fb3..32ad4d316e8 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/BeamsplitterMageTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/BeamsplitterMageTest.java
@@ -9,11 +9,25 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
* @author TheElk801
*/
public class BeamsplitterMageTest extends CardTestPlayerBase {
+ /**
+ * 2/2
+ *
+ * Whenever you cast an instant or sorcery spell that targets only Beamsplitter Mage,
+ * if you control one or more creatures that spell could target, choose one of those
+ * creatures. Copy that spell. The copy targets the chosen creature.
+ */
private static final String bsm = "Beamsplitter Mage";
+
+ /**
+ * Target creature gets +1/+1 until end of turn.
+ * Target creature gets +1/+1 until end of turn.
+ * Target creature gets +1/+1 until end of turn.
+ */
+ private static final String seeds = "Seeds of Strength";
+
private static final String lion = "Silvercoat Lion";
private static final String duelist = "Deft Duelist";
private static final String bolt = "Lightning Bolt";
- private static final String seeds = "Seeds of Strength";
@Test
public void testLightningBolt() {
@@ -24,7 +38,9 @@ public class BeamsplitterMageTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, bolt);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, bsm);
+ setChoice(playerA, lion); // target for copied bolt
+ setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
@@ -45,12 +61,18 @@ public class BeamsplitterMageTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, bsm);
addCard(Zone.HAND, playerA, seeds);
- castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, seeds, bsm);
+ // put x3 targets to bsm and copy it to lion
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, seeds);
+ addTarget(playerA, bsm);
+ addTarget(playerA, bsm);
+ addTarget(playerA, bsm);
+ setChoice(playerA, lion);
+ setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
- assertPowerToughness(playerA, bsm, 5, 5);
- assertPowerToughness(playerA, lion, 5, 5);
+ assertPowerToughness(playerA, bsm, 2 + 3, 2 + 3);
+ assertPowerToughness(playerA, lion, 2 + 3, 2 + 3);
}
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java
index 5e3c2131f92..2daabf2511d 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java
@@ -1,23 +1,22 @@
package org.mage.test.cards.single.znr;
-import mage.cards.decks.Deck;
import mage.constants.PhaseStep;
import mage.constants.Zone;
-import org.assertj.core.api.Assertions;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
- * {@link mage.cards.y.YasharnImplacableEarth Yasharn, Implacable Earth}
- * When Yasharn enters the battlefield, search your library for a basic Forest card and a basic Plains card, reveal those cards, put them into your hand, then shuffle.
- * Players can’t pay life or sacrifice nonland permanents to cast spells or activate abilities.
- *
* @author Alex-Vasile
*/
public class YasharnImplacableEarthTest extends CardTestPlayerBase {
+ /**
+ * {@link mage.cards.y.YasharnImplacableEarth Yasharn, Implacable Earth}
+ * When Yasharn enters the battlefield, search your library for a basic Forest card and a basic Plains card, reveal those cards, put them into your hand, then shuffle.
+ * Players can’t pay life or sacrifice nonland permanents to cast spells or activate abilities.
+ */
private static final String yasharn = "Yasharn, Implacable Earth";
/**
@@ -143,6 +142,8 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase {
*/
@Test
public void canSacrificeLandToActivate() {
+ removeAllCardsFromLibrary(playerA);
+
addCard(Zone.BATTLEFIELD, playerA, yasharn);
addCard(Zone.BATTLEFIELD, playerA, "Evolving Wilds");
addCard(Zone.LIBRARY, playerA, "Island");
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java
index f334f88dba2..2217cb4ee82 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java
@@ -77,7 +77,8 @@ public class EnterLeaveBattlefieldExileTargetTest extends CardTestPlayerBase {
// test NPE error while AI targeting battlefield with tokens
// Flying
- // When Angel of Serenity enters the battlefield, you may exile up to three other target creatures from the battlefield and/or creature cards from graveyards.
+ // When Angel of Serenity enters the battlefield, you may exile up to three other target creatures
+ // from the battlefield and/or creature cards from graveyards.
addCard(Zone.HAND, playerA, "Angel of Serenity");
addCard(Zone.BATTLEFIELD, playerA, "Plains", 7);
//
@@ -98,6 +99,7 @@ public class EnterLeaveBattlefieldExileTargetTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angel of Serenity");
setChoice(playerA, true);
//addTarget(playerA, "Silvercoat Lion^Balduvian Bears"); // AI must target
+ //addTarget(playerA, TestPlayer.TARGET_SKIP);
setStrictChooseMode(false); // AI must target
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
diff --git a/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java b/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java
index 4a5f7b783b9..939fab44585 100644
--- a/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java
@@ -3,6 +3,7 @@ package org.mage.test.dialogs;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.InfoEffect;
+import mage.abilities.effects.common.continuous.PlayAdditionalLandsAllEffect;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.utils.testers.TestableDialog;
@@ -33,8 +34,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
@Test
public void test_RunSingle_Manual() {
- addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
- addCard(Zone.HAND, playerA, "Forest", 6);
+ prepareCards();
runCode("run single", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
TestableDialog dialog = findDialog(runner, "target.choose(you, target)", "any 0-3");
@@ -57,8 +57,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
@Test
public void test_RunSingle_AI() {
- addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
- addCard(Zone.HAND, playerA, "Forest", 6);
+ prepareCards();
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
@@ -75,37 +74,13 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
assertAndPrintRunnerResults(false, true);
}
- @Test
- @Ignore // debug only - run single dialog by reg number
- public void test_RunSingle_Debugging() {
- int needRegNumber = 7;
-
- addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
- addCard(Zone.HAND, playerA, "Forest", 6);
-
- aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
- aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
- runCode("run by number", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
- TestableDialog dialog = findDialog(runner, needRegNumber);
- dialog.prepare();
- dialog.showDialog(playerA, fakeAbility, game, playerB);
- });
-
- setStrictChooseMode(true);
- setStopAt(1, PhaseStep.END_TURN);
- execute();
-
- assertAndPrintRunnerResults(false, true);
- }
-
@Test
@Ignore // TODO: enable and fix all failed dialogs
public void test_RunAll_AI() {
// it's impossible to setup 700+ dialogs, so all choices made by AI
// current AI uses only simple choices in dialogs, not simulations
- addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
- addCard(Zone.HAND, playerA, "Forest", 6);
+ prepareCards();
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
@@ -130,6 +105,43 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
assertAndPrintRunnerResults(true, true);
}
+ @Test
+ @Ignore // debug only - run single dialog by reg number
+ public void test_RunSingle_Debugging() {
+ int needRegNumber = 93;
+
+ prepareCards();
+
+ aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
+ aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
+ runCode("run by number", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
+ TestableDialog dialog = findDialog(runner, needRegNumber);
+ dialog.prepare();
+ dialog.showDialog(playerA, fakeAbility, game, playerB);
+ });
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertAndPrintRunnerResults(false, true);
+ }
+
+ private void prepareCards() {
+ // runner calls dialogs for both A and B, so players must have same cards
+ removeAllCardsFromLibrary(playerA);
+ removeAllCardsFromLibrary(playerB);
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
+ addCard(Zone.HAND, playerA, "Forest", 6);
+ addCard(Zone.BATTLEFIELD, playerB, "Mountain", 6);
+ addCard(Zone.HAND, playerB, "Forest", 6);
+
+ runCode("restrict lands", 1, PhaseStep.UPKEEP, playerA, (info, player, game) -> {
+ // restrict any lands play, so AI will keep lands in hand
+ game.addEffect(new PlayAdditionalLandsAllEffect(-1), fakeAbility);
+ });
+ }
+
private TestableDialog findDialog(TestableDialogsRunner runner, String byGroup, String byName) {
List res = runner.getDialogs().stream()
.filter(dialog -> dialog.getGroup().equals(byGroup))
@@ -194,6 +206,9 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
int totalGood = 0;
int totalBad = 0;
int totalUnknown = 0;
+ TestableDialog firstBadDialog = null;
+ String firstBadAssert = "";
+ String firstBadDebugSource = "";
boolean usedHorizontalBorder = true; // mark that last print used horizontal border (fix duplicates)
Map coloredTexts = new HashMap<>(); // must colorize after string format to keep pretty table
for (TestableDialog dialog : runner.getDialogs()) {
@@ -244,8 +259,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
coloredTexts.clear();
coloredTexts.put(resAssert, asRed(resAssert));
coloredTexts.put(resDebugSource, asRed(resDebugSource));
- System.out.print(getColoredRow(totalsRightFormat, coloredTexts, resAssert));
- System.out.print(getColoredRow(totalsRightFormat, coloredTexts, resDebugSource));
+ String badAssert = getColoredRow(totalsRightFormat, coloredTexts, resAssert);
+ String badDebugSource = getColoredRow(totalsRightFormat, coloredTexts, resDebugSource);
+ if (firstBadDialog == null) {
+ firstBadDialog = dialog;
+ firstBadAssert = badAssert;
+ firstBadDebugSource = badDebugSource;
+ }
+ System.out.print(badAssert);
+ System.out.print(badDebugSource);
System.out.println(horizontalBorder);
usedHorizontalBorder = true;
}
@@ -267,6 +289,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
coloredTexts.put(unknownStats, String.format("%s unknown", asYellow(String.valueOf(totalUnknown))));
System.out.print(getColoredRow(totalsLeftFormat, coloredTexts, String.format("Total results: %s, %s, %s",
goodStats, badStats, unknownStats)));
+ // first error for fast access in big list
+ if (totalDialogs > 1 && firstBadDialog != null) {
+ System.out.println(horizontalBorder);
+ System.out.print(getColoredRow(totalsRightFormat, coloredTexts, "First bad dialog: " + firstBadDialog.getRegNumber()));
+ System.out.print(getColoredRow(totalsRightFormat, coloredTexts, firstBadDialog.getName() + " - " + firstBadDialog.getDescription()));
+ System.out.print(firstBadAssert);
+ System.out.print(firstBadDebugSource);
+ }
+
// table end
System.out.println(horizontalBorder);
usedHorizontalBorder = true;
diff --git a/Mage/src/main/java/mage/constants/Outcome.java b/Mage/src/main/java/mage/constants/Outcome.java
index 1f4e4681214..1e976f9ad7b 100644
--- a/Mage/src/main/java/mage/constants/Outcome.java
+++ b/Mage/src/main/java/mage/constants/Outcome.java
@@ -47,23 +47,23 @@ public enum Outcome {
private final boolean good;
// no different between own or opponent targets (example: copy must choose from all permanents)
- private boolean canTargetAll;
+ private boolean anyTargetHasSameValue;
Outcome(boolean good) {
this.good = good;
}
- Outcome(boolean good, boolean canTargetAll) {
+ Outcome(boolean good, boolean anyTargetHasSameValue) {
this.good = good;
- this.canTargetAll = canTargetAll;
+ this.anyTargetHasSameValue = anyTargetHasSameValue;
}
public boolean isGood() {
return good;
}
- public boolean isCanTargetAll() {
- return canTargetAll;
+ public boolean anyTargetHasSameValue() {
+ return anyTargetHasSameValue;
}
public static Outcome inverse(Outcome outcome) {
diff --git a/Mage/src/main/java/mage/filter/common/FilterAnyTarget.java b/Mage/src/main/java/mage/filter/common/FilterAnyTarget.java
index 0958607c370..45e16a5c591 100644
--- a/Mage/src/main/java/mage/filter/common/FilterAnyTarget.java
+++ b/Mage/src/main/java/mage/filter/common/FilterAnyTarget.java
@@ -4,6 +4,8 @@ import mage.constants.CardType;
import mage.filter.predicate.Predicates;
/**
+ * Warning, it's a filter for damage effects only (ignore lands, artifacts and other non-damageable objects)
+ *
* @author TheElk801
*/
public class FilterAnyTarget extends FilterPermanentOrPlayer {
diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java
index e5268c1ea98..2f2c4f78e49 100644
--- a/Mage/src/main/java/mage/target/Target.java
+++ b/Mage/src/main/java/mage/target/Target.java
@@ -28,6 +28,7 @@ public interface Target extends Copyable, Serializable {
*/
boolean isChosen(Game game);
+ @Deprecated // TODO: replace usage in cards by full version from choose methods
boolean isChoiceCompleted(Game game);
boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game);
@@ -84,7 +85,7 @@ public interface Target extends Copyable, Serializable {
/**
* @param id
- * @param source WARNING, it can be null for AI or other calls from events (TODO: introduce normal source in AI ComputerPlayer)
+ * @param source WARNING, it can be null for AI or other calls from events
* @param game
* @return
*/
diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java
index 8c5f1f73ea2..1a15044b417 100644
--- a/Mage/src/main/java/mage/target/TargetImpl.java
+++ b/Mage/src/main/java/mage/target/TargetImpl.java
@@ -261,7 +261,6 @@ public abstract class TargetImpl implements Target {
}
@Override
- @Deprecated // TODO: replace usage in cards by full version from choose methods
public boolean isChoiceCompleted(Game game) {
return isChoiceCompleted(null, null, game);
}
diff --git a/Mage/src/main/java/mage/target/common/TargetAnyTarget.java b/Mage/src/main/java/mage/target/common/TargetAnyTarget.java
index 87de24a32ab..038571d32d3 100644
--- a/Mage/src/main/java/mage/target/common/TargetAnyTarget.java
+++ b/Mage/src/main/java/mage/target/common/TargetAnyTarget.java
@@ -3,6 +3,8 @@ package mage.target.common;
import mage.filter.common.FilterAnyTarget;
/**
+ * Warning, it's a target for damage effects only (ignore lands, artifacts and other non-damageable objects)
+ *
* @author JRHerlehy Created on 4/8/18.
*/
public class TargetAnyTarget extends TargetPermanentOrPlayer {
diff --git a/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java b/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java
index 7257e334805..b86014f5cac 100644
--- a/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java
+++ b/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java
@@ -18,6 +18,7 @@ import java.util.Set;
import java.util.UUID;
/**
+ *
* @author LevelX2
*/
public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
@@ -97,7 +98,7 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
@Override
public boolean canTarget(UUID id, Game game) {
- return this.canTarget(null, id, null, game);
+ return this.canTarget(null, id, null, game); // wtf
}
@Override