11
\$\begingroup\$

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.

odds/winning

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 option

  • All 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:

  1. 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

\$\endgroup\$
0

1 Answer 1

6
\$\begingroup\$

Overall, I found your code to be pretty easy to follow and understand. You stated your assumptions in your prose and most of them in the code (you could state a few more in the code too... ^_^).

  1. [Good practices] You don't have any tests. That makes it very hard to make changes and be sure that the right behavior keeps happening. Because of that, all of the changes I made are only guaranteed to compile, not to actually be correct.

  2. [Efficiency] The readable method is comparatively inefficient and overly broad. You force people to provide an allocated String, then build another string anyway, ignoring the allocated space you already have. You also always push to the beginning of the array, which forces moving subsequent values down, causing an O(N^2) run time. You return a String, but if you implemented something that wrote to a Write, it would be more flexible and potentially avoid some allocation.

  3. [Style] You need to use indentation better. As an example:

    let purchase_output = format!("Checking ticket {}/{} [{}%]", readable(x.to_string()),
    readable(tickets.len().to_string()), x * 100 / tickets.len());
    

    This is impossible to tell that it is one line unless you read it very carefully. Don't force your readers to read very carefully.

  4. [Style] Use implicit returns. You should only have a return if you are exiting early from a method.

  5. [Efficiency, Compatibility] Don't take a String or a Vec if you are not going to use that allocated space. Basically never accept an &String or &Vec either. Instead, accept a &str or &[T]. This means consumers can reuse allocated memory they already have, and you can accept a broader range of input types.

  6. [Style] Don't use redundant type specifications:

    let mut s: String = String::new();
    

    The first String is redundant.

  7. [Usability] I cannot ^C or exit your program in any easy way. Killing the program from another window leaves my terminal in a bad state.

  8. [Efficiency] Allocating a String just to then append into another string is wasteful. Use write! instead to format directly into the target string.

  9. [Good practices] Instead of adding a comma and then removing it, it's probably easier to just not add the comma if it's not needed.

  10. [Idiomatic] There's no need for a mut keyword if the value is set only once. This includes when it is set in branches of an if statement:

    let foo;
    if condition {
        foo = true;
        // other stuff
    } else {
        foo = false;
        // other stuff
    }
    
  11. [idiomatic] Use Rng::gen_range to sample from a range, instead of the longer way.

  12. [Idiomatic] itertools is pretty great. It provides a one-line solution for readable_whites and provides a unique used elsewhere.

  13. [Good practices] It's safer to use a range of 0-100 and then check if the value is < 15. This allows the 15% number to be encoded directly into the source.

  14. [Good practices] Remove all those magic numbers to constants! Especially when the value is used from multiple places (jackpot, second place!).

  15. [Style] Use spaces around and inside of {} when used as a struct constructor.

  16. [Good practices] Implement existing traits to be flexible. You are randomly generating a ticket, so implement Rand. This has the benefit that your code is no longer tied to thread_rng!.

  17. [Good practices, Efficiency] Don't use signed integers when you have unsigned data, don't use 32 bits when 8 will do.

  18. [Good practices] Don't add useless documentation. "Counts the matches between two tickets" on a method called count_matches that takes two tickets is very redundant. Describe the why not the how or what. When you have useful documentation, place it before the method and make it a doc comment (///).

  19. [Good practices] There's rarely a reason to use sentinel values like -1! Use an Option or a Result instead. In this case, you could just use 0 because the winning amount is 0.

  20. [Idiomatic] You can match on a tuple of things. This cleans up the base pricing cases.

  21. [Bug] What happens if you have a powerplay and hit the jackpot? It looks like you will multiply the jackpot... Mistake I made while changing the code!

  22. [Style] hit_jackpot doesn't need to be mutable, or even a variable.

  23. [Style] In-place mutation operators like +=, *= exist. You use these some places and not others.

  24. [Efficiency] You can directly push a character onto a string, no need to to_string and allocate more memory.

  25. [Good practices] Short names like pp or pb are annoying to read and understand. Use real names.

  26. [Style] Use an is_ prefix (or similar) for boolean variables.

  27. [Bug] According to Wikipedia, Power Play can be 2×, 3×, 4×, 5×, 10×, but you have 1,2,3,4...

  28. [Style] as_ref is unusual; normally you just take the reference with &.

  29. [Idiomatic] Some methods seem better suited as inherent methods instead of free functions.

  30. [Efficiency] Why store an array of i32 and then convert them to string? Just do it in one go.

  31. [Idiomatic] Use String::new instead of "".to_string.

  32. [Idiomatic] Use type elision (Vec<_>) when parts of the type can be inferred.

  33. [Good practices] Jackpot is used just to put values in and then take right back out? Seems useless.

  34. [Idiomatic] 1..max+1 is awkward. just do 0..max.

  35. [Efficiency] Calling String::to_string, which is redundant.

  36. [Idiomatic] Unneeded if/else on the increment_amount.

  37. [Idiomatic] Use min for capping values.

  38. [Style] It's dangerous to use type names (char) as variable names!

