diff --git a/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java b/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java index 7cfc0c66c25..51eaabe44bf 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java @@ -326,6 +326,13 @@ public class TestCardRenderDialog extends MageDialog { possibleTargets.add(playerYou.getId()); } + // chosen target + Set chosenTargets = null; + if (false) { // TODO: add GUI's checkbox for checkPlayerAsChosen + chosenTargets = new LinkedHashSet<>(); + chosenTargets.add(playerYou.getId()); + } + // player's panel if (this.player == null) { // create new panel @@ -345,7 +352,7 @@ public class TestCardRenderDialog extends MageDialog { .findFirst() .orElse(null); this.player.init(this.game.getId(), playerYou.getId(), isMe, this.bigCard, 0); - this.player.update(gameView, currentPlayerView, possibleTargets); + this.player.update(gameView, currentPlayerView, possibleTargets, chosenTargets); this.player.sizePlayerPanel(smallMode); // update CARDS diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 56d4e666592..6b687a3c305 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -216,6 +216,22 @@ public final class GamePanel extends javax.swing.JPanel { }); } + public List getPossibleTargets() { + if (options != null && options.containsKey("possibleTargets")) { + return (List) options.get("possibleTargets"); + } else { + return Collections.emptyList(); + } + } + + public Set getChosenTargets() { + if (options != null && options.containsKey("chosenTargets")) { + return new HashSet<>((List) options.get("chosenTargets")); + } else { + return Collections.emptySet(); + } + } + public CardView findCard(UUID id) { return this.allCardsIndex.getOrDefault(id, null); } @@ -642,7 +658,7 @@ public final class GamePanel extends javax.swing.JPanel { // see test render dialog for refresh commands order playPanel.getPlayerPanel().fullRefresh(GUISizeHelper.playerPanelGuiScale); playPanel.init(player, bigCard, gameId, player.getPriorityTimeLeftSecs()); - playPanel.update(lastGameData.game, player, lastGameData.targets); + playPanel.update(lastGameData.game, player, lastGameData.targets, lastGameData.getChosenTargets()); playPanel.getPlayerPanel().sizePlayerPanel(false); } }); @@ -1170,7 +1186,7 @@ public final class GamePanel extends javax.swing.JPanel { } } } - players.get(player.getPlayerId()).update(lastGameData.game, player, lastGameData.targets); + players.get(player.getPlayerId()).update(lastGameData.game, player, lastGameData.targets, lastGameData.getChosenTargets()); if (player.getPlayerId().equals(playerId)) { skipButtons.updateFromPlayer(player); } @@ -1786,12 +1802,7 @@ public final class GamePanel extends javax.swing.JPanel { needZone = (Zone) lastGameData.options.get("targetZone"); } - List needChosen; - if (lastGameData.options != null && lastGameData.options.containsKey("chosenTargets")) { - needChosen = (List) lastGameData.options.get("chosenTargets"); - } else { - needChosen = new ArrayList<>(); - } + Set needChosen = lastGameData.getChosenTargets(); Set needSelectable; if (lastGameData.targets != null) { @@ -2021,7 +2032,7 @@ public final class GamePanel extends javax.swing.JPanel { private void prepareSelectableWindows( Collection windows, Set needSelectable, - List needChosen, + Set needChosen, PlayableObjectsList needPlayable ) { // lookAt or reveals windows clean up on next priority, so users can see dialogs, but xmage can't restore it diff --git a/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java b/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java index a3209317eef..162a821703a 100644 --- a/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java @@ -57,7 +57,7 @@ public class PlayAreaPanel extends javax.swing.JPanel { // data init init(player, bigCard, gameId, priorityTime); - update(null, player, null); + update(null, player, null, null); playerPanel.sizePlayerPanel(isSmallMode()); // init popup menu (must run after data init) @@ -510,8 +510,8 @@ public class PlayAreaPanel extends javax.swing.JPanel { this.isMe = player.getControlled(); } - public final void update(GameView game, PlayerView player, Set possibleTargets) { - this.playerPanel.update(game, player, possibleTargets); + public final void update(GameView game, PlayerView player, Set possibleTargets, Set chosenTargets) { + this.playerPanel.update(game, player, possibleTargets, chosenTargets); this.battlefieldPanel.update(player.getBattlefield()); if (this.allowViewHandCardsMenuItem != null) { this.allowViewHandCardsMenuItem.setSelected(player.getUserData().isAllowRequestHandToAll()); diff --git a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java index 3ba80d606d9..3fb8f428b08 100644 --- a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java +++ b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java @@ -247,7 +247,7 @@ public class PlayerPanelExt extends javax.swing.JPanel { .orElse(0); } - public void update(GameView game, PlayerView player, Set possibleTargets) { + public void update(GameView game, PlayerView player, Set possibleTargets, Set chosenTargets) { this.player = player; int pastLife = player.getLife(); if (playerLives != null) { @@ -427,6 +427,12 @@ public class PlayerPanelExt extends javax.swing.JPanel { this.btnPlayer.setBorder(YELLOW_BORDER); } + // selected targeting (draw as priority) + if (chosenTargets != null && chosenTargets.contains(this.playerId)) { + this.avatar.setBorder(GREEN_BORDER); // TODO: use diff green color for chosen targeting and current priority? + this.btnPlayer.setBorder(GREEN_BORDER); + } + update(player.getManaPool()); } diff --git a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java index 30e88a12e1e..3fb8dcea16f 100644 --- a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java +++ b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java @@ -10,6 +10,7 @@ import mage.client.draft.DraftPanel; import mage.client.game.GamePanel; import mage.client.plugins.impl.Plugins; import mage.client.util.DeckUtil; +import mage.client.util.GUISizeHelper; import mage.client.util.IgnoreList; import mage.client.util.audio.AudioManager; import mage.client.util.object.SaveObjectUtil; @@ -22,9 +23,12 @@ import mage.util.DebugUtil; import mage.view.*; import mage.view.ChatMessage.MessageType; import org.apache.log4j.Logger; +import org.mage.card.arcane.ManaSymbols; import javax.swing.*; +import java.awt.*; import java.awt.event.KeyEvent; +import java.util.List; import java.util.*; /** @@ -203,11 +207,7 @@ public class CallbackClientImpl implements CallbackClient { case SERVER_MESSAGE: { if (callback.getData() != null) { ChatMessage message = (ChatMessage) callback.getData(); - if (message.getColor() == ChatMessage.MessageColor.RED) { - JOptionPane.showMessageDialog(null, message.getMessage(), "Server message", JOptionPane.WARNING_MESSAGE); - } else { - JOptionPane.showMessageDialog(null, message.getMessage(), "Server message", JOptionPane.INFORMATION_MESSAGE); - } + showMessageDialog(null, message.getMessage(), "Server message"); } break; } @@ -401,7 +401,7 @@ public class CallbackClientImpl implements CallbackClient { case SHOW_USERMESSAGE: { List messageData = (List) callback.getData(); if (messageData.size() == 2) { - JOptionPane.showMessageDialog(null, messageData.get(1), messageData.get(0), JOptionPane.WARNING_MESSAGE); + showMessageDialog(null, messageData.get(1), messageData.get(0)); } break; } @@ -420,8 +420,7 @@ public class CallbackClientImpl implements CallbackClient { GameClientMessage message = (GameClientMessage) callback.getData(); GamePanel panel = MageFrame.getGame(callback.getObjectId()); if (panel != null) { - JOptionPane.showMessageDialog(panel, message.getMessage(), "Game message", - JOptionPane.INFORMATION_MESSAGE); + showMessageDialog(panel, message.getMessage(), "Game message"); } break; } @@ -510,6 +509,18 @@ public class CallbackClientImpl implements CallbackClient { }); } + /** + * Show modal message box, so try to use it only for critical errors or global message. As less as possible. + */ + private void showMessageDialog(Component parentComponent, String message, String title) { + // convert to html + // message - supported + // title - not supported + message = ManaSymbols.replaceSymbolsWithHTML(message, ManaSymbols.Type.DIALOG); + message = GUISizeHelper.textToHtmlWithSize(message, GUISizeHelper.dialogFont); + JOptionPane.showMessageDialog(parentComponent, message, title, JOptionPane.INFORMATION_MESSAGE); + } + private ActionData appendJsonEvent(String name, UUID gameId, Object value) { Session session = SessionHandler.getSession(); if (session.isJsonLogActive()) { diff --git a/Mage.Client/src/main/java/mage/client/util/GUISizeHelper.java b/Mage.Client/src/main/java/mage/client/util/GUISizeHelper.java index d7559ecce12..d0b1e9f06df 100644 --- a/Mage.Client/src/main/java/mage/client/util/GUISizeHelper.java +++ b/Mage.Client/src/main/java/mage/client/util/GUISizeHelper.java @@ -6,6 +6,7 @@ import mage.client.util.gui.GuiDisplayUtil; import org.mage.card.arcane.CardRenderer; import javax.swing.*; +import javax.swing.plaf.FontUIResource; import java.awt.*; import java.lang.reflect.Field; import java.util.Locale; @@ -108,7 +109,7 @@ public final class GUISizeHelper { // app - frame/window title // nimbus's LaF limited to static title size, so font can't be too big (related code in SynthInternalFrameTitlePane, BasicInternalFrameTitlePane) - UIManager.put("InternalFrame.titleFont", dialogFont.deriveFont(Font.BOLD, Math.min(17, 0.8f * dialogFont.getSize()))); + UIManager.put("InternalFrame.titleFont", new FontUIResource(dialogFont.deriveFont(Font.BOLD, Math.min(17, 0.8f * dialogFont.getSize())))); // app - tables tableFont = new java.awt.Font("Arial", 0, dialogFontSize); @@ -150,7 +151,11 @@ public final class GUISizeHelper { cardTooltipLargeImageHeight = 30 * tooltipFontSize; cardTooltipLargeTextWidth = Math.max(150, 20 * tooltipFontSize - 50); cardTooltipLargeTextHeight = Math.max(100, 12 * tooltipFontSize - 20); - UIManager.put("ToolTip.font", cardTooltipFont); + UIManager.put("ToolTip.font", new FontUIResource(cardTooltipFont)); + + // app - information boxes (only title, text controls by content) + // TODO: doesn't work + //UIManager.put("OptionPane.titleFont", new FontUIResource(dialogFont.deriveFont(Font.BOLD, Math.min(17, 1.3f * dialogFont.getSize())))); // game - player panel playerPanelGuiScale = (float) (PreferencesDialog.getCachedValue(PreferencesDialog.KEY_GUI_PLAYER_PANEL_SIZE, 14) / 14.0); diff --git a/Mage.Client/src/main/java/mage/client/util/gui/GuiDisplayUtil.java b/Mage.Client/src/main/java/mage/client/util/gui/GuiDisplayUtil.java index 49d6b83feda..7066f6ae128 100644 --- a/Mage.Client/src/main/java/mage/client/util/gui/GuiDisplayUtil.java +++ b/Mage.Client/src/main/java/mage/client/util/gui/GuiDisplayUtil.java @@ -474,7 +474,7 @@ public final class GuiDisplayUtil { public static void refreshThemeSettings() { // apply Nimbus's look and fill // possible settings: - // https://docs.oracle.com/en%2Fjava%2Fjavase%2F17%2Fdocs%2Fapi%2F%2F/java.desktop/javax/swing/plaf/nimbus/doc-files/properties.html + // https://docs.oracle.com/en/java/javase/23/docs/api/java.desktop/javax/swing/plaf/nimbus/doc-files/properties.html // enable nimbus try { diff --git a/Mage.Common/src/main/java/mage/utils/SystemUtil.java b/Mage.Common/src/main/java/mage/utils/SystemUtil.java index 7d314cdf4ec..6bb5d7d53d6 100644 --- a/Mage.Common/src/main/java/mage/utils/SystemUtil.java +++ b/Mage.Common/src/main/java/mage/utils/SystemUtil.java @@ -28,6 +28,7 @@ import mage.target.common.TargetOpponent; import mage.util.CardUtil; import mage.util.MultiAmountMessage; import mage.util.RandomUtil; +import mage.utils.testers.TestableDialogsRunner; import java.io.File; import java.lang.reflect.Constructor; @@ -69,6 +70,7 @@ public final class SystemUtil { private static final String COMMAND_LANDS_ADD_TO_BATTLEFIELD = "@lands add"; private static final String COMMAND_UNDER_CONTROL_TAKE = "@under control take"; private static final String COMMAND_UNDER_CONTROL_GIVE = "@under control give"; + private static final String COMMAND_SHOW_TEST_DIALOGS = "@show dialog"; private static final String COMMAND_MANA_ADD = "@mana add"; // TODO: not implemented private static final String COMMAND_RUN_CUSTOM_CODE = "@run custom code"; // TODO: not implemented private static final String COMMAND_SHOW_OPPONENT_HAND = "@show opponent hand"; @@ -76,6 +78,7 @@ public final class SystemUtil { private static final String COMMAND_SHOW_MY_HAND = "@show my hand"; private static final String COMMAND_SHOW_MY_LIBRARY = "@show my library"; private static final Map supportedCommands = new HashMap<>(); + private static final TestableDialogsRunner testableDialogsRunner = new TestableDialogsRunner(); // for tests static { // special commands names in choose dialog @@ -89,6 +92,7 @@ public final class SystemUtil { supportedCommands.put(COMMAND_SHOW_OPPONENT_LIBRARY, "SHOW OPPONENT LIBRARY"); supportedCommands.put(COMMAND_SHOW_MY_HAND, "SHOW MY HAND"); supportedCommands.put(COMMAND_SHOW_MY_LIBRARY, "SHOW MY LIBRARY"); + supportedCommands.put(COMMAND_SHOW_TEST_DIALOGS, "SHOW TEST DIALOGS"); } private static final Pattern patternGroup = Pattern.compile("\\[(.+)\\]"); // [test new card] @@ -263,8 +267,13 @@ public final class SystemUtil { public static void executeCheatCommands(Game game, String commandsFilePath, Player feedbackPlayer) { // fake test ability for triggers and events - Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); + Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("fake ability")); fakeSourceAbilityTemplate.setControllerId(feedbackPlayer.getId()); + Card fakeSourceCard = feedbackPlayer.getLibrary().getFromTop(game); + if (fakeSourceCard != null) { + // set any existing card as source, so dialogs will show all GUI elements, including source and workable popup info + fakeSourceAbilityTemplate.setSourceId(fakeSourceCard.getId()); + } List errorsList = new ArrayList<>(); try { @@ -304,8 +313,9 @@ public final class SystemUtil { // add default commands initLines.add(0, String.format("[%s]", COMMAND_LANDS_ADD_TO_BATTLEFIELD)); initLines.add(1, String.format("[%s]", COMMAND_CARDS_ADD_TO_HAND)); - initLines.add(2, String.format("[%s]", COMMAND_UNDER_CONTROL_TAKE)); - initLines.add(3, String.format("[%s]", COMMAND_UNDER_CONTROL_GIVE)); + initLines.add(2, String.format("[%s]", COMMAND_SHOW_TEST_DIALOGS)); + initLines.add(3, String.format("[%s]", COMMAND_UNDER_CONTROL_TAKE)); + initLines.add(4, String.format("[%s]", COMMAND_UNDER_CONTROL_GIVE)); // collect all commands CommandGroup currentGroup = null; @@ -538,6 +548,11 @@ public final class SystemUtil { break; } + case COMMAND_SHOW_TEST_DIALOGS: { + testableDialogsRunner.selectAndShowTestableDialog(feedbackPlayer, fakeSourceAbilityTemplate.copy(), game, opponent); + break; + } + default: { String mes = String.format("Unknown system command: %s", runGroup.name); errorsList.add(mes); diff --git a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java new file mode 100644 index 00000000000..0ee78b67e51 --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java @@ -0,0 +1,74 @@ +package mage.utils.testers; + +import mage.constants.SubType; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; +import mage.players.Player; +import mage.target.Target; +import mage.target.common.TargetCreaturePermanent; +import mage.target.common.TargetPermanentOrPlayer; + +/** + * Part of testable game dialogs + * + * @author JayDi85 + */ +abstract class BaseTestableDialog implements TestableDialog { + + private final String group; + private final String name; + private final String description; + + public BaseTestableDialog(String group, String name, String description) { + this.group = group; + this.name = name; + this.description = description; + } + + @Override + final public String getGroup() { + return this.group; + } + + @Override + final public String getName() { + return this.name; + } + + @Override + final public String getDescription() { + return this.description; + } + + @Override + final public void showResult(Player player, Game game, String result) { + // show message with result + game.informPlayer(player, result); + // reset game and gui (in most use cases it must return to player's priority) + game.firePriorityEvent(player.getId()); + } + + static Target createAnyTarget(int min, int max) { + return createAnyTarget(min, max, false); + } + + private static Target createAnyTarget(int min, int max, boolean notTarget) { + return new TargetPermanentOrPlayer(min, max).withNotTarget(notTarget); + } + + static Target createCreatureTarget(int min, int max) { + return createCreatureTarget(min, max, false); + } + + private static Target createCreatureTarget(int min, int max, boolean notTarget) { + return new TargetCreaturePermanent(min, max).withNotTarget(notTarget); + } + + static Target createImpossibleTarget(int min, int max) { + return createImpossibleTarget(min, max, false); + } + + private static Target createImpossibleTarget(int min, int max, boolean notTarget) { + return new TargetCreaturePermanent(min, max, new FilterCreaturePermanent(SubType.TROOPER, "rare type"), notTarget); + } +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseAmountTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseAmountTestableDialog.java new file mode 100644 index 00000000000..b95937a88b9 --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseAmountTestableDialog.java @@ -0,0 +1,111 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetAmount; +import mage.target.Targets; +import mage.target.common.TargetAnyTargetAmount; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Part of testable game dialogs + *

