diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6f89c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..284262d --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Dice4j + +A Java library for rolling dice and parsing tabletop style dice-expressions. + +Examples: +* 3d10 +* 1d20+4 + +The Dice class possesses static methods to parse dice-expressions. +Rolls can also be instantiated directly +through classes implementing the Rollable interface. diff --git a/pom.xml b/pom.xml index 804afc4..a934978 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,15 @@ dev.thearcticgiant dice4j - 0.1-SNAPSHOT + 0.1 + + + org.junit.jupiter + junit-jupiter-api + 5.6.2 + test + + 16 diff --git a/src/main/java/dev/thearcticgiant/dice4j/Dice.java b/src/main/java/dev/thearcticgiant/dice4j/Dice.java new file mode 100644 index 0000000..6a762c9 --- /dev/null +++ b/src/main/java/dev/thearcticgiant/dice4j/Dice.java @@ -0,0 +1,33 @@ +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"; + } + + if(bonusStr == null) bonusStr = "0"; + + final int + count = Integer.parseInt(countStr), + sides = Integer.parseInt(sidesStr), + bonus = Integer.parseInt(bonusStr); + return roll(count, sides, bonus); + } + public static Roll roll(int count, int sides, int bonus){ + return new Roll(count, sides, bonus); + } + public static Roll roll(int count, int sides){ + return new Roll(count, sides); + } +} diff --git a/src/main/java/dev/thearcticgiant/dice4j/Die.java b/src/main/java/dev/thearcticgiant/dice4j/Die.java new file mode 100644 index 0000000..4f17b91 --- /dev/null +++ b/src/main/java/dev/thearcticgiant/dice4j/Die.java @@ -0,0 +1,63 @@ +package dev.thearcticgiant.dice4j; + +import java.util.Random; + +public class Die implements Rollable{ + public final int sides; + private int roll; + + private final Random random; + + /** + * 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. + * @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; + } + + public int read(){ + return roll; + } + + public int fudge(int roll){ + return this.roll = roll; + } + + public String getName(){ + return String.format("d%d", sides); + } + + public String toString(){ + return String.format("%s (%d)", getName(), roll); + } +} diff --git a/src/main/java/dev/thearcticgiant/dice4j/Roll.java b/src/main/java/dev/thearcticgiant/dice4j/Roll.java new file mode 100644 index 0000000..714c8d5 --- /dev/null +++ b/src/main/java/dev/thearcticgiant/dice4j/Roll.java @@ -0,0 +1,105 @@ +package dev.thearcticgiant.dice4j; + +import java.util.Iterator; +import java.util.Set; + +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. + */ + 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); + } + 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(); + } + + /** + * 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. + */ + 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(", "); + } + string.append(']'); + if(bonus > 0) string.append('+').append(bonus); + else if(bonus < 0) string.append('-').append(-bonus); + string.append(" = ") + .append(read()); + return string.toString(); + } +} diff --git a/src/main/java/dev/thearcticgiant/dice4j/Rollable.java b/src/main/java/dev/thearcticgiant/dice4j/Rollable.java new file mode 100644 index 0000000..3a4c841 --- /dev/null +++ b/src/main/java/dev/thearcticgiant/dice4j/Rollable.java @@ -0,0 +1,24 @@ +package dev.thearcticgiant.dice4j; + +public interface Rollable{ + + /** + * Generate a new random result. + * @return The newly rolled result; + */ + int roll(); + + /** + * Return the most recently rolled result; + * this function should be guaranteed to return the same result in successive calls until roll() is called again. + * @return The most recent roll. + */ + int read(); + + /** + * A human readable string representation of this Rollable's state, irrespective of the result of a given roll. + * The result should provide all the necessary information to construct a similar Rollable. + * @return The name of this particular Rollable. + */ + String getName(); +} diff --git a/src/test/java/dev/thearcticgiant/dice4j/DiceTest.java b/src/test/java/dev/thearcticgiant/dice4j/DiceTest.java new file mode 100644 index 0000000..0cb47a2 --- /dev/null +++ b/src/test/java/dev/thearcticgiant/dice4j/DiceTest.java @@ -0,0 +1,71 @@ +package dev.thearcticgiant.dice4j; + +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" + }; + + Iterator + testExpressionsIterator = Arrays.stream(testExpressions).iterator(), + expectedNamesIterator = Arrays.stream(expectedNames).iterator(); + + while(testExpressionsIterator.hasNext()){ + assertEquals(expectedNamesIterator.next(), Dice.roll(testExpressionsIterator.next()).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 new file mode 100644 index 0000000..ab15ee7 --- /dev/null +++ b/src/test/java/dev/thearcticgiant/dice4j/DieTest.java @@ -0,0 +1,67 @@ +package dev.thearcticgiant.dice4j; + +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; + +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()); + } + } + + @Test + void read(){ + Die d6 = new Die(6); + assertEquals(d6.roll(), d6.read()); + } + + @Test + void getName(){ + Die d6 = new Die(6); + assertEquals("d6", d6.getName()); + } + + @Test + void testToString(){ + Die d6 = new Die(6); + d6.fudge(6); + assertEquals("d6 (6)", d6.toString()); + } + + @Test + void testSeededDice(){ + Random random = new Random(); + for(int i = 0; i<2<<3; i++){ + Die d1, d2; + long seed = random.nextLong(); + 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()); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/dev/thearcticgiant/dice4j/RollTest.java b/src/test/java/dev/thearcticgiant/dice4j/RollTest.java new file mode 100644 index 0000000..1f93f9c --- /dev/null +++ b/src/test/java/dev/thearcticgiant/dice4j/RollTest.java @@ -0,0 +1,76 @@ +package dev.thearcticgiant.dice4j; + +import org.junit.jupiter.api.Test; + +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(); + + 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()); + } + + @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(); + + //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("[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()); + } +} \ No newline at end of file