casino 0.3.0

A casino built right into your terminal
Documentation
use std::{fmt::Display, thread::sleep, time::Duration};

use anyhow::{Context, Result};
use colored::Colorize;
use inquire::{Select, Text};
use num::rational::Ratio;
use spinners::{Spinner, Spinners};

use crate::{
    cards::{Card, Rank, Shoe},
    money::Money,
    Casino,
};

#[derive(Clone, Debug)]
pub struct Baccarat {
    bet_type: BaccaratBet,
    bet_amount: Money,
    player: Vec<Card>,
    banker: Vec<Card>,
    shoe: Shoe,
}

impl Baccarat {
    pub fn new(bet_type: &BaccaratBet, bet_amount: &Money) -> Self {
        Self {
            bet_type: bet_type.clone(),
            bet_amount: *bet_amount,
            shoe: Shoe::new(8, 0.75),
            player: Vec::new(),
            banker: Vec::new(),
        }
    }

    pub fn deal_cards(&mut self) {
        self.player.push(self.shoe.draw_card());
        self.banker.push(self.shoe.draw_card());
        self.player.push(self.shoe.draw_card());
        self.banker.push(self.shoe.draw_card());
    }

    pub fn draw_player_card(&mut self) {
        self.player.push(self.shoe.draw_card());
    }

    pub fn draw_banker_card(&mut self) {
        self.banker.push(self.shoe.draw_card());
    }

    pub fn print_hands(&self) {
        print!("Player: ");
        for card in self.player.iter() {
            print!("{}", card);
        }
        print!(" ({})", self.player.baccarat_sum());

        print!("  Banker: ");
        for card in self.banker.iter() {
            print!("{}", card);
        }
        print!(" ({})", self.banker.baccarat_sum());
        println!();
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BaccaratBet {
    Player,
    Banker,
    Tie,
}

impl Display for BaccaratBet {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Player => write!(f, "Player (1:1 payout)"),
            Self::Banker => write!(f, "Banker (1:1 payout, -5% commission)"),
            Self::Tie => write!(f, "Tie (8:1 payout)"),
        }
    }
}

pub trait BaccaratValue {
    fn baccarat_value(&self) -> u8;
}

impl BaccaratValue for Card {
    fn baccarat_value(&self) -> u8 {
        match &self.rank {
            Rank::Ace => 1,
            Rank::Two => 2,
            Rank::Three => 3,
            Rank::Four => 4,
            Rank::Five => 5,
            Rank::Six => 6,
            Rank::Seven => 7,
            Rank::Eight => 8,
            Rank::Nine => 9,
            Rank::Ten | Rank::Jack | Rank::Queen | Rank::King => 0,
        }
    }
}

pub trait BaccaratSum {
    fn baccarat_sum(&self) -> u8;
}

impl<T> BaccaratSum for Vec<T>
where
    T: BaccaratValue,
{
    fn baccarat_sum(&self) -> u8 {
        let mut sum = 0;
        for bv in self {
            sum += bv.baccarat_value();
        }

        sum % 10
    }
}

