Day 12 – Mathematician’s Yahtzee

Santa was playing and losing yet another game of Yahtzee with Mrs. Claus and the elves when a thought occurred to him: why don’t you get points for rolling the first 5 digits of the Fibonacci sequence (1, 1, 2, 3, 5)? For that matter, why isn’t there a mathematician’s version of Yahtzee where you get points for rolling all even numbers, all odd, etc. After a bit of brainstorming, Santa came up with the following rules:

  • Rolling all odd numbers replaces 3 of a kind (because ‘odd’ has 3 letters).
  • Rolling all even numbers replaces 4 of a kind (because ‘even’ has 4 letters).
  • Rolling pi (a 3, 1, and 4) replaces a full house.
  • Rolling all prime numbers replaces a small straight.
  • Rolling the Fibonacci sequence (1, 1, 2, 3, 5) replaces a large straight.
  • A Yahtzee (all five of the same number) is still a Yahtzee.

Santa and the others tried an experimental game with these rules, but it was more difficult than they expected. Everyone was so used to identifying a small/large straight, full house, etc, that it was hard to break those habits and identify the new patterns. So, Santa decided to write a Raku program to help him practice.

To start with, Santa created a function that uses the roll method to simulate rolling an arbitrary number of dice, then called it with 5 dice and printed the output:

sub roll-dice($n-dice) {
    return (1..6).roll($n-dice);
}

my @dice = roll-dice(5);
print-summary(@dice);

The print-summary function will be defined later.

Next up is allowing the user to re-roll an arbitrary number of dice up to two times. The user is prompted to enter which numbers they want to re-roll, using spaces to separate each number. To stop, the user can simply press enter. Getting input from the user is done via the prompt function.

for 1..2 -> $reroll-count {
    my $answer = prompt("> Which numbers would you like to re-roll? ");
    last if !$answer;
    my @indices;
    for $answer.split(/\s+/-> $number {
        my $index = @dice.first: $number, :k;
        if !$index.defined {
            note "Could not find number $number";
            exit 1;
        }
        @indices.push: $index;
        @dice[$index]:delete;
    }
    for @indices -> $index {
        @dice[$index= roll-dice(1).first;
    }
    print-summary(@diceif $reroll-count == 1;
}

print-summary(@dice);

A few notes on the above code:

  • Once a user’s input is received, it’s split on whitespace, looping over each number.
  • The first method finds the first die with that number, while the :k adverb makes first return the index of that number (rather than the number itself).
  • If the number isn’t found, note is used to print to standard error and then the program exits.
  • The index is tracked by pushing it onto an array, and then the die value is deleted.
  • Using the delete adverb on an array creates a hole at that index. That’s okay, though, because the next loop fills it in with a new roll. Note that the roll-dice function always returns a Seq, even when rolling a single die, so the first method is used to get the first (and only) value from the sequence.

At this point Santa is halfway done. He doesn’t just want to print out the dice that were rolled, though, he also wants to identify all the new rules the dice match. To start with, identifying a Yahtzee in an array of dice is simple: just count the number of unique values. If it’s one, it’s a Yahtzee:

@dice.unique.elems == 1

For a Fibonacci sequence, Santa uses a Bag, which is a collection that keeps track of duplicate values. Two bags can be compared with the eqv (equivalence) operator. (Santa learned the hard way you can’t use == here because it turns a Bag into a Numeric, which would be the number of elements in the Bag).

@dice.Bag eqv (1, 1, 2, 3, 5).Bag

For pi, Santa brushes up on his rusty set theory. After some research, he makes use of the ⊂ (subset of)  operator, which returns true if all the left-side elements are in the right-side elements (and the right side must have more elements, which it will since there are 5 dice).

(3, 1, 4)  @dice

All prime numbers are easily identified by combining the all method with is-prime. This creates an all Junction that runs is-prime on each value and collapses into a single boolean.

@dice.all.is-prime

Something similar can be done for all even numbers, this time using the %% (divisibility) operator, which returns true if the left side is evenly divisible by the right side.

@dice.all %% 2

For all odd numbers, none is used instead of all.

@dice.none %% 2

Finally, putting it all together into a function results in this:

sub print-summary(@dice where *.elems == 5) {
    with my @summary {
        .push: 'yahtzee'   if @dice.unique.elems == 1;
        .push: 'fibonacci' if @dice.Bag eqv (1, 1, 2, 3, 5).Bag;
        .push: 'pi'        if (3, 1, 4)  @dice;
        .push: 'all-prime' if @dice.all.is-prime;
        .push: 'all-even'  if @dice.all %% 2;
        .push: 'all-odd'   if @dice.none %% 2;
    };
    my $output = @dice.join(' ');
    if ( @summary ) {
        $output ~= " (@summary.join(', '))";
    }
    say $output;
}

Note the signature to the function uses a type constraint (where *.elems == 5) to prove there are always 5 dice; an error is thrown if there are not 5 elements.

Here’s a sample run of the program:

$ raku math-yahtzee.raku
3 3 1 6 2
> Which numbers would you like to re-roll? 3 6
4 3 1 5 2 (pi)
> Which numbers would you like to re-roll? 4
1 3 1 5 2 (fibonacci)

This is exactly what Santa wanted to help him practice Mathematician’s Yahtzee! He had a lot of fun writing the program, especially the print-summary function, and is especially impressed that the program did not need to use any modules. There’s plenty more that could be done to improve this program, but Santa will stop here.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.