ETOOBUSY 🚀 minimal blogging for the impatient
Double check a puzzle result
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!