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

* Added Goldberry

* Slight optimizaztion

* Happy Path Test

* More unhappy tests

* Sanity check for Goldberry's counter choices

* Updated player.getMultiAmount to support individual constraints

* Some cleanup

Also modified ResourcefulDefense to use new multi amount api

* Updated logging

* Added hint for number of counters

* Fixed issue with Resourceful Defense

* Improvements to defaults

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

* -1/-1 Counter test

* Fixed issue with -1/-1 counters

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

View file

@ -1,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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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