# PODNAME: Game::Lottery
# ABSTRACT: Generate and Check Big Draw Lottery Tickets

=pod

=head1 NAME

Game::Lottery

=head1 VERSION

version 1.03

=head1 DESCRIPTION

Pick and check numbers for popular Lottery Draw Games using a Cryptographically Secure randomizer.

General Draw Games and Big Draw Games are supported, but ticket valuation functions are only available for PowerBall and MegaMillions.

=head1 SYNOPSIS

Use the bundled lottery-big-draw.pl script to pick your numbers:

  lottery-big-draw.pl -h

To use in your code:

  use Game::Lottery;
  my $lottery = Game::Lottery->new( game => 'PowerBall');

=head1 DRAW GAMES

Lottery Draw games have a pool of balls. Players bet on as many numbers as the number of balls that will be drawn.

=head1 BIG DRAW LOTTERY GAMES

Have a pool of balls (white) players must match, and a second pool (red) from which (usually) a single ball is drawn, many of the prizes require matching the red ball.

The odds of winning the big prize in the two main games in the United States: PowerBall and MegaMillions, are about 300 Million to 1. When the prize isn't won, the jackpot increases. The JackPots frequently get quite large. When the JackPots get large enough, the expected value of a ticket exceeds the cost. The smaller prizes in both US games return less than 20% of the ticket value.

There are several options to increase bet sizes, both major games have a multiplier ball option; these are not currently supported.

California has a different payout on non-jackpot prizes. For California and any other jurisdictions not using the standard payout table, the expected value generated by this software does not apply.

=cut

use 5.038;
use experimental qw/class/;

class Game::Lottery;
our $VERSION="1.03";
use Math::Random::Secure;
use Path::Tiny;
# use Data::Dumper;
# use Data::Printer;
use English;

field $game : param;
field $wb;     # White Balls
field $rb;     # Red Balls
field $wbd;    # White Balls to Draw
field $rbd;    # Red Balls to Draw
field %coll;   # collision prevention

ADJUST {
  if ( $game =~ /^power/i ) {
    $game = 'PowerBall';
    $wb   = 69;
    $wbd  = 5;
    $rb   = 26;
    $rbd  = 1;
  }
  elsif ( $game =~ /^mega/i ) {
    $game = 'MegaMillions';
    $wb   = 70;
    $wbd  = 5;
    $rb   = 25;
    $rbd  = 1;
  }
  elsif ( $game =~ /^draw/i ) {
    $game = 'Draw';
  }
  elsif ( $game =~ /^custom/i ) {
    $game = 'CustomBigDraw';
  }
  else {
    die "Unknown GAME $game.\n";
  }
}

sub _PickBall ($BallLastNum) {
  # rand starts at 0 -- offset the pick.
  my $pick = 1 + int Math::Random::Secure::rand($BallLastNum);
}

sub _DrawBalls ( $BallLastNum, $BallsDrawn ) {
  unless( $BallLastNum and $BallsDrawn ) {
    die "Balls are not defined. If this is a BigDraw did you use CustomBigDrawSetup?\n";
  }
  my %balls = ();
  until ( scalar( keys %balls ) == $BallsDrawn ) {
    my $newball = sprintf( "%02d", _PickBall($BallLastNum) );
    $balls{$newball} = $newball;
  }
  return [ sort { $a <=> $b } ( keys(%balls) ) ];
}

=head1 METHODS

=head2 new

Required parameter: game

Games currently supported are PowerBall, MegaMillions, Draw (1 ball pool) and CustomBigDraw(custom 2 ball pools). Game names may be abbreviated as mega, power, draw and custom.

  my $game = Game::Lottery->new( game => 'PowerBall');
  my $game = Game::Lottery->new( game => 'power');

=head2 Game

Returns the Game.

  say $game->Game();

=cut

method _NoCollision ( $balls1, $balls2=[] ) {
  my $key = join '', ($balls1->@*, $balls2->@*);
  if ( defined $coll{$key }) { return 0 }
  $coll{$key } = 1 ;
  return 1;
}

method Game {
  return $game;
}

=head2 BasicDraw

Returns an Array Reference of balls picked. Applicable to a large number of Draw games.

  my $drawn = $game->BasicDraw( $BallLastNum, $BallsDrawn );

=cut

