package squidpony.squidmath;

import java.io.Serializable;
import java.util.Map;

/**
 * A class that wraps an RNG and allows different String keys to be associated with biases toward low or high results
 * when a method is called that gets a number from the wrapped RNG. With this, you could make a category of "blessed" or
 * "cursed" biases that, instead of using a uniform distribution that produces all numbers approximately with equal
 * likelihood (with doubles between 0.0 and 1.0 averaging at 0.5), have different averages, like 0.7 for blessed or 0.3
 * for cursed, when generating between 0.0 and 1.0. You could also use this to favor or disfavor the player for "easy
 * mode" or "hard mode" categories of play.
 * <br>
 * The API allows you to associate an alternative average with a kind as a String, like "blessed to-hit" or "hard mode
 * enemy damage", if you expect to use that number more than once and might want to tweak any averages by changing one
 * number at a later point. You can also give an average as a double in place of a kind as a String, which avoids a
 * HashMap lookup and lets you give flexibly-adjusted numbers, but does need more effort to change many values
 * throughout a codebase if averages aren't all generated by a formula. You can also set the distribution in the
 * constructor or by changing the public distribution field; you can use constants in this class, TRIANGULAR,
 * EXPONENTIAL, TRUNCATED, SOFT_TRIANGULAR, and EXP_TRI (the average of EXPONENTIAL and TRIANGULAR), for different
 * choices, with the default being EXP_TRI. Each one of these has different behavior regarding a preference toward
 * extreme values; TRIANGULAR almost never produces very high or very low values, EXPONENTIAL frequently produces the
 * highest or lowest values for high or low expected averages, respectively, TRUNCATED will simply never generate values
 * that are too far from the average (otherwise it's uniform), SOFT_TRIANGULAR will produce a rounded version of
 * TRIANGULAR's distribution with less of an angular peak and more frequent high and low values, and EXP_TRI will have
 * something like a curve shape that may "collide" slightly with the upper bound if the average is high enough.
 * <br>
 * Credit for the technique used for the exponential modification to distributions goes to user pjs on StackOverflow,
 * http://stackoverflow.com/a/17796997 .
 * Credit should also be given to user vydd of the LispGames community, who made a visualization of the distribution
 * changing as the expected average changed (at the time, the typical behavior of an exponential distribution looked
 * like a bug, and the visualization showed that it was correct behavior). Derrick Creamer noticed how strange the
 * exponential distribution would seem to most players, and that led to adding the simple triangular distribution.
 * Created by Tommy Ettinger on 3/20/2016.
 */
public class RandomBias implements Serializable {
    private OrderedMap<String, Double> biases;
    public RNG rng;
    public int distribution = EXP_TRI;

    /**
     * A constant for a distribution that linearly increases in probability from a 0.0 chance of 0.0. to a 0.3333...
     * chance of getting the expected average, then linearly decreases until it reaches a 0.0 chance of 1.0. Doesn't
     * really support expected averages below 1/3 or above 2/3, due to how the triangular distribution works.
     */
    public static final int TRIANGULAR = 0,
    /**
     * A constant for a distribution that, for all values other than 0.5, will strongly favor either high or low
     * results to push the odds in favor of a high or low expected average. This is probably not what most players
     * expect, since it leads to massively more critical hits or failures if min or max results are counted as such.
     */
    EXPONENTIAL = 1,
    /**
     * Not like the other distributions; this is a constant for a distribution that simply truncates a random number's
     * possible range to less than 1.0, and adjusts the minimum or maximum value so that the average is the desired one.
     * This is a uniform random number generator, unlike the others which have a bias toward certain values; it simply
     * cannot generate values outside a certain range, and the values within the range it generates are all equally
     * likely. The range gets smaller the closer the expected average is to 0.0 or 1.0, with an expected average of 0.4
     * producing values between 0.0 and 0.8, and an expected average of 0.9 producing values of 0.8 to 1.0 (in all
     * cases, this is exclusive on the upper bound).
     */
    TRUNCATED = 2,
    /**
     * A constant for a distribution that averages two random floats, each with a triangular distribution (the same as
     * what using the TRIANGULAR constant would produce, but the distribution becomes more curved when multiple random
     * "dice rolls" are involved), to soften the point of the triangle and make very high or very low values appear
     * somewhat more frequently, while the expected average appears less frequently. This should not be used to generate
     * very large numbers, since the floats this uses lose precision after 24 bits, or about 16 million. It should
     * produce very reasonable results for common values in games, like 0 to 100 or 0 to 20. Doesn't really support
     * expected averages below 1/3 or above 2/3, due to how the triangular distribution works.
     */
    SOFT_TRIANGULAR = 3,
    /**
     * A constant for a distribution that averages two random floats, one with a triangular distribution (the same as
     * what using the TRIANGULAR constant would produce), and one with an exponential distribution (the same as what
     * using the EXPONENTIAL constant would produce) to soften the point of the triangle and make very high or very low
     * values appear much more frequently, while the expected average appears somewhat less frequently. This should not
     * be used to generate very large numbers, since the floats this uses lose precision after 24 bits, or about 16
     * million. It should produce very reasonable results for common values in games, like 0 to 100 or 0 to 20. Has
     * limited support for expected averages below 1/3 or above 2/3; unlike TRIANGULAR or SOFT_TRIANGULAR, expected
     * averages outside that range will still affect the generated average due to the EXPONENTIAL distribution
     * contributing half of the correction needed to match the expected average. An expected average of 5/6 will produce
     * an approximate average with this of 3/4, as opposed to 2/3 (for pure TRIANGULAR) or 5/6 (for EXPONENTIAL).
     */
    EXP_TRI = 4,
    /**
     * "Bathtub-shaped" or "U-shaped" distribution (technically the arcsine distribution) that is significantly more
     * likely to produce results at either extreme than it is to generate them in the center. The extremes in this case
     * are the same as the truncated distribution, so not all values are possible unless the expected average is 0.5.
     */
    BATHTUB_TRUNCATED = 5;

