31 Aug 2019

The Average of 4d6 Drop lowest

The outside sky was dark; a cold wind blew in under the door, making me pull my jacket across my chest and wish that I hadn't taken my coat off when I came into my friend's house. "Roll four six sided dice and add up the highest 3. Do that 6 times and they're your stats" my friend told me. As I did what he asked I couldn't help but wonder what that was likely to give me. The average of 3d6 (three six sided dice) is \(3.5 \times 3 = 10.5\) so we settled on "Probably about 13ish?" and didn't think about it.

But that approximation never really sat right with me. The average is higher than the average of 3d6 (denoted \(\overline{3d6}\)) as the lowest result is always removed, it's also obviously lower than \(\overline{4d6}\) so our result must be \( \overline{3d6} < x < \overline{4d6}\).

I spent some time wondering if I could sample a normal distribution of dice and somehow calculate the lowest average roll of a set of 4 dice and subtract that from \(\overline{4d6}\). Then I remembered I'm not a mathematician and I didn't really understand half of the words in the last sentence. I decided to play to my strengths and take a brute force approach.

Brute Force and Ignorance

I wrote the following code that simulates 4d6 drop lowest 100,000 times and calculates the average. It can be run in the Rust playground here

use rand::Rng;

fn roll() -> u64 {
    let mut rng = rand::thread_rng();
    let mut rolls: Vec<_> = (0..4).map(|_| rng.gen_range(1, 7)).collect();
    rolls.sort();
    rolls.into_iter().skip(1).sum()
}

/// Count occurances of each roll in the results
fn count(vec: &Vec<u64>) -> Vec<(u8, usize)> {
    let mut count = [0usize; 19];

    for item in vec.iter() {
        count[*item as usize] += 1;
    }

    count
        .into_iter()
        .enumerate()
        .skip(3)
        .map(|(num, count)| (num as u8, *count))
        .collect()
}

fn main() {
    let amount = 100_000;

    let rolls: Vec<_> = (0..amount).map(|_| roll()).collect();

    let total = rolls.iter().sum::<u64>();
    let average = total as f64 / amount as f64;

    println!("Average {}", average);
    for (no, count) in count(&rolls) {
        println!("{}:{}", no, count);
    }
}

Average 12.23731
3:64
4:334
5:810
6:1629
7:2948
8:4777
9:7017
10:9495
11:11410
12:12835
13:13322
14:12158
15:10122
16:7270
17:4216
18:1593

According to this the average is just over 12 with the most common result being 13.

The Hard Way

total rolls = 6 ^ 4 = 1296