pub fn play_baccarat() -> Result<()> {
    let mut casino = Casino::from_filesystem()?;

    println!("Your money: {}", casino.bankroll);

    let opts = vec![BaccaratBet::Player, BaccaratBet::Banker, BaccaratBet::Tie];
    let bet_type = Select::new("What bet do you want to make?", opts).prompt()?;

    let bet_amount: Money;

    loop {
        let bet_text = Text::new("How much will you bet?").prompt()?;

        let bet = bet_text
            .trim()
            .parse::<Money>()
            .with_context(|| "Failed to parse prompt text into an integer")?;

        if casino.bankroll >= bet {
            bet_amount = bet;
            break;
        } else {
            println!("You can't bet that amount, try again.");
        }
    }

    println!("Betting {} on {}", bet_amount, bet_type);

    let mut sp = Spinner::new(Spinners::Dots, "Dealing cards...".into());
    sleep(Duration::from_millis(1_500));
    sp.stop_with_message(format!("{}", "* The dealer issues your cards.".dimmed()));

    let mut game = Baccarat::new(&bet_type, &bet_amount);

    game.deal_cards();

    game.print_hands();

    if game.player.baccarat_sum() < 8 && game.banker.baccarat_sum() < 8 {
        if game.player.baccarat_sum() <= 5 {
            let mut sp = Spinner::new(Spinners::Dots, "Dealing cards...".into());
            sleep(Duration::from_millis(500));
            sp.stop_with_message(format!(
                "{}",
                "* The dealer issues another card to the Player hand".dimmed()
            ));
            game.draw_player_card();
        } else {
            let mut sp = Spinner::new(Spinners::Dots, "Dealing cards...".into());
            sleep(Duration::from_millis(500));
            sp.stop_with_message(format!("{}", "* The Player hand stands".dimmed()));
        }

        let banker_draw = (game.banker.baccarat_sum() <= 2)
            || (game.banker.baccarat_sum() == 3 && game.player.baccarat_sum() != 8)
            || (game.banker.baccarat_sum() == 4 && (2..7).contains(&game.player.baccarat_sum()))
            || (game.banker.baccarat_sum() == 5 && (4..7).contains(&game.player.baccarat_sum()))
            || (game.banker.baccarat_sum() == 5 && (6..7).contains(&game.player.baccarat_sum()));

        if banker_draw {
            let mut sp = Spinner::new(Spinners::Dots, "Dealing cards...".into());
            sleep(Duration::from_millis(500));
            sp.stop_with_message(format!(
                "{}",
                "* The dealer issues another card to the Banker hand".dimmed()
            ));
            game.draw_banker_card();
        } else {
            println!("{}", "* The Banker hand stands".dimmed());
        }

        game.print_hands();
    }

    if game.player.baccarat_sum() > game.banker.baccarat_sum() {
        let mut sp = Spinner::new(Spinners::Dots, "Calculating results...".into());
        sleep(Duration::from_millis(1000));
        sp.stop_with_message(format!("{}", "Player wins!".bold()));

        if game.bet_type == BaccaratBet::Player {
            casino.add_bankroll(game.bet_amount);
            println!(
                "YOU WIN! You receive {}. You now have {}",
                game.bet_amount, casino.bankroll
            );
            casino
                .stats
                .baccarat
                .record_win(&game.bet_type, game.bet_amount);
        } else {
            casino.subtract_bankroll(game.bet_amount)?;
            casino
                .stats
                .baccarat
                .record_loss(&game.bet_type, game.bet_amount);
            println!(
                "You lose {}. You now have {}",
                game.bet_amount, casino.bankroll
            )
        }
    } else if game.banker.baccarat_sum() > game.player.baccarat_sum() {
        let mut sp = Spinner::new(Spinners::Dots, "Calculating results...".into());
        sleep(Duration::from_millis(1000));
        sp.stop_with_message(format!("{}", "Banker wins!".bold()));

        if game.bet_type == BaccaratBet::Banker {
            let win_amount = game.bet_amount * Ratio::new(19, 20);

            casino.add_bankroll(win_amount);
            casino.stats.baccarat.record_win(&game.bet_type, win_amount);
            println!(
                "YOU WIN! You receive {} (even money minus 5% commission). You now have {}",
                win_amount, casino.bankroll
            )
        } else {
            casino.subtract_bankroll(bet_amount)?;
            casino
                .stats
                .baccarat
                .record_loss(&game.bet_type, game.bet_amount);
            println!(
                "You lose {}. You now have {}",
                game.bet_amount, casino.bankroll
            )
        }
    } else if game.player.baccarat_sum() == game.banker.baccarat_sum() {
        let mut sp = Spinner::new(Spinners::Dots, "Calculating results...".into());
        sleep(Duration::from_millis(1000));
        sp.stop_with_message(format!("{}", "Player and Banker tie!".bold()));

        casino.stats.baccarat.record_tie(&game.bet_type);

        println!(
            "You receive your {} bet back. You still have {}",
            game.bet_amount, casino.bankroll
        )
    }

    casino.check_for_mister_green();

    casino.save();

    Ok(())
}