I ran out of steam here, but I'm sure someone else can chime in with some more tips!

extern crate rand;
extern crate ncurses;
extern crate postgres;
extern crate itertools;

use std::cmp::min;

use postgres::{Connection, SslMode};
use ncurses::*;
use std::char;
use rand::{Rng, Rand};
use itertools::Itertools;

struct Ticket {
    whites: Vec<u8>,
    power_ball: u8,
    is_power_play: bool,
}

// This is probably an incorrect assumption but for this simulation it will work.
const POWER_PLAY_PERCENTAGE: u8 = 15;
const WHITE_MIN: u8 = 1;
const WHITE_MAX: u8 = 69;
const POWER_BALL_MIN: u8 = 1;
const POWER_BALL_MAX: u8 = 26;

impl Rand for Ticket {
    fn rand<R: Rng>(rng: &mut R) -> Self {
        let pp_guess = rng.gen_range(0, 100);
        let pp_value = pp_guess < POWER_PLAY_PERCENTAGE;

        let mut whites_vec: Vec<_> = (0..).map(|_| rng.gen_range(WHITE_MIN, WHITE_MAX + 1)).unique().take(5).collect();
        whites_vec.sort();

        let pb_value = rng.gen_range(POWER_BALL_MIN, POWER_BALL_MAX + 1);

        Ticket { whites: whites_vec, power_ball: pb_value, is_power_play: pp_value }
    }
}

const SECOND_PLACE: i64 = 1_000_000;
const JACKPOT: i64 = 50_000_000;

impl Ticket {
    fn matching_white_numbers(&self, winner: &Ticket) -> i32 {
        let mut matching_white_numbers = 0;
        for i in 0..5 {
            for x in 0..5 {
                if self.whites[i] == winner.whites[x] {
                    matching_white_numbers += 1;
                }
            }
        }
        matching_white_numbers
    }

    fn calculate_winnings(&self, winner: &Ticket, power_play: i64) -> i64 {
        let hit_pb = self.power_ball == winner.power_ball;
        let n_matches = self.matching_white_numbers(winner);

        let mut winnings = match (n_matches, hit_pb) {
            (0, true)  => 4,
            (1, true)  => 4,
            (2, true)  => 7,
            (3, false) => 7,
            (3, true)  => 100,
            (4, false) => 100,
            (4, true)  => 50_000,
            (5, false) => SECOND_PLACE,
            (5, true)  => JACKPOT,
            _          => 0,
        };

        // Calculate power play
        if self.is_power_play {
            if winnings == SECOND_PLACE {
                winnings *= 2;
            } else {
                winnings *= power_play;
            }
        }

        winnings
    }
}

/// Goes through all tickets in this drawing assessing total winnings
fn compare_tickets(tickets: &[Ticket], winner: &Ticket) -> (i64, bool) {
    let mut winnings = 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 mut rng = rand::thread_rng();
    let pp_value = rng.gen_range(1, 5);

    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);
            refresh();
        }

        let winnings_this_ticket = ticket.calculate_winnings(winner, pp_value);
        if winnings_this_ticket == JACKPOT {
            return (winnings, true)
        } else {
            winnings += winnings_this_ticket;
        }
    }

    (winnings, false)
}

