other: reworked target selection: (#13638)

- WIP: AI and multi targets, human and X=0 use cases, human and impossible targets use cases;
- improved stability and shared logic (related to #13606, #11134, #11666, continue from a53eb66b58, close #13617, close #13613);
- improved test logs and debug info to show more target info on errors;
- improved test framework to support multiple addTarget calls;
- improved test framework to find bad commands order for targets (related to #11666);
- fixed game freezes on auto-choice usages with disconnected or under control players (related to #11285);
- gui, game: fixed that player doesn't mark avatar as selected/green in "up to" targeting;
- gui, game: fixed small font in some popup messages on big screens (related to #969);
- gui, game: added min targets info for target selection dialog;
- for devs: added new cheat option to call and test any game dialog (define own dialogs, targets, etc in HumanDialogsTester);
- for devs: now tests require complete an any or up to target selection by addTarget + TestPlayer.TARGET_SKIP or setChoice + TestPlayer.CHOICE_SKIP (if not all max/possible targets used);
- for devs: added detail targets info for activate/trigger/cast, can be useful to debug unit tests, auto-choose or AI (see DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS)
This commit is contained in:
Oleg Agafonov 2025-05-16 13:55:54 +04:00 committed by GitHub
parent 80d62727e1
commit 133e4fe425
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 2737 additions and 743 deletions

View file

@ -326,6 +326,13 @@ public class TestCardRenderDialog extends MageDialog {
possibleTargets.add(playerYou.getId());
}
// chosen target
Set<UUID> 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

View file

@ -216,6 +216,22 @@ public final class GamePanel extends javax.swing.JPanel {
});
}
public List<UUID> getPossibleTargets() {
if (options != null && options.containsKey("possibleTargets")) {
return (List<UUID>) options.get("possibleTargets");
} else {
return Collections.emptyList();
}
}
public Set<UUID> getChosenTargets() {
if (options != null && options.containsKey("chosenTargets")) {
return new HashSet<>((List<UUID>) 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<UUID> needChosen;
if (lastGameData.options != null && lastGameData.options.containsKey("chosenTargets")) {
needChosen = (List<UUID>) lastGameData.options.get("chosenTargets");
} else {
needChosen = new ArrayList<>();
}
Set<UUID> needChosen = lastGameData.getChosenTargets();
Set<UUID> needSelectable;
if (lastGameData.targets != null) {
@ -2021,7 +2032,7 @@ public final class GamePanel extends javax.swing.JPanel {
private void prepareSelectableWindows(
Collection<CardInfoWindowDialog> windows,
Set<UUID> needSelectable,
List<UUID> needChosen,
Set<UUID> needChosen,
PlayableObjectsList needPlayable
) {
// lookAt or reveals windows clean up on next priority, so users can see dialogs, but xmage can't restore it

View file

@ -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<UUID> possibleTargets) {
this.playerPanel.update(game, player, possibleTargets);
public final void update(GameView game, PlayerView player, Set<UUID> possibleTargets, Set<UUID> chosenTargets) {
this.playerPanel.update(game, player, possibleTargets, chosenTargets);
this.battlefieldPanel.update(player.getBattlefield());
if (this.allowViewHandCardsMenuItem != null) {
this.allowViewHandCardsMenuItem.setSelected(player.getUserData().isAllowRequestHandToAll());

View file

@ -247,7 +247,7 @@ public class PlayerPanelExt extends javax.swing.JPanel {
.orElse(0);
}
public void update(GameView game, PlayerView player, Set<UUID> possibleTargets) {
public void update(GameView game, PlayerView player, Set<UUID> possibleTargets, Set<UUID> 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());
}

View file

@ -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<String> messageData = (List<String>) 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()) {

View file

@ -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);

View file

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

View file

@ -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<String, String> 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<String> 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);

View file

@ -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);
}
}

View file

@ -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
* <p>
* 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<String> 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<String> 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<Boolean> 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));
}
}
}

View file

@ -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
* <p>
* 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<String> 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<Card> 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<String> 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<Boolean> notTargets = Arrays.asList(false, true);
List<Boolean> isYous = Arrays.asList(false, true);
List<Boolean> 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)));
}
}
}
}
}

View file

@ -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
* <p>
* 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<String> 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<String> 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<Boolean> 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
}
}
}

View file

@ -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
* <p>
* 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<String> 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 <font color=green>message</font> with html" + CardUtil.getSourceLogName(game, source);
// random piles (make sure it contain good amount of cards)
List<Card> all = new ArrayList<>(game.getCards());
Collections.shuffle(all);
List<Card> pile1 = all.stream().limit(this.pileSize1).collect(Collectors.toList());
Collections.shuffle(all);
List<Card> 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<String> 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<Boolean> 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));
}
}
}

View file

