package squidpony.squidmath; import squidpony.annotation.GwtIncompatible; import java.io.Serializable; import java.util.*; /** * An alteration to a RandomnessSource that attempts to produce values that are perceived as fair to an imperfect user. * <p> * This takes a RandomnessSource, defaulting to a LightRNG, and uses it to generate random values, but tracks the total * and compares it to the potential total of a generator of only numbers with a desired value (default 0.54, * so it compares against a sequence of all 0.54). If the current generated total is too high or low compared to the * desired total, the currently used seed is possibly changed, the generated number is moved in the direction of the * desired fairness, and it returns that instead of the number that would have pushed the current generated total * beyond the desired threshold. The new number, if one is changed, will always be closer to the desired fairness. * This is absolutely insecure for cryptographic purposes, but should seem more "fair" to a player than a * random number generator that seeks to be truly random. * You can create multiple DharmaRNG objects with different fairness values and seeds, and use favorable generators * (with fairness greater than 0.54) for characters that need an easier time, or unfavorable generators if you want * the characters that use that RNG to be impeded somewhat. * The name comes from the Wheel of Dharma. * This class currently will have a slight bias toward lower numbers with many RNGs unless fairness is tweaked; 0.54 * can be used as a stand-in because 0.5 leans too low. * * <p> * You can get values from this generator with: {@link #nextDouble()}, {@link #nextInt()}, * {@link #nextLong()}, and the bounded variants on each of those. * <p> * You can alter the tracking information or requested fairness with {@link #resetFortune()}, * {@link #setFairness(double)}, and {@link #getFairness()}. * * Created by Tommy Ettinger on 5/2/2015. */ public class DharmaRNG extends RNG implements Serializable{ /** Used to tweak the generator toward high or low values. */ private double fairness = 0.54; /** Running total for what this has actually produced. */ private double produced = 0.0; /** Running total for what this would produce if it always produced a value equal to fairness. */ private double baseline = 0.0; private static final long serialVersionUID = -8919455766853811999L; /** * Constructs a DharmaRNG with a pseudo-random seed from Math.random(). */ public DharmaRNG() { this((long)(Math.random() * ((1L << 50) - 1))); } /** * Construct a new DharmaRNG with the given seed. * * @param seed used to seed the default RandomnessSource. */ public DharmaRNG(final long seed) { super(seed); } /** * Construct a new DharmaRNG with the given seed. * * @param seed used to seed the default RandomnessSource. * @param fairness the desired fairness metric, which must be between 0.0 and 1.0 */ public DharmaRNG(final long seed, final double fairness) { super(seed); if(fairness < 0.0 || fairness >= 1.0) this.fairness = 0.54; else this.fairness = fairness; } /** * String-seeded constructor; uses a platform-independent hash of the String (it does not use String.hashCode) as a * seed for LightRNG, which is of high quality, but low period (which rarely matters for games), and has good speed, * tiny state size, and excellent 64-bit number generation. * * @param seedString a String as a seed */ public DharmaRNG(String seedString) { super(seedString); } /** * String-seeded constructor; uses a platform-independent hash of the String (it does not use String.hashCode) as a * seed for LightRNG, which is of high quality, but low period (which rarely matters for games), and has good speed, * tiny state size, and excellent 64-bit number generation. * * @param seedString a String as a seed */ public DharmaRNG(String seedString, double fairness) { super(seedString); if(fairness < 0.0 || fairness >= 1.0) this.fairness = 0.54; else this.fairness = fairness; } /** * Construct a new DharmaRNG with the given seed. * * @param rs the implementation used to generate random bits. */ public DharmaRNG(final RandomnessSource rs) { super(rs); } /** * Construct a new DharmaRNG with the given seed. * * @param rs the implementation used to generate random bits. * @param fairness the desired fairness metric, which must be between 0.0 and 1.0 */ public DharmaRNG(final RandomnessSource rs, final double fairness) { super(rs); if(fairness < 0.0 || fairness >= 1.0) this.fairness = 0.54; else this.fairness = fairness; } /** * Generate a random double, altering the result if recently generated results have been leaning * away from this class' fairness value. * @return a double between 0.0 (inclusive) and 1.0 (exclusive) */ @Override public double nextDouble() { double gen = random.nextLong() * DOUBLE_UNIT; /*if(Math.abs((produced + gen) - (baseline + fairness)) > 1.5) { //do some reseeding here if possible }*/ if(Math.abs((produced + gen) - (baseline + fairness)) > 0.5) { gen = (gen + fairness) / 2.0; produced *= 0.5; baseline *= 0.5; produced += gen; baseline += fairness; return gen; } else { produced += gen; baseline += fairness; return gen; } } /** * This returns a random double between 0.0 (inclusive) and max (exclusive). * * @return a value between 0 (inclusive) and max (exclusive) */ @Override public double nextDouble(double max) { return super.nextDouble(max); } /** * Returns a value from a even distribution from min (inclusive) to max * (exclusive). * * @param min the minimum bound on the return value (inclusive) * @param max the maximum bound on the return value (exclusive) * @return the found value */ @Override public double between(double min, double max) { return super.between(min, max); } /** * Returns a value between min (inclusive) and max (exclusive). * * The inclusive and exclusive behavior is to match the behavior of the * similar method that deals with floating point values. * * @param min the minimum bound on the return value (inclusive) * @param max the maximum bound on the return value (exclusive) * @return the found value */ @Override public int between(int min, int max) { return super.between(min, max); } /** * Returns the average of a number of randomly selected numbers from the * provided range, with min being inclusive and max being exclusive. It will * sample the number of times passed in as the third parameter. * * The inclusive and exclusive behavior is to match the behavior of the * similar method that deals with floating point values. * * This can be used to weight RNG calls to the average between min and max. * * @param min the minimum bound on the return value (inclusive) * @param max the maximum bound on the return value (exclusive) * @param samples the number of samples to take * @return the found value */ @Override public int betweenWeighted(int min, int max, int samples) { return super.betweenWeighted(min, max, samples); } /** * Returns a random element from the provided array and maintains object * type. * * @param <T> the type of the returned object * @param array the array to get an element from * @return the randomly selected element */ @Override public <T> T getRandomElement(T[] array) { return super.getRandomElement(array); } /** * Returns a random element from the provided list. If the list is empty * then null is returned. * * @param <T> the type of the returned object * @param list the list to get an element from * @return the randomly selected element */ @Override public <T> T getRandomElement(List<T> list) { return super.getRandomElement(list); } /** * Returns a random element from the provided ShortSet. If the set is empty * then an exception is thrown. * * <p> * Requires iterating through a random amount of the elements in set, so performance depends on the size of set but * is likely to be decent. This is mostly meant for internal use, the same as ShortSet. * </p> * @param set the ShortSet to get an element from * @return the randomly selected element */ public short getRandomElement(ShortSet set) { return super.getRandomElement(set); } /** * Returns a random element from the provided Collection, which should have predictable iteration order if you want * predictable behavior for identical RNG seeds, though it will get a random element just fine for any Collection * (just not predictably in all cases). If you give this a Set, it should be a LinkedHashSet or some form of sorted * Set like TreeSet if you want predictable results. Any List or Queue should be fine. Map does not implement * Collection, thank you very much Java library designers, so you can't actually pass a Map to this, though you can * pass the keys or values. If coll is empty, returns null. * * <p> * Requires iterating through a random amount of coll's elements, so performance depends on the size of coll but is * likely to be decent, as long as iteration isn't unusually slow. This replaces {@code getRandomElement(Queue)}, * since Queue implements Collection and the older Queue-using implementation was probably less efficient. * </p> * @param <T> the type of the returned object * @param coll the Collection to get an element from; remember, Map does not implement Collection * @return the randomly selected element */ public <T> T getRandomElement(Collection<T> coll) { return super.getRandomElement(coll); } /** * @return a value from the gaussian distribution */ @Override public synchronized double nextGaussian() { return super.nextGaussian(); } /** * Returns a random integer below the given bound, or 0 if the bound is 0 or * negative. Affects the current fortune. * * @param bound the upper bound (exclusive) * @return the found number */ @Override public int nextInt(int bound) { if (bound <= 0) { return 0; } return (int)(nextDouble() * bound); } /** * Returns a random integer, which may be positive or negative. Affects the current fortune. * @return A random int */ @Override public int nextInt() { return (int)((nextDouble() - 0.5) * 2.0 * 0x7FFFFFFF); } /** * Returns a random long, which may be positive or negative. Affects the current fortune. * @return A random long */ @Override public long nextLong() { return (long)((nextDouble() - 0.5) * 2.0 * 0x7FFFFFFFFFFFFFFFL); } /** * Returns a random long below the given bound, or 0 if the bound is 0 or * negative. * * @param bound the upper bound (exclusive) * @return the found number */ @Override public long nextLong(long bound) { if (bound <= 0) { return 0; } return (long)(nextDouble() * bound); } /** * Gets the measure that this class uses for RNG fairness, defaulting to 0.54 (always between 0.0 and 1.0). * @return the current fairness metric. */ public double getFairness() { return fairness; } /** * Sets the measure that this class uses for RNG fairness, which must always be between 0.0 and 1.0, and will be * set to 0.54 if an invalid value is passed. * @param fairness the desired fairness metric, which must be 0.0 <= fairness < 1.0 */ public void setFairness(double fairness) { if(fairness < 0.0 || fairness >= 1.0) this.fairness = 0.54; else this.fairness = fairness; } /** * Gets the status of the fortune used when calculating fairness adjustments. * @return the current value used to determine whether the results should be adjusted toward fairness. */ public double getFortune() { return Math.abs(produced - baseline); } /** * Resets the stored history this RNG uses to try to ensure fairness. */ public void resetFortune() { produced = 0.0; baseline = 0.0; } /** * * @param bits the number of bits to be returned * @return a random int of the number of bits specified. */ @Override public int next(int bits) { if(bits <= 0) return 0; if(bits > 32) bits = 32; return (int)(nextDouble() * (1l << bits)); } @Override public Random asRandom() { return super.asRandom(); } @Override @GwtIncompatible public <T> List<T> randomRotation(List<T> l) { return super.randomRotation(l); } @Override public <T> Iterable<T> getRandomStartIterable(List<T> list) { return super.getRandomStartIterable(list); } /* @Override @GwtIncompatible public <T> T[] shuffle(T[] elements) { return super.shuffle(elements); } */ @Override public <T> T[] shuffle(T[] elements, T[] dest) { return super.shuffle(elements, dest); } @Override public <T> ArrayList<T> shuffle(Collection<T> elements) { return super.shuffle(elements); } /** * Returns a value between min (inclusive) and max (exclusive). * <p> * The inclusive and exclusive behavior is to match the behavior of the * similar method that deals with floating point values. * * @param min the minimum bound on the return value (inclusive) * @param max the maximum bound on the return value (exclusive) * @return the found value */ @Override public long between(long min, long max) { return min + nextLong(max - min); } /** * Shuffle an array using the Fisher-Yates algorithm. Not GWT-compatible; use the overload that takes two arrays. * <br> * https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle * * @param elements an array of T; will not be modified * @return a shuffled copy of elements */ @GwtIncompatible @Override public <T> T[] shuffle(T[] elements) { return super.shuffle(elements); } /** * Generates a random permutation of the range from 0 (inclusive) to length (exclusive). * Useful for passing to OrderedMap or OrderedSet's reorder() methods. * * @param length the size of the ordering to produce * @return a random ordering containing all ints from 0 to length (exclusive) */ @Override public int[] randomOrdering(int length) { return super.randomOrdering(length); } /** * Returns a random non-negative integer below the given bound, or 0 if the bound is 0. * Uses a slightly optimized technique. This method is considered "hasty" since * it should be faster than nextInt() doesn't check for "less-valid" bounds values. It also * has undefined behavior if bound is negative, though it will probably produce a negative * number (just how negative is an open question). * * @param bound the upper bound (exclusive); behavior is undefined if bound is negative * @return the found number */ @Override public int nextIntHasty(int bound) { return (int)(nextDouble() * bound); } @Override public float nextFloat() { return (float)nextDouble(); } @Override public boolean nextBoolean() { return nextDouble() >= 0.5; } @Override public RandomnessSource getRandomness() { return random; } @Override public void setRandomness(RandomnessSource random) { this.random = random; } /** * Creates a copy of this DharmaRNG; it will generate the same random numbers, given the same calls in order, as * this DharmaRNG at the point copy() is called. The copy will not share references with this DharmaRNG. * * @return a copy of this DharmaRNG */ @Override public RNG copy() { DharmaRNG next = new DharmaRNG(random.copy(), fairness); next.produced = produced; next.baseline = baseline; return next; } /** * Gets a random portion of data (an array), assigns that portion to output (an array) so that it fills as much as * it can, and then returns output. Will only use a given position in the given data at most once; does this by * shuffling a copy of data and getting a section of it that matches the length of output. * * Based on http://stackoverflow.com/a/21460179 , credit to Vincent van der Weele; modifications were made to avoid * copying or creating a new generic array (a problem on GWT). * @param data an array of T; will not be modified. * @param output an array of T that will be overwritten; should always be instantiated with the portion length * @param <T> can be any non-primitive type. * @return an array of T that has length equal to output's length and may contain null elements if output is shorter * than data */ @Override public <T> T[] randomPortion(T[] data, T[] output) { return super.randomPortion(data, output); } /** * Gets a random portion of a List and returns it as a new List. Will only use a given position in the given * List at most once; does this by shuffling a copy of the List and getting a section of it. * * @param data a List of T; will not be modified. * @param count the non-negative number of elements to randomly take from data * @return a List of T that has length equal to the smaller of count or data.length */ @Override public <T> List<T> randomPortion(List<T> data, int count) { return super.randomPortion(data, count); } /** * Gets a random subrange of the non-negative ints from start (inclusive) to end (exclusive), using count elements. * May return an empty array if the parameters are invalid (end is less than/equal to start, or start is negative). * * @param start the start of the range of numbers to potentially use (inclusive) * @param end the end of the range of numbers to potentially use (exclusive) * @param count the total number of elements to use; will be less if the range is smaller than count * @return an int array that contains at most one of each number in the range */ @Override public int[] randomRange(int start, int end, int count) { return super.randomRange(start, end, count); } @Override public String toString() { return "DharmaRNG{" + "fairness=" + fairness + ", produced=" + produced + ", baseline=" + baseline + ", Randomness Source=" + random + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DharmaRNG dharmaRNG = (DharmaRNG) o; if (Double.compare(dharmaRNG.fairness, fairness) != 0) return false; return Double.compare(dharmaRNG.produced, produced) == 0 && Double.compare(dharmaRNG.baseline, baseline) == 0; } @Override public int hashCode() { int result; long temp; temp = NumberTools.doubleToLongBits(fairness); result = (int) (temp ^ (temp >>> 32)); temp = NumberTools.doubleToLongBits(produced); result = 31 * result + (int) (temp ^ (temp >>> 32)); temp = NumberTools.doubleToLongBits(baseline); result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } }