[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 <jaydi85@gmail.com>
This commit is contained in:
Daniel Bomar 2021-04-17 05:28:01 -05:00 committed by GitHub
parent 042aa61ad4
commit 600cac6fc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1209 additions and 232 deletions

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.2" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JInternalFrameFormInfo">
<SyntheticProperties>
<SyntheticProperty name="formSizePolicy" type="int" value="2"/>
</SyntheticProperties>
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
</AuxValues>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<EmptySpace min="12" pref="12" max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="header" alignment="1" max="32767" attributes="0"/>
<Component id="counterText" max="32767" attributes="0"/>
</Group>
</Group>
<Group type="102" attributes="0">
<EmptySpace min="-2" pref="184" max="-2" attributes="0"/>
<Component id="chooseButton" min="-2" max="-2" attributes="0"/>
<EmptySpace min="0" pref="172" max="32767" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="jScrollPane1" max="32767" attributes="0"/>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="1" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="header" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="counterText" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="jScrollPane1" pref="276" max="32767" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="chooseButton" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Component class="javax.swing.JButton" name="chooseButton">
<Properties>
<Property name="text" type="java.lang.String" value="Choose"/>
<Property name="enabled" type="boolean" value="false"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="chooseButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="header">
<Properties>
<Property name="text" type="java.lang.String" value="Header"/>
</Properties>
</Component>
<Component class="javax.swing.JLabel" name="counterText">
<Properties>
<Property name="text" type="java.lang.String" value="Counter"/>
</Properties>
</Component>
<Container class="javax.swing.JScrollPane" name="jScrollPane1">
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
<SubComponents>
<Container class="javax.swing.JPanel" name="jPanel1">
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<EmptySpace min="0" pref="413" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<EmptySpace min="0" pref="273" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
</Layout>
</Container>
</SubComponents>
</Container>
</SubComponents>
</Form>

View file

@ -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<JLabel> labelList = null;
private List<JSpinner> spinnerList = null;
public PickMultiNumberDialog() {
initComponents();
this.setModal(true);
}
public void showDialog(List<String> messages, int min, int max, Map<String, Serializable> 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("<html>" + manaText);
Image image = ManaSymbols.getSizedManaSymbol(input);
if (image != null) {
label.setIcon(new ImageIcon(image));
}
} else {
// text mode
label.setText("<html>" + 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")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//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())
);
}// </editor-fold>//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
}

View file

@ -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<String> messages, int min, int max, Map<String, Serializable> 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?

View file

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

View file

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

View file

@ -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<String, Serializable> options;
@Expose
private Choice choice;
@Expose
private List<String> messages;
public GameClientMessage(GameView gameView) {
this.gameView = gameView;
@ -93,6 +96,13 @@ public class GameClientMessage implements Serializable {
this.message = name;
}
public GameClientMessage(List<String> messages, int min, int max, Map<String, Serializable> 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<String> getMessages() {
return messages;
}
public String toJson() {
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()

View file

@ -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<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) {
log.debug("getMultiAmount");
int needCount = messages.size();
List<Integer> 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<Permanent> attackers, Game game) {
//TODO: improve this

View file

@ -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<UUID> 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<String, Serializable> options = getOptions(target, null);
java.util.List<UUID> chosen = target.getTargets();
options.put("chosen", (Serializable) chosen);
// selectable
java.util.List<UUID> 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()
+ "<br> 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<UUID> 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<String> 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<Integer> 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<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) {
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
if (needCount == 0) {
return defaultList;
}
if (gameInCheckPlayableState(game)) {
return defaultList;
}
List<Integer> answer = null;
while (canRespond()) {
updateGameStatePriority("getMultiAmount", game);
prepareForResponse(game);
if (!isExecutingMacro()) {
Map<String, Serializable> 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);

View file

@ -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<String> messages, final int min, final int max, final Map<String, Serializable> 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) {

View file

@ -114,6 +114,13 @@ public class GameSessionPlayer extends GameSessionWatcher {
}
}
public void getMultiAmount(final List<String> messages, final int min, final int max, final Map<String, Serializable> 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))));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2667,6 +2667,51 @@ public class TestPlayer implements Player {
return computerPlayer.getAmount(min, max, message, game);
}
@Override
public List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) {
assertAliasSupportInChoices(false);
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
if (needCount == 0) {
return defaultList;
}
List<Integer> 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);

View file

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

View file

