Implement double-down, save interactive bank to disk

This commit is contained in:
Nick Pegg 2025-07-05 08:35:23 -07:00
parent c114094285
commit de22cb02f4
2 changed files with 119 additions and 36 deletions

View file

@ -3,11 +3,11 @@ use crate::hand::Hand;
use rand::prelude::*; use rand::prelude::*;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
#[derive(Debug, PartialEq)]
pub enum PlayerChoice { pub enum PlayerChoice {
Hit, Hit,
Stand, Stand,
// TODO DoubleDown,
// DoubleDown,
// TODO // TODO
// Split, // Split,
// TODO // TODO
@ -26,7 +26,6 @@ pub enum PlayResult {
/// The state at the end of a round, useful for printing out /// The state at the end of a round, useful for printing out
pub struct EndState { pub struct EndState {
pub result: PlayResult, pub result: PlayResult,
pub player_winnings: u64,
pub dealer_cards: Vec<Card>, pub dealer_cards: Vec<Card>,
pub player_cards: Vec<Card>, pub player_cards: Vec<Card>,
pub shuffled: bool, pub shuffled: bool,
@ -35,14 +34,12 @@ pub struct EndState {
impl EndState { impl EndState {
fn new( fn new(
result: PlayResult, result: PlayResult,
player_winnings: u64,
dealer_cards: Vec<Card>, dealer_cards: Vec<Card>,
player_cards: Vec<Card>, player_cards: Vec<Card>,
shuffled: bool, shuffled: bool,
) -> Self { ) -> Self {
Self { Self {
result, result,
player_winnings,
dealer_cards, dealer_cards,
player_cards, player_cards,
shuffled, shuffled,
@ -104,7 +101,12 @@ impl Game {
/// and returns a choice (hit, stand, etc.). /// and returns a choice (hit, stand, etc.).
/// ///
/// Returns the winnings of the round /// Returns the winnings of the round
pub fn play<F>(self: &mut Self, bet: u32, player_decision: F) -> EndState pub fn play<F>(
&mut self,
money_on_table: &mut u32,
bet_amount: u32,
player_decision: F,
) -> EndState
where where
F: Fn(&Hand, &Card) -> PlayerChoice, F: Fn(&Hand, &Card) -> PlayerChoice,
{ {
@ -116,6 +118,9 @@ impl Game {
shuffled = true; shuffled = true;
} }
let mut bet_amount = bet_amount;
*money_on_table -= bet_amount;
// Deal cards // Deal cards
let mut dealer_hand = let mut dealer_hand =
Hand::from([self.deal(), self.deal()].to_vec()).with_discard(self.discard.clone()); Hand::from([self.deal(), self.deal()].to_vec()).with_discard(self.discard.clone());
@ -126,16 +131,15 @@ impl Game {
if dealer_hand.is_blackjack() { if dealer_hand.is_blackjack() {
return EndState::new( return EndState::new(
PlayResult::DealerBlackjack, PlayResult::DealerBlackjack,
0,
dealer_hand.cards(), dealer_hand.cards(),
player_hand.cards(), player_hand.cards(),
shuffled, shuffled,
); );
} else if player_hand.is_blackjack() { } 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( return EndState::new(
PlayResult::Blackjack, PlayResult::Blackjack,
returns as u64,
dealer_hand.cards(), dealer_hand.cards(),
player_hand.cards(), player_hand.cards(),
shuffled, shuffled,
@ -151,13 +155,19 @@ impl Game {
PlayerChoice::Hit => { PlayerChoice::Hit => {
player_hand.push(self.deal()); 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 busts, immediately lose bet
if player_hand.value() > 21 { if player_hand.value() > 21 {
return EndState::new( return EndState::new(
PlayResult::Bust, PlayResult::Bust,
0,
dealer_hand.cards(), dealer_hand.cards(),
player_hand.cards(), player_hand.cards(),
shuffled, shuffled,
@ -171,25 +181,25 @@ impl Game {
} }
if dealer_hand.value() > 21 { if dealer_hand.value() > 21 {
*money_on_table += bet_amount * 2;
EndState::new( EndState::new(
PlayResult::Win, PlayResult::Win,
(bet * 2).into(),
dealer_hand.cards(), dealer_hand.cards(),
player_hand.cards(), player_hand.cards(),
shuffled, shuffled,
) )
} else if dealer_hand.value() < player_hand.value() { } else if dealer_hand.value() < player_hand.value() {
*money_on_table += bet_amount * 2;
EndState::new( EndState::new(
PlayResult::Win, PlayResult::Win,
(bet * 2).into(),
dealer_hand.cards(), dealer_hand.cards(),
player_hand.cards(), player_hand.cards(),
shuffled, shuffled,
) )
} else if dealer_hand.value() == player_hand.value() { } else if dealer_hand.value() == player_hand.value() {
*money_on_table += bet_amount;
EndState::new( EndState::new(
PlayResult::Push, PlayResult::Push,
bet.into(),
dealer_hand.cards(), dealer_hand.cards(),
player_hand.cards(), player_hand.cards(),
shuffled, shuffled,
@ -197,7 +207,6 @@ impl Game {
} else { } else {
EndState::new( EndState::new(
PlayResult::Lose, PlayResult::Lose,
0,
dealer_hand.cards(), dealer_hand.cards(),
player_hand.cards(), player_hand.cards(),
shuffled, shuffled,

View file

@ -1,10 +1,14 @@
use blackjack::card::Card; use blackjack::card::Card;
use blackjack::game::{Game, PlayResult, PlayerChoice}; use blackjack::game::{Game, PlayResult, PlayerChoice};
use blackjack::hand::Hand; use blackjack::hand::Hand;
use std::env;
use std::fs;
use std::io; use std::io;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::{Path, PathBuf};
fn main() { fn main() {
// TODO: Use anyhow for error handling, get rid of unwraps
// TODO: CLI flags to choose how to play // TODO: CLI flags to choose how to play
interactive_play(); interactive_play();
// old_man(); // old_man();
@ -13,8 +17,13 @@ fn main() {
fn interactive_play() { fn interactive_play() {
// TODO: Persist bank between plays // TODO: Persist bank between plays
// TODO: Make a way to reset bank // 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<u32> = None; let mut last_bet: Option<u32> = None;
loop { loop {
@ -43,18 +52,17 @@ fn interactive_play() {
Err(_) => continue, Err(_) => continue,
} }
} }
if bet as u64 <= bank { if bet <= bank {
break; break;
} else { } else {
println!("You don't have enough money!\n"); println!("You don't have enough money!\n");
} }
} }
bank -= bet as u64;
last_bet = Some(bet); last_bet = Some(bet);
println!(); 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 dealer_hand = Hand::from(result.dealer_cards);
let player_hand = Hand::from(result.player_cards); let player_hand = Hand::from(result.player_cards);
println!("Dealer's hand: {} = {}", dealer_hand, dealer_hand.value()); println!("Dealer's hand: {} = {}", dealer_hand, dealer_hand.value());
@ -72,7 +80,7 @@ fn interactive_play() {
println!("Deck was shuffled at beginning of round"); println!("Deck was shuffled at beginning of round");
} }
bank += result.player_winnings; save_bank(bank);
if bank == 0 { if bank == 0 {
println!("You're out of money."); println!("You're out of money.");
@ -92,11 +100,12 @@ fn interactive_decision(hand: &Hand, dealer_showing: &Card) -> PlayerChoice {
let choice: PlayerChoice; let choice: PlayerChoice;
loop { loop {
print!("(h)it or (s)tand? "); print!("(h)it, (s)tand, (d)ouble-down? ");
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
choice = match read_input().to_lowercase().as_ref() { choice = match read_input().to_lowercase().as_ref() {
"h" => PlayerChoice::Hit, "h" => PlayerChoice::Hit,
"s" => PlayerChoice::Stand, "s" => PlayerChoice::Stand,
"d" => PlayerChoice::DoubleDown,
_ => continue, _ => continue,
}; };
break; break;
@ -107,18 +116,27 @@ fn interactive_decision(hand: &Hand, dealer_showing: &Card) -> PlayerChoice {
fn basic_strategy(hand: &Hand, dealer_showing: &Card) -> PlayerChoice { fn basic_strategy(hand: &Hand, dealer_showing: &Card) -> PlayerChoice {
// Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy // Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy
// TODO: Add doubling when it's supported // TODO: Add doubling when it's supported
let dealer_val = u8::from(dealer_showing); let dv = u8::from(dealer_showing);
if hand.has_ace() { if hand.has_ace() && hand.cards().len() == 2 {
// Ace and some other card
match hand.value() { match hand.value() {
v if v >= 19 => PlayerChoice::Stand, 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, _ => PlayerChoice::Hit,
} }
} else { } else {
match hand.value() { match hand.value() {
v if v >= 17 => PlayerChoice::Stand, v if v >= 17 => PlayerChoice::Stand,
v if v > 12 && dealer_val < 7 => PlayerChoice::Stand, v if v > 12 && dv < 7 => PlayerChoice::Stand,
v if v == 12 && (4..=6).contains(&dealer_val) => 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, _ => 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? /// Does this actually work, or was the driver full of it?
fn old_man() { fn old_man() {
let mut bank: u64 = 100_000; const START: u32 = 100_000;
const PER_DAY: u32 = 400; const PER_DAY: u32 = 400;
const MIN_BET: u32 = 15; const MIN_BET: u32 = 15;
// Walk away when we're up by this much // Walk away when we're up by this much
const WALK_AWAY: u32 = 100; const MAX_WIN: u32 = 100;
const DAYS: u8 = 30; // 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}"); println!("Starting with bank: ${bank}");
// Let's simulate this for 30 days // Let's simulate this for 30 days
for day in 1..=DAYS { for day in 1..=DAYS {
let mut in_hand: u32 = PER_DAY; 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; 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; let bet = MIN_BET;
in_hand -= bet; game.play(&mut in_hand, bet, basic_strategy);
let result = game.play(bet, basic_strategy);
in_hand += u32::try_from(result.player_winnings).unwrap();
rounds += 1; rounds += 1;
} }
println!("Day {day}, after {rounds} rounds, in-hand: ${in_hand}"); 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!("Ending with: ${bank}");
println!("Profit/Loss: ${pl}");
} }
fn read_input() -> String { fn read_input() -> String {
@ -165,3 +189,53 @@ fn read_input() -> String {
io::stdin().read_line(&mut buf).unwrap(); io::stdin().read_line(&mut buf).unwrap();
buf.trim().to_owned() 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<u32> {
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,
)
}
}