method BasicDraw ( $BallLastNum, $BallsDrawn ) {
  my $balls = undef;
  until (defined $balls) {
    $balls = _DrawBalls( $BallLastNum, $BallsDrawn );
    $balls = undef unless $self->_NoCollision( $balls );
  }
  $balls;
}

=head2 BigDraw

Draws White and Red Balls for Big Draw Games.

Returns a Hash Reference.

  my $drawn = $game->BigDraw();

  {
    game => $game,
    whiteballs => ArrayRef,
    redballs => ArrayRef
  }

=head2 CustomBigDrawSetup

For CustomBigDraw games, the pool ranges and number of balls to draw must be set before using the BigDraw method. For the directly supported Big Draw Games this is done at new. The game name is set to CustomBigDraw

  $game->CustomBigDrawSetup(
    game => 'Some other big draw game', # optionally set a custom game name.
    white => 99,
    whitecount => 1,
    red => 20,
    redcount => 5
);

=cut

method CustomBigDrawSetup (%balls) {
  $wb   = $balls{'white'};
  $wbd  = $balls{'whitecount'};
  $rb   = $balls{'red'};
  $rbd  = $balls{'redcount'} || 1;
  $game = $balls{'game'} if $balls{'game'};
}

method BigDraw {
  while (1) {
    my $draw = {
      'game'       => $game,
      'whiteballs' => _DrawBalls( $wb, $wbd ),
      'redballs'   => _DrawBalls( $rb, $rbd )
    };
    if ( $self->_NoCollision( $draw->{whiteballs}, $draw->{redballs} ) ) {
      return $draw;
    }
  }
}

sub _round_val ($val) { sprintf( "%.2f", $val ) }

# Calculate the
sub _BaseMMVal {
  my $val = 2 * ( 1 / 37 );            # Red
  $val += 4 * ( 1 / 89 );              #   Red + 1 White
  $val += 10 * ( 1 / 693 );            #  Red + 2 White
  $val += 10 * ( 1 / 606 );            #  3 White
  $val += 200 * ( 1 / 14547 );         # Red + 3 White
  $val += 500 * ( 1 / 38792 );         # 4 White
  $val += 10000 * ( 1 / 931001 );      # Red + 4 White
  $val += 10**6 * ( 1 / 12607306 );    # 5 White
  return $val;
}

sub _BasePBVal {
  my $val = 4 * ( 1 / 38.32 );            # Red
  $val += 4 * ( 1 / 91.98 );              #   Red + 1 White
  $val += 7 * ( 1 / 701.33 );             #  Red + 2 White
  $val += 7 * ( 1 / 579.76 );             #  3 White
  $val += 100 * ( 1 / 1494.11 );          # Red + 3 White
  $val += 100 * ( 1 / 36525.17 );         # 4 White
  $val += 50000 * ( 1 / 913129.18 );      # Red + 4 White
  $val += 10**6 * ( 1 / 11688053.52 );    # 5 White
  return $val;
}

=head2 TicketValue

Returns the expected value of a ticket given a JackPot. The Cash Value should be used, not the advertised annuity value. The value passed may be either in dollars or millions of dollars. If no value (or 0) is based the result will be the return of all other prizes than the jackpot.

  my $TicketValue = $game->TicketValue( 600 );
  my $BaseTicketValue = $game->TicketValue( 0 );

=head2 TicketJackPotValue

Returns just value of the JackPot divided by the number of possible combinations.

  my $JackPot = $game->TicketJackPotValue( 600 );

=cut

method TicketValue ( $jackpot = 0 ) {
  # allows jackpot in millions or in dollars.
  $jackpot = $jackpot * 10**6 if $jackpot < 10**6;
  if ( 'PowerBall' eq $game ) {
    return _round_val( _BasePBVal() + $jackpot / 292201338 );
  }
  elsif ( 'MegaMillions' eq $game ) {
    return _round_val( _BaseMMVal() + $jackpot / 302575350 );
  }
  else {
    die "Payout data not available for ${game}!";
  }
}

method TicketJackPotValue ($jackpot) {
  # allows jackpot in millions or in dollars.
  $jackpot = $jackpot * 10**6 if $jackpot < 10**6;
  if ( 'PowerBall' eq $game ) {
    return _round_val( $jackpot / 292201338 );
  }
  elsif ( 'MegaMillions' eq $game ) {
    return _round_val( $jackpot / 302575350 );
  }
  else {
    die "Payout data not available for ${game}!";
  }
}

