001package squidpony.squidmath;
002
003import java.io.Serializable;
004import java.util.ArrayList;
005import java.util.SortedSet;
006
007/**
008 * A generic method of holding a probability table to determine weighted random
009 * outcomes.
010 *
011 * The weights do not need to add up to any particular value; they will be
012 * normalized when choosing a random entry. This class allows adding {@code T} items and the weights for
013 * those items after the ProbabilityTable has been constructed with {@link #add(Object, int)} or
014 * {@link #addAll(OrderedMap)}, as well as removing items entirely with {@link #remove(Object)} or
015 * adjusting the weight for an existing item with {@link #add(Object, int)} or {@link #remove(Object, int)}.
016 * You can also add a nested ProbabilityTable, which has its own weight and can be chosen like any other
017 * item, except it makes its own random choice of its own {@code T} items; you can use the nested table
018 * with {@link #add(ProbabilityTable, int)} and {@link #addAllNested(OrderedMap)}. Actually getting a
019 * randomly-selected item is easy; just use {@link #random()}.
020 * 
021 * @see WeightedTable An alternative for when you want to track the items separately from their weights.
022 * 
023 * @author Eben Howard - http://squidpony.com - howard@squidpony.com
024 * 
025 * @param <T> The type of object to be held in the table
026 */
027public class ProbabilityTable<T> implements Serializable {
028    private static final long serialVersionUID = -1307656083434154736L;
029    /**
030     * The set of items that can be produced directly from {@link #random()} (without additional lookups).
031     */
032    public final Arrangement<T> table;
033    /**
034     * The list of items that can be produced indirectly from {@link #random()} (looking up values from inside
035     * the nested tables).
036     */
037    public final ArrayList<ProbabilityTable<T>> extraTable;
038    public final IntVLA weights;
039    public GWTRNG rng;
040    protected int total, normalTotal;
041
042    /**
043     * Creates a new probability table with a random seed.
044     */
045    public ProbabilityTable() {
046        this((RandomnessSource) null);
047    }
048
049    /**
050     * Creates a new probability table with the provided source of randomness
051     * used.
052     *
053     * @param rng the source of randomness
054     */
055    public ProbabilityTable(RandomnessSource rng) {
056        this.rng = rng == null ? new GWTRNG() : new GWTRNG(rng.next(32), rng.next(32));
057        table = new Arrangement<>(64, 0.75f);
058        extraTable = new ArrayList<>(16);
059        weights = new IntVLA(64);
060        total = 0;
061        normalTotal = 0;
062    }
063
064    /**
065     * Creates a new probability table with the provided long seed used.
066     *
067     * @param seed the RNG seed as a long
068     */
069    public ProbabilityTable(long seed) {
070        this.rng = new GWTRNG(seed);
071        table = new Arrangement<>(64, 0.75f);
072        extraTable = new ArrayList<>(16);
073        weights = new IntVLA(64);
074        total = 0;
075        normalTotal = 0;
076    }
077
078    /**
079     * Creates a new probability table with the provided String seed used.
080     *
081     * @param seed the RNG seed as a String
082     */
083    public ProbabilityTable(String seed) {
084        this(CrossHash.hash64(seed));
085    }
086
087    /**
088     * Returns an object randomly based on assigned weights.
089     *
090     * Returns null if no elements have been put in the table.
091     *
092     * @return the chosen object or null
093     */
094    public T random() {
095        if (table.isEmpty()) {
096            return null;
097        }
098        int index = (int) ((total * ((long)rng.next(31))) >>> 31), sz = table.size();
099        for (int i = 0; i < sz; i++) {
100            index -= weights.get(i);
101            if (index < 0)
102                return table.keyAt(i);
103        }
104        for (int i = 0; i < extraTable.size(); i++) {
105            index -= weights.get(sz + i);
106            if(index < 0)
107                return extraTable.get(i).random();
108        }
109        return null;//something went wrong, shouldn't have been able to get all the way through without finding an item
110    }
111
112    /**
113     * Adds the given item to the table.
114     *
115     * Weight must be greater than 0.
116     *
117     * @param item the object to be added
118     * @param weight the weight to be given to the added object
119     * @return this for chaining
120     */
121    public ProbabilityTable<T> add(T item, int weight) {
122        if(weight <= 0)
123            return this;
124        int i = table.getInt(item);
125        if (i < 0) {
126            weights.insert(table.size, weight);
127            table.add(item);
128            int w = weight;
129            total += w;
130            normalTotal += w;
131        } else {
132            int i2 = weights.get(i);
133            int w = Math.max(0, i2 + weight);
134            weights.set(i, w);
135            total += w - i2;
136            normalTotal += w - i2;
137        }
138        return this;
139    }
140
141    /**
142     * Given an OrderedMap of T element keys and Integer weight values, adds all T keys with their corresponding weights
143     * into this ProbabilityTable. You may want to use {@link OrderedMap#makeMap(Object, Object, Object...)} to produce
144     * the parameter, unless you already have one.
145     * @param itemsAndWeights an OrderedMap of T keys to Integer values, where a key will be an item this can retrieve
146     *                        and a value will be its weight
147     * @return this for chaining
148     */
149    public ProbabilityTable<T> addAll(OrderedMap<T, Integer> itemsAndWeights)
150    {
151        if(itemsAndWeights == null) return this;
152        int sz = itemsAndWeights.size;
153        for (int i = 0; i < sz; i++) {
154            add(itemsAndWeights.keyAt(i), itemsAndWeights.getAt(i));
155        }
156        return this;
157    }
158
159    /**
160     * Removes the possibility of generating the given T item, except by nested ProbabilityTable results.
161     * Returns true iff the item was removed.
162     * @param item the item to make less likely or impossible
163     * @return true if the probability changed or false if nothing changed
164     */
165    public boolean remove(T item)
166    {
167        return remove(item, weight(item));
168    }
169
170    /**
171     * Reduces the likelihood of generating the given T item by the given weight, which can reduce the chance below 0
172     * and thus remove the item entirely. Does not affect nested ProbabilityTables. The value for weight must be greater
173     * than 0, otherwise this simply returns false without doing anything. Returns true iff the probabilities changed.
174     * @param item the item to make less likely or impossible
175     * @param weight how much to reduce the item's weight by, as a positive non-zero int (greater values here subtract
176     *               more from the item's weight)
177     * @return true if the probability changed or false if nothing changed
178     */
179    public boolean remove(T item, int weight)
180    {
181        if(weight <= 0)
182            return false;
183        int idx = table.getInt(item);
184        if(idx < 0)
185            return false;
186        int o = weights.get(idx);
187        weights.incr(idx, -weight);
188        int w = weights.get(idx);
189        if(w <= 0)
190        {
191            table.removeAt(idx);
192            weights.removeIndex(idx);
193        }
194        w = Math.min(o, o - w);
195        total -= w;
196        normalTotal -= w;
197        return true;
198    }
199
200    /**
201     * Given an Iterable of T item keys to remove, this tries to remove each item in items, though it can't affect items
202     * in nested ProbabilityTables, and returns true if any probabilities were changed.
203     * @param items an Iterable of T items that will all be removed from the normal (non-nested) items in this
204     * @return true if the probabilities changed, or false otherwise
205     */
206    public boolean removeAll(Iterable<T> items)
207    {
208        boolean changed = false;
209        for(T t : items)
210        {
211            changed |= remove(t);
212        }
213        return changed;
214    }
215
216    /**
217     * Given an OrderedMap of T item keys and Integer weight values, reduces the weights in this ProbabilityTable for
218     * all T keys by their corresponding weights, removing them if the weight becomes 0 or less. You may want to use
219     * {@link OrderedMap#makeMap(Object, Object, Object...)} to produce the parameter, unless you already have one.
220     * Returns true iff the probabilities changed.
221     * @param itemsAndWeights an OrderedMap of T keys to Integer values, where a key will be an item that should be
222     *                        reduced in weight or removed and a value will be that item's weight
223     * @return true if the probabilities changed or false otherwise
224     */
225    public boolean removeAll(OrderedMap<T, Integer> itemsAndWeights)
226    {
227        if(itemsAndWeights == null) return false;
228        boolean changed = false;
229        int sz = itemsAndWeights.size;
230        for (int i = 0; i < sz; i++) {
231            changed |= remove(itemsAndWeights.keyAt(i), itemsAndWeights.getAt(i));
232        }
233        return changed;
234    }
235
236    /**
237     * Adds the given probability table as a possible set of results for this table.
238     * The table parameter should not be the same object as this ProbabilityTable, nor should it contain cycles
239     * that could reference this object from inside the values of table. This could cause serious issues that would
240     * eventually terminate in a StackOverflowError if the cycles randomly repeated for too long. Only the first case
241     * is checked for (if the contents of this and table are equivalent, it returns without doing anything; this also
242     * happens if table is empty or null).
243     *
244     * Weight must be greater than 0.
245     *
246     * @param table the ProbabilityTable to be added; should not be the same as this object (avoid cycles)
247     * @param weight the weight to be given to the added table
248     * @return this for chaining
249     */
250    public ProbabilityTable<T> add(ProbabilityTable<T> table, int weight) {
251        if(weight <= 0 || table == null || contentEquals(table) || table.total <= 0)
252            return this;
253        weights.add(weight);
254        extraTable.add(table);
255        total += weight;
256        return this;
257    }
258
259    /**
260     * Given an OrderedMap of ProbabilityTable keys and Integer weight values, adds all keys as nested tables with their
261     * corresponding weights into this ProbabilityTable. All ProbabilityTable keys should have the same T type as this
262     * ProbabilityTable. You may want to use {@link OrderedMap#makeMap(Object, Object, Object...)} to produce the
263     * parameter, unless you already have one.
264     *
265     * The same rules apply to this as apply to {@link #add(ProbabilityTable, int)}; that is, no key in itemsAndWeights
266     * can be the same object as this ProbabilityTable, nor should any key contain cycles that could reference this
267     * object from inside the values of a key. This could cause serious issues that would eventually terminate in a
268     * StackOverflowError if the cycles randomly repeated for too long. Only the first case is checked for (if the
269     * contents of this and a key are equivalent, it ignores that key; this also
270     * happens if a key is empty or null).
271
272     * @param itemsAndWeights an OrderedMap of T keys to Integer values, where a key will be an item this can retrieve
273     *                        and a value will be its weight
274     * @return this for chaining
275     */
276    public ProbabilityTable<T> addAllNested(OrderedMap<ProbabilityTable<T>, Integer> itemsAndWeights)
277    {
278        if(itemsAndWeights == null) return this;
279        int sz = itemsAndWeights.size;
280        for (int i = 0; i < sz; i++) {
281            add(itemsAndWeights.keyAt(i), itemsAndWeights.getAt(i));
282        }
283        return this;
284    }
285
286    /**
287     * Returns the weight of the item if the item is in the table. Returns zero
288     * if the item is not in the table.
289     *
290     * @param item the item searched for
291     * @return the weight of the item, or zero
292     */
293    public int weight(T item) {
294        int i = table.getInt(item);
295        return i < 0 ? 0 : weights.get(i);
296    }
297
298    /**
299     * Returns the weight of the extra table if present. Returns zero
300     * if the extra table is not present.
301     *
302     * @param item the extra ProbabilityTable to search for
303     * @return the weight of the ProbabilityTable, or zero
304     */
305    public int weight(ProbabilityTable<T> item) {
306        int i = extraTable.indexOf(item);
307        return i < 0 ? 0 : weights.get(i + table.size());
308    }
309
310    /**
311     * Provides a set of the items in this table, without reference to their
312     * weight. Includes nested ProbabilityTable values, but as is the case throughout
313     * this class, cyclical references to ProbabilityTable values that reference this
314     * table will result in significant issues (such as a {@link StackOverflowError}
315     * crashing your program).
316     *
317     * @return an OrderedSet of all items stored; iteration order should be predictable
318     */
319    public OrderedSet<T> items() {
320        OrderedSet<T> os = table.keysAsOrderedSet();
321        for (int i = 0; i < extraTable.size(); i++) {
322            os.addAll(extraTable.get(i).items());
323        }
324        return os;
325    }
326
327    /**
328     * Provides a set of the items in this table that are not in nested tables, without
329     * reference to their weight. These are the items that are simple to access, hence
330     * the name. If you want the items that are in both the top-level and nested tables,
331     * you can use {@link #items()}.
332     * @return a predictably-ordered set of the items in the top-level table
333     */
334    public SortedSet<T> simpleItems()
335    {
336        return table.keySet();
337    }
338
339    /**
340     * Provides a set of the nested ProbabilityTable values in this table, without reference
341     * to their weight. Does not include normal values (non-table); for that, use items().
342     *
343     * @return a "sorted" set of all nested tables stored, really sorted in insertion order
344     */
345    public ArrayList<ProbabilityTable<T>> tables() {
346        return extraTable;
347    }
348
349    /**
350     * Sets the current random number generator to the given GWTRNG.
351     * @param random an RNG, typically with a seed you want control over; may be a StatefulRNG or some other subclass
352     */
353    public void setRandom(GWTRNG random)
354    {
355        if(random != null)
356            rng = random;
357    }
358
359    /**
360     * Gets the random number generator (a RandomnessSource) this uses.
361     * @return the RandomnessSource used by this class, which is always a GWTRNG
362     */
363    public GWTRNG getRandom()
364    {
365        return rng;
366    }
367
368    /**
369     * Copies this ProbabilityTable so nothing in the copy is shared with the original, except for the T items (which
370     * might not be possible to copy). The RNG is also copied.
371     * @return a copy of this ProbabilityTable; no references should be shared except for T items
372     */
373    public ProbabilityTable<T> copy()
374    {
375        ProbabilityTable<T> n = new ProbabilityTable<>(rng.getState());
376        n.weights.addAll(weights);
377        n.table.putAll(table);
378        for (int i = 0; i < extraTable.size(); i++) {
379            n.extraTable.add(extraTable.get(i).copy());
380        }
381        n.total = total;
382        n.normalTotal = normalTotal;
383        return n;
384    }
385
386    @Override
387    public boolean equals(Object o) {
388        if (this == o) return true;
389        if (o == null || getClass() != o.getClass()) return false;
390
391        ProbabilityTable<?> that = (ProbabilityTable<?>) o;
392
393        if (!table.equals(that.table)) return false;
394        if (!extraTable.equals(that.extraTable)) return false;
395        return weights.equals(that.weights);
396    }
397
398    /**
399     * Can avoid some checks that {@link #equals(Object)} needs because this always takes a ProbabilityTable.
400     * @param o another ProbabilityTable
401     * @return true if both ProbabilityTables are equivalent in contents and likelihoods, not necessarily random state
402     */
403    public boolean contentEquals(ProbabilityTable<T> o) {
404        if (this == o) return true;
405        if (o == null) return false;
406
407        if (!table.equals(o.table)) return false;
408        if (!extraTable.equals(o.extraTable)) return false;
409        return weights.equals(o.weights);
410    }
411
412    @Override
413    public int hashCode() {
414        int result = table.hashCode();
415        result = 31 * result + extraTable.hashCode() | 0;
416        result = 31 * result + weights.hashHive() | 0;
417        return result;
418    }
419}