File Coverage

blib/lib/Games/Dice/Loaded.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


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