use blackjack::card::Card; use blackjack::game::{Game, PlayResult, PlayerChoice}; use blackjack::hand::Hand; use std::env; use std::fs; use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; fn main() { // TODO: Use anyhow for error handling, get rid of unwraps // TODO: CLI flags to choose how to play interactive_play(); // old_man(); } fn interactive_play() { // TODO: Persist bank between plays // TODO: Make a way to reset bank // let mut bank: u32 = match load_bank() { Some(b) => b, None => 1_000, }; let mut game = Game::new().with_decks(6).with_hit_on_soft_17(); let mut last_bet: Option = None; loop { println!("\nMoney in the bank: {bank}"); // Bet checking loop let mut bet: u32; loop { if last_bet.is_some() { print!("Your bet [{}]? ", last_bet.unwrap()); io::stdout().flush().unwrap(); let input = read_input(); if input == "" { bet = last_bet.unwrap(); } else { match input.parse() { Ok(b) => bet = b, Err(_) => continue, } } } else { print!("Your bet? "); io::stdout().flush().unwrap(); match read_input().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 result = game.play(&mut bank, bet, interactive_decision); let dealer_hand = Hand::from(result.dealer_cards); let player_hand = Hand::from(result.player_cards); println!("Dealer's hand: {} = {}", dealer_hand, dealer_hand.value()); println!("Your hand: {} = {}", player_hand, 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"), } if result.shuffled { println!("Deck was shuffled at beginning of round"); } save_bank(bank); if bank == 0 { println!("You're out of money."); return; } } } fn interactive_decision(hand: &Hand, dealer_showing: &Card) -> PlayerChoice { println!("\nDealer showing: {dealer_showing}"); println!("Your hand: {hand}\n"); if hand.value() == 21 { println!("You have 21, standing."); return PlayerChoice::Stand; } let choice: PlayerChoice; loop { print!("(h)it, (s)tand, (d)ouble-down? "); io::stdout().flush().unwrap(); choice = match read_input().to_lowercase().as_ref() { "h" => PlayerChoice::Hit, "s" => PlayerChoice::Stand, "d" => PlayerChoice::DoubleDown, _ => continue, }; break; } choice } fn basic_strategy(hand: &Hand, dealer_showing: &Card) -> PlayerChoice { // Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy let dv = u8::from(dealer_showing); let can_dd = hand.cards().len() == 2; if hand.has_ace() && hand.cards().len() == 2 { // Ace and some other card match hand.value() { v if v >= 19 => PlayerChoice::Stand, v if v == 18 && (7..=8).contains(&dv) => PlayerChoice::Stand, v if v == 18 && dv < 7 && can_dd => PlayerChoice::DoubleDown, v if v == 18 && dv < 7 => PlayerChoice::Stand, v if v == 17 && (3..=6).contains(&dv) && can_dd => PlayerChoice::DoubleDown, v if v == 17 && (3..=6).contains(&dv) => PlayerChoice::Hit, v if (15..=16).contains(&v) && (4..=6).contains(&dv) && can_dd => { PlayerChoice::DoubleDown } v if (15..=16).contains(&v) && (4..=6).contains(&dv) => PlayerChoice::Hit, v if (13..=14).contains(&v) && (5..=6).contains(&dv) && can_dd => { PlayerChoice::DoubleDown } v if (13..=14).contains(&v) && (5..=6).contains(&dv) => PlayerChoice::Hit, v if v == 12 && dv == 6 && can_dd => PlayerChoice::DoubleDown, v if v == 12 && dv == 6 => PlayerChoice::Hit, _ => PlayerChoice::Hit, } } else { match hand.value() { v if v >= 17 => PlayerChoice::Stand, v if v > 12 && dv < 7 => PlayerChoice::Stand, v if v == 12 && (4..=6).contains(&dv) => PlayerChoice::Stand, v if v == 11 && can_dd => PlayerChoice::DoubleDown, v if v == 11 => PlayerChoice::Hit, v if v == 10 && dv < 10 && can_dd => PlayerChoice::DoubleDown, v if v == 10 && dv < 10 => PlayerChoice::Hit, v if v == 9 && (3..=6).contains(&dv) && can_dd => PlayerChoice::DoubleDown, v if v == 9 && (3..=6).contains(&dv) => PlayerChoice::Hit, _ => PlayerChoice::Hit, } } } /// 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() { 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 { let mut in_hand: u32 = PER_DAY; bank -= in_hand; let mut game = Game::new().with_decks(6).with_hit_on_soft_17(); let mut rounds = 0; while in_hand > MIN_BET && in_hand < (PER_DAY + MAX_WIN) && in_hand > (PER_DAY - MAX_LOSS) { let bet = MIN_BET; game.play(&mut in_hand, bet, basic_strategy); rounds += 1; } println!("Day {day}, after {rounds} rounds, in-hand: ${in_hand}"); bank += in_hand; } let pl: i64 = bank as i64 - START as i64; println!("Ending with: ${bank}"); println!("Profit/Loss: ${pl}"); } fn read_input() -> String { let mut buf = String::new(); io::stdin().read_line(&mut buf).unwrap(); buf.trim().to_owned() } fn data_dir() -> PathBuf { env::home_dir().unwrap().join(".local/share/blackjack") } fn save_bank(bank: u32) { ensure_data_dir(); let bank_path = data_dir().join("bank.txt"); fs::write(bank_path, bank.to_string()).unwrap(); } fn load_bank() -> Option { let bank_path = data_dir().join("bank.txt"); if fs::exists(&bank_path).unwrap() { Some(fs::read_to_string(&bank_path).unwrap().parse().unwrap()) } else { None } } fn ensure_data_dir() { if !fs::exists(data_dir()).unwrap() { fs::create_dir_all(data_dir()).unwrap(); } } #[cfg(test)] mod tests { use super::*; use blackjack::card::Suit; #[test] /// Test edge cases I found where my basic stragey 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") ), PlayerChoice::Stand, ) } }