Implement double-down, save interactive bank to disk
This commit is contained in:
parent
c114094285
commit
de22cb02f4
2 changed files with 119 additions and 36 deletions
37
src/game.rs
37
src/game.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
118
src/main.rs
118
src/main.rs
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue