[LTR] Add Goldberry, River-Daughter (#10524)

* Added Goldberry

* Slight optimizaztion

* Happy Path Test

* More unhappy tests

* Sanity check for Goldberry's counter choices

* Updated player.getMultiAmount to support individual constraints

* Some cleanup

Also modified ResourcefulDefense to use new multi amount api

* Updated logging

* Added hint for number of counters

* Fixed issue with Resourceful Defense

* Improvements to defaults

Default list will properly make sure to stay within individual maximums
If a player is asked for a choice that isn't actually a choice because each choice's min and max are equal, instead the default response is immediately returned. This helps with situations like moving a counter off of Goldberry when she only has one counter on her.

* -1/-1 Counter test

* Fixed issue with -1/-1 counters

* Adjusted dialog to properly enforce constraints
This commit is contained in:
Alexander Novotny 2023-07-28 21:29:40 -04:00 committed by GitHub
parent fe1efef25b
commit a36a7d9b7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 678 additions and 180 deletions

View file

@ -1,6 +1,8 @@
package mage.client.dialog; package mage.client.dialog;
import mage.constants.ColoredManaSymbol; import mage.constants.ColoredManaSymbol;
import mage.util.MultiAmountMessage;
import org.mage.card.arcane.ManaSymbols; import org.mage.card.arcane.ManaSymbols;
import javax.swing.*; import javax.swing.*;
@ -25,7 +27,7 @@ public class PickMultiNumberDialog extends MageDialog {
this.setModal(true); this.setModal(true);
} }
public void showDialog(List<String> messages, int min, int max, Map<String, Serializable> options) { public void showDialog(List<MultiAmountMessage> messages, int min, int max, Map<String, Serializable> options) {
this.header.setText((String) options.get("header")); this.header.setText((String) options.get("header"));
this.header.setHorizontalAlignment(SwingConstants.CENTER); this.header.setHorizontalAlignment(SwingConstants.CENTER);
this.setTitle((String) options.get("title")); this.setTitle((String) options.get("title"));
@ -51,7 +53,7 @@ public class PickMultiNumberDialog extends MageDialog {
// mana mode // mana mode
String manaText = null; String manaText = null;
String input = messages.get(i); String input = messages.get(i).message;
switch (input) { switch (input) {
case "W": case "W":
manaText = ColoredManaSymbol.W.getColorHtmlName(); manaText = ColoredManaSymbol.W.getColorHtmlName();
@ -87,13 +89,13 @@ public class PickMultiNumberDialog extends MageDialog {
labelList.add(label); labelList.add(label);
JSpinner spinner = new JSpinner(); JSpinner spinner = new JSpinner();
spinner.setModel(new SpinnerNumberModel(0, 0, max, 1)); spinner.setModel(new SpinnerNumberModel(0, messages.get(i).min, messages.get(i).max, 1));
spinnerC.weightx = 0.5; spinnerC.weightx = 0.5;
spinnerC.gridx = 1; spinnerC.gridx = 1;
spinnerC.gridy = i; spinnerC.gridy = i;
spinnerC.ipadx = 20; spinnerC.ipadx = 20;
spinner.addChangeListener(e -> { spinner.addChangeListener(e -> {
updateControls(min, max); updateControls(min, max, messages);
}); });
jPanel1.add(spinner, spinnerC); jPanel1.add(spinner, spinnerC);
spinnerList.add(spinner); spinnerList.add(spinner);
@ -101,20 +103,28 @@ public class PickMultiNumberDialog extends MageDialog {
this.counterText.setText("0 out of 0"); this.counterText.setText("0 out of 0");
this.counterText.setHorizontalAlignment(SwingConstants.CENTER); this.counterText.setHorizontalAlignment(SwingConstants.CENTER);
updateControls(min, max); updateControls(min, max, messages);
this.pack(); this.pack();
this.makeWindowCentered(); this.makeWindowCentered();
this.setVisible(true); this.setVisible(true);
} }
private void updateControls(int min, int max) { private void updateControls(int min, int max, List<MultiAmountMessage> messages) {
int totalChosenAmount = 0; int totalChosenAmount = 0;
for (JSpinner jSpinner : spinnerList) { boolean chooseEnabled = true;
totalChosenAmount += ((Number) jSpinner.getValue()).intValue();
for (int i = 0; i < spinnerList.size(); i++) {
JSpinner jSpinner = spinnerList.get(i);
int value = ((Number) jSpinner.getValue()).intValue();
totalChosenAmount += value;
chooseEnabled &= value >= messages.get(i).min && value <= messages.get(i).max;
} }
counterText.setText(totalChosenAmount + " out of " + max); counterText.setText(totalChosenAmount + " out of " + max);
chooseButton.setEnabled(totalChosenAmount >= min && totalChosenAmount <= max);
chooseEnabled &= totalChosenAmount >= min && totalChosenAmount <= max;
chooseButton.setEnabled(chooseEnabled);
} }
public String getMultiAmount() { public String getMultiAmount() {

View file

@ -28,6 +28,7 @@ import mage.constants.*;
import mage.game.events.PlayerQueryEvent; import mage.game.events.PlayerQueryEvent;
import mage.players.PlayableObjectStats; import mage.players.PlayableObjectStats;
import mage.players.PlayableObjectsList; import mage.players.PlayableObjectsList;
import mage.util.MultiAmountMessage;
import mage.view.*; import mage.view.*;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.mage.plugins.card.utils.impl.ImageManagerImpl; import org.mage.plugins.card.utils.impl.ImageManagerImpl;
@ -1774,7 +1775,8 @@ public final class GamePanel extends javax.swing.JPanel {
} }
} }
public void getMultiAmount(List<String> messages, GameView gameView, Map<String, Serializable> options, int min, int max) { public void getMultiAmount(List<MultiAmountMessage> messages, GameView gameView, Map<String, Serializable> options,
int min, int max) {
updateGame(gameView, false, options, null); updateGame(gameView, false, options, null);
hideAll(); hideAll();
DialogManager.getManager(gameId).fadeOut(); DialogManager.getManager(gameId).fadeOut();

View file

@ -13,6 +13,7 @@ import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose; import com.google.gson.annotations.Expose;
import mage.choices.Choice; import mage.choices.Choice;
import mage.util.MultiAmountMessage;
/** /**
* *
@ -42,7 +43,7 @@ public class GameClientMessage implements Serializable {
@Expose @Expose
private Choice choice; private Choice choice;
@Expose @Expose
private List<String> messages; private List<MultiAmountMessage> messages;
public GameClientMessage(GameView gameView, Map<String, Serializable> options) { public GameClientMessage(GameView gameView, Map<String, Serializable> options) {
this.gameView = gameView; this.gameView = gameView;
@ -80,7 +81,8 @@ public class GameClientMessage implements Serializable {
this.cardsView2 = pile2; this.cardsView2 = pile2;
} }
public GameClientMessage(GameView gameView, Map<String, Serializable> options, List<String> messages, int min, int max) { public GameClientMessage(GameView gameView, Map<String, Serializable> options, List<MultiAmountMessage> messages,
int min, int max) {
this.gameView = gameView; this.gameView = gameView;
this.options = options; this.options = options;
this.messages = messages; this.messages = messages;
@ -134,7 +136,7 @@ public class GameClientMessage implements Serializable {
return choice; return choice;
} }
public List<String> getMessages() { public List<MultiAmountMessage> getMessages() {
return messages; return messages;
} }

View file

@ -2169,11 +2169,12 @@ public class ComputerPlayer extends PlayerImpl implements Player {
} }
@Override @Override
public List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) { public List<Integer> getMultiAmountWithIndividualConstraints(Outcome outcome, List<MultiAmountMessage> messages,
int min, int max, MultiAmountType type, Game game) {
log.debug("getMultiAmount"); log.debug("getMultiAmount");
int needCount = messages.size(); int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, min, max);
if (needCount == 0) { if (needCount == 0) {
return defaultList; return defaultList;
} }
@ -2188,7 +2189,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
// GOOD effect // GOOD effect
// values must be stable, so AI must able to simulate it and choose correct actions // values must be stable, so AI must able to simulate it and choose correct actions
// fill max values as much as possible // fill max values as much as possible
return MultiAmountType.prepareMaxValues(needCount, min, max); return MultiAmountType.prepareMaxValues(messages, min, max);
} }
@Override @Override

View file

@ -45,6 +45,8 @@ import mage.util.CardUtil;
import mage.util.GameLog; import mage.util.GameLog;
import mage.util.ManaUtil; import mage.util.ManaUtil;
import mage.util.MessageToClient; import mage.util.MessageToClient;
import mage.util.MultiAmountMessage;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.awt.*; import java.awt.*;
@ -2049,10 +2051,12 @@ public class HumanPlayer extends PlayerImpl {
} }
@Override @Override
public List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) { public List<Integer> getMultiAmountWithIndividualConstraints(Outcome outcome, List<MultiAmountMessage> messages,
int min, int max, MultiAmountType type, Game game) {
int needCount = messages.size(); int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, min, max);
if (needCount == 0) { if (needCount == 0 || (needCount == 1 && min == max)
|| messages.stream().map(m -> m.min == m.max).reduce(true, Boolean::logicalAnd)) {
return defaultList; return defaultList;
} }
@ -2074,8 +2078,8 @@ public class HumanPlayer extends PlayerImpl {
// waiting correct values only // waiting correct values only
if (response.getString() != null) { if (response.getString() != null) {
answer = MultiAmountType.parseAnswer(response.getString(), needCount, min, max, false); answer = MultiAmountType.parseAnswer(response.getString(), messages, min, max, false);
if (MultiAmountType.isGoodValues(answer, needCount, min, max)) { if (MultiAmountType.isGoodValues(answer, messages, min, max)) {
break; break;
} else { } else {
// it's not normal: can be cheater or a wrong GUI checks // it's not normal: can be cheater or a wrong GUI checks

View file

@ -28,6 +28,7 @@ import mage.server.User;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.util.Splitter; import mage.server.util.Splitter;
import mage.server.util.SystemUtil; import mage.server.util.SystemUtil;
import mage.util.MultiAmountMessage;
import mage.utils.StreamUtils; import mage.utils.StreamUtils;
import mage.utils.timer.PriorityTimer; import mage.utils.timer.PriorityTimer;
import mage.view.*; import mage.view.*;
@ -870,7 +871,9 @@ public class GameController implements GameCallback {
perform(playerId, playerId1 -> getGameSession(playerId1).getAmount(message, min, max)); 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 { private synchronized void multiAmount(UUID playerId, final List<MultiAmountMessage> messages,
final int min, final int max, final Map<String, Serializable> options)
throws MageException {
perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options)); perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options));
} }

View file

@ -13,6 +13,7 @@ import mage.players.Player;
import mage.server.User; import mage.server.User;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.managers.UserManager; import mage.server.managers.UserManager;
import mage.util.MultiAmountMessage;
import mage.view.*; import mage.view.*;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@ -114,7 +115,8 @@ public class GameSessionPlayer extends GameSessionWatcher {
} }
} }
public void getMultiAmount(final List<String> messages, final int min, final int max, final Map<String, Serializable> options) { public void getMultiAmount(final List<MultiAmountMessage> messages, final int min, final int max,
final Map<String, Serializable> options) {
if (!killed) { if (!killed) {
userManager.getUser(userId).ifPresent(user userManager.getUser(userId).ifPresent(user
-> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(getGameView(), options, messages, min, max)))); -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(getGameView(), options, messages, min, max))));

View file

@ -0,0 +1,183 @@
package mage.cards.g;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.util.MultiAmountMessage;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author alexander_novo
*/
public final class GoldberryRiverDaughter extends CardImpl {
private static final FilterPermanent filter = new FilterControlledPermanent("another target permanent you control");
static {
filter.add(AnotherPredicate.instance);
}
public GoldberryRiverDaughter(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[] { CardType.CREATURE }, "{1}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.NYMPH);
this.power = new MageInt(1);
this.toughness = new MageInt(3);
// {T}: Move a counter of each kind not on Goldberry, River-Daughter from another target permanent you control onto Goldberry.
SimpleActivatedAbility abilityFrom = new SimpleActivatedAbility(new GoldberryRiverDaughterFromEffect(),
new TapSourceCost());
abilityFrom.addTarget(new TargetPermanent(filter));
this.addAbility(abilityFrom);
// {U}, {T}: Move one or more counters from Goldberry onto another target permanent you control. If you do, draw a card.
SimpleActivatedAbility abilityTo = new SimpleActivatedAbility(new GoldberryRiverDaughterToEffect(),
new ManaCostsImpl<>("{U}"));
abilityTo.addCost(new TapSourceCost());
abilityTo.addTarget(new TargetPermanent(filter));
this.addAbility(abilityTo);
}
private GoldberryRiverDaughter(final GoldberryRiverDaughter card) {
super(card);
}
@Override
public GoldberryRiverDaughter copy() {
return new GoldberryRiverDaughter(this);
}
}
class GoldberryRiverDaughterFromEffect extends OneShotEffect {
GoldberryRiverDaughterFromEffect() {
super(Outcome.Neutral);
staticText = "Move a counter of each kind not on {this} from another target permanent you control onto Goldberry.";
}
private GoldberryRiverDaughterFromEffect(final GoldberryRiverDaughterFromEffect effect) {
super(effect);
}
@Override
public GoldberryRiverDaughterFromEffect copy() {
return new GoldberryRiverDaughterFromEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Permanent fromPermanent = game.getPermanent(source.getFirstTarget());
Permanent toPermanent = game.getPermanent(source.getSourceId());
// Create a set of all of the unique counter types on the target permanent that aren't on Goldberry
Set<Counter> fromCounters = new HashSet<Counter>(fromPermanent.getCounters(game).values());
fromCounters.removeAll(toPermanent.getCounters(game).values());
if (fromPermanent == null
|| toPermanent == null
|| controller == null
|| fromCounters.size() == 0) {
return false;
}
for (Counter counter : fromCounters) {
fromPermanent.removeCounters(counter.getName(), 1, source, game);
toPermanent.addCounters(CounterType.findByName(counter.getName()).createInstance(1),
source.getControllerId(), source, game);
}
return true;
}
}
class GoldberryRiverDaughterToEffect extends OneShotEffect {
GoldberryRiverDaughterToEffect() {
super(Outcome.Neutral);
staticText = "Move one or more counters from Goldberry onto another target permanent you control. If you do, draw a card.";
}
private GoldberryRiverDaughterToEffect(final GoldberryRiverDaughterToEffect effect) {
super(effect);
}
@Override
public GoldberryRiverDaughterToEffect copy() {
return new GoldberryRiverDaughterToEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Permanent toPermanent = game.getPermanent(source.getFirstTarget());
Permanent fromPermanent = game.getPermanent(source.getSourceId());
if (fromPermanent == null
|| toPermanent == null
|| controller == null
|| fromPermanent.getCounters(game).size() == 0) {
return false;
}
List<Counter> counters = new ArrayList<>(fromPermanent.getCounters(game).values());
counters.sort((c1, c2) -> c1.getName().compareTo(c2.getName()));
List<MultiAmountMessage> messages = counters.stream()
.map(c -> new MultiAmountMessage(c.getName() + " (" + c.getCount() + ")", 0, c.getCount()))
.collect(Collectors.toList());
int max = messages.stream().map(m -> m.max).reduce(0, Integer::sum);
int total;
List<Integer> choices;
do {
choices = controller.getMultiAmountWithIndividualConstraints(Outcome.Neutral, messages, 1,
max, MultiAmountType.COUNTERS, game);
total = choices.stream().reduce(0, Integer::sum);
} while (total < 1);
// Move the counters. Make sure some counters were actually moved.
boolean movedCounters = false;
for (int i = 0; i < choices.size(); i++) {
Integer amount = choices.get(i);
if (amount > 0) {
String counterName = counters.get(i).getName();
movedCounters |= toPermanent.addCounters(
CounterType.findByName(counterName).createInstance(amount),
source,
game);
fromPermanent.removeCounters(counterName, amount, source, game);
game.informPlayers(
controller.getLogName() + "moved " +
amount + " " +
counterName + " counter" + (amount > 1 ? "s" : "") +
" from " + fromPermanent.getLogName() +
"to " + toPermanent.getLogName() + ".");
}
}
// If some counters were actually moved, draw a card
if (movedCounters) {
controller.drawCards(1, source, game);
}
return false;
}
}

View file

@ -9,6 +9,7 @@ import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.MultiAmountType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.counters.Counter; import mage.counters.Counter;
import mage.counters.CounterType; import mage.counters.CounterType;
@ -24,10 +25,12 @@ import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetControlledPermanent;
import mage.util.MultiAmountMessage;
import java.util.HashMap; import java.util.ArrayList;
import java.util.Map; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
/** /**
* @author Alex-Vasile * @author Alex-Vasile
@ -92,33 +95,39 @@ class ResourcefulDefenseMoveCounterEffect extends OneShotEffect {
return false; return false;
} }
// Counter name and how many to move List<Counter> counters = new ArrayList<>(fromPermanent.getCounters(game).values());
Map<String, Integer> counterMap = new HashMap<>(); counters.sort((c1, c2) -> c1.getName().compareTo(c2.getName()));
for (Map.Entry<String, Counter> entry : fromPermanent.getCounters(game).entrySet()) {
int num = controller.getAmount(
0,
entry.getValue().getCount(),
"Choose how many " + entry.getKey() +
" counters to remove from " + fromPermanent.getLogName(),
game);
int newAmount = num + counterMap.getOrDefault(entry.getKey(), 0);
counterMap.put(entry.getKey(), newAmount);
}
// Move the counters List<MultiAmountMessage> messages = counters.stream()
for (String counterName : counterMap.keySet()) { .map(c -> new MultiAmountMessage(c.getName() + " (" + c.getCount() + ")", 0, c.getCount()))
toPermanent.addCounters( .collect(Collectors.toList());
CounterType.findByName(counterName).createInstance(counterMap.get(counterName)), int max = messages.stream().map(m -> m.max).reduce(0, Integer::sum);
source,
game); int total;
fromPermanent.removeCounters(counterName, counterMap.get(counterName), source, game); List<Integer> choices;
do {
choices = controller.getMultiAmountWithIndividualConstraints(Outcome.Neutral, messages, 0,
max, MultiAmountType.COUNTERS, game);
total = choices.stream().reduce(0, Integer::sum);
} while (total < 0);
// Move the counters. Make sure some counters were actually moved.
for (int i = 0; i < choices.size(); i++) {
Integer amount = choices.get(i);
if (amount > 0) {
String counterName = counters.get(i).getName();
toPermanent.addCounters(CounterType.findByName(counterName).createInstance(amount), source, game);
fromPermanent.removeCounters(counterName, amount, source, game);
game.informPlayers( game.informPlayers(
controller.getLogName() + "moved " + controller.getLogName() + "moved " +
counterMap.get(counterName) + " " + amount + " " +
counterName + "counter" + (counterMap.get(counterName) > 1 ? "s" : "") + counterName + " counter" + (amount > 1 ? "s" : "") +
"from " + fromPermanent.getLogName() + " from " + fromPermanent.getLogName() +
"to " + toPermanent.getLogName() + "." "to " + toPermanent.getLogName() + ".");
); }
} }
return true; return true;

View file

@ -125,6 +125,7 @@ public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet {
cards.add(new SetCardInfo("Glorious Gale", 51, Rarity.COMMON, mage.cards.g.GloriousGale.class)); cards.add(new SetCardInfo("Glorious Gale", 51, Rarity.COMMON, mage.cards.g.GloriousGale.class));
cards.add(new SetCardInfo("Goblin Assailant", 295, Rarity.COMMON, mage.cards.g.GoblinAssailant.class)); cards.add(new SetCardInfo("Goblin Assailant", 295, Rarity.COMMON, mage.cards.g.GoblinAssailant.class));
cards.add(new SetCardInfo("Goblin Fireleaper", 133, Rarity.UNCOMMON, mage.cards.g.GoblinFireleaper.class)); cards.add(new SetCardInfo("Goblin Fireleaper", 133, Rarity.UNCOMMON, mage.cards.g.GoblinFireleaper.class));
cards.add(new SetCardInfo("Goldberry, River-Daughter", 52, Rarity.RARE, mage.cards.g.GoldberryRiverDaughter.class));
cards.add(new SetCardInfo("Gollum's Bite", 85, Rarity.UNCOMMON, mage.cards.g.GollumsBite.class)); cards.add(new SetCardInfo("Gollum's Bite", 85, Rarity.UNCOMMON, mage.cards.g.GollumsBite.class));
cards.add(new SetCardInfo("Gollum, Patient Plotter", 84, Rarity.UNCOMMON, mage.cards.g.GollumPatientPlotter.class)); cards.add(new SetCardInfo("Gollum, Patient Plotter", 84, Rarity.UNCOMMON, mage.cards.g.GollumPatientPlotter.class));
cards.add(new SetCardInfo("Gollum, Scheming Guide", 292, Rarity.RARE, mage.cards.g.GollumSchemingGuide.class)); cards.add(new SetCardInfo("Gollum, Scheming Guide", 292, Rarity.RARE, mage.cards.g.GollumSchemingGuide.class));

View file

@ -0,0 +1,145 @@
package org.mage.test.cards.single.ltr;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
public class GoldberryRiverDaughterTest extends CardTestPlayerBase {
static final String goldberry = "Goldberry, River-Daughter";
static final String ability1 = "{T}: Move a counter of each kind not on {this} from another target permanent you control onto Goldberry.";
static final String ability2 = "{U}, {T}: Move one or more counters from Goldberry onto another target permanent you control. If you do, draw a card.";
@Test
// Author: alexander-novo
// Happy path test - remove some counters from something. Then put some of them on something else.
public void testHappyPath() {
CounterType counter1 = CounterType.ACORN;
CounterType counter2 = CounterType.AEGIS;
String island = "Island";
addCard(Zone.BATTLEFIELD, playerA, goldberry, 1);
addCard(Zone.BATTLEFIELD, playerA, island, 1);
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, island, counter1, 2);
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, island, counter2, 1);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, ability1, island);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
checkPermanentCounters("First Ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, island, counter1, 1);
checkPermanentCounters("First Ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, island, counter2, 0);
checkPermanentCounters("First Ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, goldberry, counter1, 1);
checkPermanentCounters("First Ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, goldberry, counter2, 1);
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, ability2, island);
waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN, 1);
setChoiceAmount(playerA, 0, 1); // acorn, aegis counters
setStrictChooseMode(true);
setStopAt(3, PhaseStep.PRECOMBAT_MAIN);
execute();
assertCounterCount(island, counter1, 1);
assertCounterCount(island, counter2, 1);
assertCounterCount(goldberry, counter1, 1);
assertCounterCount(goldberry, counter2, 0);
// One from turn 2 draw, one from goldberry
assertHandCount(playerA, 2);
}
@Test
// Author: alexander-novo
// Unhappy path - Try to remove some counters from something when some of those counters are already on Goldberry
public void testCounterAlreadyOnGoldberry() {
CounterType counter = CounterType.ACORN;
String island = "Island";
addCard(Zone.BATTLEFIELD, playerA, goldberry, 1);
addCard(Zone.BATTLEFIELD, playerA, island, 1);
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, island, counter, 1);
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, goldberry, counter, 1);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, ability1, island);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertCounterCount(island, counter, 1);
assertCounterCount(goldberry, counter, 1);
}
@Test(expected = AssertionError.class)
// Author: alexander-novo
// Unhappy path - Try to not move a counter from Goldberry even though she has a counter on her.
// Should fail since we are attempting to move 0 counters, even though we must move at least one if possible.
public void testNotMovingCounter() {
CounterType counter = CounterType.ACORN;
String island = "Island";
addCard(Zone.BATTLEFIELD, playerA, goldberry, 1);
addCard(Zone.BATTLEFIELD, playerA, island, 1);
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, goldberry, counter, 1);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, ability2, island);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
setChoiceAmount(playerA, 0); // Try to remove 0 counters from goldberry
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertCounterCount(goldberry, counter, 0);
assertCounterCount(island, counter, 1);
assertHandCount(playerA, 1);
}
@Test
// Author: alexander-novo
// Unhappy path - Activate second ability with no counters on Goldberry
public void testNoCounters() {
String island = "Island";
addCard(Zone.BATTLEFIELD, playerA, goldberry, 1);
addCard(Zone.BATTLEFIELD, playerA, island, 1);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, ability2, island);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertHandCount(playerA, 0);
}
@Test
// Author: alexander-novo
// Bug - Goldberry doesn't seem to get some of the effects from some of the counters she takes
public void testM1M1Counters() {
CounterType counter = CounterType.M1M1;
String island = "Island";
addCard(Zone.BATTLEFIELD, playerA, goldberry, 1);
addCard(Zone.BATTLEFIELD, playerA, island, 1);
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, island, counter, 1);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, ability1, island);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertCounterCount(goldberry, counter, 1);
assertPowerToughness(playerA, goldberry, 0, 2);
}
}

View file

@ -115,8 +115,7 @@ public class ResourcefulDefenseTest extends CardTestPlayerBase {
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: "); activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: ");
addTarget(playerA, steelbaneHydra); addTarget(playerA, steelbaneHydra);
addTarget(playerA, vividCreek); addTarget(playerA, vividCreek);
setChoiceAmount(playerA, 2); setChoiceAmount(playerA, 1, 2); // +1/+1, Charge
setChoiceAmount(playerA, 1);
setStopAt(3, PhaseStep.END_TURN); setStopAt(3, PhaseStep.END_TURN);
execute(); execute();

View file

@ -3,6 +3,8 @@ package org.mage.test.cards.targets;
import mage.constants.MultiAmountType; import mage.constants.MultiAmountType;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.util.MultiAmountMessage;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.player.TestPlayer; import org.mage.test.player.TestPlayer;
@ -10,6 +12,7 @@ import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
/** /**
* @author JayDi85 * @author JayDi85
@ -20,117 +23,132 @@ public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
@Test @Test
public void test_DefaultValues() { public void test_DefaultValues() {
// default values must be assigned from first to last by minimum values // default values must be assigned from first to last by minimum values
assertDefaultValues("", 0, 0, 0); assertDefaultValuesUnconstrained("", 0, 0, 0);
// //
assertDefaultValues("0", 1, 0, 0); assertDefaultValuesUnconstrained("0", 1, 0, 0);
assertDefaultValues("0 0", 2, 0, 0); assertDefaultValuesUnconstrained("0 0", 2, 0, 0);
assertDefaultValues("0 0 0", 3, 0, 0); assertDefaultValuesUnconstrained("0 0 0", 3, 0, 0);
// //
assertDefaultValues("1", 1, 1, 1); assertDefaultValuesUnconstrained("1", 1, 1, 1);
assertDefaultValues("1 0", 2, 1, 1); assertDefaultValuesUnconstrained("1 0", 2, 1, 1);
assertDefaultValues("1 0 0", 3, 1, 1); assertDefaultValuesUnconstrained("1 0 0", 3, 1, 1);
// //
assertDefaultValues("1", 1, 1, 2); assertDefaultValuesUnconstrained("1", 1, 1, 2);
assertDefaultValues("1 0", 2, 1, 2); assertDefaultValuesUnconstrained("1 0", 2, 1, 2);
assertDefaultValues("1 0 0", 3, 1, 2); assertDefaultValuesUnconstrained("1 0 0", 3, 1, 2);
// //
assertDefaultValues("2", 1, 2, 2); assertDefaultValuesUnconstrained("2", 1, 2, 2);
assertDefaultValues("2 0", 2, 2, 2); assertDefaultValuesUnconstrained("2 0", 2, 2, 2);
assertDefaultValues("2 0 0", 3, 2, 2); assertDefaultValuesUnconstrained("2 0 0", 3, 2, 2);
// //
assertDefaultValues("2", 1, 2, 10); assertDefaultValuesUnconstrained("2", 1, 2, 10);
assertDefaultValues("2 0", 2, 2, 10); assertDefaultValuesUnconstrained("2 0", 2, 2, 10);
assertDefaultValues("2 0 0", 3, 2, 10); assertDefaultValuesUnconstrained("2 0 0", 3, 2, 10);
// //
// performance test // performance test
assertDefaultValues("2 0 0", 3, 2, Integer.MAX_VALUE); assertDefaultValuesUnconstrained("2 0 0", 3, 2, Integer.MAX_VALUE);
} }
private void assertDefaultValues(String need, int count, int min, int max) { private List<MultiAmountMessage> getUnconstrainedConstraints(int count) {
List<Integer> defaultValues = MultiAmountType.prepareDefaltValues(count, min, max); return IntStream.range(0, count).mapToObj(i -> new MultiAmountMessage("", Integer.MIN_VALUE, Integer.MAX_VALUE))
.collect(Collectors.toList());
}
private void assertDefaultValuesUnconstrained(String need, int count, int min, int max) {
List<MultiAmountMessage> constraints = getUnconstrainedConstraints(count);
List<Integer> defaultValues = MultiAmountType.prepareDefaltValues(constraints, min, max);
String current = defaultValues String current = defaultValues
.stream() .stream()
.map(String::valueOf) .map(String::valueOf)
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" "));
Assert.assertEquals("default values", need, current); Assert.assertEquals("default values", need, current);
Assert.assertTrue("default values must be good", MultiAmountType.isGoodValues(defaultValues, count, min, max)); Assert.assertTrue("default values must be good",
MultiAmountType.isGoodValues(defaultValues, constraints, min, max));
} }
@Test @Test
public void test_MaxValues() { public void test_MaxValues() {
// max possible values must be assigned from first to last by max possible values // max possible values must be assigned from first to last by max possible values
assertMaxValues("", 0, 0, 0); assertMaxValuesUnconstrained("", 0, 0, 0);
// //
assertMaxValues("0", 1, 0, 0); assertMaxValuesUnconstrained("0", 1, 0, 0);
assertMaxValues("0 0", 2, 0, 0); assertMaxValuesUnconstrained("0 0", 2, 0, 0);
assertMaxValues("0 0 0", 3, 0, 0); assertMaxValuesUnconstrained("0 0 0", 3, 0, 0);
// //
assertMaxValues("1", 1, 1, 1); assertMaxValuesUnconstrained("1", 1, 1, 1);
assertMaxValues("1 0", 2, 1, 1); assertMaxValuesUnconstrained("1 0", 2, 1, 1);
assertMaxValues("1 0 0", 3, 1, 1); assertMaxValuesUnconstrained("1 0 0", 3, 1, 1);
// //
assertMaxValues("2", 1, 1, 2); assertMaxValuesUnconstrained("2", 1, 1, 2);
assertMaxValues("1 1", 2, 1, 2); assertMaxValuesUnconstrained("1 1", 2, 1, 2);
assertMaxValues("1 1 0", 3, 1, 2); assertMaxValuesUnconstrained("1 1 0", 3, 1, 2);
// //
assertMaxValues("2", 1, 2, 2); assertMaxValuesUnconstrained("2", 1, 2, 2);
assertMaxValues("1 1", 2, 2, 2); assertMaxValuesUnconstrained("1 1", 2, 2, 2);
assertMaxValues("1 1 0", 3, 2, 2); assertMaxValuesUnconstrained("1 1 0", 3, 2, 2);
// //
assertMaxValues("10", 1, 2, 10); assertMaxValuesUnconstrained("10", 1, 2, 10);
assertMaxValues("5 5", 2, 2, 10); assertMaxValuesUnconstrained("5 5", 2, 2, 10);
assertMaxValues("4 3 3", 3, 2, 10); assertMaxValuesUnconstrained("4 3 3", 3, 2, 10);
// //
assertMaxValues("1 1 1 1 1 0 0 0 0 0", 10, 2, 5); assertMaxValuesUnconstrained("1 1 1 1 1 0 0 0 0 0", 10, 2, 5);
// //
// performance test // performance test
assertMaxValues(String.valueOf(Integer.MAX_VALUE), 1, 2, Integer.MAX_VALUE); assertMaxValuesUnconstrained(String.valueOf(Integer.MAX_VALUE), 1, 2, Integer.MAX_VALUE);
int part = Integer.MAX_VALUE / 3; int part = Integer.MAX_VALUE / 3;
String need = String.format("%d %d %d", part + 1, part, part); String need = String.format("%d %d %d", part + 1, part, part);
assertMaxValues(need, 3, 2, Integer.MAX_VALUE); assertMaxValuesUnconstrained(need, 3, 2, Integer.MAX_VALUE);
} }
private void assertMaxValues(String need, int count, int min, int max) { private void assertMaxValuesUnconstrained(String need, int count, int min, int max) {
List<Integer> maxValues = MultiAmountType.prepareMaxValues(count, min, max); List<MultiAmountMessage> constraints = getUnconstrainedConstraints(count);
List<Integer> maxValues = MultiAmountType.prepareMaxValues(constraints, min, max);
String current = maxValues String current = maxValues
.stream() .stream()
.map(String::valueOf) .map(String::valueOf)
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" "));
Assert.assertEquals("max values", need, current); Assert.assertEquals("max values", need, current);
Assert.assertTrue("max values must be good", MultiAmountType.isGoodValues(maxValues, count, min, max)); Assert.assertTrue("max values must be good", MultiAmountType.isGoodValues(maxValues, constraints, min, max));
} }
@Test @Test
public void test_GoodValues() { public void test_GoodValues() {
List<List<MultiAmountMessage>> constraints = List.of(
getUnconstrainedConstraints(0),
getUnconstrainedConstraints(1),
getUnconstrainedConstraints(2),
getUnconstrainedConstraints(3),
getUnconstrainedConstraints(4));
// good values are checking in test_DefaultValues, it's an additional // good values are checking in test_DefaultValues, it's an additional
List<Integer> list = MultiAmountType.prepareDefaltValues(3, 0, 0); List<Integer> list = MultiAmountType.prepareDefaltValues(constraints.get(3), 0, 0);
// count (0, 0, 0) // count (0, 0, 0)
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 0, 0, 0)); Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(0), 0, 0));
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 1, 0, 0)); Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(1), 0, 0));
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 2, 0, 0)); Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(2), 0, 0));
Assert.assertTrue("count", MultiAmountType.isGoodValues(list, 3, 0, 0)); Assert.assertTrue("count", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 0));
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 4, 0, 0)); Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(4), 0, 0));
// min (0, 1, 1) // min (0, 1, 1)
list.set(0, 0); list.set(0, 0);
list.set(1, 1); list.set(1, 1);
list.set(2, 1); list.set(2, 1);
Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 0, 100)); Assert.assertTrue("min", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 100));
Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 1, 100)); Assert.assertTrue("min", MultiAmountType.isGoodValues(list, constraints.get(3), 1, 100));
Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 2, 100)); Assert.assertTrue("min", MultiAmountType.isGoodValues(list, constraints.get(3), 2, 100));
Assert.assertFalse("min", MultiAmountType.isGoodValues(list, 3, 3, 100)); Assert.assertFalse("min", MultiAmountType.isGoodValues(list, constraints.get(3), 3, 100));
Assert.assertFalse("min", MultiAmountType.isGoodValues(list, 3, 4, 100)); Assert.assertFalse("min", MultiAmountType.isGoodValues(list, constraints.get(3), 4, 100));
// max (0, 1, 1) // max (0, 1, 1)
list.set(0, 0); list.set(0, 0);
list.set(1, 1); list.set(1, 1);
list.set(2, 1); list.set(2, 1);
Assert.assertFalse("max", MultiAmountType.isGoodValues(list, 3, 0, 0)); Assert.assertFalse("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 0));
Assert.assertFalse("max", MultiAmountType.isGoodValues(list, 3, 0, 1)); Assert.assertFalse("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 1));
Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 2)); Assert.assertTrue("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 2));
Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 3)); Assert.assertTrue("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 3));
Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 4)); Assert.assertTrue("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 4));
} }
@Test @Test
@ -138,32 +156,36 @@ public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
// parse must use correct values on good data or default values on broken data // parse must use correct values on good data or default values on broken data
// simple parse without data check // simple parse without data check
assertParse("", 3, 1, 3, "", false); assertParseUnconstrained("", 3, 1, 3, "", false);
assertParse("1", 3, 1, 3, "1", false); assertParseUnconstrained("1", 3, 1, 3, "1", false);
assertParse("0 0 0", 3, 1, 3, "0 0 0", false); assertParseUnconstrained("0 0 0", 3, 1, 3, "0 0 0", false);
assertParse("1 0 3", 3, 1, 3, "1 0 3", false); assertParseUnconstrained("1 0 3", 3, 1, 3, "1 0 3", false);
assertParse("0 5 0 6", 3, 1, 3, "1,text 5 4. 6", false); assertParseUnconstrained("0 5 0 6", 3, 1, 3, "1,text 5 4. 6", false);
// parse with data check - good data // parse with data check - good data
assertParse("1 0 2", 3, 0, 3, "1 0 2", true); assertParseUnconstrained("1 0 2", 3, 0, 3, "1 0 2", true);
// parse with data check - broken data (must return defalt - 1 0 0) // parse with data check - broken data (must return defalt - 1 0 0)
assertParse("1 0 0", 3, 1, 3, "", true); assertParseUnconstrained("1 0 0", 3, 1, 3, "", true);
assertParse("1 0 0", 3, 1, 3, "1", true); assertParseUnconstrained("1 0 0", 3, 1, 3, "1", true);
assertParse("1 0 0", 3, 1, 3, "0 0 0", true); assertParseUnconstrained("1 0 0", 3, 1, 3, "0 0 0", true);
assertParse("1 0 0", 3, 1, 3, "1 0 3", true); assertParseUnconstrained("1 0 0", 3, 1, 3, "1 0 3", true);
assertParse("1 0 0", 3, 1, 3, "1,text 4.", true); assertParseUnconstrained("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) { private void assertParseUnconstrained(String need, int count, int min, int max, String answerToParse,
List<Integer> parsedValues = MultiAmountType.parseAnswer(answerToParse, count, min, max, returnDefaultOnError); Boolean returnDefaultOnError) {
List<MultiAmountMessage> constraints = getUnconstrainedConstraints(count);
List<Integer> parsedValues = MultiAmountType.parseAnswer(answerToParse, constraints, min, max,
returnDefaultOnError);
String current = parsedValues String current = parsedValues
.stream() .stream()
.map(String::valueOf) .map(String::valueOf)
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" "));
Assert.assertEquals("parsed values", need, current); Assert.assertEquals("parsed values", need, current);
if (returnDefaultOnError) { if (returnDefaultOnError) {
Assert.assertTrue("parsed values must be good", MultiAmountType.isGoodValues(parsedValues, count, min, max)); Assert.assertTrue("parsed values must be good",
MultiAmountType.isGoodValues(parsedValues, constraints, min, max));
} }
} }

View file

@ -49,6 +49,7 @@ import mage.players.net.UserData;
import mage.target.*; import mage.target.*;
import mage.target.common.*; import mage.target.common.*;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.MultiAmountMessage;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.junit.Assert; import org.junit.Assert;
@ -2831,11 +2832,12 @@ public class TestPlayer implements Player {
} }
@Override @Override
public List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) { public List<Integer> getMultiAmountWithIndividualConstraints(Outcome outcome, List<MultiAmountMessage> messages,
int min, int max, MultiAmountType type, Game game) {
assertAliasSupportInChoices(false); assertAliasSupportInChoices(false);
int needCount = messages.size(); int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, min, max);
if (needCount == 0) { if (needCount == 0) {
return defaultList; return defaultList;
} }
@ -2861,7 +2863,7 @@ public class TestPlayer implements Player {
} }
// extra check // extra check
if (!MultiAmountType.isGoodValues(answer, needCount, min, max)) { if (!MultiAmountType.isGoodValues(answer, messages, min, max)) {
Assert.fail("Wrong choices in multi amount: " + answer Assert.fail("Wrong choices in multi amount: " + answer
.stream() .stream()
.map(String::valueOf) .map(String::valueOf)
@ -2872,7 +2874,7 @@ public class TestPlayer implements Player {
} }
this.chooseStrictModeFailed("choice", game, "Multi amount: " + type.getHeader()); this.chooseStrictModeFailed("choice", game, "Multi amount: " + type.getHeader());
return computerPlayer.getMultiAmount(outcome, messages, min, max, type, game); return computerPlayer.getMultiAmountWithIndividualConstraints(outcome, messages, min, max, type, game);
} }
@Override @Override

View file

@ -42,6 +42,7 @@ import mage.target.Target;
import mage.target.TargetAmount; import mage.target.TargetAmount;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetCardInLibrary;
import mage.util.MultiAmountMessage;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
@ -983,7 +984,8 @@ public class PlayerStub implements Player {
} }
@Override @Override
public List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) { public List<Integer> getMultiAmountWithIndividualConstraints(Outcome outcome, List<MultiAmountMessage> messages,
int min, int max, MultiAmountType type, Game game) {
return null; return null;
} }

View file

@ -1,19 +1,19 @@
package mage.constants; package mage.constants;
import com.google.common.collect.Iterables;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.MultiAmountMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.stream.IntStream; import java.util.stream.Collectors;
public enum MultiAmountType { public enum MultiAmountType {
MANA("Add mana", "Distribute mana among colors"), MANA("Add mana", "Distribute mana among colors"),
DAMAGE("Assign damage", "Assign damage among targets"), DAMAGE("Assign damage", "Assign damage among targets"),
P1P1("Add +1/+1 counters", "Distribute +1/+1 counters among creatures"); P1P1("Add +1/+1 counters", "Distribute +1/+1 counters among creatures"),
COUNTERS("Choose counters", "Move counters");
private final String title; private final String title;
private final String header; private final String header;
@ -31,63 +31,125 @@ public enum MultiAmountType {
return header; return header;
} }
public static List<Integer> prepareDefaltValues(int count, int min, int max) { public static List<Integer> prepareDefaltValues(List<MultiAmountMessage> constraints, int min, int max) {
// default values must be assigned from first to last by minimum values // default values must be assigned from first to last by minimum values
List<Integer> res = new ArrayList<>(); List<Integer> res = constraints.stream().map(m -> m.min > Integer.MIN_VALUE ? m.min : (0 < max ? 0 : max))
if (count == 0) { .collect(Collectors.toList());
if (res.isEmpty()) {
return res; return res;
} }
// fill list int total = res.stream().reduce(0, Integer::sum);
IntStream.range(0, count).forEach(i -> res.add(0));
// fill values // Fill values until we reach the overall minimum. Do this by filling values up until either their max or however much is leftover, starting with the first option.
if (min > 0) { if (min > 0 && total < min) {
res.set(0, min); int left = min - total;
for (int i = 0; i < res.size(); i++) {
// How much space there is left to add to
if (constraints.get(i).max == Integer.MAX_VALUE || constraints.get(i).max - res.get(i) > left) {
res.set(i, res.get(i) + left);
break;
} else {
int add = constraints.get(i).max - res.get(i);
res.set(i, constraints.get(i).max);
left -= add;
}
}
} }
return res; return res;
} }
public static List<Integer> prepareMaxValues(int count, int min, int max) { public static List<Integer> prepareMaxValues(List<MultiAmountMessage> constraints, int min, int max) {
// fill max values as much as possible if (constraints.isEmpty()) {
List<Integer> res = new ArrayList<>(); return new ArrayList<Integer>();
if (count == 0) {
return res;
} }
// fill list // Start by filling in minimum values where it makes sense
int startingValue = max / count; int default_val = max / constraints.size();
IntStream.range(0, count).forEach(i -> res.add(startingValue)); List<Integer> res = constraints.stream()
.map(m -> m.min > Integer.MIN_VALUE ? m.min : (default_val < m.max ? default_val : m.max))
.collect(Collectors.toList());
// fill values // Total should fall between the sum of all of the minimum values and max (in the case that everything was filled with default_value).
// from first to last until complete // So, we'll never start with too much.
List<Integer> resIndexes = new ArrayList<>(res.size()); int total = res.stream().reduce(0, Integer::sum);
IntStream.range(0, res.size()).forEach(resIndexes::add);
// infinite iterator (no needs with starting values use, but can be used later for different logic) // So add some values evenly until we hit max
Iterator<Integer> resIterator = Iterables.cycle(resIndexes).iterator(); while (total < max) {
int valueInc = 1; // Find the most amount we can add to several items at once without going over the maximum values
int valueTotal = startingValue * count; int addable = Integer.MIN_VALUE;
while (valueTotal < max) { List<Integer> consider = new ArrayList<Integer>();
int currentIndex = resIterator.next(); for (int i = 0; i < res.size(); i++) {
int newValue = CardUtil.overflowInc(res.get(currentIndex), valueInc);
res.set(currentIndex, newValue); if (constraints.get(i).max == Integer.MAX_VALUE) {
valueTotal += valueInc; consider.add(i);
} else {
int diff = constraints.get(i).max - res.get(i);
if (diff > 0) {
consider.add(i);
if (diff < addable) {
addable = diff;
}
}
}
}
// We hit max for all of the individual constraints - so this is as far as we can go.
if (consider.isEmpty()) {
break;
}
if (addable > Integer.MIN_VALUE && total + addable * consider.size() < max) {
for (int i : consider) {
res.set(i, res.get(i) + addable);
}
total += addable * consider.size();
} else {
addable = (max - total) / consider.size();
int extras = (max - total) % consider.size();
for (int i = 0; i < consider.size(); i++) {
// Remove from the end options first
int idx = consider.get(i);
// Add the extras evenly to the first options
if (i < extras) {
res.set(idx, res.get(idx) + addable + 1);
} else {
res.set(idx, res.get(idx) + addable);
}
}
total = max;
}
} }
return res; return res;
} }
public static boolean isGoodValues(List<Integer> values, int count, int min, int max) { public static boolean isGoodValues(List<Integer> values, List<MultiAmountMessage> constraints, int min, int max) {
if (values.size() != count) { if (values.size() != constraints.size()) {
return false; return false;
} }
int currentSum = values.stream().mapToInt(i -> i).sum(); int currentSum = 0;
for (int i = 0; i < values.size(); i++) {
int value = values.get(i);
if (value < constraints.get(i).min || value > constraints.get(i).max) {
return false;
}
currentSum += value;
}
return currentSum >= min && currentSum <= max; return currentSum >= min && currentSum <= max;
} }
public static List<Integer> parseAnswer(String answerToParse, int count, int min, int max, boolean returnDefaultOnError) { public static List<Integer> parseAnswer(String answerToParse, List<MultiAmountMessage> constraints, int min,
int max, boolean returnDefaultOnError) {
List<Integer> res = new ArrayList<>(); List<Integer> res = new ArrayList<>();
// parse // parse
@ -99,9 +161,9 @@ public enum MultiAmountType {
} }
// data check // data check
if (returnDefaultOnError && !isGoodValues(res, count, min, max)) { if (returnDefaultOnError && !isGoodValues(res, constraints, min, max)) {
// on broken data - return default // on broken data - return default
return prepareDefaltValues(count, min, max); return prepareDefaltValues(constraints, min, max);
} }
return res; return res;

View file

@ -39,6 +39,7 @@ import mage.players.PlayerList;
import mage.players.Players; import mage.players.Players;
import mage.util.Copyable; import mage.util.Copyable;
import mage.util.MessageToClient; import mage.util.MessageToClient;
import mage.util.MultiAmountMessage;
import mage.util.functions.CopyApplier; import mage.util.functions.CopyApplier;
import java.io.Serializable; import java.io.Serializable;
@ -301,7 +302,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
void fireGetAmountEvent(UUID playerId, String message, int min, int max); 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 fireGetMultiAmountEvent(UUID playerId, List<MultiAmountMessage> messages, int min, int max, Map<String, Serializable> options);
void fireChoosePileEvent(UUID playerId, String message, List<? extends Card> pile1, List<? extends Card> pile2); void fireChoosePileEvent(UUID playerId, String message, List<? extends Card> pile1, List<? extends Card> pile2);

View file

@ -66,6 +66,7 @@ import mage.target.TargetPlayer;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.GameLog; import mage.util.GameLog;
import mage.util.MessageToClient; import mage.util.MessageToClient;
import mage.util.MultiAmountMessage;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import mage.util.functions.CopyApplier; import mage.util.functions.CopyApplier;
import mage.watchers.Watcher; import mage.watchers.Watcher;
@ -2885,7 +2886,8 @@ public abstract class GameImpl implements Game {
} }
@Override @Override
public void fireGetMultiAmountEvent(UUID playerId, List<String> messages, int min, int max, Map<String, Serializable> options) { public void fireGetMultiAmountEvent(UUID playerId, List<MultiAmountMessage> messages, int min, int max,
Map<String, Serializable> options) {
if (simulation) { if (simulation) {
return; return;
} }

View file

@ -17,6 +17,7 @@ import mage.cards.Cards;
import mage.choices.Choice; import mage.choices.Choice;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.MultiAmountMessage;
/** /**
* *
@ -60,11 +61,11 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
private List<? extends Card> pile1; private List<? extends Card> pile1;
private List<? extends Card> pile2; private List<? extends Card> pile2;
private Choice choice; private Choice choice;
private List<String> messages; private List<MultiAmountMessage> messages;
private PlayerQueryEvent(UUID playerId, String message, List<? extends Ability> abilities, Set<String> choices, 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, Set<UUID> targets, Cards cards, QueryType queryType, int min, int max,
Map<String, Serializable> options, List<String> messages) { boolean required, Map<String, Serializable> options, List<MultiAmountMessage> messages) {
super(playerId); super(playerId);
CardUtil.checkSetParamForSerializationCompatibility(choices); CardUtil.checkSetParamForSerializationCompatibility(choices);
@ -216,8 +217,10 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.AMOUNT, min, max, false, null, 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) { public static PlayerQueryEvent multiAmountEvent(UUID playerId, List<MultiAmountMessage> messages, int min,
return new PlayerQueryEvent(playerId, null, null, null, null, null, QueryType.MULTI_AMOUNT, min, max, false, options, messages); 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) { public static PlayerQueryEvent pickCard(UUID playerId, String message, List<Card> booster, int time) {
@ -300,7 +303,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
return choice; return choice;
} }
public List<String> getMessages() { public List<MultiAmountMessage> getMessages() {
return messages; return messages;
} }
} }

View file

@ -12,6 +12,7 @@ import mage.cards.Card;
import mage.cards.Cards; import mage.cards.Cards;
import mage.choices.Choice; import mage.choices.Choice;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.util.MultiAmountMessage;
/** /**
* *
@ -84,7 +85,8 @@ public class PlayerQueryEventSource implements EventSource<PlayerQueryEvent>, Se
dispatcher.fireEvent(PlayerQueryEvent.amountEvent(playerId, message, min, max)); dispatcher.fireEvent(PlayerQueryEvent.amountEvent(playerId, message, min, max));
} }
public void multiAmount(UUID playerId, List<String> messages, int min, int max, Map<String, Serializable> options) { public void multiAmount(UUID playerId, List<MultiAmountMessage> messages, int min, int max,
Map<String, Serializable> options) {
dispatcher.fireEvent(PlayerQueryEvent.multiAmountEvent(playerId, messages, min, max, options)); dispatcher.fireEvent(PlayerQueryEvent.multiAmountEvent(playerId, messages, min, max, options));
} }

View file

@ -38,9 +38,11 @@ import mage.target.TargetAmount;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetCardInLibrary;
import mage.util.Copyable; import mage.util.Copyable;
import mage.util.MultiAmountMessage;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -746,7 +748,27 @@ public interface Player extends MageItem, Copyable<Player> {
* @param game Game * @param game Game
* @return List of integers with size equal to messages.size(). The sum of the integers is equal to max. * @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); default List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type,
Game game) {
List<MultiAmountMessage> constraints = messages.stream().map(s -> new MultiAmountMessage(s, 0, max))
.collect(Collectors.toList());
return getMultiAmountWithIndividualConstraints(outcome, constraints, min, max, type, game);
}
/**
* Player distributes amount among multiple options
*
* @param outcome AI hint
* @param messages List of options to distribute amount among. Each option has a constraint on the min, max chosen for it
* @param totalMin Total minimum amount to be distributed
* @param totalMax 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> getMultiAmountWithIndividualConstraints(Outcome outcome, List<MultiAmountMessage> messages, int min,
int max, MultiAmountType type, Game game);
void sideboard(Match match, Deck deck); void sideboard(Match match, Deck deck);

View file

@ -25,6 +25,7 @@ import mage.target.Target;
import mage.target.TargetAmount; import mage.target.TargetAmount;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.TargetPlayer; import mage.target.TargetPlayer;
import mage.util.MultiAmountMessage;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
@ -207,7 +208,8 @@ public class StubPlayer extends PlayerImpl implements Player {
} }
@Override @Override
public List<Integer> getMultiAmount(Outcome outcome, List<String> messages, int min, int max, MultiAmountType type, Game game) { public List<Integer> getMultiAmountWithIndividualConstraints(Outcome outcome, List<MultiAmountMessage> messages,
int min, int max, MultiAmountType type, Game game) {
return null; return null;
} }

View file

@ -0,0 +1,17 @@
package mage.util;
import java.io.Serializable;
// Author: alexander-novo
// A helper class for facilitating the multi-choose dialog
public class MultiAmountMessage implements Serializable {
public String message;
public int min;
public int max;
public MultiAmountMessage(String message, int min, int max) {
this.message = message;
this.min = min;
this.max = max;
}
}