=head2 SavePicks

Save an arrayref of picks from BasicDraw or BigDraw to a file.

  $game->SavePicks( $file, $picks );

=head2 ReadPicks

Read saved picks from a file, returns an array.

  my @picks = $game->ReadPicks( $file );

=cut

method SavePicks ( $file, $picks ) {
  open( my $out, '>', $file ) || die "unable to open output $file, $?";
  for my $pick ( $picks->@* ) {
    print $out do { join ' ', $pick->{whiteballs}->@* };
    print $out ' ';
    for my $red ( $pick->{redballs}->@* ) {
      print $out "[${red}]";
    }
    print $out "\n";
  }
}

method ReadPicks ($file) {
  my @picks = ();
  for my $line ( path($file)->lines() ) {
    next if $line     =~ /^#/;    # skip comment lines
    next unless $line =~ /\d/;    # skip lines missing data
    my $pick = {};
    chomp $line;
    ( $pick->{memo}, $line ) = split /::\s+|::/, $line if ( $line =~ /::/ );
    $pick->{balls} = [ split /\s+/, $line ];
    push @picks, $pick;
  }
  @picks;
}

=head1 Checking Tickets

=head2 CheckPicks

  say join "\n", $game->CheckPicks( $winnersfile, $ticketsfile );

=head2 The Saved Tickets File Format

This library pads single digit numbers with a leading 0. It uses [] to differentiate red balls. The balls are checked as strings so 01 and 1 are not matching numbers just as 02 and [02] are not. The entries are simply the numbers on a line, separated by spaces, with the red balls in [brackets].

A file generated by SavePicks looks like this:

  05 40 50 57 60 [15]
  02 33 36 44 68 [19]

The format allows for a memo at the beginning of a line, separated from the data by a double colon ::. When included in a winners file, the memo is included in the output. Usually the memo is the date of the draw, but it can be any string. Lines beginning with # are considered comments and not read.

Winners files need to be created by hand.

  # example winners file
  2023-09-16:: 22 30 37 44 45 [18]
  # the memo is just a string, it doesn't have to be the date
  Wednesday 2023-10-11::22 24 40 52 64 [10]
  # the memo isn't required but is helpful when you have
  # multiple lines in your winners file.
  16 34 46 57 59 [23]

=cut

method CheckPicks ( $winnersfile, $ticketsfile ) {
  local $LIST_SEPARATOR = " ";
  my @winners = $self->ReadPicks($winnersfile);
  my @tickets = $self->ReadPicks($ticketsfile);
  my @result  = ();
  for my $drawing (@winners) {
    my $ctr      = 1;
    my $linehead = '* ';
    my %winballs = map { $_ => $_ } ( $drawing->{balls}->@* );
    if ( defined $drawing->{memo} ) {
      $linehead .= $drawing->{memo} . ' * ';
    }
    $linehead .= "@{ $drawing->{balls} }";
    push @result, $linehead;
    for my $ticket (@tickets) {
      my @matched    = ();
      my @checkballs = $ticket->{balls}->@*;
      for my $ball (@checkballs) {
        push @matched, ($ball) if defined $winballs{$ball};
      }
      my $out = " ${\ $ctr++ } -- @checkballs ";
      if (@matched) {
        $out .= "matched (${\ scalar(@matched) }): @matched";
      }
      push @result, $out;
    }
  }
  return @result;
}

1;

=pod

=head1 SEE ALSO

You can allow the Lottery to pick numbers for you, which will save you from having to put numbers you picked onto the slips for the lottery machines. If you search the web and the mobile app stores, you'll find plenty of sites and apps that do similar things to this module.

=head1 AUTHOR

John Karr L<BRAINBUZ|https://metacpan.org/author/BRAINBUZ>

=head2 CONTRIBUTORS

=head1 BUGS

Please report any bugs or feature requests through the web interface at L<https://bitbucket.org/brainbuz/lottery/issues>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

=head1 TODO

The most interesting thing todo would be to use an API to get winning ticket data. There are several providers that require registering for a key. The author would prefer to use a keyless api, preferably provided by the major games or one of the state lotteries. The major games do not provide such an API.

=head1 LICENSE AND COPYRIGHT

Copyright 2023 John Karr.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see L<http://www.gnu.org/licenses/>.

=cut
