Skip to content

Instantly share code, notes, and snippets.

@hamishmorgan
Created October 6, 2012 13:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hamishmorgan/3844967 to your computer and use it in GitHub Desktop.
Save hamishmorgan/3844967 to your computer and use it in GitHub Desktop.
Dice Roller
import java.util.Random;
import static java.text.MessageFormat.format;
/**
* http://www.reddit.com/r/dailyprogrammer/comments/10pf0j/9302012_challenge_102_easy_dice_roller/
*/
public class Main {
public static void main(String[] args) throws DiceParserException {
// Run though a bunch of example dice, printing the string, expectation, and a bunch of roles.
String[] examples = {"d6", "4d8", "d20-8", "10d6-2", "d20+7", "d8", "1d8+0", "1d8", "1d20+4", "5d10+10",
"10d6", "10d6-2", "10d6+2", "d6+2", "4d45+26", "d34-15", "3d12-5", "d42+32", "3d6", "d3"};
for (String example : examples) {
DiceRoller roller = DiceRoller.valueOf(example);
System.out.println(format("\"{0}\" -> \"{1}\" (expected min={2}, max={3}, mean={4})",
example, roller, roller.min(), roller.max(), roller.mean()));
final int repeats = 50;
int sum = 0, max = Integer.MIN_VALUE, min = Integer.MAX_VALUE;
for (int i = 0; i < repeats; i++) {
int roll = roller.roll();
min = Math.min(min, roll);
max = Math.max(max, roll);
sum += roll;
System.out.print(roll + " ");
}
System.out.println();
System.out.println(format("estimate min={0}, max={1}, mean={2}",
min, max, ((double) sum / repeats)));
System.out.println();
assert min >= roller.min();
assert max <= roller.max();
}
// Check numDice optionality
assert DiceRoller.valueOf("d8").equals(DiceRoller.valueOf("1d8"));
// Check adjustment optionality
assert DiceRoller.valueOf("d8+0").equals(DiceRoller.valueOf("d8"));
}
/**
* A random number generator that produce values by drawing numDice uniformly distributed random integers from the
* range adjustment+1 to adjustment+numSides (inclusive).
*/
public static class DiceRoller {
private final Random random = new Random();
private final int numDice;
private final int numSides;
private final int adjustment;
public DiceRoller(int numDice, int numSides, int adjustment) {
this.numDice = numDice;
this.numSides = numSides;
this.adjustment = adjustment;
}
public int roll() {
int result = adjustment;
for (int i = 0; i < numDice; i++)
result += 1 + random.nextInt(numSides);
return result;
}
public int min() {
return numDice + adjustment;
}
public int max() {
return (numDice * numSides) + adjustment;
}
public double mean() {
return numDice * (1. + (numSides - 1) / 2.) + adjustment;
}
public String toString() {
final StringBuilder builder = new StringBuilder();
if (numDice != 1)
builder.append(numDice);
builder.append('d');
builder.append(numSides);
if (adjustment != 0) {
builder.append(adjustment < 0 ? '-' : '+');
builder.append(adjustment);
}
return builder.toString();
}
public static DiceRoller valueOf(String string) throws DiceParserException {
return new DiceParser(string).parse();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DiceRoller)) return false;
DiceRoller that = (DiceRoller) o;
return adjustment == that.adjustment && numDice == that.numDice && numSides == that.numSides;
}
@Override
public int hashCode() {
return 31 * (31 * numDice + numSides) + adjustment;
}
}
/**
* Parser that takes strings from the dice roll language, producing a DiceRoller instance. Generally it's easier to
* call DiceRoller.valueOf(String), rather than directly use this object.
*/
public static class DiceParser {
private CharSequence chars;
private int offset = 0;
public DiceParser(CharSequence chars) {
this.chars = chars;
offset = 0;
}
public DiceRoller parse() throws DiceParserException {
final int numDice = isDigitNext() ? parseInteger() : 1;
if (numDice < 1)
throw new DiceParserException(this, "Not enough dice.");
if (!isNextAnyOf('d', 'D'))
throw new DiceParserException(this, "Expecting 'D' character.");
next();
final int numSides = parseInteger();
final int adjustment;
if (isNextAnyOf('+', '-')) {
next();
adjustment = parseInteger();
} else {
adjustment = 0;
}
if (hasNext())
throw new DiceParserException(this, "Expecting end of character sequence.");
return new DiceRoller(numDice, numSides, adjustment);
}
public int parseInteger() throws DiceParserException {
if (!isDigitNext())
throw new DiceParserException(this, "Expecting digit.");
int value = 0;
do {
value = value * 10 + (next() - '0');
} while (isDigitNext());
return value;
}
public boolean isDigitNext() {
return hasNext() && Character.isDigit(chars.charAt(offset));
}
public boolean isNextAnyOf(char... characters) {
if (!hasNext())
return false;
for (char character : characters)
if (chars.charAt(offset) == character)
return true;
return false;
}
public char next() throws DiceParserException {
if (!hasNext())
throw new DiceParserException(this, "Unexpected end of character sequence.");
return chars.charAt(offset++);
}
public boolean hasNext() {
return offset < chars.length();
}
}
/**
* Exception thrown by DiceParser when something goes wrong.
*/
public static class DiceParserException extends Exception {
private final CharSequence characters;
private final int offset;
public DiceParserException(DiceParser parser) {
this.characters = parser.chars;
this.offset = parser.offset;
}
public DiceParserException(DiceParser parser, String message) {
super(message);
this.characters = parser.chars;
this.offset = parser.offset;
}
public DiceParserException(DiceParser parser, String message, Throwable throwable) {
super(message, throwable);
this.characters = parser.chars;
this.offset = parser.offset;
}
public DiceParserException(DiceParser parser, Throwable throwable) {
super(throwable);
this.characters = parser.chars;
this.offset = parser.offset;
}
@Override
public String getMessage() {
return format("Error parsing \"{0}\" at offset {1}: {2}",
characters, offset, super.getMessage());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment