[RPG] How does one calculate the distribution of the Matt Colville way of rolling stats


Specifically, the Matt Colville way of rolling stats is:

  1. Roll 4d6, drop the lowest value die for 1 stat;
  2. If this roll is lower than 8, reroll it;
  3. Repeat steps 1 and 2 until you have a set of 6 stats greater than 8;
  4. If there are not at least 2 values of 15 or higher in this set, drop it entirely and start over.

I've written some AnyDice code for calculating this process's distribution but I got stuck at this:

function: ROLL:n reroll BAD:s as REROLL:d {
  if ROLL = BAD { result: REROLL }
  result: ROLL
function: ROLL:d reroll BAD:s {
  loop I over {1..20} {
    ROLL: [ROLL reroll BAD as ROLL]
  result: ROLL
X: [highest 3 of 4d6]
Y: 6 d[dX reroll {3..7}]
loop P over {1..6} {
 output P @ Y named "Ability [P]"

This gives me the probabilities for all my abilities individually, but does not take into account the discarding of the set if there are not at least 2 15s. How should I make it take that into account? (Or how do I calculate this distribution in another way?)

Best Answer

The following anydice program will show you what the statistical distribution of ability score results for the Colville method looks like.

function: roll ROLL:n min MIN:n{
 if ROLL < MIN { result: d{} }
 result: ROLL

function: colville ARRAY:s INDEX:n {
  if (ARRAY >= 15) < 2 { result: d{} }
  result: INDEX@ARRAY

ROLL: [highest 3 of 4d6]
SCORE: [roll ROLL min 8]

output [colville ARRAY 1] named "Score 1"
output [colville ARRAY 2] named "Score 2"
output [colville ARRAY 3] named "Score 3"
output [colville ARRAY 4] named "Score 4"
output [colville ARRAY 5] named "Score 5"
output [colville ARRAY 6] named "Score 6"

The trick here is that we don't actually want to have to reroll anything, because recursive functions are expensive and take forever (plus there's a limit to how far Anydice will recurse). Fortunately we actually have a really neat shortcut we can use in the specific case of rerolling until we get a result that's in the range we actually want; we can use a function as a filter to check the value is in the desired range, which returns the input value if it is, or the so-called empty die, d{}, if it is not.

The result of the empty die is basically discarded when anydice calculates probabilities, so we are shown results based only on rolls which met our parameters; since we were just going to reroll anyway until we got a result that was in our range, this is statistically identical to actually rerolling (potentially forever).

So we have two functions, one of which discards results for individual ability scores unless they are 8 or higher, one of which discards arrays of ability scores if there aren't two scores of 15 or more.

The other trick is that the latter function also takes an index to return one of those ability scores since unfortunately we can't get anydice to return a sequence from a function, only a flat number, and we so have to use the index to inspect the individual rolls; fortunately the generated sequence is automatically sorted in descending order by default, so we can just iterate through each position to build a complete distribution.

That gives us a result that looks like this when graphed:

Colville stat distribution graph from anydice

This seems to agree perfectly with Ryan Thompson's R-based answer so I feel pretty confident I haven't messed up how this works anywhere.