@ -974,6 +974,11 @@ public class PlayerStub implements Player {
return 0;
}
@Override
public List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) {
return null;
}
@Override
public void sideboard(Match match, Deck deck) {

View file

@ -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<String> manaStrings = new ArrayList<>(5);
manaStrings.add("W");
manaStrings.add("U");
manaStrings.add("B");
manaStrings.add("R");
manaStrings.add("G");
List<Integer> 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();

View file

@ -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 <b>" + coloredManaSymbol.getColorHtmlName() + "</b> 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<String> manaStrings = new ArrayList<>(size);
for (ColoredManaSymbol coloredManaSymbol : manaSymbols) {
manaStrings.add(coloredManaSymbol.toString());
}
List<Integer> 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<ManaType> getProducableManaTypes(Game game, Ability source) {
Set<ManaType> 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();
}
}

View file

@ -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<String> manaStrings = new ArrayList<>(5);
manaStrings.add("W");
manaStrings.add("U");
manaStrings.add("B");
manaStrings.add("R");
manaStrings.add("G");
List<Integer> 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 {

View file

@ -56,6 +56,9 @@ public class Choices extends ArrayList<Choice> {
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)) {

View file

@ -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<Integer> prepareDefaltValues(int count, int min, int max) {
// default values must be assigned from first to last by minimum values
List<Integer> 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<Integer> prepareMaxValues(int count, int min, int max) {
// fill max values as much as possible
List<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> parseAnswer(String answerToParse, int count, int min, int max, boolean returnDefaultOnError) {
List<Integer> 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;
}
}

View file

@ -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<String> messages, int min, int max, Map<String, Serializable> options);
void fireChoosePileEvent(UUID playerId, String message, List<? extends Card> pile1, List<? extends Card> pile2);
void fireInformEvent(String message);

View file

@ -2515,6 +2515,14 @@ public abstract class GameImpl implements Game, Serializable {
playerQueryEventSource.amount(playerId, message, min, max);
}
@Override
public void fireGetMultiAmountEvent(UUID playerId, List<String> messages, int min, int max, Map<String, Serializable> options) {
if (simulation) {
return;
}
playerQueryEventSource.multiAmount(playerId, messages, min, max, options);
}
@Override
public void fireChooseChoiceEvent(UUID playerId, Choice choice) {
if (simulation) {

View file

@ -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<? extends Card> pile1;
private List<? extends Card> pile2;
private Choice choice;
private List<String> messages;
private PlayerQueryEvent(UUID playerId, String message, List<? extends Ability> abilities, Set<String> choices, Set<UUID> targets, Cards cards, QueryType queryType, int min, int max, boolean required, Map<String, Serializable> options) {
private PlayerQueryEvent(UUID playerId, String message, List<? extends Ability> abilities, Set<String> choices,
Set<UUID> targets, Cards cards, QueryType queryType, int min, int max, boolean required,
Map<String, Serializable> options, List<String> 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<Card> 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<? extends ActivatedAbility> 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<? extends Card> pile1, List<? extends Card> pile2) {
@ -168,19 +173,19 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
}
public static PlayerQueryEvent targetEvent(UUID playerId, String message, Set<UUID> 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<UUID> targets, boolean required, Map<String, Serializable> 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<String, Serializable> 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<TriggeredAbility> 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<Permanent> 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<String, Serializable> 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<String, Serializable> 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<String> messages, int min, int max, Map<String, Serializable> 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<Card> booster, int time) {
@ -287,4 +296,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
return choice;
}
public List<String> getMessages() {
return messages;
}
}

View file

@ -84,6 +84,10 @@ public class PlayerQueryEventSource implements EventSource<PlayerQueryEvent>, Se
dispatcher.fireEvent(PlayerQueryEvent.amountEvent(playerId, message, min, max));
}
public void multiAmount(UUID playerId, List<String> messages, int min, int max, Map<String, Serializable> options) {
dispatcher.fireEvent(PlayerQueryEvent.multiAmountEvent(playerId, messages, min, max, options));
}
public void chooseChoice(UUID playerId, Choice choice) {
dispatcher.fireEvent(PlayerQueryEvent.chooseChoiceEvent(playerId, choice));
}

View file

@ -708,6 +708,19 @@ public interface Player extends MageItem, Copyable<Player> {
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<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game);
void sideboard(Match match, Deck deck);
void construct(Tournament tournament, Deck deck);

View file

@ -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<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) {
return null;
}
@Override
public void sideboard(Match match, Deck deck) {

View file

@ -147,4 +147,8 @@ public interface Target extends Serializable {
String getChooseHint();
void setEventReporting(boolean shouldReport);
int getSize();
boolean contains(UUID targetId);
}

View file

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

View file

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

View file

@ -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<Target> {
if (!canChoose(source.getSourceId(), playerId, game)) {
return false;
}
//int state = game.bookmarkState();
while (!isChosen()) {
Target target = this.getUnchosen().get(0);

View file

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

View file

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