File Coverage

blib/lib/Games/Dice/Probability.pm
Criterion Covered Total %
statement 92 127 72.4
branch 42 70 60.0
condition 5 13 38.4
subroutine 14 17 82.3
pod 0 10 0.0
total 153 237 64.5


line stmt bran cond sub pod time code
1             package Games::Dice::Probability;
2              
3 29     29   669610 use 5.006;
  29         196  
  29         1316  
4              
5             # Be a Good Module
6 29     29   179 use strict;
  29         61  
  29         990  
7 29     29   805 use warnings;
  29         64  
  29         1614  
8             #use diagnostics;
9              
10             # Our version number.
11             our $VERSION = '0.02';
12              
13             # Required Modules
14             # Math::Sumbolic::AuxFunctions is for the calcs of binomial coefficients.
15             #
16             # Parse::RecDescent parses the dice expressions.
17 29     29   32161 use Math::Symbolic::AuxFunctions;
  29         2863530  
  29         1133  
18 29     29   58556 use Parse::RecDescent;
  29         30093252  
  29         296  
19              
20             # Optional Modules
21             # Debug::ShowStuff is used by $self->debug() to descend and display hashes
22             # in the object.
23             #
24             # Memoize is used by $self->new() to speed up calculations on individual
25             # nodes in the expression tree. If there are duplicate nodes, the second
26             # and all subsequent calc_distribution calls with the same parameters will
27             # just return the cached values.
28             #
29             # Thanks to Mark Mills for the optional-module code snippet, and general
30             # ideas, tips, and tricks for the whole module. He's my local Perl
31             # Monk...you can email him thanks and offers of cookies at:
32             # extremely{plus}pm{at}hostile{dot}org
33             BEGIN {
34             # Is Debug::ShowStuff available?
35 29 50   29   5194 if ( eval q/ require Debug::ShowStuff / ) {
36             # If so, import the routines.
37 0         0 Debug::ShowStuff->import("showref");
38             } else {
39             # Not available. Place stub in its place.
40 29     0 0 1557 eval q/ sub showref { print @_; print "\n"; } /;
  0         0  
  0         0  
41             }
42              
43             # Is Memoize available?
44 29 50       1722 if ( eval q/ require Memoize / ) {
45             # If so, import the routines.
46 29         93772 Memoize->import();
47             } else {
48             # Not available. Place stub in its place.
49 0         0 eval q/ sub memoize { return; } /;
50             }
51             }
52              
53             # Binomial Coefficient Shortcut
54             # Non-polluted namespaces are cool and all, but sometimes it's too much to
55             # type a 44 character long subroutine name multiple times in a simple
56             # equation. Sheesh.
57             my $binco = \&Math::Symbolic::AuxFunctions::binomial_coeff;
58              
59             # Recursive Parsing Grammar
60             # Parsing grammar and tree code outright stolen from Sam Holden's
61             # DiceDistribution.pm at http://sam.holden.id.au/junk/DICEDISTRIBUTION/.
62             # Added a divide expression for division calculations. Changed the way
63             # Fudge Dice are expressed to be #d[fF] which is more inline with how
64             # they are represented elsewhere. Added mid# dice expression as part
65             # of dicenode.
66             my $DiceGrammar = <<'END_GRAMMAR';
67             expression: add_sub end { $item[1] }
68             add_sub: mult_div '+' add_sub { { left => $item[1], op => '+', right => $item[3] } }
69             add_sub: mult_div '-' add_sub { { left => $item[1], op => '-', right => $item[3] } }
70             add_sub: mult_div
71             mult_div: bracket '/' mult_div { { left => $item[1], op => '/', right => $item[3] } }
72             mult_div: bracket '*' mult_div { { left => $item[1], op => '*', right => $item[3] } }
73             mult_div: bracket
74             bracket: '(' add_sub ')' { $item[2] }
75             bracket: dicenode
76             dicenode: /(\d+|mi)d(\d+|f)/i
77             dicenode: /\d+/
78             end: /\s*$/
79             END_GRAMMAR
80              
81             # Dice Parsing Object
82             my $DiceParser = Parse::RecDescent->new($DiceGrammar) || die("bad grammar");
83              
84             # import()
85             # Faux import function that either memoizes the calc portions of the
86             # module (default) or doesn't.
87             sub import {
88             # Default is to memoize. Saving cycles is a Good Thing. However, if
89             # someone passes an unmemoize argument, then we will respect their wishes.
90 29 50   29   983 if ( ! grep(/(un|no)memo(ize)*/i,@_) ) {
91             # Attempt to memoize the calculation subroutines. This will either
92             # truly memoize, or the stub memoize function will simply return having
93             # done nothing.
94 29         555 memoize('calc_distribution');
95             # Not certain if calc_combination will benefit from Memoize. Need to
96             # test further.
97 29         8690 memoize('calc_combination');
98             }
99              
100             # All is well. Return.
101 29         265266 return;
102             }
103              
104             # debug()
105             # Print debugging information about object.
106             sub debug {
107             # The object of our attention.
108 0     0 0 0 my $self = shift;
109              
110             # For every piece of the object...
111 0         0 foreach my $key ( sort(keys(%$self)) ) {
112             # Output the name...
113 0         0 print "self->{$key}=";
114             # And if it is a reference...
115 0 0       0 if ( ref($self->{$key}) ) {
116             # Print the contents of the reference...
117 0         0 print "\n";
118 0         0 showref($self->{$key});
119             } else {
120             # Or, print the value.
121 0         0 print $self->{$key} . "\n";
122             }
123             }
124              
125             # All is well.
126 0         0 return(0);
127             }
128              
129             # new(expression)
130             # Creates a new object based on the provided dice expression.
131             sub new {
132 29     29 0 461 my $invocant = shift;
133 29   33     318 my $class = ref($invocant) || $invocant;
134 29         81 my $self = {};
135              
136 29 50       140 die("must provide dice expression") unless (@_);
137              
138 29 50       158 if (@_ != 1) {
139 0         0 die("new() called with too many arguments");
140             } else {
141 29   50     509 $self->{EXPRESSION} = $DiceParser->expression(@_) || die "could not parse expression";
142 29   50     388373 $self->{DISTRIBUTION} = travel($self->{EXPRESSION}) || die "could not travel parsed expression";
143             }
144              
145 29         1424 return(bless($self, $class));
146             }
147              
148             # travel(expression)
149             # Travel the parsed expression, returning hash of value => permutations.
150             sub travel {
151 61   50 61 0 382 my $node = shift || die "invalid or missing expression node";
152              
153 61 100       212 if ( ref($node) ) {
154 16         48 for ($node->{op}) {
155 16 100       110 /(\+)/ && return( calc_combination($1, travel($node->{left}), travel($node->{right})) );
156 9 100       62 /(\-)/ && return( calc_combination($1, travel($node->{left}), travel($node->{right})) );
157 6 100       47 /(\*)/ && return( calc_combination($1, travel($node->{left}), travel($node->{right})) );
158 2 50       43 /(\/)/ && return( calc_combination($1, travel($node->{left}), travel($node->{right})) );
159             }
160             }
161              
162 45         133 for ($node) {
163 45 100       1256 /^(\d+)(d)(\d+|f)/i && return( calc_distribution($2,$1,$3) );
164 8 100       208 /^(mi)(d)(\d+|f)/i && return( calc_distribution("m",3,$3) );
165 2 50       49 /^\d+$/ && return( {$node => 1} );
166             }
167              
168 0         0 die("invalid token in expression.");
169             }
170              
171             # calc_distribution(method,numdice,numsides)
172             # Calculate distribution of values/permutations given: method, number of
173             # dice, and number of sides per die.
174             sub calc_distribution {
175             # The dice method requested.
176             my $method = shift;
177             # The number of dice.
178             my $n = shift; $n += 0;
179             # The number of sides/faces on each die.
180             # f/F = Fudge dice = d3.
181             my $s = shift; $s += 0 unless $s =~ /f/i;
182             # Loop value based on total/face.
183             my $t1;
184             # Hash to return containing the distribution.
185             my %dist;
186              
187             # If dice node method is simple-sum nDs or nDf...
188             if ( $method =~ /d/i ) {
189             if ( $s =~ /f/i ) {
190             # If dice node method is fudge nDf...
191              
192             # First, get the distribution for nDs...
193             my $tempdist = calc_distribution("d",$n,3);
194             # Then loop to build the new distribution from (-n..0)...
195             my $origkey = $n;
196             for ($t1=-1 * $n; $t1 <= 0; $t1++) {
197             # Changing the original values in (n..ns) to (-n..n),
198             # copy the original distribution to the new one with
199             # the correct values...
200             $dist{$t1} = $$tempdist{$origkey};
201             $dist{$t1*-1} = $$tempdist{$origkey};
202             $origkey++;
203             }
204             } elsif ( $n eq 1 ) {
205             # Save compute cycles if only one die...
206              
207             # Each face has a single chance...
208             for ($t1=1; $t1 <= $s; $t1++) {
209             $dist{$t1} = 1;
210             }
211             } else {
212             # Else compute number of combinations for a total on the face of n
213             # dice.
214              
215             # Minimum and maximum sums.
216             my $tmin = $n;
217             my $tmax = $n*$s;
218             # Peak sum is the sum around which the bell-curve mirrors, saving
219             # compute time.
220             my $tpeak = ($tmin+$tmax)/2;
221              
222             # For each total (t1) in $tmin..$tmax, calculate the number of
223             # combinations giving that total.
224             for ($t1=$tmin; $t1 <= $tpeak; $t1++) {
225             # Total (t2) that is the mirror point in the bell curve.
226             my $t2 = $tmin + $tmax - $t1;
227             # Ceiling for the sum function.
228             my $ceil = int( ($t1-$n) / $s );
229             # Result.
230             my $res = 0;
231             # Loop control for the sum funtion.
232             my $k;
233              
234             # Sum Function: For each k in 0 to $ceil...
235             for ($k=0; $k <= $ceil; $k++) {
236             # Calculate and add to previous results.
237             $res += ((-1)**$k) * &$binco($n,$k) * &$binco(($t1-($s*$k)-1),($n-1));
238             }
239              
240             # Set the mirrored points of the distribution.
241             # Note: $t1 and $t2 can be equal once at
242             # $t1=$tpeak when s is even.
243             $dist{$t1} = $res;
244             $dist{$t2} = $res;
245             }
246             }
247             } elsif ( $method =~ /m/i ) {
248             # If dice method is take-the-middle-value nMs...
249              
250             # Minimum and maximum values.
251             my $tmin = 1;
252             my $tmax = $s;
253              
254             # For each value (t1) in $tmin..$tmax, calculate the number of
255             # combinations giving t1 as the middle face value.
256             for ($t1=$tmin; $t1 <= $tmax; $t1++) {
257             $dist{$t1} = 1 + ( 3 * ($s - 1) ) + ( 6 * ($t1 - 1) * ($s - $t1) );
258             }
259             }
260              
261             # Return the value=>combinations distribution hash for this node.
262             return({%dist});
263             }
264              
265             # calc_combination(operand,distribution1,distribution2)
266             # Combine two distributions with the method provided.
267             sub calc_combination {
268             # Calculation to perform on values.
269             my $op = shift;
270             # Distributions to combine.
271             my $dist1 = shift;
272             my $dist2 = shift;
273             # The combined distribution.
274             my %cdist;
275              
276             # For each value in the first distribution...
277             foreach my $val1 ( sort {$a+0 <=> $b+0} keys(%$dist1) ) {
278             # Combine it with every value in the second distribution...
279             foreach my $val2 ( sort {$a+0 <=> $b+0} keys(%$dist2) ) {
280             # The new value of which is calculated based on combine method...
281             my $newval;
282             for ($op) {
283             /\+/ && do { $newval = $val1 + $val2 };
284             /\-/ && do { $newval = $val1 - $val2 };
285             /\*/ && do { $newval = $val1 * $val2 };
286             /\// && do { $newval = int($val1 / $val2) };
287             }
288             # Calculate the new combined combinations and set it to the new
289             # value in the new distribution.
290             $cdist{$newval} += $$dist1{$val1} * $$dist2{$val2};
291             }
292             }
293              
294             # Return the combined distribution.
295             return({%cdist});
296             }
297              
298             # combinations(targetvalue)
299             # Calculate number of combinations for a target value.
300             sub combinations {
301 303     303 0 547 my $self = shift;
302 303         378 my $targetvalue = shift;
303              
304 303 100       738 $targetvalue = "ALL" unless defined($targetvalue);
305              
306 303 50       651 if ( ref($self) ) {
307 303 50       608 if (@_ != 0) {
308 0         0 die("combinations() called incorrectly");
309             } else {
310 303 100       679 if ( $targetvalue eq "ALL" ) {
311 28 50       133 if ( $self->{COMBINATIONS} ) {
312 0         0 return ( $self->{COMBINATIONS} );
313             } else {
314 28         56 foreach my $value ( values(%{$self->{DISTRIBUTION}}) ) {
  28         880  
315 12201         12281 $self->{COMBINATIONS} += $value;
316             }
317 28         125 return($self->{COMBINATIONS});
318             }
319             } else {
320 275         1355 return( $self->{DISTRIBUTION}->{$targetvalue} );
321             }
322             }
323             } else {
324 0         0 die("combinations() called on non-object");
325             }
326             }
327              
328             # distribution()
329             # Returns a hash containing the distribution in value=>combinations format.
330             sub distribution {
331 0     0 0 0 my $self = shift;
332              
333 0 0       0 if ( ref($self) ) {
334 0 0       0 if (@_ != 0) {
335 0         0 die("distribution() called with argument on object");
336             } else {
337 0         0 return( $self->{DISTRIBUTION} );
338             }
339             } else {
340 0 0       0 if (@_ != 0) {
341 0   0     0 my $expression = $DiceParser->expression(@_) || die "could not parse expression";
342 0         0 return( travel($expression) );
343             } else {
344 0         0 die("no expression provided for non-object distribution() call");
345             }
346             }
347             }
348              
349             # probability(targetvalue)
350             # Returns the probability for targetvalue, or a hash of probabilities in
351             # value=>probability format.
352             sub probability {
353 12201     12201 0 17924 my $self = shift;
354 12201         13823 my $targetvalue = shift;
355              
356 12201 50       23596 $targetvalue = "ALL" unless defined($targetvalue);
357              
358 12201 50       22107 if ( ref($self) ) {
359 12201 50       21747 if (@_ != 0) {
360 0         0 die("probability() called incorrectly");
361             } else {
362 12201 100       27573 if ( ! exists($self->{PROBABILITIES}) ) {
363 28         157 my $combs = $self->combinations();
364 28         67 my %probs;
365 28         71 foreach my $value ( keys(%{$self->{DISTRIBUTION}}) ) {
  28         36321  
366 12201         26113 $probs{$value} = $self->{DISTRIBUTION}->{$value} / $combs;
367             }
368 28         11049 $self->{PROBABILITIES} = {%probs};
369             }
370 12201 50       22987 if ( $targetvalue eq "ALL" ) {
371 0         0 return( $self->{PROBABILITIES} );
372             } else {
373 12201   50     78851 return( $self->{PROBABILITIES}->{$targetvalue} || undef );
374             }
375             }
376             } else {
377 0         0 die("probability() called on non-object");
378             }
379             }
380              
381             # bounds()
382             # Returns the min and max values of the valueset.
383             sub bounds {
384 56     56 0 125 my $self = shift;
385              
386 56 50       190 if ( ref($self) ) {
387 56 50       180 if (@_ != 0) {
388 0         0 die("bounds() called with argument on object");
389             } else {
390 56         161 return( [ $self->min(), $self->max() ] );
391             }
392             } else {
393 0         0 die("bounds() called on non-object");
394             }
395             }
396              
397             # max()
398             # Returns the max value of the valueset.
399             sub max {
400 84     84 0 160 my $self = shift;
401              
402 84 50       220 if ( ref($self) ) {
403 84 50       236 if (@_ != 0) {
404 0         0 die("max() called with argument on object");
405             } else {
406 84 100       210 if ( $self->{MAX} ) {
407 56         401 return( $self->{MAX} );
408             } else {
409 28         58 my @values = sort {$b+0 <=> $a+0} keys(%{$self->{DISTRIBUTION}});
  145546         170861  
  28         2033  
410 28         1013 $self->{MAX} = shift(@values);
411 28         726 return( $self->{MAX} );
412             }
413             }
414             } else {
415 0         0 die("max() called on non-object");
416             }
417             }
418              
419             # min()
420             # Returns the min value of the valueset.
421             sub min {
422 84     84 0 12866 my $self = shift;
423              
424 84 50       292 if ( ref($self) ) {
425 84 50       234 if (@_ != 0) {
426 0         0 die("min() called with argument on object");
427             } else {
428 84 100       508 if ( $self->{MIN} ) {
429 52         225 return( $self->{MIN} );
430             } else {
431 32         98 my @values = sort {$a+0 <=> $b+0} keys(%{$self->{DISTRIBUTION}});
  145751         168339  
  32         1538  
432 32         1546 $self->{MIN} = shift(@values);
433 32         1235 return( $self->{MIN} );
434             }
435             }
436             } else {
437 0           die("min() called on non-object");
438             }
439             }
440              
441             1;
442              
443             __END__