With the recent craziness of the Powerball in the US, I got interested in building a little lotto simulator to see how frequently I could win with purchasing large amounts of tickets.
For those not familiar with the game and its odds and ways of winning, a Powerball ticket consists of 5 white balls between 1 and 69 and 1 red ball between 1 and 26. Below is a chart that shows the ways to win with those numbers. The left side is what you would have to match, the right side is the odds of getting that match. For example the jackpot you have to match all 5 white balls and the red ball (order doesn't matter) and the odds are 1 in 292,201,338.
Image source Kansas Lottery
A few things not taken into account in my program:
- No 10X multiplier for power play
- Multiple winner split of jackpot
- Invalid characters in the input (backspacing)
Assumptions I have made in the code:
15% of ticket sales are with the
Power Play
optionAll winnings are removed from the jackpot. I am not 100% sure where the small winnings come from in the real world, but here I am assuming they are removed from the jackpot prior to the next drawing.
Nothing but ticket purchases and the original 40M are added to the jackpot. I am not sure if anything is added between drawings in the real world, but here I am assuming that there is nothing.
Things I am looking for in my review:
- Confirmation I am doing the random number generation correctly. Sometimes I feel like I am winning too frequently (less than 100M jackpots..)
- Following best practices of either the language or general software design patterns including code styling
- Bugs/Mistakes
- Faster implementations
- Clearer implementations
Bugs found:
- The
Range
used in the generation of numbers was not inclusive.
I have been starting off at 250K tickets and allowing for incremental increases of the amount of ticket purchases. Typically by the time it starts taking awhile to generate tickets it has a winner. Your experiences may differ.
extern crate rand;
extern crate ncurses;
extern crate postgres;
use postgres::{Connection, SslMode};
use ncurses::*;
use std::char;
use rand::distributions::{IndependentSample, Range};
struct Ticket {
whites: Vec<i32>,
pb: i32,
pp: bool,
}
struct Jackpot {
// For simplicity with postgres not doing ticket: Ticket
whites: String,
pb: i32,
jackpot: i64,
games_played: u32,
total_tickets: i64,
total_loss: i64
}
fn gen_ticket() -> Ticket {
let mut rng = rand::thread_rng();
// Power play
let between_pp = Range::new(1, 101);
let pp_guess = between_pp.ind_sample(&mut rng);
// Guessing 15% of tickets are power play.
// This is probably an incorrect assumption but for this simulation it will work.
let pp_value = pp_guess >= 85;
// Whites
let mut whites_vec = Vec::new();
let between_white = Range::new(1, 70);
loop {
whites_vec.push(between_white.ind_sample(&mut rng));
// Removing duplicates
whites_vec.sort();
whites_vec.dedup();
// Stop once all are collected
if whites_vec.len() == 5 { break; }
}
// Power ball
let between_pb = Range::new(1, 27);
let pb_value = between_pb.ind_sample(&mut rng);
return Ticket{whites: whites_vec, pb: pb_value, pp: pp_value};
}
fn matches(t1: &Ticket, winner: &Ticket) -> i32 {
// Counts the matches between two tickets.
let mut matches: i32 = 0;
for i in 0..5 {
for x in 0..5 {
if t1.whites[i] == winner.whites[x] {
matches = matches + 1;
}
}
}
return matches;
}
fn compare_ticket(t1: &Ticket, winner: &Ticket, power_play: i64) -> i64 {
// Assesses the amount a ticket wins by comparing a ticket with the winning ticket.
let hit_pb = t1.pb == winner.pb;
let mut winning: i64;
match matches(t1, winner) {
0|1 => {
if hit_pb { winning = 4; } else { winning = 0; }
}
2 => {
if hit_pb { winning = 7; } else { winning = 0; }
}
3 => {
if hit_pb { winning = 100; } else { winning = 7; }
}
4 => {
if hit_pb { winning = 50_000; } else { winning = 100; }
}
5 => {
if hit_pb {
// Jackpot
return 50_000_000;
} else {
// Second prize
winning = 1_000_000;
}
}
_ => {
winning = -1;
}
}
// Calculate power play
if t1.pp {
if winning == 1_000_000 {
winning = 2_000_000;
} else {
winning = winning * power_play;
}
}
return winning;
}
fn compare_tickets(tickets: Vec<Ticket>, winner: &Ticket) -> (i64, bool) {
// Goes through all tickets in this drawing assessing total winnings
let mut hit_jackpot = false;
let mut winnings: i64 = 0;
// Generate power play multiplier.
// Normally this is done during the drawing but since the 'drawing' is just to
// generate a ticket the same way other tickets are generated it must be done here.
let between_pp = Range::new(1, 5);
let mut rng = rand::thread_rng();
let pp_value = between_pp.ind_sample(&mut rng);
for (x, ticket) in tickets.iter().enumerate() {
if x % 10_000 == 0 {
let purchase_output = format!("Checking ticket {}/{} [{}%]", readable(x.to_string()),
readable(tickets.len().to_string()), x * 100 / tickets.len());
mvprintw(9, 0, purchase_output.as_ref());
refresh();
}
let winnings_this_ticket = compare_ticket(&ticket, winner, pp_value);
if winnings_this_ticket == 50_000_000 {
hit_jackpot = true;
return (winnings, hit_jackpot)
} else {
winnings = winnings + winnings_this_ticket;
}
}
return (winnings, hit_jackpot);
}
fn setup_curses() {
initscr();
raw();
keypad(stdscr, true);
noecho();
}
fn get_input(prompt: &str) -> String {
let mut values = Vec::new();
printw(prompt);
loop {
echo();
let ch = getch();
if ch == KEY_ENTER || ch == 10 {
break;
} else {
values.push(ch);
}
}
let mut value = "".to_string();
for x in values {
value = value + char::from_u32(x as u32).expect("Invalid Char").to_string().as_ref();
}
clear();
return value;
}
fn output_current_values(games_played: u32, total_spent: i64, total_loss: i64,
total_winnings: i64, total_tickets: i64, total_power_plays: i64,
jackpot: i64) {
let jp_output = format!("Current jackpot {}", readable(jackpot.to_string()));
mvprintw(0, 0, jp_output.as_ref());
let gp_output = format!("Games played {}", readable(games_played.to_string()));
mvprintw(1, 0, gp_output.as_ref());
let s_output = format!("Spent {}", readable(total_spent.to_string()));
mvprintw(2, 0, s_output.as_ref());
let l_output = format!("Loss {}", readable(total_loss.to_string()));
mvprintw(3, 0, l_output.as_ref());
let tt_output = format!("Tickets purchased {}", readable(total_tickets.to_string()));
mvprintw(4, 0, tt_output.as_ref());
let tt_output = format!("Powerplay Tickets {}", readable(total_power_plays.to_string()));
mvprintw(5, 0, tt_output.as_ref());
let tw_output = format!("Total winnings {}", readable(total_winnings.to_string()));
mvprintw(6, 0, tw_output.as_ref());
refresh();
}
fn readable(mut o_s: String) -> String {
let mut s = String::new();
let mut negative = false;
let values: Vec<char> = o_s.chars().collect();
if values[0] == '-' {
o_s.remove(0);
negative = true;
}
for (i ,char) in o_s.chars().rev().enumerate() {
if i % 3 == 0 && i != 0 {
s.insert(0, ',');
}
s.insert(0, char);
}
if negative {
s.insert(0, '-');
}
return s
}
fn readable_whites(orig_vec: Vec<i32>) -> String {
let mut s: String = String::new();
for x in orig_vec {
s = s + &x.to_string();
s = s + ",";
}
// Get rid of trailing ','
s.pop();
return s;
}
fn play(mut num_tickets: i64, increment_num_tickets: String, pg_conn: Connection) {
let orig_num_tickets = num_tickets;
// Determine if incrementing number of tickets each drawing
let incr_num_tickets: bool;
let ticket_incr_percentage: f64 = 0.03;
let mut max_num_tickets = 35_000_000;
if increment_num_tickets == "Y" || increment_num_tickets == "y" {
incr_num_tickets = true;
// If the amount they ask for is more than the default maximum then
// increase the maximum by 15 of the input
if num_tickets > max_num_tickets {
let incr_amount = (num_tickets as f64 * 0.15) as i64;
max_num_tickets = num_tickets + incr_amount;
}
} else {
incr_num_tickets = false;
}
// Base stats to track
let mut games_played = 0;
let mut total_spent: i64 = 0;
let mut total_loss: i64 = 0;
let mut total_winnings: i64 = 0; // Only used if you are just watching the screen
let mut total_tickets: i64 = 0;
let mut total_power_plays: i64 = 0; // Only used if you are just watching the screen
let mut jackpot: i64 = 40_000_000;
loop {
// Generate tickets before drawing
let mut tickets = Vec::new();
for x in 1..num_tickets + 1 {
let t = gen_ticket();
if t.pp {
total_spent += 3;
total_loss -= 3;
jackpot += 2;
total_power_plays += 1;
} else {
total_spent += 2;
total_loss -= 2;
jackpot += 1;
}
tickets.push(t);
total_tickets += 1;
if x % 10_000 == 0 {
clear();
output_current_values(games_played, total_spent, total_loss,
total_winnings, total_tickets, total_power_plays, jackpot);
let purchase_output = format!("Purchasing ticket {}/{} [{}%]", readable(x.to_string()),
readable(num_tickets.to_string()), x * 100 / num_tickets);
mvprintw(8, 0, purchase_output.as_ref());
refresh();
}
}
let purchase_out = format!("Purchasing ticket {0}/{0} [100%]", readable(num_tickets.to_string()));
mvprintw(8, 0, purchase_out.as_ref());
refresh();
// Do 'drawing'
let winner = gen_ticket();
// Compare tickets with winning numbers
let (winnings, hit_jp) = compare_tickets(tickets, &winner);
if hit_jp {
total_loss += jackpot;
// Send data to postgres
let jp = Jackpot {
whites: readable_whites(winner.whites),
pb: winner.pb,
jackpot: jackpot,
games_played: games_played,
total_tickets: total_tickets,
total_loss: total_loss,
};
let sql_value = "INSERT INTO jackpot (
whites, pb, jackpot, games_played, total_tickets, total_loss
) VALUES ($1, $2, $3, $4, $5, $6)";
pg_conn.execute(sql_value, &[&jp.whites.to_string(), &jp.pb, &jp.jackpot, &jp.games_played,
&jp.total_tickets, &jp.total_loss]).unwrap();
// Reset all stats
games_played = 0;
total_spent = 0;
total_loss = 0;
total_winnings = 0;
total_tickets = 0;
total_power_plays = 0;
jackpot = 40_000_000;
num_tickets = orig_num_tickets;
} else {
games_played += 1;
total_loss += winnings;
total_winnings += winnings;
jackpot -= winnings;
}
if incr_num_tickets && num_tickets < max_num_tickets && !hit_jp {
let incr_amount = (num_tickets as f64 * ticket_incr_percentage) as i64;
if incr_amount > 1 {
num_tickets += incr_amount
} else {
num_tickets += 1
}
if num_tickets > max_num_tickets {
num_tickets = max_num_tickets;
}
}
}
}
fn main() {
let pg_conn: Connection = Connection::connect("postgres://lotto:lotto@172.16.40.128:5432/lotto", SslMode::None).unwrap();
pg_conn.execute("CREATE TABLE IF NOT EXISTS jackpot (
id SERIAL PRIMARY KEY,
whites VARCHAR NOT NULL,
pb INT NOT NULL,
jackpot BIGINT NOT NULL,
games_played OID NOT NULL,
total_tickets BIGINT NOT NULL,
total_loss BIGINT NOT NULL
)", &[]).unwrap();
setup_curses();
let num_tickets: i64 = get_input("How many tickets to purchase at once?: ").parse().expect("Invalid number");
let increment_num_tickets: String = get_input("Increment number of tickets purchased after each drawing? (Y/N): ");
// Done with input, turning echo off.
noecho();
play(num_tickets, increment_num_tickets, pg_conn);
endwin();
}
And of course here is my Cargo.toml
:
[package]
name = "lotto_sim"
version = "0.1.0"
authors = ["me"]
[dependencies]
rand = "*"
ncurses = "*"
postgres = "*"
Largest Jackpot: $1,964,636,418