+### -*-perl-*-
+
+package Deal;
+
+use MIME::Base32;
+
+###--------------------------------------------------------------------------
+### Random numbers.
+
+package Deal::Random;
+
+sub init ($) { }
+
+sub range ($$) {
+ my ($me, $max) = @_;
+
+ my ($nby, $get, $r);
+ if ($max <= 255) { $nby = 1; $get = "C"; }
+ elsif ($max <= 65535) { $nby = 2; $get = "S"; }
+ else { $nby = 4; $get = "L"; }
+
+ my $lim = unpack $get, "\xff" x $nby;
+ my $m = int $lim/$max;
+ my $thresh = $m*$max;
+ do { $r = unpack $get, $me->bytes($nby); } while $r >= $thresh;
+ return $r%$max;
+}
+
+package Deal::BufferedRandom;
+
+@ISA = qw(Deal::Random);
+
+sub init ($) {
+ my ($me) = @_;
+ $me->{buf} = "";
+ $me->SUPER::init;
+}
+
+sub bytes ($$) {
+ my ($me, $n) = @_;
+
+ if ($n > length $me->{buf}) {
+ my $want = ($n + 4095 - length $me->{buf})&-4096;
+ while ($want > 0) {
+ my $more = $me->fetch($want);
+ $want -= length $more;
+ $me->{buf} .= $more;
+ }
+ }
+
+ my $chunk = substr $me->{buf}, 0, $n;
+ $me->{buf} = substr $me->{buf}, $n;
+ return $chunk;
+}
+
+package Deal::SysRandom;
+
+@ISA = qw(Deal::BufferedRandom);
+
+sub new ($) {
+ my ($pkg) = @_;
+ my $me = bless {};
+ $me->init;
+ return $me;
+}
+
+sub init ($) {
+ my ($me) = @_;
+ open my $rand, "/dev/urandom" or die "open (/dev/urandom): $!";
+ $me->{rand} = $rand;
+ $me->SUPER::init;
+}
+
+sub fetch ($$) {
+ my ($me, $want) = @_;
+ defined (read $me->{rand}, my $more, $want)
+ or die "read (/dev/urandom): $!";
+ return $more;
+}
+
+package Deal::SeedRandom;
+
+use Digest::SHA qw(sha256);
+
+@ISA = qw(Deal::BufferedRandom);
+
+sub new ($@) {
+ my ($pkg, @args) = @_;
+ my $me = bless {};
+ $me->init(@args);
+ return $me;
+}
+
+sub init ($$) {
+ my ($me, $seed) = @_;
+ $me->{seed} = $seed;
+ $me->{i} = 0;
+}
+
+sub fetch ($$) {
+ my ($me, $want) = @_;
+ return sha256(pack "La*", $me->{i}++, $me->{seed});
+}
+
+###--------------------------------------------------------------------------
+### General stuff.
+
+package Deal;
+
+our $RAND = Deal::SysRandom->new;
+
+sub shuffle ($@) {
+ my ($r, @x) = @_;
+ my $n = @x;
+ for (my $i = 0; $i < $n - 1; $i++) {
+ my $k = $i + $r->range($n - $i);
+ ($x[$i], $x[$k]) = ($x[$k], $x[$i]);
+ }
+ return @x;
+}
+
+our @LINE; $LINE[$_] = $_%4 + 1 for 0..51;
+
+sub line () { return shuffle $RAND, @LINE; }
+
+###--------------------------------------------------------------------------
+### Generating hands for study.
+
+our @RANK = ("A", "K", "Q", "J", "10", "9",
+ "8", "7", "6", "5", "4", "3", "2");
+our @SUIT = ("C", "D", "H", "S");
+our @DECK;
+for (my $r = 0; $r < @RANK; $r++)
+ { for my $s (@SUIT) { push @DECK, [$s, $r]; } }
+
+our @SEAT = ("N", "W", "S", "E");
+our @VULN = ("None", "N/S", "E/W", "All");
+
+our %HCP = ("A" => 4, "K" => 3, "Q" => 2, "J" => 1);
+
+sub hand (@) {
+ my (@cards) = @_;
+ my %card;
+
+ ## Work out the cards and sort them into suits.
+ for my $s (@SUIT) { $card{$s} = []; }
+ for my $c (@cards) { push @{$card{$c->[0]}}, $c->[1]; }
+ for my $s (@SUIT) {
+ @{$card{$s}} = map { $RANK[$_] } sort { $a <=> $b } @{$card{$s}};
+ }
+
+ ## Count the high-card points and losers.
+ my $hcp = 0;
+ my $ltc = 0;
+ for my $s (@SUIT) {
+ my @c = @{$card{$s}};
+ my $n = @c;
+ my %c = map { $_ => 1 } @c;
+ for my $i (keys %HCP) { $hcp += $HCP{$i} if $c{$i}; }
+ for my $i ("A", "K", "Q") {
+ last unless $n--;
+ $ltc++ unless $c{$i};
+ }
+ }
+
+ ## Done.
+ return { %card, ltc => $ltc, hcp => $hcp };
+}
+
+sub deal (%) {
+ my $deal = { @_ };
+ $deal->{seed} //= lc(MIME::Base32::encode($RAND->bytes(16)));
+ my $r = Deal::SeedRandom->new($deal->{seed});
+ my @cards = shuffle $r, @DECK;
+ for my $s (@SEAT) { $deal->{$s} = hand splice @cards, 0, 13; }
+ $deal->{dealer} //= $r->range(4);
+ $deal->{vuln} //= $r->range(4);
+
+ return $deal;
+}
+
+###----- That's all, folks --------------------------------------------------
+
+1;