+ * Supported methods: + * - player.chooseTarget(amount) + * + * @author JayDi85 + */ +class ChooseAmountTestableDialog extends BaseTestableDialog { + + boolean isYou; // who choose - you or opponent + int distributeAmount; + int targetsMin; + int targetsMax; + + public ChooseAmountTestableDialog(boolean isYou, String name, int distributeAmount, int targetsMin, int targetsMax) { + super(String.format("player.chooseTarget(%s, amount)", isYou ? "you" : "AI"), + name, + String.format("%d between %d-%d targets", distributeAmount, targetsMin, targetsMax)); + this.isYou = isYou; + this.distributeAmount = distributeAmount; + this.targetsMin = targetsMin; + this.targetsMax = targetsMax; + } + + @Override + public List showDialog(Player player, Ability source, Game game, Player opponent) { + TargetAmount choosingTarget = new TargetAnyTargetAmount(this.distributeAmount, this.targetsMin, this.targetsMax); + Player choosingPlayer = this.isYou ? player : opponent; + + // TODO: add "damage" word in ability text, so chooseTargetAmount an show diff dialog (due inner logic - distribute damage or 1/1) + boolean chooseRes = choosingPlayer.chooseTargetAmount(Outcome.Benefit, choosingTarget, source, game); + List result = new ArrayList<>(); + if (chooseRes) { + Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, result); + } else { + Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, result); + } + return result; + } + + static public void register(TestableDialogsRunner runner) { + // test game started with 2 players and 1 land on battlefield + // so it's better to use target limits like 0, 1, 3, 5, max + + List isYous = Arrays.asList(false, true); + + for (boolean isYou : isYous) { + // up to + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 0)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 1, 0, 0)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 2, 0, 0)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 3, 0, 0)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 5, 0, 0)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 5)); + + // need target + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 5)); + // + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 1)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 3)); + runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 5)); + } + } +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseCardsTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseCardsTestableDialog.java new file mode 100644 index 00000000000..8837d9b92ce --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseCardsTestableDialog.java @@ -0,0 +1,111 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.Targets; +import mage.target.common.TargetCardInHand; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Part of testable game dialogs + *

+ * Supported methods: + * - player.choose(cards) + * - player.chooseTarget(cards) + * + * @author JayDi85 + */ +class ChooseCardsTestableDialog extends BaseTestableDialog { + + TargetCard target; + boolean isTargetChoice; // how to choose - by xxx.choose or xxx.chooseTarget + boolean isYou; // who choose - you or opponent + + public ChooseCardsTestableDialog(boolean isTargetChoice, boolean notTarget, boolean isYou, String name, TargetCard target) { + super(String.format("%s(%s, %s, cards)", + isTargetChoice ? "player.chooseTarget" : "player.choose", + isYou ? "you" : "AI", + notTarget ? "not target" : "target"), name, target.toString()); + this.isTargetChoice = isTargetChoice; + this.target = target.withNotTarget(notTarget); + this.isYou = isYou; + } + + @Override + public List showDialog(Player player, Ability source, Game game, Player opponent) { + TargetCard choosingTarget = this.target.copy(); + Player choosingPlayer = this.isYou ? player : opponent; + + // make sure hand go first, so user can test diff type of targets + List all = new ArrayList<>(); + all.addAll(choosingPlayer.getHand().getCards(game)); + //all.addAll(choosingPlayer.getLibrary().getCards(game)); + Cards choosingCards = new CardsImpl(all.stream().limit(100).collect(Collectors.toList())); + + boolean chooseRes; + if (this.isTargetChoice) { + chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingCards, choosingTarget, source, game); + } else { + chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingCards, choosingTarget, source, game); + } + + List result = new ArrayList<>(); + if (chooseRes) { + Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, result); + } else { + Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, result); + } + return result; + } + + static public void register(TestableDialogsRunner runner) { + // test game started with 2 players and 7 cards in hand and 1 draw + // so it's better to use target limits like 0, 1, 3, 9, max + + FilterCard anyCard = StaticFilters.FILTER_CARD; + FilterCard impossibleCard = new FilterCard(); + impossibleCard.add(SubType.TROOPER.getPredicate()); + + List notTargets = Arrays.asList(false, true); + List isYous = Arrays.asList(false, true); + List isTargetChoices = Arrays.asList(false, true); + for (boolean notTarget : notTargets) { + for (boolean isYou : isYous) { + for (boolean isTargetChoice : isTargetChoices) { + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 0, X=0", new TargetCardInHand(0, 0, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 1", new TargetCardInHand(1, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 3", new TargetCardInHand(3, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 9", new TargetCardInHand(9, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 0-1", new TargetCardInHand(0, 1, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 0-3", new TargetCardInHand(0, 3, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 0-9", new TargetCardInHand(0, 9, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand any", new TargetCardInHand(0, Integer.MAX_VALUE, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 1-3", new TargetCardInHand(1, 3, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 2-3", new TargetCardInHand(2, 3, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 2-9", new TargetCardInHand(2, 9, anyCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "hand 8-9", new TargetCardInHand(8, 9, anyCard))); + // + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "impossible 0, X=0", new TargetCardInHand(0, impossibleCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "impossible 1", new TargetCardInHand(1, impossibleCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "impossible 3", new TargetCardInHand(3, impossibleCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "impossible 0-1", new TargetCardInHand(0, 1, impossibleCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "impossible 0-3", new TargetCardInHand(0, 3, impossibleCard))); + runner.registerDialog(new ChooseCardsTestableDialog(isTargetChoice, notTarget, isYou, "impossible any", new TargetCardInHand(0, Integer.MAX_VALUE, impossibleCard))); + } + } + } + } +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseChoiceTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseChoiceTestableDialog.java new file mode 100644 index 00000000000..7e90c6cb124 --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseChoiceTestableDialog.java @@ -0,0 +1,66 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.choices.*; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Part of testable game dialogs + *

+ * Supported methods: + * - player.choose(choice) + * + * @author JayDi85 + */ +class ChooseChoiceTestableDialog extends BaseTestableDialog { + + boolean isYou; // who choose - you or opponent + Choice choice; + + public ChooseChoiceTestableDialog(boolean isYou, String name, Choice choice) { + super(String.format("player.choose(%s, choice)", isYou ? "you" : "AI"), name, choice.getClass().getSimpleName()); + this.isYou = isYou; + this.choice = choice; + } + + @Override + public List showDialog(Player player, Ability source, Game game, Player opponent) { + Player choosingPlayer = this.isYou ? player : opponent; + Choice dialog = this.choice.copy(); + boolean chooseRes = choosingPlayer.choose(Outcome.Benefit, dialog, game); + + List result = new ArrayList<>(); + result.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE")); + result.add(""); + if (dialog.isKeyChoice()) { + String key = dialog.getChoiceKey(); + result.add(String.format("* selected key: %s (%s)", key, dialog.getKeyChoices().getOrDefault(key, null))); + } else { + result.add(String.format("* selected value: %s", dialog.getChoice())); + } + + return result; + } + + static public void register(TestableDialogsRunner runner) { + // TODO: add require option + // TODO: add ChoiceImpl with diff popup hints + List isYous = Arrays.asList(false, true); + for (boolean isYou : isYous) { + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoiceBasicLandType())); + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoiceCardType())); + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoiceColor())); + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoiceColorOrArtifact())); + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoiceCreatureType(null, null))); // TODO: must be dynamic to pass game/source + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoiceLandType())); + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoiceLeftOrRight())); + runner.registerDialog(new ChooseChoiceTestableDialog(isYou, "", new ChoicePlaneswalkerType())); // TODO: must be dynamic to pass game/source + } + } +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChoosePileTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChoosePileTestableDialog.java new file mode 100644 index 00000000000..68bc9bbb265 --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/ChoosePileTestableDialog.java @@ -0,0 +1,69 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Part of testable game dialogs + *

+ * Supported methods: + * - player.choosePile() + * + * @author JayDi85 + */ +class ChoosePileTestableDialog extends BaseTestableDialog { + + boolean isYou; // who choose - you or opponent + int pileSize1; + int pileSize2; + + public ChoosePileTestableDialog(boolean isYou, int pileSize1, int pileSize2) { + super(String.format("player.choosePile(%s)", isYou ? "you" : "AI"), "pile sizes: " + pileSize1 + " and " + pileSize2, ""); + this.isYou = isYou; + this.pileSize1 = pileSize1; + this.pileSize2 = pileSize2; + } + + @Override + public List showDialog(Player player, Ability source, Game game, Player opponent) { + // TODO: it's ok to show broken title - must add html support in windows's title someday + String mainMessage = "main message with html" + CardUtil.getSourceLogName(game, source); + + // random piles (make sure it contain good amount of cards) + List all = new ArrayList<>(game.getCards()); + Collections.shuffle(all); + List pile1 = all.stream().limit(this.pileSize1).collect(Collectors.toList()); + Collections.shuffle(all); + List pile2 = all.stream().limit(this.pileSize2).collect(Collectors.toList()); + + Player choosingPlayer = this.isYou ? player : opponent; + boolean chooseRes = choosingPlayer.choosePile(Outcome.Benefit, mainMessage, pile1, pile2, game); + List result = new ArrayList<>(); + result.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE")); + result.add(" * selected pile: " + (chooseRes ? "pile 1" : "pile 2")); + return result; + } + + static public void register(TestableDialogsRunner runner) { + List isYous = Arrays.asList(false, true); + for (boolean isYou : isYous) { + runner.registerDialog(new ChoosePileTestableDialog(isYou, 3, 5)); + runner.registerDialog(new ChoosePileTestableDialog(isYou, 10, 10)); + runner.registerDialog(new ChoosePileTestableDialog(isYou, 30, 30)); + runner.registerDialog(new ChoosePileTestableDialog(isYou, 90, 90)); + runner.registerDialog(new ChoosePileTestableDialog(isYou, 0, 10)); + runner.registerDialog(new ChoosePileTestableDialog(isYou, 10, 0)); + runner.registerDialog(new ChoosePileTestableDialog(isYou, 0, 0)); + } + } +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java new file mode 100644 index 00000000000..152428fb8d8 --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java @@ -0,0 +1,123 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.target.Target; +import mage.target.Targets; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Part of testable game dialogs + *

+ * Supported methods: + * - target.choose() + * - target.chooseTarget() + * - player.choose(target) + * - player.chooseTarget(target) + * + * @author JayDi85 + */ +class ChooseTargetTestableDialog extends BaseTestableDialog { + + Target target; + boolean isPlayerChoice; // how to choose - by player.choose or by target.choose + boolean isTargetChoice; // how to choose - by xxx.choose or xxx.chooseTarget + boolean isYou; // who choose - you or opponent + + public ChooseTargetTestableDialog(boolean isPlayerChoice, boolean isTargetChoice, boolean notTarget, boolean isYou, String name, Target target) { + super(String.format("%s%s(%s, %s)", + isPlayerChoice ? "player.choose" : "target.choose", + isTargetChoice ? "target" : "", // chooseTarget or choose + isYou ? "you" : "AI", + notTarget ? "not target" : "target"), name, target.toString()); + this.isPlayerChoice = isPlayerChoice; + this.isTargetChoice = isTargetChoice; + this.target = target.withNotTarget(notTarget); + this.isYou = isYou; + } + + @Override + public List showDialog(Player player, Ability source, Game game, Player opponent) { + Target choosingTarget = this.target.copy(); + Player choosingPlayer = this.isYou ? player : opponent; + + boolean chooseRes; + if (this.isPlayerChoice) { + // player.chooseXXX + if (this.isTargetChoice) { + chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingTarget, source, game); + } else { + chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingTarget, source, game); + } + } else { + // target.chooseXXX + if (this.isTargetChoice) { + chooseRes = choosingTarget.chooseTarget(Outcome.Benefit, choosingPlayer.getId(), source, game); + } else { + chooseRes = choosingTarget.choose(Outcome.Benefit, choosingPlayer.getId(), source, game); + } + } + + List result = new ArrayList<>(); + if (chooseRes) { + Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, result); + } else { + Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, result); + } + return result; + } + + static public void register(TestableDialogsRunner runner) { + // test game started with 2 players and 1 land on battlefield + // so it's better to use target limits like 0, 1, 3, 5, max + + List notTargets = Arrays.asList(false, true); + List isYous = Arrays.asList(false, true); + List isPlayerChoices = Arrays.asList(false, true); + List isTargetChoices = Arrays.asList(false, true); + for (boolean notTarget : notTargets) { + 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))); // simulate X=0 + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1", createAnyTarget(1, 1))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3", createAnyTarget(3, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 5", createAnyTarget(5, 5))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any max", createAnyTarget(0, Integer.MAX_VALUE))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-1", createAnyTarget(0, 1))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-3", createAnyTarget(0, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-5", createAnyTarget(0, 5))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-3", createAnyTarget(1, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-3", createAnyTarget(2, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-5", createAnyTarget(1, 5))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-5", createAnyTarget(2, 5))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3-5", createAnyTarget(3, 5))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 4-5", createAnyTarget(4, 5))); // impossible on 3 targets + // + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0, e.g. X=0", createImpossibleTarget(0, 0))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1", createImpossibleTarget(1, 1))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 3", createImpossibleTarget(3, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-1", createImpossibleTarget(0, 1))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-3", createImpossibleTarget(0, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1-3", createImpossibleTarget(1, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 2-3", createImpossibleTarget(2, 3))); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible max", createImpossibleTarget(0, Integer.MAX_VALUE))); + // + /* + runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 0, e.g. X=0", createCreatureTarget(0, 0))); // simulate X=0 + runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 1", createCreatureTarget(1, 1))); + runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 3", createCreatureTarget(3, 3))); + runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 5", createCreatureTarget(5, 5))); + runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures max", createCreatureTarget(0, Integer.MAX_VALUE))); + */ + } + } + } + } + } +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseUseTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseUseTestableDialog.java new file mode 100644 index 00000000000..3e8137af32b --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseUseTestableDialog.java @@ -0,0 +1,77 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Part of testable game dialogs + *

+ * Supported methods: + * - player.chooseUse() + * + * @author JayDi85 + */ +class ChooseUseTestableDialog extends BaseTestableDialog { + + boolean isYou; // who choose - you or opponent + String trueText; + String falseText; + String messageMain; + String messageAdditional; + + public ChooseUseTestableDialog(boolean isYou, String name, String trueText, String falseText, String messageMain, String messageAdditional) { + super(String.format("player.chooseUse(%s)", isYou ? "you" : "AI"), name + buildName(trueText, falseText, messageMain, messageAdditional), ""); + this.isYou = isYou; + this.trueText = trueText; + this.falseText = falseText; + this.messageMain = messageMain; + this.messageAdditional = messageAdditional; + } + + private static String buildName(String trueText, String falseText, String messageMain, String messageAdditional) { + String buttonsInfo = (trueText == null ? "default" : "custom") + "/" + (falseText == null ? "default" : "custom"); + String messagesInfo = (messageMain == null ? "-" : "main") + "/" + (messageAdditional == null ? "-" : "additional"); + return String.format("buttons: %s, messages: %s", buttonsInfo, messagesInfo); + } + + @Override + public List showDialog(Player player, Ability source, Game game, Player opponent) { + Player choosingPlayer = this.isYou ? player : opponent; + boolean chooseRes = choosingPlayer.chooseUse( + Outcome.Benefit, + messageMain, + messageAdditional == null ? null : messageAdditional + CardUtil.getSourceLogName(game, source), + trueText, + falseText, + source, + game + ); + List result = new ArrayList<>(); + result.add(chooseRes ? "TRUE" : "FALSE"); + return result; + } + + static public void register(TestableDialogsRunner runner) { + List isYous = Arrays.asList(false, true); + String trueButton = "true button"; + String falseButton = "false button"; + String mainMessage = "main message with html"; + String additionalMessage = "additional main message with html"; + for (boolean isYou : isYous) { + runner.registerDialog(new ChooseUseTestableDialog(isYou, "", null, null, mainMessage, additionalMessage)); + runner.registerDialog(new ChooseUseTestableDialog(isYou, "", trueButton, falseButton, mainMessage, additionalMessage)); + runner.registerDialog(new ChooseUseTestableDialog(isYou, "", null, falseButton, mainMessage, additionalMessage)); + runner.registerDialog(new ChooseUseTestableDialog(isYou, "", trueButton, null, mainMessage, additionalMessage)); + runner.registerDialog(new ChooseUseTestableDialog(isYou, "error ", trueButton, falseButton, null, additionalMessage)); + runner.registerDialog(new ChooseUseTestableDialog(isYou, "", trueButton, falseButton, mainMessage, null)); + runner.registerDialog(new ChooseUseTestableDialog(isYou, "error ", trueButton, falseButton, null, null)); + } + } +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/TestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/TestableDialog.java new file mode 100644 index 00000000000..1479ee9f306 --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/TestableDialog.java @@ -0,0 +1,31 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.game.Game; +import mage.players.Player; + +import java.util.List; + +/** + * Part of testable game dialogs + *

+ * How to use: + * - extends BaseTestableDialog + * - implement showDialog + * - create register with all possible sample dialogs + * - call register in main runner's constructor + * + * @author JayDi85 + */ +interface TestableDialog { + + String getGroup(); + + String getName(); + + String getDescription(); + + List showDialog(Player player, Ability source, Game game, Player opponent); + + void showResult(Player player, Game game, String result); +} diff --git a/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java new file mode 100644 index 00000000000..3afbcd62f73 --- /dev/null +++ b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java @@ -0,0 +1,201 @@ +package mage.utils.testers; + +import mage.abilities.Ability; +import mage.choices.Choice; +import mage.choices.ChoiceHintType; +import mage.choices.ChoiceImpl; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Part of testable game dialogs + *

+ * Helper class to create additional options in cheat menu - allow to test any game dialogs with any settings + * Allow to call game dialogs from you or from your opponent, e.g. from AI player + *

+ * All existing human's choose dialogs (search by waitForResponse): + *

+ * Support of single dialogs (can be called inside effects): + * [x] choose(target) + * [x] choose(cards) + * [x] choose(choice) + * [x] chooseTarget(target) + * [x] chooseTarget(cards) + * [x] chooseTargetAmount + * [x] chooseUse + * [x] choosePile + * [ ] announceXMana // TODO: implement + * [ ] announceXCost // TODO: implement + * [ ] getAmount // TODO: implement + * [ ] getMultiAmountWithIndividualConstraints // TODO: implement + *

+ * Support of priority dialogs (can be called by game engine, some can be implemented in theory): + * --- priority + * --- playManaHandling + * --- activateSpecialAction + * --- activateAbility + * --- chooseAbilityForCast + * --- chooseLandOrSpellAbility + * --- chooseMode + * --- chooseMulligan + * --- chooseReplacementEffect + * --- chooseTriggeredAbility + * --- selectAttackers + * --- selectBlockers + * --- selectCombatGroup (part of selectBlockers) + *

+ * Support of outdated dialogs (not used anymore) + * --- announceRepetitions (part of removed macro feature) + * + * @author JayDi85 + */ +public class TestableDialogsRunner { + + private final List dialogs = new ArrayList<>(); + + static final int LAST_SELECTED_GROUP_ID = 997; + static final int LAST_SELECTED_DIALOG_ID = 998; + + // for better UX - save last selected options, so can return to it later + // it's ok to have it global, cause test mode for local and single user environment + static String lastSelectedGroup = null; + static TestableDialog lastSelectedDialog = null; + + + public TestableDialogsRunner() { + ChooseTargetTestableDialog.register(this); + ChooseCardsTestableDialog.register(this); + ChooseUseTestableDialog.register(this); + ChooseChoiceTestableDialog.register(this); + ChoosePileTestableDialog.register(this); + ChooseAmountTestableDialog.register(this); + } + + void registerDialog(TestableDialog dialog) { + this.dialogs.add(dialog); + } + + public void selectAndShowTestableDialog(Player player, Ability source, Game game, Player opponent) { + // select group or fast links + List groups = this.dialogs.stream() + .map(TestableDialog::getGroup) + .distinct() + .sorted() + .collect(Collectors.toList()); + Choice choice = prepareSelectGroupChoice(groups); + player.choose(Outcome.Benefit, choice, game); + String needGroup = null; + TestableDialog needDialog = null; + if (choice.getChoiceKey() != null) { + int needIndex = Integer.parseInt(choice.getChoiceKey()); + if (needIndex == LAST_SELECTED_GROUP_ID && lastSelectedGroup != null) { + // fast link to group + needGroup = lastSelectedGroup; + } else if (needIndex == LAST_SELECTED_DIALOG_ID && lastSelectedDialog != null) { + // fast link to dialog + needGroup = lastSelectedDialog.getGroup(); + needDialog = lastSelectedDialog; + } else if (needIndex < groups.size()) { + // group + needGroup = groups.get(needIndex); + } + } + if (needGroup == null) { + return; + } + + // select dialog + if (needDialog == null) { + choice = prepareSelectDialogChoice(needGroup); + player.choose(Outcome.Benefit, choice, game); + if (choice.getChoiceKey() != null) { + int needIndex = Integer.parseInt(choice.getChoiceKey()); + if (needIndex < this.dialogs.size()) { + needDialog = this.dialogs.get(needIndex); + } + } + } + if (needDialog == null) { + return; + } + + // all fine, can show it and finish + lastSelectedGroup = needGroup; + lastSelectedDialog = needDialog; + List resInfo = needDialog.showDialog(player, source, game, opponent); + needDialog.showResult(player, game, String.join("
", resInfo)); + } + + private Choice prepareSelectGroupChoice(List groups) { + // try to choose group or fast links + + Choice choice = new ChoiceImpl(false); + choice.setMessage("Choose dialogs group to run"); + + // main groups + int recNumber = 0; + for (int i = 0; i < groups.size(); i++) { + recNumber++; + String group = groups.get(i); + choice.withItem( + String.valueOf(i), + String.format("%02d. %s", recNumber, group), + recNumber, + ChoiceHintType.TEXT, + String.join("
", group) + ); + } + + // fast link to last group + String lastGroupInfo = String.format(" -> last group: %s", lastSelectedGroup == null ? "not used" : lastSelectedGroup); + choice.withItem( + String.valueOf(LAST_SELECTED_GROUP_ID), + lastGroupInfo, + -2, + ChoiceHintType.TEXT, + lastGroupInfo + ); + + // fast link to last dialog + String lastDialogName = (lastSelectedDialog == null ? "not used" : String.format("%s - %s", + lastSelectedDialog.getName(), lastSelectedDialog.getDescription())); + String lastDialogInfo = String.format(" -> last dialog: %s", lastDialogName); + choice.withItem( + String.valueOf(LAST_SELECTED_DIALOG_ID), + lastDialogInfo, + -1, + ChoiceHintType.TEXT, + lastDialogInfo + ); + + return choice; + } + + private Choice prepareSelectDialogChoice(String needGroup) { + Choice choice = new ChoiceImpl(false); + choice.setMessage("Choose game dialog to run from " + needGroup); + int recNumber = 0; + for (int i = 0; i < this.dialogs.size(); i++) { + TestableDialog dialog = this.dialogs.get(i); + if (!dialog.getGroup().equals(needGroup)) { + continue; + } + recNumber++; + String info = String.format("%s - %s - %s", dialog.getGroup(), dialog.getName(), dialog.getDescription()); + choice.withItem( + String.valueOf(i), + String.format("%02d. %s", recNumber, info), + recNumber, + ChoiceHintType.TEXT, + String.join("
", info) + ); + } + return choice; + } +} + 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 6a2a2e1cd35..14ba9d0ea2b 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 @@ -399,7 +399,7 @@ public class ComputerPlayer6 extends ComputerPlayer { if (effect != null && stackObject.getControllerId().equals(playerId)) { Target target = effect.getTarget(); - if (!target.doneChoosing(game)) { + if (!target.isChoiceCompleted(game)) { for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) { Game sim = game.createSimulationForAI(); StackAbility newAbility = (StackAbility) stackObject.copy(); @@ -848,10 +848,10 @@ public class ComputerPlayer6 extends ComputerPlayer { if (targets.isEmpty()) { return super.chooseTarget(outcome, cards, target, source, game); } - if (!target.doneChoosing(game)) { + if (!target.isChoiceCompleted(game)) { for (UUID targetId : targets) { target.addTarget(targetId, source, game); - if (target.doneChoosing(game)) { + if (target.isChoiceCompleted(game)) { targets.clear(); return true; } @@ -866,10 +866,10 @@ public class ComputerPlayer6 extends ComputerPlayer { if (targets.isEmpty()) { return super.choose(outcome, cards, target, source, game); } - if (!target.doneChoosing(game)) { + if (!target.isChoiceCompleted(game)) { for (UUID targetId : targets) { target.add(targetId, game); - if (target.doneChoosing(game)) { + if (target.isChoiceCompleted(game)) { targets.clear(); return true; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 65ff643a73d..4452e3e682e 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -160,7 +160,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer { } newAbility.adjustTargets(game); // add the different possible target option for the specific X value - if (!newAbility.getTargets().getUnchosen(game).isEmpty()) { + if (newAbility.getTargets().getNextUnchosen(game) != null) { addTargetOptions(options, newAbility, targetNum, game); } } 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 a5a99306423..b5503877d85 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 @@ -72,7 +72,7 @@ public class ComputerPlayer extends PlayerImpl { protected 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 = false; // DebugUtil.AI_ENABLE_DEBUG_MODE; + protected 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 @@ -84,8 +84,7 @@ public class ComputerPlayer extends PlayerImpl { // * use your's 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 = 5;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5; - + final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 1;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5; private final transient Map unplayable = new TreeMap<>(); @@ -146,10 +145,12 @@ 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()); } + + boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish + // controller hints: // - target.getTargetController(), this.getId() -- player that must makes choices (must be same with this.getId) // - target.getAbilityController(), abilityControllerId -- affected player/controller for all actions/filters @@ -179,8 +180,10 @@ public class ComputerPlayer extends PlayerImpl { // discard not playable first if (!unplayable.isEmpty()) { for (int i = unplayable.size() - 1; i >= 0; i--) { - if (target.canTarget(abilityControllerId, unplayable.values().toArray(new Card[0])[i].getId(), source, game)) { - target.add(unplayable.values().toArray(new Card[0])[i].getId(), game); + 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; } @@ -189,15 +192,17 @@ public class ComputerPlayer extends PlayerImpl { } if (!hand.isEmpty()) { for (int i = 0; i < hand.size(); i++) { - if (target.canTarget(abilityControllerId, hand.toArray(new UUID[0])[i], source, game)) { - target.add(hand.toArray(new UUID[0])[i], game); + 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 false; + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetControlledPermanent @@ -209,18 +214,18 @@ public class ComputerPlayer extends PlayerImpl { Collections.reverse(targets); } for (Permanent permanent : targets) { - if (origTarget.canTarget(abilityControllerId, permanent.getId(), source, game, false) && !target.getTargets().contains(permanent.getId())) { + 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 false; + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetPermanent) { - FilterPermanent filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) { filter = (FilterPermanent) target.getOriginalTarget().getFilter(); @@ -249,25 +254,25 @@ public class ComputerPlayer extends PlayerImpl { } for (Permanent permanent : targets) { - if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) { - // stop to add targets if not needed and outcome is no advantage for AI player - // TODO: need research and improve - is it good to check target.isChosen(game) instead return true? - if (target.getMinNumberOfTargets() == target.getTargets().size()) { + 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 true; + return isAddedSomething; } if (!outcome.isGood() && !hasOpponent(permanent.getControllerId(), game)) { - return true; + return isAddedSomething; } } // add the target target.add(permanent.getId(), game); - if (target.doneChoosing(game)) { + isAddedSomething = true; + if (target.isChosen(game)) { return true; } } } - return target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCardInHand @@ -283,11 +288,19 @@ public class ComputerPlayer extends PlayerImpl { && !cards.isEmpty()) { Card card = selectCard(abilityControllerId, cards, outcome, target, game); if (card != null) { - target.add(card.getId(), game); 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetAnyTarget) { @@ -299,25 +312,31 @@ public class ComputerPlayer extends PlayerImpl { targets = threats(randomOpponentId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); } for (Permanent permanent : targets) { - List alreadyTargetted = target.getTargets(); - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { - if (alreadyTargetted != null && !alreadyTargetted.contains(permanent.getId())) { - target.add(permanent.getId(), game); + 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)) { + 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; } - } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { - target.add(randomOpponentId, game); - return true; } if (!required) { - return false; + return isAddedSomething; } } @@ -332,33 +351,43 @@ public class ComputerPlayer extends PlayerImpl { targets = opponentTargets; } for (Permanent permanent : targets) { - List alreadyTargeted = target.getTargets(); - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { - if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { - target.add(permanent.getId(), game); - return true; - } + 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)) { + 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; } - } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { - target.add(randomOpponentId, game); - return true; } if (!target.isRequired(sourceId, game) || target.getMinNumberOfTargets() == 0) { - return false; + return isAddedSomething; // TODO: need research why it here (between diff type of targets) } - if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) { target.add(randomOpponentId, game); - return true; + isAddedSomething = true; + if (target.isChosen(game)) { + return true; + } } - if (target.canTarget(abilityControllerId, getId(), source, game)) { + if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) { target.add(getId(), game); - return true; + isAddedSomething = true; + if (target.isChosen(game)) { + return true; + } } if (outcome.isGood()) { // no other valid targets so use a permanent targets = opponentTargets; @@ -366,15 +395,15 @@ public class ComputerPlayer extends PlayerImpl { targets = ownedTargets; } for (Permanent permanent : targets) { - List alreadyTargeted = target.getTargets(); - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { - if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { - target.add(permanent.getId(), game); + 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 false; + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) { @@ -393,14 +422,19 @@ public class ComputerPlayer extends PlayerImpl { && !cards.isEmpty()) { Card card = selectCard(abilityControllerId, cards, outcome, target, game); if (card != null) { - target.add(card.getId(), game); 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCardInGraveyard @@ -420,36 +454,40 @@ public class ComputerPlayer extends PlayerImpl { && !cards.isEmpty()) { Card card = selectCard(abilityControllerId, cards, outcome, target, game); if (card != null) { - target.add(card.getId(), game); 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCardInYourGraveyard || target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) { - List alreadyTargeted = target.getTargets(); 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 && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) { + if (card != null && !target.contains(card.getId())) { target.add(card.getId(), game); + isAddedSomething = true; if (target.isChosen(game)) { return true; } } } - return false; + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCardInExile) { - List alreadyTargeted = target.getTargets(); - FilterCard filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterCard) { filter = (FilterCard) target.getOriginalTarget().getFilter(); @@ -462,14 +500,15 @@ public class ComputerPlayer extends PlayerImpl { List cards = game.getExile().getCards(filter, game); while (!cards.isEmpty()) { Card card = selectCard(abilityControllerId, cards, outcome, target, game); - if (card != null && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) { + if (card != null && !target.contains(card.getId())) { target.add(card.getId(), game); + isAddedSomething = true; if (target.isChosen(game)) { return true; } } } - return false; + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetSource) { @@ -478,52 +517,69 @@ public class ComputerPlayer extends PlayerImpl { for (UUID targetId : targets) { MageObject targetObject = game.getObject(targetId); if (targetObject != null) { - List alreadyTargeted = target.getTargets(); if (target.canTarget(abilityControllerId, targetObject.getId(), source, game)) { - if (alreadyTargeted != null && !alreadyTargeted.contains(targetObject.getId())) { + if (!target.contains(targetObject.getId())) { target.add(targetObject.getId(), game); - return true; + isAddedSomething = true; + if (target.isChosen(game)) { + return true; + } } } } } if (!required) { - return false; + return isAddedSomething; } throw new IllegalStateException("TargetSource wasn't handled in computer's choose method: " + target.getClass().getCanonicalName()); } if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) { - Cards cards = new CardsImpl(possibleTargets); - List possibleCards = new ArrayList<>(cards.getCards(game)); - while (!target.isChosen(game) && !possibleCards.isEmpty()) { - Card card = selectCard(abilityControllerId, possibleCards, outcome, target, game); + List cards = new ArrayList<>(new CardsImpl(possibleTargets).getCards(game)); + while (!cards.isEmpty()) { + Card card = selectCard(abilityControllerId, cards, outcome, target, game); if (card != null) { - target.add(card.getId(), game); - possibleCards.remove(card); // selectCard don't remove cards (only on second+ tries) + 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCard && (target.getZone() == Zone.COMMAND)) { // Hellkite Courser - List cardsInCommandZone = new ArrayList<>(); + 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)) { - cardsInCommandZone.add(card); + cards.add(card); } } } - while (!target.isChosen(game) && !cardsInCommandZone.isEmpty()) { - Card pick = selectCard(abilityControllerId, cardsInCommandZone, outcome, target, game); - if (pick != null) { - target.add(pick.getId(), game); - cardsInCommandZone.remove(pick); + 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 target.isChosen(game); + return isAddedSomething; } throw new IllegalStateException("Target wasn't handled in computer's choose method: " + target.getClass().getCanonicalName()); @@ -535,6 +591,8 @@ public class ComputerPlayer extends PlayerImpl { 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 @@ -565,28 +623,36 @@ public class ComputerPlayer extends PlayerImpl { // Angel of Serenity trigger if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) { - Cards cards = new CardsImpl(possibleTargets); - List possibleCards = new ArrayList<>(cards.getCards(game)); - for (Card card : possibleCards) { + 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)) { + 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)) { + 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; + } } } } @@ -594,29 +660,38 @@ public class ComputerPlayer extends PlayerImpl { if (game.getState().getZone(card.getId()) == Zone.GRAVEYARD) { if (outcome.isGood() && card.isOwnedBy(abilityControllerId)) { - if (target.canTarget(abilityControllerId, card.getId(), source, game)) { + 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)) { + 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetDiscard || target.getOriginalTarget() instanceof TargetCardInHand) { + isAddedSomething = false; if (outcome.isGood()) { // good Cards cards = new CardsImpl(possibleTargets); @@ -626,10 +701,11 @@ public class ComputerPlayer extends PlayerImpl { && 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)) { + if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) { target.addTarget(card.getId(), source, game); + isAddedSomething = true; cardsInHand.remove(card); - if (target.isChosen(game)) { + if (target.isChoiceCompleted(game)) { return true; } } @@ -640,9 +716,11 @@ public class ComputerPlayer extends PlayerImpl { findPlayables(game); for (Card card : unplayable.values()) { if (possibleTargets.contains(card.getId()) - && target.canTarget(abilityControllerId, card.getId(), source, game)) { + && target.canTarget(abilityControllerId, card.getId(), source, game) + && !target.contains(card.getId())) { target.addTarget(card.getId(), source, game); - if (target.isChosen(game)) { + isAddedSomething = true; + if (target.isChoiceCompleted(game)) { return true; } } @@ -650,16 +728,18 @@ public class ComputerPlayer extends PlayerImpl { if (!hand.isEmpty()) { for (Card card : hand.getCards(game)) { if (possibleTargets.contains(card.getId()) - && target.canTarget(abilityControllerId, card.getId(), source, game)) { + && target.canTarget(abilityControllerId, card.getId(), source, game) + && !target.contains(card.getId())) { target.addTarget(card.getId(), source, game); - if (target.isChosen(game)) { + isAddedSomething = true; + if (target.isChoiceCompleted(game)) { return true; } } } } } - return false; + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetControlledPermanent @@ -670,15 +750,17 @@ public class ComputerPlayer extends PlayerImpl { if (!outcome.isGood()) { Collections.reverse(targets); } + isAddedSomething = false; for (Permanent permanent : targets) { - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { + 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; + return true; // TODO: need research - is it good optimization for good/bad effects? } } } - return target.isChosen(game); + return isAddedSomething; } @@ -688,7 +770,6 @@ public class ComputerPlayer extends PlayerImpl { // 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(); @@ -702,25 +783,30 @@ public class ComputerPlayer extends PlayerImpl { 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)) { + 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.getTargets().size() >= target.getMinNumberOfTargets()) { - break; + 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; } - target.addTarget(permanent.getId(), source, game); } } - return target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetAnyTarget) { @@ -734,10 +820,10 @@ public class ComputerPlayer extends PlayerImpl { if (targets.isEmpty()) { if (outcome.isGood()) { - if (target.canTarget(abilityControllerId, getId(), source, game)) { + if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) { return tryAddTarget(target, getId(), source, game); } - } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) { return tryAddTarget(target, randomOpponentId, source, game); } } @@ -747,7 +833,7 @@ public class ComputerPlayer extends PlayerImpl { } for (Permanent permanent : targets) { List alreadyTargeted = target.getTargets(); - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { + 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); } @@ -755,10 +841,10 @@ public class ComputerPlayer extends PlayerImpl { } if (outcome.isGood()) { - if (target.canTarget(abilityControllerId, getId(), source, game)) { + if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) { return tryAddTarget(target, getId(), source, game); } - } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) { return tryAddTarget(target, randomOpponentId, source, game); } @@ -777,10 +863,10 @@ public class ComputerPlayer extends PlayerImpl { if (targets.isEmpty()) { if (outcome.isGood()) { - if (target.canTarget(abilityControllerId, getId(), source, game)) { + if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) { return tryAddTarget(target, getId(), source, game); } - } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) { return tryAddTarget(target, randomOpponentId, source, game); } } @@ -790,7 +876,7 @@ public class ComputerPlayer extends PlayerImpl { } for (Permanent permanent : targets) { List alreadyTargeted = target.getTargets(); - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { + if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) { if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { return tryAddTarget(target, permanent.getId(), source, game); } @@ -816,10 +902,10 @@ public class ComputerPlayer extends PlayerImpl { // possible good/bad players if (targets.isEmpty()) { if (outcome.isGood()) { - if (target.canTarget(abilityControllerId, getId(), source, game)) { + if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) { return tryAddTarget(target, getId(), source, game); } - } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { + } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) { return tryAddTarget(target, randomOpponentId, source, game); } } @@ -831,30 +917,27 @@ public class ComputerPlayer extends PlayerImpl { // try target permanent for (Permanent permanent : targets) { - List alreadyTargeted = target.getTargets(); - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { - if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { - return tryAddTarget(target, permanent.getId(), source, game); - } + 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)) { + if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) { return tryAddTarget(target, getId(), source, game); } - } else if (target.canTarget(abilityControllerId, randomOpponentId, 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)) { + if (target.canTarget(abilityControllerId, opponentId, source, game) && !target.contains(opponentId)) { return tryAddTarget(target, opponentId, source, game); } } - if (target.canTarget(abilityControllerId, getId(), source, game)) { + if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) { return tryAddTarget(target, getId(), source, game); } @@ -867,7 +950,7 @@ public class ComputerPlayer extends PlayerImpl { cards.addAll(player.getGraveyard().getCards(game)); } Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game); - if (card != null) { + if (card != null && !target.contains(card.getId())) { return tryAddTarget(target, card.getId(), source, game); } //if (!target.isRequired()) @@ -877,7 +960,7 @@ public class ComputerPlayer extends PlayerImpl { 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) { + if (card != null && !target.contains(card.getId())) { return tryAddTarget(target, card.getId(), source, game); } return false; @@ -885,14 +968,23 @@ public class ComputerPlayer extends PlayerImpl { 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) { - target.addTarget(card.getId(), source, game); 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetSpell @@ -901,7 +993,8 @@ public class ComputerPlayer extends PlayerImpl { for (StackObject o : game.getStack()) { if (o instanceof Spell && !source.getId().equals(o.getStackAbility().getId()) - && target.canTarget(abilityControllerId, o.getStackAbility().getId(), source, game)) { + && target.canTarget(abilityControllerId, o.getStackAbility().getId(), source, game) + && !target.contains(o.getId())) { return tryAddTarget(target, o.getId(), source, game); } } @@ -925,17 +1018,17 @@ public class ComputerPlayer extends PlayerImpl { outcomeTargets = false; } for (Permanent permanent : targets) { - if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { + 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; + 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)) { + if (target.getFilter().match(stackObject, game) && !target.contains(stackObject.getId())) { return tryAddTarget(target, stackObject.getId(), source, game); } } @@ -952,18 +1045,45 @@ public class ComputerPlayer extends PlayerImpl { cards.addAll(player.getGraveyard().getCards(game)); } } - Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game); - if (card != null) { - return tryAddTarget(target, card.getId(), source, 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 false; + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetDefender) { - UUID randomDefender = RandomUtil.randomFromCollection(possibleTargets); - target.addTarget(randomDefender, source, game); - return target.isChosen(game); + 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) { @@ -971,18 +1091,26 @@ public class ComputerPlayer extends PlayerImpl { for (Player player : game.getPlayers().values()) { cards.addAll(player.getGraveyard().getCards(game)); } - while (!target.isChosen(game) && !cards.isEmpty()) { + isAddedSomething = false; + while (!cards.isEmpty()) { Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game); if (card != null) { - target.addTarget(card.getId(), source, game); 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCardInExile) { - FilterCard filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterCard) { filter = (FilterCard) target.getOriginalTarget().getFilter(); @@ -999,14 +1127,23 @@ public class ComputerPlayer extends PlayerImpl { cards.add(card); } } - while (!target.isChosen(game) && !cards.isEmpty()) { + isAddedSomething = false; + while (!cards.isEmpty()) { Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game); if (card != null) { - target.addTarget(card.getId(), source, game); 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetActivatedAbility) { @@ -1017,22 +1154,43 @@ public class ComputerPlayer extends PlayerImpl { stackObjects.add(stackObject); } } - while (!target.isChosen(game) && !stackObjects.isEmpty()) { + while (!stackObjects.isEmpty()) { StackObject pick = stackObjects.get(0); if (pick != null) { - target.addTarget(pick.getId(), source, game); 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetActivatedOrTriggeredAbility) { - Iterator iterator = target.possibleTargets(source.getControllerId(), source, game).iterator(); - while (!target.isChosen(game) && iterator.hasNext()) { - target.addTarget(iterator.next(), source, game); + 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 target.isChosen(game); + return isAddedSomething; } if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) { @@ -1041,23 +1199,42 @@ public class ComputerPlayer extends PlayerImpl { cards.addAll(player.getGraveyard().getCards(game)); cards.addAll(game.getBattlefield().getAllActivePermanents(new FilterPermanent(), player.getId(), game)); } - Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game); - if (card != null) { - return tryAddTarget(target, card.getId(), source, 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) { - Cards cards = new CardsImpl(possibleTargets); - List possibleCards = new ArrayList<>(cards.getCards(game)); - while (!target.isChosen(game) && !possibleCards.isEmpty()) { - Card card = selectCardTarget(abilityControllerId, possibleCards, outcome, target, source, game); + 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) { - target.addTarget(card.getId(), source, game); - possibleCards.remove(card); // selectCard don't remove cards (only on second+ tries) + 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 target.isChosen(game); + return isAddedSomething; } throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName()); @@ -2033,9 +2210,11 @@ 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 target.isRequired(source); + 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 @@ -2045,21 +2224,28 @@ public class ComputerPlayer extends PlayerImpl { // 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)); - do { + isAddedSomething = false; + while (!cardChoices.isEmpty()) { Card card = selectCardTarget(abilityControllerId, cardChoices, outcome, target, source, game); if (card != null) { - target.addTarget(card.getId(), source, game); - cardChoices.remove(card); + 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 { - // We don't have any valid target to choose so stop choosing - return target.isChosen(game); + break; } + // 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); + if (target.isChosen(game) && !outcome.isGood() && target.getTargets().size() > target.getMinNumberOfTargets() + (target.getMaxNumberOfTargets() - target.getMinNumberOfTargets()) / 2) { + return true; } - } while (target.getTargets().size() < target.getMaxNumberOfTargets()); - return true; + } + return isAddedSomething; } @Override @@ -2891,6 +3077,7 @@ 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(); @@ -2917,6 +3104,8 @@ public class ComputerPlayer extends PlayerImpl { /** * 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 */ @@ -2933,22 +3122,30 @@ public class ComputerPlayer extends PlayerImpl { if (target.getOriginalTarget() instanceof TargetOpponent) { if (targetingSource == null) { if (target.canTarget(randomOpponentId, game)) { - target.add(randomOpponentId, game); - return true; + if (!target.contains(randomOpponentId)) { + target.add(randomOpponentId, game); + return true; + } } } else if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game)) { - target.addTarget(randomOpponentId, targetingSource, game); - return true; + 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)) { - target.add(possibleOpponentId, game); - return true; + if (!target.contains(possibleOpponentId)) { + target.add(possibleOpponentId, game); + return true; + } } } else if (target.canTarget(abilityControllerId, possibleOpponentId, targetingSource, game)) { - target.addTarget(possibleOpponentId, targetingSource, game); - return true; + if (!target.contains(possibleOpponentId)) { + target.addTarget(possibleOpponentId, targetingSource, game); + return true; + } } } return false; @@ -2959,24 +3156,24 @@ public class ComputerPlayer extends PlayerImpl { if (affectedOutcome.isGood()) { if (targetingSource == null) { // good - if (target.canTarget(getId(), game)) { + if (target.canTarget(getId(), game) && !target.contains(getId())) { target.add(getId(), game); return true; } if (target.isRequired(sourceId, game)) { - if (target.canTarget(randomOpponentId, game)) { + if (target.canTarget(randomOpponentId, game) && !target.contains(randomOpponentId)) { target.add(randomOpponentId, game); return true; } } } else { // good - if (target.canTarget(abilityControllerId, getId(), targetingSource, game)) { + 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)) { + if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game) && !target.contains(randomOpponentId)) { target.addTarget(randomOpponentId, targetingSource, game); return true; } @@ -2984,24 +3181,24 @@ public class ComputerPlayer extends PlayerImpl { } } else if (targetingSource == null) { // bad - if (target.canTarget(randomOpponentId, game)) { + if (target.canTarget(randomOpponentId, game) && !target.contains(randomOpponentId)) { target.add(randomOpponentId, game); return true; } if (target.isRequired(sourceId, game)) { - if (target.canTarget(getId(), game)) { + if (target.canTarget(getId(), game) && !target.contains(getId())) { target.add(getId(), game); return true; } } } else { // bad - if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game)) { + 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)) { + if (target.canTarget(abilityControllerId, getId(), targetingSource, game) && !target.contains(getId())) { target.addTarget(getId(), targetingSource, game); return true; } diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 5d73229b5fb..61f35a1cd2a 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -41,7 +41,6 @@ import mage.players.net.UserData; import mage.target.Target; import mage.target.TargetAmount; import mage.target.TargetCard; -import mage.target.TargetPermanent; import mage.target.common.TargetAttackingCreature; import mage.target.common.TargetDefender; import mage.target.targetpointer.TargetPointer; @@ -148,7 +147,7 @@ public class HumanPlayer extends PlayerImpl { } /** - * Make fake player from any other + * Make fake player from any other */ public HumanPlayer(final PlayerImpl sourcePlayer, final PlayerResponse sourceResponse) { super(sourcePlayer); @@ -387,9 +386,10 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean chooseMulligan(Game game) { if (!canCallFeedback(game)) { - return true; + return false; } + // TODO: rework to use existing chooseUse dialog, but do not remove chooseMulligan (AI must use special logic inside it) while (canRespond()) { int nextHandSize = game.mulliganDownTo(playerId); String cardsCountInfo = nextHandSize + (nextHandSize == 1 ? " card" : " cards"); @@ -427,7 +427,11 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) { if (!canCallFeedback(game)) { - return true; + return false; + } + + if (message == null) { + throw new IllegalArgumentException("Wrong code usage: main message is null"); } MessageToClient messageToClient = new MessageToClient(message, secondMessage); @@ -620,7 +624,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choose(Outcome outcome, Choice choice, Game game) { if (!canCallFeedback(game)) { - return true; + return false; } if (choice.isKeyChoice() && choice.getKeyChoices().isEmpty()) { @@ -683,7 +687,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) { if (!canCallFeedback(game)) { - return true; + return false; } // choose one or multiple permanents @@ -696,98 +700,85 @@ public class HumanPlayer extends PlayerImpl { options = new HashMap<>(); } + // stop on completed, e.g. X=0 + if (target.isChoiceCompleted(abilityControllerId, source, game)) { + return false; + } + while (canRespond()) { - Set possibleTargetIds = target.possibleTargets(abilityControllerId, source, game); - if (possibleTargetIds == null || possibleTargetIds.isEmpty()) { - return target.getTargets().size() >= target.getMinNumberOfTargets(); - } boolean required = target.isRequired(source != null ? source.getSourceId() : null, game); + + // enable done button after min targets selected if (target.getTargets().size() >= target.getMinNumberOfTargets()) { required = false; } - UUID responseId = target.tryToAutoChoose(abilityControllerId, source, game); + // stop on impossible selection + if (required && !target.canChoose(abilityControllerId, source, game)) { + break; + } - // responseId is null if a choice couldn't be automatically made - if (responseId == null) { - List chosenTargets = target.getTargets(); - options.put("chosenTargets", (Serializable) chosenTargets); + // stop on nothing to choose + Set possibleTargets = target.possibleTargets(abilityControllerId, source, game); + if (required && possibleTargets.isEmpty()) { + break; + } + + // MAKE A CHOICE + UUID autoChosenId = target.tryToAutoChoose(abilityControllerId, source, game); + if (autoChosenId != null && !target.contains(autoChosenId)) { + // auto-choose + target.add(autoChosenId, game); + // continue to next target (example: auto-choose must fill min/max = 2 from 2 possible cards) + } else { + // manual choose + options.put("chosenTargets", (Serializable) target.getTargets()); prepareForResponse(game); if (!isExecutingMacro()) { - game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(game), getRelatedObjectName(source, game)), possibleTargetIds, required, getOptions(target, options)); + game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(game), getRelatedObjectName(source, game)), possibleTargets, required, getOptions(target, options)); } waitForResponse(game); - responseId = getFixedResponseUUID(game); - } + UUID responseId = getFixedResponseUUID(game); - if (responseId != null) { - // selected some target + if (responseId != null) { + // selected something - // remove selected - if (target.getTargets().contains(responseId)) { - target.remove(responseId); - continue; - } + // remove selected + if (target.contains(responseId)) { + target.remove(responseId); + continue; + } - if (!possibleTargetIds.contains(responseId)) { - continue; - } - - if (target instanceof TargetPermanent) { - if (((TargetPermanent) target).canTarget(abilityControllerId, responseId, source, game, false)) { + if (possibleTargets.contains(responseId) && target.canTarget(getId(), responseId, source, game)) { target.add(responseId, game); - if (target.doneChoosing(game)) { - return true; + if (target.isChoiceCompleted(abilityControllerId, source, game)) { + break; } } } else { - MageObject object = game.getObject(source); - if (object instanceof Ability) { - if (target.canTarget(responseId, (Ability) object, game)) { - if (target.getTargets().contains(responseId)) { // if already included remove it with - target.remove(responseId); - } else { - target.addTarget(responseId, (Ability) object, game); - if (target.doneChoosing(game)) { - return true; - } - } - } - } else if (target.canTarget(responseId, game)) { - if (target.getTargets().contains(responseId)) { // if already included remove it with - target.remove(responseId); - } else { - target.addTarget(responseId, null, game); - if (target.doneChoosing(game)) { - return true; - } + // stop on done/cancel button press + if (target.isChosen(game)) { + break; + } else { + if (!required) { + // can stop at any moment + break; } } } - } else { - // send other command like cancel or done (??sends other commands like concede??) - - // auto-complete on all selected - if (target.getTargets().size() >= target.getMinNumberOfTargets()) { - return true; - } - - // cancel/done button - if (!required) { - return false; - } + // continue to next target } } - return false; + return target.isChosen(game) && target.getTargets().size() > 0; } @Override public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { if (!canCallFeedback(game)) { - return true; + return false; } // choose one or multiple targets @@ -799,57 +790,60 @@ public class HumanPlayer extends PlayerImpl { Map options = new HashMap<>(); while (canRespond()) { - Set possibleTargetIds = target.possibleTargets(abilityControllerId, source, game); + Set possibleTargets = target.possibleTargets(abilityControllerId, source, game); boolean required = target.isRequired(source != null ? source.getSourceId() : null, game); - if (possibleTargetIds.isEmpty() + if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getMinNumberOfTargets()) { required = false; } + // auto-choose UUID responseId = target.tryToAutoChoose(abilityControllerId, source, game); - // responseId is null if a choice couldn't be automatically made + // manual choice if (responseId == null) { - - - List chosenTargets = target.getTargets(); - options.put("chosenTargets", (Serializable) chosenTargets); + options.put("chosenTargets", (Serializable) target.getTargets()); prepareForResponse(game); if (!isExecutingMacro()) { game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(game), getRelatedObjectName(source, game)), - possibleTargetIds, required, getOptions(target, options)); + possibleTargets, required, getOptions(target, options)); } waitForResponse(game); responseId = getFixedResponseUUID(game); } if (responseId != null) { - // remove selected - if (target.getTargets().contains(responseId)) { + // remove old target + if (target.contains(responseId)) { target.remove(responseId); continue; } - if (possibleTargetIds.contains(responseId)) { + // add new target + if (possibleTargets.contains(responseId)) { if (target.canTarget(abilityControllerId, responseId, source, game)) { target.addTarget(responseId, source, game); - if (target.doneChoosing(game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game)) { return true; } } } } else { - if (target.getTargets().size() >= target.getMinNumberOfTargets()) { - return true; - } - if (!required) { + // done or cancel button pressed + if (target.isChosen(game)) { + // try to finish return false; + } else { + if (!required) { + // can stop at any moment + return false; + } } } } - return false; + return target.isChosen(game) && target.getTargets().size() > 0; } private Map getOptions(Target target, Map options) { @@ -867,10 +861,10 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { if (!canCallFeedback(game)) { - return true; + return false; } - // choose one or multiple cards + // ignore bad state if (cards == null || cards.isEmpty()) { return false; } @@ -884,6 +878,14 @@ public class HumanPlayer extends PlayerImpl { } while (canRespond()) { + + List possibleTargets = new ArrayList<>(); + for (UUID cardId : cards) { + if (target.canTarget(abilityControllerId, cardId, source, cards, game)) { + possibleTargets.add(cardId); + } + } + boolean required = target.isRequired(source != null ? source.getSourceId() : null, game); int count = cards.count(target.getFilter(), abilityControllerId, source, game); if (count == 0 @@ -891,23 +893,22 @@ public class HumanPlayer extends PlayerImpl { required = false; } - List chosenTargets = target.getTargets(); - List possibleTargets = new ArrayList<>(); - for (UUID cardId : cards) { - if (target.canTarget(abilityControllerId, cardId, source, cards, game)) { - possibleTargets.add(cardId); - } - } // if nothing to choose then show dialog (user must see non selectable items and click on any of them) + // TODO: need research - is it used? if (required && possibleTargets.isEmpty()) { required = false; } - UUID responseId = target.tryToAutoChoose(abilityControllerId, source, game, possibleTargets); - - if (responseId == null) { + // MAKE A CHOICE + UUID autoChosenId = target.tryToAutoChoose(abilityControllerId, source, game, possibleTargets); + if (autoChosenId != null && !target.contains(autoChosenId)) { + // auto-choose + target.add(autoChosenId, game); + // continue to next target (example: auto-choose must fill min/max = 2 from 2 possible cards) + } else { + // manual choose Map options = getOptions(target, null); - options.put("chosenTargets", (Serializable) chosenTargets); + options.put("chosenTargets", (Serializable) target.getTargets()); if (!possibleTargets.isEmpty()) { options.put("possibleTargets", (Serializable) possibleTargets); } @@ -918,27 +919,36 @@ public class HumanPlayer extends PlayerImpl { } waitForResponse(game); - responseId = getFixedResponseUUID(game); - } + UUID responseId = getFixedResponseUUID(game); - if (responseId != null) { - if (target.getTargets().contains(responseId)) { // if already included remove it with - target.remove(responseId); - } else { - if (target.canTarget(abilityControllerId, responseId, source, cards, game)) { + if (responseId != null) { + // selected something + + // remove selected + if (target.contains(responseId)) { + target.remove(responseId); + continue; + } + + if (possibleTargets.contains(responseId) && target.canTarget(getId(), responseId, source, cards, game)) { target.add(responseId, game); - if (target.doneChoosing(game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game)) { return true; } } + } else { + // done or cancel button pressed + if (target.isChosen(game)) { + // try to finish + return false; + } else { + if (!required) { + // can stop at any moment + return false; + } + } } - } else { - if (target.getTargets().size() >= target.getMinNumberOfTargets()) { - return true; - } - if (!required) { - return false; - } + // continue to next target } } @@ -949,7 +959,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { if (!canCallFeedback(game)) { - return true; + return false; } if (cards == null || cards.isEmpty()) { @@ -978,17 +988,16 @@ public class HumanPlayer extends PlayerImpl { possibleTargets.add(cardId); } } - // if nothing to choose then show dialog (user must see non selectable items and click on any of them) - if (required && possibleTargets.isEmpty()) { + // if nothing to choose then show dialog (user must see non-selectable items and click on any of them) + if (possibleTargets.isEmpty()) { required = false; } UUID responseId = target.tryToAutoChoose(abilityControllerId, source, game, possibleTargets); if (responseId == null) { - List chosenTargets = target.getTargets(); Map options = getOptions(target, null); - options.put("chosenTargets", (Serializable) chosenTargets); + options.put("chosenTargets", (Serializable) target.getTargets()); if (!possibleTargets.isEmpty()) { options.put("possibleTargets", (Serializable) possibleTargets); @@ -1004,11 +1013,11 @@ public class HumanPlayer extends PlayerImpl { } if (responseId != null) { - if (target.getTargets().contains(responseId)) { // if already included remove it + if (target.contains(responseId)) { // if already included remove it target.remove(responseId); } else if (target.canTarget(abilityControllerId, responseId, source, cards, game)) { target.addTarget(responseId, source, game); - if (target.doneChoosing(game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game)) { return true; } } @@ -1030,7 +1039,7 @@ public class HumanPlayer extends PlayerImpl { // choose amount // human can choose or un-choose MULTIPLE targets at once if (!canCallFeedback(game)) { - return true; + return false; } if (source == null) { @@ -1057,6 +1066,7 @@ public class HumanPlayer extends PlayerImpl { // 2. Distribute amount between selected targets // 1. Select targets + // TODO: rework to use existing chooseTarget instead custom select? while (canRespond()) { Set possibleTargetIds = target.possibleTargets(abilityControllerId, source, game); boolean required = target.isRequired(source.getSourceId(), game); @@ -1069,7 +1079,6 @@ public class HumanPlayer extends PlayerImpl { // responseId is null if a choice couldn't be automatically made if (responseId == null) { - List chosenTargets = target.getTargets(); List possibleTargets = new ArrayList<>(); for (UUID targetId : possibleTargetIds) { if (target.canTarget(abilityControllerId, targetId, source, game)) { @@ -1083,7 +1092,7 @@ public class HumanPlayer extends PlayerImpl { // selected Map options = getOptions(target, null); - options.put("chosenTargets", (Serializable) chosenTargets); + options.put("chosenTargets", (Serializable) target.getTargets()); if (!possibleTargets.isEmpty()) { options.put("possibleTargets", (Serializable) possibleTargets); } @@ -1185,17 +1194,8 @@ public class HumanPlayer extends PlayerImpl { // TODO: change pass and other states like passedUntilStackResolved for controlling player, not for "this" // TODO: check and change all "this" to controling player calls, many bugs with hand, mana, skips - https://github.com/magefree/mage/issues/2088 // TODO: use controlling player in all choose dialogs (and canRespond too, what's with take control of player AI?!) - UserData controllingUserData = this.userData; + UserData controllingUserData = this.getControllingPlayersUserData(game); if (canRespond()) { - if (!isGameUnderControl()) { - Player player = game.getPlayer(getTurnControlledBy()); - if (player instanceof HumanPlayer) { - controllingUserData = player.getUserData(); - } else { - // TODO: add computer opponent here?! - } - } - // TODO: check that all skips and stops used from real controlling player // like holdingPriority (is it a bug here?) if (getJustActivatedType() != null && !holdingPriority) { @@ -1489,7 +1489,7 @@ public class HumanPlayer extends PlayerImpl { @Override public TriggeredAbility chooseTriggeredAbility(java.util.List abilities, Game game) { - // choose triggered abilitity from list + // choose triggered ability from list if (!canCallFeedback(game)) { return abilities.isEmpty() ? null : abilities.get(0); } @@ -1620,7 +1620,7 @@ public class HumanPlayer extends PlayerImpl { protected boolean playManaHandling(Ability abilityToCast, ManaCost unpaid, String promptText, Game game) { // choose mana to pay (from permanents or from pool) if (!canCallFeedback(game)) { - return true; + return false; } // TODO: make canRespond cycle? @@ -2624,7 +2624,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choosePile(Outcome outcome, String message, java.util.List pile1, java.util.List pile2, Game game) { if (!canCallFeedback(game)) { - return true; + return false; } while (canRespond()) { diff --git a/Mage.Sets/src/mage/cards/a/Aetherspouts.java b/Mage.Sets/src/mage/cards/a/Aetherspouts.java index e2bfdb27cfc..506d81aed8a 100644 --- a/Mage.Sets/src/mage/cards/a/Aetherspouts.java +++ b/Mage.Sets/src/mage/cards/a/Aetherspouts.java @@ -150,7 +150,6 @@ class AetherspoutsEffect extends OneShotEffect { target = new TargetCard(Zone.BATTLEFIELD, new FilterCard("order to put on bottom of library (last chosen will be bottommost card)")); while (player.canRespond() && cards.size() > 1) { player.choose(Outcome.Neutral, cards, target, source, game); - Card card = cards.get(target.getFirstTarget(), game); if (card != null) { cards.remove(card); @@ -158,8 +157,10 @@ class AetherspoutsEffect extends OneShotEffect { if (permanent != null) { toLibrary.add(permanent); } + target.clearChosen(); + } else { + break; } - target.clearChosen(); } if (cards.size() == 1) { Card card = cards.get(cards.iterator().next(), game); diff --git a/Mage.Sets/src/mage/cards/b/BalduvianWarlord.java b/Mage.Sets/src/mage/cards/b/BalduvianWarlord.java index 55a1fcc34b2..43420c8bd1a 100644 --- a/Mage.Sets/src/mage/cards/b/BalduvianWarlord.java +++ b/Mage.Sets/src/mage/cards/b/BalduvianWarlord.java @@ -114,11 +114,7 @@ class BalduvianWarlordUnblockEffect extends OneShotEffect { filter.add(new PermanentReferenceInCollectionPredicate(list, game)); TargetPermanent target = new TargetPermanent(filter); target.withNotTarget(true); - if (target.canChoose(controller.getId(), source, game)) { - while (!target.isChosen(game) && target.canChoose(controller.getId(), source, game) && controller.canRespond()) { - controller.chooseTarget(outcome, target, source, game); - } - } else { + if (!controller.chooseTarget(outcome, target, source, game)) { return true; } Permanent chosenPermanent = game.getPermanent(target.getFirstTarget()); diff --git a/Mage.Sets/src/mage/cards/b/BrunaLightOfAlabaster.java b/Mage.Sets/src/mage/cards/b/BrunaLightOfAlabaster.java index de934d39ef3..bfb13f8b65d 100644 --- a/Mage.Sets/src/mage/cards/b/BrunaLightOfAlabaster.java +++ b/Mage.Sets/src/mage/cards/b/BrunaLightOfAlabaster.java @@ -135,17 +135,17 @@ class BrunaLightOfAlabasterEffect extends OneShotEffect { && controller.chooseUse(Outcome.Benefit, "Attach an Aura from your hand?", source, game)) { TargetCard targetAura = new TargetCard(Zone.HAND, filterAuraCard); if (!controller.choose(Outcome.Benefit, controller.getHand(), targetAura, source, game)) { - continue; + break; } Card aura = game.getCard(targetAura.getFirstTarget()); if (aura == null) { - continue; + break; } Target target = aura.getSpellAbility().getTargets().get(0); if (target == null) { - continue; + break; } fromHandGraveyard.add(aura); filterAuraCard.add(Predicates.not(new CardIdPredicate(aura.getId()))); @@ -159,17 +159,17 @@ class BrunaLightOfAlabasterEffect extends OneShotEffect { && controller.chooseUse(Outcome.Benefit, "Attach an Aura from your graveyard?", source, game)) { TargetCard targetAura = new TargetCard(Zone.GRAVEYARD, filterAuraCard); if (!controller.choose(Outcome.Benefit, controller.getGraveyard(), targetAura, source, game)) { - continue; + break; } Card aura = game.getCard(targetAura.getFirstTarget()); if (aura == null) { - continue; + break; } Target target = aura.getSpellAbility().getTargets().get(0); if (target == null) { - continue; + break; } fromHandGraveyard.add(aura); diff --git a/Mage.Sets/src/mage/cards/b/BurningOfXinye.java b/Mage.Sets/src/mage/cards/b/BurningOfXinye.java index a81caec204d..bfd7779a3a9 100644 --- a/Mage.Sets/src/mage/cards/b/BurningOfXinye.java +++ b/Mage.Sets/src/mage/cards/b/BurningOfXinye.java @@ -82,15 +82,12 @@ class BurningOfXinyeEffect extends OneShotEffect { Target target = new TargetControlledPermanent(amount, amount, filter, true); if (amount > 0 && target.canChoose(player.getId(), source, game)) { - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.choose(Outcome.DestroyPermanent, target, source, game); - } - - for (UUID targetId : target.getTargets()) { - Permanent permanent = game.getPermanent(targetId); - - if (permanent != null) { - abilityApplied |= permanent.destroy(source, game, false); + if (player.choose(Outcome.DestroyPermanent, target, source, game)) { + for (UUID targetId : target.getTargets()) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + abilityApplied |= permanent.destroy(source, game, false); + } } } } diff --git a/Mage.Sets/src/mage/cards/d/DevourFlesh.java b/Mage.Sets/src/mage/cards/d/DevourFlesh.java index 638d0b1b144..c5bbeccc4e1 100644 --- a/Mage.Sets/src/mage/cards/d/DevourFlesh.java +++ b/Mage.Sets/src/mage/cards/d/DevourFlesh.java @@ -64,9 +64,7 @@ class DevourFleshSacrificeEffect extends OneShotEffect { } if (game.getBattlefield().count(TargetSacrifice.makeFilter(StaticFilters.FILTER_PERMANENT_CREATURE), player.getId(), source, game) > 0) { Target target = new TargetSacrifice(StaticFilters.FILTER_PERMANENT_CREATURE); - while (player.canRespond() && !target.isChosen(game) && target.canChoose(player.getId(), source, game)) { - player.choose(Outcome.Sacrifice, target, source, game); - } + player.choose(Outcome.Sacrifice, target, source, game); Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { int gainLife = permanent.getToughness().getValue(); diff --git a/Mage.Sets/src/mage/cards/d/DivineReckoning.java b/Mage.Sets/src/mage/cards/d/DivineReckoning.java index b16bfdbc3eb..1f16b7df8e1 100644 --- a/Mage.Sets/src/mage/cards/d/DivineReckoning.java +++ b/Mage.Sets/src/mage/cards/d/DivineReckoning.java @@ -67,10 +67,7 @@ class DivineReckoningEffect extends OneShotEffect { Player player = game.getPlayer(playerId); if (player != null) { Target target = new TargetControlledPermanent(1, 1, new FilterControlledCreaturePermanent(), true); - if (target.canChoose(player.getId(), source, game)) { - while (player.canRespond() && !target.isChosen(game) && target.canChoose(player.getId(), source, game)) { - player.chooseTarget(Outcome.Benefit, target, source, game); - } + if (player.chooseTarget(Outcome.Benefit, target, source, game)) { Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { chosen.add(permanent); diff --git a/Mage.Sets/src/mage/cards/e/EntrapmentManeuver.java b/Mage.Sets/src/mage/cards/e/EntrapmentManeuver.java index 87f020ada89..686e3311a03 100644 --- a/Mage.Sets/src/mage/cards/e/EntrapmentManeuver.java +++ b/Mage.Sets/src/mage/cards/e/EntrapmentManeuver.java @@ -66,9 +66,7 @@ class EntrapmentManeuverSacrificeEffect extends OneShotEffect { } if (game.getBattlefield().count(TargetSacrifice.makeFilter(StaticFilters.FILTER_ATTACKING_CREATURE), player.getId(), source, game) > 0) { Target target = new TargetSacrifice(StaticFilters.FILTER_ATTACKING_CREATURE); - while (player.canRespond() && !target.isChosen(game) && target.canChoose(player.getId(), source, game)) { - player.choose(Outcome.Sacrifice, target, source, game); - } + player.choose(Outcome.Sacrifice, target, source, game); Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { int amount = permanent.getToughness().getValue(); diff --git a/Mage.Sets/src/mage/cards/e/EunuchsIntrigues.java b/Mage.Sets/src/mage/cards/e/EunuchsIntrigues.java index ff4e69d2012..439b5c416ff 100644 --- a/Mage.Sets/src/mage/cards/e/EunuchsIntrigues.java +++ b/Mage.Sets/src/mage/cards/e/EunuchsIntrigues.java @@ -67,10 +67,7 @@ class EunuchsIntriguesEffect extends OneShotEffect { FilterCreaturePermanent filter = new FilterCreaturePermanent("creature you control"); filter.add(new ControllerIdPredicate(player.getId())); Target target = new TargetPermanent(1, 1, filter, true); - if (target.canChoose(player.getId(), source, game)) { - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.chooseTarget(Outcome.DestroyPermanent, target, source, game); - } + if (player.chooseTarget(Outcome.DestroyPermanent, target, source, game)) { Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { game.informPlayers(player.getLogName() + " has chosen " + permanent.getLogName() + " as their only creature able to block this turn"); diff --git a/Mage.Sets/src/mage/cards/g/GoblinWarCry.java b/Mage.Sets/src/mage/cards/g/GoblinWarCry.java index 243edcf07cc..f7e38088f60 100644 --- a/Mage.Sets/src/mage/cards/g/GoblinWarCry.java +++ b/Mage.Sets/src/mage/cards/g/GoblinWarCry.java @@ -67,10 +67,7 @@ class GoblinWarCryEffect extends OneShotEffect { FilterCreaturePermanent filter = new FilterCreaturePermanent("creature you control"); filter.add(new ControllerIdPredicate(player.getId())); Target target = new TargetPermanent(1, 1, filter, true); - if (target.canChoose(player.getId(), source, game)) { - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.chooseTarget(Outcome.DestroyPermanent, target, source, game); - } + if (player.chooseTarget(Outcome.DestroyPermanent, target, source, game)) { Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { game.informPlayers(player.getLogName() + " has chosen " + permanent.getLogName() + " as their only creature able to block this turn"); diff --git a/Mage.Sets/src/mage/cards/i/ImperialEdict.java b/Mage.Sets/src/mage/cards/i/ImperialEdict.java index 4749cf35661..284aea926bf 100644 --- a/Mage.Sets/src/mage/cards/i/ImperialEdict.java +++ b/Mage.Sets/src/mage/cards/i/ImperialEdict.java @@ -66,10 +66,7 @@ class ImperialEdictEffect extends OneShotEffect { FilterCreaturePermanent filter = new FilterCreaturePermanent("creature you control"); filter.add(new ControllerIdPredicate(player.getId())); Target target = new TargetPermanent(1, 1, filter, true); - if (target.canChoose(player.getId(), source, game)) { - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.chooseTarget(Outcome.DestroyPermanent, target, source, game); - } + if (player.chooseTarget(Outcome.DestroyPermanent, target, source, game)) { Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { permanent.destroy(source, game, false); diff --git a/Mage.Sets/src/mage/cards/r/Rupture.java b/Mage.Sets/src/mage/cards/r/Rupture.java index c322b10d4e2..3b495306b74 100644 --- a/Mage.Sets/src/mage/cards/r/Rupture.java +++ b/Mage.Sets/src/mage/cards/r/Rupture.java @@ -65,10 +65,7 @@ class RuptureEffect extends OneShotEffect { if (player != null) { int power = 0; TargetSacrifice target = new TargetSacrifice(StaticFilters.FILTER_PERMANENT_CREATURE); - if (target.canChoose(player.getId(), source, game)) { - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.choose(Outcome.Sacrifice, target, source, game); - } + if (player.choose(Outcome.Sacrifice, target, source, game)){ Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { power = permanent.getPower().getValue(); diff --git a/Mage.Sets/src/mage/cards/s/Scapeshift.java b/Mage.Sets/src/mage/cards/s/Scapeshift.java index a5fa6db6e9d..9989c1a1960 100644 --- a/Mage.Sets/src/mage/cards/s/Scapeshift.java +++ b/Mage.Sets/src/mage/cards/s/Scapeshift.java @@ -67,6 +67,8 @@ class ScapeshiftEffect extends OneShotEffect { } int amount = 0; TargetSacrifice sacrificeLand = new TargetSacrifice(0, Integer.MAX_VALUE, StaticFilters.FILTER_LANDS); + // TODO: replace example for #8254: + // sacrificeLand.choose(Outcome.Sacrifice, controller.getId(), source.getSourceId(), source, game) if (controller.choose(Outcome.Sacrifice, sacrificeLand, source, game)) { for (UUID uuid : sacrificeLand.getTargets()) { Permanent land = game.getPermanent(uuid); diff --git a/Mage.Sets/src/mage/cards/w/WeiAssassins.java b/Mage.Sets/src/mage/cards/w/WeiAssassins.java index d99f62b01a9..badf00beaf0 100644 --- a/Mage.Sets/src/mage/cards/w/WeiAssassins.java +++ b/Mage.Sets/src/mage/cards/w/WeiAssassins.java @@ -76,10 +76,7 @@ class WeiAssassinsEffect extends OneShotEffect { FilterCreaturePermanent filter = new FilterCreaturePermanent("creature you control"); filter.add(new ControllerIdPredicate(player.getId())); Target target = new TargetPermanent(1, 1, filter, true); - if (target.canChoose(player.getId(), source, game)) { - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.chooseTarget(Outcome.DestroyPermanent, target, source, game); - } + if (player.chooseTarget(Outcome.DestroyPermanent, target, source, game)) { Permanent permanent = game.getPermanent(target.getFirstTarget()); if (permanent != null) { permanent.destroy(source, game, false); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/LightningStormTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/LightningStormTest.java index de5f27b9b06..8c06163c8d8 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/LightningStormTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/LightningStormTest.java @@ -33,9 +33,10 @@ public class LightningStormTest extends CardTestPlayerBase { // B discard and re-target activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Discard"); + setChoice(playerB, "Mountain"); // discard cost setChoice(playerB, true); // change target addTarget(playerB, playerA); // new target - setChoice(playerB, "Mountain"); // discard cost + // A discard and re-target activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java index 68dc852d72a..d9bea5640c2 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.abilities.enters; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -61,18 +62,19 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase { @Test public void sixEnterWithScapeshiftDamageToPlayerB() { - addCard(Zone.LIBRARY, playerA, "Mountain", 6); + addCard(Zone.LIBRARY, playerA, "Mountain", 10); addCard(Zone.BATTLEFIELD, playerA, "Valakut, the Molten Pinnacle"); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 6); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); addCard(Zone.HAND, playerA, "Scapeshift"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scapeshift"); - setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest"); - addTarget(playerA, "Mountain^Mountain^Mountain^Mountain^Mountain^Mountain"); - setChoice(playerA, "Whenever", 5); // order triggers - setChoice(playerA, true, 6); // yes to deal damage + setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest"); // to sac + addTarget(playerA, "Mountain^Mountain^Mountain^Mountain^Mountain^Mountain"); // to search + setChoice(playerA, "Whenever a Mountain", 6 - 1); // x6 triggers from valakut addTarget(playerA, playerB, 6); // to deal damage + setChoice(playerA, true, 6); // yes to deal damage + setStrictChooseMode(true); setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); @@ -81,7 +83,7 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Mountain", 6); assertLife(playerA, 20); - assertLife(playerB, 2); // 6 * 3 damage = 18 + assertLife(playerB, 20 - 18); // 6 * 3 damage = 18 } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ImproviseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ImproviseTest.java index 41510b0a393..e2e90a43af9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ImproviseTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ImproviseTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; /** @@ -26,9 +27,10 @@ public class ImproviseTest extends CardTestPlayerBaseWithAIHelps { activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 4); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bastion Inventor"); setChoice(playerA, "Blue", 4); // pay 1-4 - // improvise pay by one card + // improvise pay by one card (after 2025 addTarget improve - Improvise can be pay by multiple addTarget) setChoice(playerA, "Improvise"); addTarget(playerA, "Alpha Myr"); // pay 5 as improvise + addTarget(playerA, TestPlayer.TARGET_SKIP); // choose only 1 of 2 possible permanent setChoice(playerA, "Improvise"); addTarget(playerA, "Alpha Myr"); // pay 6 as improvise diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java index 39ee676487a..6abb3d2a9ac 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java @@ -6,6 +6,7 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Ignore; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -33,6 +34,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 3); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); // after @@ -73,6 +75,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); setChoice(playerB, true); // continue addTarget(playerB, "Balduvian Bears"); // player B must take control for searching + addTarget(playerB, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); // after @@ -116,6 +119,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { setChoice(playerA, true); // yes, try to cast a creature card from lib setChoice(playerA, "Panglacial Wurm"); // try to cast addTarget(playerA, "Balduvian Bears"); // choice for searching + addTarget(playerA, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); // after @@ -170,6 +174,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { setChoice(playerB, true); // yes, try to cast a creature card from lib setChoice(playerB, "Panglacial Wurm"); // try to cast addTarget(playerB, "Balduvian Bears"); // choice for searching + addTarget(playerB, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); // after @@ -230,6 +235,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); setChoice(playerB, true); // continue after new control addTarget(playerB, "Balduvian Bears"); + addTarget(playerB, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); checkGraveyardCount("after grave a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); checkGraveyardCount("after grave b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java index 35c05e4c0ff..0ce9b91e4ec 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java @@ -15,6 +15,7 @@ import mage.util.CardUtil; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; import java.util.Arrays; @@ -509,6 +510,7 @@ public class AdjusterCostTest extends CardTestPlayerBaseWithAIHelps { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fireball"); setChoice(playerA, "X=2"); addTarget(playerA, "Arbor Elf^Arbor Elf"); + addTarget(playerA, TestPlayer.TARGET_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java index 2e608ce4130..601610fc121 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java @@ -10,6 +10,7 @@ import mage.counters.CounterType; import mage.game.stack.StackObject; import org.junit.Assert; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -280,6 +281,7 @@ public class CollectEvidenceTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, monitor); setChoice(playerA, true); setChoice(playerA, giant); + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java index 41dd7262dbc..1d6ba250fcb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.cost.additional; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -22,6 +23,7 @@ public class RemoveCounterCostTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}, Remove two +1/+1 counters"); setChoice(playerA, "Novijen Sages"); // counters to remove + setChoice(playerA, TestPlayer.CHOICE_SKIP); setChoice(playerA, "X=2"); // counters to remove setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/AddManaOfAnyTypeProducedTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/AddManaOfAnyTypeProducedTest.java index db6ab097935..813b019f70f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/AddManaOfAnyTypeProducedTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/AddManaOfAnyTypeProducedTest.java @@ -29,6 +29,7 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase { activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Vedalken Mastermind"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -52,6 +53,7 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase { activateManaAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}"); + setStrictChooseMode(true); setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); @@ -71,19 +73,26 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase { // If Gemstone Caverns is in your opening hand and you're not playing first, you may begin the game with Gemstone Caverns on the battlefield with a luck counter on it. If you do, exile a card from your hand. // {T}: Add {C}. If Gemstone Caverns has a luck counter on it, instead add one mana of any color. addCard(Zone.HAND, playerB, "Gemstone Caverns", 1); + addCard(Zone.HAND, playerB, "Swamp", 1); addCard(Zone.HAND, playerB, "Silvercoat Lion", 2); + // pay and put Gemstone to battlefield on starting + setChoice(playerB, true); + setChoice(playerB, "Swamp"); + activateManaAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}: Add"); setChoice(playerB, "White"); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Silvercoat Lion"); + + setStrictChooseMode(true); setStopAt(2, PhaseStep.BEGIN_COMBAT); execute(); assertPermanentCount(playerB, "Gemstone Caverns", 1); assertCounterCount("Gemstone Caverns", CounterType.LUCK, 1); assertPermanentCount(playerB, "Silvercoat Lion", 1); - assertExileCount("Silvercoat Lion", 1); + assertExileCount("Swamp", 1); assertTapped("Gemstone Caverns", true); } @@ -99,12 +108,19 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase { // If Gemstone Caverns is in your opening hand and you're not playing first, you may begin the game with Gemstone Caverns on the battlefield with a luck counter on it. If you do, exile a card from your hand. // {T}: Add {C}. If Gemstone Caverns has a luck counter on it, instead add one mana of any color. addCard(Zone.HAND, playerB, "Gemstone Caverns", 1); + addCard(Zone.HAND, playerB, "Swamp", 1); addCard(Zone.HAND, playerB, "Silvercoat Lion", 2); - activateManaAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}: Add"); + // pay and put Gemstone to battlefield on starting + setChoice(playerB, true); + setChoice(playerB, "Swamp"); + + activateManaAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}: Add {C}"); setChoice(playerB, "White"); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Vorinclex, Voice of Hunger"); + + setStrictChooseMode(true); setStopAt(2, PhaseStep.BEGIN_COMBAT); execute(); @@ -112,7 +128,6 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase { assertCounterCount("Gemstone Caverns", CounterType.LUCK, 1); assertPermanentCount(playerB, "Vorinclex, Voice of Hunger", 1); assertTapped("Gemstone Caverns", true); - } private static final String kinnan = "Kinnan, Bonder Prodigy"; diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java index 8c6ab794249..844ca5cfb0a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.afc; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestCommander4Players; @@ -313,6 +314,7 @@ public class ShareTheSpoilsTest extends CardTestCommander4Players { // Cast an adventure card from hand castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Dizzying Swoop"); addTarget(playerA, "Prosper, Tome-Bound"); + addTarget(playerA, TestPlayer.TARGET_SKIP); // tap 1 of 2 targets waitStackResolved(5, PhaseStep.PRECOMBAT_MAIN); // Make sure the creature card can't be played from exile since there isn't the {W}{W} for it diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java index 7e06886f60a..bbc8a10ac93 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java @@ -169,15 +169,25 @@ public class ApproachOfTheSecondSunTest extends CardTestPlayerBase { @Test public void testCastFromGraveyard() { removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, "Plains", 6); - addCard(Zone.HAND, playerA, "Approach of the Second Sun", 1); - addCard(Zone.GRAVEYARD, playerA, "Approach of the Second Sun", 2); - addCard(Zone.HAND, playerA, "Finale of Promise", 2); - addCard(Zone.BATTLEFIELD, playerA, "Mystic Monastery", 25); + // + // If this spell was cast from your hand and you've cast another spell named Approach of the Second Sun this game, + // you win the game. Otherwise, put Approach of the Second Sun into its owner's library seventh from the top + // and you gain 7 life. + addCard(Zone.HAND, playerA, "Approach of the Second Sun", 1); // {6}{W} + addCard(Zone.GRAVEYARD, playerA, "Approach of the Second Sun", 2); // {6}{W} + // + // You may cast up to one target instant card and/or up to one target sorcery card from your graveyard each + // with mana value X or less without paying their mana costs. If a spell cast this way would be put into + // your graveyard, exile it instead. If X is 10 or more, copy each of those spells twice. You may choose + // new targets for the copies. + addCard(Zone.HAND, playerA, "Finale of Promise", 2); // {X}{R}{R} + // + addCard(Zone.BATTLEFIELD, playerA, "Mystic Monastery", 25); // for mana // first may have been cast from anywhere. castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Finale of Promise", true); - // You may cast up to one target instant card and/or up to one target sorcery card from your graveyard each with mana value X or less without paying their mana costs. If a spell cast this way would be put into your graveyard, exile it instead. If X is 10 or more, copy each of those spells twice. You may choose new targets for the copies. setChoice(playerA, "X=7"); // each with mana value X or less setChoice(playerA, "Yes"); // You may cast addTarget(playerA, TARGET_SKIP); // up to one target instant card diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/ChainerNightmareAdeptTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/ChainerNightmareAdeptTest.java index 630721af3b9..6d8a2a917ed 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/ChainerNightmareAdeptTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/ChainerNightmareAdeptTest.java @@ -24,6 +24,7 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase { addCard(Zone.GRAVEYARD, playerA, maaka, 2); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard"); + setChoice(playerA, mountain); // discard cost waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, maaka); @@ -33,8 +34,8 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase { attack(1, playerA, maaka); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); - execute(); assertPermanentCount(playerA, maaka, 1); @@ -52,7 +53,10 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase { addCard(Zone.GRAVEYARD, playerA, khenra); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard"); - setChoice(playerA, true); + setChoice(playerA, mountain); // discard cost + setChoice(playerA, true); // use copy + setChoice(playerA, "0"); // for casting main spell - x2 permitting object + setChoice(playerA, "0"); // for casting copied spell - x2 permitting object waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, maaka, true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, khenra); @@ -60,8 +64,8 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase { attack(1, playerA, maaka); attack(1, playerA, khenra); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); - execute(); assertPermanentCount(playerA, maaka, 1); @@ -88,8 +92,8 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase { attack(1, playerA, maaka); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); - execute(); assertPermanentCount(playerA, maaka, 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/clb/BabaLysagaNightWitchTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/clb/BabaLysagaNightWitchTest.java index a32ff3e595a..4787a8f9b5f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/clb/BabaLysagaNightWitchTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/clb/BabaLysagaNightWitchTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.clb; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -29,6 +30,7 @@ public class BabaLysagaNightWitchTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.UPKEEP, playerA, "{1}: {this} becomes a 2/2 Assembly-Worker artifact creature until end of turn. It's still a land."); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three permanents: If there "); setChoice(playerA, "Mishra's Factory"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); @@ -48,6 +50,7 @@ public class BabaLysagaNightWitchTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three permanents: If there "); setChoice(playerA, "Mishra's Factory"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java index 882c97fdb57..d8e9a0a82e3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java @@ -5,6 +5,7 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -45,6 +46,7 @@ public class MuYanlingWindRiderTest extends CardTestPlayerBase { activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Crew 1"); setChoice(playerA, "Memnite"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); attack(3, playerA, "Vehicle Token", playerB); attack(3, playerA, muyanling, playerB); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java index 812090aab8a..8f5316c9411 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java @@ -4,6 +4,7 @@ package org.mage.test.cards.single.emn; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -77,6 +78,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two"); addTarget(playerA, "Bronze Sable"); + addTarget(playerA, TestPlayer.TARGET_SKIP); attack(1, playerA, "Bronze Sable"); @@ -135,6 +137,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two"); addTarget(playerA, "Sylvan Advocate"); + addTarget(playerA, TestPlayer.TARGET_SKIP); attack(1, playerA, "Sylvan Advocate"); attack(2, playerB, "Memnite"); @@ -167,6 +170,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tamiyo, Field Researcher", true); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two"); addTarget(playerA, "Sylvan Advocate"); + addTarget(playerA, TestPlayer.TARGET_SKIP); attack(1, playerA, "Sylvan Advocate"); @@ -236,6 +240,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tamiyo, Field Researcher", true); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two"); addTarget(playerA, "Bronze Sable"); + addTarget(playerA, TestPlayer.TARGET_SKIP); attack(2, playerB, "Bronze Sable"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java index 0749f8884c5..66824c5bb01 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.grn; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -31,6 +32,7 @@ public class WandOfVertebraeTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}"); addTarget(playerA, lavaCoil); + addTarget(playerA, TestPlayer.TARGET_SKIP); // must choose 1 of 5 setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java index 349dda8af12..0a33e87222b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.j22; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -80,13 +81,17 @@ public class AgrusKosEternalSoldierTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Smoldering Werewolf"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Smoldering Werewolf"); - setChoice(playerA, "When {this} enters, it deals"); - addTarget(playerA, agrus); - addTarget(playerA, agrus); - setChoice(playerB, true); // gain life - setChoice(playerB, "Whenever {this} becomes"); - setChoice(playerB, true); // pay to copy - setChoice(playerB, true); // pay to copy + setChoice(playerB, true); // gain life on cast + // + setChoice(playerA, "When {this} enters, it deals"); // x2 triggers from Panharmonicon + addTarget(playerA, agrus); // x1 trigger + addTarget(playerA, TestPlayer.TARGET_SKIP); // x1 trigger + addTarget(playerA, agrus); // x2 trigger + addTarget(playerA, TestPlayer.TARGET_SKIP); // x2 trigger + // + setChoice(playerB, "Whenever {this} becomes"); // x2 triggers from Panharmonicon + setChoice(playerB, true); // x1 pay to copy + setChoice(playerB, true); // x2 pay to copy setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/AlpineHoundmasterTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/AlpineHoundmasterTest.java index 64008416185..329cd5247be 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/AlpineHoundmasterTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/AlpineHoundmasterTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.m21; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; public class AlpineHoundmasterTest extends CardTestPlayerBase { @@ -22,6 +23,7 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpine Houndmaster"); setChoice(playerA, true); addTarget(playerA, "Alpine Watchdog"); + addTarget(playerA, TestPlayer.TARGET_SKIP); // only single card setStrictChooseMode(true); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); @@ -44,6 +46,7 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpine Houndmaster"); setChoice(playerA, true); addTarget(playerA, "Igneous Cur"); + addTarget(playerA, TestPlayer.TARGET_SKIP); // only single card setStrictChooseMode(true); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); @@ -54,7 +57,7 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase { } @Test - public void searchBoth() { + public void searchBoth_TestFramework_AddTargetsAsSingle() { // When Alpine Houndmaster enters the battlefield, you may search your library for a card named // Alpine Watchdog and/or a card named Igneous Cur, reveal them, put them into your hand, then shuffle your library. addCard(Zone.HAND, playerA, "Alpine Houndmaster", 1); @@ -72,7 +75,30 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase { execute(); assertHandCount(playerA, 2); + } + @Test + public void searchBoth_TestFramework_AddTargetsAsMultiple() { + // test framework must support both + + // When Alpine Houndmaster enters the battlefield, you may search your library for a card named + // Alpine Watchdog and/or a card named Igneous Cur, reveal them, put them into your hand, then shuffle your library. + addCard(Zone.HAND, playerA, "Alpine Houndmaster", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.LIBRARY, playerA, "Alpine Watchdog"); + addCard(Zone.LIBRARY, playerA, "Igneous Cur"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpine Houndmaster"); + setChoice(playerA, true); + addTarget(playerA, "Igneous Cur"); + addTarget(playerA, "Alpine Watchdog"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertHandCount(playerA, 2); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java index 8d2db6405d7..658445c4daa 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.mh3; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; /** @@ -72,6 +73,7 @@ public class NethergoyfTest extends CardTestPlayerBaseWithAIHelps { checkPlayableAbility("can escape", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + nethergoyf + " with Escape", true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nethergoyf + " with Escape"); setChoice(playerA, "Memnite^Memnite^Memnite^Memnite^Bitterblossom"); // cards exiled for escape cost: Exile all the Memnite but one. + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SuppressionRayTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SuppressionRayTest.java index 17909938b02..16905dcbf74 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SuppressionRayTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SuppressionRayTest.java @@ -4,6 +4,7 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -39,6 +40,7 @@ public class SuppressionRayTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Suppression Ray", playerB); setChoiceAmount(playerA, 3); // decide to pay 3 energy setChoice(playerA, "Zodiac Pig^Zodiac Rabbit"); // put stun on those 2 creatures + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java index fd367a14e24..926d936d4a4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java @@ -2,8 +2,10 @@ package org.mage.test.cards.single.mkm; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.target.TargetPlayer; import org.junit.Ignore; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; public class CovetedFalconTest extends CardTestPlayerBase { @@ -31,6 +33,7 @@ public class CovetedFalconTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); addTarget(playerA, playerB); addTarget(playerA, "Grizzly Bears"); + addTarget(playerA, TestPlayer.TARGET_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -57,6 +60,7 @@ public class CovetedFalconTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); addTarget(playerA, playerB); addTarget(playerA, "Grizzly Bears"); + addTarget(playerA, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Turn Against", "Grizzly Bears"); @@ -82,6 +86,7 @@ public class CovetedFalconTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); addTarget(playerA, playerB); addTarget(playerA, "Putrid Goblin"); + addTarget(playerA, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Murder", "Putrid Goblin"); @@ -111,6 +116,7 @@ public class CovetedFalconTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); addTarget(playerA, playerB); addTarget(playerA, "Treacherous Pit-Dweller"); + addTarget(playerA, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Murder", "Treacherous Pit-Dweller"); @@ -151,6 +157,7 @@ public class CovetedFalconTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); addTarget(playerA, playerB); addTarget(playerA, "Darksteel Relic^Grizzly Bears"); + addTarget(playerA, TestPlayer.TARGET_SKIP); castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Murder", "Guardian Beast"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/TenthDistrictHeroTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/TenthDistrictHeroTest.java index 0a54b4ded33..ceb320da09f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/TenthDistrictHeroTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/TenthDistrictHeroTest.java @@ -5,6 +5,7 @@ import mage.constants.PhaseStep; import mage.constants.SubType; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -65,6 +66,7 @@ public class TenthDistrictHeroTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}"); setChoice(playerA, giant); + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); @@ -92,6 +94,7 @@ public class TenthDistrictHeroTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}"); setChoice(playerA, giant); + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/TheWiseMothmanTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/TheWiseMothmanTest.java index 342cbde4d91..963972ee0b0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/TheWiseMothmanTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/TheWiseMothmanTest.java @@ -4,6 +4,7 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -37,6 +38,7 @@ public class TheWiseMothmanTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{3}"); addTarget(playerA, mothman + "^Grizzly Bears"); // up to three targets => choosing 2 + addTarget(playerA, TestPlayer.TARGET_SKIP); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/CastAsInstantTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/AutoChooseTargetsAndCastAsInstantTest.java similarity index 51% rename from Mage.Tests/src/test/java/org/mage/test/cards/asthough/CastAsInstantTest.java rename to Mage.Tests/src/test/java/org/mage/test/cards/targets/AutoChooseTargetsAndCastAsInstantTest.java index 5c32c90f343..3f0f7c86144 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/CastAsInstantTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/AutoChooseTargetsAndCastAsInstantTest.java @@ -1,5 +1,4 @@ - -package org.mage.test.cards.asthough; +package org.mage.test.cards.targets; import mage.constants.PhaseStep; import mage.constants.Zone; @@ -7,13 +6,11 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * - * @author LevelX2 + * @author LevelX2, JayDi85 */ -public class CastAsInstantTest extends CardTestPlayerBase { +public class AutoChooseTargetsAndCastAsInstantTest extends CardTestPlayerBase { - @Test - public void testEffectOnlyForOneTurn() { + private void run_WithAutoSelection(int selectedTargets, int possibleTargets) { addCard(Zone.BATTLEFIELD, playerB, "Island"); addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); // The next sorcery card you cast this turn can be cast as though it had flash. @@ -23,22 +20,45 @@ public class CastAsInstantTest extends CardTestPlayerBase { // Target opponent exiles two cards from their hand and loses 2 life. addCard(Zone.HAND, playerB, "Witness the End"); // {3}{B} - addCard(Zone.HAND, playerA, "Silvercoat Lion", 2); + addCard(Zone.HAND, playerA, "Silvercoat Lion", possibleTargets); castSpell(1, PhaseStep.UPKEEP, playerB, "Quicken", true); castSpell(1, PhaseStep.UPKEEP, playerB, "Witness the End", playerA); + // it uses auto-choose logic inside, so disable strict mode + // logic for possible targets with min/max = 2: + // 0, 1, 2 - auto-choose all possible targets + // 3+ - AI choose best targets + setStrictChooseMode(false); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); assertGraveyardCount(playerB, "Quicken", 1); assertGraveyardCount(playerB, "Witness the End", 1); - assertExileCount("Silvercoat Lion", 2); + assertExileCount("Silvercoat Lion", selectedTargets); assertLife(playerA, 18); assertLife(playerB, 20); - } + @Test + public void test_AutoChoose_0_of_0() { + run_WithAutoSelection(0, 0); + } + + @Test + public void test_AutoChoose_1_of_1() { + run_WithAutoSelection(1, 1); + } + + @Test + public void test_AutoChoose_2_of_2() { + run_WithAutoSelection(2, 2); + } + + @Test + public void test_AutoChoose_2_of_5() { + run_WithAutoSelection(2, 5); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionBaseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionBaseTest.java new file mode 100644 index 00000000000..7681ef14fd4 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionBaseTest.java @@ -0,0 +1,258 @@ +package org.mage.test.cards.targets; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapAttachedCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.cards.CardsImpl; +import mage.constants.Outcome; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.Target; +import mage.target.common.TargetCardInHand; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBase; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +/** + * Helper class for target selection tests in diff use cases like selection on targets declare or on resolve + * + * @author JayDi85 + */ +public class TargetsSelectionBaseTest extends CardTestPlayerBaseWithAIHelps { + + static final boolean DEBUG_ENABLE_DETAIL_LOGS = true; + + protected void run_PlayerChooseTarget_OnActivate(int chooseCardsCount, int availableCardsCount) { + // custom effect - exile and gain life due selected targets + int minTarget = 0; + int maxTarget = 3; + String startingText = "exile and gain"; + Ability ability = new SimpleActivatedAbility( + new ExileTargetEffect().setText("exile"), + new ManaCostsImpl<>("") + ); + ability.addEffect(new GainLifeEffect(TotalTargetsValue.instance).concatBy("and").setText("gain life")); + ability.addTarget(new TargetCardInHand(minTarget, maxTarget, StaticFilters.FILTER_CARD).withNotTarget(false)); + addCustomCardWithAbility("test choice", playerA, ability); + + addCard(Zone.HAND, playerA, "Swamp", availableCardsCount); + + checkHandCardCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", availableCardsCount); + checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", 0); + + if (availableCardsCount > 0) { + checkPlayableAbility("can activate on non-zero targets", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText); + } else { + checkPlayableAbility("can't activate on zero targets", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, false); + } + + if (chooseCardsCount > 0) { + // need selection + List targetCards = new ArrayList<>(); + IntStream.rangeClosed(1, chooseCardsCount).forEach(x -> { + targetCards.add("Swamp"); + }); + addTarget(playerA, String.join("^", targetCards)); + // end selection: + // - x of 0 - no + // - 1 of 3 - yes + // - 2 of 3 - yes + // - 3 of 3 - no, it's auto-finish on last select + // - 3 of 5 - no, it's auto-finish on last select + if (chooseCardsCount < maxTarget) { + addTarget(playerA, TestPlayer.TARGET_SKIP); + } + } else { + // need skip + // on 0 cards there are not valid targets, so no any dialogs + if (availableCardsCount > 0) { + addTarget(playerA, TestPlayer.TARGET_SKIP); + } + } + + if (DEBUG_ENABLE_DETAIL_LOGS) { + System.out.println("planning actions:"); + playerA.getActions().forEach(System.out::println); + System.out.println("planning targets:"); + playerA.getTargets().forEach(System.out::println); + System.out.println("planning choices:"); + playerA.getChoices().forEach(System.out::println); + } + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertExileCount(playerA, "Swamp", chooseCardsCount); + assertLife(playerA, 20 + chooseCardsCount); + } + + protected void run_PlayerChoose_OnResolve(int chooseCardsCount, int availableCardsCount) { + // custom effect - select, exile and gain life + int minTarget = 0; + int maxTarget = 3; + String startingText = "select, exile and gain life"; + Ability ability = new SimpleActivatedAbility( + new SelectExileAndGainLifeCustomEffect(minTarget, maxTarget, Outcome.Benefit), + new ManaCostsImpl<>("") + ); + addCustomCardWithAbility("test choice", playerA, ability); + + addCard(Zone.HAND, playerA, "Swamp", availableCardsCount); + + checkHandCardCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", availableCardsCount); + checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", 0); + + checkPlayableAbility("can activate any time (even with zero cards)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText); + + if (chooseCardsCount > 0) { + // need selection + List targetCards = new ArrayList<>(); + IntStream.rangeClosed(1, chooseCardsCount).forEach(x -> { + targetCards.add("Swamp"); + }); + setChoice(playerA, String.join("^", targetCards)); + } else { + // need skip + // on 0 cards there are must be dialog with done button anyway + + // end selection: + // - x of 0 - yes + // - 1 of 3 - yes + // - 2 of 3 - yes + // - 3 of 3 - no, it's auto-finish on last select + // - 3 of 5 - no, it's auto-finish on last select + if (chooseCardsCount < maxTarget) { + setChoice(playerA, TestPlayer.CHOICE_SKIP); + } + + } + + if (DEBUG_ENABLE_DETAIL_LOGS) { + System.out.println("planning actions:"); + playerA.getActions().forEach(System.out::println); + System.out.println("planning targets:"); + playerA.getTargets().forEach(System.out::println); + System.out.println("planning choices:"); + playerA.getChoices().forEach(System.out::println); + } + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertExileCount(playerA, "Swamp", chooseCardsCount); + assertLife(playerA, 20 + chooseCardsCount); + } + + protected void run_PlayerChoose_OnResolve_AI(Outcome outcome, int minTargets, int maxTargets, int aiMustChooseCardsCount, int availableCardsCount) { + // custom effect - select, exile and gain life + String startingText = "{T}: Select, exile and gain life"; + Ability ability = new SimpleActivatedAbility( + new SelectExileAndGainLifeCustomEffect(minTargets, maxTargets, outcome), + new ManaCostsImpl<>("") + ); + ability.addCost(new TapSourceCost()); + addCustomCardWithAbility("test choice", playerA, ability); + + addCard(Zone.HAND, playerA, "Swamp", availableCardsCount); + addCard(Zone.HAND, playerA, "Forest", 1); + + // Forest play is workaround to disable lands play in ai priority + // {T} cost is workaround to disable multiple calls of the ability + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest"); + + checkPlayableAbility("can activate any time (even with zero cards)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true); + + // AI must see bad effect and select only halves of the max targets, e.g. 1 of 3 + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertExileCount(playerA, "Swamp", aiMustChooseCardsCount); + assertLife(playerA, 20 + aiMustChooseCardsCount); + } +} + +enum TotalTargetsValue implements DynamicValue { + instance; + + @Override + public TotalTargetsValue copy() { + return instance; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return effect.getTargetPointer().getTargets(game, sourceAbility).size(); + } + + @Override + public String getMessage() { + return "total targets"; + } + + @Override + public String toString() { + return "X"; + } +} + +class SelectExileAndGainLifeCustomEffect extends OneShotEffect { + + private final int minTargets; + private final int maxTargets; + + SelectExileAndGainLifeCustomEffect(int minTargets, int maxTargets, Outcome outcome) { + super(outcome); + this.minTargets = minTargets; + this.maxTargets = maxTargets; + staticText = "select, exile and gain life"; + } + + private SelectExileAndGainLifeCustomEffect(final SelectExileAndGainLifeCustomEffect effect) { + super(effect); + this.minTargets = effect.minTargets; + this.maxTargets = effect.maxTargets; + } + + @Override + public SelectExileAndGainLifeCustomEffect copy() { + return new SelectExileAndGainLifeCustomEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + + Target target = new TargetCardInHand(this.minTargets, this.maxTargets, StaticFilters.FILTER_CARD).withNotTarget(true); + if (!player.choose(outcome, target, source, game)) { + return false; + } + + player.moveCardsToExile(new CardsImpl(target.getTargets()).getCards(game), source, game, false, source.getSourceId(), player.getLogName()); + player.gainLife(target.getTargets().size(), game, source); + return true; + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnActivateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnActivateTest.java new file mode 100644 index 00000000000..6dde5d749b6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnActivateTest.java @@ -0,0 +1,89 @@ +package org.mage.test.cards.targets; + +import org.junit.Test; + +/** + * Testing targets selection on activate/cast (player.chooseTarget) + * + * @author JayDi85 + */ +public class TargetsSelectionOnActivateTest extends TargetsSelectionBaseTest { + + // no selects + + @Test + public void test_OnActivate_0_of_0() { + run_PlayerChooseTarget_OnActivate(0, 0); + } + + @Test + public void test_OnActivate_0_of_1() { + run_PlayerChooseTarget_OnActivate(0, 1); + } + + @Test + public void test_OnActivate_0_of_2() { + run_PlayerChooseTarget_OnActivate(0, 2); + } + + @Test + public void test_OnActivate_0_of_3() { + run_PlayerChooseTarget_OnActivate(0, 3); + } + + @Test + public void test_OnActivate_0_of_10() { + run_PlayerChooseTarget_OnActivate(0, 10); + } + + // 1 select + + @Test + public void test_OnActivate_1_of_1() { + run_PlayerChooseTarget_OnActivate(1, 1); + } + + @Test + public void test_OnActivate_1_of_2() { + run_PlayerChooseTarget_OnActivate(1, 2); + } + + @Test + public void test_OnActivate_1_of_3() { + run_PlayerChooseTarget_OnActivate(1, 3); + } + + @Test + public void test_OnActivate_1_of_10() { + run_PlayerChooseTarget_OnActivate(1, 10); + } + + // 2 selects + + @Test + public void test_OnActivate_2_of_2() { + run_PlayerChooseTarget_OnActivate(2, 2); + } + + @Test + public void test_OnActivate_2_of_3() { + run_PlayerChooseTarget_OnActivate(2, 3); + } + + @Test + public void test_OnActivate_2_of_10() { + run_PlayerChooseTarget_OnActivate(2, 10); + } + + // 3 selects + + @Test + public void test_OnActivate_3_of_3() { + run_PlayerChooseTarget_OnActivate(3, 3); + } + + @Test + public void test_OnActivate_3_of_10() { + run_PlayerChooseTarget_OnActivate(3, 10); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnResolveAITest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnResolveAITest.java new file mode 100644 index 00000000000..f5b39950bf4 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnResolveAITest.java @@ -0,0 +1,66 @@ +package org.mage.test.cards.targets; + +import mage.constants.Outcome; +import org.junit.Test; + +/** + * Testing targets selection on resolve (player.choose) for AI + *

+ * AI must use logic like: + * - for good effects - choose as much as possible targets + * - for bad effects - choose as much as lower targets + * + * @author JayDi85 + */ +public class TargetsSelectionOnResolveAITest extends TargetsSelectionBaseTest { + + @Test + public void test_OnResolve_Good_0_of_0() { + run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 0, 0); + } + + @Test + public void test_OnResolve_Good_1_of_1() { + run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 1, 1); + } + + @Test + public void test_OnResolve_Good_2_of_2() { + run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 2, 2); + } + + @Test + public void test_OnResolve_Good_3_of_3() { + run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 3, 3); + } + + @Test + public void test_OnResolve_Good_3_of_10() { + run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 3, 10); + } + + @Test + public void test_OnResolve_Bad_0_of_0() { + run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 0); + } + + @Test + public void test_OnResolve_Bad_0_of_1() { + run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 1); + } + + @Test + public void test_OnResolve_Bad_0_of_2() { + run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 2); + } + + @Test + public void test_OnResolve_Bad_0_of_3() { + run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 3); + } + + @Test + public void test_OnResolve_Bad_0_of_10() { + run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 10); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnResolveTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnResolveTest.java new file mode 100644 index 00000000000..5fe8e70d2d4 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionOnResolveTest.java @@ -0,0 +1,91 @@ +package org.mage.test.cards.targets; + +import org.junit.Test; + +/** + * Testing targets selection on resolve (player.choose) + *

+ * Player can use any logic and choose any number of targets + * + * @author JayDi85 + */ +public class TargetsSelectionOnResolveTest extends TargetsSelectionBaseTest { + + // no selects + + @Test + public void test_OnActivate_0_of_0() { + run_PlayerChoose_OnResolve(0, 0); + } + + @Test + public void test_OnActivate_0_of_1() { + run_PlayerChoose_OnResolve(0, 1); + } + + @Test + public void test_OnActivate_0_of_2() { + run_PlayerChoose_OnResolve(0, 2); + } + + @Test + public void test_OnActivate_0_of_3() { + run_PlayerChoose_OnResolve(0, 3); + } + + @Test + public void test_OnActivate_0_of_10() { + run_PlayerChoose_OnResolve(0, 10); + } + + // 1 select + + @Test + public void test_OnActivate_1_of_1() { + run_PlayerChoose_OnResolve(1, 1); + } + + @Test + public void test_OnActivate_1_of_2() { + run_PlayerChoose_OnResolve(1, 2); + } + + @Test + public void test_OnActivate_1_of_3() { + run_PlayerChoose_OnResolve(1, 3); + } + + @Test + public void test_OnActivate_1_of_10() { + run_PlayerChoose_OnResolve(1, 10); + } + + // 2 selects + + @Test + public void test_OnActivate_2_of_2() { + run_PlayerChoose_OnResolve(2, 2); + } + + @Test + public void test_OnActivate_2_of_3() { + run_PlayerChoose_OnResolve(2, 3); + } + + @Test + public void test_OnActivate_2_of_10() { + run_PlayerChoose_OnResolve(2, 10); + } + + // 3 selects + + @Test + public void test_OnActivate_3_of_3() { + run_PlayerChoose_OnResolve(3, 3); + } + + @Test + public void test_OnActivate_3_of_10() { + run_PlayerChoose_OnResolve(3, 10); + } +} 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 0b1b861f318..f334f88dba2 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 @@ -3,6 +3,7 @@ package org.mage.test.cards.triggers; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -23,6 +24,7 @@ public class EnterLeaveBattlefieldExileTargetTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angel of Serenity"); addTarget(playerA, "Silvercoat Lion^Pillarfield Ox"); + addTarget(playerA, TestPlayer.TARGET_SKIP); setChoice(playerA, true); setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/WorldgorgerDragonTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/WorldgorgerDragonTest.java index 3fbfb6678a6..648086e1d78 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/WorldgorgerDragonTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/WorldgorgerDragonTest.java @@ -7,7 +7,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class WorldgorgerDragonTest extends CardTestPlayerBase { @@ -83,62 +82,48 @@ public class WorldgorgerDragonTest extends CardTestPlayerBase { // When Staunch Defenders enters the battlefield, you gain 4 life. addCard(Zone.BATTLEFIELD, playerA, "Staunch Defenders", 1); + // 1 cast and resolve castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Animate Dead", "Worldgorger Dragon"); + setChoice(playerA, "Worldgorger Dragon"); // attach + setChoice(playerA, "When {this} enters, if it's"); // x2 triggers (gain life from Staunch Defenders and etb from Animate Dead) + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3); + + // 2 etb and attach setChoice(playerA, "Worldgorger Dragon"); setChoice(playerA, "When {this} enters, if it's"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3); + // 3 etb and attach setChoice(playerA, "Worldgorger Dragon"); setChoice(playerA, "When {this} enters, if it's"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3); + // 4 etb and attach setChoice(playerA, "Worldgorger Dragon"); setChoice(playerA, "When {this} enters, if it's"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3); + // 5 etb and attach + setChoice(playerA, "Worldgorger Dragon"); + setChoice(playerA, false); // no draws on infinite loop + setChoice(playerA, "When {this} enters, if it's"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3); + + // 6 etb and attach setChoice(playerA, "Worldgorger Dragon"); setChoice(playerA, "When {this} enters, if it's"); - setChoice(playerA, false); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3); - setChoice(playerA, "Worldgorger Dragon"); - setChoice(playerA, "When {this} enters, if it's"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3 - 1); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - - setChoice(playerA, "Worldgorger Dragon"); - setChoice(playerA, "When {this} enters, if it's"); - - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - - setChoice(playerA, "Worldgorger Dragon"); - setChoice(playerA, "When {this} enters, if it's"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volcanic Geyser", playerB, 22); + // cast spell and stop infinite loop after 20+ mana in pool + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volcanic Geyser", playerB, 20); setChoice(playerA, "X=20"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - assertLife(playerA, 44); + assertLife(playerA, 20 + 5 * 4); assertLife(playerB, 0); assertGraveyardCount(playerA, "Volcanic Geyser", 1); @@ -154,12 +139,11 @@ public class WorldgorgerDragonTest extends CardTestPlayerBase { * you choose to skip or pick a different creature, it always returns the * first creature you picked. Kind of hard to explain, but here's how to * reproduce: - * + *

* 1) Cast Animate Dead, targeting Worldgorger Dragon 2) Worldgorger Dragon * will exile Animate Dead, killing the dragon and returning the permanents * 3) Select Worldgorger again 4) Step 2 repeats 5) Attempt to select a * different creature. Worldgorger Dragon is returned instead. - * */ @Test public void testWithAnimateDeadDifferentTargets() { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/VengefulTownsfolkTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/VengefulTownsfolkTest.java index 9f63da3acc7..2e36053fdb4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/VengefulTownsfolkTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/VengefulTownsfolkTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.triggers.dies; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -159,6 +160,7 @@ public class VengefulTownsfolkTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three"); setChoice(playerA, "Angel of the God-Pharaoh"); // sac cost + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -183,6 +185,7 @@ public class VengefulTownsfolkTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three"); setChoice(playerA, "Grizzly Bears^Angel of the God-Pharaoh"); // sac cost + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 62b2826a382..34d1fd417ab 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -70,7 +70,7 @@ public class TestPlayer implements Player { private static final Logger LOGGER = Logger.getLogger(TestPlayer.class); - private static final int takeMaxTargetsPerChoose = Integer.MAX_VALUE; // TODO: set 1, fix broken tests and replace all "for (String targetDefinition" by targets.get(0) + private static final int takeMaxTargetsPerChoose = Integer.MAX_VALUE; // TODO: set 1 here, fix broken tests and replace all "for (String targetDefinition" by targets.get(0) public static final String TARGET_SKIP = "[target_skip]"; // stop/skip targeting public static final String CHOICE_SKIP = "[choice_skip]"; // stop/skip choice @@ -113,7 +113,7 @@ public class TestPlayer implements Player { // (example: card call TestPlayer's choice, but it uses another choices, see docs in TestComputerPlayer) private boolean strictChooseMode = false; - private String[] groupsForTargetHandling = null; + private String[] groupsForTargetHandling = null; // predefined targets list from cast/activate command // Tracks the initial turns (turn 0s) both players are given at the start of the game. // Before actual turns start. Needed for checking attacker/blocker legality in the tests @@ -516,7 +516,7 @@ public class TestPlayer implements Player { } } for (UUID id : currentTarget.possibleTargets(ability.getControllerId(), ability, game)) { - if (!currentTarget.getTargets().contains(id)) { + if (!currentTarget.contains(id)) { MageObject object = game.getObject(id); if (object == null) { @@ -594,7 +594,7 @@ public class TestPlayer implements Player { } // fake test ability for triggers and events - Ability source = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); + Ability source = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("fake ability")); source.setControllerId(this.getId()); int numberOfActions = actions.size(); @@ -2099,8 +2099,18 @@ public class TestPlayer implements Player { return "Ability: null"; } - private String getInfo(Target o, Game game) { - return "Target: " + (o != null ? o.getClass().getSimpleName() + ": " + o.getMessage(game) : "null"); + private String getInfo(Target target, Ability source, Game game) { + if (target == null) { + return "Target: null"; + } + UUID abilityControllerId = getId(); + if (target.getTargetController() != null && target.getAbilityController() != null) { + abilityControllerId = target.getAbilityController(); + } + Set possibleTargets = target.possibleTargets(abilityControllerId, source, game); + + return "Target: selected " + target.getSize() + ", possible " + possibleTargets.size() + + ", " + target.getClass().getSimpleName() + ": " + target.getMessage(game); } private void assertAliasSupportInChoices(boolean methodSupportAliases) { @@ -2211,7 +2221,7 @@ public class TestPlayer implements Player { // skip choices if (possibleChoice.equals(CHOICE_SKIP)) { choices.remove(0); - return true; + return false; // false - stop to choose } if (choice.setChoiceByAnswers(choices, true)) { @@ -2264,27 +2274,28 @@ public class TestPlayer implements Player { abilityControllerId = target.getAbilityController(); } + // TODO: warning, some cards call player.choose methods instead target.choose, see #8254 + // most use cases - discard and other cost with choice like that method + // must migrate all choices.remove(xxx) to choices.remove(0), takeMaxTargetsPerChoose can help to find it + // ignore player select if (target.getMessage(game).equals("Select a starting player")) { return computerPlayer.choose(outcome, target, source, game, options); } + boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish + assertAliasSupportInChoices(true); if (!choices.isEmpty()) { // skip choices - if (choices.get(0).equals(CHOICE_SKIP)) { + if (tryToSkipSelection(choices, CHOICE_SKIP)) { Assert.assertTrue("found skip choice, but it require more choices, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - choices.remove(0); - return true; + return false; // false - stop to choose } - List usedChoices = new ArrayList<>(); - List usedTargets = new ArrayList<>(); - - // TODO: Allow to choose a player with TargetPermanentOrPlayer if ((target.getOriginalTarget() instanceof TargetPermanent) || (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)) { // player target not implemented yet @@ -2294,10 +2305,13 @@ public class TestPlayer implements Player { } else { filterPermanent = ((TargetPermanent) target.getOriginalTarget()).getFilter(); } - while (!choices.isEmpty()) { + while (!choices.isEmpty()) { // TODO: remove cycle after main commits + if (tryToSkipSelection(choices, CHOICE_SKIP)) { + return false; // stop dialog + } String choiceRecord = choices.get(0); String[] targetList = choiceRecord.split("\\^"); - boolean targetFound = false; + isAddedSomething = false; for (String targetName : targetList) { boolean originOnly = false; boolean copyOnly = false; @@ -2312,14 +2326,14 @@ public class TestPlayer implements Player { } } for (Permanent permanent : game.getBattlefield().getActivePermanents(filterPermanent, abilityControllerId, source, game)) { - if (target.getTargets().contains(permanent.getId())) { + if (target.contains(permanent.getId())) { continue; } if (hasObjectTargetNameOrAlias(permanent, targetName)) { if (target.isNotTarget() || target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { target.add(permanent.getId(), game); - targetFound = true; + isAddedSomething = true; break; } } @@ -2327,7 +2341,7 @@ public class TestPlayer implements Player { if (target.isNotTarget() || target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { target.add(permanent.getId(), game); - targetFound = true; + isAddedSomething = true; break; } } @@ -2335,51 +2349,50 @@ public class TestPlayer implements Player { } } - if (!targetFound) { - //failOnLastBadChoice(game, source, target, choiceRecord, "unknown or can't target"); - } - try { - if (target.isChosen(game)) { - return true; - } else { - // TODO: move check above and fix all fail tests (not after target.isChosen) - if (!targetFound) { - failOnLastBadChoice(game, source, target, choiceRecord, "selected, but not all required targets"); + if (isAddedSomething) { + if (target.isChoiceCompleted(abilityControllerId, source, game)) { + return true; } + } else { + failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); } } finally { choices.remove(0); } - } + return isAddedSomething; + } // choices } if (target instanceof TargetPlayer) { - while (!choices.isEmpty()) { + while (!choices.isEmpty()) { // TODO: remove cycle after main commits + if (tryToSkipSelection(choices, CHOICE_SKIP)) { + return false; // stop dialog + } String choiceRecord = choices.get(0); - boolean targetFound = false; + isAddedSomething = false; for (Player player : game.getPlayers().values()) { if (player.getName().equals(choiceRecord)) { - if (target.canTarget(abilityControllerId, player.getId(), null, game) && !target.getTargets().contains(player.getId())) { + if (target.canTarget(abilityControllerId, player.getId(), null, game) && !target.contains(player.getId())) { target.add(player.getId(), game); - targetFound = true; - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "can't target"); + isAddedSomething = true; } } } try { - if (target.isChosen(game)) { - return true; - } - if (!targetFound) { - failOnLastBadChoice(game, source, target, choiceRecord, "unknown target"); + if (isAddedSomething) { + if (target.isChoiceCompleted(abilityControllerId, source, game)) { + return true; + } + } else { + failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); } } finally { choices.remove(0); } - } + return isAddedSomething; + } // while choices } // TODO: add same choices fixes for other target types (one choice must uses only one time for one target) @@ -2388,102 +2401,78 @@ public class TestPlayer implements Player { // only unique targets //TargetCard targetFull = ((TargetCard) target); - usedChoices.clear(); - usedTargets.clear(); - boolean targetCompleted = false; - - CheckAllChoices: - for (int choiceIndex = 0; choiceIndex < choices.size(); choiceIndex++) { - String choiceRecord = choices.get(choiceIndex); - if (targetCompleted) { - break CheckAllChoices; + for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits + if (tryToSkipSelection(choices, CHOICE_SKIP)) { + return false; // stop dialog } - - boolean targetFound = false; + isAddedSomething = false; String[] possibleChoices = choiceRecord.split("\\^"); - CheckOneChoice: for (String possibleChoice : possibleChoices) { Set possibleCards = target.possibleTargets(abilityControllerId, source, game); - CheckTargetsList: for (UUID targetId : possibleCards) { MageObject targetObject = game.getCard(targetId); if (hasObjectTargetNameOrAlias(targetObject, possibleChoice)) { - if (target.canTarget(targetObject.getId(), game)) { - // only unique targets - if (usedTargets.contains(targetObject.getId())) { - continue; - } - - // OK, can use it + if (target.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) { target.add(targetObject.getId(), game); - targetFound = true; - usedTargets.add(targetObject.getId()); - - // break on full targets list - if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { - targetCompleted = true; - break CheckOneChoice; - } - - // restart search - break CheckTargetsList; + isAddedSomething = true; + break; } } } } - - if (targetFound) { - usedChoices.add(choiceIndex); - } - } - - // apply only on ALL targets or revert - if (usedChoices.size() > 0) { - if (target.isChosen(game)) { - // remove all used choices - for (int i = choices.size(); i >= 0; i--) { - if (usedChoices.contains(i)) { - choices.remove(i); + try { + if (isAddedSomething) { + if (target.isChoiceCompleted(abilityControllerId, source, game)) { + return true; } + } else { + failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); } - return true; - } else { - Assert.fail("Not full targets list."); - target.clearChosen(); + } finally { + choices.remove(0); } - } + return isAddedSomething; + } // for choices } if (target.getOriginalTarget() instanceof TargetSource) { - Set possibleTargets; TargetSource t = ((TargetSource) target.getOriginalTarget()); - possibleTargets = t.possibleTargets(abilityControllerId, source, game); - for (String choiceRecord : choices) { + Set possibleTargets = t.possibleTargets(abilityControllerId, source, game); + // TODO: enable choices.get first instead all + for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits + if (tryToSkipSelection(choices, CHOICE_SKIP)) { + return false; // stop dialog + } String[] targetList = choiceRecord.split("\\^"); - boolean targetFound = false; + isAddedSomething = false; for (String targetName : targetList) { for (UUID targetId : possibleTargets) { MageObject targetObject = game.getObject(targetId); if (targetObject != null) { if (hasObjectTargetNameOrAlias(targetObject, targetName)) { - List alreadyTargetted = target.getTargets(); - if (t.canTarget(targetObject.getId(), game)) { - if (alreadyTargetted != null && !alreadyTargetted.contains(targetObject.getId())) { - target.add(targetObject.getId(), game); - choices.remove(choiceRecord); - targetFound = true; - } + if (t.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) { + target.add(targetObject.getId(), game); + isAddedSomething = true; + break; } } } - if (targetFound) { - choices.remove(choiceRecord); - return true; - } } } - } + try { + if (isAddedSomething) { + if (target.isChoiceCompleted(abilityControllerId, source, game)) { + return true; + } + } else { + failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); + } + } finally { + choices.remove(choiceRecord); + } + return isAddedSomething; + } // for choices } // TODO: enable fail checks and fix tests @@ -2492,7 +2481,7 @@ public class TestPlayer implements Player { } } - this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, game)); + this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game)); return computerPlayer.choose(outcome, target, source, game, options); } @@ -2535,7 +2524,7 @@ public class TestPlayer implements Player { + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); targets.remove(0); - return true; + return false; // false - stop to choose } Set targetCardZonesChecked = new HashSet<>(); // control miss implementation @@ -2594,7 +2583,7 @@ public class TestPlayer implements Player { } for (Permanent permanent : game.getBattlefield().getActivePermanents((FilterPermanent) filter, abilityControllerId, source, game)) { if (hasObjectTargetNameOrAlias(permanent, targetName) || (permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove exp code search? - if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) { + if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) { if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { target.addTarget(permanent.getId(), source, game); targetFound = true; @@ -2624,7 +2613,7 @@ public class TestPlayer implements Player { for (String targetName : targetList) { for (Card card : computerPlayer.getHand().getCards(((TargetCard) target.getOriginalTarget()).getFilter(), game)) { if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search? - if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) { + if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) { target.addTarget(card.getId(), source, game); targetFound = true; break; // return to next targetName @@ -2662,7 +2651,7 @@ public class TestPlayer implements Player { for (String targetName : targetList) { for (Card card : game.getExile().getCards(filter, game)) { if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search? - if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) { + if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) { target.addTarget(card.getId(), source, game); targetFound = true; break; // return to next targetName @@ -2687,7 +2676,7 @@ public class TestPlayer implements Player { for (String targetName : targetList) { for (Card card : game.getBattlefield().getAllActivePermanents()) { if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search? - if (targetFull.canTarget(abilityControllerId, card.getId(), source, game) && !targetFull.getTargets().contains(card.getId())) { + if (targetFull.canTarget(abilityControllerId, card.getId(), source, game) && !targetFull.contains(card.getId())) { targetFull.add(card.getId(), game); targetFound = true; break; // return to next targetName @@ -2739,7 +2728,7 @@ public class TestPlayer implements Player { Player player = game.getPlayer(playerId); for (Card card : player.getGraveyard().getCards(targetFull.getFilter(), game)) { if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search? - if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) { + if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) { target.addTarget(card.getId(), source, game); targetFound = true; break IterateGraveyards; // return to next targetName @@ -2754,7 +2743,6 @@ public class TestPlayer implements Player { return true; } } - } // stack @@ -2769,7 +2757,7 @@ public class TestPlayer implements Player { for (String targetName : targetList) { for (StackObject stackObject : game.getStack()) { if (hasObjectTargetNameOrAlias(stackObject, targetName)) { - if (target.canTarget(abilityControllerId, stackObject.getId(), source, game) && !target.getTargets().contains(stackObject.getId())) { + if (target.canTarget(abilityControllerId, stackObject.getId(), source, game) && !target.contains(stackObject.getId())) { target.addTarget(stackObject.getId(), source, game); targetFound = true; break; // return to next targetName @@ -2804,24 +2792,25 @@ public class TestPlayer implements Player { // how to fix: implement target class processing above (if it a permanent target then check "filter instanceof" code too) if (!targets.isEmpty()) { String message; + Set possibleTargets = target.possibleTargets(abilityControllerId, source, game); if (source != null) { message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + "\nCard: " + source.getSourceObject(game) + "\nAbility: " + source.getClass().getSimpleName() + " (" + source.getRule() + ")" - + "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + + "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets"; } else { message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + "\nCard: unknown source" + "\nAbility: unknown source" - + "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + + "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets"; } Assert.fail(message); } - this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, game)); + this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, source, game)); return computerPlayer.chooseTarget(outcome, target, source, game); } @@ -2840,7 +2829,7 @@ public class TestPlayer implements Player { + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); targets.remove(0); - return true; + return false; // false - stop to choose } for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) { String[] targetList = targetDefinition.split("\\^"); @@ -2848,7 +2837,7 @@ public class TestPlayer implements Player { for (String targetName : targetList) { for (Card card : cards.getCards(game)) { if (hasObjectTargetNameOrAlias(card, targetName) - && !target.getTargets().contains(card.getId()) + && !target.contains(card.getId()) && target.canTarget(abilityControllerId, card.getId(), source, cards, game)) { target.addTarget(card.getId(), source, game); targetFound = true; @@ -2867,7 +2856,7 @@ public class TestPlayer implements Player { LOGGER.warn("Wrong target"); } - this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, game)); + this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, source, game)); return computerPlayer.chooseTarget(outcome, cards, target, source, game); } @@ -2930,7 +2919,7 @@ public class TestPlayer implements Player { public int announceXMana(int min, int max, String message, Game game, Ability ability) { assertAliasSupportInChoices(false); if (!choices.isEmpty()) { - for (String choice : choices) { + for (String choice : new ArrayList<>(choices)) { if (choice.startsWith("X=")) { int xValue = Integer.parseInt(choice.substring(2)); assertXMinMaxValue(game, ability, xValue, min, max); @@ -2941,7 +2930,7 @@ public class TestPlayer implements Player { } this.chooseStrictModeFailed("choice", game, getInfo(ability, game) - + "\nMessage: " + message + prepareXMaxInfo(min, max)); + + "\nMessage: " + message + prepareXMaxInfo(min, max)); return computerPlayer.announceXMana(min, max, message, game, ability); } @@ -4276,32 +4265,40 @@ public class TestPlayer implements Player { return choose(outcome, target, source, game, null); } + private boolean tryToSkipSelection(List selections, String selectionMark) { + if (!selections.isEmpty() && selections.get(0).equals(selectionMark)) { + selections.remove(0); + return true; + } + return false; + } + @Override public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { assertAliasSupportInChoices(false); if (!choices.isEmpty()) { // skip choices - if (choices.get(0).equals(CHOICE_SKIP)) { - choices.remove(0); + if (tryToSkipSelection(choices, CHOICE_SKIP)) { if (cards.isEmpty()) { // cancel button forced in GUI on no possible choices + // TODO: need research return false; } else { Assert.assertTrue("found skip choice, but it require more choices, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - return true; + return false; // stop dialog } } - for (String choose2 : choices) { + for (String choose2 : new ArrayList<>(choices)) { // TODO: More targetting to fix String[] targetList = choose2.split("\\^"); boolean targetFound = false; for (String targetName : targetList) { for (Card card : cards.getCards(game)) { - if (target.getTargets().contains(card.getId())) { + if (target.contains(card.getId())) { continue; } if (hasObjectTargetNameOrAlias(card, targetName)) { @@ -4322,7 +4319,7 @@ public class TestPlayer implements Player { assertWrongChoiceUsage(choices.size() > 0 ? choices.get(0) : "empty list"); } - this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, game)); + this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game)); return computerPlayer.choose(outcome, cards, target, source, game); } @@ -4344,7 +4341,7 @@ public class TestPlayer implements Player { + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); targets.remove(0); - return false; // false in chooseTargetAmount = stop to choose + return false; // false - stop to choose } // only target amount needs @@ -4384,7 +4381,7 @@ public class TestPlayer implements Player { } if (foundTarget) { - if (!target.getTargets().contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) { + if (!target.contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) { // can select target.addTarget(possibleTarget, targetAmount, source, game); targets.remove(0); @@ -4395,7 +4392,7 @@ public class TestPlayer implements Player { } } - this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, game)); + this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, source, game)); return computerPlayer.chooseTargetAmount(outcome, target, source, game); } @@ -4744,7 +4741,7 @@ public class TestPlayer implements Player { Assert.fail(String.format("Found wrong choice command (%s):\n%s\n%s\n%s", reason, lastChoice, - getInfo(target, game), + getInfo(target, source, game), getInfo(source, game) )); } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 228e51880ab..014e2fb865e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -6,7 +6,6 @@ import mage.ObjectColor; import mage.abilities.Ability; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffectsList; -import mage.abilities.effects.Effect; import mage.cards.Card; import mage.cards.decks.Deck; import mage.cards.decks.DeckCardLists; @@ -32,7 +31,6 @@ import mage.players.Player; import mage.server.game.GameSessionPlayer; import mage.util.CardUtil; import mage.util.ThreadUtils; -import mage.utils.StreamUtils; import mage.utils.SystemUtil; import mage.view.GameView; import org.junit.Assert; @@ -1677,7 +1675,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void assertChoicesCount(TestPlayer player, int count) throws AssertionError { String mes = String.format( - "(Choices of %s) Count are not equal (found %s). Some inner choose dialogs can be set up only in strict mode.", + "(Choices of %s) Count are not equal (found %s). Make sure you use target.chooseXXX instead player.choose. Also some inner choose dialogs can be set up only in strict mode.", player.getName(), player.getChoices() ); @@ -1686,7 +1684,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void assertTargetsCount(TestPlayer player, int count) throws AssertionError { String mes = String.format( - "(Targets of %s) Count are not equal (found %s). Some inner choose dialogs can be set up only in strict mode.", + "(Targets of %s) Count are not equal (found %s). Make sure you use target.chooseXXX instead player.choose. Also some inner choose dialogs can be set up only in strict mode.", player.getName(), player.getTargets() ); @@ -2271,11 +2269,18 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement setChoice(player, choice ? "Yes" : "No", timesToChoose); } + /** + * Declare non target choice. You can use multiple choices in one line like setChoice(name1^name2) + * Also support "up to" choices, e.g. choose 2 of 3 cards by setChoice(card1^card2) + setChoice(TestPlayer.CHOICE_SKIP) + */ public void setChoice(TestPlayer player, String choice) { setChoice(player, choice, 1); } public void setChoice(TestPlayer player, String choice, int timesToChoose) { + if (choice.equals(TestPlayer.TARGET_SKIP)) { + Assert.fail("setChoice allow only TestPlayer.CHOICE_SKIP, but found " + choice); + } for (int i = 0; i < timesToChoose; i++) { player.addChoice(choice); } @@ -2340,13 +2345,18 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * * @param player * @param target you can add multiple targets by separating them by the "^" - * character e.g. "creatureName1^creatureName2" you can - * qualify the target additional by setcode e.g. + * character e.g. "creatureName1^creatureName2" + * - + * you can qualify the target additional by setcode e.g. * "creatureName-M15" you can add [no copy] to the end of the * target name to prohibit targets that are copied you can add * [only copy] to the end of the target name to allow only * targets that are copies. For modal spells use a prefix with * the mode number: mode=1Lightning Bolt^mode=2Silvercoat Lion + * - + * it's also support multiple addTarget commands instead single line, + * so you can declare not full "up to" targets list by addTarget(name) + * and addTarget(TestPlayer.TARGET_SKIP) */ // TODO: mode options doesn't work here (see BrutalExpulsionTest) public void addTarget(TestPlayer player, String target) { @@ -2354,6 +2364,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } public void addTarget(TestPlayer player, String target, int timesToChoose) { + if (target.equals(TestPlayer.CHOICE_SKIP)) { + Assert.fail("addTarget allow only TestPlayer.TARGET_SKIP, but found " + target); + } + for (int i = 0; i < timesToChoose; i++) { assertAliaseSupportInActivateCommand(target, true); player.addTarget(target); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java index 1a7d63ed24d..1bf82497179 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java @@ -55,7 +55,7 @@ public class LoadCheatsTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - setChoice(playerA, "7"); // choose [group 3]: 7 = 4 default menus + 3 group + setChoice(playerA, "8"); // choose [group 3]: 8 = 5 default menus + 3 group SystemUtil.executeCheatCommands(currentGame, commandsFile, playerA); assertHandCount(playerA, "Razorclaw Bear", 1); diff --git a/Mage/src/main/java/mage/abilities/costs/common/CollectEvidenceCost.java b/Mage/src/main/java/mage/abilities/costs/common/CollectEvidenceCost.java index f8ceb580131..04cc1ee9195 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/CollectEvidenceCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/CollectEvidenceCost.java @@ -89,7 +89,7 @@ public class CollectEvidenceCost extends CostImpl { ); } }.withNotTarget(true); - player.choose(Outcome.Exile, target, source, game); + target.choose(Outcome.Exile, player.getId(), source.getSourceId(), source, game); Cards cards = new CardsImpl(target.getTargets()); paid = cards .getCards(game) diff --git a/Mage/src/main/java/mage/abilities/effects/common/SacrificeAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/SacrificeAllEffect.java index d426ccf9627..62b51de5666 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/SacrificeAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/SacrificeAllEffect.java @@ -91,9 +91,7 @@ public class SacrificeAllEffect extends OneShotEffect { continue; } TargetSacrifice target = new TargetSacrifice(numTargets, filter); - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.choose(Outcome.Sacrifice, target, source, game); - } + target.choose(Outcome.Sacrifice, player.getId(), source, game); perms.addAll(target.getTargets()); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/SacrificeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/SacrificeEffect.java index 034cff1cd46..c9a396d98b4 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/SacrificeEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/SacrificeEffect.java @@ -64,13 +64,12 @@ public class SacrificeEffect extends OneShotEffect { continue; } TargetSacrifice target = new TargetSacrifice(amount, filter); - while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) { - player.choose(Outcome.Sacrifice, target, source, game); - } - for (UUID targetId : target.getTargets()) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && permanent.sacrifice(source, game)) { - applied = true; + if (target.choose(Outcome.Sacrifice, player.getId(), source.getSourceId(), source, game)) { + for (UUID targetId : target.getTargets()) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null && permanent.sacrifice(source, game)) { + applied = true; + } } } } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 3904856a059..fc4269a71e9 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1522,7 +1522,7 @@ public abstract class GameImpl implements Game { UUID[] players = getPlayers().keySet().toArray(new UUID[0]); UUID playerId; while (!hasEnded()) { - playerId = players[RandomUtil.nextInt(players.length)]; + playerId = players[RandomUtil.nextInt(players.length)]; // test game Player player = getPlayer(playerId); if (player != null && player.canRespond()) { fireInformEvent(state.getPlayer(playerId).getLogName() + " won the toss"); @@ -1810,14 +1810,21 @@ public abstract class GameImpl implements Game { protected void resolve() { StackObject top = null; + boolean wasError = false; try { top = state.getStack().peek(); top.resolve(this); resetControlAfterSpellResolve(top.getId()); + } catch (Throwable e) { + // workaround to show real error in tests instead checkInfiniteLoop + wasError = true; + throw e; } finally { if (top != null) { state.getStack().remove(top, this); // seems partly redundant because move card from stack to grave is already done and the stack removed - checkInfiniteLoop(top.getSourceId()); + if (!wasError) { + checkInfiniteLoop(top.getSourceId()); + } if (!getTurn().isEndTurnRequested()) { while (state.hasSimultaneousEvents()) { state.handleSimultaneousEvent(this); @@ -3746,7 +3753,7 @@ public abstract class GameImpl implements Game { @Override public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard, List command, List exiled) { // fake test ability for triggers and events - Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); + Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("fake ability")); fakeSourceAbilityTemplate.setControllerId(ownerId); Player player = getPlayer(ownerId); diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index c4f428eeb57..65d336cdf1a 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -458,10 +458,14 @@ public final class ZonesHandler { target.setRequired(true); while (player.canRespond() && cards.size() > 1) { player.choose(Outcome.Neutral, cards, target, source, game); - UUID targetObjectId = target.getFirstTarget(); - order.add(cards.get(targetObjectId, game)); - cards.remove(targetObjectId); - target.clearChosen(); + Card card = cards.get(target.getFirstTarget(), game); + if (card != null) { + order.add(card); + cards.remove(target.getFirstTarget()); + target.clearChosen(); + } else { + break; + } } order.addAll(cards.getCards(game)); return order; diff --git a/Mage/src/main/java/mage/players/Library.java b/Mage/src/main/java/mage/players/Library.java index fcc84fca2bb..c4fa44134fa 100644 --- a/Mage/src/main/java/mage/players/Library.java +++ b/Mage/src/main/java/mage/players/Library.java @@ -168,6 +168,8 @@ public class Library implements Serializable { } public Collection getUniqueCards(Game game) { + // TODO: on no performance issues - remove unique code after few releases, 2025-05-13 + if (true) return getCards(game); Map cards = new HashMap<>(); for (UUID cardId : library) { Card card = game.getCard(cardId); diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 987b47e37f8..ffa1ce89da7 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -672,6 +672,10 @@ public interface Player extends MageItem, Copyable { boolean priority(Game game); + /** + * Warning, any choose and chooseTarget dialogs must return false to stop choosing, e.g. no more possible targets + * Same logic as "something changes" in "apply" + */ boolean choose(Outcome outcome, Target target, Ability source, Game game); boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index fd761572785..02f2f2db436 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -886,7 +886,7 @@ public abstract class PlayerImpl implements Player, Serializable { return toDiscard; } TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId()); - choose(Outcome.Discard, target, source, game); + target.choose(Outcome.Discard, getId(), source, game); toDiscard.addAll(target.getTargets()); return toDiscard; } @@ -4472,14 +4472,14 @@ public abstract class PlayerImpl implements Player, Serializable { List options = new ArrayList<>(); if (ability.isModal()) { addModeOptions(options, ability, game); - } else if (!ability.getTargets().getUnchosen(game).isEmpty()) { + } else if (ability.getTargets().getNextUnchosen(game) != null) { // TODO: Handle other variable costs than mana costs if (!ability.getManaCosts().getVariableCosts().isEmpty()) { addVariableXOptions(options, ability, 0, game); } else { addTargetOptions(options, ability, 0, game); } - } else if (!ability.getCosts().getTargets().getUnchosen(game).isEmpty()) { + } else if (ability.getCosts().getTargets().getNextUnchosen(game) != null) { addCostTargetOptions(options, ability, 0, game); } @@ -4497,13 +4497,13 @@ public abstract class PlayerImpl implements Player, Serializable { newOption.getModes().clearSelectedModes(); newOption.getModes().addSelectedMode(mode.getId()); newOption.getModes().setActiveMode(mode); - if (!newOption.getTargets().getUnchosen(game).isEmpty()) { + if (newOption.getTargets().getNextUnchosen(game) != null) { if (!newOption.getManaCosts().getVariableCosts().isEmpty()) { addVariableXOptions(options, newOption, 0, game); } else { addTargetOptions(options, newOption, 0, game); } - } else if (!newOption.getCosts().getTargets().getUnchosen(game).isEmpty()) { + } else if (newOption.getCosts().getTargets().getNextUnchosen(game) != null) { addCostTargetOptions(options, newOption, 0, game); } else { options.add(newOption); @@ -4523,7 +4523,9 @@ public abstract class PlayerImpl implements Player, Serializable { */ protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { // TODO: target options calculated for triggered ability too, but do not used in real game - for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) { + // TODO: there are rare errors with wrong targetNum - maybe multiple game sims can change same target object somehow? + // do not hide NullPointError here, research instead + for (Target target : option.getTargets().getNextUnchosen(game, targetNum).getTargetOptions(option, game)) { Ability newOption = option.copy(); if (target instanceof TargetAmount) { for (UUID targetId : target.getTargets()) { @@ -5562,8 +5564,7 @@ public abstract class PlayerImpl implements Player, Serializable { TargetPermanent target = new TargetControlledCreaturePermanent(); target.withNotTarget(true); target.withChooseHint("to be your Ring-bearer"); - choose(Outcome.Neutral, target, null, game); - + target.choose(Outcome.Neutral, getId(), null, null, game); newBearerId = target.getFirstTarget(); } else { newBearerId = currentBearerId; diff --git a/Mage/src/main/java/mage/players/StubPlayer.java b/Mage/src/main/java/mage/players/StubPlayer.java index da4d4167d2d..dd86c1d90f2 100644 --- a/Mage/src/main/java/mage/players/StubPlayer.java +++ b/Mage/src/main/java/mage/players/StubPlayer.java @@ -42,7 +42,9 @@ public class StubPlayer extends PlayerImpl { public boolean choose(Outcome outcome, Target target, Ability source, Game game) { if (target instanceof TargetPlayer) { for (Player player : game.getPlayers().values()) { - if (player.getId().equals(getId()) && target.canTarget(getId(), game)) { + if (player.getId().equals(getId()) + && target.canTarget(getId(), game) + && !target.contains(getId())) { target.add(player.getId(), game); return true; } diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java index c639fdf9f0b..e5268c1ea98 100644 --- a/Mage/src/main/java/mage/target/Target.java +++ b/Mage/src/main/java/mage/target/Target.java @@ -24,12 +24,13 @@ public interface Target extends Copyable, Serializable { * All targets selected by a player *