fn setup_curses() {
    initscr();
    raw();
    keypad(stdscr, true);
    noecho();
}

fn get_input(prompt: &str) -> String {
    let mut input = String::new();
    printw(prompt);
    loop {
        echo();
        let ch = getch();
        if ch == KEY_ENTER || ch == 10 {
            break;
        } else {
            input.push(char::from_u32(ch as u32).expect("Invalid Char"));
        }
    }
    clear();

    input
}

struct Stats {
    games_played: u32,
    total_spent: i64,
    total_loss: i64,
    total_winnings: i64, // Only used if you are just watching the screen
    total_tickets: i64,
    total_power_plays: i64, // Only used if you are just watching the screen
    jackpot: i64,
}

impl Stats {
    fn new() -> Stats {
        Stats {
            games_played: 0,
            total_spent: 0,
            total_loss: 0,
            total_winnings: 0,
            total_tickets: 0,
            total_power_plays: 0,
            jackpot: 40_000_000,
        }
    }

    fn add_ticket(&mut self, is_power_play: bool) {
        self.total_tickets += 1;

        if is_power_play {
            self.total_spent += 3;
            self.total_loss -= 3;
            self.jackpot += 2;
            self.total_power_plays += 1;
        } else {
            self.total_spent += 2;
            self.total_loss -= 2;
            self.jackpot += 1;
        }
    }

    fn output_current_values(&self) {
        let jp_output = format!("Current jackpot {}", readable(self.jackpot.to_string()));
        mvprintw(0, 0, &jp_output);

        let gp_output = format!("Games played {}", readable(self.games_played.to_string()));
        mvprintw(1, 0, &gp_output);

        let s_output = format!("Spent {}", readable(self.total_spent.to_string()));
        mvprintw(2, 0, &s_output);

        let l_output = format!("Loss {}", readable(self.total_loss.to_string()));
        mvprintw(3, 0, &l_output);

        let tt_output = format!("Tickets purchased {}", readable(self.total_tickets.to_string()));
        mvprintw(4, 0, &tt_output);

        let tt_output = format!("Powerplay Tickets {}", readable(self.total_power_plays.to_string()));
        mvprintw(5, 0, &tt_output);

        let tw_output = format!("Total winnings {}", readable(self.total_winnings.to_string()));
        mvprintw(6, 0, &tw_output);

        refresh();
    }
}

fn readable(mut o_s: String) -> String {
    let mut s = String::new();
    let negative;
    let values: Vec<_> = o_s.chars().collect();

    if values[0] == '-' {
        o_s.remove(0);
        negative = true;
    } else {
        negative = false;
    }

    for (i, c) in o_s.chars().rev().enumerate() {
        if i % 3 == 0 && i != 0 {
            s.insert(0, ',');
        }
        s.insert(0, c);
    }

    if negative {
        s.insert(0, '-');
    }

    s
}

