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