* Warning, for "up to" targets it will return true all the time, so make sure your dialog - * use do-while logic and call "choose" one time min or use doneChoosing + * use do-while logic and call "choose" one time min or use isChoiceCompleted */ boolean isChosen(Game game); - // TODO: combine code or research usages (doneChoosing must be in while cycles, isChosen in other places, see #13606) - boolean doneChoosing(Game game); + boolean isChoiceCompleted(Game game); + + boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game); void clearChosen(); @@ -63,6 +64,9 @@ public interface Target extends Copyable, Serializable { */ Set possibleTargets(UUID sourceControllerId, Ability source, Game game); + /** + * Priority method to make a choice from cards and other places, not a player.chooseXXX + */ boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game); /** @@ -101,10 +105,19 @@ public interface Target extends Copyable, Serializable { Set possibleTargets(UUID sourceControllerId, Game game); + @Deprecated // TODO: need replace to source only version? boolean choose(Outcome outcome, UUID playerId, UUID sourceId, Ability source, Game game); + /** + * Priority method to make a choice from cards and other places, not a player.chooseXXX + */ + default boolean choose(Outcome outcome, UUID playerId, Ability source, Game game) { + return choose(outcome, playerId, source == null ? null : source.getSourceId(), source, game); + } + /** * Add target from non targeting methods like choose + * TODO: need usage research, looks like there are wrong usage of addTarget, e.g. in choose method (must be add) */ void add(UUID id, Game game); diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java index 37f84122b81..ccbfde6d66a 100644 --- a/Mage/src/main/java/mage/target/TargetAmount.java +++ b/Mage/src/main/java/mage/target/TargetAmount.java @@ -46,11 +46,11 @@ public abstract class TargetAmount extends TargetImpl { @Override public boolean isChosen(Game game) { - return doneChoosing(game); + return isChoiceCompleted(game); } @Override - public boolean doneChoosing(Game game) { + public boolean isChoiceCompleted(Game game) { return amountWasSet && (remainingAmount == 0 || (getMinNumberOfTargets() < getMaxNumberOfTargets() @@ -109,15 +109,16 @@ public abstract class TargetAmount extends TargetImpl { if (!amountWasSet) { setAmount(source, game); } - chosen = false; + while (remainingAmount > 0) { + chosen = false; if (!player.canRespond()) { chosen = isChosen(game); - return chosen; + break; } if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) { chosen = isChosen(game); - return chosen; + break; } chosen = isChosen(game); } diff --git a/Mage/src/main/java/mage/target/TargetCard.java b/Mage/src/main/java/mage/target/TargetCard.java index 271a91588ba..b5322d2782d 100644 --- a/Mage/src/main/java/mage/target/TargetCard.java +++ b/Mage/src/main/java/mage/target/TargetCard.java @@ -50,6 +50,12 @@ public class TargetCard extends TargetObject { return this.filter; } + @Override + public TargetCard withNotTarget(boolean notTarget) { + super.withNotTarget(notTarget); + return this; + } + /** * Checks if there are enough {@link Card cards} in the appropriate zone that the player can choose from among them * or if they are autochosen since there are fewer than the minimum number. diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java index f6bd03a7ede..8c5f1f73ea2 100644 --- a/Mage/src/main/java/mage/target/TargetImpl.java +++ b/Mage/src/main/java/mage/target/TargetImpl.java @@ -173,11 +173,14 @@ public abstract class TargetImpl implements Target { if (getMaxNumberOfTargets() != 1) { StringBuilder sb = new StringBuilder(); sb.append("Select ").append(targetName); + sb.append(" (selected ").append(targets.size()); if (getMaxNumberOfTargets() > 0 && getMaxNumberOfTargets() != Integer.MAX_VALUE) { - sb.append(" (selected ").append(targets.size()).append(" of ").append(getMaxNumberOfTargets()).append(')'); - } else { - sb.append(" (selected ").append(targets.size()).append(')'); + sb.append(" of ").append(getMaxNumberOfTargets()); } + if (getMinNumberOfTargets() > 0) { + sb.append(", min ").append(getMinNumberOfTargets()); + } + sb.append(')'); sb.append(suffix); return sb.toString(); } @@ -245,12 +248,12 @@ public abstract class TargetImpl implements Target { // limit by max amount if (getMaxNumberOfTargets() > 0 && targets.size() > getMaxNumberOfTargets()) { - return chosen; + return false; } // limit by min amount if (getMinNumberOfTargets() > 0 && targets.size() < getMinNumberOfTargets()) { - return chosen; + return false; } // all fine @@ -258,8 +261,44 @@ public abstract class TargetImpl implements Target { } @Override - public boolean doneChoosing(Game game) { - return isChoiceSelected() && isChosen(game); + @Deprecated // TODO: replace usage in cards by full version from choose methods + public boolean isChoiceCompleted(Game game) { + return isChoiceCompleted(null, null, game); + } + + @Override + public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game) { + // make sure target request called one time minimum (for "up to" targets) + // choice is selected after any addTarget call (by test, AI or human players) + if (!isChoiceSelected()) { + return false; + } + + // make sure selected targets are valid + if (!isChosen(game)) { + return false; + } + + // make sure to auto-finish on all targets selection + // - human player can select and deselect targets until fill all targets amount or press done button + // - AI player can select all new targets as much as possible + if (getMaxNumberOfTargets() > 0) { + if (getMaxNumberOfTargets() == Integer.MAX_VALUE) { + if (abilityControllerId != null && source != null) { + // any amount - nothing to choose + return this.getSize() >= this.possibleTargets(abilityControllerId, source, game).size(); + } else { + // any amount - any selected + return this.getSize() > 0; + } + } else { + // check selected limit + return this.getSize() >= getMaxNumberOfTargets(); + } + } + + // all other use cases are fine + return true; } @Override @@ -289,7 +328,7 @@ public abstract class TargetImpl implements Target { @Override public void remove(UUID id) { if (targets.containsKey(id)) { - targets.remove(id); + targets.remove(id); // TODO: miss chosen update here? zoneChangeCounters.remove(id); } } @@ -315,6 +354,8 @@ public abstract class TargetImpl implements Target { } } else { targets.put(id, 0); + rememberZoneChangeCounter(id, game); + chosen = isChosen(game); } } } @@ -366,20 +407,45 @@ public abstract class TargetImpl implements Target { return false; } + UUID abilityControllerId = playerId; + if (this.getTargetController() != null && this.getAbilityController() != null) { + abilityControllerId = this.getAbilityController(); + } + chosen = false; do { - if (!targetController.canRespond()) { - chosen = isChosen(game); - return chosen; - } - if (!targetController.choose(outcome, this, source, game)) { - chosen = isChosen(game); - return chosen; - } - chosen = isChosen(game); - } while (!doneChoosing(game)); + int prevTargetsCount = this.getTargets().size(); - return isChosen(game); + // stop by disconnect + if (!targetController.canRespond()) { + break; + } + + // stop by cancel/done + if (!targetController.choose(outcome, this, source, game)) { + break; + } + + // TODO: miss auto-choose code? see chooseTarget below + // TODO: miss random code? see chooseTarget below + + chosen = isChosen(game); + + // stop by full complete + if (isChoiceCompleted(abilityControllerId, source, game)) { + break; + } + + // stop by nothing to use (actual for human and done button) + if (prevTargetsCount == this.getTargets().size()) { + break; + } + + // can select next target + } while (true); + + chosen = isChosen(game); + return this.getTargets().size() > 0; } @Override @@ -389,44 +455,80 @@ public abstract class TargetImpl implements Target { return false; } - List possibleTargets = new ArrayList<>(possibleTargets(playerId, source, game)); + UUID abilityControllerId = playerId; + if (this.getTargetController() != null && this.getAbilityController() != null) { + abilityControllerId = this.getAbilityController(); + } + + List randomPossibleTargets = new ArrayList<>(possibleTargets(playerId, source, game)); chosen = false; do { + int prevTargetsCount = this.getTargets().size(); + + // stop by disconnect if (!targetController.canRespond()) { - chosen = isChosen(game); - return chosen; + break; } + + // MAKE A CHOICE if (isRandom()) { - if (possibleTargets.isEmpty()) { - chosen = isChosen(game); - return chosen; + // random choice + + // stop on nothing to choose + if (randomPossibleTargets.isEmpty()) { + break; } - // find valid target - while (!possibleTargets.isEmpty()) { - int index = RandomUtil.nextInt(possibleTargets.size()); - if (this.canTarget(playerId, possibleTargets.get(index), source, game)) { - this.addTarget(possibleTargets.get(index), source, game); - possibleTargets.remove(index); + + // add valid random target one by one + while (!randomPossibleTargets.isEmpty()) { + UUID possibleTarget = RandomUtil.randomFromCollection(randomPossibleTargets); + if (this.canTarget(playerId, possibleTarget, source, game) && !this.contains(possibleTarget)) { + this.addTarget(possibleTarget, source, game); + randomPossibleTargets.remove(possibleTarget); break; } else { - possibleTargets.remove(index); + randomPossibleTargets.remove(possibleTarget); } } + // continue to next target } else { - // Try to autochoosen + // player's choice + UUID autoChosenId = tryToAutoChoose(playerId, source, game); - if (autoChosenId != null) { + if (autoChosenId != null && !this.contains(autoChosenId)) { + // auto-choose addTarget(autoChosenId, source, game); - } else if (!targetController.chooseTarget(outcome, this, source, game)) { // If couldn't autochoose ask player - chosen = isChosen(game); - return chosen; + // continue to next target (example: auto-choose must fill min/max = 2 from 2 possible cards) + } else { + // manual + + // stop by cancel/done + if (!targetController.chooseTarget(outcome, this, source, game)) { + break; + } + + // continue to next target } } - chosen = isChosen(game); - } while (!doneChoosing(game)); - return isChosen(game); + chosen = isChosen(game); + + // stop by full complete + if (isChoiceCompleted(abilityControllerId, source, game)) { + break; + } + + // stop by nothing to choose (actual for human and done button?) + if (prevTargetsCount == this.getTargets().size()) { + break; + } + + // can select next target + } while (true); + + chosen = isChosen(game); + return this.getTargets().size() > 0; } @Override @@ -665,6 +767,7 @@ public abstract class TargetImpl implements Target { public void setTargetAmount(UUID targetId, int amount, Game game) { targets.put(targetId, amount); rememberZoneChangeCounter(targetId, game); + chosen = isChosen(game); } @Override @@ -716,9 +819,20 @@ public abstract class TargetImpl implements Target { } else { playerAutoTargetLevel = 2; } + + // freeze protection on disconnect - auto-choice works for online players only + boolean isOnline = player.canRespond(); + if (!player.isGameUnderControl()) { + Player controllingPlayer = game.getPlayer(player.getTurnControlledBy()); + if (player.isHuman()) { + isOnline = controllingPlayer.canRespond(); + } + } + String abilityText = source.getRule(true).toLowerCase(); boolean strictModeEnabled = player.getStrictChooseMode(); boolean canAutoChoose = this.getMinNumberOfTargets() == this.getMaxNumberOfTargets() // Targets must be picked + && isOnline && possibleTargets.size() == this.getMinNumberOfTargets() - this.getSize() // Available targets are equal to the number that must be picked && !strictModeEnabled // Test AI is not set to strictChooseMode(true) && playerAutoTargetLevel > 0 // Human player has enabled auto-choose in settings @@ -775,4 +889,13 @@ public abstract class TargetImpl implements Target { return null; } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + ", from " + this.getMinNumberOfTargets() + + " to " + this.getMaxNumberOfTargets() + + ", " + this.getDescription() + + ", selected " + this.getTargets().size(); + } } diff --git a/Mage/src/main/java/mage/target/Targets.java b/Mage/src/main/java/mage/target/Targets.java index ed25b10207e..3be408aa038 100644 --- a/Mage/src/main/java/mage/target/Targets.java +++ b/Mage/src/main/java/mage/target/Targets.java @@ -1,11 +1,13 @@ package mage.target; +import mage.MageObject; import mage.abilities.Ability; import mage.constants.Outcome; import mage.game.Game; import mage.game.events.GameEvent; import mage.players.Player; import mage.util.Copyable; +import mage.util.DebugUtil; import java.util.*; import java.util.stream.Collectors; @@ -38,12 +40,19 @@ public class Targets extends ArrayList implements Copyable { return this; } - public List getUnchosen(Game game) { - return stream().filter(target -> !target.isChoiceSelected()).collect(Collectors.toList()); + public Target getNextUnchosen(Game game) { + return getNextUnchosen(game, 0); } - public boolean doneChoosing(Game game) { - return stream().allMatch(t -> t.doneChoosing(game)); + public Target getNextUnchosen(Game game, int unchosenIndex) { + List res = stream() + .filter(target -> !target.isChoiceSelected()) + .collect(Collectors.toList()); + return unchosenIndex < res.size() ? res.get(unchosenIndex) : null; + } + + public boolean isChoiceCompleted(Game game) { + return stream().allMatch(t -> t.isChoiceCompleted(game)); } public void clearChosen() { @@ -62,19 +71,38 @@ public class Targets extends ArrayList implements Copyable { return false; } - if (this.size() > 0 && !this.doneChoosing(game)) { + // in test mode some targets can be predefined already, e.g. by cast/activate command + // so do not clear chosen status here + + if (this.size() > 0) { do { + // stop on disconnect or nothing to choose if (!player.canRespond() || !canChoose(playerId, source, game)) { - return false; + break; } - Target target = this.getUnchosen(game).get(0); - if (!target.choose(outcome, playerId, sourceId, source, game)) { - return false; + // stop on complete + Target target = this.getNextUnchosen(game); + if (target == null) { + break; } - } while (!doneChoosing(game)); + + // stop on cancel/done + if (!target.choose(outcome, playerId, sourceId, source, game)) { + if (!target.isChosen(game)) { + break; + } + } + + // target done, can take next one + } while (true); } - return true; + + if (DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS && !game.isSimulation()) { + printDebugTargets("choose finish", this, source, game); + } + + return isChosen(game); } public boolean chooseTargets(Outcome outcome, UUID playerId, Ability source, boolean noMana, Game game, boolean canCancel) { @@ -83,43 +111,100 @@ public class Targets extends ArrayList implements Copyable { return false; } - if (this.size() > 0 && !this.doneChoosing(game)) { + // in test mode some targets can be predefined already, e.g. by cast/activate command + // so do not clear chosen status here + + if (this.size() > 0) { do { + // stop on disconnect or nothing to choose if (!player.canRespond() || !canChoose(playerId, source, game)) { - return false; + break; } - Target target = this.getUnchosen(game).get(0); - UUID targetController = playerId; + // stop on complete + Target target = this.getNextUnchosen(game); + if (target == null) { + break; + } // some targets can have controller different than ability controller + UUID targetController = playerId; if (target.getTargetController() != null) { targetController = target.getTargetController(); } - // if cast without mana (e.g. by suspend you may not be able to cancel the casting if you are able to cast it + // disable cancel button - if cast without mana (e.g. by suspend you may not be able to cancel the casting if you are able to cast it if (noMana) { target.setRequired(true); } - - // can be cancel by user + // enable cancel button if (canCancel) { target.setRequired(false); } - // make response checks + // stop on cancel/done if (!target.chooseTarget(outcome, targetController, source, game)) { - return false; + if (!target.isChosen(game)) { + break; + } } - // Check if there are some rules for targets are violated, if so reset the targets and start again - if (this.getUnchosen(game).isEmpty() + + // reset on wrong restrictions and start from scratch + if (this.getNextUnchosen(game) == null && game.replaceEvent(new GameEvent(GameEvent.EventType.TARGETS_VALID, source.getSourceId(), source, source.getControllerId()), source)) { - //game.restoreState(state, "Targets"); clearChosen(); } - } while (!doneChoosing(game)); + + // target done, can take next one + } while (true); + } + + if (DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS && !game.isSimulation()) { + printDebugTargets("chooseTargets finish", this, source, game); + } + + return isChosen(game); + } + + public static void printDebugTargets(String name, Targets targets, Ability source, Game game) { + List output = new ArrayList<>(); + printDebugTargets(name, targets, source, game, output); + output.forEach(System.out::println); + } + + public static void printDebugTargets(String name, Targets targets, Ability source, Game game, List output) { + output.add(""); + output.add(name + ":"); + output.add(String.format("* chosen: %s", targets.isChosen(game) ? "yes" : "no")); + output.add(String.format("* ability: %s", source)); + for (int i = 0; i < targets.size(); i++) { + Target target = targets.get(i); + output.add(String.format("* target %d: %s", i + 1, target)); + if (target.getTargets().isEmpty()) { + output.add(" - no choices"); + } else { + for (int j = 0; j < target.getTargets().size(); j++) { + UUID targetId = target.getTargets().get(j); + String targetInfo; + Player targetPlayer = game.getPlayer(targetId); + if (targetPlayer != null) { + targetInfo = targetPlayer.toString(); + } else { + MageObject targetObject = game.getObject(targetId); + if (targetObject != null) { + targetInfo = targetObject.toString(); + } else { + targetInfo = "unknown " + targetId; + } + } + if (target instanceof TargetAmount) { + output.add(String.format(" - choice %d.%d: amount %d, %s", i + 1, j + 1, target.getTargetAmount(targetId), targetInfo)); + } else { + output.add(String.format(" - choice %d.%d: %s", i + 1, j + 1, targetInfo)); + } + } + } } - return true; } public boolean stillLegal(Ability source, Game game) { diff --git a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java index e3a37ebb666..cfb91500bec 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java @@ -76,20 +76,44 @@ public class TargetCardInLibrary extends TargetCard { Cards cardsId = new CardsImpl(); cards.forEach(cardsId::add); + UUID abilityControllerId = playerId; + if (this.getTargetController() != null && this.getAbilityController() != null) { + abilityControllerId = this.getAbilityController(); + } + chosen = false; do { - if (!player.canRespond()) { - chosen = isChosen(game); - return chosen; - } - if (!player.chooseTarget(outcome, cardsId, this, source, game)) { - chosen = isChosen(game); - return chosen; - } - chosen = isChosen(game); - } while (!doneChoosing(game)); + int prevTargetsCount = this.getTargets().size(); - return isChosen(game); + // stop by disconnect + if (!player.canRespond()) { + break; + } + + // stop by cancel/done + // TODO: need research - why it used chooseTarget instead choose? Need random and other options? + // someday must be replaced by player.choose (require whole tests fix from addTarget to setChoice) + if (!player.chooseTarget(outcome, cardsId, this, source, game)) { + return chosen; + } + + chosen = isChosen(game); + + // stop by full complete + if (isChoiceCompleted(abilityControllerId, source, game)) { + break; + } + + // stop by nothing to use (actual for human and done button) + if (prevTargetsCount == this.getTargets().size()) { + break; + } + + // can select next target + } while (true); + + chosen = isChosen(game); + return this.getTargets().size() > 0; } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java b/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java index ca6c0e573f2..6cc048b56ff 100644 --- a/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java +++ b/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java @@ -133,7 +133,7 @@ public class TargetPermanentOrPlayer extends TargetImpl { } } } - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { // TODO: miss source? if (permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game) && filter.match(permanent, sourceControllerId, source, game)) { count++; if (count >= this.minNumberOfTargets) { diff --git a/Mage/src/main/java/mage/util/DebugUtil.java b/Mage/src/main/java/mage/util/DebugUtil.java index 09ba1e89659..4c090a4557e 100644 --- a/Mage/src/main/java/mage/util/DebugUtil.java +++ b/Mage/src/main/java/mage/util/DebugUtil.java @@ -17,6 +17,11 @@ public class DebugUtil { public static boolean AI_ENABLE_DEBUG_MODE = false; public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount + // GAME + // print detail target info for activate/cast/trigger only, not a single choose dialog + // can be useful to debug unit tests, auto-choose or AI + public static boolean GAME_SHOW_CHOOSE_TARGET_LOGS = false; + // cards basic (card panels) public static boolean GUI_CARD_DRAW_OUTER_BORDER = false; public static boolean GUI_CARD_DRAW_INNER_BORDER = false;