    private static final int softRange = 1 << 24;

    private static final long serialVersionUID = 4245874924013134958L;

    public RandomBias()
    {
        biases = new OrderedMap<>(32);
        rng = new RNG();
    }
    public RandomBias(RNG rng)
    {
        this.rng = rng;
        biases = new OrderedMap<>(32);
    }
    public RandomBias(RNG rng, Map<String, Double> mapping)
    {
        this(rng, mapping, EXP_TRI);
    }
    public RandomBias(RNG rng, Map<String, Double> mapping, int distribution) {
        this.rng = rng;
        this.distribution = distribution;
        if (mapping == null) {
            biases = new OrderedMap<>(32);
        } else {
            biases = new OrderedMap<>(mapping.size());
            double exp;
            for (Map.Entry<String, Double> kv : mapping.entrySet()) {
                exp = kv.getValue();
                if (exp <= 0) exp = 0.001;
                if (exp >= 1) exp = 0.999;
                biases.put(kv.getKey(), exp);
            }
        }
    }

    /**
     * Adds a kind of bias that can be used to change the average of random numbers generated when specified with that
     * kind.
     * @param kind a String that will be used as a key in a Map; can be given later on to bias results using this key
     * @param expectedAverage above 0.0 and below 1.0, with 0.5 as the normal average but other values are more useful.
     * @return this for chaining
     */
    public RandomBias putBias(String kind, double expectedAverage)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        biases.put(kind, expectedAverage);
        return this;
    }
    /**
     * Adds a number of kinds of bias that can be used to change the average of random numbers generated when specified
     * with one of those kinds.
     * @param mapping should have String keys that can be used later, and double values greater than 0 but less than 1.
     * @return this for chaining
     */
    public RandomBias putBiases(Map<String, Double> mapping)
    {
        double exp;
        for(Map.Entry<String, Double> kv : mapping.entrySet())
        {
            exp = kv.getValue();
            if(exp <= 0) exp = 0.001;
            if(exp >= 1) exp = 0.999;
            biases.put(kv.getKey(), exp);
        }
        return this;
    }

    private double quantile(double expected)
    {
        switch (distribution)
        {
            case EXPONENTIAL: return exponentialQuantile(expected);
            case TRUNCATED: return truncatedQuantile(expected);
            case TRIANGULAR: return triangularQuantile(expected);
            case SOFT_TRIANGULAR: return softQuantile(expected);
            case BATHTUB_TRUNCATED: return bathtubTruncatedQuantile(expected);
            default: return mixQuantile(expected);
        }
    }

    private double triangularQuantile(double expected)
    {
        expected = Math.max(0.001, Math.min(0.999, expected * 3.0 - 1.0));
        double p = rng.nextDouble();
        if(p < expected)
            return Math.sqrt(expected * p);
        if(p > expected)
            return 1 - Math.sqrt((1 - expected) * (1 - p));
        return expected;
    }
    private double truncatedQuantile(double expected)
    {
        if(expected >= 0.5)
            return rng.nextDouble() * (1.0 - expected) * 2 + expected - (1.0 - expected);
        return rng.nextDouble() * expected * 2;
    }
    private double bathtubQuantile(double expected)
    {
        expected = Math.sin(expected * Math.PI * 0.5);
        return expected * expected;
    }
    private double bathtubTruncatedQuantile(double expected)
    {
        if(expected >= 0.5)
            return bathtubQuantile(rng.nextDouble()) * (1.0 - expected) * 2 + expected - (1.0 - expected);
        return bathtubQuantile(rng.nextDouble()) * expected * 2;
    }
    private double exponentialQuantile(double expected)
    {
        return 0.9999999999999999 - Math.pow( rng.nextDouble(), 1.0 / (1.0 - expected) - 1.0);
    }
    private static float longToFloat(long n)
    {
        return n * 1.0f / softRange;
    }
    private double softQuantile(double expected)
    {
        expected = Math.max(0.001, Math.min(0.999, expected * 3.0 - 1.0));
        long pair = rng.nextLong();
        float left = longToFloat(pair >>> 40), right = longToFloat((pair >>> 16) & 0xFFFFFFL);
        double v;

        if(left < expected)
            v = Math.sqrt(expected * left);
        else if(left > expected)
            v = 1 - Math.sqrt((1 - expected) * (1 - left));
        else
            v = expected;
        if(right < expected)
            return (v + Math.sqrt(expected * right)) * 0.5;
        if(right > expected)
            return (v + 1 - Math.sqrt((1 - expected) * (1 - right))) * 0.5;
        return expected;
    }
    private double mixQuantile(double expected)
    {
        double d2 = Math.max(0.001, Math.min(0.999, expected * 3.0 - 1.0)), v;
        long pair = rng.nextLong();
        float left = longToFloat(pair >>> 40), right = longToFloat((pair >>> 16) & 0xFFFFFFL);

        if(left < d2)
            v = Math.sqrt(d2 * left);
        else if(left > d2)
            v = 1 - Math.sqrt((1 - d2) * (1 - left));
        else
            v = d2;
        return (Math.pow( right, 1.0 / expected - 1.0) + v) * 0.5;
    }
    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a positive long in either case, but
     * not all long values are possible if this is biased, in part because of generating a double, which has less
     * precision than long, and in part because some numbers need to be more common than others. If the kind is not in
     * the map, this generates a positive long, using 63 bits instead of RNG's normal 64 bits since it never generates
     * negative numbers.
     * @param kind the kind of bias to look up
     * @return a random 63-bit positive long, potentially influenced by the bias associated with kind, if present
     */
    public long biasedLong(String kind)
    {
        Double d = biases.get(kind);
        if(d == null)
            return rng.nextLong() >>> 1;
        return (long)(quantile(d) * Long.MAX_VALUE);
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a long between 0 and bound (exclusive
     * on bound), where bound can be negative (and this behavior is allowed even though RNG normally returns 0 for all
     * negative bounds). If the kind is not in the map, this generates a long between 0 and bound (exclusive on bound),
     * even if bound is negative.
     * @param kind the kind of bias to look up
     * @param bound the outer bound, exclusive; can be negative
     * @return a random long between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public long biasedLong(String kind, long bound)
    {
        boolean n = bound < 0;
        Double d = biases.get(kind);
        if(d == null)
            return n ? rng.nextLong(-bound) * -1 : rng.nextLong(bound);
        return (long)(quantile(d) * bound);
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a double between 0.0 and 1.0
     * (exclusive on 1.0). If the kind is not in the map, this generates a double using RNG and no further changes.
     * @param kind the kind of bias to look up
     * @return a random double between 0.0 and 1.0, potentially influenced by the bias associated with kind, if present
     */
    public double biasedDouble(String kind)
    {
        Double d = biases.get(kind);
        if(d == null)
            return rng.nextDouble();
        return quantile(d);
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a double between 0 and bound (exclusive
     * on bound), where bound can be negative (the same as RNG). If the kind is not in the map, this doesn't adjust the
     * average, and acts exactly like RNG.
     * @param kind the kind of bias to look up
     * @param bound the outer bound, exclusive; can be negative
     * @return a random double between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public double biasedDouble(String kind, double bound)
    {
        Double d = biases.get(kind);
        if(d == null)
            return rng.nextDouble(bound);
        return quantile(d) * bound;
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a positive int in either case. If the
     * kind is not in the map, this generates a positive int, using 31 bits instead of RNG's normal 32 bits since it
     * never generates negative numbers.
     * @param kind the kind of bias to look up
     * @return a random 31-bit positive int, potentially influenced by the bias associated with kind, if present
     */
    public int biasedInt(String kind)
    {
        Double d = biases.get(kind);
        if(d == null)
            return rng.nextInt() >>> 1;
        return (int)(quantile(d) * Integer.MAX_VALUE);
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be an int between 0 and bound (exclusive
     * on bound), where bound can be negative (and this behavior is allowed even though RNG normally returns 0 for all
     * negative bounds). If the kind is not in the map, this generates an int between 0 and bound (exclusive on bound),
     * even if bound is negative.
     * @param kind the kind of bias to look up
     * @param bound the outer bound, exclusive; can be negative
     * @return a random int between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public int biasedInt(String kind, int bound)
    {
        boolean n = bound < 0;
        Double d = biases.get(kind);
        if(d == null)
            return n ? rng.nextInt(-bound) * -1 : rng.nextInt(bound);
        return (int)(quantile(d) * bound);
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a float between 0.0 and 1.0
     * (exclusive on 1.0). If the kind is not in the map, this generates a float using RNG and no further changes.
     * @param kind the kind of bias to look up
     * @return a random float between 0.0 and 1.0, potentially influenced by the bias associated with kind, if present
     */
    public float biasedFloat(String kind)
    {
        Double d = biases.get(kind);
        if(d == null)
            return rng.nextFloat();
        return (float) quantile(d);
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a float between 0 and bound (exclusive
     * on bound), where bound can be negative. If the kind is not in the map, this doesn't adjust the average.
     * @param kind the kind of bias to look up
     * @param bound the outer bound, exclusive; can be negative
     * @return a random double between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public float biasedFloat(String kind, float bound)
    {
        Double d = biases.get(kind);
        if(d == null)
            return rng.nextFloat() * bound;
        return (float)(quantile(d) * bound);
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned boolean will be true if the random number (between
     * 0.0 and 1.0, exclusive upper) is greater than or equal to 0.5. If the kind is not in the map, this generates a
     * boolean using RNG and no further changes.
     * @param kind the kind of bias to look up
     * @return a random float between 0.0 and 1.0, potentially influenced by the bias associated with kind, if present
     */
    public boolean biasedBoolean(String kind)
    {
        Double d = biases.get(kind);
        if(d == null)
            return rng.nextBoolean();
        return quantile(d) >= 0.5;
    }
    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be an int between min and max (exclusive
     * on max), where min and/or max can be negative, and the difference between the two can be either positive or
     * negative. If the kind is not in the map, this doesn't adjust the average.
     * @param kind the kind of bias to look up
     * @param min the inner bound, inclusive; can be negative
     * @param max the outer bound, exclusive; can be negative
     * @return a random int between min and max, potentially influenced by the bias associated with kind, if present
     */
    public int biasedBetween(String kind, int min, int max)
    {
        return biasedInt(kind, max - min) + min;
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a long between min and max (exclusive
     * on max), where min and/or max can be negative, and the difference between the two can be either positive or
     * negative. If the kind is not in the map, this doesn't adjust the average.
     * @param kind the kind of bias to look up
     * @param min the inner bound, inclusive; can be negative
     * @param max the outer bound, exclusive; can be negative
     * @return a random long between min and max, potentially influenced by the bias associated with kind, if present
     */
    public long biasedBetween(String kind, long min, long max)
    {
        return biasedLong(kind, max - min) + min;
    }

    /**
     * Looks up the given kind in the Map of biases this stores, and generates a random number using this object's RNG.
     * If the kind is in the Map, this adjusts the generated number so it matches a distribution that would have the
     * expected average the kind was associated with. The returned number will be a double between min and max
     * (exclusive on max), where min and/or max can be negative, and the difference between the two can be either
     * positive or negative. If the kind is not in the map, this doesn't adjust the average.
     * @param kind the kind of bias to look up
     * @param min the inner bound, inclusive; can be negative
     * @param max the outer bound, exclusive; can be negative
     * @return a random double between min and max, potentially influenced by the bias associated with kind, if present
     */
    public double biasedBetween(String kind, double min, double max)
    {
        return biasedDouble(kind, max - min) + min;
    }


    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a positive long in either case, but
     * not all long values are possible if this is biased, in part because of generating a double, which has less
     * precision than long, and in part because some numbers need to be more common than others.
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @return a random 63-bit positive long, potentially influenced by the bias associated with kind, if present
     */
    public long biasedLong(double expectedAverage)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return (long)(quantile(expectedAverage) * Long.MAX_VALUE);
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a long between 0 and bound (exclusive
     * on bound), where bound can be negative (and this behavior is allowed even though RNG normally returns 0 for all
     * negative bounds).
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @param bound the outer bound, exclusive; can be negative
     * @return a random long between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public long biasedLong(double expectedAverage, long bound)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return (long)(quantile(expectedAverage) * bound);
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a double between 0.0 and 1.0 (exclusive
     * on 1.0).
     * @param expectedAverage the desired average
     * @return a random double between 0.0 and 1.0, potentially influenced by the bias associated with kind, if present
     */
    public double biasedDouble(double expectedAverage)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return quantile(expectedAverage);
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a double between 0 and bound (exclusive
     * on bound), where bound can be negative (the same as RNG).
     * @param expectedAverage the desired average
     * @param bound the outer bound, exclusive; can be negative
     * @return a random double between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public double biasedDouble(double expectedAverage, double bound)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return quantile(expectedAverage) * bound;
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a positive int from 0 to (2 to the 31)-1
     * in either case.
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @return a random 31-bit positive int, potentially influenced by the bias associated with kind, if present
     */
    public int biasedInt(double expectedAverage)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return (int)(quantile(expectedAverage) * Integer.MAX_VALUE);
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be an int between 0 and bound (exclusive
     * on bound), where bound can be negative (and this behavior is allowed even though RNG normally returns 0 for all
     * negative bounds).
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @param bound the outer bound, exclusive; can be negative
     * @return a random int between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public int biasedInt(double expectedAverage, int bound)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return (int)(quantile(expectedAverage) * bound);
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a float between 0.0f and 1.0f (exclusive
     * on 1.0f).
     * @param expectedAverage the desired average
     * @return a random float between 0.0 and 1.0, potentially influenced by the bias associated with kind, if present
     */
    public float biasedFloat(double expectedAverage)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return (float) quantile(expectedAverage);
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a float between 0f and bound (exclusive
     * on bound), where bound can be negative.
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @param bound the outer bound, exclusive; can be negative
     * @return a random double between 0 and bound, potentially influenced by the bias associated with kind, if present
     */
    public float biasedFloat(double expectedAverage, float bound)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return (float)(quantile(expectedAverage) * bound);
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned boolean will be true if the random number (between 0.0
     * and 1.0, exclusive upper) is greater than or equal to 0.5.
     * @param expectedAverage the desired probability of a true result, between 0.0 and 1.0
     * @return a random float between 0.0 and 1.0, potentially influenced by the bias associated with kind, if present
     */
    public boolean biasedBoolean(double expectedAverage)
    {
        if(expectedAverage <= 0) expectedAverage = 0.001;
        if(expectedAverage >= 1) expectedAverage = 0.999;
        return quantile(expectedAverage) >= 0.5;
    }
    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be an int between min and max (exclusive
     * on max), where min and/or max can be negative, and the difference between the two can be either positive or
     * negative.
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @param min the inner bound, inclusive; can be negative
     * @param max the outer bound, exclusive; can be negative
     * @return a random int between min and max, potentially influenced by the bias associated with kind, if present
     */
    public int biasedBetween(double expectedAverage, int min, int max)
    {
        return biasedInt(expectedAverage, max - min) + min;
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a long between min and max (exclusive
     * on max), where min and/or max can be negative, and the difference between the two can be either positive or
     * negative.
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @param min the inner bound, inclusive; can be negative
     * @param max the outer bound, exclusive; can be negative
     * @return a random long between min and max, potentially influenced by the bias associated with kind, if present
     */
    public long biasedBetween(double expectedAverage, long min, long max)
    {
        return biasedLong(expectedAverage, max - min) + min;
    }

    /**
     * Generates a random number using this object's RNG and adjusts the generated number so it matches a distribution
     * that would have the given expected average. The returned number will be a double between min and max (exclusive
     * on max), where min and/or max can be negative, and the difference between the two can be either positive or
     * negative.
     * @param expectedAverage the desired average if the minimum value was 0.0 and the exclusive max was 1.0
     * @param min the inner bound, inclusive; can be negative
     * @param max the outer bound, exclusive; can be negative
     * @return a random double between min and max, potentially influenced by the bias associated with kind, if present
     */
    public double biasedBetween(double expectedAverage, double min, double max)
    {
        return biasedDouble(expectedAverage, max - min) + min;
    }

    @Override
    public String toString() {
        return "RandomBias{" +
                "biases=" + biases +
                ", rng=" + rng +
                ", distribution=" + distribution +
                '}';
    }

}
