TL;DR

I double checked a puzzle (theoretical) solution with some simulation in Raku.

I have a free account on Brilliant, which means I can enjoy a little puzzle a day from them.

These puzzles are normally meant to be solved theoretically, that is by thinking a bit on the problem and solving it e.g. with a couple of formulas.

One recent puzzle was a interesting question about a game similar to this:

Roll a six-sided die and accumulate as many points as the outcome. If the outcome is greater than 2, repeat to gain more points; if the outcome is 1 or 2 stop.

What is the expected value of the points accumulated?

Assuming a fair six-sided die, each face value has probability $\frac{1}{6}$ of coming out, so the expected value for a single round is:

$E_1 = \frac{1}{6} (1 + 2 + 3 + 4 + 5 + 6) = \frac{1}{6} \frac{7 \cdot 6}{2} = \frac{7}{2} = 3.5$

Then, of course, we have to consider that we would have to go on if the outcome is greater than $2$. If we call our target, unknown expected value $E$, this additional component that we have from continuing will have to be considered only $4$ times out of $6$, i.e. only when the outcome is one of $3$, $4$, $5$, or $6$. It contribution, then, will be $\frac{4}{6} E = \frac{2}{3} E$.

Overall, then, our $E$ will be formed by the outcome of a single roll $E_1$ and this additional component, i.e.:

$E = E_1 + \frac{2}{3} E = 3.5 + \frac{2}{3} E$

We can now solve for $E$:

$E - \frac{2}{3} E = 3.5 \\ \frac{1}{3} E = 3.5 \\ E = 10.5$

On average, then, our score will be 10.5 points.

Did I get the calculations right? Letâ€™s set up a simulation and double check!

In pure bottom-up style, letâ€™s start defining one full round of the game:

sub simulation-round () {
return [+] gather {
loop {
my $value = roll-die(); take$value;
last if $value < 3; } } }  The loop goes on indefinitely, although it has a positive and finite probability of being interrupted thanks to statement last if$value < 3, that is exactly our exit condition for a round of the game.

We use gather/take to get the outcome of each roll of the die; as our score is actually the sum of all these outcomes, we use the [+] reduction operator to obtain this sum.

Now, to estimate the expected value we want to double check, we can set up a lot of simulation rounds and take the average of the outcomes:

my $N = @*ARGS.shift || 100; my$total = [+] gather { take simulation-round() for 1 .. $N }; put 'average gain: ',$total / $N;  Again, we calculate the total sum of all the $N outcomes using [+], applied to a gather/take pair that operates on whole simulation rounds this time. At this point, the average is calculated by dividing this $total by the number of rounds $N that we did.

The die rolling will be done a bit crudely, leveraging the stock rand facility. Iâ€™m not sure about its statistical characteristics, but for our simulation it will do:

sub roll-die (Int:D $sides where * > 0 = 6) { (1 ..$sides).pick }


Iâ€™m also not entirely convinced that putting the default value after the condition makes this entirely readable, but itâ€™s life.

Letâ€™s run this a few time, by averaging over 10000 rounds each time:

$for i in 1 2 3 4 5 ; do raku multiple-rolls.raku 10000 ; done average gain: 10.5096 average gain: 10.4038 average gain: 10.3704 average gain: 10.3157 average gain: 10.6384  It seems pretty consistent with the theoretical value of$10.5\$ that we calculated above, yay!

If you want to play with the code, there is a local copy here.

Have fun and stay safe!

Comments? Octodon, , GitHub, Reddit, or drop me a line!