blackjack/src/main.rs

485 lines
14 KiB
Rust

use blackjack::card::Card;
use blackjack::game::{EndState, PlayResult, PlayerChoice, PlayerTurn, Table};
use blackjack::hand::Hand;
use clap::{Parser, ValueEnum};
use console::Term;
use std::env;
use std::fs;
use std::io;
use std::io::prelude::*;
use std::path::PathBuf;
fn main() -> anyhow::Result<()> {
// TODO: Use anyhow for error handling, get rid of unwraps
let args = Args::parse();
match args.mode {
Mode::Interactive => interactive_play(args),
Mode::OldMan => old_man(args),
}
}
#[derive(ValueEnum, Clone, Debug)]
enum Mode {
Interactive,
OldMan,
}
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[arg(long, default_value = "interactive")]
mode: Mode,
#[arg(long, default_value_t = false)]
show_count: bool,
#[arg(long, default_value_t = 6)]
decks: u8,
}
fn interactive_play(args: Args) -> anyhow::Result<()> {
// TODO: Make a way to reset bank
let term = Term::stdout();
let mut bank: u32 = load_bank()?.unwrap_or(1_000);
let mut table = Table::new(args.decks, bank).with_hit_on_soft_17();
let mut last_bet: Option<u32> = None;
loop {
println!("\nMoney in the bank: ${bank}");
// TODO: show normalized card count
if args.show_count {
println!(
"Card count: {} ({})",
table.count(),
table.count() / args.decks as i16
);
println!("Cards in shoe: {}", table.shoe_count());
}
// Bet checking loop
let mut bet: u32;
loop {
if last_bet.is_some() {
let last_bet = last_bet.unwrap();
print!("Your bet [{}]? ", last_bet);
io::stdout().flush().unwrap();
let input = term.read_line()?;
if input.is_empty() {
bet = last_bet;
} else {
match input.parse() {
Ok(b) => bet = b,
Err(_) => continue,
}
}
} else {
print!("Your bet? ");
io::stdout().flush()?;
match term.read_line()?.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 mut turn = table.deal_hand(bet);
let mut split_turn = interactive_play_turn(&mut turn, &mut table, &args)?;
if let Some(st) = &mut split_turn {
interactive_play_turn(st, &mut table, &args)?;
}
table.dealers_turn(&turn)?;
term.clear_screen()?;
let result = table.results(turn)?;
print_result(&result);
if let Some(st) = split_turn {
println!();
print_result(&table.results(st)?);
}
table.end_game()?;
bank = table.player_chips();
save_bank(bank)?;
if bank == 0 {
println!("You're out of money.");
break;
}
}
Ok(())
}
/// Play a turn. Returns another turn if this one was split
fn interactive_play_turn(
turn: &mut PlayerTurn,
table: &mut Table,
args: &Args,
) -> anyhow::Result<Option<PlayerTurn>> {
let mut initial_play = !turn.was_split;
let mut other_turn = None;
let term = Term::stdout();
loop {
term.clear_screen()?;
let hand = turn.player_hand();
let can_double = initial_play && table.player_chips() > turn.bet;
let can_split = initial_play && hand.is_pair();
if turn.shuffled {
println!("Deck was shuffled");
}
if args.show_count {
println!(
"Card count: {} ({})",
table.count(),
table.count() / args.decks as i16
);
}
println!("Your bet: ${}", turn.bet);
println!("Dealer showing: {}", table.dealer_showing());
println!("Your hand: {hand}");
println!();
if hand.value() == 21 {
println!("You have 21, standing.");
table.stand(turn)?;
break;
} else if hand.value() > 21 {
println!("You busted");
table.stand(turn)?;
break;
}
let mut msg = String::from("(h)it, (s)tand");
if can_double {
msg += ", (d)ouble-down";
}
if can_split {
msg += ", s(p)lit";
}
msg += "? ";
io::stdout().write_all(msg.as_bytes())?;
io::stdout().flush()?;
let mut stop = false;
match term.read_char()? {
'h' => {
table.hit(turn)?;
initial_play = false;
}
's' => {
table.stand(turn)?;
stop = true;
initial_play = false;
}
'd' if can_double => {
table.double_down(turn)?;
stop = true;
initial_play = false;
}
'p' if can_split => {
other_turn = Some(table.split(turn)?);
initial_play = false;
}
_ => {}
};
if stop {
break;
}
}
Ok(other_turn)
}
fn print_result(result: &EndState) {
println!(
"Dealer's hand: {} = {}",
result.dealer_hand,
result.dealer_hand.value()
);
println!(
"Your hand: {} = {}",
result.player_hand,
result.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"),
}
println!("You got: ${}", result.returns);
}
fn basic_strategy(
hand: &Hand,
dealer_showing: &Card,
can_split: bool,
can_dd: bool,
) -> PlayerChoice {
// Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy
let dv = u8::from(dealer_showing);
if hand.is_pair() && can_split {
// Got a pair, maybe should split
// We check can_split right at the get-go, because if we can't we just follow the Hard
// Total table.
match hand.value() {
12 if hand.has_ace() => PlayerChoice::Split,
20 => PlayerChoice::Stand,
18 => match dv {
2..=6 => PlayerChoice::Split,
7 => PlayerChoice::Stand,
_ => PlayerChoice::Split,
},
16 => PlayerChoice::Split,
14 => match dv {
2..=7 => PlayerChoice::Split,
_ => PlayerChoice::Hit,
},
12 => match dv {
2..=6 => PlayerChoice::Split,
_ => PlayerChoice::Hit,
},
10 => match dv {
2..=9 if can_dd => PlayerChoice::DoubleDown,
_ => PlayerChoice::Hit,
},
8 => match dv {
5..=6 => PlayerChoice::Split,
_ => PlayerChoice::Hit,
},
4..=6 => match dv {
2..=7 => PlayerChoice::Split,
_ => PlayerChoice::Hit,
},
_ => PlayerChoice::Stand,
}
} else if hand.has_ace() && hand.cards().len() == 2 {
// Ace and some other card
match hand.value() {
20 => PlayerChoice::Stand,
19 if dv == 6 && can_dd => PlayerChoice::DoubleDown,
19 => PlayerChoice::Stand,
18 => match dv {
2..=6 if can_dd => PlayerChoice::DoubleDown,
9..=11 => PlayerChoice::Hit,
_ => PlayerChoice::Stand,
},
17 => match dv {
3..=6 if can_dd => PlayerChoice::DoubleDown,
_ => PlayerChoice::Hit,
},
15..=16 => match dv {
4..=6 if can_dd => PlayerChoice::DoubleDown,
_ => PlayerChoice::Hit,
},
13..=14 => match dv {
5..=6 if can_dd => PlayerChoice::DoubleDown,
_ => PlayerChoice::Hit,
},
12 if dv == 6 && can_dd => PlayerChoice::DoubleDown,
_ => PlayerChoice::Hit,
}
} else {
match hand.value() {
17..=21 => PlayerChoice::Stand,
13..=16 if dv >= 7 => PlayerChoice::Hit,
13..=16 => PlayerChoice::Stand,
12 => match dv {
4..=6 => PlayerChoice::Stand,
_ => PlayerChoice::Hit,
},
11 if can_dd => PlayerChoice::DoubleDown,
11 => PlayerChoice::Hit,
10 => match dv {
2..=9 if can_dd => PlayerChoice::DoubleDown,
_ => PlayerChoice::Hit,
},
9 => match dv {
3..=6 if can_dd => PlayerChoice::DoubleDown,
_ => PlayerChoice::Hit,
},
_ => PlayerChoice::Stand,
}
}
}
fn basic_strategy_play_turn(
turn: &mut PlayerTurn,
table: &mut Table,
) -> anyhow::Result<Option<PlayerTurn>> {
let mut other_turn: Option<PlayerTurn> = None;
loop {
// Enough chips to split or double down?
let enough_chips = turn.bet <= table.player_chips();
let can_split = !turn.was_split && enough_chips && turn.player_hand().is_pair();
let can_dd = enough_chips && turn.player_hand().cards().len() == 2;
match basic_strategy(
&turn.player_hand(),
&table.dealer_showing(),
can_split,
can_dd,
) {
PlayerChoice::Hit => table.hit(turn)?,
PlayerChoice::Stand => {
table.stand(turn)?;
break;
}
PlayerChoice::DoubleDown => {
table.double_down(turn)?;
break;
}
PlayerChoice::Split => {
other_turn = Some(table.split(turn)?);
}
}
}
Ok(other_turn)
}
/// 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(args: Args) -> anyhow::Result<()> {
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 {
bank -= PER_DAY;
let mut table = Table::new(args.decks, PER_DAY).with_hit_on_soft_17();
let mut rounds = 0;
while table.player_chips() > MIN_BET
&& table.player_chips() < (PER_DAY + MAX_WIN)
&& table.player_chips() > (PER_DAY - MAX_LOSS)
{
let mut turn = table.deal_hand(MIN_BET);
let mut split_turn = basic_strategy_play_turn(&mut turn, &mut table)?;
if let Some(st) = &mut split_turn {
basic_strategy_play_turn(st, &mut table)?;
}
table.dealers_turn(&turn)?;
if let Some(st) = split_turn {
table.results(st)?;
}
table.results(turn)?;
// No need to handle result since we take chips off table at end of day
table.end_game()?;
rounds += 1;
}
bank += table.player_chips();
println!(
"Day {day}, after {rounds} rounds, returns: ${}, bank: ${}",
table.player_chips() as i32 - PER_DAY as i32,
bank,
);
}
let pl: i64 = bank as i64 - START as i64;
println!("Ending with: ${bank}");
println!("Profit/Loss: ${pl}");
Ok(())
}
fn data_dir() -> PathBuf {
env::home_dir().unwrap().join(".local/share/blackjack")
}
fn save_bank(bank: u32) -> anyhow::Result<()> {
ensure_data_dir()?;
let bank_path = data_dir().join("bank.txt");
Ok(fs::write(bank_path, bank.to_string())?)
}
fn load_bank() -> anyhow::Result<Option<u32>> {
let bank_path = data_dir().join("bank.txt");
if fs::exists(&bank_path)? {
let bank = fs::read_to_string(&bank_path)?.parse()?;
if bank > 0 {
Ok(Some(bank))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn ensure_data_dir() -> anyhow::Result<()> {
if !fs::exists(data_dir())? {
fs::create_dir_all(data_dir())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use blackjack::card::Suit;
#[test]
/// Test edge cases I found where my basic strategy 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"),
true,
true,
),
PlayerChoice::Stand,
);
// Always split aces
assert_eq!(
basic_strategy(
&Hand::from([("", "A"), ("", "A")].to_vec()),
&Card::from(("", "2")),
true,
true,
),
PlayerChoice::Split,
);
}
}