All Downloads are FREE. Search and download functionalities are using the official Maven repository.

squidpony.squidmath.ProbabilityTable Maven / Gradle / Ivy

Go to download

SquidLib platform-independent logic and utility code. Please refer to https://github.com/SquidPony/SquidLib .

There is a newer version: 3.0.6
Show newest version
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 - [email protected]
 *
 * @param  The type of object to be held in the table
 */
@Beta
public class ProbabilityTable 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 table;
    /**
     * The list of items that can be produced indirectly from {@link #random()} (looking up values from inside
     * the nested tables).
     */
    public final ArrayList> extraTable;
    public final IntVLA weights;
    public GWTRNG rng;
    protected int total, normalTotal;

    /**
     * Creates a new probability table with a random seed.
     */
    public ProbabilityTable() {
        this((RandomnessSource) null);
    }

    /**
     * Creates a new probability table with the provided source of randomness
     * used.
     *
     * @param rng the source of randomness
     */
    public ProbabilityTable(RandomnessSource rng) {
        this.rng = rng == null ? new GWTRNG() : new GWTRNG(rng.next(32), rng.next(32));
        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 GWTRNG(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.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 = (int) ((total * ((long)rng.next(31))) >>> 31), 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 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;
    }

    /**
     * Given an OrderedMap of T element keys and Integer weight values, adds all T keys with their corresponding weights
     * into this ProbabilityTable. You may want to use {@link OrderedMap#makeMap(Object, Object, Object...)} to produce
     * the parameter, unless you already have one.
     * @param itemsAndWeights an OrderedMap of T keys to Integer values, where a key will be an item this can retrieve
     *                        and a value will be its weight
     * @return this for chaining
     */
    public ProbabilityTable addAll(OrderedMap itemsAndWeights)
    {
        if(itemsAndWeights == null) return this;
        int sz = itemsAndWeights.size;
        for (int i = 0; i < sz; i++) {
            add(itemsAndWeights.keyAt(i), itemsAndWeights.getAt(i));
        }
        return this;
    }

    /**
     * Removes the possibility of generating the given T item, except by nested ProbabilityTable results.
     * Returns true iff the item was removed.
     * @param item the item to make less likely or impossible
     * @return true if the probability changed or false if nothing changed
     */
    public boolean remove(T item)
    {
        return remove(item, weight(item));
    }

    /**
     * Reduces the likelihood of generating the given T item by the given weight, which can reduce the chance below 0
     * and thus remove the item entirely. Does not affect nested ProbabilityTables. The value for weight must be greater
     * than 0, otherwise this simply returns false without doing anything. Returns true iff the probabilities changed.
     * @param item the item to make less likely or impossible
     * @param weight how much to reduce the item's weight by, as a positive non-zero int (greater values here subtract
     *               more from the item's weight)
     * @return true if the probability changed or false if nothing changed
     */
    public boolean remove(T item, int weight)
    {
        if(weight <= 0)
            return false;
        int idx = table.getInt(item);
        if(idx < 0)
            return false;
        int o = weights.get(idx);
        weights.incr(idx, -weight);
        int w = weights.get(idx);
        if(w <= 0)
        {
            table.removeAt(idx);
            weights.removeIndex(idx);
        }
        w = Math.min(o, o - w);
        total -= w;
        normalTotal -= w;
        return true;
    }

    /**
     * Given an Iterable of T item keys to remove, this tries to remove each item in items, though it can't affect items
     * in nested ProbabilityTables, and returns true if any probabilities were changed.
     * @param items an Iterable of T items that will all be removed from the normal (non-nested) items in this
     * @return true if the probabilities changed, or false otherwise
     */
    public boolean removeAll(Iterable items)
    {
        boolean changed = false;
        for(T t : items)
        {
            changed |= remove(t);
        }
        return changed;
    }

    /**
     * Given an OrderedMap of T item keys and Integer weight values, reduces the weights in this ProbabilityTable for
     * all T keys by their corresponding weights, removing them if the weight becomes 0 or less. You may want to use
     * {@link OrderedMap#makeMap(Object, Object, Object...)} to produce the parameter, unless you already have one.
     * Returns true iff the probabilities changed.
     * @param itemsAndWeights an OrderedMap of T keys to Integer values, where a key will be an item that should be
     *                        reduced in weight or removed and a value will be that item's weight
     * @return true if the probabilities changed or false otherwise
     */
    public boolean removeAll(OrderedMap itemsAndWeights)
    {
        if(itemsAndWeights == null) return false;
        boolean changed = false;
        int sz = itemsAndWeights.size;
        for (int i = 0; i < sz; i++) {
            changed |= remove(itemsAndWeights.keyAt(i), itemsAndWeights.getAt(i));
        }
        return changed;
    }

    /**
     * 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 add(ProbabilityTable 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;
    }

    /**
     * Given an OrderedMap of ProbabilityTable keys and Integer weight values, adds all keys as nested tables with their
     * corresponding weights into this ProbabilityTable. All ProbabilityTable keys should have the same T type as this
     * ProbabilityTable. You may want to use {@link OrderedMap#makeMap(Object, Object, Object...)} to produce the
     * parameter, unless you already have one.
     *
     * The same rules apply to this as apply to {@link #add(ProbabilityTable, int)}; that is, no key in itemsAndWeights
     * can be the same object as this ProbabilityTable, nor should any key contain cycles that could reference this
     * object from inside the values of a key. 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 a key are equivalent, it ignores that key; this also
     * happens if a key is empty or null).

     * @param itemsAndWeights an OrderedMap of T keys to Integer values, where a key will be an item this can retrieve
     *                        and a value will be its weight
     * @return this for chaining
     */
    public ProbabilityTable addAllNested(OrderedMap, Integer> itemsAndWeights)
    {
        if(itemsAndWeights == null) return this;
        int sz = itemsAndWeights.size;
        for (int i = 0; i < sz; i++) {
            add(itemsAndWeights.keyAt(i), itemsAndWeights.getAt(i));
        }
        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 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 items() {
        OrderedSet 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 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> tables() {
        return extraTable;
    }

    /**
     * Sets the current random number generator to the given GWTRNG.
     * @param random an RNG, typically with a seed you want control over; may be a StatefulRNG or some other subclass
     */
    public void setRandom(GWTRNG random)
    {
        if(random != null)
            rng = random;
    }

    /**
     * Gets the random number generator (a RandomnessSource) this uses.
     * @return the RandomnessSource used by this class, which is always a GWTRNG
     */
    public GWTRNG getRandom()
    {
        return rng;
    }

    /**
     * Copies this ProbabilityTable so nothing in the copy is shared with the original, except for the T items (which
     * might not be possible to copy). The RNG is also copied.
     * @return a copy of this ProbabilityTable; no references should be shared except for T items
     */
    public ProbabilityTable copy()
    {
        ProbabilityTable n = new ProbabilityTable<>(rng.getState());
        n.weights.addAll(weights);
        n.table.putAll(table);
        for (int i = 0; i < extraTable.size(); i++) {
            n.extraTable.add(extraTable.get(i).copy());
        }
        n.total = total;
        n.normalTotal = normalTotal;
        return n;
    }

    @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;
        return weights.equals(that.weights);
    }

    /**
     * Can avoid some checks that {@link #equals(Object)} needs because this always takes a ProbabilityTable.
     * @param o another ProbabilityTable
     * @return true if both ProbabilityTables are equivalent in contents and likelihoods, not necessarily random state
     */
    public boolean contentEquals(ProbabilityTable 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() | 0;
        result = 31 * result + weights.hashHive() | 0;
        return result;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy