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 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<Card>,
pub player_cards: Vec<Card>,
pub shuffled: bool,
@ -35,14 +34,12 @@ pub struct EndState {
impl EndState {
fn new(
result: PlayResult,
player_winnings: u64,
dealer_cards: Vec<Card>,
player_cards: Vec<Card>,
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<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
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,

View file

@ -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<u32> = 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<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,
)
}
}