package squidpony.squidmath; import squidpony.annotation.Beta; import java.io.Serializable; import java.util.ArrayList; import java.util.SortedSet; /** * A generic method of holding a probability table to determine weighted random * outcomes. * * The weights do not need to add up to any particular value, they will be * normalized when choosing a random entry. * * @author Eben Howard - http://squidpony.com - howard@squidpony.com * * @param <T> The type of object to be held in the table */ @Beta public class ProbabilityTable<T> implements Serializable { private static final long serialVersionUID = -1307656083434154736L; /** * The set of items that can be produced directly from {@link #random()} (without additional lookups). */ public final Arrangement<T> table; /** * The list of items that can be produced indirectly from {@link #random()} (looking up values from inside * the nested tables). */ public final ArrayList<ProbabilityTable<T>> extraTable; public final IntVLA weights; public RNG rng; protected int total, normalTotal; /** * Creates a new probability table with a random seed. */ public ProbabilityTable() { this(new StatefulRNG()); } /** * Creates a new probability table with the provided source of randomness * used. Gets one random long from rng to use as an internal identifier. * * @param rng the source of randomness */ public ProbabilityTable(RNG rng) { this.rng = rng; table = new Arrangement<>(64, 0.75f); extraTable = new ArrayList<>(16); weights = new IntVLA(64); total = 0; normalTotal = 0; } /** * Creates a new probability table with the provided long seed used. * * @param seed the RNG seed as a long */ public ProbabilityTable(long seed) { this.rng = new StatefulRNG(seed); table = new Arrangement<>(64, 0.75f); extraTable = new ArrayList<>(16); weights = new IntVLA(64); total = 0; normalTotal = 0; } /** * Creates a new probability table with the provided String seed used. * * @param seed the RNG seed as a String */ public ProbabilityTable(String seed) { this(CrossHash.Wisp.hash64(seed)); } /** * Returns an object randomly based on assigned weights. * * Returns null if no elements have been put in the table. * * @return the chosen object or null */ public T random() { if (table.isEmpty()) { return null; } int index = rng.nextInt(total), sz = table.size(); for (int i = 0; i < sz; i++) { index -= weights.get(i); if (index < 0) return table.keyAt(i); } for (int i = 0; i < extraTable.size(); i++) { index -= weights.get(sz + i); if(index < 0) return extraTable.get(i).random(); } return null;//something went wrong, shouldn't have been able to get all the way through without finding an item } /** * Adds the given item to the table. * * Weight must be greater than 0. * * @param item the object to be added * @param weight the weight to be given to the added object * @return this for chaining */ public ProbabilityTable<T> add(T item, int weight) { if(weight <= 0) return this; int i = table.getInt(item); if (i < 0) { weights.insert(table.size, Math.max(0, weight)); table.add(item); int w = Math.max(0, weight); total += w; normalTotal += w; } else { int i2 = weights.get(i); int w = Math.max(0, i2 + weight); weights.set(i, w); total += w - i2; normalTotal += w - i2; } return this; } /** * Adds the given probability table as a possible set of results for this table. * The table parameter should not be the same object as this ProbabilityTable, nor should it contain cycles * that could reference this object from inside the values of table. This could cause serious issues that would * eventually terminate in a StackOverflowError if the cycles randomly repeated for too long. Only the first case * is checked for (if the contents of this and table are equivalent, it returns without doing anything; this also * happens if table is empty or null). * * Weight must be greater than 0. * * @param table the ProbabilityTable to be added; should not be the same as this object (avoid cycles) * @param weight the weight to be given to the added table * @return this for chaining */ public ProbabilityTable<T> add(ProbabilityTable<T> table, int weight) { if(weight <= 0 || table == null || contentEquals(table) || table.total <= 0) return this; weights.add(Math.max(0, weight)); extraTable.add(table); total += Math.max(0, weight); return this; } /** * Returns the weight of the item if the item is in the table. Returns zero * if the item is not in the table. * * @param item the item searched for * @return the weight of the item, or zero */ public int weight(T item) { int i = table.getInt(item); return i < 0 ? 0 : weights.get(i); } /** * Returns the weight of the extra table if present. Returns zero * if the extra table is not present. * * @param item the extra ProbabilityTable to search for * @return the weight of the ProbabilityTable, or zero */ public int weight(ProbabilityTable<T> item) { int i = extraTable.indexOf(item); return i < 0 ? 0 : weights.get(i + table.size()); } /** * Provides a set of the items in this table, without reference to their * weight. Includes nested ProbabilityTable values, but as is the case throughout * this class, cyclical references to ProbabilityTable values that reference this * table will result in significant issues (such as a {@link StackOverflowError} * crashing your program). * * @return an OrderedSet of all items stored; iteration order should be predictable */ public OrderedSet<T> items() { OrderedSet<T> os = table.keysAsOrderedSet(); for (int i = 0; i < extraTable.size(); i++) { os.addAll(extraTable.get(i).items()); } return os; } /** * Provides a set of the items in this table that are not in nested tables, without * reference to their weight. These are the items that are simple to access, hence * the name. If you want the items that are in both the top-level and nested tables, * you can use {@link #items()}. * @return a predictably-ordered set of the items in the top-level table */ public SortedSet<T> simpleItems() { return table.keySet(); } /** * Provides a set of the nested ProbabilityTable values in this table, without reference * to their weight. Does not include normal values (non-table); for that, use items(). * * @return a "sorted" set of all nested tables stored, really sorted in insertion order */ public ArrayList<ProbabilityTable<T>> tables() { return extraTable; } /** * Sets the current RNG to the given RNG. You may prefer using a StatefulRNG (typically passing one in the * constructor, but you can pass one here too) and setting its state in other code, which does not require calling * this method again when the StatefulRNG has its state set. * @param random an RNG, typically with a seed you want control over; may be a StatefulRNG or some other subclass */ public void setRandom(RNG random) { if(random != null) rng = random; } /** * Gets the RNG this uses. * @return the RNG used by this class, which is often (but not always) a StatefulRNG */ public RNG getRandom() { return rng; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProbabilityTable<?> that = (ProbabilityTable<?>) o; if (!table.equals(that.table)) return false; if (!extraTable.equals(that.extraTable)) return false; if (!weights.equals(that.weights)) return false; return rng != null ? rng.equals(that.rng) : that.rng == null; } public boolean contentEquals(ProbabilityTable<T> o) { if (this == o) return true; if (o == null) return false; if (!table.equals(o.table)) return false; if (!extraTable.equals(o.extraTable)) return false; return weights.equals(o.weights); } @Override public int hashCode() { int result = table.hashCode(); result = 31 * result + extraTable.hashCode(); result = 31 * result + weights.hashCode(); result = 31 * result + (rng != null ? rng.hashCode() : 0); return result; } }