package mhfc.net.common.ai.general;
import java.util.Collection;
import java.util.Iterator;
import java.util.Random;
import mhfc.net.MHFCMain;
/**
* This selects a Random Item from a list of items.<br>
* This class is written to be thread-safe.<br>
* BEWARE: Re-entering any picking method (recursive calls) of this class (from the same thread) will result in a crash.
*
* @author WorldSEnder
*
*/
public class WeightedPick {
/**
* Making the whole thing relatively thread-safe. ONLY PROBLEM: re-entry will result in a crash.
*/
private static final ThreadLocal<Integer> size;
private static final ThreadLocal<WeightedItem[]> itemcache;
private static final ThreadLocal<double[]> weightcache;
private static final Random rand = new Random();
static {
size = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0x10;
}
};
itemcache = new ThreadLocal<WeightedItem[]>() {
@Override
protected WeightedItem[] initialValue() {
return new WeightedItem[size.get()];
}
};
weightcache = new ThreadLocal<double[]>() {
@Override
protected double[] initialValue() {
return new double[size.get()];
}
};
}
private static void rescale(int minSize) {
renewCache(minSize); // Maybe another size??!
}
private static void renewCache(int newSize) {
size.set(newSize);
itemcache.set(new WeightedItem[size.get()]);
weightcache.set(new double[size.get()]);
}
/**
* Randomly picks one of the items in the list if no item returns <code>true</code> for
* {@link WeightedItem#forceSelection()}. If an item does, the first one that does so will be returned.<br>
* Each item's weight is determined by {@link WeightedItem#getWeight()}. If the weight is less than zero it is
* defaulted to zero. Every items has a chance of <code>w<sub>i</sub>/sum(w)</code> to be picked. This implies that
* items that return a weight of zero will never be picked.<br>
* If and only if all items return a weight of zero or the list given is <code>null</code> this method will return
* <code>null</code>. Else the picked item is returned.<br>
* Note: This method does not guard against items that return a weight of infinity. They will be selected almost
* every time.<br>
* This implementation IS threadsafe.
*
* @param list
* the list to pick items from
* @return the picked item
*/
public static <T extends WeightedItem> T pickRandom(Collection<T> list) {
if (list == null) {
MHFCMain.logger().debug("List supplied to random pick null. Is some IAttackManager invalid?");
return null;
}
WeightedItem[] items = itemcache.get();
double[] weights = weightcache.get();
// Check cache size
int listsize = list.size();
if (listsize > size.get()) {
rescale(listsize);
}
// Collect items in cache and sum
double sum = 0.0D;
int i = 0;
for (Iterator<T> iterator = list.iterator(); iterator.hasNext();) {
T item = iterator.next();
if (item.forceSelection()) {
return item;
}
double w = Math.max(0.0d, item.getWeight());
if (w <= 0.0d)
continue;
sum += w;
items[i] = item;
weights[i] = w;
i++;
}
int index = pickRandomIndex(weights, i, sum);
if (index < 0) {
return null;
}
@SuppressWarnings("unchecked")
T t = (T) items[index];
return t;
}
private static int pickRandomIndex(double weights[], int count, double sum) {
// If no items
if (count <= 0) {
return -1;
}
// Generate selection
double value = rand.nextDouble() * sum;
sum = 0.0d;
// Select item from cache
for (; count > 0;) {
sum += weights[--count];
if (sum < value)
continue;
return count;
}
return -1;
}
public static interface WeightedItem {
/**
* Returns the (positive) weight of this item. The chance of this item being selected is
* <code>(weight/sum)</code> where <code>sum</code> is the sum of all weights of all items to be picked out of.
* <br>
* Returning a negative number or zero ensures that this item will not be selected.
*
* @return the weight to be selected
*/
public float getWeight();
/**
* Return <code>true</code> to instantly select this item before chances have been taken for other items.<br>
* This ensures that any item return <code>true</code> here will be selected if it is the first item to do so.
*
* @return <code>true</code> if this item should always be selected
*/
public boolean forceSelection();
}
}