@ -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
* <p>
* 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<String> 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<String> 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<Boolean> notTargets = Arrays.asList(false, true);
List<Boolean> isYous = Arrays.asList(false, true);
List<Boolean> isPlayerChoices = Arrays.asList(false, true);
List<Boolean> 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)));
*/
}
}
}
}
}
}

View file

@ -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
* <p>
* 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<String> 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<String> result = new ArrayList<>();
result.add(chooseRes ? "TRUE" : "FALSE");
return result;
}
static public void register(TestableDialogsRunner runner) {
List<Boolean> isYous = Arrays.asList(false, true);
String trueButton = "true button";
String falseButton = "false button";
String mainMessage = "main <font color=green>message</font> with html";
String additionalMessage = "additional main <font color=red>message</font> 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));
}
}
}

View file

@ -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
* <p>
* 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<String> showDialog(Player player, Ability source, Game game, Player opponent);
void showResult(Player player, Game game, String result);
}

View file

@ -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
* <p>
* 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
* <p>
* All existing human's choose dialogs (search by waitForResponse):
* <p>
* 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
* <p>
* 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)
* <p>
* Support of outdated dialogs (not used anymore)
* --- announceRepetitions (part of removed macro feature)
*
* @author JayDi85
*/
public class TestableDialogsRunner {
private final List<TestableDialog> 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<String> 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<String> resInfo = needDialog.showDialog(player, source, game, opponent);
needDialog.showResult(player, game, String.join("<br>", resInfo));
}
private Choice prepareSelectGroupChoice(List<String> 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("<br>", 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("<br>", info)
);
}
return choice;
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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;
@ -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<String, Serializable> 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<>();
}
while (canRespond()) {
Set<UUID> possibleTargetIds = target.possibleTargets(abilityControllerId, source, game);
if (possibleTargetIds == null || possibleTargetIds.isEmpty()) {
return target.getTargets().size() >= target.getMinNumberOfTargets();
// stop on completed, e.g. X=0
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
return false;
}
while (canRespond()) {
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<UUID> chosenTargets = target.getTargets();
options.put("chosenTargets", (Serializable) chosenTargets);
// stop on nothing to choose
Set<UUID> 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
// selected something
// remove selected
if (target.getTargets().contains(responseId)) {
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);
// stop on done/cancel button press
if (target.isChosen(game)) {
break;
} 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;
}
}
}
}
} 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;
// can stop at any moment
break;
}
}
}
// 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<String, Serializable> options = new HashMap<>();
while (canRespond()) {
Set<UUID> possibleTargetIds = target.possibleTargets(abilityControllerId, source, game);
Set<UUID> 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<UUID> 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<String, Serializable> getOptions(Target target, Map<String, Serializable> 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<UUID> 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<UUID> chosenTargets = target.getTargets();
List<UUID> 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<String, Serializable> options = getOptions(target, null);
options.put("chosenTargets", (Serializable) chosenTargets);
options.put("chosenTargets", (Serializable) target.getTargets());
if (!possibleTargets.isEmpty()) {
options.put("possibleTargets", (Serializable) possibleTargets);
}
@ -918,29 +919,38 @@ 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
// selected something
// remove selected
if (target.contains(responseId)) {
target.remove(responseId);
} else {
if (target.canTarget(abilityControllerId, responseId, source, cards, game)) {
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 {
if (target.getTargets().size() >= target.getMinNumberOfTargets()) {
return true;
}
// done or cancel button pressed
if (target.isChosen(game)) {
// try to finish
return false;
} else {
if (!required) {
// can stop at any moment
return false;
}
}
}
// continue to next target
}
}
return false;
}
@ -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<UUID> chosenTargets = target.getTargets();
Map<String, Serializable> 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<UUID> 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<UUID> chosenTargets = target.getTargets();
List<UUID> 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<String, Serializable> 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<TriggeredAbility> 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<? extends Card> pile1, java.util.List<? extends Card> pile2, Game game) {
if (!canCallFeedback(game)) {
return true;
return false;
}
while (canRespond()) {

View file

@ -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;
}
}
if (cards.size() == 1) {
Card card = cards.get(cards.iterator().next(), game);

View file

@ -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());

View file

@ -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);

View file

@ -82,18 +82,15 @@ 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);
}
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);
}
}
}
}
return abilityApplied;
}

View file

@ -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);
}
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent != null) {
int gainLife = permanent.getToughness().getValue();

View file

@ -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);

View file

@ -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);
}
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent != null) {
int amount = permanent.getToughness().getValue();

View file

@ -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");

View file

@ -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");

View file

@ -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);

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -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");

View file

@ -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
}

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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";

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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();

View file

@ -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);

View file

@ -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");

View file

@ -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();

View file

@ -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();

View file

@ -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);
}

View file

@ -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();

View file

@ -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();

View file

@ -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");

View file

@ -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);

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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<String> 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<String> 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;
}
}

View file

@ -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);
}
}

View file

