/*
* RollingMethods.java
* Copyright 2001 (C) Mario Bonassin
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This library 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. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*
* Created on April 21, 2001, 2:15 PM
*
* $Id$
*/
package pcgen.core;
import java.util.Arrays;
import java.util.Stack;
import java.util.Vector;
import java.util.stream.IntStream;
import pcgen.base.util.RandomUtil;
import pcgen.util.Logging;
import org.nfunk.jep.JEP;
import org.nfunk.jep.ParseException;
import org.nfunk.jep.function.List;
import org.nfunk.jep.function.PostfixMathCommand;
/**
* {@code RollingMethods}.
*
* @author Mario Bonassin <zebuleon@users.sourceforge.net>
* @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
*/
public final class RollingMethods
{
private RollingMethods()
{
}
/**
* Roll <var>times</var> number of dice with <var>sides</var>
* shape.
*
* @param times int how many dice to roll?
* @param sides int what shape dice?
*
* @return int dice total
*/
public static int roll(final int times, final int sides)
{
return roll(times, sides, times, 0, 0);
}
/**
* One random number between 1 and <var>sides</var>, good, for
* example, for rolling percentage dice.
*
* @param sides int what shape die?
*
* @return int die roll
*/
public static int roll(final int sides)
{
return RandomUtil.getRandomInt(sides) + 1;
}
/**
* Roll <var>times</var> dice with <var>sides</var> shape,
* sort them, and return the sum of only those listed in
* <var>keep</var> (0-indexed).
*
* @param times int how many dice to roll?
* @param sides int what shape dice?
* @param keep int[] which dice to keep (0-indexed)?
*
* @return int dice total
*/
public static int roll(int times, final int sides, final int[] keep)
{
// return roll (times, sides, keep, 0, 0);
int[] ints = IntStream.generate(() -> RandomUtil.getRandomInt(sides))
.limit(times)
.sorted()
.toArray();
// keep the +1 at the end
return Arrays.stream(keep)
.reduce(keep.length, (a, aKeep) -> a + ints[aKeep]);
}
private static int getLeftIndex(final StringBuilder expression, int startIndex)
{
int startIndex1 = startIndex;
int parenth = 0;
char c;
do
{
startIndex1--;
if (startIndex1 >= 0)
{
c = expression.charAt(startIndex1);
}
else
{
break;
}
if (c == ')')
{
parenth++;
}
else if (c == '(')
{
parenth--;
}
} while ((parenth > 0) || (c == 'd') || (c == '*') || (c == '/') || (c == ' ') ||
Character.isDigit(c));
return startIndex1 + 1;
}
private static int getRightIndex(final StringBuilder expression, int startIndex)
{
int startIndex1 = startIndex;
int parenth = 0;
char c;
do
{
startIndex1++;
if (startIndex1 < expression.length())
{
c = expression.charAt(startIndex1);
}
else
{
break;
}
if (c == '(')
{
parenth++;
}
else if (c == ')')
{
parenth--;
}
} while ((parenth > 0) || (c == '*') || (c == '/') || (c == ' ') ||
Character.isDigit(c));
return startIndex1;
}
/**
* Takes many forms including "2d6-2" and returns the result
* Whitespace is ignored; case insensitive; Most simple math
* operations (including exponentiation) are supported
* Functions builtin include max, min, roll
* Add new functions to DiceExpressionFunctions
*
* @param method String formatted string representing dice roll
*
* @return int dice total
*/
public static int roll(final String method)
{
int r = 0;
if (method.length() <= 0)
{
return r;
}
final StringBuilder expression = new StringBuilder(method.replaceAll("d%",
"1d100"));
int index = expression.lastIndexOf("d");
while (index != -1)
{
expression.insert(getRightIndex(expression, index), ')');
expression.setCharAt(index, ',');
expression.insert(getLeftIndex(expression, index), "roll(");
index = expression.lastIndexOf("d", index + 4);
}
String exp = expression.toString();
exp = exp.replaceAll("\\[", "list(").replaceAll("\\]", ")");
final JEP jep = new JEP();
jep.addStandardFunctions();
jep.addFunction("roll", new Roll());
jep.addFunction("top", new Top());
jep.addFunction("reroll", new Reroll());
jep.addFunction("list", new List());
jep.parseExpression(exp);
if (!jep.hasError())
{
r = (int) jep.getValue();
}
else
{
Logging.errorPrint("Bad dice: " + expression + ":" +
jep.getErrorInfo());
}
return r;
}
/**
* Roll {{@code times}} 1d{{@code sides}}, reroll any result <= {{@code reroll}}.
* Add together the highest {{@code numToKeep}} dice then add {{@code modifier}}
* and return the result.
*
* @param times
* @param sides
* @param numToKeep
* @param reroll
* @param modifier
* @return the result of the die roll
*/
private static int roll(
final int times,
final int sides,
final int numToKeep,
final int reroll,
final int modifier)
{
return IntStream.generate(
() -> roll(sides - reroll) + reroll
)
.limit(times)
.sorted()
.limit(numToKeep)
.sum() + modifier;
}
/**
* @author RossLodge
*
* <p>This class forms the basis for the dJEP extensions to the JEP library.
* It evaluates a {@code ROLL} token, which is an operator that comes
* in precedence between multiplicative and additive operators.</p>
*
* <p>The class receives two parameters, the number of rolls and the number of
* faces per die, as in "3d6," or whatever. It initializes a random number
* generator, which must be a subclass of {@code edu.cornell.lassp.houle.RngPack.RandomElement}.
* A default randomizer class is provided that wraps the java.util.Random class.
* The class used may be changed via setRandomClassName.</p>
*
* <p>The class provides very minimal retrieval of individual rolls, via the
* {@code getRolls()} method.</p>
*/
private static final class Roll extends PostfixMathCommand
{
/**
*
* <p>The default (and only) constructor. Sets the number
* of parameters for the JEP package, and calls {@code setRandom()} to
* initialize the randomizer.</p>
*/
private Roll()
{
numberOfParameters = -1;
}
/**
* <p>The run command for the JEP framework. This receives the arguments
* to the operator as a stack, pops off the two it needs, does type checking,
* rolls the dice on the randomizer, and pushes the result back onto the stack.
* Logging is performed if it is turned on.</p>
*
* @see org.nfunk.jep.function.PostfixMathCommandI#run(java.util.Stack)
*/
@Override
public void run(final Stack stack) throws ParseException
{
// check the stack
checkStack(stack);
if (curNumberOfParameters < 2)
{
throw new ParseException("Too few parameters");
}
if (curNumberOfParameters > 4)
{
throw new ParseException("Too many parameters");
}
int numToKeep = 0;
int[] keep = null;
int reroll = 0;
while (curNumberOfParameters > 2)
{
final Object param = stack.pop();
if ((param instanceof Top.TopRolls) && (numToKeep == 0))
{
numToKeep = ((Top.TopRolls) param).getRolls();
}
else if ((param instanceof Reroll.Rerolls) && (reroll == 0))
{
reroll = ((Reroll.Rerolls) param).getRolls();
}
else if ((param instanceof Vector) && (curNumberOfParameters == 3))
{
if (numToKeep != 0)
{
throw new ParseException("Redundant Arugments");
}
if (reroll != 0)
{
throw new ParseException("Reroll not compatable with older syntax, use top(NUMBER) instead");
}
final Vector vec = (Vector) param;
keep = new int[vec.size()];
for (int x = 0; x < vec.size(); x++)
{
keep[x] = ((int) Math.round((Double) vec.get(x))) -
1;
}
}
else
{
throw new ParseException("Invalid parameter type");
}
curNumberOfParameters--;
}
// get the parameter from the stack
Object faces = stack.pop();
final Object numberOfRolls = stack.pop();
if (faces instanceof Vector)
{
final java.util.List vec = (java.util.List) faces;
faces = vec.get(RandomUtil.getRandomInt(vec.size()));
}
// check whether the argument is of the right type
if ((faces instanceof Double) &&
(numberOfRolls instanceof Double))
{
// calculate the result
//Integer.MAX_VALUE
final double dRolls = (Double) numberOfRolls;
final double dFaces = (Double) faces;
if ((dRolls > Integer.MAX_VALUE) || (dFaces >
Integer.MAX_VALUE))
{
throw new ParseException("Values greater than " +
Integer.MAX_VALUE +
" not allowed.");
}
int iRolls = (int) Math.round(((Double) numberOfRolls).doubleValue());
int iFaces = (int) Math.round(((Double) faces).doubleValue());
if (numToKeep == 0)
{
numToKeep = iRolls;
}
double result = 0;
if (keep == null)
{
result = roll(iRolls, iFaces, numToKeep, reroll, 0);
}
else
{
result = roll(iRolls, iFaces, keep);
}
// push the result on the inStack
stack.push(new Double(result));
}
else
{
throw new ParseException("Invalid parameter type");
}
}
}
private static final class Top extends PostfixMathCommand
{
private Top()
{
numberOfParameters = 1;
}
@Override
public void run(final Stack stack) throws ParseException
{
final Object param = stack.pop();
if (param instanceof Double)
{
final double dRolls = (Double) param;
if (dRolls > Integer.MAX_VALUE)
{
throw new ParseException("Values greater than " +
Integer.MAX_VALUE + " not allowed.");
}
final int iRolls = (int) Math.round(dRolls);
if (iRolls > 0)
{
stack.push(new TopRolls(iRolls));
}
else
{
throw new ParseException("Values less than 1 are not allowed");
}
}
else
{
throw new ParseException("Invalid parameter type");
}
}
static final class TopRolls
{
private final Integer rolls;
private TopRolls(final Integer rolls)
{
this.rolls = rolls;
}
public Integer getRolls()
{
return rolls;
}
}
}
private static final class Reroll extends PostfixMathCommand
{
private Reroll()
{
numberOfParameters = 1;
}
@Override
public void run(final Stack stack) throws ParseException
{
final Object param = stack.pop();
if (param instanceof Double)
{
final double dRolls = (Double) param;
if (dRolls > Integer.MAX_VALUE)
{
throw new ParseException("Values greater than " +
Integer.MAX_VALUE + " not allowed.");
}
final int iRolls = (int) Math.round(dRolls);
if (iRolls > 0)
{
stack.push(new Rerolls(iRolls));
}
else
{
throw new ParseException("Values less than 1 are not allowed");
}
}
else
{
throw new ParseException("Invalid parameter type");
}
}
static final class Rerolls
{
private final Integer rolls;
private Rerolls(final Integer rolls)
{
this.rolls = rolls;
}
public Integer getRolls()
{
return rolls;
}
}
}
}