fn readable_whites(orig_vec: &[u8]) -> String {
    orig_vec.iter().join(",")
}

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;
    let ticket_incr_percentage = 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 += incr_amount;
        }
    } else {
        incr_num_tickets = false;
    }

    let mut stats = Stats::new();

    let mut rng = rand::thread_rng();

    loop {
        // Generate tickets before drawing
        let tickets: Vec<_> = rng.gen_iter::<Ticket>()
            .take(num_tickets as usize)
            .enumerate()
            .inspect(|&(x, ref t)| {
                stats.add_ticket(t.is_power_play);

                if x % 10_000 == 0 {
                    clear();
                    stats.output_current_values();
                    let purchase_output = format!(
                        "Purchasing ticket {}/{} [{}%]",
                        readable(x.to_string()),
                        readable(num_tickets.to_string()),
                        x * 100 / num_tickets as usize
                    );
                    mvprintw(8, 0, &purchase_output);
                    refresh();
                }
            })
            .map(|(_, t)| t)
            .collect();

        let purchase_out = format!("Purchasing ticket {0}/{0} [100%]", readable(num_tickets.to_string()));
        mvprintw(8, 0, &purchase_out);
        refresh();

        // Do 'drawing'
        let winner = rng.gen();

        // Compare tickets with winnings numbers
        let (winnings, hit_jp) = compare_tickets(&tickets, &winner);

        if hit_jp {
            stats.total_loss += stats.jackpot;

            pg_conn.execute(
                "INSERT INTO jackpot (
                   whites, pb, jackpot, games_played, total_tickets, total_loss
                 ) VALUES ($1, $2, $3, $4, $5, $6)",
                &[&readable_whites(&winner.whites),
                  &(winner.power_ball as i32),
                  &stats.jackpot,
                  &stats.games_played,
                  &stats.total_tickets,
                  &stats.total_loss]
            ).unwrap();

            stats = Stats::new();
            num_tickets = orig_num_tickets;
        } else {
            stats.games_played += 1;
            stats.total_loss += winnings;
            stats.total_winnings += winnings;
            stats.jackpot -= winnings;
        }

        if incr_num_tickets && num_tickets < max_num_tickets && !hit_jp {
            num_tickets += (num_tickets as f64 * ticket_incr_percentage) as i64;

            num_tickets = min(num_tickets, max_num_tickets);
        }
    }
}

fn main() {
    let pg_conn = Connection::connect("postgres://lotto:lotto@127.0.0.1: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 = 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();
}

How does the inspect and map [ticket generation] section work?

I love me some iterators. Perhaps a bit too much, sometimes. This may be a case where I overused them, but it was also a bit fun to play with. Let's break it down line-by-line:

  1. rng.gen_iter — Creates an iterator that will yield an infinite number of randomly-generated items. Because Ticket implements Rand, we get to use this for free!
  2. take — Only take the first N items from the iterator. This makes the infinite sequence into a finite one.
  3. enumerate — Modify each iterated item by adding an index to each result value.
  4. inspect — Perform some side-effect using each iterated item.
  5. map — Modify each iterated item by removing the index, undoing the previous enumerate.
  6. collect — bundle up each item into some collection object.

The biggest benefit to doing this (other than that I like iterators) is Iterator::size_hint. When creating a collection from the iterator, that collection can use size_hint to pre-allocate the correct amount of space. In this case, that should be exact and we will only allocate the space backing the Vec exactly once, and not have any additional reallocations.

\$\endgroup\$
6
  • \$\begingroup\$ First Impressions: Wow, I suck! :) I really love your implementation of Rand for ticket and the Stats struct. And the way you use multiple values in the match is so much cleaner. Really is soooo much better. I will give more comments later as I am able to go through and look at this more intensely. The Jackpot powerplay bug you mention is only in your code. Mine is very unclear but it does return before calculating the powerplay. \$\endgroup\$ Jan 13, 2016 at 18:05
  • \$\begingroup\$ @electrometro "Wow, I suck"Absolutely not. You are on a site asking for feedback on your code; think how many thousands (millions?) of developers are not doing something similar. I've been doing Rust for a while now and programming for ~10 years, so it's only natural I have a few tips to hand out ^_^. \$\endgroup\$
    – Shepmaster
    Jan 13, 2016 at 18:16
  • \$\begingroup\$ @electrometro added some text about the inspect and map. \$\endgroup\$
    – Shepmaster
    Jan 14, 2016 at 15:24
  • \$\begingroup\$ That is a really clear explanation and I feel I really understand the difficult parts now. |&(x, ref t)| says give me the index of the iteration or enumeration at variable x and the actual reference to the ticket at ref t or just t. Then later in the map you are going through and removing the x from the iterator to collect everything back up and ignore the index. Correct? \$\endgroup\$ Jan 14, 2016 at 15:58
  • \$\begingroup\$ @electrometro that's correct. \$\endgroup\$
    – Shepmaster
    Jan 14, 2016 at 15:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.