package bjc.utils.gen; import java.util.Random; import bjc.data.Pair; import bjc.data.SimplePair; import bjc.funcdata.FunctionalList; import bjc.funcdata.ListEx; /** * Represents a random number generator where certain results are weighted more * heavily than others. * * @author ben * * @param * The type of values that are randomly selected. */ public class WeightedRandom { private final ListEx> values; /* The source for any needed random numbers */ private Random source; /* The total chance for all values. */ private int totalChance; private final static Random BASE = new Random(); private boolean exhaust; /** * Create a new weighted random generator with the specified source of * randomness. * * @param src * The source of randomness to use. */ public WeightedRandom(Random src) { values = new FunctionalList<>(); if (src == null) throw new NullPointerException("Source of randomness must not be null"); source = src; } /** * Create a new weighted random generator. */ public WeightedRandom() { this(BASE); } private WeightedRandom(Random src, ListEx> vals, int chance) { source = src; values = vals; totalChance = chance; } /** * Add a probability for a specific result to be given. * * @param chance * The chance to get this result. * * @param result * The result to get when the chance comes up. */ public void addProbability(final int chance, final E result) { values.add(new SimplePair<>(chance, result)); totalChance += chance; } /** * Generate a weighted random value. * * @return A random value selected in a weighted fashion. */ public E generateValue() { return generateValue(source); } /** * Generate a random value, using the specified Random. * * @param rn * The Random instance to use. * @return A random value. */ public E generateValue(Random rn) { int target = rn.nextInt(totalChance); for (Pair val : values) { int prob = val.getLeft(); if (target < prob) { if (exhaust) { totalChance -= val.getLeft(); values.removeMatching(val); } return val.getRight(); } target -= prob; } return null; } /** * Return a list of values that can be generated by this generator * * @return A list of all the values that can be generated */ public ListEx getResults() { return values.map(Pair::getRight); } /** * Return a list containing values that can be generated paired with the * probability of those values being generated * * @return A list of pairs of values and value probabilities */ public ListEx> getValues() { return values; } /** * Get a descending value. * * Descending values are quite simple. You have a 1 in factor chance to advance * to the next value, otherwise, the current value is the one returned. * * @param factor * The descent factor to use. * @return A random value. */ public E getDescent(int factor) { return getDescent(factor, source); } /** * Get a descending value. * * Descending values are quite simple. You have a 1 in factor chance to advance * to the next value, otherwise, the current value is the one returned. * * @param factor * The descent factor to use. * @param rn * The Random instance to use. * @return A random value. */ public E getDescent(int factor, Random rn) { if (values.getSize() == 0) return null; for (Pair val : values) { if (rn.nextInt(factor) == 0) continue; if (exhaust) { totalChance -= val.getLeft(); values.removeMatching(val); } return val.getRight(); } Pair val = values.getByIndex(values.getSize() - 1); if (exhaust) values.removeMatching(val); return val.getRight(); } /** * Get a value, treating this as a set of binomial trials. * * Essentially, the system rolls a bound-sided dice trials times, and marks a * success for every roll less than or equal to target. * * @param target * The number to roll at or under. * @param bound * The maximum roll value. * @param trials * The number of times to roll. * @return The value at the index corresponding to the number of successes. */ public E getBinomial(int target, int bound, int trials) { return getBinomial(target, bound, trials, source); } /** * Get a value, treating this as a set of binomial trials. * * Essentially, the system rolls a bound-sided dice trials times, and marks a * success for every roll less than or equal to target. * * @param target * The number to roll at or under. * @param bound * The maximum roll value. * @param trials * The number of times to roll. * @param rn * The Random instance to use. * @return The value at the index corresponding to the number of successes. */ public E getBinomial(int target, int bound, int trials, Random rn) { if (values.getSize() == 0) return null; int numSuc = 0; for (int i = 0; i < trials; i++) { /* * Adjust for zero, because it's easy to think of this as rolling a * bound-sided dice and marking a success for every roll less than or equal to * target. */ int num = rn.nextInt(bound) + 1; if (num <= target) { // System.err.printf("\t\tTRACE: mark binomial success (%d <= 1d%d, // %d)\n", target, bound, num); numSuc += 1; } } // System.err.printf("\tTRACE: got %d success for binomial trials (%d <= 1d%d, // %d times)\n", numSuc, target, bound, trials); Pair val = values.getByIndex(Math.min(numSuc, values.getSize() - 1)); if (exhaust) { totalChance -= val.getLeft(); values.removeMatching(val); } return val.getRight(); } /** * Return a new WeightedRandom that is exhaustible (only returns one value * once). * * @return A new WeightedRandom that is exhaustible. */ public WeightedRandom exhaustible() { ListEx> lst = new FunctionalList<>(); for (Pair val : values) lst.add(val); WeightedRandom res = new WeightedRandom<>(source, lst, totalChance); res.exhaust = true; return res; } }