mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
[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:
parent
fe1efef25b
commit
a36a7d9b7f
23 changed files with 678 additions and 180 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package mage.client.dialog;
|
||||
|
||||
import mage.constants.ColoredManaSymbol;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import org.mage.card.arcane.ManaSymbols;
|
||||
|
||||
import javax.swing.*;
|
||||
|
|
@ -25,7 +27,7 @@ public class PickMultiNumberDialog extends MageDialog {
|
|||
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.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
this.setTitle((String) options.get("title"));
|
||||
|
|
@ -51,7 +53,7 @@ public class PickMultiNumberDialog extends MageDialog {
|
|||
|
||||
// mana mode
|
||||
String manaText = null;
|
||||
String input = messages.get(i);
|
||||
String input = messages.get(i).message;
|
||||
switch (input) {
|
||||
case "W":
|
||||
manaText = ColoredManaSymbol.W.getColorHtmlName();
|
||||
|
|
@ -87,13 +89,13 @@ public class PickMultiNumberDialog extends MageDialog {
|
|||
labelList.add(label);
|
||||
|
||||
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.gridx = 1;
|
||||
spinnerC.gridy = i;
|
||||
spinnerC.ipadx = 20;
|
||||
spinner.addChangeListener(e -> {
|
||||
updateControls(min, max);
|
||||
updateControls(min, max, messages);
|
||||
});
|
||||
jPanel1.add(spinner, spinnerC);
|
||||
spinnerList.add(spinner);
|
||||
|
|
@ -101,20 +103,28 @@ public class PickMultiNumberDialog extends MageDialog {
|
|||
this.counterText.setText("0 out of 0");
|
||||
this.counterText.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
updateControls(min, max);
|
||||
updateControls(min, max, messages);
|
||||
|
||||
this.pack();
|
||||
this.makeWindowCentered();
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
private void updateControls(int min, int max) {
|
||||
private void updateControls(int min, int max, List<MultiAmountMessage> messages) {
|
||||
int totalChosenAmount = 0;
|
||||
for (JSpinner jSpinner : spinnerList) {
|
||||
totalChosenAmount += ((Number) jSpinner.getValue()).intValue();
|
||||
boolean chooseEnabled = true;
|
||||
|
||||
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);
|
||||
chooseButton.setEnabled(totalChosenAmount >= min && totalChosenAmount <= max);
|
||||
|
||||
chooseEnabled &= totalChosenAmount >= min && totalChosenAmount <= max;
|
||||
chooseButton.setEnabled(chooseEnabled);
|
||||
}
|
||||
|
||||
public String getMultiAmount() {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import mage.constants.*;
|
|||
import mage.game.events.PlayerQueryEvent;
|
||||
import mage.players.PlayableObjectStats;
|
||||
import mage.players.PlayableObjectsList;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.view.*;
|
||||
import org.apache.log4j.Logger;
|
||||
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);
|
||||
hideAll();
|
||||
DialogManager.getManager(gameId).fadeOut();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.google.gson.GsonBuilder;
|
|||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import mage.choices.Choice;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -42,7 +43,7 @@ public class GameClientMessage implements Serializable {
|
|||
@Expose
|
||||
private Choice choice;
|
||||
@Expose
|
||||
private List<String> messages;
|
||||
private List<MultiAmountMessage> messages;
|
||||
|
||||
public GameClientMessage(GameView gameView, Map<String, Serializable> options) {
|
||||
this.gameView = gameView;
|
||||
|
|
@ -80,7 +81,8 @@ public class GameClientMessage implements Serializable {
|
|||
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.options = options;
|
||||
this.messages = messages;
|
||||
|
|
@ -134,7 +136,7 @@ public class GameClientMessage implements Serializable {
|
|||
return choice;
|
||||
}
|
||||
|
||||
public List<String> getMessages() {
|
||||
public List<MultiAmountMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2169,11 +2169,12 @@ public class ComputerPlayer extends PlayerImpl implements Player {
|
|||
}
|
||||
|
||||
@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");
|
||||
|
||||
int needCount = messages.size();
|
||||
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
|
||||
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, min, max);
|
||||
if (needCount == 0) {
|
||||
return defaultList;
|
||||
}
|
||||
|
|
@ -2188,7 +2189,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
|
|||
// 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);
|
||||
return MultiAmountType.prepareMaxValues(messages, min, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ import mage.util.CardUtil;
|
|||
import mage.util.GameLog;
|
||||
import mage.util.ManaUtil;
|
||||
import mage.util.MessageToClient;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.awt.*;
|
||||
|
|
@ -2049,10 +2051,12 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
|
||||
@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();
|
||||
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
|
||||
if (needCount == 0) {
|
||||
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, min, max);
|
||||
if (needCount == 0 || (needCount == 1 && min == max)
|
||||
|| messages.stream().map(m -> m.min == m.max).reduce(true, Boolean::logicalAnd)) {
|
||||
return defaultList;
|
||||
}
|
||||
|
||||
|
|
@ -2074,8 +2078,8 @@ public class HumanPlayer extends PlayerImpl {
|
|||
|
||||
// waiting correct values only
|
||||
if (response.getString() != null) {
|
||||
answer = MultiAmountType.parseAnswer(response.getString(), needCount, min, max, false);
|
||||
if (MultiAmountType.isGoodValues(answer, needCount, min, max)) {
|
||||
answer = MultiAmountType.parseAnswer(response.getString(), messages, min, max, false);
|
||||
if (MultiAmountType.isGoodValues(answer, messages, min, max)) {
|
||||
break;
|
||||
} else {
|
||||
// it's not normal: can be cheater or a wrong GUI checks
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import mage.server.User;
|
|||
import mage.server.managers.ManagerFactory;
|
||||
import mage.server.util.Splitter;
|
||||
import mage.server.util.SystemUtil;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.utils.StreamUtils;
|
||||
import mage.utils.timer.PriorityTimer;
|
||||
import mage.view.*;
|
||||
|
|
@ -870,7 +871,9 @@ 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 {
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import mage.players.Player;
|
|||
import mage.server.User;
|
||||
import mage.server.managers.ManagerFactory;
|
||||
import mage.server.managers.UserManager;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.view.*;
|
||||
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) {
|
||||
userManager.getUser(userId).ifPresent(user
|
||||
-> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(getGameView(), options, messages, min, max))));
|
||||
|
|
|
|||
183
Mage.Sets/src/mage/cards/g/GoldberryRiverDaughter.java
Normal file
183
Mage.Sets/src/mage/cards/g/GoldberryRiverDaughter.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import mage.abilities.effects.OneShotEffect;
|
|||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.MultiAmountType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.counters.Counter;
|
||||
import mage.counters.CounterType;
|
||||
|
|
@ -24,10 +25,12 @@ import mage.players.Player;
|
|||
import mage.target.Target;
|
||||
import mage.target.TargetPermanent;
|
||||
import mage.target.common.TargetControlledPermanent;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Alex-Vasile
|
||||
|
|
@ -92,33 +95,39 @@ class ResourcefulDefenseMoveCounterEffect extends OneShotEffect {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Counter name and how many to move
|
||||
Map<String, Integer> counterMap = new HashMap<>();
|
||||
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);
|
||||
}
|
||||
List<Counter> counters = new ArrayList<>(fromPermanent.getCounters(game).values());
|
||||
counters.sort((c1, c2) -> c1.getName().compareTo(c2.getName()));
|
||||
|
||||
// Move the counters
|
||||
for (String counterName : counterMap.keySet()) {
|
||||
toPermanent.addCounters(
|
||||
CounterType.findByName(counterName).createInstance(counterMap.get(counterName)),
|
||||
source,
|
||||
game);
|
||||
fromPermanent.removeCounters(counterName, counterMap.get(counterName), source, game);
|
||||
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, 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(
|
||||
controller.getLogName() + "moved " +
|
||||
counterMap.get(counterName) + " " +
|
||||
counterName + "counter" + (counterMap.get(counterName) > 1 ? "s" : "") +
|
||||
"from " + fromPermanent.getLogName() +
|
||||
"to " + toPermanent.getLogName() + "."
|
||||
);
|
||||
amount + " " +
|
||||
counterName + " counter" + (amount > 1 ? "s" : "") +
|
||||
" from " + fromPermanent.getLogName() +
|
||||
"to " + toPermanent.getLogName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -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("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("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, 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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -115,8 +115,7 @@ public class ResourcefulDefenseTest extends CardTestPlayerBase {
|
|||
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: ");
|
||||
addTarget(playerA, steelbaneHydra);
|
||||
addTarget(playerA, vividCreek);
|
||||
setChoiceAmount(playerA, 2);
|
||||
setChoiceAmount(playerA, 1);
|
||||
setChoiceAmount(playerA, 1, 2); // +1/+1, Charge
|
||||
|
||||
setStopAt(3, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package org.mage.test.cards.targets;
|
|||
import mage.constants.MultiAmountType;
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.player.TestPlayer;
|
||||
|
|
@ -10,6 +12,7 @@ import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
|
|
@ -20,117 +23,132 @@ 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);
|
||||
assertDefaultValuesUnconstrained("", 0, 0, 0);
|
||||
//
|
||||
assertDefaultValues("0", 1, 0, 0);
|
||||
assertDefaultValues("0 0", 2, 0, 0);
|
||||
assertDefaultValues("0 0 0", 3, 0, 0);
|
||||
assertDefaultValuesUnconstrained("0", 1, 0, 0);
|
||||
assertDefaultValuesUnconstrained("0 0", 2, 0, 0);
|
||||
assertDefaultValuesUnconstrained("0 0 0", 3, 0, 0);
|
||||
//
|
||||
assertDefaultValues("1", 1, 1, 1);
|
||||
assertDefaultValues("1 0", 2, 1, 1);
|
||||
assertDefaultValues("1 0 0", 3, 1, 1);
|
||||
assertDefaultValuesUnconstrained("1", 1, 1, 1);
|
||||
assertDefaultValuesUnconstrained("1 0", 2, 1, 1);
|
||||
assertDefaultValuesUnconstrained("1 0 0", 3, 1, 1);
|
||||
//
|
||||
assertDefaultValues("1", 1, 1, 2);
|
||||
assertDefaultValues("1 0", 2, 1, 2);
|
||||
assertDefaultValues("1 0 0", 3, 1, 2);
|
||||
assertDefaultValuesUnconstrained("1", 1, 1, 2);
|
||||
assertDefaultValuesUnconstrained("1 0", 2, 1, 2);
|
||||
assertDefaultValuesUnconstrained("1 0 0", 3, 1, 2);
|
||||
//
|
||||
assertDefaultValues("2", 1, 2, 2);
|
||||
assertDefaultValues("2 0", 2, 2, 2);
|
||||
assertDefaultValues("2 0 0", 3, 2, 2);
|
||||
assertDefaultValuesUnconstrained("2", 1, 2, 2);
|
||||
assertDefaultValuesUnconstrained("2 0", 2, 2, 2);
|
||||
assertDefaultValuesUnconstrained("2 0 0", 3, 2, 2);
|
||||
//
|
||||
assertDefaultValues("2", 1, 2, 10);
|
||||
assertDefaultValues("2 0", 2, 2, 10);
|
||||
assertDefaultValues("2 0 0", 3, 2, 10);
|
||||
assertDefaultValuesUnconstrained("2", 1, 2, 10);
|
||||
assertDefaultValuesUnconstrained("2 0", 2, 2, 10);
|
||||
assertDefaultValuesUnconstrained("2 0 0", 3, 2, 10);
|
||||
//
|
||||
// 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) {
|
||||
List<Integer> defaultValues = MultiAmountType.prepareDefaltValues(count, min, max);
|
||||
private List<MultiAmountMessage> getUnconstrainedConstraints(int count) {
|
||||
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
|
||||
.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));
|
||||
Assert.assertTrue("default values must be good",
|
||||
MultiAmountType.isGoodValues(defaultValues, constraints, 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);
|
||||
assertMaxValuesUnconstrained("", 0, 0, 0);
|
||||
//
|
||||
assertMaxValues("0", 1, 0, 0);
|
||||
assertMaxValues("0 0", 2, 0, 0);
|
||||
assertMaxValues("0 0 0", 3, 0, 0);
|
||||
assertMaxValuesUnconstrained("0", 1, 0, 0);
|
||||
assertMaxValuesUnconstrained("0 0", 2, 0, 0);
|
||||
assertMaxValuesUnconstrained("0 0 0", 3, 0, 0);
|
||||
//
|
||||
assertMaxValues("1", 1, 1, 1);
|
||||
assertMaxValues("1 0", 2, 1, 1);
|
||||
assertMaxValues("1 0 0", 3, 1, 1);
|
||||
assertMaxValuesUnconstrained("1", 1, 1, 1);
|
||||
assertMaxValuesUnconstrained("1 0", 2, 1, 1);
|
||||
assertMaxValuesUnconstrained("1 0 0", 3, 1, 1);
|
||||
//
|
||||
assertMaxValues("2", 1, 1, 2);
|
||||
assertMaxValues("1 1", 2, 1, 2);
|
||||
assertMaxValues("1 1 0", 3, 1, 2);
|
||||
assertMaxValuesUnconstrained("2", 1, 1, 2);
|
||||
assertMaxValuesUnconstrained("1 1", 2, 1, 2);
|
||||
assertMaxValuesUnconstrained("1 1 0", 3, 1, 2);
|
||||
//
|
||||
assertMaxValues("2", 1, 2, 2);
|
||||
assertMaxValues("1 1", 2, 2, 2);
|
||||
assertMaxValues("1 1 0", 3, 2, 2);
|
||||
assertMaxValuesUnconstrained("2", 1, 2, 2);
|
||||
assertMaxValuesUnconstrained("1 1", 2, 2, 2);
|
||||
assertMaxValuesUnconstrained("1 1 0", 3, 2, 2);
|
||||
//
|
||||
assertMaxValues("10", 1, 2, 10);
|
||||
assertMaxValues("5 5", 2, 2, 10);
|
||||
assertMaxValues("4 3 3", 3, 2, 10);
|
||||
assertMaxValuesUnconstrained("10", 1, 2, 10);
|
||||
assertMaxValuesUnconstrained("5 5", 2, 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
|
||||
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;
|
||||
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) {
|
||||
List<Integer> maxValues = MultiAmountType.prepareMaxValues(count, min, max);
|
||||
private void assertMaxValuesUnconstrained(String need, int count, int min, int max) {
|
||||
List<MultiAmountMessage> constraints = getUnconstrainedConstraints(count);
|
||||
List<Integer> maxValues = MultiAmountType.prepareMaxValues(constraints, 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));
|
||||
Assert.assertTrue("max values must be good", MultiAmountType.isGoodValues(maxValues, constraints, min, max));
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
List<Integer> list = MultiAmountType.prepareDefaltValues(3, 0, 0);
|
||||
List<Integer> list = MultiAmountType.prepareDefaltValues(constraints.get(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));
|
||||
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(0), 0, 0));
|
||||
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(1), 0, 0));
|
||||
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(2), 0, 0));
|
||||
Assert.assertTrue("count", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 0));
|
||||
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(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));
|
||||
Assert.assertTrue("min", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 100));
|
||||
Assert.assertTrue("min", MultiAmountType.isGoodValues(list, constraints.get(3), 1, 100));
|
||||
Assert.assertTrue("min", MultiAmountType.isGoodValues(list, constraints.get(3), 2, 100));
|
||||
Assert.assertFalse("min", MultiAmountType.isGoodValues(list, constraints.get(3), 3, 100));
|
||||
Assert.assertFalse("min", MultiAmountType.isGoodValues(list, constraints.get(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));
|
||||
Assert.assertFalse("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 0));
|
||||
Assert.assertFalse("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 1));
|
||||
Assert.assertTrue("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 2));
|
||||
Assert.assertTrue("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 3));
|
||||
Assert.assertTrue("max", MultiAmountType.isGoodValues(list, constraints.get(3), 0, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -138,32 +156,36 @@ public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
|
|||
// 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);
|
||||
assertParseUnconstrained("", 3, 1, 3, "", false);
|
||||
assertParseUnconstrained("1", 3, 1, 3, "1", false);
|
||||
assertParseUnconstrained("0 0 0", 3, 1, 3, "0 0 0", false);
|
||||
assertParseUnconstrained("1 0 3", 3, 1, 3, "1 0 3", false);
|
||||
assertParseUnconstrained("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);
|
||||
assertParseUnconstrained("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);
|
||||
assertParseUnconstrained("1 0 0", 3, 1, 3, "", true);
|
||||
assertParseUnconstrained("1 0 0", 3, 1, 3, "1", true);
|
||||
assertParseUnconstrained("1 0 0", 3, 1, 3, "0 0 0", true);
|
||||
assertParseUnconstrained("1 0 0", 3, 1, 3, "1 0 3", 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) {
|
||||
List<Integer> parsedValues = MultiAmountType.parseAnswer(answerToParse, count, min, max, returnDefaultOnError);
|
||||
private void assertParseUnconstrained(String need, int count, int min, int max, String answerToParse,
|
||||
Boolean returnDefaultOnError) {
|
||||
List<MultiAmountMessage> constraints = getUnconstrainedConstraints(count);
|
||||
List<Integer> parsedValues = MultiAmountType.parseAnswer(answerToParse, constraints, 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));
|
||||
Assert.assertTrue("parsed values must be good",
|
||||
MultiAmountType.isGoodValues(parsedValues, constraints, min, max));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import mage.players.net.UserData;
|
|||
import mage.target.*;
|
||||
import mage.target.common.*;
|
||||
import mage.util.CardUtil;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.util.RandomUtil;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.junit.Assert;
|
||||
|
|
@ -2831,11 +2832,12 @@ public class TestPlayer implements Player {
|
|||
}
|
||||
|
||||
@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);
|
||||
|
||||
int needCount = messages.size();
|
||||
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
|
||||
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, min, max);
|
||||
if (needCount == 0) {
|
||||
return defaultList;
|
||||
}
|
||||
|
|
@ -2861,7 +2863,7 @@ public class TestPlayer implements Player {
|
|||
}
|
||||
|
||||
// extra check
|
||||
if (!MultiAmountType.isGoodValues(answer, needCount, min, max)) {
|
||||
if (!MultiAmountType.isGoodValues(answer, messages, min, max)) {
|
||||
Assert.fail("Wrong choices in multi amount: " + answer
|
||||
.stream()
|
||||
.map(String::valueOf)
|
||||
|
|
@ -2872,7 +2874,7 @@ public class TestPlayer implements Player {
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import mage.target.Target;
|
|||
import mage.target.TargetAmount;
|
||||
import mage.target.TargetCard;
|
||||
import mage.target.common.TargetCardInLibrary;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
|
@ -983,7 +984,8 @@ public class PlayerStub implements Player {
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
package mage.constants;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import mage.util.CardUtil;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum MultiAmountType {
|
||||
|
||||
MANA("Add mana", "Distribute mana among colors"),
|
||||
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 header;
|
||||
|
|
@ -31,63 +31,125 @@ public enum MultiAmountType {
|
|||
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
|
||||
List<Integer> res = new ArrayList<>();
|
||||
if (count == 0) {
|
||||
List<Integer> res = constraints.stream().map(m -> m.min > Integer.MIN_VALUE ? m.min : (0 < max ? 0 : max))
|
||||
.collect(Collectors.toList());
|
||||
if (res.isEmpty()) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// fill list
|
||||
IntStream.range(0, count).forEach(i -> res.add(0));
|
||||
int total = res.stream().reduce(0, Integer::sum);
|
||||
|
||||
// fill values
|
||||
if (min > 0) {
|
||||
res.set(0, min);
|
||||
// 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 && total < 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;
|
||||
}
|
||||
|
||||
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;
|
||||
public static List<Integer> prepareMaxValues(List<MultiAmountMessage> constraints, int min, int max) {
|
||||
if (constraints.isEmpty()) {
|
||||
return new ArrayList<Integer>();
|
||||
}
|
||||
|
||||
// fill list
|
||||
int startingValue = max / count;
|
||||
IntStream.range(0, count).forEach(i -> res.add(startingValue));
|
||||
// Start by filling in minimum values where it makes sense
|
||||
int default_val = max / constraints.size();
|
||||
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
|
||||
// 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;
|
||||
// Total should fall between the sum of all of the minimum values and max (in the case that everything was filled with default_value).
|
||||
// So, we'll never start with too much.
|
||||
int total = res.stream().reduce(0, Integer::sum);
|
||||
|
||||
// So add some values evenly until we hit max
|
||||
while (total < max) {
|
||||
// Find the most amount we can add to several items at once without going over the maximum values
|
||||
int addable = Integer.MIN_VALUE;
|
||||
List<Integer> consider = new ArrayList<Integer>();
|
||||
for (int i = 0; i < res.size(); i++) {
|
||||
|
||||
if (constraints.get(i).max == Integer.MAX_VALUE) {
|
||||
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;
|
||||
}
|
||||
|
||||
public static boolean isGoodValues(List<Integer> values, int count, int min, int max) {
|
||||
if (values.size() != count) {
|
||||
public static boolean isGoodValues(List<Integer> values, List<MultiAmountMessage> constraints, int min, int max) {
|
||||
if (values.size() != constraints.size()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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<>();
|
||||
|
||||
// parse
|
||||
|
|
@ -99,9 +161,9 @@ public enum MultiAmountType {
|
|||
}
|
||||
|
||||
// data check
|
||||
if (returnDefaultOnError && !isGoodValues(res, count, min, max)) {
|
||||
if (returnDefaultOnError && !isGoodValues(res, constraints, min, max)) {
|
||||
// on broken data - return default
|
||||
return prepareDefaltValues(count, min, max);
|
||||
return prepareDefaltValues(constraints, min, max);
|
||||
}
|
||||
|
||||
return res;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import mage.players.PlayerList;
|
|||
import mage.players.Players;
|
||||
import mage.util.Copyable;
|
||||
import mage.util.MessageToClient;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.util.functions.CopyApplier;
|
||||
|
||||
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 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ import mage.target.TargetPlayer;
|
|||
import mage.util.CardUtil;
|
||||
import mage.util.GameLog;
|
||||
import mage.util.MessageToClient;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.util.RandomUtil;
|
||||
import mage.util.functions.CopyApplier;
|
||||
import mage.watchers.Watcher;
|
||||
|
|
@ -2885,7 +2886,8 @@ public abstract class GameImpl implements Game {
|
|||
}
|
||||
|
||||
@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) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import mage.cards.Cards;
|
|||
import mage.choices.Choice;
|
||||
import mage.game.permanent.Permanent;
|
||||
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> pile2;
|
||||
private Choice choice;
|
||||
private List<String> messages;
|
||||
private List<MultiAmountMessage> 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, List<String> messages) {
|
||||
Set<UUID> targets, Cards cards, QueryType queryType, int min, int max,
|
||||
boolean required, Map<String, Serializable> options, List<MultiAmountMessage> messages) {
|
||||
super(playerId);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 multiAmountEvent(UUID playerId, List<MultiAmountMessage> 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) {
|
||||
|
|
@ -300,7 +303,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
|
|||
return choice;
|
||||
}
|
||||
|
||||
public List<String> getMessages() {
|
||||
public List<MultiAmountMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import mage.cards.Card;
|
|||
import mage.cards.Cards;
|
||||
import mage.choices.Choice;
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ import mage.target.TargetAmount;
|
|||
import mage.target.TargetCard;
|
||||
import mage.target.common.TargetCardInLibrary;
|
||||
import mage.util.Copyable;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
|
|
@ -746,7 +748,27 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
* @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);
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import mage.target.Target;
|
|||
import mage.target.TargetAmount;
|
||||
import mage.target.TargetCard;
|
||||
import mage.target.TargetPlayer;
|
||||
import mage.util.MultiAmountMessage;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -207,7 +208,8 @@ public class StubPlayer extends PlayerImpl implements Player {
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
|
|
|||
17
Mage/src/main/java/mage/util/MultiAmountMessage.java
Normal file
17
Mage/src/main/java/mage/util/MultiAmountMessage.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue