diff --git a/build.gradle b/build.gradle index 9b4bda9f14..3460760a75 100644 --- a/build.gradle +++ b/build.gradle @@ -321,8 +321,6 @@ dependencies { implementation 'com.github.RPTools:maptool-resources:1.6.0' // parser for macros implementation 'com.github.RPTools:parser:1.8.3' - // dice expressions - implementation 'net.rptools.dicelib:dicelib:1.8.6' // Currently hosted on nerps.net/repo implementation group: 'com.jidesoft', name: 'jide-common', version: '3.7.9' diff --git a/settings.gradle b/settings.gradle index a7198a167e..bd472eecbf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,10 +16,3 @@ include 'services:webservice' */ rootProject.name = 'MapTool' - - -sourceControl { - gitRepository(uri("https://github.com/RPTools/dicelib.git")) { - producesModule("net.rptools.dicelib:dicelib") - } -} \ No newline at end of file diff --git a/src/main/java/net/rptools/dicelib/expression/ExpressionParser.java b/src/main/java/net/rptools/dicelib/expression/ExpressionParser.java new file mode 100755 index 0000000000..e96e0fb8ac --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/ExpressionParser.java @@ -0,0 +1,295 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression; + +import net.rptools.dicelib.expression.function.ArsMagicaStress; +import net.rptools.dicelib.expression.function.CountSuccessDice; +import net.rptools.dicelib.expression.function.DropHighestRoll; +import net.rptools.dicelib.expression.function.DropRoll; +import net.rptools.dicelib.expression.function.ExplodeDice; +import net.rptools.dicelib.expression.function.ExplodingSuccessDice; +import net.rptools.dicelib.expression.function.FudgeRoll; +import net.rptools.dicelib.expression.function.HeroKillingRoll; +import net.rptools.dicelib.expression.function.HeroRoll; +import net.rptools.dicelib.expression.function.If; +import net.rptools.dicelib.expression.function.KeepLowestRoll; +import net.rptools.dicelib.expression.function.KeepRoll; +import net.rptools.dicelib.expression.function.OpenTestDice; +import net.rptools.dicelib.expression.function.RerollDice; +import net.rptools.dicelib.expression.function.RerollDiceOnce; +import net.rptools.dicelib.expression.function.Roll; +import net.rptools.dicelib.expression.function.RollWithBounds; +import net.rptools.dicelib.expression.function.ShadowRun4Dice; +import net.rptools.dicelib.expression.function.ShadowRun4ExplodeDice; +import net.rptools.dicelib.expression.function.ShadowRun5Dice; +import net.rptools.dicelib.expression.function.ShadowRun5ExplodeDice; +import net.rptools.dicelib.expression.function.UbiquityRoll; +import net.rptools.parser.*; +import net.rptools.parser.transform.RegexpStringTransformer; +import net.rptools.parser.transform.StringLiteralTransformer; + +public class ExpressionParser { + private static String[][] DICE_PATTERNS = + new String[][] { + // Comments + new String[] {"//.*", ""}, + + // Color hex strings #FFF or #FFFFFF or #FFFFFFFF (with alpha) + new String[] { + "(? xp.format()); + ret.setValue(xp.evaluate(resolver)); + ret.setRolled(newRunData.getRolled()); + } + } finally { + RunData.setCurrent(oldData); + } + + return ret; + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/Result.java b/src/main/java/net/rptools/dicelib/expression/Result.java new file mode 100755 index 0000000000..a8f39b2779 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/Result.java @@ -0,0 +1,91 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public class Result { + private final String expression; + private Supplier detailExpression; + private Object value; + private String description; + private List rolled; + + private final Map properties = new HashMap(); + + public Result(String expression) { + this.expression = expression; + } + + public String getExpression() { + return expression; + } + + public String getDetailExpression() { + return detailExpression != null ? detailExpression.get() : ""; + } + + public void setDetailExpression(Supplier detailExpression) { + this.detailExpression = detailExpression; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public Map getProperties() { + return this.properties; + } + + public void setRolled(List rolls) { + rolled = new ArrayList<>(rolls); + } + + public List getRolled() { + return Collections.unmodifiableList(rolled); + } + + public String format() { + StringBuilder sb = new StringBuilder(64); + sb.append(expression).append(" = "); + + if (detailExpression != null && !detailExpression.equals(value.toString())) { + sb.append("(").append(detailExpression).append(") = "); + } + + sb.append(value); + if (description != null) { + sb.append(" // ").append(description); + } + return sb.toString(); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/RunData.java b/src/main/java/net/rptools/dicelib/expression/RunData.java new file mode 100755 index 0000000000..738d1b864a --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/RunData.java @@ -0,0 +1,147 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression; + +import java.security.SecureRandom; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +public class RunData { + private static ThreadLocal current = new ThreadLocal(); + public static Random RANDOM = new SecureRandom(); + + private final Result result; + + private long randomValue; + private long randomMax; + private long randomMin; + + /** Should not be modified directly. Use {@link #recordRolled(Integer)} */ + private final List rolled = new LinkedList<>(); + + private final RunData parent; + + public RunData(Result result) { + this(result, null); + } + + /** + * Constructor for a new RunData with a parent. + * + * @param result the result object + * @param parent the parent RunData, to whom all rolls will be reported (if parent is not null) + */ + RunData(Result result, RunData parent) { + this.result = result; + this.parent = parent; + } + + /** Returns a random integer between 1 and maxValue */ + public int randomInt(int maxValue) { + return randomInt(1, maxValue); + } + + /** Returns a list of random integers between 1 and maxValue */ + public int[] randomInts(int num, int maxValue) { + int[] ret = new int[num]; + for (int i = 0; i < num; i++) { + ret[i] = randomInt(maxValue); + } + return ret; + } + + /** Returns a random integer between minValue and maxValue */ + public int randomInt(int minValue, int maxValue) { + randomMin += minValue; + randomMax += maxValue; + + int result = RANDOM.nextInt(maxValue - minValue + 1) + minValue; + + recordRolled(result); + + randomValue += result; + + return result; + } + + /** + * Returns a list of random integers between minValue and maxValue + * + * @return + */ + public int[] randomInts(int num, int minValue, int maxValue) { + int[] ret = new int[num]; + for (int i = 0; i < num; i++) ret[i] = randomInt(minValue, maxValue); + return ret; + } + + public Result getResult() { + return result; + } + + public static boolean hasCurrent() { + return current.get() != null; + } + + public static RunData getCurrent() { + RunData data = current.get(); + if (data == null) { + throw new NullPointerException("data cannot be null"); + } + return data; + } + + public static void setCurrent(RunData data) { + current.set(data); + } + + // If a seed is set we need to switch from SecureRandom to + // random. + public static void setSeed(long seed) { + RANDOM = new Random(seed); + } + + /** + * Records the rolls, passing through to the parent RunData (if any) + * + * @param roll the new roll to record + */ + void recordRolled(Integer roll) { + if (parent != null) parent.recordRolled(roll); + rolled.add(roll); + } + + /** + * Gets the list of rolled integers, including rolls generated by any child instances + * + * @return the list of rolls, in order + */ + public List getRolled() { + return Collections.unmodifiableList(rolled); + } + + /** + * Create a new RunData instance for a child execution. Rolls produced by the child instance + * should propagate up to the list of rolls maintained by this (parent) instance. + * + * @param childResult the Result object for the new RunData + * @return a new RunData with this RunData as its parent + */ + public RunData createChildRunData(Result childResult) { + return new RunData(childResult, this); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/ArsMagicaStress.java b/src/main/java/net/rptools/dicelib/expression/function/ArsMagicaStress.java new file mode 100644 index 0000000000..afa5610355 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/ArsMagicaStress.java @@ -0,0 +1,109 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; + +public class ArsMagicaStress extends AbstractNumberFunction { + + public ArsMagicaStress() { + super("arsMagicaStressNum", "arsMagicaStress"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws ParserException { + + int botchDice = ((BigDecimal) parameters.get(0)).intValue(); + int bonus = ((BigDecimal) parameters.get(1)).intValue(); + + if ("arsMagicaStressNum".equals(functionName)) { + return arsMagicaStress(botchDice, bonus); + } else if ("arsMagicaStress".equalsIgnoreCase(functionName)) { + return arsMagicaStressAsString(botchDice, bonus); + } + throw new ParserException("Unknown function name: " + functionName); + } + + private String arsMagicaStressAsString(int botchDice, int bonus) { + int val = arsMagicaStress(botchDice, bonus).intValue(); + if (val == -1) { + return "0 (1 botch)"; + } else if (val < -1) { + return "0 (" + (-val) + " botches)"; + } else { + return Integer.toString(val); + } + } + + /** + * Returns the results of an Ars Magicia Stress roll. If there are botches rolled then the result + * will be a negative value indicating the number of botches. + * + * @param botchDice the number of dice to roll if the first roll is a 0. + * @param bonus + * @return + */ + private BigDecimal arsMagicaStress(int botchDice, int bonus) { + int multiplier = 1; + boolean done = false; + int botches = 0; + int rollTotal = 0; + while (!done) { + int roll = DiceHelper.rollDice(1, 10); + if (roll == 10 && multiplier == 1) { // only a botch if we haven't rolled a 1 before + for (int i = 0; i < botchDice; i++) { + if (DiceHelper.rollDice(1, 10) == 10) { + botches++; + } + } + if (botches > 0) { + bonus = 0; // Once you have botches you no longer get any bonus. + } + done = true; + } else if (roll == 1) { + multiplier *= 2; + } else { + rollTotal = roll * multiplier; + done = true; + } + } + + /* + * If 10 (0) was rolled as first roll and no other 10s are rolled then + * rollTotal = 0 + * bonus = bonus + * botches = 0 + * + * If 10 (0) was rolled as first roll then more 10s where rolled + * rollTotal = 0 + * bonus = 0 + * botches = number of 10s rolled after first + * + * if the first roll was not 10 (0) then + * rollTotal = total from the roll + * bonus = bonus + * botches = 0 + */ + int val = Math.max(rollTotal + bonus, 0) - botches; + return BigDecimal.valueOf(val); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/CountSuccessDice.java b/src/main/java/net/rptools/dicelib/expression/function/CountSuccessDice.java new file mode 100755 index 0000000000..149a4eb5e2 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/CountSuccessDice.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class CountSuccessDice extends AbstractNumberFunction { + + public CountSuccessDice() { + super(3, 3, false, "countsuccess", "success"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int success = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.countSuccessDice(times, sides, success)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/DiceHelper.java b/src/main/java/net/rptools/dicelib/expression/function/DiceHelper.java new file mode 100755 index 0000000000..d2eb4a44da --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/DiceHelper.java @@ -0,0 +1,287 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.util.Arrays; +import java.util.Comparator; +import net.rptools.dicelib.expression.RunData; +import net.rptools.parser.function.*; + +public class DiceHelper { + public static int rollDice(int times, int sides) { + int result = 0; + + RunData runData = RunData.getCurrent(); + + for (int i = 0; i < times; i++) { + result += runData.randomInt(sides); + } + + return result; + } + + public static String explodingSuccessDice(int times, int sides, int target) + throws EvaluationException { + String rolls = "Dice: "; + int successes = 0; + + for (int i = 0; i < times; i++) { + int currentRoll = explodeDice(1, sides); + rolls += currentRoll + ", "; + if (currentRoll >= target) successes++; + } + return rolls + "Successes: " + successes; + } + + public static String openTestDice(int times, int sides) throws EvaluationException { + String rolls = "Dice: "; + int max = 0; + + for (int i = 0; i < times; i++) { + int currentRoll = explodeDice(1, sides); + rolls += currentRoll + ", "; + if (currentRoll > max) max = currentRoll; + } + return rolls + "Maximum: " + max; + } + + public static int fudgeDice(int times) { + return rollDice(times, 3) - (2 * times); + } + + public static int ubiquityDice(int times) { + return rollDice(times, 2) - times; + } + + public static int keepDice(int times, int sides, int keep) throws EvaluationException { + if (keep > times) throw new EvaluationException("You cannot keep more dice than you roll"); + return dropDice(times, sides, times - keep); + } + + public static int keepLowestDice(int times, int sides, int keep) throws EvaluationException { + if (keep > times) throw new EvaluationException("You cannot keep more dice than you roll"); + return dropDiceHighest(times, sides, times - keep); + } + + public static int dropDice(int times, int sides, int drop) throws EvaluationException { + if (times - drop <= 0) throw new EvaluationException("You cannot drop more dice than you roll"); + + RunData runData = RunData.getCurrent(); + + int[] values = runData.randomInts(times, sides); + + Arrays.sort(values); + + int result = 0; + for (int i = drop; i < times; i++) { + result += values[i]; + } + + return result; + } + + public static int dropDiceHighest(int times, int sides, int drop) throws EvaluationException { + if (times - drop <= 0) throw new EvaluationException("You cannot drop more dice than you roll"); + + RunData runData = RunData.getCurrent(); + + int[] values = runData.randomInts(times, sides); + + int[] descValues = + Arrays.stream(values) + .boxed() + .sorted(Comparator.reverseOrder()) + .mapToInt(Integer::intValue) + .toArray(); + + int result = 0; + for (int i = drop; i < times; i++) { + result += descValues[i]; + } + + return result; + } + + public static int rerollDice(int times, int sides, int lowerBound) throws EvaluationException { + RunData runData = RunData.getCurrent(); + + if (lowerBound > sides) + throw new EvaluationException( + "When rerolling, the lowerbound must be smaller than the number of sides on the rolling dice."); + + int[] values = new int[times]; + + for (int i = 0; i < values.length; i++) { + int roll; + while ((roll = runData.randomInt(sides)) < lowerBound) + ; + + values[i] = roll; + } + + int result = 0; + for (int i = 0; i < values.length; i++) { + result += values[i]; + } + + return result; + } + + /** + * Rolls X dice with Y sides each, with any result lower than L being re-rolled once. If + * chooseHigher is true, the higher of the two rolled values is kept. Otherwise, the new roll is + * kept regardless. + * + *

Differs from {@link #rerollDice(int, int, int)} in that the new results are allowed to fall + * beneath the given lowerBound, instead of being re-rolled again. + * + * @param times the number of dice + * @param sides the number of sides + * @param lowerBound the number below which dice will be re-rolled. Must be strictly lower than + * the number of sides. + * @param chooseHigher whether the original result may be preserved if it was the higher value + * @return the total of the rolled and re-rolled dice + * @throws EvaluationException if an invalid lowerBound is provided + */ + public static int rerollDiceOnce(int times, int sides, int lowerBound, boolean chooseHigher) + throws EvaluationException { + RunData runData = RunData.getCurrent(); + + if (lowerBound > sides) + throw new EvaluationException( + "When rerolling, the lowerbound must be smaller than the number of sides on the rolling dice."); + + int[] values = new int[times]; + + for (int i = 0; i < values.length; i++) { + int roll = runData.randomInt(sides); + if (roll < lowerBound) { + int roll2 = runData.randomInt(sides); + if (chooseHigher) { + roll = Math.max(roll, roll2); + } else { + roll = roll2; + } + } + values[i] = roll; + } + + int result = 0; + for (int i = 0; i < values.length; i++) { + result += values[i]; + } + + return result; + } + + public static int explodeDice(int times, int sides) throws EvaluationException { + int result = 0; + + if (sides == 0 || sides == 1) throw new EvaluationException("Number of sides must be > 1"); + + RunData runData = RunData.getCurrent(); + + for (int i = 0; i < times; i++) { + int roll = runData.randomInt(sides); + if (roll == sides) times++; + result += roll; + } + + return result; + } + + public static int countSuccessDice(int times, int sides, int success) { + RunData runData = RunData.getCurrent(); + + int result = 0; + for (int value : runData.randomInts(times, sides)) { + if (value >= success) result++; + } + + return result; + } + + public enum ShadowrunEdition { + EDITION_4, + EDITION_5 + } + + public static String countShadowRun( + int poolSize, int gremlins, boolean explode, ShadowrunEdition edition) { + RunData runData = RunData.getCurrent(); + + int hitCount = 0; + int oneCount = 0; + int sides = 6; + int success = 5; + StringBuilder actual = new StringBuilder(); + + int times = poolSize; + for (int i = 0; i < times; i++) { + int value = runData.randomInt(sides); + + if (value >= success) hitCount++; + + if (value == 1) oneCount++; + + if (value == 6 && explode) times++; + + actual.append(value).append(" "); + } + + // Check for Glitchs + // TODO check, if there already was a bug here concerning glitches on exploding dice in SR4 + // in SR5, Exploding dice are re-rolled and do not increase the pool size tested here + boolean normalGlitch = + edition == ShadowrunEdition.EDITION_4 + // SR4: half of pool or more + ? ((double) oneCount >= ((double) times / 2)) + // SR5: strictly more than half of pool + : ((double) oneCount > ((double) poolSize / 2)); + + boolean gremlinGlitch = + edition == ShadowrunEdition.EDITION_4 + ? ((double) oneCount >= ((double) times / 2 - gremlins)) + : ((double) oneCount > ((double) poolSize / 2 - gremlins)); + + boolean noSuccess = hitCount == 0; + // Both Editions: Critical, if no success + String criticalPart = noSuccess ? "Critical " : ""; + // Signalize glitches only caused due to gremlins for storytelling + String gremlinPart = (gremlinGlitch ^ normalGlitch) ? "Gremlin " : ""; + // but only if this was a glitch. + // if anyone feeds invalid negative gremlin values into this, non-glitches will become gremlin + // glitches. + String glitchFormatted = + (normalGlitch || gremlinGlitch) ? " *" + criticalPart + gremlinPart + "Glitch*" : ""; + + String result = + "Hits: " + hitCount + " Ones: " + oneCount + glitchFormatted + " Results: " + actual; + + return result; + } + + public static int rollModWithBounds(int times, int sides, int mod, int lower, int upper) { + int result = 0; + + for (int i = 0; i < times; i++) { + int roll = rollDice(1, sides); + int val = Math.min(Math.max(roll + mod, lower), upper); + result += val; + } + + return result; + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/DropHighestRoll.java b/src/main/java/net/rptools/dicelib/expression/function/DropHighestRoll.java new file mode 100755 index 0000000000..9baf93a65d --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/DropHighestRoll.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class DropHighestRoll extends AbstractNumberFunction { + + public DropHighestRoll() { + super(3, 3, false, "dropHighest"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int drop = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.dropDiceHighest(times, sides, drop)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/DropRoll.java b/src/main/java/net/rptools/dicelib/expression/function/DropRoll.java new file mode 100755 index 0000000000..4f667b3af5 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/DropRoll.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class DropRoll extends AbstractNumberFunction { + + public DropRoll() { + super(3, 3, false, "drop"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int drop = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.dropDice(times, sides, drop)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/ExplodeDice.java b/src/main/java/net/rptools/dicelib/expression/function/ExplodeDice.java new file mode 100755 index 0000000000..2ad3a985e6 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/ExplodeDice.java @@ -0,0 +1,40 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class ExplodeDice extends AbstractNumberFunction { + + public ExplodeDice() { + super(2, 2, false, "explode"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.explodeDice(times, sides)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/ExplodingSuccessDice.java b/src/main/java/net/rptools/dicelib/expression/function/ExplodingSuccessDice.java new file mode 100755 index 0000000000..1004753364 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/ExplodingSuccessDice.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class ExplodingSuccessDice extends AbstractNumberFunction { + + public ExplodingSuccessDice() { + super(3, 3, true, "explodingSuccess"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int target = ((BigDecimal) parameters.get(n++)).intValue(); + + return DiceHelper.explodingSuccessDice(times, sides, target); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/FudgeRoll.java b/src/main/java/net/rptools/dicelib/expression/function/FudgeRoll.java new file mode 100755 index 0000000000..ed7b760059 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/FudgeRoll.java @@ -0,0 +1,47 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; + +/** + * fudge(range) + * + *

Generate a random number form 1 to sides, times number of times. If + * times is not supplied it defaults to 1. + * + *

Example: fudge(4) = 4dF (-4..4) + */ +public class FudgeRoll extends AbstractNumberFunction { + + public FudgeRoll() { + super(1, 1, false, "f", "fudge"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) { + int n = 0; + + int times = 1; + if (parameters.size() == 1) times = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.fudgeDice(times)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/HeroKillingRoll.java b/src/main/java/net/rptools/dicelib/expression/function/HeroKillingRoll.java new file mode 100755 index 0000000000..091f57735e --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/HeroKillingRoll.java @@ -0,0 +1,110 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.dicelib.expression.RunData; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; + +/* + * Hero System Dice + * + * Used to get both the stun & body of an attack roll. + * + */ +public class HeroKillingRoll extends AbstractNumberFunction { + public HeroKillingRoll() { + super(2, 3, false, "herokilling", "herokilling2", "killing", "heromultiplier", "multiplier"); + } + + // Use variable names with illegal character to minimize chances of variable overlap + private static String lastKillingBodyVar = "#Hero-LastKillingBodyVar"; + + @Override + public Object childEvaluate( + Parser parser, VariableResolver vr, String functionName, List parameters) + throws ParserException { + int n = 0; + + double times = ((BigDecimal) parameters.get(n++)).doubleValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + double half = times - Math.floor(times); + int extra = 0; + if (parameters.size() > 2) extra = ((BigDecimal) parameters.get(n++)).intValue(); + + RunData runData = RunData.getCurrent(); + + if (functionName.equalsIgnoreCase("herokilling")) { + + int body = DiceHelper.rollDice((int) times, sides); + body = body + extra; + /* + * If value half or more roll a half die + */ + if (half >= 0.5) { + /* + * Roll a half dice. + */ + int die = runData.randomInt(sides); + body += (die + 1) / 2; + } else if (half >= 0.2) { + /* + * Add a single pip + */ + body++; + } + + vr.setVariable(lastKillingBodyVar, new BigDecimal(body)); + return new BigDecimal(body); + } else if (functionName.equalsIgnoreCase("herokilling2")) { + + int body = DiceHelper.rollDice((int) times, sides); + body = body + extra; + /* + * If value half or more roll a die -1. minimum value of 1. + */ + if (half >= 0.5) { + /* + * Roll a half dice. + */ + int die = runData.randomInt(sides); + if (die > 1) die = die - 1; + body += die; + } else if (half >= 0.2) { + /* + * Add a single pip + */ + body++; + } + + vr.setVariable(lastKillingBodyVar, new BigDecimal(body)); + return new BigDecimal(body); + } else { + int multi = DiceHelper.rollDice((int) times, sides); + multi = multi + extra; + if (multi < 1) multi = 1; + + int lastBody = 0; + if (vr.containsVariable(lastKillingBodyVar)) + lastBody = ((BigDecimal) vr.getVariable(lastKillingBodyVar)).intValue(); + + return new BigDecimal(lastBody * multi); + } + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/HeroRoll.java b/src/main/java/net/rptools/dicelib/expression/function/HeroRoll.java new file mode 100755 index 0000000000..0f817955ae --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/HeroRoll.java @@ -0,0 +1,114 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.dicelib.expression.RunData; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; + +/* + * Hero System Dice + * + * Used to get both the stun & body of an attack roll. + * + */ +public class HeroRoll extends AbstractNumberFunction { + public HeroRoll() { + super(2, 2, false, "hero", "herostun", "herobody"); + } + + // Use variable names with illegal character to minimize chances of variable overlap + private static String lastTimesVar = "#Hero-LastTimesVar"; + private static String lastSidesVar = "#Hero-LastSidesVar"; + private static String lastBodyVar = "#Hero-LastBodyVar"; + + @Override + public Object childEvaluate( + Parser parser, VariableResolver vr, String functionName, List parameters) + throws ParserException { + int n = 0; + + double times = ((BigDecimal) parameters.get(n++)).doubleValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + + if (functionName.equalsIgnoreCase("herobody")) { + double lastTimes = 0; + if (vr.containsVariable(lastTimesVar)) + lastTimes = ((BigDecimal) vr.getVariable(lastTimesVar)).doubleValue(); + + int lastSides = 0; + if (vr.containsVariable(lastSidesVar)) + lastSides = ((BigDecimal) vr.getVariable(lastSidesVar)).intValue(); + + int lastBody = 0; + if (vr.containsVariable(lastBodyVar)) + lastBody = ((BigDecimal) vr.getVariable(lastBodyVar)).intValue(); + + if (times == lastTimes && sides == lastSides) return new BigDecimal(lastBody); + + return new BigDecimal(-1); // Should this be -1? Perhaps it should return null. + } else if ("hero".equalsIgnoreCase(functionName) || "herostun".equalsIgnoreCase(functionName)) { + // assume stun + + double lastTimes = times; + int lastSides = sides; + int lastBody = 0; + + RunData runData = RunData.getCurrent(); + + int stun = 0; + double half = times - Math.floor(times); + for (int i = 0; i < Math.floor(times); i++) { + int die = runData.randomInt(sides); + /* + * Keep track of the body generated. In theory + * Hero System only uses 6-sided where a 1 is + * 0 body, 2-5 is 1 body and 6 is 2 body but I + * left the sides unbounded just in case. + */ + if (die > 1) lastBody++; + if (die == sides) lastBody++; + + stun += die; + } + + if (half >= 0.5) { + /* + * Roll a half dice. In theory Hero System + * only uses 6-sided and for half dice + * 1 & 2 = 1 Stun 0 body + * 3 = 2 stun 0 body + * 4 = 2 stun 1 body + * 5 & 6 = 3 stun 1 body + */ + int die = runData.randomInt(sides); + if (die * 2 > sides) lastBody++; + + stun += (die + 1) / 2; + } + + vr.setVariable(lastTimesVar, new BigDecimal(lastTimes)); + vr.setVariable(lastSidesVar, new BigDecimal(lastSides)); + vr.setVariable(lastBodyVar, new BigDecimal(lastBody)); + + return new BigDecimal(stun); + } + throw new ParserException("Unknown function name: " + functionName); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/If.java b/src/main/java/net/rptools/dicelib/expression/function/If.java new file mode 100755 index 0000000000..249ae8a600 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/If.java @@ -0,0 +1,38 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractFunction; +import net.rptools.parser.function.EvaluationException; + +public class If extends AbstractFunction { + + public If() { + super(3, 3, false, "if"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + if (BigDecimal.ZERO.equals((BigDecimal) parameters.get(0))) return parameters.get(2); + + return parameters.get(1); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/KeepLowestRoll.java b/src/main/java/net/rptools/dicelib/expression/function/KeepLowestRoll.java new file mode 100755 index 0000000000..bd5e0a3a1c --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/KeepLowestRoll.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class KeepLowestRoll extends AbstractNumberFunction { + + public KeepLowestRoll() { + super(3, 3, false, "keepLowest"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int keep = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.keepLowestDice(times, sides, keep)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/KeepRoll.java b/src/main/java/net/rptools/dicelib/expression/function/KeepRoll.java new file mode 100755 index 0000000000..da6f662826 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/KeepRoll.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class KeepRoll extends AbstractNumberFunction { + + public KeepRoll() { + super(3, 3, false, "keep"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int keep = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.keepDice(times, sides, keep)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/OpenTestDice.java b/src/main/java/net/rptools/dicelib/expression/function/OpenTestDice.java new file mode 100755 index 0000000000..201c15f090 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/OpenTestDice.java @@ -0,0 +1,40 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class OpenTestDice extends AbstractNumberFunction { + + public OpenTestDice() { + super(2, 2, true, "openTest"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + + return DiceHelper.openTestDice(times, sides); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/RerollDice.java b/src/main/java/net/rptools/dicelib/expression/function/RerollDice.java new file mode 100755 index 0000000000..e8a592efe6 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/RerollDice.java @@ -0,0 +1,41 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class RerollDice extends AbstractNumberFunction { + + public RerollDice() { + super(3, 3, false, "reroll"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int lowerBound = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.rerollDice(times, sides, lowerBound)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/RerollDiceOnce.java b/src/main/java/net/rptools/dicelib/expression/function/RerollDiceOnce.java new file mode 100755 index 0000000000..86bea8ee68 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/RerollDiceOnce.java @@ -0,0 +1,52 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +/** + * Will re-roll dice under a given threshold once, and optionally choose the higher of the two + * results. + * + *

Differs from {@link RerollDice} in that the new results are allowed to be below the + * lowerBound. + */ +public class RerollDiceOnce extends AbstractNumberFunction { + + public RerollDiceOnce() { + super(3, 4, false, "rerollOnce"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + int n = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + int lowerBound = ((BigDecimal) parameters.get(n++)).intValue(); + boolean chooseHigher = false; + if (parameters.size() > n) + chooseHigher = + !BigDecimal.ZERO.equals(parameters.get(n)); // as with If, anything other than 0 is truthy + + return new BigDecimal(DiceHelper.rerollDiceOnce(times, sides, lowerBound, chooseHigher)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/Roll.java b/src/main/java/net/rptools/dicelib/expression/function/Roll.java new file mode 100755 index 0000000000..1bb64dd800 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/Roll.java @@ -0,0 +1,49 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; + +/** + * roll(range) or roll(times, range) + * + *

Generate a random number form 1 to sides, times number of times. If + * times is not supplied it defaults to 1. + * + *

Example: roll(4, 6) = 4d6 + */ +public class Roll extends AbstractNumberFunction { + + public Roll() { + super(1, 2, false, "d", "roll", "dice"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) { + int n = 0; + + int times = 1; + if (parameters.size() == 2) times = ((BigDecimal) parameters.get(n++)).intValue(); + + int sides = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.rollDice(times, sides)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/RollWithBounds.java b/src/main/java/net/rptools/dicelib/expression/function/RollWithBounds.java new file mode 100644 index 0000000000..c33dc3a2d4 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/RollWithBounds.java @@ -0,0 +1,89 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; + +public class RollWithBounds extends AbstractNumberFunction { + + public RollWithBounds() { + super( + 3, + 4, + false, + "rollWithUpper", + "rollWithLower", + "rollAddWithUpper", + "rollAddWithLower", + "rollSubWithUpper", + "rollSubWithLower"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws ParserException { + int times = 0; + int sides = 0; + int mod = 0; + int lower = Integer.MIN_VALUE; + int upper = Integer.MAX_VALUE; + + switch (functionName) { + case "rollWithUpper": + times = ((BigDecimal) parameters.get(0)).intValue(); + sides = ((BigDecimal) parameters.get(1)).intValue(); + upper = ((BigDecimal) parameters.get(2)).intValue(); + break; + case "rollWithLower": + times = ((BigDecimal) parameters.get(0)).intValue(); + sides = ((BigDecimal) parameters.get(1)).intValue(); + lower = ((BigDecimal) parameters.get(2)).intValue(); + break; + case "rollAddWithUpper": + times = ((BigDecimal) parameters.get(0)).intValue(); + sides = ((BigDecimal) parameters.get(1)).intValue(); + mod = ((BigDecimal) parameters.get(2)).intValue(); + upper = ((BigDecimal) parameters.get(3)).intValue(); + break; + case "rollAddWithLower": + times = ((BigDecimal) parameters.get(0)).intValue(); + sides = ((BigDecimal) parameters.get(1)).intValue(); + mod = ((BigDecimal) parameters.get(2)).intValue(); + lower = ((BigDecimal) parameters.get(3)).intValue(); + break; + case "rollSubWithUpper": + times = ((BigDecimal) parameters.get(0)).intValue(); + sides = ((BigDecimal) parameters.get(1)).intValue(); + mod = -((BigDecimal) parameters.get(2)).intValue(); + upper = ((BigDecimal) parameters.get(3)).intValue(); + break; + case "rollSubWithLower": + times = ((BigDecimal) parameters.get(0)).intValue(); + sides = ((BigDecimal) parameters.get(1)).intValue(); + mod = -((BigDecimal) parameters.get(2)).intValue(); + lower = ((BigDecimal) parameters.get(3)).intValue(); + break; + default: + throw new ParserException("Unknown function name: " + functionName); + } + return new BigDecimal(DiceHelper.rollModWithBounds(times, sides, mod, lower, upper)); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/ShadowRun4Dice.java b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun4Dice.java new file mode 100755 index 0000000000..beba1127e8 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun4Dice.java @@ -0,0 +1,42 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class ShadowRun4Dice extends AbstractNumberFunction { + + public ShadowRun4Dice() { + super(1, 2, true, "sr4"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + + int n = 0; + int gremlins = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + if (parameters.size() == 2) gremlins = ((BigDecimal) parameters.get(n++)).intValue(); + + return DiceHelper.countShadowRun(times, gremlins, false, DiceHelper.ShadowrunEdition.EDITION_4); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/ShadowRun4ExplodeDice.java b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun4ExplodeDice.java new file mode 100755 index 0000000000..ca5f572444 --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun4ExplodeDice.java @@ -0,0 +1,42 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class ShadowRun4ExplodeDice extends AbstractNumberFunction { + + public ShadowRun4ExplodeDice() { + super(1, 2, true, "sr4e"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + + int n = 0; + int gremlins = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + if (parameters.size() == 2) gremlins = ((BigDecimal) parameters.get(n++)).intValue(); + + return DiceHelper.countShadowRun(times, gremlins, true, DiceHelper.ShadowrunEdition.EDITION_4); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/ShadowRun5Dice.java b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun5Dice.java new file mode 100644 index 0000000000..276496b7df --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun5Dice.java @@ -0,0 +1,42 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class ShadowRun5Dice extends AbstractNumberFunction { + + public ShadowRun5Dice() { + super(1, 2, true, "sr5"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + + int n = 0; + int gremlins = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + if (parameters.size() == 2) gremlins = ((BigDecimal) parameters.get(n++)).intValue(); + + return DiceHelper.countShadowRun(times, gremlins, false, DiceHelper.ShadowrunEdition.EDITION_5); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/ShadowRun5ExplodeDice.java b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun5ExplodeDice.java new file mode 100644 index 0000000000..49c24b5f4a --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/ShadowRun5ExplodeDice.java @@ -0,0 +1,42 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; +import net.rptools.parser.function.EvaluationException; + +public class ShadowRun5ExplodeDice extends AbstractNumberFunction { + + public ShadowRun5ExplodeDice() { + super(1, 2, true, "sr5e"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws EvaluationException { + + int n = 0; + int gremlins = 0; + int times = ((BigDecimal) parameters.get(n++)).intValue(); + if (parameters.size() == 2) gremlins = ((BigDecimal) parameters.get(n++)).intValue(); + + return DiceHelper.countShadowRun(times, gremlins, true, DiceHelper.ShadowrunEdition.EDITION_5); + } +} diff --git a/src/main/java/net/rptools/dicelib/expression/function/UbiquityRoll.java b/src/main/java/net/rptools/dicelib/expression/function/UbiquityRoll.java new file mode 100755 index 0000000000..81fcb4293a --- /dev/null +++ b/src/main/java/net/rptools/dicelib/expression/function/UbiquityRoll.java @@ -0,0 +1,47 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import java.math.BigDecimal; +import java.util.List; +import net.rptools.parser.Parser; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractNumberFunction; + +/** + * ubiquity(range) + * + *

Generate a random number form 1 to sides, times number of times. If + * times is not supplied it defaults to 1. + * + *

Example: ubiquity(4) = 4dU (0..4 successes) + */ +public class UbiquityRoll extends AbstractNumberFunction { + + public UbiquityRoll() { + super(1, 1, false, "u", "ubiquity"); + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) { + int n = 0; + + int times = 1; + if (parameters.size() == 1) times = ((BigDecimal) parameters.get(n++)).intValue(); + + return new BigDecimal(DiceHelper.ubiquityDice(times)); + } +} diff --git a/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java b/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java index b535cf3ad6..92bb63a4ca 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java +++ b/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java @@ -19,11 +19,10 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import net.rptools.common.expression.ExpressionParser; +import net.rptools.dicelib.expression.ExpressionParser; import net.rptools.maptool.client.functions.*; import net.rptools.maptool.client.functions.json.JSONMacroFunctions; import net.rptools.maptool.client.script.javascript.*; -import net.rptools.maptool.client.script.javascript.api.*; import net.rptools.parser.Expression; import net.rptools.parser.Parser; import net.rptools.parser.ParserException; diff --git a/src/main/java/net/rptools/maptool/client/MapToolLineParser.java b/src/main/java/net/rptools/maptool/client/MapToolLineParser.java index fc3ddb3f48..c17652c02f 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolLineParser.java +++ b/src/main/java/net/rptools/maptool/client/MapToolLineParser.java @@ -21,7 +21,7 @@ import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; -import net.rptools.common.expression.Result; +import net.rptools.dicelib.expression.Result; import net.rptools.maptool.client.functions.*; import net.rptools.maptool.client.functions.exceptions.*; import net.rptools.maptool.client.functions.json.JSONMacroFunctions; diff --git a/src/main/java/net/rptools/maptool/client/OptionInfo.java b/src/main/java/net/rptools/maptool/client/OptionInfo.java index 3bf8f815c6..22f6f5df81 100644 --- a/src/main/java/net/rptools/maptool/client/OptionInfo.java +++ b/src/main/java/net/rptools/maptool/client/OptionInfo.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import net.rptools.common.expression.Result; +import net.rptools.dicelib.expression.Result; import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.Token; import net.rptools.parser.ParserException; diff --git a/src/main/java/net/rptools/maptool/client/functions/JSONMacroFunctionsOld.java b/src/main/java/net/rptools/maptool/client/functions/JSONMacroFunctionsOld.java index 43c094cb1f..ef6ca66351 100644 --- a/src/main/java/net/rptools/maptool/client/functions/JSONMacroFunctionsOld.java +++ b/src/main/java/net/rptools/maptool/client/functions/JSONMacroFunctionsOld.java @@ -19,7 +19,7 @@ import com.jayway.jsonpath.spi.json.GsonJsonProvider; import java.math.BigDecimal; import java.util.*; -import net.rptools.common.expression.ExpressionParser; +import net.rptools.dicelib.expression.ExpressionParser; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.MapToolVariableResolver; import net.rptools.maptool.language.I18N; diff --git a/src/main/java/net/rptools/maptool/client/functions/json/JSONMacroFunctions.java b/src/main/java/net/rptools/maptool/client/functions/json/JSONMacroFunctions.java index 5c7ed52e4f..e4bf00b01c 100644 --- a/src/main/java/net/rptools/maptool/client/functions/json/JSONMacroFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/json/JSONMacroFunctions.java @@ -22,8 +22,8 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import net.rptools.common.expression.ExpressionParser; -import net.rptools.common.expression.Result; +import net.rptools.dicelib.expression.ExpressionParser; +import net.rptools.dicelib.expression.Result; import net.rptools.maptool.client.MapToolVariableResolver; import net.rptools.maptool.client.functions.EvalMacroFunctions; import net.rptools.maptool.language.I18N; diff --git a/src/main/java/net/rptools/maptool/model/LookupTable.java b/src/main/java/net/rptools/maptool/model/LookupTable.java index 7e789c412b..4ed696ac9b 100644 --- a/src/main/java/net/rptools/maptool/model/LookupTable.java +++ b/src/main/java/net/rptools/maptool/model/LookupTable.java @@ -19,8 +19,8 @@ import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import net.rptools.common.expression.ExpressionParser; -import net.rptools.common.expression.Result; +import net.rptools.dicelib.expression.ExpressionParser; +import net.rptools.dicelib.expression.Result; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.server.proto.LookupEntryDto; diff --git a/src/test/java/net/rptools/dicelib/expression/ExpressionParserTest.java b/src/test/java/net/rptools/dicelib/expression/ExpressionParserTest.java new file mode 100755 index 0000000000..e6c46821c9 --- /dev/null +++ b/src/test/java/net/rptools/dicelib/expression/ExpressionParserTest.java @@ -0,0 +1,405 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.rptools.parser.MapVariableResolver; +import net.rptools.parser.ParserException; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.EvaluationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ExpressionParserTest { + + /** + * Make sure these tests aren't using mock RunData instances that might still be Current. + * Safeguard against possible problematic interleaving if tests get run in parallel. + */ + @BeforeEach + public void setUp() { + RunData.setCurrent(null); + } + + @Test + public void testEvaluate() throws ParserException { + Result result = new ExpressionParser().evaluate("100+4d1*10"); + + assertNotNull(result); + assertEquals("100+4d1*10", result.getExpression()); + assertEquals("100 + 4 * 10", result.getDetailExpression()); + assertEquals(new BigDecimal(140), (BigDecimal) result.getValue()); + } + + @Test + public void testEvaluate_Explode() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("100+10d6e+1"); + + assertEquals(new BigDecimal(164), result.getValue()); + } + + @Test + public void testEvaluate_Drop() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("100+10d6d2+1"); + + assertEquals(new BigDecimal(138), result.getValue()); + } + + @Test + public void testEvaluate_Keep() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("100+10d6k8+1"); + + assertEquals(new BigDecimal(138), result.getValue()); + } + + @Test + public void testEvaluate_Keep_error_tooManyDice() throws ParserException { + try { + Result result = new ExpressionParser().evaluate("2d6k4"); + fail("Expected EvaluationException from trying to keep too many dice"); + } catch (EvaluationException expected) { + // test passes if expected exception is produced + } + } + + @Test + public void testEvaluate_KeepLowest_error_tooManyDice() throws ParserException { + try { + Result result = new ExpressionParser().evaluate("2d6kl4"); + fail("Expected EvaluationException from trying to keep too many dice"); + } catch (EvaluationException expected) { + // test passes if expected exception is produced + } + } + + @Test + public void testEvaluate_RerollOnceAndKeep() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("20d10rk5"); + // the sequence of rolls produced includes an instance of a 4 being replaced by a 3 + assertEquals(new BigDecimal(121), result.getValue()); + } + + @Test + public void testEvaluate_RerollOnceAndChoose() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("20d10rc5"); + // in rerollAndChoose mode, the 4 gets preserved instead of being replaced by the 3 + assertEquals(new BigDecimal(122), result.getValue()); + } + + @Test + public void testEvaluate_CountSuccess() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("100+10d6s4+1"); + + assertEquals(new BigDecimal(109), result.getValue()); + } + + @Test + public void testEvaluate_ExplodingSuccess() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("10d4es6"); + assertEquals("10d4es6", result.getExpression()); + assertEquals("explodingSuccess(10, 4, 6)", result.getDetailExpression()); + assertEquals("Dice: 1, 2, 2, 1, 2, 7, 1, 7, 2, 3, Successes: 2", result.getValue()); + RunData.setSeed(10423L); + + result = new ExpressionParser().evaluate("10es9"); + assertEquals("10es9", result.getExpression()); + assertEquals("explodingSuccess(10, 6, 9)", result.getDetailExpression()); + assertEquals("Dice: 4, 4, 4, 3, 16, 5, 1, 4, 14, 8, Successes: 2", result.getValue()); + } + + @Test + public void testEvaluate_OpenTest() throws ParserException { + RunData.setSeed(10423L); + Result result = new ExpressionParser().evaluate("10d4o"); + assertEquals("10d4o", result.getExpression()); + assertEquals("openTest(10, 4)", result.getDetailExpression()); + assertEquals("Dice: 1, 2, 2, 1, 2, 7, 1, 7, 2, 3, Maximum: 7", result.getValue()); + + RunData.setSeed(10423L); + result = new ExpressionParser().evaluate("10o"); + assertEquals("10o", result.getExpression()); + assertEquals("openTest(10, 6)", result.getDetailExpression()); + assertEquals("Dice: 4, 4, 4, 3, 16, 5, 1, 4, 14, 8, Maximum: 16", result.getValue()); + } + + @Test + public void testEvaluate_SR4Success() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr4"); + assertEquals("5sr4", result.getExpression()); + assertEquals("sr4(5)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 1 Results: 3 1 4 6 3 ", result.getValue()); + } + + @Test + public void testEvaluate_SR4GremlinSuccess() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr4g2"); + assertEquals("5sr4g2", result.getExpression()); + assertEquals("sr4(5, 2)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 1 *Gremlin Glitch* Results: 3 1 4 6 3 ", result.getValue()); + } + + @Test + public void testEvaluate_SR4ExplodingSuccess() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr4e"); + assertEquals("5sr4e", result.getExpression()); + assertEquals("sr4e(5)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 2 Results: 3 1 4 6 3 1 ", result.getValue()); + } + + @Test + public void testEvaluate_SR4ExplodingGremlinSuccess() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr4eg2"); + assertEquals("5sr4eg2", result.getExpression()); + assertEquals("sr4e(5, 2)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 2 *Gremlin Glitch* Results: 3 1 4 6 3 1 ", result.getValue()); + } + + @Test + public void testEvaluate_SR5Success() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr5"); + assertEquals("5sr5", result.getExpression()); + assertEquals("sr5(5)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 1 Results: 3 1 4 6 3 ", result.getValue()); + } + + @Test + public void testEvaluate_SR5GremlinSuccess() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr5g2"); + assertEquals("5sr5g2", result.getExpression()); + assertEquals("sr5(5, 2)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 1 *Gremlin Glitch* Results: 3 1 4 6 3 ", result.getValue()); + } + + @Test + public void testEvaluate_SR5ExplodingSuccess() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr5e"); + assertEquals("5sr5e", result.getExpression()); + assertEquals("sr5e(5)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 2 Results: 3 1 4 6 3 1 ", result.getValue()); + } + + @Test + public void testEvaluate_SR5ExplodingGremlinSuccess() throws ParserException { + RunData.setSeed(10523L); + Result result = new ExpressionParser().evaluate("5sr5eg2"); + assertEquals("5sr5eg2", result.getExpression()); + assertEquals("sr5e(5, 2)", result.getDetailExpression()); + assertEquals("Hits: 1 Ones: 2 *Gremlin Glitch* Results: 3 1 4 6 3 1 ", result.getValue()); + } + + @Test + public void testEvaluate_HeroRoll() throws ParserException { + RunData.setSeed(10423L); + ExpressionParser parser = new ExpressionParser(); + VariableResolver resolver = new MapVariableResolver(); + + Result result = parser.evaluate("4.5d6h", resolver); + assertEquals(new BigDecimal(18), result.getValue()); + + result = parser.evaluate("4.5d6b", resolver); + assertEquals(new BigDecimal(5), result.getValue()); + + RunData.setSeed(10423L); + parser = new ExpressionParser(); + resolver = new MapVariableResolver(); + + result = parser.evaluate("4d6h", resolver); + assertEquals(new BigDecimal(15), result.getValue()); + + result = parser.evaluate("4d6b", resolver); + assertEquals(new BigDecimal(4), result.getValue()); + } + + @Test + private VariableResolver initVar(String name, Object value) throws ParserException { + VariableResolver result = new MapVariableResolver(); + result.setVariable(name, value); + return result; + } + + @Test + public void testEvaluate_FudgeRoll() throws ParserException { + RunData.setSeed(10423L); + ExpressionParser parser = new ExpressionParser(); + + Result result = parser.evaluate("dF"); + assertEquals(new BigDecimal(-1), result.getValue()); + + result = parser.evaluate("4df"); + assertEquals(new BigDecimal(0), result.getValue()); + + // Don't parse df in the middle of things + result = parser.evaluate("asdfg", initVar("asdfg", new BigDecimal(10))); + assertEquals(new BigDecimal(10), result.getValue()); + } + + @Test + public void testEvaluate_UbiquityRoll() throws ParserException { + RunData.setSeed(10423L); + ExpressionParser parser = new ExpressionParser(); + + Result result = parser.evaluate("dU"); + assertEquals(new BigDecimal(0), result.getValue()); + + result = parser.evaluate("10du"); + assertEquals(new BigDecimal(4), result.getValue()); + + // Don't parse a uf in the middle of other things + result = parser.evaluate("asufg", initVar("asufg", new BigDecimal(10))); + assertEquals(new BigDecimal(10), result.getValue()); + } + + @Test + public void testEvaluate_ColorHex() throws ParserException { + RunData.setSeed(10423L); + ExpressionParser parser = new ExpressionParser(); + + Result result = parser.evaluate("#FF0000"); + assertEquals(new BigDecimal(new BigInteger("FF0000", 16)), result.getValue()); + + result = parser.evaluate("#00FF0000"); + assertEquals(new BigDecimal(new BigInteger("FF0000", 16)), result.getValue()); + + result = parser.evaluate("#FF0"); + assertEquals(new BigDecimal(new BigInteger("FFFF00", 16)), result.getValue()); + } + + @Test + public void testEvaluate_If() throws ParserException { + ExpressionParser parser = new ExpressionParser(); + + evaluateExpression(parser, "if(10 > 2, 10, 2)", new BigDecimal(10)); + evaluateExpression(parser, "if(10 < 2, 10, 2)", new BigDecimal(2)); + evaluateStringExpression(parser, "if(10 < 2, 's1', 's2')", "s2"); + evaluateStringExpression(parser, "if(10 > 2, 's1', 's2')", "s1"); + } + + @Test + public void testEvaluate_Multiline() throws ParserException { + RunData.setSeed(10423L); + ExpressionParser parser = new ExpressionParser(); + + evaluateExpression(parser, "10 + \r\n d6 + \n 2", new BigDecimal(16)); + + String s = "10 + // Constant expression\n" + "2 + // Another bit\n" + "d20 // The roll\n"; + + evaluateExpression(parser, s, new BigDecimal(26)); + } + + @Test + public void testMultilineRegex() { + String str1 = "one two three"; + String str2 = "one two\nthree"; + + Pattern p1 = Pattern.compile("^one(.*)three$"); + Pattern p2 = Pattern.compile("one(.*)three", Pattern.MULTILINE); + + Matcher m1 = p1.matcher(str1); + Matcher m2 = p2.matcher(str2); + + System.out.println(m1.matches()); + System.out.println(m2.matches()); + } + + @Test + public void testNoTransformInStrings() throws ParserException { + ExpressionParser parser = new ExpressionParser(); + + evaluateStringExpression(parser, "'10' + 'd10'", "10d10"); + } + + @Test + public void testVariableRegexOverlaps() throws ParserException { + ExpressionParser parser = new ExpressionParser(); + Result result = parser.evaluate("food10 + 10", initVar("food10", new BigDecimal(10))); + assertEquals(new BigDecimal(20), result.getValue()); + } + + @Test + public void testNonDetailedExpression() throws ParserException { + ExpressionParser parser = new ExpressionParser(); + + int[] flattenings = new int[] {0}; + + MapVariableResolver resolver = new MapVariableResolver(); + resolver.setVariable( + "anumber", + new BigDecimal(3) { + @Override + public String toString() { + flattenings[0]++; + return super.toString(); + } + }); + + // one evaluation with detailed expression (the default) + Result result = parser.evaluate("anumber + 1", resolver, true); + assertEquals("anumber + 1", result.getExpression()); + assertEquals("3 + 1", result.getDetailExpression()); + assertEquals(new BigDecimal(4), result.getValue()); + + // one evaluation without detailed expression (this makes dicelib not go through a deterministic + // expression) + result = parser.evaluate("anumber + 1", resolver, false); + assertEquals("anumber + 1", result.getExpression()); + assertEquals("anumber + 1", result.getDetailExpression()); + assertEquals(new BigDecimal(4), result.getValue()); + + // expecting one flattening only for the former, not for the latter + // this makes json arrays not being flattened/coerced on every macro + // line *unless* the result is printed out + assertEquals(flattenings[0], 1); + } + + @Test + private void evaluateExpression(ExpressionParser p, String expression, BigDecimal answer) + throws ParserException { + Result result = p.evaluate(expression); + assertEquals( + 0, + answer.compareTo((BigDecimal) result.getValue()), + String.format( + "%s evaluated incorrectly expected <%s> but was <%s>", + expression, answer, result.getValue())); + } + + private void evaluateStringExpression(ExpressionParser p, String expression, String answer) + throws ParserException { + Result result = p.evaluate(expression); + + assertEquals(answer, result.getValue()); + } +} diff --git a/src/test/java/net/rptools/dicelib/expression/ExpressionParserWithMockRollsTest.java b/src/test/java/net/rptools/dicelib/expression/ExpressionParserWithMockRollsTest.java new file mode 100644 index 0000000000..96262a8d1e --- /dev/null +++ b/src/test/java/net/rptools/dicelib/expression/ExpressionParserWithMockRollsTest.java @@ -0,0 +1,437 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import net.rptools.parser.ParserException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * An alternative test suite that evaluates dice expressions against specific known roll sequences. + * Uses {@link RunDataMockForTesting}. + */ +public class ExpressionParserWithMockRollsTest { + /** + * Helper method to initialize the RunData with the given rolls + * + * @param rolls the sequence of rolls that should be used + */ + private void setUpMockRunData(int[] rolls) { + RunDataMockForTesting mockRD = new RunDataMockForTesting(new Result(""), rolls); + RunData.setCurrent(mockRD); + } + + /** Clear the RunData after each test, in case we leave a mock instance as Current. */ + @AfterEach + public void clearRunData() { + RunData.setCurrent(null); + } + + @Test + public void testEvaluate_ShadowRun4NonGlich25() throws ParserException { + int[] rolls = {2, 5}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr4"); + assertEquals("Hits: 1 Ones: 0 Results: 2 5 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun4GremlinGlich25() throws ParserException { + int[] rolls = {2, 5}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr4g1"); + assertEquals("Hits: 1 Ones: 0 *Gremlin Glitch* Results: 2 5 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun4CriticalGremlinGlich22() throws ParserException { + int[] rolls = {2, 2}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr4g1"); + assertEquals("Hits: 0 Ones: 0 *Critical Gremlin Glitch* Results: 2 2 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun4Glitch15() throws ParserException { + int[] rolls = {1, 5}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr4"); + assertEquals("Hits: 1 Ones: 1 *Glitch* Results: 1 5 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun4CriticalGlitch12() throws ParserException { + int[] rolls = {1, 2}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr4"); + assertEquals("Hits: 0 Ones: 1 *Critical Glitch* Results: 1 2 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun4CriticalGlitch11() throws ParserException { + int[] rolls = {1, 1}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr4"); + assertEquals("Hits: 0 Ones: 2 *Critical Glitch* Results: 1 1 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5NonGlich25() throws ParserException { + int[] rolls = {2, 5}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5"); + assertEquals("Hits: 1 Ones: 0 Results: 2 5 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5NonGlitch15() throws ParserException { + int[] rolls = {1, 5}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5"); + assertEquals("Hits: 1 Ones: 1 Results: 1 5 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5CriticalGlitch11() throws ParserException { + int[] rolls = {1, 1}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5"); + assertEquals("Hits: 0 Ones: 2 *Critical Glitch* Results: 1 1 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5CriticalGremlinGlitch12() throws ParserException { + int[] rolls = {1, 2}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5g1"); + assertEquals("Hits: 0 Ones: 1 *Critical Gremlin Glitch* Results: 1 2 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5GremlinGlitch15() throws ParserException { + int[] rolls = {1, 5}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5g1"); + assertEquals("Hits: 1 Ones: 1 *Gremlin Glitch* Results: 1 5 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5CriticalNonGremlinGlitch11() throws ParserException { + int[] rolls = {1, 1}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5g1"); + // This one would have glitched even without gremlins, thus, don't show the gremlin mark + assertEquals("Hits: 0 Ones: 2 *Critical Glitch* Results: 1 1 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5Glitch61Exploding1() throws ParserException { + int[] rolls = {6, 1, 1}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5e"); + assertEquals("Hits: 1 Ones: 2 *Glitch* Results: 6 1 1 ", result.getValue()); + } + + @Test + public void testEvaluate_ShadowRun5Glitch66Exploding6611() throws ParserException { + int[] rolls = {6, 6, 6, 6, 1, 1}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2sr5e"); + // this is still a glitch. + assertEquals("Hits: 4 Ones: 2 *Glitch* Results: 6 6 6 6 1 1 ", result.getValue()); + } + + @Test + public void testEvaluate_ExplodeWithMockRunData() throws ParserException { + int[] rolls = {3, 6, 6, 2}; // explode both sixes + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("2d6e"); + assertEquals(new BigDecimal(17), result.getValue()); + } + + @Test + public void testEvaluate_DropWithMockRunData() throws ParserException { + int[] rolls = {6, 2, 5, 4, 1, 6}; // drop 2 and 1 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("6d6d2"); + assertEquals(new BigDecimal(21), result.getValue()); + } + + @Test + public void testEvaluate_KeepWithMockRunData() throws ParserException { + int[] rolls = {6, 2, 5, 4, 1, 6}; // keep the sixes and the 5 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("6d6k3"); + assertEquals(new BigDecimal(17), result.getValue()); + } + + @Test + public void testEvaluate_KeepLowestWithMockRunData() throws ParserException { + int[] rolls = {6, 2, 5, 4, 1, 6}; // keep the 1 and 2 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("6d6kl2"); + assertEquals(new BigDecimal(3), result.getValue()); + } + + @Test + public void testEvaluate_RerollOnceAndKeepWithMockRunData() throws ParserException { + int[] rolls = {4, 2, 1}; // the 2 will be re-rolled, and the 1 will be kept + RunDataMockForTesting mockRD = new RunDataMockForTesting(new Result(""), rolls); + RunData.setCurrent(mockRD); + Result result = new ExpressionParser().evaluate("2d6rk3"); + assertEquals(new BigDecimal(5), result.getValue()); + } + + @Test + public void testEvaluate_RerollOnceAndChooseWithMockRunData() throws ParserException { + int[] rolls = {4, 2, 1}; // the 2 will be re-rolled, but still kept as it is higher than the 1 + RunDataMockForTesting mockRD = new RunDataMockForTesting(new Result(""), rolls); + RunData.setCurrent(mockRD); + Result result = new ExpressionParser().evaluate("2d6rc3"); + assertEquals(new BigDecimal(6), result.getValue()); + } + + @Test + public void testEvaluate_CountSuccessesWithMockRunData() throws ParserException { + int[] rolls = {6, 2, 5, 4, 1, 6}; // count the 5 and 6s + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("6d6s5"); + assertEquals(new BigDecimal(3), result.getValue()); + } + + @Test + public void testEvaluate_ExplodingSuccessesWithMockRunData() throws ParserException { + int[] rolls = {5, 4, 6, 1, 6, 6, 5}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("4d6es8"); + assertEquals("Dice: 5, 4, 7, 17, Successes: 1", result.getValue()); + } + + @Test + public void testEvaluate_OpenWithMockRunData() throws ParserException { + int[] rolls = {5, 6, 4, 6, 6, 2, 3}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("4d6o"); + assertEquals("Dice: 5, 10, 14, 3, Maximum: 14", result.getValue()); + } + + @Test + public void testEvaluate_DropHighWithMockRunData() throws ParserException { + int[] rolls = {6, 2, 5, 4, 1, 6}; // drop 6s and 5 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("6d6dh3"); + assertEquals(new BigDecimal(7), result.getValue()); + } + + @Test + public void testEvaluate_KeepLowWithMockRunData() throws ParserException { + int[] rolls = {6, 2, 5, 4, 1, 6}; // keep the 1 and 2 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("6d6kl2"); + assertEquals(new BigDecimal(3), result.getValue()); + } + + @Test + public void testEvaluate_FudgeRollWithMockRunData() throws ParserException { + int[] rolls = {1, 2, 2, 3}; // fudge dice are weird - they're shifted d3s to equal -1, 0, 1 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("4df"); + assertEquals(BigDecimal.ZERO, result.getValue()); + } + + @Test + public void testEvaluate_RollWithUpperWithMockRunData() throws ParserException { + int[] rolls = {6, 16, 18, 15, 14}; // 16, 18 bounded to 15 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("5d20u15"); + assertEquals(new BigDecimal(65), result.getValue()); + } + + @Test + public void testEvaluate_RollWithLowerWithMockRunData() throws ParserException { + int[] rolls = {13, 7, 6, 8, 2}; // 6, 2 bounded to 7 + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("5d20l7"); + assertEquals(new BigDecimal(42), result.getValue()); + } + + @Test + public void testEvaluate_RollAddWithUpperWithMockRunData() throws ParserException { + int[] rolls = {17, 4, 20, 15, 16}; // adds to 22, 9, 25, 20, 21. Then 22, 25 bounded to 21. + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("5d20a5u21"); + assertEquals(new BigDecimal(92), result.getValue()); + } + + @Test + public void testEvaluate_RollAddWithLowerWithMockRunData() throws ParserException { + int[] rolls = {13, 6, 7, 4, 8}; // adds to 16, 9, 10, 7, 11. Then 9, 7 bounded to 10. + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("5d20a3l10"); + assertEquals(new BigDecimal(57), result.getValue()); + } + + @Test + public void testEvaluate_RollSubWithUpperWithMockRunData() throws ParserException { + int[] rolls = {16, 7, 19, 15, 17}; // subs to 12, 3, 15, 11, 13. Then 15, 13 bounded to 12. + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("5d20s4u12"); + assertEquals(new BigDecimal(50), result.getValue()); + } + + @Test + public void testEvaluate_RollSubWithLowerWithMockRunData() throws ParserException { + int[] rolls = {13, 12, 18, 5, 11}; // subs to 6, 5, 11, -2, 4. Then -2, 4 bounded to 5. + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("5d20s7l5"); + assertEquals(new BigDecimal(32), result.getValue()); + } + + @Test + public void testEvaluate_arsMagicaStress() throws ParserException { + int[] rolls = {3, 7, 5, 2, 3, 2, 8, 9, 4, 7}; + setUpMockRunData(rolls); + for (int i = 0; i < rolls.length; i++) { + Result result = new ExpressionParser().evaluate("ans2"); + assertEquals(BigDecimal.valueOf(rolls[i]), result.getValue()); + } + + setUpMockRunData(rolls); + for (int i = 0; i < rolls.length; i++) { + Result result = new ExpressionParser().evaluate("as2"); + assertEquals(Integer.toString(rolls[i]), result.getValue()); + } + + int[] bonus = {1, 3, 0, -4, 5, -2, -1, 4}; + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + for (int i = 0; i < rolls.length; i++) { + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + Result result = new ExpressionParser().evaluate("ans2b#" + bonusStr); + assertEquals(BigDecimal.valueOf(Math.max(rolls[i] + bonus[x], 0)), result.getValue()); + } + + setUpMockRunData(rolls); + for (int i = 0; i < rolls.length; i++) { + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + Result result = new ExpressionParser().evaluate("as2b#" + bonusStr); + assertEquals(Integer.toString(Math.max(rolls[i] + bonus[x], 0)), result.getValue()); + } + } + } + + @Test + public void testEvaluate_arsMagicaStressNoBotch() throws ParserException { + int[] rolls = {10, 3, 2}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("ans2"); + assertEquals(BigDecimal.valueOf(0), result.getValue()); + + setUpMockRunData(rolls); + result = new ExpressionParser().evaluate("as2"); + assertEquals(Integer.toString(0), result.getValue()); + + int[] bonus = {1, 3, 0, -4, 5, -2, -1, 4}; + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("ans2b#" + bonusStr); + assertEquals(BigDecimal.valueOf(Math.max(0, bonus[x])), result.getValue()); + } + + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("as2b#" + bonusStr); + assertEquals(Integer.toString(Math.max(0, bonus[x])), result.getValue()); + } + + rolls = new int[] {10, 1, 1}; + setUpMockRunData(rolls); + result = new ExpressionParser().evaluate("ans2"); + assertEquals(BigDecimal.valueOf(0), result.getValue()); + + setUpMockRunData(rolls); + result = new ExpressionParser().evaluate("as2"); + assertEquals(Integer.toString(0), result.getValue()); + + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("ans2b#" + bonusStr); + assertEquals(BigDecimal.valueOf(Math.max(0, bonus[x])), result.getValue()); + } + + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("as2b#" + bonusStr); + assertEquals(Integer.toString(Math.max(0, bonus[x])), result.getValue()); + } + } + + @Test + public void testEvaluate_arsMagicaStressBotch() throws ParserException { + int[] rolls = {10, 10, 1}; + setUpMockRunData(rolls); + Result result = new ExpressionParser().evaluate("ans2"); + assertEquals(BigDecimal.valueOf(-1), result.getValue()); + + setUpMockRunData(rolls); + result = new ExpressionParser().evaluate("as2"); + assertEquals("0 (1 botch)", result.getValue()); + + rolls = new int[] {10, 10, 10, 10}; + setUpMockRunData(rolls); + result = new ExpressionParser().evaluate("ans2"); + assertEquals(BigDecimal.valueOf(-2), result.getValue()); + + setUpMockRunData(rolls); + result = new ExpressionParser().evaluate("as2"); + assertEquals("0 (2 botches)", result.getValue()); + + int[] bonus = {1, 3, 0, -4, 5, -2, -1, 4}; + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("ans2b#" + bonusStr); + assertEquals(BigDecimal.valueOf(-2), result.getValue()); + } + + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("as2b#" + bonusStr); + assertEquals("0 (2 botches)", result.getValue()); + } + + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("ans3b#" + bonusStr); + assertEquals(BigDecimal.valueOf(-3), result.getValue()); + } + + for (int x = 0; x < bonus.length; x++) { + setUpMockRunData(rolls); + String bonusStr = bonus[x] < 0 ? Integer.toString(bonus[x]) : "+" + bonus[x]; + result = new ExpressionParser().evaluate("as3b#" + bonusStr); + assertEquals("0 (3 botches)", result.getValue()); + } + } +} diff --git a/src/test/java/net/rptools/dicelib/expression/RunDataMockForTesting.java b/src/test/java/net/rptools/dicelib/expression/RunDataMockForTesting.java new file mode 100644 index 0000000000..4f0de17177 --- /dev/null +++ b/src/test/java/net/rptools/dicelib/expression/RunDataMockForTesting.java @@ -0,0 +1,159 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Logger; + +/** + * A version of RunData that replaces the random integer generation with a queue of preconfigured + * "rolls". Useful for testing evaluation of dice expressions against a deliberately crafted + * sequence of rolled values. + */ +public class RunDataMockForTesting extends RunData { + private static final Logger log = Logger.getLogger(RunDataMockForTesting.class.getName()); + private Queue toRoll = new ConcurrentLinkedQueue<>(); + + /** + * Construct the RunData with desired sequence of values to return as "roll" results. + * + * @param result the Result object, required by {@link RunData} + * @param rolls the roll values to return, in order + */ + public RunDataMockForTesting(Result result, int[] rolls) { + super(result); + for (int i : rolls) toRoll.add(i); + } + + /** + * Private constructor to create a new instance with an existing pre-configured queue. + * + * @param result the Result object, required by {@link RunData} + * @param rolls the pre-configured queue of rolls + * @param parent the parent RunData instance, to whom all rolls will be reported + */ + private RunDataMockForTesting(Result result, Queue rolls, RunData parent) { + super(result, parent); + toRoll = rolls; + } + + /** + * Create a child RunData instance, sharing the queue of pre-configured rolls. This allows child + * execution contexts (such as UDFs) to continue operating from the same pre-configured queue of + * rolls. + * + * @param childResult the Result object for the new RunData + * @return the child RunData + */ + @Override + public RunData createChildRunData(Result childResult) { + return new RunDataMockForTesting(childResult, toRoll, this); + } + + /** + * Gets the next value from the pre-configured queue, or throws an exception if the queue is + * empty. + * + * @return the next value, if any + * @throws ArrayIndexOutOfBoundsException if the queue is empty + */ + private int getNextInt() { + Integer next = toRoll.poll(); + if (next == null) + throw new ArrayIndexOutOfBoundsException( + "Requested more rolls than were provided to the RunDataMock"); + log.fine("Providing next pre-configured roll: " + next); + recordRolled(next); + return next; + } + + /** + * Gets the next value from the pre-configured queue. If that value would be greater than + * maxValue, an exception is thrown instead. + * + * @param maxValue the upper bound + * @return an integer less than or equal to maxValue + * @throws IllegalArgumentException if maxValue is too low for the next pre-configured roll + */ + @Override + public int randomInt(int maxValue) { + int next = getNextInt(); + if (next > maxValue) + throw new IllegalArgumentException( + "The given maxValue is too low for the next configured roll: " + next); + return next; + } + + /** + * Gets the next N values from the pre-configured queue. If any of those values would be greater + * than maxValue, an exception is thrown instead. + * + * @param num the desired number of rolls (N) + * @param maxValue the upper bound + * @return integers less than or equal to maxValue + * @throws IllegalArgumentException if maxValue is too low for the next N pre-configured rolls + */ + @Override + public int[] randomInts(int num, int maxValue) { + int[] ret = new int[num]; + for (int i = 0; i < num; i++) { + ret[i] = randomInt(maxValue); + } + return ret; + } + + /** + * Gets the next value from the pre-configured queue. If that value would be less than minValue or + * greater than maxValue, an exception is thrown instead. + * + * @param minValue the lower bound + * @param maxValue the upper bound + * @return an integer less than or equal to maxValue + * @throws IllegalArgumentException if minValue is too high or maxValue is too low for the next + * pre-configured roll + */ + @Override + public int randomInt(int minValue, int maxValue) { + int next = getNextInt(); + if (next < minValue) + throw new IllegalArgumentException( + "The given minValue is too high for the next configured roll: " + next); + if (next > maxValue) + throw new IllegalArgumentException( + "The given maxValue is too low for the next configured roll: " + next); + return next; + } + + /** + * Gets the next N values from the pre-configured queue. If any of those values would be less than + * minValue or greater than maxValue, an exception is thrown instead. + * + * @param num the desired number of rolls (N) + * @param minValue the lower bound + * @param maxValue the upper bound + * @return integers greater than or equal to minValue and less than or equal to maxValue + * @throws IllegalArgumentException if minValue is too high or maxValue is too low for the next N + * pre-configured rolls + */ + @Override + public int[] randomInts(int num, int minValue, int maxValue) { + int[] ret = new int[num]; + for (int i = 0; i < num; i++) { + ret[i] = randomInt(minValue, maxValue); + } + return ret; + } +} diff --git a/src/test/java/net/rptools/dicelib/expression/RunDataTest.java b/src/test/java/net/rptools/dicelib/expression/RunDataTest.java new file mode 100755 index 0000000000..81893fd615 --- /dev/null +++ b/src/test/java/net/rptools/dicelib/expression/RunDataTest.java @@ -0,0 +1,59 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class RunDataTest { + + @Test + public void testRandomIntInt() { + RunData runData = new RunData(null); + + for (int i = 0; i < 10000; i++) { + int value = runData.randomInt(10); + assertTrue(1 <= value && value <= 10); + } + } + + @Test + public void testRandomIntIntInt() { + RunData runData = new RunData(null); + + for (int i = 0; i < 10000; i++) { + int value = runData.randomInt(10, 20); + assertTrue(10 <= value && value <= 20, String.format("Value outside range: %s", value)); + } + } + + @Test + public void testParentChild() { + List allRolls = new ArrayList<>(); + RunData parent = new RunData(null); + RunData child = parent.createChildRunData(null); + + allRolls.add(parent.randomInt(20)); + for (int i = 0; i < 4; i++) { + allRolls.add(child.randomInt(20)); + } + + assertEquals(allRolls, parent.getRolled()); + } +} diff --git a/src/test/java/net/rptools/dicelib/expression/function/DiceHelperTest.java b/src/test/java/net/rptools/dicelib/expression/function/DiceHelperTest.java new file mode 100755 index 0000000000..ae30dc6e5f --- /dev/null +++ b/src/test/java/net/rptools/dicelib/expression/function/DiceHelperTest.java @@ -0,0 +1,76 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import net.rptools.dicelib.expression.RunData; +import net.rptools.parser.function.EvaluationException; +import org.junit.jupiter.api.Test; + +public class DiceHelperTest { + @Test + public void testRollDice() throws Exception { + RunData.setCurrent(new RunData(null)); + RunData.setSeed(102312L); + + assertEquals(42, DiceHelper.rollDice(10, 6)); + } + + @Test + public void testKeepDice() throws Exception { + RunData.setCurrent(new RunData(null)); + RunData.setSeed(102312L); + + assertEquals(28, DiceHelper.keepDice(10, 6, 5)); + } + + @Test + public void testDropDice() throws Exception { + RunData.setCurrent(new RunData(null)); + RunData.setSeed(102312L); + + assertEquals(28, DiceHelper.dropDice(10, 6, 5)); + } + + @Test + public void testRerollDice() throws Exception { + RunData.setCurrent(new RunData(null)); + RunData.setSeed(102312L); + + assertEquals(50, DiceHelper.rerollDice(10, 6, 2)); + } + + @Test + public void testExplodeDice() throws Exception { + RunData.setCurrent(new RunData(null)); + RunData.setSeed(102312L); + + assertEquals(23, DiceHelper.explodeDice(4, 6)); + } + + @Test + public void testExplodeDice_Exception() throws Exception { + try { + RunData.setCurrent(new RunData(null)); + RunData.setSeed(102312L); + + assertEquals(23, DiceHelper.explodeDice(4, 1)); + fail("Expected EvaluationException"); + } catch (EvaluationException e) { + } + } +} diff --git a/src/test/java/net/rptools/dicelib/expression/function/DropRollTest.java b/src/test/java/net/rptools/dicelib/expression/function/DropRollTest.java new file mode 100755 index 0000000000..88fd6faa30 --- /dev/null +++ b/src/test/java/net/rptools/dicelib/expression/function/DropRollTest.java @@ -0,0 +1,48 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import net.rptools.dicelib.expression.RunData; +import net.rptools.parser.Expression; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.function.EvaluationException; +import net.rptools.parser.function.ParameterException; +import org.junit.jupiter.api.Test; + +public class DropRollTest { + @Test + public void testEvaluateRoll() throws ParserException, EvaluationException, ParameterException { + + Parser p = new Parser(); + p.addFunction(new DropRoll()); + + try { + RunData.setCurrent(new RunData(null)); + RunData.setSeed(10423L); + + Expression xp = p.parseExpression("drop(4,6,1)"); + + long result = ((BigDecimal) xp.evaluate()).longValueExact(); + + assertEquals(12L, result); + } finally { + RunData.setCurrent(null); + } + } +} diff --git a/src/test/java/net/rptools/dicelib/expression/function/RollTest.java b/src/test/java/net/rptools/dicelib/expression/function/RollTest.java new file mode 100755 index 0000000000..35fe409e5c --- /dev/null +++ b/src/test/java/net/rptools/dicelib/expression/function/RollTest.java @@ -0,0 +1,66 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.dicelib.expression.function; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import net.rptools.dicelib.expression.RunData; +import net.rptools.parser.Expression; +import net.rptools.parser.MapVariableResolver; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.function.EvaluationException; +import net.rptools.parser.function.ParameterException; +import org.junit.jupiter.api.Test; + +public class RollTest { + @Test + public void testEvaluateRoll() throws ParserException, EvaluationException, ParameterException { + Parser p = new Parser(); + p.addFunction(new Roll()); + + try { + RunData.setCurrent(new RunData(null)); + + Expression xp = p.parseExpression("roll(6)"); + + for (int i = 0; i < 1000; i++) { + long result = ((BigDecimal) xp.evaluate()).longValueExact(); + + assertTrue(result >= 1 && result <= 6); + } + } finally { + RunData.setCurrent(null); + } + } + + public void testIsNonDeterministic() + throws ParserException, EvaluationException, ParameterException { + Parser p = new Parser(); + p.addFunction(new Roll()); + + try { + RunData.setCurrent(new RunData(null)); + + Expression xp = p.parseExpression("roll(10, 6) + 10"); + Expression dxp = xp.getDeterministicExpression(new MapVariableResolver()); + + assertTrue(dxp.format().matches("\\d+ \\+ 10")); + } finally { + RunData.setCurrent(null); + } + } +} diff --git a/src/test/java/net/rptools/maptool/client/MapToolLineParserTest.java b/src/test/java/net/rptools/maptool/client/MapToolLineParserTest.java index 0b263376b5..b8267b2871 100644 --- a/src/test/java/net/rptools/maptool/client/MapToolLineParserTest.java +++ b/src/test/java/net/rptools/maptool/client/MapToolLineParserTest.java @@ -19,7 +19,7 @@ import java.math.BigDecimal; import java.util.Collections; -import net.rptools.common.expression.Result; +import net.rptools.dicelib.expression.Result; import net.rptools.maptool.model.MacroButtonProperties; import net.rptools.maptool.model.Token; import net.rptools.parser.ParserException; diff --git a/src/test/java/net/rptools/maptool/client/functions/GetRolledTest.java b/src/test/java/net/rptools/maptool/client/functions/GetRolledTest.java index ed7240bb99..360af3b7c2 100644 --- a/src/test/java/net/rptools/maptool/client/functions/GetRolledTest.java +++ b/src/test/java/net/rptools/maptool/client/functions/GetRolledTest.java @@ -20,7 +20,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.util.*; -import net.rptools.common.expression.Result; +import net.rptools.dicelib.expression.Result; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.MapToolLineParser; import net.rptools.maptool.client.MapToolMacroContext;