File Coverage

blib/lib/Role/Random/PerInstance.pm
Criterion Covered Total %
statement 66 67 98.5
branch 20 24 83.3
condition 11 15 73.3
subroutine 13 13 100.0
pod 5 5 100.0
total 115 124 92.7


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