From 600cac6fc7b96df81d5b77782a8bcc2c1e465846 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Sat, 17 Apr 2021 05:28:01 -0500 Subject: [PATCH] [READY FOR REVIEW] Implement a "multi-amount" dialog (#7528) * Implemented chooseTargetAmount and new GUI dialog (distribute damage, distribute mana) * Added tests and AI support; * Test framework: added aliases support in TargetAmount dialogs; Co-authored-by: Oleg Agafonov --- .../client/dialog/PickMultiNumberDialog.form | 103 +++++++ .../client/dialog/PickMultiNumberDialog.java | 215 ++++++++++++++ .../main/java/mage/client/game/GamePanel.java | 12 + .../client/remote/CallbackClientImpl.java | 12 + .../callback/ClientCallbackMethod.java | 1 + .../java/mage/view/GameClientMessage.java | 14 + .../java/mage/player/ai/ComputerPlayer.java | 24 ++ .../src/mage/player/human/HumanPlayer.java | 161 +++++++++-- .../java/mage/server/game/GameController.java | 7 + .../mage/server/game/GameSessionPlayer.java | 7 + Mage.Sets/src/mage/cards/b/Boulderfall.java | 1 - Mage.Sets/src/mage/cards/b/BurntOffering.java | 95 +------ .../mage/cards/s/SelvalaHeartOfTheWilds.java | 5 +- .../cards/single/dka/AltarOfTheLostTest.java | 14 +- .../cards/targets/TargetMultiAmountTest.java | 268 ++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 64 ++++- .../base/impl/CardTestPlayerAPIImpl.java | 14 + .../java/org/mage/test/stub/PlayerStub.java | 5 + .../AddConditionalManaOfAnyColorEffect.java | 34 ++- .../mana/AddManaInAnyCombinationEffect.java | 55 ++-- .../effects/mana/DynamicManaEffect.java | 29 +- Mage/src/main/java/mage/choices/Choices.java | 3 + .../java/mage/constants/MultiAmountType.java | 108 +++++++ Mage/src/main/java/mage/game/Game.java | 2 + Mage/src/main/java/mage/game/GameImpl.java | 8 + .../mage/game/events/PlayerQueryEvent.java | 36 ++- .../game/events/PlayerQueryEventSource.java | 4 + Mage/src/main/java/mage/players/Player.java | 13 + .../main/java/mage/players/StubPlayer.java | 6 + Mage/src/main/java/mage/target/Target.java | 4 + .../main/java/mage/target/TargetAmount.java | 24 +- .../src/main/java/mage/target/TargetImpl.java | 65 +++-- Mage/src/main/java/mage/target/Targets.java | 2 + .../target/common/TargetCardInLibrary.java | 16 +- Mage/src/main/java/mage/util/CardUtil.java | 10 + 35 files changed, 1209 insertions(+), 232 deletions(-) create mode 100644 Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.form create mode 100644 Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java create mode 100644 Mage/src/main/java/mage/constants/MultiAmountType.java diff --git a/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.form b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.form new file mode 100644 index 0000000000..a0252c10eb --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.form @@ -0,0 +1,103 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java new file mode 100644 index 0000000000..550cf72640 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java @@ -0,0 +1,215 @@ +package mage.client.dialog; + +import mage.constants.ColoredManaSymbol; +import org.mage.card.arcane.ManaSymbols; + +import javax.swing.*; +import java.awt.*; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * + * @author weirddan455 + */ +public class PickMultiNumberDialog extends MageDialog { + + private List labelList = null; + private List spinnerList = null; + + public PickMultiNumberDialog() { + initComponents(); + this.setModal(true); + } + + public void showDialog(List messages, int min, int max, Map options) { + this.header.setText((String) options.get("header")); + this.header.setHorizontalAlignment(SwingConstants.CENTER); + this.setTitle((String) options.get("title")); + + if (labelList != null) { + for (JLabel label : labelList) { + jPanel1.remove(label); + } + } + if (spinnerList != null) { + for (JSpinner spinner : spinnerList) { + jPanel1.remove(spinner); + } + } + int size = messages.size(); + labelList = new ArrayList<>(size); + spinnerList = new ArrayList<>(size); + jPanel1.setLayout(new GridBagLayout()); + GridBagConstraints labelC = new GridBagConstraints(); + GridBagConstraints spinnerC = new GridBagConstraints(); + for (int i = 0; i < size; i++) { + JLabel label = new JLabel(); + + // mana mode + String manaText = null; + String input = messages.get(i); + switch (input) { + case "W": + manaText = ColoredManaSymbol.W.getColorHtmlName(); + break; + case "U": + manaText = ColoredManaSymbol.U.getColorHtmlName(); + break; + case "B": + manaText = ColoredManaSymbol.B.getColorHtmlName(); + break; + case "R": + manaText = ColoredManaSymbol.R.getColorHtmlName(); + break; + case "G": + manaText = ColoredManaSymbol.G.getColorHtmlName(); + break; + } + if (manaText != null) { + label.setText("" + manaText); + Image image = ManaSymbols.getSizedManaSymbol(input); + if (image != null) { + label.setIcon(new ImageIcon(image)); + } + } else { + // text mode + label.setText("" + input); + } + + labelC.weightx = 0.5; + labelC.gridx = 0; + labelC.gridy = i; + jPanel1.add(label, labelC); + labelList.add(label); + + JSpinner spinner = new JSpinner(); + spinner.setModel(new SpinnerNumberModel(0, 0, max, 1)); + spinnerC.weightx = 0.5; + spinnerC.gridx = 1; + spinnerC.gridy = i; + spinnerC.ipadx = 20; + spinner.addChangeListener(e -> { + updateControls(min, max); + }); + jPanel1.add(spinner, spinnerC); + spinnerList.add(spinner); + } + this.counterText.setText("0 out of 0"); + this.counterText.setHorizontalAlignment(SwingConstants.CENTER); + + updateControls(min, max); + + this.pack(); + this.makeWindowCentered(); + this.setVisible(true); + } + + private void updateControls(int min, int max) { + int totalChosenAmount = 0; + for (JSpinner jSpinner : spinnerList) { + totalChosenAmount += ((Number) jSpinner.getValue()).intValue(); + } + counterText.setText(totalChosenAmount + " out of " + max); + chooseButton.setEnabled(totalChosenAmount >= min && totalChosenAmount <= max); + } + + public String getMultiAmount() { + return spinnerList + .stream() + .map(spinner -> ((Number) spinner.getValue()).intValue()) + .map(String::valueOf) + .collect(Collectors.joining(" ")); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + chooseButton = new javax.swing.JButton(); + header = new javax.swing.JLabel(); + counterText = new javax.swing.JLabel(); + jScrollPane1 = new javax.swing.JScrollPane(); + jPanel1 = new javax.swing.JPanel(); + + chooseButton.setText("Choose"); + chooseButton.setEnabled(false); + chooseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chooseButtonActionPerformed(evt); + } + }); + + header.setText("Header"); + + counterText.setText("Counter"); + + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 413, Short.MAX_VALUE) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 273, Short.MAX_VALUE) + ); + + jScrollPane1.setViewportView(jPanel1); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(12, 12, 12) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(header, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(counterText, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addGroup(layout.createSequentialGroup() + .addGap(184, 184, 184) + .addComponent(chooseButton) + .addGap(0, 172, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(jScrollPane1))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addComponent(header) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(counterText) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 276, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(chooseButton) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + private void chooseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseButtonActionPerformed + this.hideDialog(); + }//GEN-LAST:event_chooseButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton chooseButton; + private javax.swing.JLabel counterText; + private javax.swing.JLabel header; + private javax.swing.JPanel jPanel1; + private javax.swing.JScrollPane jScrollPane1; + // End of variables declaration//GEN-END:variables +} diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 937b2d1a81..f33661e122 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -86,6 +86,7 @@ public final class GamePanel extends javax.swing.JPanel { GamePane gamePane; private ReplayTask replayTask; private final PickNumberDialog pickNumber; + private final PickMultiNumberDialog pickMultiNumber; private JLayeredPane jLayeredPane; private String chosenHandKey = "You"; private boolean smallMode = false; @@ -134,6 +135,9 @@ public final class GamePanel extends javax.swing.JPanel { pickNumber = new PickNumberDialog(); MageFrame.getDesktop().add(pickNumber, JLayeredPane.MODAL_LAYER); + pickMultiNumber = new PickMultiNumberDialog(); + MageFrame.getDesktop().add(pickMultiNumber, JLayeredPane.MODAL_LAYER); + this.feedbackPanel.setConnectedChatPanel(this.userChatPanel); // Override layout (I can't edit generated code) @@ -238,6 +242,9 @@ public final class GamePanel extends javax.swing.JPanel { if (pickNumber != null) { pickNumber.removeDialog(); } + if (pickMultiNumber != null) { + pickMultiNumber.removeDialog(); + } for (CardInfoWindowDialog exileDialog : exiles.values()) { exileDialog.cleanUp(); exileDialog.removeDialog(); @@ -1617,6 +1624,11 @@ public final class GamePanel extends javax.swing.JPanel { } } + public void getMultiAmount(List messages, int min, int max, Map options) { + pickMultiNumber.showDialog(messages, min, max, options); + SessionHandler.sendPlayerString(gameId, pickMultiNumber.getMultiAmount()); + } + public void getChoice(Choice choice, UUID objectId) { hideAll(); // TODO: remember last choices and search incremental for same events? diff --git a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java index aeeb3fd273..8b7e42099d 100644 --- a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java +++ b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java @@ -297,6 +297,18 @@ public class CallbackClientImpl implements CallbackClient { break; } + case GAME_GET_MULTI_AMOUNT: { + GameClientMessage message = (GameClientMessage) callback.getData(); + + GamePanel panel = MageFrame.getGame(callback.getObjectId()); + if (panel != null) { + appendJsonEvent("GAME_GET_MULTI_AMOUNT", callback.getObjectId(), message); + + panel.getMultiAmount(message.getMessages(), message.getMin(), message.getMax(), message.getOptions()); + } + break; + } + case GAME_UPDATE: { GamePanel panel = MageFrame.getGame(callback.getObjectId()); diff --git a/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java b/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java index 95f21ad837..5fff7629eb 100644 --- a/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java +++ b/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java @@ -44,6 +44,7 @@ public enum ClientCallbackMethod { GAME_PLAY_MANA("gamePlayMana"), GAME_PLAY_XMANA("gamePlayXMana"), GAME_GET_AMOUNT("gameSelectAmount"), + GAME_GET_MULTI_AMOUNT("gameSelectMultiAmount"), DRAFT_INIT("draftInit"), DRAFT_PICK("draftPick"), DRAFT_UPDATE("draftUpdate"); diff --git a/Mage.Common/src/main/java/mage/view/GameClientMessage.java b/Mage.Common/src/main/java/mage/view/GameClientMessage.java index bf02f2522d..092079bf7c 100644 --- a/Mage.Common/src/main/java/mage/view/GameClientMessage.java +++ b/Mage.Common/src/main/java/mage/view/GameClientMessage.java @@ -3,6 +3,7 @@ package mage.view; import java.io.Serializable; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -42,6 +43,8 @@ public class GameClientMessage implements Serializable { private Map options; @Expose private Choice choice; + @Expose + private List messages; public GameClientMessage(GameView gameView) { this.gameView = gameView; @@ -93,6 +96,13 @@ public class GameClientMessage implements Serializable { this.message = name; } + public GameClientMessage(List messages, int min, int max, Map options) { + this.messages = messages; + this.min = min; + this.max = max; + this.options = options; + } + public GameClientMessage(Choice choice) { this.choice = choice; } @@ -145,6 +155,10 @@ public class GameClientMessage implements Serializable { return choice; } + public List getMessages() { + return messages; + } + public String toJson() { Gson gson = new GsonBuilder() .excludeFieldsWithoutExposeAnnotation() diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index b8a6c69c3f..183f47bb76 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -2028,6 +2028,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { } @Override + // TODO: add AI support with outcome and replace random with min/max public int getAmount(int min, int max, String message, Game game) { log.debug("getAmount"); if (message.startsWith("Assign damage to ")) { @@ -2039,6 +2040,29 @@ public class ComputerPlayer extends PlayerImpl implements Player { return min; } + @Override + public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + log.debug("getMultiAmount"); + + int needCount = messages.size(); + List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); + if (needCount == 0) { + return defaultList; + } + + // BAD effect + // default list uses minimum possible values, so return it on bad effect + // TODO: need something for damage target and mana logic here, current version is useless but better than random + if (!outcome.isGood()) { + return defaultList; + } + + // GOOD effect + // values must be stable, so AI must able to simulate it and choose correct actions + // fill max values as much as possible + return MultiAmountType.prepareMaxValues(needCount, min, max); + } + @Override public UUID chooseAttackerOrder(List attackers, Game game) { //TODO: improve this diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 5b85ac9c26..593f23d2e4 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -50,6 +50,7 @@ import org.apache.log4j.Logger; import java.awt.*; import java.io.Serializable; import java.util.*; +import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.stream.Collectors; @@ -668,7 +669,7 @@ public class HumanPlayer extends PlayerImpl { options.put("choosable", (Serializable) choosable); } - // if nothing to choose then show window (user must see non selectable items and click on any of them) + // if nothing to choose then show dialog (user must see non selectable items and click on any of them) if (required && choosable.isEmpty()) { required = false; } @@ -743,7 +744,7 @@ public class HumanPlayer extends PlayerImpl { options.put("choosable", (Serializable) choosable); } - // if nothing to choose then show window (user must see non selectable items and click on any of them) + // if nothing to choose then show dialog (user must see non selectable items and click on any of them) if (required && choosable.isEmpty()) { required = false; } @@ -781,6 +782,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { // choose amount + // human can choose or un-choose MULTIPLE targets at once if (gameInCheckPlayableState(game)) { return true; } @@ -790,55 +792,106 @@ public class HumanPlayer extends PlayerImpl { abilityControllerId = target.getAbilityController(); } + int amountTotal = target.getAmountTotal(game, source); + + // Two steps logic: + // 1. Select targets + // 2. Distribute amount between selected targets + + // 1. Select targets while (canRespond()) { Set possibleTargets = target.possibleTargets(source == null ? null : source.getSourceId(), abilityControllerId, game); boolean required = target.isRequired(source != null ? source.getSourceId() : null, game); if (possibleTargets.isEmpty() - || target.getTargets().size() >= target.getNumberOfTargets()) { + || target.getSize() >= target.getNumberOfTargets()) { + required = false; + } + + // selected + Map options = getOptions(target, null); + java.util.List chosen = target.getTargets(); + options.put("chosen", (Serializable) chosen); + // selectable + java.util.List choosable = new ArrayList<>(); + for (UUID targetId : possibleTargets) { + if (target.canTarget(abilityControllerId, targetId, source, game)) { + choosable.add(targetId); + } + } + if (!choosable.isEmpty()) { + options.put("choosable", (Serializable) choosable); + } + + // if nothing to choose then show dialog (user must see non selectable items and click on any of them) + if (required && choosable.isEmpty()) { required = false; } updateGameStatePriority("chooseTargetAmount", game); prepareForResponse(game); if (!isExecutingMacro()) { - String selectedNames = target.getTargetedName(game); - game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage() - + "
Amount remaining: " + target.getAmountRemaining() - + (selectedNames.isEmpty() ? "" : ", selected: " + selectedNames), - getRelatedObjectName(source, game)), - possibleTargets, - required, - getOptions(target, null)); + // target amount uses for damage only, if you see another use case then message must be changed here and on getMultiAmount call + String message = String.format("Select targets to distribute %d damage (selected %d)", amountTotal, target.getTargets().size()); + game.fireSelectTargetEvent(playerId, new MessageToClient(message, getRelatedObjectName(source, game)), possibleTargets, required, options); } waitForResponse(game); UUID responseId = getFixedResponseUUID(game); if (responseId != null) { - if (target.canTarget(abilityControllerId, responseId, source, game)) { - UUID targetId = responseId; - MageObject targetObject = game.getObject(targetId); - - boolean removeMode = target.getTargets().contains(targetId) - && chooseUse(outcome, "What do you want to do with " + (targetObject != null ? targetObject.getLogName() : " target") + "?", "", - "Remove from selected", "Add extra amount (remaining " + target.getAmountRemaining() + ")", source, game); - - if (removeMode) { - target.remove(targetId); - } else { - if (target.getAmountRemaining() > 0) { - int amountSelected = getAmount(1, target.getAmountRemaining(), "Select amount", game); - target.addTarget(targetId, amountSelected, source, game); - } - } - - return true; + if (target.contains(responseId)) { + // unselect + target.remove(responseId); + } else if (possibleTargets.contains(responseId) && target.canTarget(abilityControllerId, responseId, source, game)) { + // select + target.addTarget(responseId, source, game); } } else if (!required) { - return false; + break; } } - return false; + // no targets to choose or disconnected + List targets = target.getTargets(); + if (targets.isEmpty()) { + return false; + } + + // 2. Distribute amount between selected targets + + // prepare targets list with p/t or life stats (cause that's dialog used for damage distribute) + List targetNames = new ArrayList<>(); + for (UUID targetId : targets) { + MageObject targetObject = game.getObject(targetId); + if (targetObject != null) { + targetNames.add(String.format("%s, P/T: %d/%d", + targetObject.getIdName(), + targetObject.getPower().getValue(), + targetObject.getToughness().getValue() + )); + } else { + Player player = game.getPlayer(targetId); + if (player != null) { + targetNames.add(String.format("%s, life: %d", player.getName(), player.getLife())); + } else { + targetNames.add("ERROR, unknown target " + targetId.toString()); + } + } + } + + // ask and assign new amount + List targetValues = getMultiAmount(outcome, targetNames, 1, amountTotal, MultiAmountType.DAMAGE, game); + for (int i = 0; i < targetValues.size(); i++) { + int newAmount = targetValues.get(i); + UUID targetId = targets.get(i); + if (newAmount <= 0) { + // remove target + target.remove(targetId); + } else { + // set amount + target.setTargetAmount(targetId, newAmount, source, game); + } + } + return true; } @Override @@ -1880,6 +1933,52 @@ public class HumanPlayer extends PlayerImpl { } } + @Override + public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + int needCount = messages.size(); + List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); + if (needCount == 0) { + return defaultList; + } + + if (gameInCheckPlayableState(game)) { + return defaultList; + } + + List answer = null; + while (canRespond()) { + updateGameStatePriority("getMultiAmount", game); + prepareForResponse(game); + if (!isExecutingMacro()) { + Map options = new HashMap<>(2); + options.put("title", type.getTitle()); + options.put("header", type.getHeader()); + game.fireGetMultiAmountEvent(playerId, messages, min, max, options); + } + waitForResponse(game); + + // waiting correct values only + if (response.getString() != null) { + answer = MultiAmountType.parseAnswer(response.getString(), needCount, min, max, false); + if (MultiAmountType.isGoodValues(answer, needCount, min, max)) { + break; + } else { + // it's not normal: can be cheater or a wrong GUI checks + answer = null; + logger.error(String.format("GUI return wrong MultiAmountType values: %d %d %d - %s", needCount, min, max, response.getString())); + game.informPlayer(this, "Error, you must enter correct values."); + } + } + } + + if (answer != null) { + return answer; + } else { + // something wrong, e.g. player disconnected + return defaultList; + } + } + @Override public void sideboard(Match match, Deck deck) { match.fireSideboardEvent(playerId, deck); diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index f6ea6a032a..8317ea91cd 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -219,6 +219,9 @@ public class GameController implements GameCallback { case AMOUNT: amount(event.getPlayerId(), event.getMessage(), event.getMin(), event.getMax()); break; + case MULTI_AMOUNT: + multiAmount(event.getPlayerId(), event.getMessages(), event.getMin(), event.getMax(), event.getOptions()); + break; case PERSONAL_MESSAGE: informPersonal(event.getPlayerId(), event.getMessage()); break; @@ -844,6 +847,10 @@ public class GameController implements GameCallback { perform(playerId, playerId1 -> getGameSession(playerId1).getAmount(message, min, max)); } + private synchronized void multiAmount(UUID playerId, final List messages, final int min, final int max, final Map options) throws MageException { + perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options)); + } + private void informOthers(UUID playerId) throws MageException { StringBuilder message = new StringBuilder(); if (game.getStep() != null) { diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java index b7f3c9aae9..0939c26eb6 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java @@ -114,6 +114,13 @@ public class GameSessionPlayer extends GameSessionWatcher { } } + public void getMultiAmount(final List messages, final int min, final int max, final Map options) { + if (!killed) { + userManager.getUser(userId).ifPresent(user + -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(messages, min, max, options)))); + } + } + public void endGameInfo(Table table) { if (!killed) { userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.END_GAME_INFO, game.getId(), getGameEndView(playerId, table)))); diff --git a/Mage.Sets/src/mage/cards/b/Boulderfall.java b/Mage.Sets/src/mage/cards/b/Boulderfall.java index b497229dd8..b69157265b 100644 --- a/Mage.Sets/src/mage/cards/b/Boulderfall.java +++ b/Mage.Sets/src/mage/cards/b/Boulderfall.java @@ -17,7 +17,6 @@ public final class Boulderfall extends CardImpl { public Boulderfall(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{6}{R}{R}"); - // Boulderfall deals 5 damage divided as you choose among any number of target creatures and/or players. this.getSpellAbility().addEffect(new DamageMultiEffect(5)); this.getSpellAbility().addTarget(new TargetAnyTargetAmount(5)); diff --git a/Mage.Sets/src/mage/cards/b/BurntOffering.java b/Mage.Sets/src/mage/cards/b/BurntOffering.java index da08857373..d1b4389bbe 100644 --- a/Mage.Sets/src/mage/cards/b/BurntOffering.java +++ b/Mage.Sets/src/mage/cards/b/BurntOffering.java @@ -1,25 +1,15 @@ package mage.cards.b; -import java.util.LinkedHashSet; -import java.util.Set; import java.util.UUID; -import mage.Mana; -import mage.abilities.Ability; -import mage.abilities.costs.Cost; import mage.abilities.costs.common.SacrificeTargetCost; -import mage.abilities.effects.Effect; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.dynamicvalue.common.SacrificeCostConvertedMana; +import mage.abilities.effects.mana.AddManaInAnyCombinationEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.choices.Choice; -import mage.choices.ChoiceImpl; import mage.constants.CardType; -import mage.constants.Outcome; +import mage.constants.ColoredManaSymbol; import static mage.filter.StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.common.TargetControlledCreaturePermanent; /** @@ -34,7 +24,10 @@ public final class BurntOffering extends CardImpl { //As an additional cost to cast Burnt Offering, sacrifice a creature. this.getSpellAbility().addCost(new SacrificeTargetCost(new TargetControlledCreaturePermanent(FILTER_CONTROLLED_CREATURE_SHORT_TEXT))); //Add an amount of {B} and/or {R} equal to the sacrificed creature's converted mana cost. - this.getSpellAbility().addEffect(new BurntOfferingEffect()); + SacrificeCostConvertedMana xValue = new SacrificeCostConvertedMana("creature"); + this.getSpellAbility().addEffect(new AddManaInAnyCombinationEffect( + xValue, xValue, ColoredManaSymbol.B, ColoredManaSymbol.R + )); } private BurntOffering(final BurntOffering card) { @@ -46,77 +39,3 @@ public final class BurntOffering extends CardImpl { return new BurntOffering(this); } } - -class BurntOfferingEffect extends OneShotEffect { - - public BurntOfferingEffect() { - super(Outcome.PutManaInPool); - this.staticText = "Add X mana in any combination of {B} and/or {R}," - + " where X is the sacrificed creature's converted mana cost"; - } - - public BurntOfferingEffect(final BurntOfferingEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player != null) { - Choice manaChoice = new ChoiceImpl(); - Set choices = new LinkedHashSet<>(); - choices.add("Red"); - choices.add("Black"); - manaChoice.setChoices(choices); - manaChoice.setMessage("Select color of mana to add"); - - int xValue = getCost(source); - - for (int i = 0; i < xValue; i++) { - Mana mana = new Mana(); - if (!player.choose(Outcome.Benefit, manaChoice, game)) { - return false; - } - if (manaChoice.getChoice() == null) { //Can happen if player leaves game - return false; - } - switch (manaChoice.getChoice()) { - case "Red": - mana.increaseRed(); - break; - case "Black": - mana.increaseBlack(); - break; - } - player.getManaPool().addMana(mana, game, source); - } - return true; - } - return false; - } - - @Override - public Effect copy() { - return new BurntOfferingEffect(this); - } - - /** - * Helper method to determine the CMC of the sacrificed creature. - * - * @param sourceAbility - * @return - */ - private int getCost(Ability sourceAbility) { - for (Cost cost : sourceAbility.getCosts()) { - if (cost instanceof SacrificeTargetCost) { - SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost; - int totalCMC = 0; - for (Permanent permanent : sacrificeCost.getPermanents()) { - totalCMC += permanent.getConvertedManaCost(); - } - return totalCMC; - } - } - return 0; - } -} diff --git a/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java b/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java index a599a9cecf..a0a72de56b 100644 --- a/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java +++ b/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java @@ -35,7 +35,6 @@ public final class SelvalaHeartOfTheWilds extends CardImpl { } private static final String rule = "Whenever another creature enters the battlefield, its controller may draw a card if its power is greater than each other creature's power."; - private static final String rule2 = "Add X mana in any combination of colors, where X is the greatest power among creatures you control."; public SelvalaHeartOfTheWilds(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{G}"); @@ -50,8 +49,8 @@ public final class SelvalaHeartOfTheWilds extends CardImpl { // {G}, {T}: Add X mana in any combination of colors, where X is the greatest power among creatures you control. ManaEffect manaEffect = new AddManaInAnyCombinationEffect( - GreatestPowerAmongControlledCreaturesValue.instance, GreatestPowerAmongControlledCreaturesValue.instance, rule2, - ColoredManaSymbol.B, ColoredManaSymbol.U, ColoredManaSymbol.R, ColoredManaSymbol.W, ColoredManaSymbol.G); + GreatestPowerAmongControlledCreaturesValue.instance, GreatestPowerAmongControlledCreaturesValue.instance, + ColoredManaSymbol.W, ColoredManaSymbol.U, ColoredManaSymbol.B, ColoredManaSymbol.R, ColoredManaSymbol.G); Ability ability = new SimpleManaAbility(Zone.BATTLEFIELD, manaEffect, new ManaCostsImpl("{G}")); ability.addCost(new TapSourceCost()); this.addAbility(ability); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java index d16a357817..f9f0fc11ff 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.dka; 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; /** @@ -20,8 +21,11 @@ public class AltarOfTheLostTest extends CardTestPlayerBase { // Flashback {1}{B} addCard(Zone.GRAVEYARD, playerA, "Lingering Souls"); - setChoice(playerA, "Black"); - setChoice(playerA, "Black"); + // Add 2 black mana (mana choice in WUBRG order) + setChoice(playerA, "X=0"); + setChoice(playerA, "X=0"); + setChoice(playerA, "X=2"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback {1}{B}"); setStopAt(3, PhaseStep.BEGIN_COMBAT); @@ -38,8 +42,10 @@ public class AltarOfTheLostTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Altar of the Lost"); addCard(Zone.HAND, playerA, "Lingering Souls"); - setChoice(playerA, "Black"); - setChoice(playerA, "Black"); + setChoice(playerA, "X=0"); + setChoice(playerA, "X=0"); + setChoice(playerA, "X=2"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Lingering Souls"); setStopAt(3, PhaseStep.BEGIN_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java new file mode 100644 index 0000000000..e2661e4c35 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java @@ -0,0 +1,268 @@ +package org.mage.test.cards.targets; + +import mage.constants.MultiAmountType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author JayDi85 + */ + +public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_DefaultValues() { + // default values must be assigned from first to last by minimum values + assertDefaultValues("", 0, 0, 0); + // + assertDefaultValues("0", 1, 0, 0); + assertDefaultValues("0 0", 2, 0, 0); + assertDefaultValues("0 0 0", 3, 0, 0); + // + assertDefaultValues("1", 1, 1, 1); + assertDefaultValues("1 0", 2, 1, 1); + assertDefaultValues("1 0 0", 3, 1, 1); + // + assertDefaultValues("1", 1, 1, 2); + assertDefaultValues("1 0", 2, 1, 2); + assertDefaultValues("1 0 0", 3, 1, 2); + // + assertDefaultValues("2", 1, 2, 2); + assertDefaultValues("2 0", 2, 2, 2); + assertDefaultValues("2 0 0", 3, 2, 2); + // + assertDefaultValues("2", 1, 2, 10); + assertDefaultValues("2 0", 2, 2, 10); + assertDefaultValues("2 0 0", 3, 2, 10); + // + // performance test + assertDefaultValues("2 0 0", 3, 2, Integer.MAX_VALUE); + } + + private void assertDefaultValues(String need, int count, int min, int max) { + List defaultValues = MultiAmountType.prepareDefaltValues(count, min, max); + String current = defaultValues + .stream() + .map(String::valueOf) + .collect(Collectors.joining(" ")); + Assert.assertEquals("default values", need, current); + Assert.assertTrue("default values must be good", MultiAmountType.isGoodValues(defaultValues, count, min, max)); + } + + @Test + public void test_MaxValues() { + // max possible values must be assigned from first to last by max possible values + assertMaxValues("", 0, 0, 0); + // + assertMaxValues("0", 1, 0, 0); + assertMaxValues("0 0", 2, 0, 0); + assertMaxValues("0 0 0", 3, 0, 0); + // + assertMaxValues("1", 1, 1, 1); + assertMaxValues("1 0", 2, 1, 1); + assertMaxValues("1 0 0", 3, 1, 1); + // + assertMaxValues("2", 1, 1, 2); + assertMaxValues("1 1", 2, 1, 2); + assertMaxValues("1 1 0", 3, 1, 2); + // + assertMaxValues("2", 1, 2, 2); + assertMaxValues("1 1", 2, 2, 2); + assertMaxValues("1 1 0", 3, 2, 2); + // + assertMaxValues("10", 1, 2, 10); + assertMaxValues("5 5", 2, 2, 10); + assertMaxValues("4 3 3", 3, 2, 10); + // + assertMaxValues("1 1 1 1 1 0 0 0 0 0", 10, 2, 5); + // + // performance test + assertMaxValues(String.valueOf(Integer.MAX_VALUE), 1, 2, Integer.MAX_VALUE); + int part = Integer.MAX_VALUE / 3; + String need = String.format("%d %d %d", part + 1, part, part); + assertMaxValues(need, 3, 2, Integer.MAX_VALUE); + } + + private void assertMaxValues(String need, int count, int min, int max) { + List maxValues = MultiAmountType.prepareMaxValues(count, min, max); + String current = maxValues + .stream() + .map(String::valueOf) + .collect(Collectors.joining(" ")); + Assert.assertEquals("max values", need, current); + Assert.assertTrue("max values must be good", MultiAmountType.isGoodValues(maxValues, count, min, max)); + } + + @Test + public void test_GoodValues() { + // good values are checking in test_DefaultValues, it's an additional + List list = MultiAmountType.prepareDefaltValues(3, 0, 0); + + // count (0, 0, 0) + Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 0, 0, 0)); + Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 1, 0, 0)); + Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 2, 0, 0)); + Assert.assertTrue("count", MultiAmountType.isGoodValues(list, 3, 0, 0)); + Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 4, 0, 0)); + + // min (0, 1, 1) + list.set(0, 0); + list.set(1, 1); + list.set(2, 1); + Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 0, 100)); + Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 1, 100)); + Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 2, 100)); + Assert.assertFalse("min", MultiAmountType.isGoodValues(list, 3, 3, 100)); + Assert.assertFalse("min", MultiAmountType.isGoodValues(list, 3, 4, 100)); + + // max (0, 1, 1) + list.set(0, 0); + list.set(1, 1); + list.set(2, 1); + Assert.assertFalse("max", MultiAmountType.isGoodValues(list, 3, 0, 0)); + Assert.assertFalse("max", MultiAmountType.isGoodValues(list, 3, 0, 1)); + Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 2)); + Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 3)); + Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 4)); + } + + @Test + public void test_Parse() { + // parse must use correct values on good data or default values on broken data + + // simple parse without data check + assertParse("", 3, 1, 3, "", false); + assertParse("1", 3, 1, 3, "1", false); + assertParse("0 0 0", 3, 1, 3, "0 0 0", false); + assertParse("1 0 3", 3, 1, 3, "1 0 3", false); + assertParse("0 5 0 6", 3, 1, 3, "1,text 5 4. 6", false); + + // parse with data check - good data + assertParse("1 0 2", 3, 0, 3, "1 0 2", true); + + // parse with data check - broken data (must return defalt - 1 0 0) + assertParse("1 0 0", 3, 1, 3, "", true); + assertParse("1 0 0", 3, 1, 3, "1", true); + assertParse("1 0 0", 3, 1, 3, "0 0 0", true); + assertParse("1 0 0", 3, 1, 3, "1 0 3", true); + assertParse("1 0 0", 3, 1, 3, "1,text 4.", true); + } + + private void assertParse(String need, int count, int min, int max, String answerToParse, Boolean returnDefaultOnError) { + List parsedValues = MultiAmountType.parseAnswer(answerToParse, count, min, max, returnDefaultOnError); + String current = parsedValues + .stream() + .map(String::valueOf) + .collect(Collectors.joining(" ")); + Assert.assertEquals("parsed values", need, current); + if (returnDefaultOnError) { + Assert.assertTrue("parsed values must be good", MultiAmountType.isGoodValues(parsedValues, count, min, max)); + } + } + + @Test + public void test_Mana_Manamorphose_Manual() { + removeAllCardsFromHand(playerA); + + // Add two mana in any combination of colors. + // Draw a card. + addCard(Zone.HAND, playerA, "Manamorphose", 2); // {1}{R/G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 * 2); + + // cast and select {B}{B} + // one type of choices: wubrg order + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Manamorphose"); + setChoiceAmount(playerA, 0); // W + setChoiceAmount(playerA, 0); // U + setChoiceAmount(playerA, 2); // B + setChoice(playerA, TestPlayer.CHOICE_SKIP); // skip RG + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkManaPool("after first cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "B", 2); + + // cast and select {R}{G} + // another type of choices + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Manamorphose"); + setChoiceAmount(playerA, 0, 0, 0, 1, 1); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + checkManaPool("after second cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1); + checkManaPool("after second cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "G", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_Mana_Manamorphose_AI() { + removeAllCardsFromHand(playerA); + + // Add two mana in any combination of colors. + // Draw a card. + addCard(Zone.HAND, playerA, "Manamorphose", 1); // {1}{R/G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + // cast, but AI must select first manas (WU) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Manamorphose"); + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkManaPool("after ai cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "W", 1); + checkManaPool("after ai cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "U", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_Damage_Boulderfall_Manual() { + // Boulderfall deals 5 damage divided as you choose among any number of target creatures and/or players. + addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + // + addCard(Zone.BATTLEFIELD, playerA, "Kitesail Corsair@bear", 3); // 2/1 + + // distribute 4x + 1x damage (kill two creatures) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Boulderfall"); + addTargetAmount(playerA, "@bear.1", 4); + addTargetAmount(playerA, "@bear.2", 1); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear.1", 0); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear.2", 0); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear.3", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_Damage_Boulderfall_AI() { + // AI don't use multi amount dialogs like human (it's just one target amount choose/simulation) + + // Boulderfall deals 5 damage divided as you choose among any number of target creatures and/or players. + addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + // + addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 6); // 2/1 + + // play card and distribute damage by game simulations for best score (kills 5x creatures) + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Kitesail Corsair", 5); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index c04aad5af6..8cf09ff0ce 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2667,6 +2667,51 @@ public class TestPlayer implements Player { return computerPlayer.getAmount(min, max, message, game); } + @Override + public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + assertAliasSupportInChoices(false); + + int needCount = messages.size(); + List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); + if (needCount == 0) { + return defaultList; + } + + List answer = new ArrayList<>(defaultList); + if (!choices.isEmpty()) { + // must fill all possible choices or skip it + for (int i = 0; i < messages.size(); i++) { + if (!choices.isEmpty()) { + // normal choice + if (choices.get(0).startsWith("X=")) { + answer.set(i, Integer.parseInt(choices.get(0).substring(2))); + choices.remove(0); + continue; + } + // skip + if (choices.get(0).equals(CHOICE_SKIP)) { + choices.remove(0); + break; + } + } + Assert.fail(String.format("Missing choice in multi amount: %s (pos %d - %s)", type.getHeader(), i + 1, messages.get(i))); + } + + // extra check + if (!MultiAmountType.isGoodValues(answer, needCount, min, max)) { + Assert.fail("Wrong choices in multi amount: " + answer + .stream() + .map(String::valueOf) + .collect(Collectors.joining(","))); + } + + return answer; + } + + this.chooseStrictModeFailed("choice", game, "Multi amount: " + type.getHeader()); + return computerPlayer.getMultiAmount(outcome, messages, min, max, type, game); + } + @Override public void addAbility(Ability ability) { computerPlayer.addAbility(ability); @@ -3873,7 +3918,7 @@ public class TestPlayer implements Player { Assert.assertNotEquals("chooseTargetAmount needs non zero amount remaining", 0, target.getAmountRemaining()); - assertAliasSupportInTargets(false); + assertAliasSupportInTargets(true); if (!targets.isEmpty()) { // skip targets @@ -3894,6 +3939,8 @@ public class TestPlayer implements Player { String targetName = choiceSettings[0]; int targetAmount = Integer.parseInt(choiceSettings[1].substring("X=".length())); + checkTargetDefinitionMarksSupport(target, targetName, "="); + // player target support if (targetName.startsWith("targetPlayer=")) { targetName = targetName.substring(targetName.indexOf("targetPlayer=") + "targetPlayer=".length()); @@ -3905,10 +3952,21 @@ public class TestPlayer implements Player { if (target.getAmountRemaining() > 0) { for (UUID possibleTarget : target.possibleTargets(source.getSourceId(), source.getControllerId(), game)) { + boolean foundTarget = false; + + // permanent MageObject objectPermanent = game.getObject(possibleTarget); + if (objectPermanent != null && hasObjectTargetNameOrAlias(objectPermanent, targetName)) { + foundTarget = true; + } + + // player Player objectPlayer = game.getPlayer(possibleTarget); - String objectName = objectPermanent != null ? objectPermanent.getName() : objectPlayer.getName(); - if (objectName.equals(targetName)) { + if (!foundTarget && objectPlayer != null && objectPlayer.getName().equals(targetName)) { + foundTarget = true; + } + + if (foundTarget) { if (!target.getTargets().contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) { // can select target.addTarget(possibleTarget, targetAmount, source, game); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 4f448d6a71..e737c809dd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1908,6 +1908,20 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } } + /** + * Setup amount choices. + * + * Multi amount choices uses WUBRG order (so use 1,2,3,4,5 values list) + * + * @param player + * @param amountList + */ + public void setChoiceAmount(TestPlayer player, int... amountList) { + for (int amount : amountList) { + setChoice(player, "X=" + amount); + } + } + /** * Set the modes for modal spells * diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 4ddfb7f824..dc7f44f0d7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -974,6 +974,11 @@ public class PlayerStub implements Player { return 0; } + @Override + public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + return null; + } + @Override public void sideboard(Match match, Deck deck) { diff --git a/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java b/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java index 6e2bb56506..82060231a4 100644 --- a/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java @@ -9,6 +9,7 @@ import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.mana.builder.ConditionalManaBuilder; import mage.choices.ChoiceColor; +import mage.constants.MultiAmountType; import mage.game.Game; import mage.players.Player; import mage.util.CardUtil; @@ -87,29 +88,26 @@ public class AddConditionalManaOfAnyColorEffect extends ManaEffect { if (game != null) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - ConditionalMana mana = null; int value = amount.calculate(game, source, this); - ChoiceColor choice = new ChoiceColor(true); - for (int i = 0; i < value; i++) { - if (choice.getChoice() == null) { + if (value > 0) { + if (oneChoice || value == 1) { + ChoiceColor choice = new ChoiceColor(true); controller.choose(outcome, choice, game); - } - if (choice.getChoice() == null) { - return null; - } - if (oneChoice) { - mana = new ConditionalMana(manaBuilder.setMana(choice.getMana(value), source, game).build()); - break; - } else { - if (mana == null) { - mana = new ConditionalMana(manaBuilder.setMana(choice.getMana(1), source, game).build()); - } else { - mana.add(choice.getMana(1)); + if (choice.getChoice() == null) { + return null; } - choice.clearChoice(); + return new ConditionalMana(manaBuilder.setMana(choice.getMana(value), source, game).build()); } + List manaStrings = new ArrayList<>(5); + manaStrings.add("W"); + manaStrings.add("U"); + manaStrings.add("B"); + manaStrings.add("R"); + manaStrings.add("G"); + List choices = controller.getMultiAmount(this.outcome, manaStrings, 0, value, MultiAmountType.MANA, game); + Mana mana = new Mana(choices.get(0), choices.get(1), choices.get(2), choices.get(3), choices.get(4), 0, 0, 0); + return new ConditionalMana(manaBuilder.setMana(mana, source, game).build()); } - return mana; } } return new Mana(); diff --git a/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java b/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java index d98ecfaec0..30490589f2 100644 --- a/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java @@ -1,11 +1,5 @@ package mage.abilities.effects.mana; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import mage.Mana; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; @@ -13,10 +7,14 @@ import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.mana.ManaOptions; import mage.constants.ColoredManaSymbol; import mage.constants.ManaType; +import mage.constants.MultiAmountType; import mage.game.Game; import mage.players.Player; import mage.util.CardUtil; +import java.util.*; +import java.util.stream.Collectors; + /** * @author LevelX2 */ @@ -27,7 +25,13 @@ public class AddManaInAnyCombinationEffect extends ManaEffect { private final DynamicValue netAmount; public AddManaInAnyCombinationEffect(int amount) { - this(StaticValue.get(amount), StaticValue.get(amount), ColoredManaSymbol.B, ColoredManaSymbol.U, ColoredManaSymbol.R, ColoredManaSymbol.W, ColoredManaSymbol.G); + this(StaticValue.get(amount), StaticValue.get(amount), + ColoredManaSymbol.W, + ColoredManaSymbol.U, + ColoredManaSymbol.B, + ColoredManaSymbol.R, + ColoredManaSymbol.G + ); } public AddManaInAnyCombinationEffect(int amount, ColoredManaSymbol... coloredManaSymbols) { @@ -106,26 +110,20 @@ public class AddManaInAnyCombinationEffect extends ManaEffect { public Mana produceMana(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); if (player != null) { + int size = manaSymbols.size(); Mana mana = new Mana(); - int amountOfManaLeft = amount.calculate(game, source, this); - int maxAmount = amountOfManaLeft; - - while (amountOfManaLeft > 0 && player.canRespond()) { - for (ColoredManaSymbol coloredManaSymbol : manaSymbols) { - int number = player.getAmount(0, amountOfManaLeft, "Distribute mana by color (" + mana.count() - + " of " + maxAmount + " done). How many " + coloredManaSymbol.getColorHtmlName() + " mana to add (enter 0 to pass to next color)?", game); - if (number > 0) { - for (int i = 0; i < number; i++) { - mana.add(new Mana(coloredManaSymbol)); - } - amountOfManaLeft -= number; - } - if (amountOfManaLeft == 0) { - break; - } + List manaStrings = new ArrayList<>(size); + for (ColoredManaSymbol coloredManaSymbol : manaSymbols) { + manaStrings.add(coloredManaSymbol.toString()); + } + List manaList = player.getMultiAmount(this.outcome, manaStrings, 0, amount.calculate(game, source, this), MultiAmountType.MANA, game); + for (int i = 0; i < size; i++) { + ColoredManaSymbol coloredManaSymbol = manaSymbols.get(i); + int amount = manaList.get(i); + for (int j = 0; j < amount; j++) { + mana.add(new Mana(coloredManaSymbol)); } } - return mana; } return null; @@ -134,7 +132,7 @@ public class AddManaInAnyCombinationEffect extends ManaEffect { @Override public Set getProducableManaTypes(Game game, Ability source) { Set manaTypes = new HashSet<>(); - for(ColoredManaSymbol coloredManaSymbol: manaSymbols) { + for (ColoredManaSymbol coloredManaSymbol : manaSymbols) { if (coloredManaSymbol.equals(ColoredManaSymbol.B)) { manaTypes.add(ManaType.BLACK); } @@ -156,7 +154,8 @@ public class AddManaInAnyCombinationEffect extends ManaEffect { private String setText() { StringBuilder sb = new StringBuilder("Add "); - sb.append(CardUtil.numberToText(amount.toString())); + String amountString = CardUtil.numberToText(amount.toString()); + sb.append(amountString); sb.append(" mana in any combination of "); if (manaSymbols.size() == 5) { sb.append("colors"); @@ -170,6 +169,10 @@ public class AddManaInAnyCombinationEffect extends ManaEffect { sb.append('{').append(coloredManaSymbol.toString()).append('}'); } } + if (amountString.equals("X")) { + sb.append(", where X is "); + sb.append(amount.getMessage()); + } return sb.toString(); } } diff --git a/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java b/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java index e0d80d1d8d..d14c147c51 100644 --- a/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java @@ -5,7 +5,7 @@ import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.dynamicvalue.DynamicValue; import mage.choices.ChoiceColor; -import mage.constants.Outcome; +import mage.constants.MultiAmountType; import mage.game.Game; import mage.players.Player; @@ -143,18 +143,23 @@ public class DynamicManaEffect extends ManaEffect { computedMana.setColorless(count); } else if (baseMana.getAny() > 0) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - ChoiceColor choiceColor = new ChoiceColor(true); - for (int i = 0; i < count; i++) { - if (!choiceColor.isChosen()) { - if (!controller.choose(Outcome.Benefit, choiceColor, game)) { - return computedMana; - } - } - choiceColor.increaseMana(computedMana); - if (!oneChoice) { - choiceColor.clearChoice(); + if (controller != null && count > 0) { + if (oneChoice || count == 1) { + ChoiceColor choice = new ChoiceColor(true); + controller.choose(outcome, choice, game); + if (choice.getChoice() == null) { + return computedMana; } + computedMana.add(choice.getMana(count)); + } else { + List manaStrings = new ArrayList<>(5); + manaStrings.add("W"); + manaStrings.add("U"); + manaStrings.add("B"); + manaStrings.add("R"); + manaStrings.add("G"); + List choices = controller.getMultiAmount(this.outcome, manaStrings, 0, count, MultiAmountType.MANA, game); + computedMana.add(new Mana(choices.get(0), choices.get(1), choices.get(2), choices.get(3), choices.get(4), 0, 0, 0)); } } } else { diff --git a/Mage/src/main/java/mage/choices/Choices.java b/Mage/src/main/java/mage/choices/Choices.java index 9bb5c0910d..c934575142 100644 --- a/Mage/src/main/java/mage/choices/Choices.java +++ b/Mage/src/main/java/mage/choices/Choices.java @@ -56,6 +56,9 @@ public class Choices extends ArrayList { public boolean choose(Game game, Ability source) { if (this.size() > 0) { Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } while (!isChosen()) { Choice choice = this.getUnchosen().get(0); if (!player.choose(outcome, choice, game)) { diff --git a/Mage/src/main/java/mage/constants/MultiAmountType.java b/Mage/src/main/java/mage/constants/MultiAmountType.java new file mode 100644 index 0000000000..feed33809d --- /dev/null +++ b/Mage/src/main/java/mage/constants/MultiAmountType.java @@ -0,0 +1,108 @@ +package mage.constants; + +import com.google.common.collect.Iterables; +import mage.util.CardUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.stream.IntStream; + +public enum MultiAmountType { + + MANA("Add mana", "Distribute mana among colors"), + DAMAGE("Assign damage", "Assign damage among targets"); + + private final String title; + private final String header; + + MultiAmountType(String title, String header) { + this.title = title; + this.header = header; + } + + public String getTitle() { + return title; + } + + public String getHeader() { + return header; + } + + public static List prepareDefaltValues(int count, int min, int max) { + // default values must be assigned from first to last by minimum values + List res = new ArrayList<>(); + if (count == 0) { + return res; + } + + // fill list + IntStream.range(0, count).forEach(i -> res.add(0)); + + // fill values + if (min > 0) { + res.set(0, min); + } + + return res; + } + + public static List prepareMaxValues(int count, int min, int max) { + // fill max values as much as possible + List res = new ArrayList<>(); + if (count == 0) { + return res; + } + + // fill list + int startingValue = max / count; + IntStream.range(0, count).forEach(i -> res.add(startingValue)); + + // fill values + // from first to last until complete + List resIndexes = new ArrayList<>(res.size()); + IntStream.range(0, res.size()).forEach(resIndexes::add); + // infinite iterator (no needs with starting values use, but can be used later for different logic) + Iterator resIterator = Iterables.cycle(resIndexes).iterator(); + int valueInc = 1; + int valueTotal = startingValue * count; + while (valueTotal < max) { + int currentIndex = resIterator.next(); + int newValue = CardUtil.overflowInc(res.get(currentIndex), valueInc); + res.set(currentIndex, newValue); + valueTotal += valueInc; + } + + return res; + } + + public static boolean isGoodValues(List values, int count, int min, int max) { + if (values.size() != count) { + return false; + } + + int currentSum = values.stream().mapToInt(i -> i).sum(); + return currentSum >= min && currentSum <= max; + } + + public static List parseAnswer(String answerToParse, int count, int min, int max, boolean returnDefaultOnError) { + List res = new ArrayList<>(); + + // parse + String normalValue = answerToParse.trim(); + if (!normalValue.isEmpty()) { + Arrays.stream(normalValue.split(" ")).forEach(valueStr -> { + res.add(CardUtil.parseIntWithDefault(valueStr, 0)); + }); + } + + // data check + if (returnDefaultOnError && !isGoodValues(res, count, min, max)) { + // on broken data - return default + return prepareDefaltValues(count, min, max); + } + + return res; + } +} diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 9012a1ad3b..b7a6510200 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -277,6 +277,8 @@ public interface Game extends MageItem, Serializable { void fireGetAmountEvent(UUID playerId, String message, int min, int max); + void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, Map options); + void fireChoosePileEvent(UUID playerId, String message, List pile1, List pile2); void fireInformEvent(String message); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 2fcfc747ab..45d66188f0 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2515,6 +2515,14 @@ public abstract class GameImpl implements Game, Serializable { playerQueryEventSource.amount(playerId, message, min, max); } + @Override + public void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, Map options) { + if (simulation) { + return; + } + playerQueryEventSource.multiAmount(playerId, messages, min, max, options); + } + @Override public void fireChooseChoiceEvent(UUID playerId, Choice choice) { if (simulation) { diff --git a/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java b/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java index 4aaf42d033..4b906c6811 100644 --- a/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java +++ b/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java @@ -35,6 +35,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri PLAY_MANA, PLAY_X_MANA, AMOUNT, + MULTI_AMOUNT, PICK_CARD, CONSTRUCT, CHOOSE_PILE, @@ -58,8 +59,11 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri private List pile1; private List pile2; private Choice choice; + private List messages; - private PlayerQueryEvent(UUID playerId, String message, List abilities, Set choices, Set targets, Cards cards, QueryType queryType, int min, int max, boolean required, Map options) { + private PlayerQueryEvent(UUID playerId, String message, List abilities, Set choices, + Set targets, Cards cards, QueryType queryType, int min, int max, boolean required, + Map options, List messages) { super(playerId); this.queryType = queryType; this.message = message; @@ -77,6 +81,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri this.options = options; } this.options.put("queryType", queryType); + this.messages = messages; } private PlayerQueryEvent(UUID playerId, String message, List booster, QueryType queryType, int time) { @@ -143,7 +148,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri } options.put("originalId", source.getOriginalId()); } - return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.ASK, 0, 0, false, options); + return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.ASK, 0, 0, false, options, null); } public static PlayerQueryEvent chooseAbilityEvent(UUID playerId, String message, String objectName, List choices) { @@ -152,7 +157,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri nameAsSet = new HashSet<>(); nameAsSet.add(objectName); } - return new PlayerQueryEvent(playerId, message, choices, nameAsSet, null, null, QueryType.CHOOSE_ABILITY, 0, 0, false, null); + return new PlayerQueryEvent(playerId, message, choices, nameAsSet, null, null, QueryType.CHOOSE_ABILITY, 0, 0, false, null, null); } public static PlayerQueryEvent choosePileEvent(UUID playerId, String message, List pile1, List pile2) { @@ -168,19 +173,19 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri } public static PlayerQueryEvent targetEvent(UUID playerId, String message, Set targets, boolean required) { - return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, null); + return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, null, null); } public static PlayerQueryEvent targetEvent(UUID playerId, String message, Set targets, boolean required, Map options) { - return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, options); + return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, options, null); } public static PlayerQueryEvent targetEvent(UUID playerId, String message, Cards cards, boolean required, Map options) { - return new PlayerQueryEvent(playerId, message, null, null, null, cards, QueryType.PICK_TARGET, 0, 0, required, options); + return new PlayerQueryEvent(playerId, message, null, null, null, cards, QueryType.PICK_TARGET, 0, 0, required, options, null); } public static PlayerQueryEvent targetEvent(UUID playerId, String message, List abilities) { - return new PlayerQueryEvent(playerId, message, abilities, null, null, null, QueryType.PICK_ABILITY, 0, 0, true, null); + return new PlayerQueryEvent(playerId, message, abilities, null, null, null, QueryType.PICK_ABILITY, 0, 0, true, null, null); } public static PlayerQueryEvent targetEvent(UUID playerId, String message, List perms, boolean required) { @@ -188,23 +193,27 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri } public static PlayerQueryEvent selectEvent(UUID playerId, String message) { - return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, null); + return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, null, null); } public static PlayerQueryEvent selectEvent(UUID playerId, String message, Map options) { - return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, options); + return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, options, null); } public static PlayerQueryEvent playManaEvent(UUID playerId, String message, Map options) { - return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_MANA, 0, 0, false, options); + return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_MANA, 0, 0, false, options, null); } public static PlayerQueryEvent playXManaEvent(UUID playerId, String message) { - return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_X_MANA, 0, 0, false, null); + return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_X_MANA, 0, 0, false, null, null); } public static PlayerQueryEvent amountEvent(UUID playerId, String message, int min, int max) { - return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.AMOUNT, min, max, false, null); + return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.AMOUNT, min, max, false, null, null); + } + + public static PlayerQueryEvent multiAmountEvent(UUID playerId, List messages, int min, int max, Map options) { + return new PlayerQueryEvent(playerId, null, null, null, null, null, QueryType.MULTI_AMOUNT, min, max, false, options, messages); } public static PlayerQueryEvent pickCard(UUID playerId, String message, List booster, int time) { @@ -287,4 +296,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri return choice; } + public List getMessages() { + return messages; + } } diff --git a/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java b/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java index 87dd6f45ec..b2aac5ec30 100644 --- a/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java +++ b/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java @@ -84,6 +84,10 @@ public class PlayerQueryEventSource implements EventSource, Se dispatcher.fireEvent(PlayerQueryEvent.amountEvent(playerId, message, min, max)); } + public void multiAmount(UUID playerId, List messages, int min, int max, Map options) { + dispatcher.fireEvent(PlayerQueryEvent.multiAmountEvent(playerId, messages, min, max, options)); + } + public void chooseChoice(UUID playerId, Choice choice) { dispatcher.fireEvent(PlayerQueryEvent.chooseChoiceEvent(playerId, choice)); } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 2192d2bf0a..3d9b815069 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -708,6 +708,19 @@ public interface Player extends MageItem, Copyable { int getAmount(int min, int max, String message, Game game); + /** + * Player distributes amount among multiple options + * + * @param outcome AI hint + * @param messages List of options to distribute amount among + * @param min Minimum value per option + * @param max Total amount to be distributed + * @param type MultiAmountType enum to set dialog options such as title and header + * @param game Game + * @return List of integers with size equal to messages.size(). The sum of the integers is equal to max. + */ + List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game); + void sideboard(Match match, Deck deck); void construct(Tournament tournament, Deck deck); diff --git a/Mage/src/main/java/mage/players/StubPlayer.java b/Mage/src/main/java/mage/players/StubPlayer.java index 1b86856123..ad47e96d2a 100644 --- a/Mage/src/main/java/mage/players/StubPlayer.java +++ b/Mage/src/main/java/mage/players/StubPlayer.java @@ -11,6 +11,7 @@ import mage.cards.Card; import mage.cards.Cards; import mage.cards.decks.Deck; import mage.choices.Choice; +import mage.constants.MultiAmountType; import mage.constants.Outcome; import mage.constants.RangeOfInfluence; import mage.filter.FilterMana; @@ -205,6 +206,11 @@ public class StubPlayer extends PlayerImpl implements Player { return 0; } + @Override + public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + return null; + } + @Override public void sideboard(Match match, Deck deck) { diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java index ae0db1ccb5..72f5c25a8a 100644 --- a/Mage/src/main/java/mage/target/Target.java +++ b/Mage/src/main/java/mage/target/Target.java @@ -147,4 +147,8 @@ public interface Target extends Serializable { String getChooseHint(); void setEventReporting(boolean shouldReport); + + int getSize(); + + boolean contains(UUID targetId); } diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java index 72012fe9d6..692cc0cb74 100644 --- a/Mage/src/main/java/mage/target/TargetAmount.java +++ b/Mage/src/main/java/mage/target/TargetAmount.java @@ -5,6 +5,7 @@ import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.StaticValue; import mage.constants.Outcome; import mage.game.Game; +import mage.players.Player; import java.util.*; import java.util.stream.Collectors; @@ -59,7 +60,7 @@ public abstract class TargetAmount extends TargetImpl { public void clearChosen() { super.clearChosen(); amountWasSet = false; - // remainingAmount = amount; + // remainingAmount = amount; // auto-calced on target remove } public void setAmountDefinition(DynamicValue amount) { @@ -71,6 +72,9 @@ public abstract class TargetAmount extends TargetImpl { amountWasSet = true; } + public int getAmountTotal(Game game, Ability source) { + return amount.calculate(game, source, null); + } @Override public void addTarget(UUID id, int amount, Ability source, Game game, boolean skipEvent) { @@ -92,17 +96,25 @@ public abstract class TargetAmount extends TargetImpl { @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { + Player player = game.getPlayer(playerId); + if (player == null) { + return false; + } + if (!amountWasSet) { setAmount(source, game); } chosen = isChosen(); while (remainingAmount > 0) { + if (!player.canRespond()) { + return chosen; + } if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) { return chosen; } chosen = isChosen(); } - return chosen = true; + return chosen; } @Override @@ -163,4 +175,12 @@ public abstract class TargetAmount extends TargetImpl { } } } + + public void setTargetAmount(UUID targetId, int amount, Ability source, Game game) { + if (!amountWasSet) { + setAmount(source, game); + } + remainingAmount -= (amount - this.getTargetAmount(targetId)); + this.setTargetAmount(targetId, amount, game); + } } diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java index 053000e106..d1d9b83d74 100644 --- a/Mage/src/main/java/mage/target/TargetImpl.java +++ b/Mage/src/main/java/mage/target/TargetImpl.java @@ -278,51 +278,60 @@ public abstract class TargetImpl implements Target { @Override public boolean choose(Outcome outcome, UUID playerId, UUID sourceId, Game game) { - Player player = game.getPlayer(playerId); - if (player == null) { + Player targetController = getTargetController(game, playerId); + if (targetController == null) { return false; } - while (!isChosen() && !doneChosing()) { - if (!player.canRespond()) { - return chosen = targets.size() >= getNumberOfTargets(); + chosen = targets.size() >= getNumberOfTargets(); + do { + if (!targetController.canRespond()) { + return chosen; } - chosen = targets.size() >= getNumberOfTargets(); - if (!player.choose(outcome, this, sourceId, game)) { + if (!targetController.choose(outcome, this, sourceId, game)) { return chosen; } chosen = targets.size() >= getNumberOfTargets(); - } - return chosen = true; + } while (!isChosen() && !doneChosing()); + return chosen; } @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { - Player player = game.getPlayer(playerId); - if (player == null) { + Player targetController = getTargetController(game, playerId); + if (targetController == null) { return false; } List possibleTargets = new ArrayList<>(possibleTargets(source.getSourceId(), playerId, game)); - while (!isChosen() && !doneChosing()) { - if (!player.canRespond()) { - return chosen = targets.size() >= getNumberOfTargets(); + + chosen = targets.size() >= getNumberOfTargets(); + do { + if (!targetController.canRespond()) { + return chosen; } - chosen = targets.size() >= getNumberOfTargets(); if (isRandom()) { - if (!possibleTargets.isEmpty()) { - int index = RandomUtil.nextInt(possibleTargets.size()); - this.addTarget(possibleTargets.get(index), source, game); - possibleTargets.remove(index); - } else { + if (possibleTargets.isEmpty()) { return chosen; } - } else if (!getTargetController(game, playerId).chooseTarget(outcome, this, source, game)) { + // 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); + break; + } else { + possibleTargets.remove(index); + } + } + } else if (!targetController.chooseTarget(outcome, this, source, game)) { return chosen; } chosen = targets.size() >= getNumberOfTargets(); - } - return chosen = true; + } while (!isChosen() && !doneChosing()); + + return chosen; } @Override @@ -574,4 +583,14 @@ public abstract class TargetImpl implements Target { public void setEventReporting(boolean shouldReport) { this.shouldReportEvents = shouldReport; } + + @Override + public int getSize() { + return targets.size(); + } + + @Override + public boolean contains(UUID targetId) { + return targets.containsKey(targetId); + } } diff --git a/Mage/src/main/java/mage/target/Targets.java b/Mage/src/main/java/mage/target/Targets.java index 09d0b080aa..2c2a0a4796 100644 --- a/Mage/src/main/java/mage/target/Targets.java +++ b/Mage/src/main/java/mage/target/Targets.java @@ -4,6 +4,7 @@ import mage.abilities.Ability; import mage.constants.Outcome; import mage.game.Game; import mage.game.events.GameEvent; +import mage.players.Player; import mage.target.targetpointer.*; import org.apache.log4j.Logger; @@ -65,6 +66,7 @@ public class Targets extends ArrayList { if (!canChoose(source.getSourceId(), playerId, game)) { return false; } + //int state = game.bookmarkState(); while (!isChosen()) { Target target = this.getUnchosen().get(0); diff --git a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java index bbbd6d63a5..99577565c0 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java @@ -72,21 +72,19 @@ public class TargetCardInLibrary extends TargetCard { } cards.sort(Comparator.comparing(MageObject::getName)); Cards cardsId = new CardsImpl(); - cards.forEach((card) -> { - cardsId.add(card); - }); + cards.forEach(cardsId::add); - while (!isChosen() && !doneChosing()) { + chosen = targets.size() >= getMinNumberOfTargets(); + do { if (!player.canRespond()) { - return chosen = targets.size() >= minNumberOfTargets; + return chosen; } - chosen = targets.size() >= minNumberOfTargets; if (!player.chooseTarget(outcome, cardsId, this, null, game)) { return chosen; } - chosen = targets.size() >= minNumberOfTargets; - } - return chosen = true; + chosen = targets.size() >= getMinNumberOfTargets(); + } while (!isChosen() && !doneChosing()); + return chosen; } @Override diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 5a181ca973..2060293d64 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1271,6 +1271,16 @@ public final class CardUtil { return res; } + public static int parseIntWithDefault(String value, int defaultValue) { + int res; + try { + res = Integer.parseInt(value); + } catch(NumberFormatException ex) { + res = defaultValue; + } + return res; + } + /** * Find mapping from original to copied card (e.g. map original left side with copied left side) *