I had a similar need recently making a baseball game. There were all kinds of random choices to make, so I created two utility classes to help make my code simpler.
Consider the following enumeration for the possible trajectories of a baseball when hit.
enum Trajectory {
FLY, GROUNDER, LINE_DRIVE, POPUP;
}
With my utility classes, determining a random trajectory is achieved by the following code snippet.
List<Probability<Trajectory>> odds = Arrays.asList(
Probability.of(Trajectory.FLY, 0.5),
Probability.of(Trajectory.GROUNDER, 0.2),
Probability.of(Trajectory.LINE_DRIVE, 0.3));
Trajectory result = Luck.determine(odds);
If you like this design, here are simplified but totally functional versions of the two classes involved.
Probability.java
public class Probability<T>
{
public static <T> Probability<T> of(T value, double chance) {
return new Probability<T>(value, chance);
}
private final T value;
private final double chance;
public Probability(T value, double chance) {
this.value = value;
this.chance = chance;
}
T getValue() {
return value;
}
double getChance() {
return chance;
}
@Override
public String toString() {
return new StringBuilder(20)
.append(value).append(": ").append(chance)
.toString();
}
}
Luck.java
import java.math.*;
import java.util.*;
public class Luck
{
private static final MathContext CONTEXT = new MathContext(5);
/**
* Make a random determination from a list of probabilities based
* on the fractional chance of each probability.
* @throws IllegalArgumentException if the total chance of the
* probabilities is not within 1/10000th of 1.
*/
public static <X, T extends Probability<X>> X determine(List<T> probabilities) {
double chance = 0f;
for (Probability<X> probability : probabilities) {
chance += probability.getChance();
}
BigDecimal total = new BigDecimal(chance).round(CONTEXT);
double determination = Math.random();
// System.out.println(new StringBuilder(128).append(probabilities)
// .append(" :random: ").append(determination));
if (BigDecimal.ONE.compareTo(total) != 0) {
throw new IllegalArgumentException("probabilities' chances must total 1");
}
chance = 0f;
for (Probability<X> probability : probabilities) {
chance += probability.getChance();
if (determination < chance) {
return probability.getValue();
}
}
return probabilities.get(0).getValue();
}
}
Main.java
import java.util.*;
public class Main
{
enum Trajectory {
FLY, GROUNDER, LINE_DRIVE, POPUP;
}
public static void main(String[] args) {
List<Probability<Trajectory>> odds = Arrays.asList(
Probability.of(Trajectory.FLY, 0.5),
Probability.of(Trajectory.GROUNDER, 0.2),
Probability.of(Trajectory.LINE_DRIVE, 0.3));
Trajectory result = Luck.determine(odds);
// run the odds a hundred times to see how they work out
Map<Trajectory, Integer> counts = new HashMap<Trajectory, Integer>();
for (Trajectory trajectory: Trajectory.values()) {
counts.put(trajectory, 0);
}
int i = 0;
do {
counts.put(result, counts.get(result) + 1);
result = Luck.determine(odds);
i++;
} while (i < 100);
System.out.println(counts);
}
}
Output
{FLY=50, GROUNDER=19, LINE_DRIVE=31, POPUP=0}