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}