diff --git a/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java index 550cf72640b2..a17c509e7e2b 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java @@ -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 PickMultiNumberDialog() { this.setModal(true); } - public void showDialog(List messages, int min, int max, Map options) { + public void showDialog(List messages, int min, int max, Map options) { this.header.setText((String) options.get("header")); this.header.setHorizontalAlignment(SwingConstants.CENTER); this.setTitle((String) options.get("title")); @@ -51,7 +53,7 @@ public void showDialog(List messages, int min, int max, Map messages, int min, int max, Map { - updateControls(min, max); + updateControls(min, max, messages); }); jPanel1.add(spinner, spinnerC); spinnerList.add(spinner); @@ -101,20 +103,28 @@ public void showDialog(List messages, int min, int max, Map 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() { diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 7f6185a45403..8cdb7213a6a8 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -28,6 +28,7 @@ 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 void getAmount(GameView gameView, Map options, int } } - public void getMultiAmount(List messages, GameView gameView, Map options, int min, int max) { + public void getMultiAmount(List messages, GameView gameView, Map options, + int min, int max) { updateGame(gameView, false, options, null); hideAll(); DialogManager.getManager(gameId).fadeOut(); diff --git a/Mage.Common/src/main/java/mage/view/GameClientMessage.java b/Mage.Common/src/main/java/mage/view/GameClientMessage.java index 64a4c43e3795..005dc0131682 100644 --- a/Mage.Common/src/main/java/mage/view/GameClientMessage.java +++ b/Mage.Common/src/main/java/mage/view/GameClientMessage.java @@ -13,6 +13,7 @@ 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 messages; + private List messages; public GameClientMessage(GameView gameView, Map options) { this.gameView = gameView; @@ -80,7 +81,8 @@ public GameClientMessage(GameView gameView, Map options, S this.cardsView2 = pile2; } - public GameClientMessage(GameView gameView, Map options, List messages, int min, int max) { + public GameClientMessage(GameView gameView, Map options, List messages, + int min, int max) { this.gameView = gameView; this.options = options; this.messages = messages; @@ -134,7 +136,7 @@ public Choice getChoice() { return choice; } - public List getMessages() { + public List getMessages() { return messages; } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 1b369718bf8e..6f8c45db13e0 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -2169,11 +2169,12 @@ public int getAmount(int min, int max, String message, Game game) { } @Override - public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, + int min, int max, MultiAmountType type, Game game) { log.debug("getMultiAmount"); int needCount = messages.size(); - List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); + List defaultList = MultiAmountType.prepareDefaltValues(messages, min, max); if (needCount == 0) { return defaultList; } @@ -2188,7 +2189,7 @@ public List getMultiAmount(Outcome outcome, List messages, int // 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 diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 3a9a38c1c611..e118d8739566 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -45,6 +45,8 @@ import mage.util.GameLog; import mage.util.ManaUtil; import mage.util.MessageToClient; +import mage.util.MultiAmountMessage; + import org.apache.log4j.Logger; import java.awt.*; @@ -2045,10 +2047,12 @@ public int getAmount(int min, int max, String message, Game game) { } @Override - public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, + int min, int max, MultiAmountType type, Game game) { int needCount = messages.size(); - List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); - if (needCount == 0) { + List 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; } @@ -2070,8 +2074,8 @@ public List getMultiAmount(Outcome outcome, List messages, int // 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 diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index c052f639d80e..35bb794db691 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -28,6 +28,7 @@ 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 @@ private synchronized void amount(UUID playerId, final String message, final int perform(playerId, playerId1 -> getGameSession(playerId1).getAmount(message, min, max)); } - private synchronized void multiAmount(UUID playerId, final List messages, final int min, final int max, final Map options) throws MageException { + private synchronized void multiAmount(UUID playerId, final List messages, + final int min, final int max, final Map options) + throws MageException { perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options)); } diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java index 81c948c04b26..65a7f4653a9f 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java @@ -13,6 +13,7 @@ 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 void getAmount(final String message, final int min, final int max) { } } - public void getMultiAmount(final List messages, final int min, final int max, final Map options) { + public void getMultiAmount(final List messages, final int min, final int max, + final Map options) { if (!killed) { userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(getGameView(), options, messages, min, max)))); diff --git a/Mage.Sets/src/mage/cards/g/GoldberryRiverDaughter.java b/Mage.Sets/src/mage/cards/g/GoldberryRiverDaughter.java new file mode 100644 index 000000000000..9c89b16aef88 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GoldberryRiverDaughter.java @@ -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 fromCounters = new HashSet(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 counters = new ArrayList<>(fromPermanent.getCounters(game).values()); + counters.sort((c1, c2) -> c1.getName().compareTo(c2.getName())); + + List 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 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; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/ResourcefulDefense.java b/Mage.Sets/src/mage/cards/r/ResourcefulDefense.java index 35d104fff0a8..1f3286313f8e 100644 --- a/Mage.Sets/src/mage/cards/r/ResourcefulDefense.java +++ b/Mage.Sets/src/mage/cards/r/ResourcefulDefense.java @@ -9,6 +9,7 @@ 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.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 @@ public boolean apply(Game game, Ability source) { return false; } - // Counter name and how many to move - Map counterMap = new HashMap<>(); - for (Map.Entry 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 - for (String counterName : counterMap.keySet()) { - toPermanent.addCounters( - CounterType.findByName(counterName).createInstance(counterMap.get(counterName)), - source, - game); - fromPermanent.removeCounters(counterName, counterMap.get(counterName), source, game); - game.informPlayers( - controller.getLogName() + "moved " + - counterMap.get(counterName) + " " + - counterName + "counter" + (counterMap.get(counterName) > 1 ? "s" : "") + - "from " + fromPermanent.getLogName() + - "to " + toPermanent.getLogName() + "." - ); + List counters = new ArrayList<>(fromPermanent.getCounters(game).values()); + counters.sort((c1, c2) -> c1.getName().compareTo(c2.getName())); + + List 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 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 " + + amount + " " + + counterName + " counter" + (amount > 1 ? "s" : "") + + " from " + fromPermanent.getLogName() + + "to " + toPermanent.getLogName() + "."); + } } return true; diff --git a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java index d3f8e0ce114f..6ae9e5dc27ab 100644 --- a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java +++ b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java @@ -112,6 +112,7 @@ private TheLordOfTheRingsTalesOfMiddleEarth() { 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("Gorbag of Minas Morgul", 86, Rarity.UNCOMMON, mage.cards.g.GorbagOfMinasMorgul.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/GoldberryRiverDaughterTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/GoldberryRiverDaughterTest.java new file mode 100644 index 000000000000..a6813df64b40 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/GoldberryRiverDaughterTest.java @@ -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); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ResourcefulDefenseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ResourcefulDefenseTest.java index 78ee55569ca6..171dee563645 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ResourcefulDefenseTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ResourcefulDefenseTest.java @@ -115,8 +115,7 @@ public void testMoveAllMultipleCounters() { 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(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java index 95e57908683c..6f1cdae8ba8a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java @@ -3,6 +3,8 @@ 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 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 List getUnconstrainedConstraints(int count) { + return IntStream.range(0, count).mapToObj(i -> new MultiAmountMessage("", Integer.MIN_VALUE, Integer.MAX_VALUE)) + .collect(Collectors.toList()); } - private void assertDefaultValues(String need, int count, int min, int max) { - List defaultValues = MultiAmountType.prepareDefaltValues(count, min, max); + private void assertDefaultValuesUnconstrained(String need, int count, int min, int max) { + List constraints = getUnconstrainedConstraints(count); + List 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 maxValues = MultiAmountType.prepareMaxValues(count, min, max); + private void assertMaxValuesUnconstrained(String need, int count, int min, int max) { + List constraints = getUnconstrainedConstraints(count); + List 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> 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 list = MultiAmountType.prepareDefaltValues(3, 0, 0); + List 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 void test_Parse() { // parse must use correct values on good data or default values on broken data // simple parse without data check - assertParse("", 3, 1, 3, "", false); - assertParse("1", 3, 1, 3, "1", false); - assertParse("0 0 0", 3, 1, 3, "0 0 0", false); - assertParse("1 0 3", 3, 1, 3, "1 0 3", false); - assertParse("0 5 0 6", 3, 1, 3, "1,text 5 4. 6", false); + 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 parsedValues = MultiAmountType.parseAnswer(answerToParse, count, min, max, returnDefaultOnError); + private void assertParseUnconstrained(String need, int count, int min, int max, String answerToParse, + Boolean returnDefaultOnError) { + List constraints = getUnconstrainedConstraints(count); + List 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)); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index a55769023662..9831560ac7cd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -49,6 +49,7 @@ 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; @@ -2824,11 +2825,12 @@ public int getAmount(int min, int max, String message, Game game) { } @Override - public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, + int min, int max, MultiAmountType type, Game game) { assertAliasSupportInChoices(false); int needCount = messages.size(); - List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max); + List defaultList = MultiAmountType.prepareDefaltValues(messages, min, max); if (needCount == 0) { return defaultList; } @@ -2854,7 +2856,7 @@ public List getMultiAmount(Outcome outcome, List messages, int } // 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) @@ -2865,7 +2867,7 @@ public List getMultiAmount(Outcome outcome, List messages, int } 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 diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index d434e329a662..1b28f4b33bb6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -42,6 +42,7 @@ import mage.target.TargetAmount; import mage.target.TargetCard; import mage.target.common.TargetCardInLibrary; +import mage.util.MultiAmountMessage; import java.io.Serializable; import java.util.*; @@ -975,7 +976,8 @@ public int getAmount(int min, int max, String message, Game game) { } @Override - public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, + int min, int max, MultiAmountType type, Game game) { return null; } diff --git a/Mage/src/main/java/mage/constants/MultiAmountType.java b/Mage/src/main/java/mage/constants/MultiAmountType.java index 6f88b6a25621..34d6e9e8100a 100644 --- a/Mage/src/main/java/mage/constants/MultiAmountType.java +++ b/Mage/src/main/java/mage/constants/MultiAmountType.java @@ -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 String getHeader() { return header; } - public static List prepareDefaltValues(int count, int min, int max) { + public static List prepareDefaltValues(List constraints, int min, int max) { // default values must be assigned from first to last by minimum values - List res = new ArrayList<>(); - if (count == 0) { + List 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)); - - // fill values - if (min > 0) { - res.set(0, min); + int total = res.stream().reduce(0, Integer::sum); + + // 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 prepareMaxValues(int count, int min, int max) { - // fill max values as much as possible - List res = new ArrayList<>(); - if (count == 0) { - return res; + public static List prepareMaxValues(List constraints, int min, int max) { + if (constraints.isEmpty()) { + return new ArrayList(); } - // fill list - int startingValue = max / count; - IntStream.range(0, count).forEach(i -> res.add(startingValue)); - - // fill values - // from first to last until complete - List resIndexes = new ArrayList<>(res.size()); - IntStream.range(0, res.size()).forEach(resIndexes::add); - // infinite iterator (no needs with starting values use, but can be used later for different logic) - Iterator resIterator = Iterables.cycle(resIndexes).iterator(); - int valueInc = 1; - int valueTotal = startingValue * count; - while (valueTotal < max) { - int currentIndex = resIterator.next(); - int newValue = CardUtil.overflowInc(res.get(currentIndex), valueInc); - res.set(currentIndex, newValue); - valueTotal += valueInc; + // Start by filling in minimum values where it makes sense + int default_val = max / constraints.size(); + List res = constraints.stream() + .map(m -> m.min > Integer.MIN_VALUE ? m.min : (default_val < m.max ? default_val : m.max)) + .collect(Collectors.toList()); + + // 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 consider = new ArrayList(); + 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 values, int count, int min, int max) { - if (values.size() != count) { + public static boolean isGoodValues(List values, List 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 parseAnswer(String answerToParse, int count, int min, int max, boolean returnDefaultOnError) { + public static List parseAnswer(String answerToParse, List constraints, int min, + int max, boolean returnDefaultOnError) { List res = new ArrayList<>(); // parse @@ -99,9 +161,9 @@ public static List parseAnswer(String answerToParse, int count, int min } // 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; diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index fd8813ae357d..5a94b2547a44 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -39,6 +39,7 @@ 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; @@ -299,7 +300,7 @@ default boolean isOpponent(Player player, UUID playerToCheckId) { void fireGetAmountEvent(UUID playerId, String message, int min, int max); - void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, Map options); + void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, Map options); void fireChoosePileEvent(UUID playerId, String message, List pile1, List pile2); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index b438e9c47f03..6f0207fdd6d4 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -66,6 +66,7 @@ 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; @@ -2879,7 +2880,8 @@ public void fireGetAmountEvent(UUID playerId, String message, int min, int max) } @Override - public void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, Map options) { + public void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, + Map options) { if (simulation) { return; } diff --git a/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java b/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java index 4a2d65854b7d..1dff3bbf6382 100644 --- a/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java +++ b/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java @@ -17,6 +17,7 @@ import mage.choices.Choice; import mage.game.permanent.Permanent; import mage.util.CardUtil; +import mage.util.MultiAmountMessage; /** * @@ -60,11 +61,11 @@ public enum QueryType { private List pile1; private List pile2; private Choice choice; - private List messages; + private List messages; private PlayerQueryEvent(UUID playerId, String message, List abilities, Set choices, - Set targets, Cards cards, QueryType queryType, int min, int max, boolean required, - Map options, List messages) { + Set targets, Cards cards, QueryType queryType, int min, int max, + boolean required, Map options, List messages) { super(playerId); CardUtil.checkSetParamForSerializationCompatibility(choices); @@ -216,8 +217,10 @@ public static PlayerQueryEvent amountEvent(UUID playerId, String message, int mi return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.AMOUNT, min, max, false, null, null); } - public static PlayerQueryEvent multiAmountEvent(UUID playerId, List messages, int min, int max, Map options) { - return new PlayerQueryEvent(playerId, null, null, null, null, null, QueryType.MULTI_AMOUNT, min, max, false, options, messages); + public static PlayerQueryEvent multiAmountEvent(UUID playerId, List messages, int min, + int max, Map options) { + return new PlayerQueryEvent(playerId, null, null, null, null, null, QueryType.MULTI_AMOUNT, min, max, false, + options, messages); } public static PlayerQueryEvent pickCard(UUID playerId, String message, List booster, int time) { @@ -300,7 +303,7 @@ public Choice getChoice() { return choice; } - public List getMessages() { + public List getMessages() { return messages; } } diff --git a/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java b/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java index b2aac5ec3059..d341aa523eb1 100644 --- a/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java +++ b/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java @@ -12,6 +12,7 @@ import mage.cards.Cards; import mage.choices.Choice; import mage.game.permanent.Permanent; +import mage.util.MultiAmountMessage; /** * @@ -84,7 +85,8 @@ public void amount(UUID playerId, String message, int min, int max) { dispatcher.fireEvent(PlayerQueryEvent.amountEvent(playerId, message, min, max)); } - public void multiAmount(UUID playerId, List messages, int min, int max, Map options) { + public void multiAmount(UUID playerId, List messages, int min, int max, + Map options) { dispatcher.fireEvent(PlayerQueryEvent.multiAmountEvent(playerId, messages, min, max, options)); } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 16de75751f1f..17d113b60d40 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -38,9 +38,11 @@ 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 @@ -743,7 +745,27 @@ default int announceXMana(int min, int max, String message, Game game, Ability a * @param game Game * @return List of integers with size equal to messages.size(). The sum of the integers is equal to max. */ - List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game); + default List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, + Game game) { + List 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 getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, int min, + int max, MultiAmountType type, Game game); void sideboard(Match match, Deck deck); diff --git a/Mage/src/main/java/mage/players/StubPlayer.java b/Mage/src/main/java/mage/players/StubPlayer.java index 4018c5c7ad3c..0cef8efa61bf 100644 --- a/Mage/src/main/java/mage/players/StubPlayer.java +++ b/Mage/src/main/java/mage/players/StubPlayer.java @@ -25,6 +25,7 @@ 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 int getAmount(int min, int max, String message, Game game) { } @Override - public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) { + public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, + int min, int max, MultiAmountType type, Game game) { return null; } diff --git a/Mage/src/main/java/mage/util/MultiAmountMessage.java b/Mage/src/main/java/mage/util/MultiAmountMessage.java new file mode 100644 index 000000000000..c9e607777dcb --- /dev/null +++ b/Mage/src/main/java/mage/util/MultiAmountMessage.java @@ -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; + } +}