File Coverage

blib/lib/Role/Random/PerInstance.pm
Criterion Covered Total %
statement 68 69 98.5
branch 20 24 83.3
condition 11 15 73.3
subroutine 14 14 100.0
pod 5 5 100.0
total 118 127 92.9


line stmt bran cond sub pod time code
1             package Role::Random::PerInstance;
2              
3 2     2   57610 use Moose::Role;
  2         409114  
  2         7  
4 2     2   9522 use Carp;
  2         3  
  2         115  
5 2     2   37 use 5.010;
  2         7  
6 2     2   9 use feature 'state';
  2         4  
  2         232  
7              
8             our $VERSION = '0.04';
9              
10 2     2   13 use List::Util qw(sum reduce);
  2         4  
  2         127  
11 2     2   577 use Math::Round qw(nlowmult);
  2         6592  
  2         1336  
12              
13             has random_seed => (
14             is => 'rw',
15             isa => 'Int',
16             lazy => 1,
17             builder => '_build_random_seed',
18             );
19              
20 4     4   92 sub _build_random_seed { 0 }
21              
22             # this is only used internally in deterministic_rand() to reset the seed for
23             # the next call to deterministic_rand().
24             has _seed => (
25             is => 'rw',
26             isa => 'Int',
27             predicate => '_seed_is_set',
28             );
29              
30             sub deterministic_rand {
31 4030     4030 1 14613 my $self = shift;
32 4030         4446 state $modulus = 2**31 - 1;
33 4030         4334 state $multiplier_a = 1_103_515_245;
34 4030         4135 state $increment_c = 12_345;
35              
36 4030 0 33     87376 if ( !$self->random_seed && !$self->_seed_is_set ) {
37 0         0 croak("You must provide a random_seed to the constructor");
38             }
39              
40             # only set this once via random_seed. After that, this algorithm will set
41             # it.
42 4030 100       97424 $self->_seed( $self->random_seed ) unless $self->_seed_is_set;
43 4030         84075 my $xn_new = ( $multiplier_a * $self->_seed + $increment_c ) % $modulus;
44 4030         85244 $self->_seed($xn_new);
45 4030         22103 return 0 + sprintf "%0.9f" => substr( $xn_new, -7 ) / 10_000_000;
46             }
47              
48             sub attempt {
49 3002     3002 1 16615 my ( $self, $base_chance ) = @_;
50 3002         4276 my $chance = _constrain( 0, $base_chance, 1 );
51              
52 3002         4605 my $rand = $self->random;
53 3002 100       7646 return $rand < $chance ? 1 : 0;
54             }
55              
56             sub random {
57 17032     17032 1 113252 my ( $self, $min, $max, $step ) = @_;
58 17032   100     29190 $min //= 0;
59 17032   100     26800 $max //= 1;
60 17032   100     28692 $step //= 0;
61              
62             # We add $step to ensure that $max is inclusive in our random set.
63             # If $step is set to 0, then $max will be exclusive of the result set.
64 17032         21739 my $maxrand = $max - $min + $step;
65 17032 100       27718 $maxrand = nlowmult( $step, $maxrand ) if $step;
66 17032 100       460159 my $rand =
67             $self->random_seed
68             ? $self->deterministic_rand
69             : rand();
70 17032         24096 $rand *= $maxrand;
71 17032 100       30854 $rand = nlowmult( $step, $rand ) if $step;
72 17032         88679 $rand += $min;
73              
74 17032         27177 return $rand;
75             }
76              
77             sub random_int {
78 3000     3000 1 26259 my ( $self, $min, $max ) = @_;
79 3000         4329 return $self->random( $min, $max, 1 );
80             }
81              
82             sub weighted_pick {
83 2000     2000 1 11311 my ( $self, $weight_for ) = @_;
84 2000         2422 my ( @weights, @choices );
85 2000         2616 my $total = 0;
86              
87             # Use foreach with a sort to ensure that the order of items in weights and
88             # choices is always the same
89 2000         5751 foreach my $choice ( sort keys %$weight_for ) {
90 10000         11556 my $weight = $weight_for->{$choice};
91 10000 100       13779 next unless $weight; # don't include weights of 0
92 9000         9060 $total += $weight;
93 9000         11110 push @weights => $total;
94 9000         11556 push @choices => $choice;
95             }
96             return
97 2000         3901 $choices[ $self->_binary_range( $self->random( 0, $weights[-1] ),
98             \@weights ) ];
99             }
100              
101             sub _binary_range {
102 2000     2000   3261 my ( $self, $elem, $list ) = @_;
103 2000         2745 my $max = $#$list;
104 2000         2669 my $min = 0;
105              
106 2000         3573 while ( $max >= $min ) {
107 3667         5118 my $index = int( ( $max + $min ) / 2 );
108 3667         4394 my $curr = $list->[$index];
109 3667 100       5545 my $prev = 0 == $index ? 0 : $list->[ $index - 1 ];
110 3667 100 100     8828 if ( $prev < $elem && $curr >= $elem ) { return $index }
  2000 100       7024  
111 779         1451 elsif ( $curr > $elem ) { $max = $index - 1 }
112 888         1607 else { $min = $index + 1; }
113             }
114             }
115              
116             sub _constrain {
117 3002     3002   4149 my ( $min, $num, $max ) = @_;
118 3002   33     4607 $max //= $num;
119             return
120 3002 50       6068 $num < $min ? $min
    50          
121             : $num > $max ? $max
122             : $num;
123             }
124              
125             1;
126              
127             __END__
128              
129             =head1 NAME
130              
131             Role::Random::PerInstance - A role for dealing with random values, per instance
132              
133             =head1 SYNOPSIS
134              
135             package Some::Class;
136             use Moose;
137             with 'Role::Random::PerInstance';
138              
139             # later , with an instance of Some::Class
140             if ( $self->random < .65 ) {
141             ...
142             }
143              
144             # same thing ...
145              
146             if ( $self->attempt(.65) ) {
147             ...
148             }
149              
150             =head1 DESCRIPTION
151              
152             This role allows you to use random numbers, maintaining separate random
153             numbers for each instance.
154              
155             =head1 METHODS
156              
157             =head2 C<attempt($chance)>
158              
159             if ($self->attempt(0.6)) {
160             # 60% chance of success
161             }
162              
163             Perform a random test which has a chance of success based on the $chance value,
164             where $chance is a value between 0 and 1. A $chance value of 0 will always
165             return false, and a $chance value of 1 or more will always return true.
166              
167             =head2 C<random($min, $max, $step)>
168              
169             my $gain = $self->random(0.1, 0.5, 0.1 );
170             # $gain will contain one of 0.1, 0.2, 0.3, 0.4 or 0.5
171              
172             my $even = $self->random(100, 200, 2 );
173              
174             Generate a random number from $min to $max inclusive, where the resulting
175             random number increments by a value of $step starting from $min. If C<step> is
176             not supplied, this method behaves like C<rand>, but from C<$min> to C<$max>.
177              
178             By default (if no arguments are passed), this method will work the same as the
179             built in 'rand' function, which is to return a value from 0 to 1, but not
180             including 1. The number includes seven digits after the decimal point (e.g.,
181             C<0.5273486>).
182              
183             =cut
184              
185             =head2 C<random_seed>
186              
187             package Some::Package {
188             use Moose;
189             with 'Role::Random::PerInstance';
190             ...
191             }
192             my $object = Some::Package->new(
193             random_seed => $integer_seed
194             );
195              
196             If an object consuming this role passes in an integer random seed to the
197             constructor, all "random" methods in this role will use the
198             C<deterministic_rand()> method instead of the built in C<rand()> function.
199              
200             In other words, if C<random_seed> is not supplied to the constructor, the
201             random numbers will I<not> be repeatable.
202              
203             =head2 C<deterministic_rand>
204              
205             my $rand = $object->deterministic_rand;
206             $rand = $object->deterministic_rand;
207             $rand = $object->deterministic_rand;
208             $rand = $object->deterministic_rand;
209              
210             This method returns pseudo-random numbers from 0 to 1, with up to seven digits
211             past the decimal point (e.g., "0.1417026"), but is deterministic. This is not
212             cryptographically secure, but the numbers are evenly distributed.
213              
214             C<< $self->random_seed >> must be set in the object constructor to ensure
215             deterministic randomness.
216              
217             The algorithm is the L<Linear Congruential
218             Generator|https://en.wikipedia.org/wiki/Linear_congruential_generator>.
219              
220             We've tried merely calling C<srand(seed)>, but it turned out to not be as
221             deterministic as promised and also doesn't allow us fine-grained "per instance"
222             control.
223              
224             =head2 C<random_int($min, $max)>
225              
226             my @items = qw(one two three four five);
227             my $item = $items[ $self->random_int(0, $#items) ];
228              
229             Generate a random integer from $min to $max inclusive.
230              
231             =head2 C<weighted_pick>
232              
233             my %weights = (
234             foo => 1, # 5% chance of being chosen
235             bar => 17, # 85% chance of being chosen
236             baz => 2, # 10% chance of being chosen
237             quux => 0, # will never be chosen
238             );
239             my $choice = $self->weighted_pick( \%weights ); # will usually return 'bar'
240              
241             This function accepts a hash reference whose keys are the values you wish to
242             choose from and whose values are the I<relative> weights assigned to those
243             values. A single value from the hash will be returned. The higher its "key"
244             value, the more likely it is to be returned. Note that if you wanted an even
245             chance of all values, ensure that all keys have the same value (but at that
246             point, a straight C<rand()> would be more efficient.
247              
248             =head1 BACKGROUND
249              
250             The narrative sci-fi game, L<Tau Station|https://taustation.space/>, needed a
251             way to have I<repeatable> random numbers, with different instances of objects
252             creating their own series of random numbers. Perl's
253             L<rand|https://perldoc.perl.org/functions/rand.html> function is global, and
254             seeding it with L<srand|https://perldoc.perl.org/functions/srand.html> turns
255             out to not be as deterministic as we had hoped.
256             L<Math::Random|https://metacpan.org/pod/Math::Random> is also global. Hence,
257             our own module.
258              
259             Not only does this give you repeatable (via C<random_seed>) random numbers, it
260             gives you non-repeatable random numbers (just don't provide a seed) and many
261             useful random utilities.
262              
263             We implemented a L<Linear Congruential
264             Generator|https://en.wikipedia.org/wiki/Linear_congruential_generator> and you
265             get seven digits after the decimal point, so each number has a 1 in ten
266             million chance of occuring. That is perfect for our needs. It may not be
267             perfect for yours.
268              
269             Also, while the Linear Congruential Generator is fairly efficient and random,
270             it's not cryptographically secure.
271              
272             =head1 CAVEAT
273              
274             Note that if C<$Config{use64bitint}> (from the L<Config> module) is false, you
275             can still get deterministic "random" numbers, but they may be slightly
276             different than those generated by Perls compiled to use 64 bit integers.
277             Otherwise, the output from this module is portable. See the F<t/random.t> test
278             for examples.
279              
280             =head1 AUTHOR
281              
282             Curtis "Ovid" Poe, C<< <curtis.poe at gmail.com> >>
283              
284             =head1 BUGS
285              
286             Please report any bugs or feature requests via the Web interface at
287             L<https://github.com/Ovid/role-random-perinstance/issues>. I will be
288             notified, and then you'll automatically be notified of progress on your bug as
289             I make changes.
290              
291             =head1 SUPPORT
292              
293             You can find documentation for this module with the perldoc command.
294              
295             perldoc Role::Random::PerInstance
296              
297             You can also look for information at:
298              
299             =over 4
300              
301             =item * Bug Tracker
302              
303             L<https://github.com/Ovid/role-random-perinstance/issues>
304              
305             =item * Search CPAN
306              
307             L<https://metacpan.org/release/Role-Random-PerInstance>
308              
309             =back
310              
311             =head1 SEE ALSO
312              
313             C<Role::Random::PerInstance> was developed for the narrative sci-fi game L<Tau
314             Station|https://taustation.space>. We like it because the syntax is simple,
315             clear, and intuitive (to us). However, there are a few alternatives on the
316             CPAN that you might find useful:
317              
318             =over 4
319              
320             =item * L<Class::Delegation|https://metacpan.org/pod/Class::Delegation>
321              
322             =item * L<Class::Delegation::Simple|https://metacpan.org/pod/Class::Delegation::Simple>
323              
324             =item * L<Class::Delegate|https://metacpan.org/pod/Class::Delegate>
325              
326             =item * L<Class::Method::Delegate|https://metacpan.org/pod/Class::Method::Delegate>
327              
328             =back
329              
330              
331             =head1 ACKNOWLEDGEMENTS
332              
333             This code was written to help reduce the complexity of the narrative sci-fi
334             adventure, L<Tau Station|https://taustation.space>. As of this writing, it's
335             around 1/3 of a million lines of code (counting front-end, back-end, tests,
336             etc.), and anything to reduce that complexity is a huge win.
337              
338             =head1 LICENSE AND COPYRIGHT
339              
340             This software is Copyright (c) 2019 by Curtis "Ovid" Poe.
341              
342             This is free software, licensed under:
343              
344             The Artistic License 2.0 (GPL Compatible)
345              
346             =cut
347              
348             1;