Games-Dice-Loaded

 view release on metacpan or  search on metacpan

lib/Games/Dice/Loaded.pm  view on Meta::CPAN

package Games::Dice::Loaded;
{
  $Games::Dice::Loaded::VERSION = '0.002';
}
use Moose 2.0300;
use List::Util qw/max sum/;

# ABSTRACT: Perl extension to simulate rolling loaded dice

# Keith Schwarz's article is lovely and has lots of pretty diagrams and proofs,
# but unfortunately it's also very long. Here's the tl;dr:

# Draw a bar chart of the probabilities of landing on the various faces, then
# throw darts at it (by picking X and Y coordinates uniformly at random). If
# you hit a bar with your dart, choose that face. This works OK, but has very
# bad worst-case behaviour; fortunately, it's possible to cut up the taller
# bars and stack them on top of the shorter bars in such a way that the area
# covered is exactly a (1/n) \* n rectangle. Constructing this rectangular
# "dartboard" can be done in O(n) time, by maintaining a list of short (less
# than average height) bars and a list of long bars; add the next short bar to
# the dartboard, then take enough of the next long bar to fill that slice up to
# the top. Add the index of the long bar to the relevant entry of the "alias
# table", then put the remainder of the long bar back into either the list of
# short bars or the list of long bars, depending on how long it now is.

# Once we've done this, simulating a dice roll can be done in O(1) time:
# Generate the dart's coordinates; which vertical slice did the dart land in,
# and is it in the shorter bar on the bottom or the "alias" that's been stacked
# above it?

# Heights of the lower halves of the strips
has 'dartboard' => ( is => 'ro', isa => 'ArrayRef' );
# Identities of the upper halves of the strips
has 'aliases' => ( is => 'ro', isa => 'ArrayRef' );
has 'num_faces' => ( is => 'ro', isa => 'Num' );

# Construct the dartboard and alias table
around BUILDARGS => sub {
	my $orig = shift;
	my $class = shift;
	# scale so average weight is 1
	my @weights = @_;
	my $n = scalar @weights;
	my $scalefactor = $n / sum(@weights);
	my $i = 0;
	@weights = map { [$i++, $scalefactor * $_] } @weights; 
	my @small = grep { $_->[1] < 1 } @weights;
	my @large = grep { $_->[1] >= 1 } @weights;
	my @dartboard; my @aliases;
	while ((@small > 0) && (@large > 0)) {
		my ($small_id, $small_p) = @{pop @small};
		my ($large_id, $large_p) = @{pop @large};
		$dartboard[$small_id] = $small_p;
		$aliases[$small_id] = $large_id;
		$large_p = $small_p + $large_p - 1;
		if ($large_p >= 1) {
			push @large, [$large_id, $large_p];
		} else {
			push @small, [$large_id, $large_p];
		}
	}
	for my $unused (@small, @large) {
		$dartboard[$unused->[0]] = 1;
		$aliases[$unused->[0]] = $unused->[0];
	}
	for my $face (0 .. $n - 1) {
		my $d = $dartboard[$face];
		die("Undefined dartboard for face $face") unless defined $d;
		die("Height $d too large for face $face") unless $d <= 1;
		die("Height $d too small for face $face") unless $d >= 0;
	}
	return $class->$orig(
		dartboard => \@dartboard,
		aliases => \@aliases,
		num_faces => $n,
	);
};

# Roll the die
sub roll {
	my ($self) = @_;
	my $face = int(rand $self->num_faces);
	my $height = rand 1;
	my @dartboard = @{$self->dartboard()};
	die("Dartboard undefined for face $face")
		unless defined $dartboard[$face];
	if ($height > $dartboard[$face]) {
		my @aliases = @{$self->aliases};
		return $aliases[$face] + 1;
	} else {
		return $face + 1;
	}
}



( run in 1.927 second using v1.01-cache-2.11-cpan-0d23b851a93 )