.
+ */
+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;