File Coverage

blib/lib/Games/Dice/Advanced.pm
Criterion Covered Total %
statement 53 56 94.6
branch 25 34 73.5
condition 9 18 50.0
subroutine 13 13 100.0
pod 2 2 100.0
total 102 123 82.9


line stmt bran cond sub pod time code
1             package Games::Dice::Advanced;
2              
3 2     2   316304 use strict;
  2         5  
  2         66  
4 2     2   9 use warnings;
  2         3  
  2         136  
5 2     2   11 use vars qw($VERSION);
  2         8  
  2         3790  
6              
7             $VERSION = '1.2';
8              
9             =head1 NAME
10              
11             Games::Dice::Advanced - simulate dice rolls, including weird and
12             loaded dice
13              
14             =head1 SYNOPSIS
15              
16             print Games::Dice::Advanced->roll(); # roll a six-sided die
17             print Games::Dice::Advanced->roll('d4'); # roll a four-sided die
18              
19             # roll a four-sided die and a 6-sided die and return the total
20             $die1 = Games::Dice::Advanced->new('d4');
21             $die2 = Games::Dice::Advanced->new('d6');
22             print Games::Dice::Advanced->roll($die1, $die2);
23              
24             print $die1->roll(); # roll the d4 we created above
25              
26             # roll 2 four-sided dice and a 6-sided die and return the total
27             print Games::Dice::Advanced->roll('2d4', 'd6');
28              
29             # create a four-sided die with the squares of 1, 2, 3 and 4
30             Games::Dice::Advanced->new(sub { int(1+rand(4)) ** 2 });
31              
32             # create a non-numeric die
33             Games::Dice::Advanced->new(sub {
34             my @alphas = qw(C D E F G A B);
35             return $alphas[int rand @alphas];
36             });
37              
38             =head1 METHODS
39              
40             =over 4
41              
42             =item roll
43              
44             Roll one or more dice. If called as a class method, it first calls
45             appropriate constructors and creates objects before calling those objects'
46             roll() methods. When called on an object it simply rolls the die.
47              
48             When called as a class method, it takes a list of arguments defining a
49             'recipe' to roll. These are added
50             together to produce a result. Each item in the recipe must be a valid
51             argument to the constructor (see the description of the 'new' method below).
52             If no argument is given to a class method, we assume a six-sided die - 'd6'.
53             You will note that the multiplier constructor is not available when roll
54             is called in this way.
55              
56             When called as an object method, no arguments are permitted.
57              
58             =cut
59              
60             sub roll {
61 130143     130143 1 380806 my($self, @args) = @_;
62 130143 100 66     460471 if(ref($self) && $self->isa('Games::Dice::Advanced')) {
    50          
63             # called as object method
64 130128 50       240077 die("roll() called incorrectly") if(@args);
65 130128         179552 return &{$self};
  130128         222404  
66             } elsif($self eq 'Games::Dice::Advanced') {
67             # called as class method
68 15 50       69 @args = ('d6') unless(@args);
69             return _sum(map {
70 15 100 66     30 (ref($_) && $_->isa('Games::Dice::Advanced')) ?
  25         95  
71             $_->roll() :
72             Games::Dice::Advanced->new($_)->roll()
73             } @args);
74             } else {
75 0         0 die("Out of cucumber error\n");
76             }
77             }
78              
79             =item new
80              
81             This method defines a die. You may call it yourself to create a die for
82             later rolling, or it may be called by the roll() method. It takes zero,
83             one or two
84             arguments. If no argument is given, we silently assume that the user
85             wants to create a six-sided die, a 'd6'. Valid arguments are:
86              
87             =over 4
88              
89             =item integer constant, eg '5'
90              
91             Creates a die that always returns that constant
92              
93             =item dN, where N is integer, eg 'd10'
94              
95             Creates a die that returns a random integer from 1 to N with results spread
96             evenly across the range.
97              
98             =item NdM, where N and M are integer, eg '2d10'
99              
100             Creates a die dM as above which is rolled N times to generate a result.
101             Note that the N is *not* just a multiplier.
102              
103             =item N and any other valid argument, where N is a number, eg (2, 'd4')
104              
105             Note that the two arguments may be in any order. Creates a die as specified,
106             and multiplies the results by N when it is rolled. Compare with NdM above.
107              
108             =back
109              
110             Leading and trailing whitespace is stripped, no other whitespace is allowed
111             in any of the above.
112              
113             =over 4
114              
115             =item SUBREF
116              
117             A reference to a subroutine, which is to be called whenever we need to generate
118             a result. It should take no parameters.
119              
120             =item HASHREF
121              
122             Use this to easily specify truly weird dice. NOT YET IMPLEMENTED, so use
123             a SUBREF for the moment.
124              
125             =back
126              
127             =cut
128              
129             sub new {
130 25     25 1 465607 my($class, @args) = @_;
131              
132 25 50       70 @args = ('d6') unless(@args);
133 25         46 @args = map { s/(^\s+|\s+$)//; $_; } @args;
  28         146  
  28         82  
134              
135 25         65 my $self = '';
136              
137 25 100       67 if(@args == 1) { push @args, 1; } # multiply by 1
  22         47  
138              
139 25 50       59 if(@args == 2) {
140 25         55 my($recipe, $mul) = @args;
141 25 50 33     135 ($recipe, $mul) = ($mul, $recipe) if(ref($mul) || $mul=~ /\D/);
142 25 50 33     129 die("Bad arguments to new()") if(ref($mul) || $mul=~ /\D/);
143              
144 25 100       134 if($recipe !~ /\D/) { # constant
    100          
    100          
    50          
145 17     25   68 $self = sub { $recipe * $mul };
  25         120  
146             } elsif($recipe =~ /^d(\d+)$/) { # dINT
147             # $self = eval("sub { (1 + int(rand($1))) * $mul }");
148 2         11 my $faces = $1;
149 2     20000   15 $self = sub { (1 + int(rand($faces))) * $mul };
  20000         59941  
150             } elsif($recipe =~ /^(\d+)d(\d+)/) { # INTdINT
151 1         24 my($repeats, $faces) = ($1, $2);
152             $self = sub {
153 100000     100000   159459 my $random = _sum(map { 1 + int(rand($faces)) } (1..$repeats));
  200000         396438  
154 100000 50 33     281374 $random *= $mul if($mul != 1 && _die_if_not_number($random));
155 100000         223268 return $random
156 1         10 };
157             } elsif(ref($recipe) eq 'CODE') {
158             $self = sub {
159 10103     10103   14844 my $random = &{$recipe};
  10103         20129  
160 10103 100 66     43766 $random *= $mul if($mul != 1 && _die_if_not_number($random));
161 10102         29838 return $random;
162 5         37 };
163             } else {
164 0         0 die("$recipe isn't valid");
165             }
166             } else {
167 0         0 die("new() called incorrectly");
168             }
169              
170 25         99 bless($self, $class);
171             }
172              
173             =back
174              
175             =cut
176              
177             sub _die_if_not_number {
178 101 100   101   713 $_[0] =~ /^-?\d+(\.\d+)?(e\d+)?$/i ||
179             die("Can't multiply a non-numeric value: $_[0]\n");
180             }
181              
182 100010     100010   179276 sub _sum { _foldl(sub { shift() + shift(); }, @_); }
  100015     100015   254473  
183              
184             sub _foldl {
185 100015     100015   179628 my($f, $z, @xs) = @_;
186 100015         190494 $z = $f->($z, $_) foreach(@xs);
187 100015         169632 return $z;
188             }
189              
190             =head1 BUGS
191              
192             For random, read 'pseudo-random'. Patches to work with sources of true
193             randomness are welcome.
194              
195             Doesn't support dice with fractional or complex numbers of sides :-)
196              
197             If you find any bugs please report them on Github, preferably with a test case.
198              
199             =head1 FEEDBACK
200              
201             I welcome feedback about my code, including constructive criticism.
202              
203             =head1 AUTHOR, COPYRIGHT and LICENCE
204              
205             Copyright 2024 David Cantrell EFE
206              
207             This software is free-as-in-speech software, and may be used,
208             distributed, and modified under the terms of either the GNU
209             General Public Licence version 2 or the Artistic Licence. It's
210             up to you which one you use. The full text of the licences can
211             be found in the files GPL2.txt and ARTISTIC.txt, respectively.
212              
213             =head1 CONSPIRACY
214              
215             This module is also free-as-in-mason software.
216              
217             =cut
218              
219             1;