diff options
| author | Ben Culkin <scorpress@gmail.com> | 2020-11-14 22:07:50 -0500 |
|---|---|---|
| committer | Ben Culkin <scorpress@gmail.com> | 2020-11-14 22:07:50 -0500 |
| commit | 84f0803068ba50bcba67de42634a4bf3d0e9e170 (patch) | |
| tree | 6c489ae7d727558333664ac8ff834d398a424322 | |
| parent | 45f36834bec138429abaecd37021e7d243d7506a (diff) | |
Begin new dice implementation
For various reasons, I'm not happy with the other implementations.
Here's hoping that this one is better
7 files changed, 506 insertions, 0 deletions
diff --git a/dice/src/main/java/bjc/dicelang/neodice/Die.java b/dice/src/main/java/bjc/dicelang/neodice/Die.java new file mode 100644 index 0000000..a331e4e --- /dev/null +++ b/dice/src/main/java/bjc/dicelang/neodice/Die.java @@ -0,0 +1,96 @@ +package bjc.dicelang.neodice; + +import java.util.*; +import java.util.stream.*; + +/** + * Represents a single polyhedral die. + * @author Ben Culkin + * + */ +@FunctionalInterface +public interface Die { + /** + * Rolls this die. + * + * @param rng The source for random numbers + * + * @return The result of rolling the die. + */ + public int roll(Random rng); + + /** + * Returns a die pool which rolls this die the specified number of times. + * + * @param numTimes The number of times to roll this die. + * + * @return A die pool that rolls this die the specified number of times. + */ + default DiePool times(int numTimes) { + return new TimesDiePool(this, numTimes); + }; + + /** + * Create an iterator which gives rolls of this dice. + * + * @param rng The source for random numbers. + * + * @return An iterator which gives rolls of this dice. + */ + default Iterator<Integer> iterator(Random rng) { + return IntStream.generate(() -> this.roll(rng)).iterator(); + } +} + +final class TimesDiePool implements DiePool { + private final Die contained; + private final int numDice; + + public TimesDiePool(Die contained, int numDice) { + this.contained = contained; + this.numDice = numDice; + } + + @Override + public int[] roll(Random rng) { + int[] results = new int[numDice]; + + for (int index = 0; index < numDice; index++) { + results[index] = contained.roll(rng); + } + + return results; + } + + @Override + public Die[] contained() { + Die[] results = new Die[numDice]; + + for (int index = 0; index < numDice; index++) { + results[index] = contained; + } + + return results; + } + + @Override + public String toString() { + return String.format("%d%s", numDice, contained); + } + + @Override + public int hashCode() { + return Objects.hash(contained, numDice); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + TimesDiePool other = (TimesDiePool) obj; + + return Objects.equals(contained, other.contained) && numDice == other.numDice; + } +}
\ No newline at end of file diff --git a/dice/src/main/java/bjc/dicelang/neodice/DieFactory.java b/dice/src/main/java/bjc/dicelang/neodice/DieFactory.java new file mode 100644 index 0000000..d9bae7e --- /dev/null +++ b/dice/src/main/java/bjc/dicelang/neodice/DieFactory.java @@ -0,0 +1,63 @@ +package bjc.dicelang.neodice; + +import java.util.*; + +/** + * Various static functions which create instances of Die. + * + * @author Ben Culkin + * + */ +public class DieFactory { + /** + * Create a simple polyhedral die with a fixed number of sides. + * + * @param sides The number of sides for the die. + * + * @return A die which returns a result from 1 to sides. + */ + public static Die polyhedral(int sides) { + return new PolyhedralDie(sides); + } +} + +final class PolyhedralDie implements Die { + private final int sides; + + public PolyhedralDie(int sides) { + this.sides = sides; + } + + @Override + public int roll(Random rng) { + // Dice are one-based, not zero-based. + return rng.nextInt(sides) + 1; + } + + @Override + public String toString() { + return String.format("d%d", sides); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + sides; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + PolyhedralDie other = (PolyhedralDie) obj; + + if (sides != other.sides) return false; + else return true; + } + + +}
\ No newline at end of file diff --git a/dice/src/main/java/bjc/dicelang/neodice/DiePool.java b/dice/src/main/java/bjc/dicelang/neodice/DiePool.java new file mode 100644 index 0000000..b7ee834 --- /dev/null +++ b/dice/src/main/java/bjc/dicelang/neodice/DiePool.java @@ -0,0 +1,176 @@ +package bjc.dicelang.neodice; + +import java.util.*; +import java.util.function.*; + +/** + * Represents a pool of dice. + * + * @author Ben Culkin + * + */ +@FunctionalInterface +public interface DiePool { + /** + * Roll each die in the pool, and return the results. + * + * Note that this array is not guaranteed to be the same size every time it + * is rolled, because there are some pool types that could add/remove dice. + * + * @param rng The source for random numbers + * + * @return The result of rolling each die in the pool. + */ + public int[] roll(Random rng); + + /** + * Gets the dice contained in this pool. + * + * Note that the length of this array may not be the same as the length of + * the array returned by roll, because certain pool types may add additional + * dice. + * + * Also note that this array (and the Die instances contained in it) should + * not be modified. That may work for certain pool types, but it isn't + * guaranteed to work, and can lead to unintuitive behavior. For instances, + * certain pool types may return an array where multiple elements of it refer + * to the same Die instance. + * + * The default implementation throws an UnsupportedOperationException. + * + * @return The dice contained in this pool. + * + * @throws UnsupportedOperationException If the composite dice can't be retrieved. + */ + default Die[] contained() { + throw new UnsupportedOperationException("Can't get composite dice"); + } + + /** + * Returns a version of this die pool which returns its results in sorted + * order. + * + * At the moment, sorting in descending order is somewhat less efficent than + * sorting in ascending order, because Java doesn't provide a built-in + * descending sort for primitive arrays. + * + * @param isDescending True to sort in descending order, false to sort in ascending order. + * + * @return The die pool, which returns its results in sorted order. + */ + default DiePool sorted(boolean isDescending) { + return new SortedDiePool(this, isDescending); + } + + /** + * Return a die pool which rolls this one, then filters out any results that + * don't match the provided predicate. + * + * @param matcher The predicate that determines + * + * @return A die pool which contains only entries that pass the predicate. + */ + default DiePool filtered(IntPredicate matcher) { + return new FilteredDiePool(this, matcher); + } + + /** + * Get an iterator which iterates over a single roll of this die pool. + * + * @param rng The source of random numbers. + * + * @return An iterator over a single roll of this die pool. + */ + default Iterator<Integer> iterator(Random rng) { + return Arrays.stream(this.roll(rng)).iterator(); + } +} + +final class SortedDiePool implements DiePool { + private final boolean isDescending; + private final DiePool pool; + + public SortedDiePool(DiePool pool, boolean isDescending) { + this.pool = pool; + this.isDescending = isDescending; + } + + @Override + public int[] roll(Random rng) { + int[] rolls = pool.roll(rng); + + Arrays.sort(rolls); + + if (isDescending) { + int[] newRolls = new int[rolls.length]; + + int newIndex = newRolls.length; + for (int index = 0; index < rolls.length; index++) { + newRolls[newIndex--] = rolls[index]; + } + + return newRolls; + } else { + return rolls; + } + } + + @Override + public String toString() { + return String.format("%s (sorted %s)", pool, + isDescending ? " descending" : "ascending"); + } + + @Override + public int hashCode() { + return Objects.hash(isDescending, pool); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + SortedDiePool other = (SortedDiePool) obj; + + return isDescending == other.isDescending + && Objects.equals(pool, other.pool); + } +} + +final class FilteredDiePool implements DiePool { + private final DiePool pool; + private final IntPredicate filter; + + public FilteredDiePool(DiePool pool, IntPredicate filter) { + this.pool = pool; + this.filter = filter; + } + + @Override + public int[] roll(Random rng) { + int[] rolls = pool.roll(rng); + + return Arrays.stream(rolls).filter(filter).toArray(); + } + + // No toString, since there isn't any sensible to output the filter + + @Override + public int hashCode() { + return Objects.hash(filter, pool); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + FilteredDiePool other = (FilteredDiePool) obj; + + return Objects.equals(filter, other.filter) + && Objects.equals(pool, other.pool); + } +}
\ No newline at end of file diff --git a/dice/src/main/java/bjc/dicelang/neodice/DiePoolFactory.java b/dice/src/main/java/bjc/dicelang/neodice/DiePoolFactory.java new file mode 100644 index 0000000..6d9314d --- /dev/null +++ b/dice/src/main/java/bjc/dicelang/neodice/DiePoolFactory.java @@ -0,0 +1,82 @@ +package bjc.dicelang.neodice; + +import java.util.*; + +/** + * Various static functions which create instances of DiePool. + * + * @author Ben Culkin + * + */ +public class DiePoolFactory { + /** + * Create a die pool containing the provided dice. + * + * @param dice The dice to put into the pool. + * + * @return A pool which contains the provided dice. + */ + public static DiePool containing(Die... dice) { + return new FixedDiePool(dice); + } +} + +final class FixedDiePool implements DiePool { + private final Die[] dice; + + public FixedDiePool(Die[] dice) { + this.dice = dice; + } + + @Override + public int[] roll(Random rng) { + int[] results = new int[dice.length]; + + for (int index = 0; index < dice.length; index++) { + results[index] = dice[index].roll(rng); + } + + return results; + } + + @Override + public Die[] contained() { + return dice; + } + + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < dice.length; i++) { + Die die = dice[i]; + + builder.append(die); + + // Don't add an extra trailing comma + if (i < dice.length - 1) builder.append(", "); + } + + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(dice); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + FixedDiePool other = (FixedDiePool) obj; + + return Arrays.equals(dice, other.dice); + } +}
\ No newline at end of file diff --git a/dice/src/main/java/bjc/dicelang/neodice/package-info.java b/dice/src/main/java/bjc/dicelang/neodice/package-info.java new file mode 100644 index 0000000..f6ec673 --- /dev/null +++ b/dice/src/main/java/bjc/dicelang/neodice/package-info.java @@ -0,0 +1,11 @@ +/** + * This package contains yet another implementation of dice and dice expressions. + * + * The other ones just didn't seem to quite serve the expected functions. + */ + +/** + * @author Ben Culkin + * + */ +package bjc.dicelang.neodice;
\ No newline at end of file diff --git a/dice/src/test/java/bjc/dicelang/neodice/DiePoolTest.java b/dice/src/test/java/bjc/dicelang/neodice/DiePoolTest.java new file mode 100644 index 0000000..1a217d3 --- /dev/null +++ b/dice/src/test/java/bjc/dicelang/neodice/DiePoolTest.java @@ -0,0 +1,33 @@ +package bjc.dicelang.neodice; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.*; + +@SuppressWarnings("javadoc") +public class DiePoolTest { + private static final Random rng = new Random(); + + @Test + public void containedOneDieYieldsOneDie() { + Die oneSidedDie = DieFactory.polyhedral(1); + DiePool pool = DiePoolFactory.containing(oneSidedDie); + + assertArrayEquals( + "A contained pool created with one die, yields that die", + new Die[] {oneSidedDie}, pool.contained()); + } + + @Test + public void containedOneDieRollsOneDie() { + Die oneSidedDie = DieFactory.polyhedral(1); + DiePool pool = DiePoolFactory.containing(oneSidedDie); + + for (int i = 0; i < 10; i++) { + assertArrayEquals("One-die pools roll one die", + new int[] {1}, pool.roll(rng)); + } + } +} diff --git a/dice/src/test/java/bjc/dicelang/neodice/DieTest.java b/dice/src/test/java/bjc/dicelang/neodice/DieTest.java new file mode 100644 index 0000000..fc69f99 --- /dev/null +++ b/dice/src/test/java/bjc/dicelang/neodice/DieTest.java @@ -0,0 +1,45 @@ +package bjc.dicelang.neodice; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.*; + +@SuppressWarnings("javadoc") +public class DieTest { + private final static Random rng = new Random(); + + @Test + public void onesidedDiceReturnOne() { + Die die = DieFactory.polyhedral(1); + + for (int i = 0; i < 10; i++) { + assertEquals("One-sided dice always return 1", 1, die.roll(rng)); + } + } + + @Test + public void polyhedralDiceStayInRange() { + Die die = DieFactory.polyhedral(6); + + for (int i = 0; i < 50; i++) { + int result = die.roll(rng); + + boolean inRange = result <= 6 && result >= 1; + + assertTrue("Six-sided dice always return a value from 1 to 6", inRange); + } + } + + public void polyhedralDiceEqualityFunctionsProperly() { + Die dieA1 = DieFactory.polyhedral(1); + Die dieA2 = DieFactory.polyhedral(1); + Die dieB1 = DieFactory.polyhedral(2); + + assertEquals("Polyhedral dice with the same number of sides are equal", + dieA1, dieA2); + assertNotEquals("Polyhedral dice with a diffeent number of sides aren't equal", + dieA1, dieB1); + } +} |
