blackjack/src/main.rs
2025-07-05 11:31:27 -07:00

253 lines
8.1 KiB
Rust

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