use blackjack::card::Card; use blackjack::game::{EndState, PlayResult, PlayerChoice, PlayerTurn, Table}; use blackjack::hand::Hand; use clap::{Parser, ValueEnum}; use console::Term; use std::env; use std::fs; use std::io; use std::io::prelude::*; use std::path::PathBuf; fn main() -> anyhow::Result<()> { // TODO: Use anyhow for error handling, get rid of unwraps let args = Args::parse(); match args.mode { Mode::Interactive => interactive_play(args), Mode::OldMan => old_man(args), } } #[derive(ValueEnum, Clone, Debug)] enum Mode { Interactive, OldMan, } #[derive(Parser, Debug)] #[command(version, about)] struct Args { #[arg(long, default_value = "interactive")] mode: Mode, #[arg(long, default_value_t = false)] show_count: bool, #[arg(long, default_value_t = 6)] decks: u8, } fn interactive_play(args: Args) -> anyhow::Result<()> { // TODO: Make a way to reset bank let term = Term::stdout(); let mut bank: u32 = load_bank()?.unwrap_or(1_000); let mut table = Table::new(args.decks, bank).with_hit_on_soft_17(); let mut last_bet: Option = None; loop { println!("\nMoney in the bank: ${bank}"); // TODO: show normalized card count if args.show_count { println!( "Card count: {} ({})", table.count(), table.count() / args.decks as i16 ); println!("Cards in shoe: {}", table.shoe_count()); } // Bet checking loop let mut bet: u32; loop { if last_bet.is_some() { let last_bet = last_bet.unwrap(); print!("Your bet [{}]? ", last_bet); io::stdout().flush().unwrap(); let input = term.read_line()?; if input.is_empty() { bet = last_bet; } else { match input.parse() { Ok(b) => bet = b, Err(_) => continue, } } } else { print!("Your bet? "); io::stdout().flush()?; match term.read_line()?.parse() { Ok(b) => bet = b, Err(_) => continue, } } if bet <= bank { break; } else { println!("You don't have enough money!\n"); } } last_bet = Some(bet); println!(); let mut turn = table.deal_hand(bet); let mut split_turn = interactive_play_turn(&mut turn, &mut table, &args)?; if let Some(st) = &mut split_turn { interactive_play_turn(st, &mut table, &args)?; } table.dealers_turn(&turn)?; term.clear_screen()?; let result = table.results(turn)?; print_result(&result); if let Some(st) = split_turn { println!(); print_result(&table.results(st)?); } table.end_game()?; bank = table.player_chips(); save_bank(bank)?; if bank == 0 { println!("You're out of money."); break; } } Ok(()) } /// Play a turn. Returns another turn if this one was split fn interactive_play_turn( turn: &mut PlayerTurn, table: &mut Table, args: &Args, ) -> anyhow::Result> { let mut initial_play = !turn.was_split; let mut other_turn = None; let term = Term::stdout(); loop { term.clear_screen()?; let hand = turn.player_hand(); if turn.shuffled { println!("Deck was shuffled"); } if args.show_count { println!( "Card count: {} ({})", table.count(), table.count() / args.decks as i16 ); } println!("Your bet: ${}", turn.bet); println!("Dealer showing: {}", table.dealer_showing()); println!("Your hand: {hand}"); println!(); if hand.value() == 21 { println!("You have 21, standing."); table.stand(turn)?; break; } else if hand.value() > 21 { println!("You busted"); table.stand(turn)?; break; } let mut msg = String::from("(h)it, (s)tand"); if initial_play { msg += ", (d)ouble-down"; if hand.is_pair() { msg += ", s(p)lit"; } } msg += "? "; io::stdout().write_all(msg.as_bytes())?; io::stdout().flush()?; let mut stop = false; match term.read_char()? { 'h' => { table.hit(turn)?; initial_play = false; } 's' => { table.stand(turn)?; stop = true; initial_play = false; } 'd' if initial_play => { table.double_down(turn)?; stop = true; initial_play = false; } 'p' if initial_play && turn.player_hand().is_pair() => { other_turn = Some(table.split(turn)?); initial_play = false; } _ => {} }; if stop { break; } } Ok(other_turn) } fn print_result(result: &EndState) { println!( "Dealer's hand: {} = {}", result.dealer_hand, result.dealer_hand.value() ); println!( "Your hand: {} = {}", result.player_hand, result.player_hand.value() ); match result.result { PlayResult::Win => println!("You won!"), PlayResult::Blackjack => println!("Blackjack!"), PlayResult::Lose => println!("You lost"), PlayResult::Push => println!("Push"), PlayResult::DealerBlackjack => println!("Dealer got blackjack"), PlayResult::Bust => println!("You busted"), } println!("You got: ${}", result.returns); } fn basic_strategy( hand: &Hand, dealer_showing: &Card, can_split: bool, can_dd: bool, ) -> PlayerChoice { // Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy let dv = u8::from(dealer_showing); if hand.is_pair() && can_split { // Got a pair, maybe should split // We check can_split right at the get-go, because if we can't we just follow the Hard // Total table. match hand.value() { 12 if hand.has_ace() => PlayerChoice::Split, 20 => PlayerChoice::Stand, 18 => match dv { 2..=6 => PlayerChoice::Split, 7 => PlayerChoice::Stand, _ => PlayerChoice::Split, }, 16 => PlayerChoice::Split, 14 => match dv { 2..=7 => PlayerChoice::Split, _ => PlayerChoice::Hit, }, 12 => match dv { 2..=6 => PlayerChoice::Split, _ => PlayerChoice::Hit, }, 10 => match dv { 2..=9 if can_dd => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, }, 8 => match dv { 5..=6 => PlayerChoice::Split, _ => PlayerChoice::Hit, }, 4..=6 => match dv { 2..=7 => PlayerChoice::Split, _ => PlayerChoice::Hit, }, _ => PlayerChoice::Stand, } } else if hand.has_ace() && hand.cards().len() == 2 { // Ace and some other card match hand.value() { 20 => PlayerChoice::Stand, 19 if dv == 6 && can_dd => PlayerChoice::DoubleDown, 19 => PlayerChoice::Stand, 18 => match dv { 2..=6 if can_dd => PlayerChoice::DoubleDown, 9..=11 => PlayerChoice::Hit, _ => PlayerChoice::Stand, }, 17 => match dv { 3..=6 if can_dd => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, }, 15..=16 => match dv { 4..=6 if can_dd => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, }, 13..=14 => match dv { 5..=6 if can_dd => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, }, 12 if dv == 6 && can_dd => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, } } else { match hand.value() { 17..=21 => PlayerChoice::Stand, 13..=16 if dv >= 7 => PlayerChoice::Hit, 13..=16 => PlayerChoice::Stand, 12 => match dv { 4..=6 => PlayerChoice::Stand, _ => PlayerChoice::Hit, }, 11 if can_dd => PlayerChoice::DoubleDown, 11 => PlayerChoice::Hit, 10 => match dv { 2..=9 if can_dd => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, }, 9 => match dv { 3..=6 if can_dd => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, }, _ => PlayerChoice::Stand, } } } fn basic_strategy_play_turn( turn: &mut PlayerTurn, table: &mut Table, ) -> anyhow::Result> { let mut other_turn: Option = None; loop { // Enough chips to split or double down? let enough_chips = turn.bet <= table.player_chips(); let can_split = !turn.was_split && enough_chips && turn.player_hand().is_pair(); let can_dd = enough_chips && turn.player_hand().cards().len() == 2; match basic_strategy( &turn.player_hand(), &table.dealer_showing(), can_split, can_dd, ) { PlayerChoice::Hit => table.hit(turn)?, PlayerChoice::Stand => { table.stand(turn)?; break; } PlayerChoice::DoubleDown => { table.double_down(turn)?; break; } PlayerChoice::Split => { other_turn = Some(table.split(turn)?); } } } Ok(other_turn) } /// An cab driver to the LAS airport was a former dealer and told me about this retired guy who /// would come play Blackjack every day at 7am like clockwork. He'd bring $400 to the table, play /// basic strategy, and would walk away once he was $100 up. He made about $3000/mo off of this, in /// addition to points/comps which he would use to eat brunch for free. /// /// Does this actually work, or was the driver full of it? fn old_man(args: Args) -> anyhow::Result<()> { const START: u32 = 100_000; const PER_DAY: u32 = 400; const MIN_BET: u32 = 15; // Walk away when we're up by this much const MAX_WIN: u32 = 100; // Walk away when we're down by this much const MAX_LOSS: u32 = 100; // Run sim for this many days const DAYS: u16 = 30; let mut bank = START; println!("Starting with bank: ${bank}"); // Let's simulate this for 30 days for day in 1..=DAYS { bank -= PER_DAY; let mut table = Table::new(args.decks, PER_DAY).with_hit_on_soft_17(); let mut rounds = 0; while table.player_chips() > MIN_BET && table.player_chips() < (PER_DAY + MAX_WIN) && table.player_chips() > (PER_DAY - MAX_LOSS) { let mut turn = table.deal_hand(MIN_BET); let mut split_turn = basic_strategy_play_turn(&mut turn, &mut table)?; if let Some(st) = &mut split_turn { basic_strategy_play_turn(st, &mut table)?; } table.dealers_turn(&turn)?; if let Some(st) = split_turn { table.results(st)?; } table.results(turn)?; // No need to handle result since we take chips off table at end of day table.end_game()?; rounds += 1; } bank += table.player_chips(); println!( "Day {day}, after {rounds} rounds, returns: ${}, bank: ${}", table.player_chips() as i32 - PER_DAY as i32, bank, ); } let pl: i64 = bank as i64 - START as i64; println!("Ending with: ${bank}"); println!("Profit/Loss: ${pl}"); Ok(()) } fn data_dir() -> PathBuf { env::home_dir().unwrap().join(".local/share/blackjack") } fn save_bank(bank: u32) -> anyhow::Result<()> { ensure_data_dir()?; let bank_path = data_dir().join("bank.txt"); Ok(fs::write(bank_path, bank.to_string())?) } fn load_bank() -> anyhow::Result> { let bank_path = data_dir().join("bank.txt"); if fs::exists(&bank_path)? { let bank = fs::read_to_string(&bank_path)?.parse()?; if bank > 0 { Ok(Some(bank)) } else { Ok(None) } } else { Ok(None) } } fn ensure_data_dir() -> anyhow::Result<()> { if !fs::exists(data_dir())? { fs::create_dir_all(data_dir())?; } Ok(()) } #[cfg(test)] mod tests { use super::*; use blackjack::card::Suit; #[test] /// Test edge cases I found where my basic strategy impl screwed up fn basic_strategy_edge_cases() { assert_eq!( basic_strategy( &Hand::from( [ Card::new(Suit::Heart, "A"), Card::new(Suit::Heart, "6"), Card::new(Suit::Heart, "Q"), ] .to_vec() ), &Card::new(Suit::Heart, "J"), true, true, ), PlayerChoice::Stand, ); // Always split aces assert_eq!( basic_strategy( &Hand::from([("", "A"), ("", "A")].to_vec()), &Card::from(("", "2")), true, true, ), PlayerChoice::Split, ); } }