From ffe902d25ef0476b4b8f74ba93ce197c86bfe6ed Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 18 Jun 2025 20:03:58 +0300 Subject: [PATCH] AI: purged whole targeting code and replaced with simple and shared logic (part of #13638) (#13766) - refactor: removed outdated/unused code from AI, battlefield score, targeting, etc; - ai: removed all targeting and filters related logic from ComputerPlayer; - ai: improved targeting with shared and simple logic due effect's outcome and possible targets priority; - ai: improved get amount selection (now AI will not choose big and useless values); - ai: improved spell selection on free cast (now AI will try to cast spells with valid targets only); - tests: improved result table with first bad dialog info for fast access (part of #13638); --- .../testers/ChooseTargetTestableDialog.java | 6 +- .../src/mage/player/ai/ComputerPlayer6.java | 1 + .../src/mage/player/ai/ComputerPlayer7.java | 1 + .../src/mage/player/ai/util/CombatUtil.java | 2 +- .../java/mage/player/ai/ComputerPlayer.java | 1603 ++--------------- .../player/ai/PossibleTargetsComparator.java | 156 ++ .../player/ai/PossibleTargetsSelector.java | 184 ++ .../ai/score}/ArtificialScoringSystem.java | 2 +- .../player/ai/score}/GameStateEvaluator2.java | 3 +- .../mage/player/ai/score}/MagicAbility.java | 3 +- .../player/ai/simulators/ActionSimulator.java | 65 - Mage.Sets/src/mage/cards/d/DeadWeight.java | 12 +- .../mage/cards/s/SepulchralPrimordial.java | 2 +- .../basic/PossibleTargetsSelectorAITest.java | 149 ++ .../oneshot/exile/SearchNameExileTests.java | 8 +- .../cards/copy/CopyPermanentSpellTest.java | 32 +- .../test/cards/planeswalker/JaceTest.java | 2 +- .../single/grn/BeamsplitterMageTest.java | 30 +- .../znr/YasharnImplacableEarthTest.java | 13 +- .../EnterLeaveBattlefieldExileTargetTest.java | 4 +- .../test/dialogs/TestableDialogsTest.java | 93 +- .../src/main/java/mage/constants/Outcome.java | 10 +- .../mage/filter/common/FilterAnyTarget.java | 2 + Mage/src/main/java/mage/target/Target.java | 3 +- .../src/main/java/mage/target/TargetImpl.java | 1 - .../mage/target/common/TargetAnyTarget.java | 2 + ...rgetCardInGraveyardBattlefieldOrStack.java | 3 +- 27 files changed, 777 insertions(+), 1615 deletions(-) create mode 100644 Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsComparator.java create mode 100644 Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java rename Mage.Server.Plugins/{Mage.Player.AI.MA/src/mage/player/ai/ma => Mage.Player.AI/src/main/java/mage/player/ai/score}/ArtificialScoringSystem.java (99%) rename Mage.Server.Plugins/{Mage.Player.AI.MA/src/mage/player/ai => Mage.Player.AI/src/main/java/mage/player/ai/score}/GameStateEvaluator2.java (99%) rename Mage.Server.Plugins/{Mage.Player.AI.MA/src/mage/player/ai/ma => Mage.Player.AI/src/main/java/mage/player/ai/score}/MagicAbility.java (95%) delete mode 100644 Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/AI/basic/PossibleTargetsSelectorAITest.java 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 pile1, List 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