Monday, December 21, 2009

Dice Roller Deconstructed

As promised, here are the elements of last week's dice rolling code:

use v6;
This is a nice way to say that we are in Perl 6 land.

subset D10 of Int where 1..10;
A "D10" is a 10-sided die, and it can only have integer values in the range 1..10. Subtyping Int is an acceptable way of taking care of that.

sub is_success (D10 $roll, D10 $target) {
Here, I am already using the subtype D10 of Int. This subroutine compares the rolled die $roll with the target number $target, and is called from the subroutine roll() for each die in the dice pool. I chose to create an explicit subroutine because it seems a bit clearer what happens in the special case of a rolled 10, which means that you get to re-roll that die for a potential new success.

    my $n = 0;
if ($roll == 10) {
say "10 again";
$n += roll 1,$target;
}
If we roll a 10, then the roll() subroutine is called with a dice pool of 1 and the same target number as we got originally for determining success.

    $roll >= $target ?? $n + 1 !! $n;
}
We always return the number of successes from the roll for the "10 again" rule (if it happened), and in case this roll was a success, we return an additional success.

sub roll (Int $poolsize where { $_ > 0 }, D10 $target? = 8) {
The dice pool size can of course not be negative, but it also cannot be zero; you always get to roll a die, so I have added a type constraint for that. The target number is optional, defaults to 8, and has to be possible with a D10.

    my D10 @rolls = (1..10).pick($poolsize, :replace);
From left to right:
  • @rolls is an array that will contain the results of the normal die rolls
  • (1..10).pick($poolsize is a way of picking $poolsize dice having possible values in the range 1..10 and "rolling" (randomizing) each of them.
  • pick($poolsize, :replace) means that we not only pick a result, but we also make it possible to achieve the same result again. Specifically, it is important for us that each die can have ANY value, not just values that have not been picked before. The semantics of pick() are explained in .pick your game (the 15th gift in the Perl 6 Advent Calendar).


    say "Roll: " ~ @rolls.sort.join(",");
@rolls.sort.join(",") sorts the elements of the @rolls array and stringifies them joined with a comma, e.g. "1,2,3,3,4" for @rolls = 4,1,3,2,3

    [+] @rolls.map: { is_success $_,$target };
}
This piece of code maps is_success $_,$target on every value in the @rolls array and creates a sum of those results. In other words, it sums up the number of successfull die rolls.

given @*ARGS.elems {
The @*ARGS array contains the command line arguments to the program, and .elems therefore is the number of arguments used.
    when 2   {
say "Target number: " ~ @*ARGS[1];
continue;
}
This block only runs in case we have two arguments, but it explicitly says that we may not be done yet: the continue statement counters the default implisit break to ensure that we can match the input value against other tests.
    when 1|2 {
my $n = roll |@*ARGS>>.Int;
say "Successes rolled: " ~ $n;
$n >= 5 and say "Exceptional success!";
}
We start off with a junction to say that either 1 or 2 is fine by us, we want both to match. Then we call roll() with the same arguments we got in, but each converted to Int. White magic. We store the value, and exclaim that the result is an exceptional success if it is.
    when *   {
$*ERR.say("roll.p6 poolsize [target]");
exit(64);
}
}
This is the equivalent of C's default, the catch-all that handles remaining uncaught cases. We print a helpful usage string to STDERR ($*ERR in Perl 6) and exit with the correct Unix exit code, praying that nobody uses a different kind of system.

No comments: