diff --git a/pom.xml b/pom.xml
index f63bcea..8b9b4a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
dev.thearcticgiant
dice4j
- 0.1.1-alpha-SNAPSHOT
+ 0.2.0-alpha-SNAPSHOT
org.junit.jupiter
diff --git a/src/main/java/dev/thearcticgiant/dice4j/Bonus.java b/src/main/java/dev/thearcticgiant/dice4j/Bonus.java
new file mode 100644
index 0000000..bd5a100
--- /dev/null
+++ b/src/main/java/dev/thearcticgiant/dice4j/Bonus.java
@@ -0,0 +1,62 @@
+package dev.thearcticgiant.dice4j;
+
+/**
+ * A static numerical bonus to a roll.
+ * Once created, its value cannot be changed, and calls to roll
have no effect.
+ */
+public class Bonus implements Rollable{
+ public final int value;
+
+ public Bonus(int value){
+ this.value = value;
+ }
+
+ /**
+ * Return this instance of Bonus
, has no effect.
+ * @return This instance.
+ */
+ @Override
+ public Bonus roll(){
+ return this;
+ }
+
+ @Override
+ public int read(){
+ return value;
+ }
+
+ /**
+ * Has no effect.
+ */
+ @Override
+ public void lock(){}
+
+ /**
+ * Always returns true.
+ * @return True
+ */
+ @Override
+ public boolean isLocked(){
+ return true;
+ }
+
+ @Override
+ public String getName(){
+ return Integer.toString(value);
+ }
+
+ @Override
+ public String getMarkdownName(){
+ return getName();
+ }
+
+ @Override
+ public String toString(){
+ return Integer.toString(value);
+ }
+
+ @Override
+ public String toMarkdownString(){
+ return toString();
+ }
+}
diff --git a/src/main/java/dev/thearcticgiant/dice4j/Dice.java b/src/main/java/dev/thearcticgiant/dice4j/Dice.java
index 6a762c9..68aaf26 100644
--- a/src/main/java/dev/thearcticgiant/dice4j/Dice.java
+++ b/src/main/java/dev/thearcticgiant/dice4j/Dice.java
@@ -1,33 +1,136 @@
package dev.thearcticgiant.dice4j;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class Dice{
- private static Matcher matcher = Pattern.compile("(?:(?[+-]?\\d++)?d(?[+-]?\\d++))?(?[+-]?\\d++)?").matcher("");
- public static Roll roll(String exp){
- if(!matcher.reset(exp).matches()) throw new RuntimeException("invalid dice expression");
- String countStr = matcher.group("count"),
- sidesStr = matcher.group("sides"),
- bonusStr = matcher.group("bonus");
-
- if(countStr == null){
- if(sidesStr == null) countStr = sidesStr = "0";
- else countStr = "1";
+import java.util.Iterator;
+import java.util.Random;
+import java.util.List;
+
+public class Dice implements Rollable{
+ public final List dice;
+ public final int count, sides;
+ private boolean locked = false;
+
+ /**
+ * Construct a roll of the format xdy.
+ * Random rolls are determined by the provided Random.
+ * @param count A positive integer indicating number of dice rolled.
+ * @param sides A positive integer indicating the number of sides on each die.
+ * @param random The Random object for making rolls.
+ * @throws RuntimeException if count or sides is non-positive.
+ */
+ public Dice(int count, int sides, Random random){
+ if(count <= 0) throw new RuntimeException("count must be positive");
+ if(sides <= 0) throw new RuntimeException("sides must be positive");
+ this.count = count;
+ this.sides = sides;
+
+ final Die[] dice = new Die[count];
+ for(int i=0; igetName with the "d" bolded.
+ * @return The markdown formatted name of this Dice.
+ */
+ @Override
+ public String getMarkdownName(){
+ return String.format("%d**d**%d", count, sides);
+ }
+
+ /**
+ * A string representation of the roll.
+ * The equation is always equal to total().
+ * In the format "[a, b, c, ...] = t".
+ * @return A string representing the rolled dice.
+ */
+ @Override
+ public String toString(){
+ StringBuilder builder = new StringBuilder();
+ builder.append('[');
+ for(Iterator i = dice.iterator(); i.hasNext();){
+ builder.append(i.next().read());
+ if(i.hasNext()) builder.append(", ");
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+
+ /**
+ * Equivalent to toString, with any max rolls bolded.
+ * @return A markdown formatted string representing the rolled dice.
+ */
+ @Override
+ public String toMarkdownString(){
+ StringBuilder builder = new StringBuilder();
+ builder.append('[');
+ for(Iterator i = dice.iterator(); i.hasNext();){
+ int die = i.next().read();
+
+ if(die == sides)
+ builder.append("**")
+ .append(die)
+ .append("**");
+ else builder.append(die);
+
+ if(i.hasNext()) builder.append(", ");
+ }
+ builder.append(']');
+ return builder.toString();
}
}
diff --git a/src/main/java/dev/thearcticgiant/dice4j/Die.java b/src/main/java/dev/thearcticgiant/dice4j/Die.java
index 4f17b91..843d116 100644
--- a/src/main/java/dev/thearcticgiant/dice4j/Die.java
+++ b/src/main/java/dev/thearcticgiant/dice4j/Die.java
@@ -4,60 +4,83 @@
public class Die implements Rollable{
public final int sides;
+ private boolean locked;
private int roll;
private final Random random;
+ /**
+ * Construct a new die, using a provided Random object.
+ * @param sides The number of sides.
+ * @param random The Random used to generate rolls.
+ */
+ public Die(int sides, Random random){
+ this.sides = sides;
+ this.random = random;
+
+ roll();
+ }
+
/**
* Construct and roll a new die with a specific random seed.
* @param sides The number of sides.
* @param seed The seed used to create the internal Random object.
- * @throws RuntimeException if sides is non-positive.
*/
public Die(int sides, long seed){
this(sides, new Random(seed));
}
/**
- * Construct and roll a new die.
+ * Construct a new die.
* @param sides The number of sides.
- * @throws RuntimeException if sides is non-positive.
*/
-
public Die(int sides){
this(sides, new Random());
}
- private Die(int sides, Random random){
- this.sides = sides;
- this.random = random;
-
- if(sides <= 0) throw new RuntimeException("sides must be positive");
-
- roll();
- }
-
- /**
- * Re-roll this die;
- * @return The new result;
- */
- public int roll(){
- return roll = random.nextInt(sides)+1;
+ @Override
+ public Die roll(){
+ if(!locked) roll = random.nextInt(sides)+1;
+ return this;
}
+ @Override
public int read(){
return roll;
}
- public int fudge(int roll){
- return this.roll = roll;
+ @Override
+ public final void lock(){
+ locked = true;
+ }
+
+ @Override
+ public final boolean isLocked(){
+ return locked;
}
+ @Override
public String getName(){
- return String.format("d%d", sides);
+ return String.format("1d%d", sides);
}
+ @Override
+ public String getMarkdownName(){
+ return String.format("1**d**%d", sides);
+ }
+
+ @Override
public String toString(){
- return String.format("%s (%d)", getName(), roll);
+ return Integer.toString(roll);
+ }
+
+ @Override
+ public String toMarkdownString(){
+ StringBuilder builder = new StringBuilder();
+
+ if(roll >= sides) builder.append("**").append(roll).append("**");
+ else builder.append(roll);
+
+ return builder.toString();
}
}
diff --git a/src/main/java/dev/thearcticgiant/dice4j/Roll.java b/src/main/java/dev/thearcticgiant/dice4j/Roll.java
index 714c8d5..dbfc64c 100644
--- a/src/main/java/dev/thearcticgiant/dice4j/Roll.java
+++ b/src/main/java/dev/thearcticgiant/dice4j/Roll.java
@@ -1,105 +1,121 @@
package dev.thearcticgiant.dice4j;
import java.util.Iterator;
-import java.util.Set;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class Roll implements Rollable{
- public final Set dice;
- public final int count, sides, bonus;
-
- /**
- * Construct a roll of the format xdy+z.
- * @param count A non-negative integer indicating number of dice rolled.
- * @param sides A positive integer indicating the number of sides on each die.
- * @param bonus The static bonus applied to the roll.
- * @throws RuntimeException if count is negative, or sides is non-positive.
- */
+ private static Matcher matcher = Pattern.compile("(?:(?[+-]?\\d++)?d(?[+-]?\\d++))?(?[+-]?\\d++)?").matcher("");
+
+ public final List rolls;
+ private boolean locked = false;
+
+ public Roll(Rollable... rolls){
+ this.rolls = List.of(rolls);
+ }
public Roll(int count, int sides, int bonus){
- if(count < 0) throw new RuntimeException("count cannot be negative");
- if(count > 0 && sides <= 0) throw new RuntimeException("sides must be positive");
- this.count = count;
- this.sides = sides;
- this.bonus = bonus;
-
- final Die[] dice = new Die[count];
- for(int i=0; i 0){
- hasDice = true;
- builder.append(count).append('d').append(sides);
+ for(Iterator i=rolls.iterator();i.hasNext();){
+ builder.append(i.next().getName());
+ if(i.hasNext()) builder.append('+');
}
- if(bonus<0) builder.append(bonus);
- else if(bonus>0){
- if(hasDice) builder.append('+');
- builder.append(bonus);
- } else if(!hasDice) builder.append(0);
+
+ return builder.toString();
+ }
+
+ @Override
+ public String getMarkdownName(){
+ StringBuilder builder = new StringBuilder();
+ for(Iterator i=rolls.iterator();i.hasNext();){
+ builder.append(i.next().getMarkdownName());
+ if(i.hasNext()) builder.append('+');
+ }
+
return builder.toString();
}
- /**
- * A string representation of the roll.
- * The equation is always equal to total().
- * In the format "[a, b, c, ...]+k = t".
- * @return A string representing the rolled dice.
- */
+ @Override
public String toString(){
- StringBuilder string = new StringBuilder();
- string.append('[');
- for(Iterator i=dice.iterator();i.hasNext();){
- string.append(i.next().read());
- if(i.hasNext()) string.append(", ");
+ StringBuilder builder = new StringBuilder();
+ for(Iterator i=rolls.iterator(); i.hasNext();){
+ builder.append(i.next().toString());
+ if(i.hasNext()) builder.append('+');
}
- string.append(']');
- if(bonus > 0) string.append('+').append(bonus);
- else if(bonus < 0) string.append('-').append(-bonus);
- string.append(" = ")
- .append(read());
- return string.toString();
+
+ return builder.toString();
+ }
+
+ @Override
+ public String toMarkdownString(){
+ StringBuilder builder = new StringBuilder();
+ for(Iterator i=rolls.iterator(); i.hasNext();){
+ builder.append(i.next().toMarkdownString());
+ if(i.hasNext()) builder.append('+');
+ }
+
+ return builder.toString();
}
}
diff --git a/src/main/java/dev/thearcticgiant/dice4j/Rollable.java b/src/main/java/dev/thearcticgiant/dice4j/Rollable.java
index 3a4c841..bb6bb61 100644
--- a/src/main/java/dev/thearcticgiant/dice4j/Rollable.java
+++ b/src/main/java/dev/thearcticgiant/dice4j/Rollable.java
@@ -3,10 +3,10 @@
public interface Rollable{
/**
- * Generate a new random result.
- * @return The newly rolled result;
+ * Generate a new result and return self.
+ * @return This Rollable;
*/
- int roll();
+ Rollable roll();
/**
* Return the most recently rolled result;
@@ -16,9 +16,46 @@ public interface Rollable{
int read();
/**
- * A human readable string representation of this Rollable's state, irrespective of the result of a given roll.
+ * Permanently locks this Rollable such that all future calls to read and toString are guaranteed to return the same result.
+ * Calls to roll, and any other attempts to modify this Rollable's total are unsuccessful.
+ */
+ void lock();
+
+ /**
+ * Determine if this Rollable is locked.
+ * @return True if lock has previously been called on this Rollable.
+ */
+ boolean isLocked();
+
+ /**
+ * A human readable string representation of this Rollable's state, irrespective of the current state.
* The result should provide all the necessary information to construct a similar Rollable.
* @return The name of this particular Rollable.
*/
String getName();
+
+ /**
+ * As getName, but with markdown formatting.
+ * Should render to the same text as getName with only aesthetic differences,
+ * eg. bolding the 'd' in 1d20.
+ * Returning the exact same as getName() is acceptable.
+ * @return The name of this particular Rollable.
+ */
+ String getMarkdownName();
+
+ /**
+ * Return a full, human readable representation of this Rollable's most recent roll.
+ * The total rolled should be unambiguously calculable from the returned String.
+ * @return The amounts rolled and any modifiers.
+ */
+ String toString();
+
+ /**
+ * As toString(), but with markdown formatting.
+ * The returned markdown should render to the same text as toString with only aesthetic changes,
+ * eg. bold-ing any maximum rolls.
+ * Returning the exact same as toString is also acceptable.
+ * @return The amounts rolled and any modifiers.
+ */
+ String toMarkdownString();
}
diff --git a/src/test/java/dev/thearcticgiant/dice4j/DiceTest.java b/src/test/java/dev/thearcticgiant/dice4j/DiceTest.java
index 0cb47a2..335cc2b 100644
--- a/src/test/java/dev/thearcticgiant/dice4j/DiceTest.java
+++ b/src/test/java/dev/thearcticgiant/dice4j/DiceTest.java
@@ -2,70 +2,23 @@
import org.junit.jupiter.api.Test;
-import java.util.Arrays;
-import java.util.Iterator;
-
import static org.junit.jupiter.api.Assertions.*;
class DiceTest{
@Test
- void roll(){
- String[] testExpressions = new String[]{
- "",
- "0",
- "+0",
- "-0",
- "5",
- "+5",
- "-5",
- "0d20",
- "0d20+0",
- "0d20-0",
- "0d20+5",
- "0d20-5",
- "d20",
- "d20+0",
- "d20-0",
- "d20+5",
- "d20-5",
- "3d6",
- "3d6+0",
- "3d6-0",
- "3d6+5",
- "3d6-5"
- },
- expectedNames = new String[]{
- "0",
- "0",
- "0",
- "0",
- "5",
- "5",
- "-5",
- "0",
- "0",
- "0",
- "5",
- "-5",
- "1d20",
- "1d20",
- "1d20",
- "1d20+5",
- "1d20-5",
- "3d6",
- "3d6",
- "3d6",
- "3d6+5",
- "3d6-5"
- };
+ void constructor(){
+ assertThrows(RuntimeException.class, ()->new Dice(1, 0, 0));
+ assertThrows(RuntimeException.class, ()->new Dice(-1, 1));
+ }
- Iterator
- testExpressionsIterator = Arrays.stream(testExpressions).iterator(),
- expectedNamesIterator = Arrays.stream(expectedNames).iterator();
+ @Test
+ void getName(){
+ Dice
+ oneDie = new Dice(1, 20),
+ noBonus = new Dice(3, 6);
- while(testExpressionsIterator.hasNext()){
- assertEquals(expectedNamesIterator.next(), Dice.roll(testExpressionsIterator.next()).getName());
- }
+ assertEquals("1d20", oneDie.getName());
+ assertEquals("3d6", noBonus.getName());
}
}
\ No newline at end of file
diff --git a/src/test/java/dev/thearcticgiant/dice4j/DieTest.java b/src/test/java/dev/thearcticgiant/dice4j/DieTest.java
index ab15ee7..2c29a8e 100644
--- a/src/test/java/dev/thearcticgiant/dice4j/DieTest.java
+++ b/src/test/java/dev/thearcticgiant/dice4j/DieTest.java
@@ -8,47 +8,31 @@
class DieTest{
- @Test
- void constructor(){
- assertThrows(RuntimeException.class, ()->new Die(0, 0));
- assertThrows(RuntimeException.class, ()->new Die(-1, 1));
- }
-
- @Test
- void fudge(){
- Die die = new Die(6);
- for(int i=0; i<6; i++){
- assertEquals(i, die.fudge(i));
- assertEquals(i, die.read());
- }
- }
-
@Test
void roll(){
Die die = new Die(Integer.MAX_VALUE);
for(int i=0; i<2<<5; i++){
- assertEquals(die.roll(), die.read());
- assertNotEquals(die.read(), die.roll());
+ assertEquals(die.roll().read(), die.read());
+ assertNotEquals(die.read(), die.roll().read());
}
}
@Test
void read(){
Die d6 = new Die(6);
- assertEquals(d6.roll(), d6.read());
+ assertEquals(d6.roll().read(), d6.read());
}
@Test
void getName(){
Die d6 = new Die(6);
- assertEquals("d6", d6.getName());
+ assertEquals("1d6", d6.getName());
}
@Test
void testToString(){
Die d6 = new Die(6);
- d6.fudge(6);
- assertEquals("d6 (6)", d6.toString());
+ assertEquals(Integer.toString(d6.read()), d6.toString());
}
@Test
@@ -60,7 +44,7 @@ void testSeededDice(){
d1 = new Die(Integer.MAX_VALUE, seed);
d2 = new Die(Integer.MAX_VALUE, seed);
for(int i1=0; i1<2<<5; i1++){
- assertEquals(d1.roll(), d2.roll());
+ assertEquals(d1.roll().read(), d2.roll().read());
}
}
}
diff --git a/src/test/java/dev/thearcticgiant/dice4j/RollTest.java b/src/test/java/dev/thearcticgiant/dice4j/RollTest.java
index 1f93f9c..a4a803c 100644
--- a/src/test/java/dev/thearcticgiant/dice4j/RollTest.java
+++ b/src/test/java/dev/thearcticgiant/dice4j/RollTest.java
@@ -2,75 +2,80 @@
import org.junit.jupiter.api.Test;
+import java.util.Arrays;
+import java.util.Iterator;
+
import static org.junit.jupiter.api.Assertions.*;
class RollTest{
@Test
- void constructor(){
- assertThrows(RuntimeException.class, ()->new Roll(1, 0, 0));
- assertThrows(RuntimeException.class, ()->new Roll(-1, 1));
- }
-
- @Test
- void total(){
- Roll roll, negativeBonus, noBonus, onlyBonus, empty;
- roll = new Roll(3, 6, 5);
- negativeBonus = new Roll(3, 6, -7);
- noBonus = new Roll(3, 6);
- onlyBonus = new Roll(0, 0, 1);
- empty = new Roll();
-
- //fudge all the rolls;
- for(Die die : roll.dice) die.fudge(6);
- for(Die die : negativeBonus.dice) die.fudge(6);
- for(Die die : noBonus.dice) die.fudge(6);
-
- assertEquals(23, roll.read());
- assertEquals(11, negativeBonus.read());
- assertEquals(18, noBonus.read());
- assertEquals(1, onlyBonus.read());
- assertEquals(0, empty.read());
- }
-
- @Test
- void getName(){
- Roll roll, oneDie, negativeBonus, noBonus, onlyBonus, onlyNegativeBonus, empty;
- roll = new Roll(3, 6, 5);
- oneDie = new Roll(1, 20);
- negativeBonus = new Roll(3, 6, -7);
- noBonus = new Roll(3, 6);
- onlyBonus = new Roll(0, 0, 1);
- onlyNegativeBonus = new Roll(0, 0, -1);
- empty = new Roll();
+ void lock(){
+ Die die = new Die(Integer.MAX_VALUE);
+ Roll roll = new Roll(die);
- assertEquals("3d6+5", roll.getName());
- assertEquals("1d20", oneDie.getName());
- assertEquals("3d6-7", negativeBonus.getName());
- assertEquals("3d6", noBonus.getName());
- assertEquals("1", onlyBonus.getName());
- assertEquals("-1", onlyNegativeBonus.getName());
- assertEquals("0", empty.getName());
+ roll.lock();
+ assertEquals(die.read(), roll.read());
+ assertTrue(die.isLocked());
}
@Test
- void testToString(){
- Roll roll, negativeBonus, noBonus, onlyBonus, empty;
- roll = new Roll(3, 6, 5);
- negativeBonus = new Roll(3, 6, -7);
- noBonus = new Roll(3, 6);
- onlyBonus = new Roll(0, 0, 1);
- empty = new Roll();
+ void roll(){
+ String[] testExpressions = new String[]{
+ "",
+ "0",
+ "+0",
+ "-0",
+ "5",
+ "+5",
+ "-5",
+ "0d20",
+ "0d20+0",
+ "0d20-0",
+ "0d20+5",
+ "0d20-5",
+ "d20",
+ "d20+0",
+ "d20-0",
+ "d20+5",
+ "d20-5",
+ "3d6",
+ "3d6+0",
+ "3d6-0",
+ "3d6+5",
+ "3d6-5"
+ },
+ expectedNames = new String[]{
+ "",
+ "0",
+ "0",
+ "0",
+ "5",
+ "5",
+ "-5",
+ "0d20-0",
+ "0d20+5",
+ "0d20-5",
+ "5",
+ "-5",
+ "1d20",
+ "1d20",
+ "1d20",
+ "1d20+5",
+ "1d20-5",
+ "3d6",
+ "3d6",
+ "3d6",
+ "3d6+5",
+ "3d6-5"
+ };
- //fudge all the rolls;
- for(Die die : roll.dice) die.fudge(6);
- for(Die die : negativeBonus.dice) die.fudge(6);
- for(Die die : noBonus.dice) die.fudge(6);
+ Iterator
+ testExpressionsIterator = Arrays.stream(testExpressions).iterator(),
+ expectedNamesIterator = Arrays.stream(expectedNames).iterator();
- assertEquals("[6, 6, 6]+5 = 23", roll.toString());
- assertEquals("[6, 6, 6]-7 = 11", negativeBonus.toString());
- assertEquals("[6, 6, 6] = 18", noBonus.toString());
- assertEquals("[]+1 = 1", onlyBonus.toString());
- assertEquals("[] = 0", empty.toString());
+ while(testExpressionsIterator.hasNext()){
+ assertEquals(expectedNamesIterator.next(), Roll.of(testExpressionsIterator.next()).getName());
+ }
}
}
\ No newline at end of file