File Coverage

blib/lib/Games/Dice/Loaded.pm
Criterion Covered Total %
statement 20 20 100.0
branch 3 4 75.0
condition n/a
subroutine 4 4 100.0
pod 1 1 100.0
total 28 29 96.5


line stmt bran cond sub pod time code
1             package Games::Dice::Loaded;
2             {
3             $Games::Dice::Loaded::VERSION = '0.001_01';
4             }
5 2     2   28602 use Moose 2.0300;
  2         595038  
  2         15  
6 2     2   9269 use List::Util qw/max sum/;
  2         4  
  2         143  
7 2     2   9 use Carp;
  2         7  
  2         908  
8              
9             # ABSTRACT: Perl extension to simulate rolling loaded dice
10              
11             # Keith Schwarz's article is lovely and has lots of pretty diagrams and proofs,
12             # but unfortunately it's also very long. Here's the tl;dr:
13              
14             # Draw a bar chart of the probabilities of landing on the various sides, then
15             # throw darts at it (by picking X and Y coordinates uniformly at random). If
16             # you hit a bar with your dart, choose that side. This works OK, but has very
17             # bad worst-case behaviour; fortunately, it's possible to cut up the taller
18             # bars and stack them on top of the shorter bars in such a way that the area
19             # covered is exactly a (1/n) \* n rectangle. Constructing this rectangular
20             # "dartboard" can be done in O(n) time, by maintaining a list of short (less
21             # than average height) bars and a list of long bars; add the next short bar to
22             # the dartboard, then take enough of the next long bar to fill that slice up to
23             # the top. Add the index of the long bar to the relevant entry of the "alias
24             # table", then put the remainder of the long bar back into either the list of
25             # short bars or the list of long bars, depending on how long it now is.
26              
27             # Once we've done this, simulating a dice roll can be done in O(1) time:
28             # Generate the dart's coordinates; which vertical slice
29             # did the dart land in, and is it in the shorter bar on the bottom or the
30             # "alias" that's been stacked above it?.
31              
32             # Heights of the lower halves of the strips
33             has 'dartboard' => ( is => 'ro', isa => 'ArrayRef' );
34             # Identities of the upper halves of the strips
35             has 'aliases' => ( is => 'ro', isa => 'ArrayRef' );
36             has 'num_sides' => ( is => 'ro', isa => 'Num' );
37              
38             # Construct the dartboard and alias table
39             around BUILDARGS => sub {
40             my $orig = shift;
41             my $class = shift;
42             # scale so average weight is 1
43             my @weights = @_;
44             my $n = scalar @weights;
45             my $scalefactor = $n / sum(@weights);
46             my $i = 0;
47             @weights = map { [$i++, $scalefactor * $_] } @weights;
48             my @small = grep { $_->[1] < 1 } @weights;
49             my @large = grep { $_->[1] >= 1 } @weights;
50             my @dartboard; my @aliases;
51             while ((@small > 0) && (@large > 0)) {
52             my ($small_id, $small_p) = @{pop @small};
53             my ($large_id, $large_p) = @{pop @large};
54             $dartboard[$small_id] = $small_p;
55             $aliases[$small_id] = $large_id;
56             $large_p = $small_p + $large_p - 1;
57             if ($large_p >= 1) {
58             push @large, [$large_id, $large_p];
59             } else {
60             push @small, [$large_id, $large_p];
61             }
62             }
63             for my $unused (@small, @large) {
64             $dartboard[$unused->[0]] = 1;
65             $aliases[$unused->[0]] = $unused->[0];
66             }
67             for my $side (0 .. $n - 1) {
68             my $d = $dartboard[$side];
69             croak("Undefined dartboard for side $side") unless defined $d;
70             croak("Height $d too large for side $side") unless $d <= 1;
71             croak("Height $d too small for side $side") unless $d >= 0;
72             }
73             return $class->$orig(
74             dartboard => \@dartboard,
75             aliases => \@aliases,
76             num_sides => $n,
77             );
78             };
79              
80             # Roll the die
81             sub roll {
82 16002     16002 1 6476876 my ($self) = @_;
83 16002         480692 my $side = int(rand $self->num_sides);
84 16002         16941 my $height = rand 1;
85 16002         12110 my @dartboard = @{$self->dartboard()};
  16002         368635  
86 16002 50       24802 croak("Dartboard undefined for side $side")
87             unless defined $dartboard[$side];
88 16002 100       25479 if ($height > $dartboard[$side]) {
89 3472         2589 my @aliases = @{$self->aliases};
  3472         78693  
90 3472         7344 return $aliases[$side] + 1;
91             } else {
92 12530         23424 return $side + 1;
93             }
94             }
95              
96             1;
97              
98             __END__
99              
100             =head1 NAME
101              
102             Games::Dice::Loaded - Simulate rolling loaded dice
103              
104             =head1 SYNOPSIS
105              
106             use Games::Dice::Loaded;
107              
108             my $die = Games::Dice::Loaded->new(1/6, 1/6, 1/2, 1/12, 1/12);
109             my $result = $die->roll();
110              
111             my $fair_d4 = Games::Dice::Loaded->new(1, 1, 1, 1);
112             $result = $fair_d4->roll();
113              
114             =head1 DESCRIPTION
115              
116             C<Games::Dice::Loaded> allows you to simulate rolling arbitrarily-weighted dice
117             with arbitrary numbers of sides - or, more formally, to model any discrete
118             random variable which may take only finitely many values. It does this using
119             Vose's elegant I<alias method>, which is described in Keith Schwarz's article
120             L<Darts, Dice, and Coins: Sampling from a Discrete
121             Distribution|http://www.keithschwarz.com/darts-dice-coins/>.
122              
123             =head1 METHODS
124              
125             =over
126              
127             =item new()
128              
129             Constructor. Takes as arguments the probabilities of rolling each "side". If
130             the weights given do not sum to 1, they are scaled so that they do. This method
131             constructs the alias table, in O(num_sides) time.
132              
133             =item roll()
134              
135             Roll the die. Takes no arguments, returns a number in the range 1 .. num_sides. Takes O(1) time.
136              
137             =item num_sides()
138              
139             The number of sides on the die. Read-only.
140              
141             =back
142              
143             =head1 AUTHOR
144              
145             Miles Gould, E<lt>mgould@cpan.orgE<gt>
146              
147             =head1 CONTRIBUTING
148              
149             Please fork
150             L<the GitHub repository|http://github.com/pozorvlak/Games-Dice-Loaded>.
151              
152             =head1 COPYRIGHT AND LICENSE
153              
154             Copyright (C) 2011 by Miles Gould
155              
156             This library is free software; you can redistribute it and/or modify
157             it under the same terms as Perl itself, either Perl version 5.12.4 or,
158             at your option, any later version of Perl 5 you may have available.
159              
160             =head1 SEE ALSO
161              
162             Perl modules for rolling dice:
163             L<Games::Dice>,
164             L<Games::Dice::Advanced>,
165             L<Bot::BasicBot::Pluggable::Module::Dice>,
166             L<random>.
167              
168             A Perl module for calculating probability distributions for dice rolls:
169             L<Games::Dice::Probability>.
170              
171             Descriptions of the alias method:
172              
173             =over
174              
175             =item L<Darts, Dice, and Coins: Sampling from a Discrete
176             Distribution|http://www.keithschwarz.com/darts-dice-coins/>
177              
178             =item L<Data structure for loaded dice?|http://stackoverflow.com/questions/5027757/data-structure-for-loaded-dice> on StackOverflow
179              
180             =item L<Wikipedia article|http://en.wikipedia.org/wiki/Alias_method>
181              
182             =back
183              
184             =cut