@ -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
* <p>
* 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);
}
}

View file

@ -0,0 +1,91 @@
package org.mage.test.cards.targets;
import org.junit.Test;
/**
* Testing targets selection on resolve (player.choose)
* <p>
* 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);
}
}

View file

@ -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);

View file

@ -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:
*
* <p>
* 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() {

View file

@ -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);

View file

@ -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<UUID> 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<Integer> usedChoices = new ArrayList<>();
List<UUID> 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)) {
if (isAddedSomething) {
if (target.isChoiceCompleted(abilityControllerId, source, 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");
}
} 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)) {
if (isAddedSomething) {
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
return true;
}
if (!targetFound) {
failOnLastBadChoice(game, source, target, choiceRecord, "unknown target");
} 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<UUID> 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 {
Assert.fail("Not full targets list.");
target.clearChosen();
failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command");
}
} finally {
choices.remove(0);
}
return isAddedSomething;
} // for choices
}
if (target.getOriginalTarget() instanceof TargetSource) {
Set<UUID> possibleTargets;
TargetSource t = ((TargetSource) target.getOriginalTarget());
possibleTargets = t.possibleTargets(abilityControllerId, source, game);
for (String choiceRecord : choices) {
Set<UUID> 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<UUID> alreadyTargetted = target.getTargets();
if (t.canTarget(targetObject.getId(), game)) {
if (alreadyTargetted != null && !alreadyTargetted.contains(targetObject.getId())) {
if (t.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) {
target.add(targetObject.getId(), game);
choices.remove(choiceRecord);
targetFound = true;
isAddedSomething = true;
break;
}
}
}
}
if (targetFound) {
choices.remove(choiceRecord);
}
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<Zone> 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<UUID> 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);
@ -4276,32 +4265,40 @@ public class TestPlayer implements Player {
return choose(outcome, target, source, game, null);
}
private boolean tryToSkipSelection(List<String> 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)
));
}

View file

@ -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);

View file

@ -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);

View file

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

View file

@ -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());
}

View file

@ -64,9 +64,7 @@ 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);
}
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)) {
@ -74,6 +72,7 @@ public class SacrificeEffect extends OneShotEffect {
}
}
}
}
return applied;
}

View file

@ -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
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<Card> library, List<Card> hand, List<PutToBattlefieldInfo> battlefield, List<Card> graveyard, List<Card> command, List<Card> 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);

View file

@ -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);
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;

View file

@ -168,6 +168,8 @@ public class Library implements Serializable {
}
public Collection<Card> getUniqueCards(Game game) {
// TODO: on no performance issues - remove unique code after few releases, 2025-05-13
if (true) return getCards(game);
Map<String, Card> cards = new HashMap<>();
for (UUID cardId : library) {
Card card = game.getCard(cardId);

View file

@ -672,6 +672,10 @@ public interface Player extends MageItem, Copyable<Player> {
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<String, Serializable> options);

View file

@ -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<Ability> 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<Ability> 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;

View file

@ -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;
}

View file

@ -24,12 +24,13 @@ public interface Target extends Copyable<Target>, Serializable {
* All targets selected by a player
* <p>
* 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<Target>, Serializable {
*/
Set<UUID> 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<Target>, Serializable {
Set<UUID> 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);

View file

@ -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);
}

View file

@ -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.

View file

@ -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<UUID> possibleTargets = new ArrayList<>(possibleTargets(playerId, source, game));
UUID abilityControllerId = playerId;
if (this.getTargetController() != null && this.getAbilityController() != null) {
abilityControllerId = this.getAbilityController();
}
List<UUID> 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
UUID autoChosenId = tryToAutoChoose(playerId, source, game);
if (autoChosenId != null) {
addTarget(autoChosenId, source, game);
} else if (!targetController.chooseTarget(outcome, this, source, game)) { // If couldn't autochoose ask player
chosen = isChosen(game);
return chosen;
}
}
chosen = isChosen(game);
} while (!doneChoosing(game));
// player's choice
return isChosen(game);
UUID autoChosenId = tryToAutoChoose(playerId, source, game);
if (autoChosenId != null && !this.contains(autoChosenId)) {
// auto-choose
addTarget(autoChosenId, source, game);
// 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);
// 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();
}
}

View file

@ -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<Target> implements Copyable<Targets> {
return this;
}
public List<Target> 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<Target> 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<Target> implements Copyable<Targets> {
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);
// stop on complete
Target target = this.getNextUnchosen(game);
if (target == null) {
break;
}
// stop on cancel/done
if (!target.choose(outcome, playerId, sourceId, source, game)) {
return false;
if (!target.isChosen(game)) {
break;
}
} while (!doneChoosing(game));
}
return true;
// target done, can take next one
} while (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<Target> implements Copyable<Targets> {
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<String> 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<String> 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) {

View file

@ -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

View file

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

View file

@ -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;