From de22cb02f485dbd8848ab86dde2cab62b193bae3 Mon Sep 17 00:00:00 2001 From: Nick Pegg Date: Sat, 5 Jul 2025 08:35:23 -0700 Subject: [PATCH] Implement double-down, save interactive bank to disk --- src/game.rs | 37 +++++++++------- src/main.rs | 118 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 119 insertions(+), 36 deletions(-) diff --git a/src/game.rs b/src/game.rs index 876f65b..e401606 100644 --- a/src/game.rs +++ b/src/game.rs @@ -3,11 +3,11 @@ use crate::hand::Hand; use rand::prelude::*; use std::sync::{Arc, RwLock}; +#[derive(Debug, PartialEq)] pub enum PlayerChoice { Hit, Stand, - // TODO - // DoubleDown, + DoubleDown, // TODO // Split, // TODO @@ -26,7 +26,6 @@ pub enum PlayResult { /// The state at the end of a round, useful for printing out pub struct EndState { pub result: PlayResult, - pub player_winnings: u64, pub dealer_cards: Vec, pub player_cards: Vec, pub shuffled: bool, @@ -35,14 +34,12 @@ pub struct EndState { impl EndState { fn new( result: PlayResult, - player_winnings: u64, dealer_cards: Vec, player_cards: Vec, shuffled: bool, ) -> Self { Self { result, - player_winnings, dealer_cards, player_cards, shuffled, @@ -104,7 +101,12 @@ impl Game { /// and returns a choice (hit, stand, etc.). /// /// Returns the winnings of the round - pub fn play(self: &mut Self, bet: u32, player_decision: F) -> EndState + pub fn play( + &mut self, + money_on_table: &mut u32, + bet_amount: u32, + player_decision: F, + ) -> EndState where F: Fn(&Hand, &Card) -> PlayerChoice, { @@ -116,6 +118,9 @@ impl Game { shuffled = true; } + let mut bet_amount = bet_amount; + *money_on_table -= bet_amount; + // Deal cards let mut dealer_hand = Hand::from([self.deal(), self.deal()].to_vec()).with_discard(self.discard.clone()); @@ -126,16 +131,15 @@ impl Game { if dealer_hand.is_blackjack() { return EndState::new( PlayResult::DealerBlackjack, - 0, dealer_hand.cards(), player_hand.cards(), shuffled, ); } else if player_hand.is_blackjack() { - let returns = bet as f32 * self.blackjack_returns; + let returns = bet_amount as f32 * self.blackjack_returns; + *money_on_table += returns as u32; return EndState::new( PlayResult::Blackjack, - returns as u64, dealer_hand.cards(), player_hand.cards(), shuffled, @@ -151,13 +155,19 @@ impl Game { PlayerChoice::Hit => { player_hand.push(self.deal()); } + PlayerChoice::DoubleDown => { + if *money_on_table >= bet_amount { + *money_on_table -= bet_amount; + bet_amount *= 2; + } + player_hand.push(self.deal()); + } } // if player busts, immediately lose bet if player_hand.value() > 21 { return EndState::new( PlayResult::Bust, - 0, dealer_hand.cards(), player_hand.cards(), shuffled, @@ -171,25 +181,25 @@ impl Game { } if dealer_hand.value() > 21 { + *money_on_table += bet_amount * 2; EndState::new( PlayResult::Win, - (bet * 2).into(), dealer_hand.cards(), player_hand.cards(), shuffled, ) } else if dealer_hand.value() < player_hand.value() { + *money_on_table += bet_amount * 2; EndState::new( PlayResult::Win, - (bet * 2).into(), dealer_hand.cards(), player_hand.cards(), shuffled, ) } else if dealer_hand.value() == player_hand.value() { + *money_on_table += bet_amount; EndState::new( PlayResult::Push, - bet.into(), dealer_hand.cards(), player_hand.cards(), shuffled, @@ -197,7 +207,6 @@ impl Game { } else { EndState::new( PlayResult::Lose, - 0, dealer_hand.cards(), player_hand.cards(), shuffled, diff --git a/src/main.rs b/src/main.rs index b5ebca8..fb2b4c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,14 @@ 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(); @@ -13,8 +17,13 @@ fn main() { fn interactive_play() { // TODO: Persist bank between plays // TODO: Make a way to reset bank - let mut bank: u64 = 1_000; - let mut game = Game::new().with_decks(6); + // + 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 { @@ -43,18 +52,17 @@ fn interactive_play() { Err(_) => continue, } } - if bet as u64 <= bank { + if bet <= bank { break; } else { println!("You don't have enough money!\n"); } } - bank -= bet as u64; last_bet = Some(bet); println!(); - let result = game.play(bet, interactive_decision); + 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()); @@ -72,7 +80,7 @@ fn interactive_play() { println!("Deck was shuffled at beginning of round"); } - bank += result.player_winnings; + save_bank(bank); if bank == 0 { println!("You're out of money."); @@ -92,11 +100,12 @@ fn interactive_decision(hand: &Hand, dealer_showing: &Card) -> PlayerChoice { let choice: PlayerChoice; loop { - print!("(h)it or (s)tand? "); + 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; @@ -107,18 +116,27 @@ fn interactive_decision(hand: &Hand, dealer_showing: &Card) -> PlayerChoice { fn basic_strategy(hand: &Hand, dealer_showing: &Card) -> PlayerChoice { // Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy // TODO: Add doubling when it's supported - let dealer_val = u8::from(dealer_showing); - if hand.has_ace() { + let dv = u8::from(dealer_showing); + 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 && dealer_val < 9 => PlayerChoice::Stand, + v if v == 18 && (7..=8).contains(&dv) => PlayerChoice::Stand, + v if v == 18 && dv < 7 => PlayerChoice::DoubleDown, + v if v == 17 && (3..=6).contains(&dv) => PlayerChoice::DoubleDown, + v if (15..=16).contains(&v) && (4..=6).contains(&dv) => PlayerChoice::DoubleDown, + v if (13..=14).contains(&v) && (5..=6).contains(&dv) => PlayerChoice::DoubleDown, + v if v == 12 && dv == 6 => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, } } else { match hand.value() { v if v >= 17 => PlayerChoice::Stand, - v if v > 12 && dealer_val < 7 => PlayerChoice::Stand, - v if v == 12 && (4..=6).contains(&dealer_val) => PlayerChoice::Stand, + v if v > 12 && dv < 7 => PlayerChoice::Stand, + v if v == 12 && (4..=6).contains(&dv) => PlayerChoice::Stand, + v if v == 11 => PlayerChoice::DoubleDown, + v if v == 10 && dv < 10 => PlayerChoice::DoubleDown, + v if v == 9 && (3..=6).contains(&dv) => PlayerChoice::DoubleDown, _ => PlayerChoice::Hit, } } @@ -131,33 +149,39 @@ fn basic_strategy(hand: &Hand, dealer_showing: &Card) -> PlayerChoice { /// /// Does this actually work, or was the driver full of it? fn old_man() { - let mut bank: u64 = 100_000; + 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 WALK_AWAY: u32 = 100; - const DAYS: u8 = 30; + 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 as u64; + bank -= in_hand; - let mut game = Game::new().with_decks(6); + 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 + WALK_AWAY) { + while in_hand > MIN_BET && in_hand < (PER_DAY + MAX_WIN) && in_hand > (PER_DAY - MAX_LOSS) { let bet = MIN_BET; - in_hand -= bet; - let result = game.play(bet, basic_strategy); - in_hand += u32::try_from(result.player_winnings).unwrap(); + game.play(&mut in_hand, bet, basic_strategy); rounds += 1; } println!("Day {day}, after {rounds} rounds, in-hand: ${in_hand}"); - bank += in_hand as u64; + bank += in_hand; } + let pl: i64 = bank as i64 - START as i64; println!("Ending with: ${bank}"); + println!("Profit/Loss: ${pl}"); } fn read_input() -> String { @@ -165,3 +189,53 @@ fn read_input() -> String { 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, + ) + } +}