253 lines
8.1 KiB
Rust
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,
|
|
)
|
|
}
|
|
}
|