As I mentioned in my comment above, I recommend you profile this before overcomplicating your code. A quick
for loop summing dice is a lot easier to understand and modify than complicated math formulae and table-building/searching. Always profile first to make sure you're solving the important problems. ;)
That said, there are two main ways to sample sophisticated probability distributions in one fell swoop:
1. Cumulative Probability Distributions
There's a neat trick to sample from continuous probability distributions by using only a single uniform random input. It has to do with the cumulative distribution, the function that answers "What is the probability of getting a value no greater than x?"
This function is non-decreasing, starting at 0 and rising to 1 over its domain. An example for the sum of two six-sided dice is shown below:
If your cumulative distribution function has a convenient-to-calculate inverse (or you can approximate it with piecewise functions like Bézier curves), you can use this to sample from the original probability function.
The inverse function handles parcelling up the domain between 0 and 1 into intervals mapped to each output of the original random process, with the catchment area of each matching its original probability. (This is true infinitescimally for continuous distributions. For discrete distributions like dice rolls we need to apply careful rounding)
Here's an example of using this to emulate 2d6:
// Get a random input in the half-open interval [0, 1).
float t = Random.Range(0f, 1f);
// Piecewise inverse calculated by hand. ;)
if(t <= 0.5f)
v = (1f + sqrt(1f + 288f * t)) * 0.5f;
v = (25f - sqrt(289f - 288f * t)) * 0.5f;
return floor(v + 1);
Compare this to:
int NaiveRollNd6(int n)
int sum = 0;
for(int i = 0; i < n; i++)
sum += Random.Range(1, 7); // I'm used to Range never returning its max
See what I mean about the difference in code clarity and flexibility? The naive way might be naive with its loops, but it's short and simple, immediately obvious about what it does, and easy to scale to different die sizes and numbers. Making changes to the cumulative distribution code requires some non-trivial math, and it would be easy to break and cause unexpected results without any obvious mistakes. (Which I hope I haven't made above)
So, before you do away with a clear loop, make absolutely certain that it's really a performance problem worth this kind of sacrifice.
2. The Alias Method
The cumulative distribution method works well when you can express the inverse of the cumulative distribution function as a simple math expression, but that's not always easy or even possible. A reliable alternative for discrete distributions is something called the Alias Method.
This lets you sample from any arbitrary discrete probability distribution by using just two independent, uniformly distributed random inputs.
It works by taking a distribution like the one below on the left (don't worry that the areas/weights don't sum to 1, for the Alias Method we care about relative weight) and converting it to a table like the one on the right where:
- There is one column for each outcome.
- Each column is split into at most two parts, each associated with one of the original outcomes.
- The relative area/weight of each outcome is preserved.
(Diagram based on images from this excellent article on sampling methods)
In code, we represent this with two tables (or a table of objects with two properties) representing the probability of choosing the alternate outcome from each column, and the identity (or "alias") of that alternate outcome. Then we can sample from the distribution like so:
int SampleFromTables(float probabiltyTable, int aliasTable)
int column = Random.Range(0, probabilityTable.Length);
float p = Random.Range(0f, 1f);
if(p < probabilityTable[column])
This involves a bit of set-up:
Compute the relative probabilities of every possible outcome (so if you're rolling 1000d6, we need to compute the number of ways to get every sum from 1000 to 6000)
Build a pair of tables with an entry for each outcome. The full method goes beyond the scope of this answer, so I highly recommend referring to this explanation of the Alias Method algorithm.
Store those tables and refer back to them each time you need a new random die roll from this distribution.
This is a space-time tradeoff. The precomputation step is somewhat exhaustive, and we need to set aside memory proportionate to the number of outcomes we have (though even for 1000d6, we're talking single-digit kilobytes, so nothing to lose sleep over), but in exchange our sampling is constant-time no matter how complex our distribution might be.
I hope one or the other of those methods may be of some use (or that I've convinced you that the naive method's simplicity is worth the time it takes to loop) ;)