Compare commits

..

No commits in common. "69a4239f9016facb72405f3e7b17717588ddb8c3" and "821d2114d550a10cace68375f40111cdc7f9279f" have entirely different histories.

8 changed files with 348 additions and 1066 deletions

248
Cargo.lock generated
View file

@ -2,62 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "bitflags"
version = "2.9.1"
@ -68,12 +12,9 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
name = "blackjack"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"console",
"itertools",
"rand",
"thiserror",
]
[[package]]
@ -82,52 +23,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "clap"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "console"
version = "0.16.0"
@ -138,7 +33,7 @@ dependencies = [
"libc",
"once_cell",
"unicode-width",
"windows-sys 0.60.2",
"windows-sys",
]
[[package]]
@ -165,18 +60,6 @@ dependencies = [
"wasi",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.14.0"
@ -198,12 +81,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -266,12 +143,6 @@ dependencies = [
"getrandom",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.104"
@ -283,26 +154,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
@ -315,12 +166,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
@ -330,38 +175,13 @@ dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
"windows-targets",
]
[[package]]
@ -370,106 +190,58 @@ version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"

View file

@ -4,9 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.98"
clap = { version = "4.5.40", features = ["derive"] }
console = "0.16.0"
itertools = "0.14.0"
rand = "0.9.1"
thiserror = "2.0.12"

View file

@ -1,8 +1,7 @@
use std::fmt;
#[derive(Clone, Debug)]
#[derive(Clone)]
pub enum Suit {
Unknown,
Club,
Diamond,
Heart,
@ -16,13 +15,12 @@ impl fmt::Display for Suit {
Suit::Diamond => "",
Suit::Heart => "",
Suit::Spade => "",
Suit::Unknown => "",
};
write!(f, "{c}")
}
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct Card {
pub suit: Suit,
pub value: String,
@ -37,27 +35,6 @@ impl Card {
}
}
impl From<(&str, &str)> for Card {
fn from(str_card: (&str, &str)) -> Self {
let suit = match str_card.0 {
"" => Suit::Club,
"" => Suit::Diamond,
"" => Suit::Heart,
"" => Suit::Spade,
"c" => Suit::Club,
"d" => Suit::Diamond,
"h" => Suit::Heart,
"s" => Suit::Spade,
_ => Suit::Unknown,
};
Self {
suit,
value: str_card.1.to_string(),
}
}
}
impl From<&Card> for u8 {
fn from(card: &Card) -> u8 {
match card.value.as_ref() {

View file

@ -1,17 +0,0 @@
use crate::game::Phase;
use crate::hand::Hand;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum BlackjackError {
#[error("Incorrect action for current game phase: {0}")]
IncorrectAction(Phase),
#[error("Cannot double down with this hand: {0}")]
Double(Hand),
#[error("Cannot split with this hand: {0}")]
Split(Hand),
#[error("This turn was already split")]
AlreadySplit,
#[error("Not enough chips on table to place bet")]
InsufficientChips,
}

View file

@ -1,36 +1,19 @@
use crate::card::{deck_without_jokers, Card};
use crate::error::BlackjackError;
use crate::hand::Hand;
use rand::prelude::*;
use std::fmt;
use std::sync::{Arc, RwLock};
#[derive(Debug, PartialEq)]
pub enum PlayerChoice {
Hit,
Stand,
DoubleDown,
Split,
// TODO
// Split,
// TODO
// Surrender,
}
/// Phase that the game is in. Used in verifying that actions are legal
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Phase {
New,
PlayerTurn,
DealerTurn,
Results,
Ended,
}
impl fmt::Display for Phase {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, PartialEq)]
pub enum PlayResult {
Win, // Player didn't bust, and either has higher value or dealer busted
Blackjack, // Player won with a face card and ace
@ -40,75 +23,63 @@ pub enum PlayResult {
Bust, // Player lost because they busted
}
/// The game state while in the middle, which does not reveal the dealer's full hand
pub struct MidState {
pub shuffled: bool,
pub player_hand: Hand,
pub dealer_showing: Card,
pub count: i16,
}
/// The state at the end of a round, useful for printing out
#[derive(Debug)]
pub struct EndState {
pub result: PlayResult,
pub dealer_hand: Hand,
pub player_hand: Hand,
pub returns: u32,
pub dealer_cards: Vec<Card>,
pub player_cards: Vec<Card>,
pub shuffled: bool,
}
pub struct Table {
impl EndState {
fn new(
result: PlayResult,
dealer_cards: Vec<Card>,
player_cards: Vec<Card>,
shuffled: bool,
) -> Self {
Self {
result,
dealer_cards,
player_cards,
shuffled,
}
}
}
pub struct Game {
rng: rand::rngs::ThreadRng,
shoe: Vec<Card>,
/// Discard pile. When the shoe runs out, this gets mixed with the shoe and reshuffled.
discard: Vec<Card>,
/// The current card count, see: https://en.wikipedia.org/wiki/Blackjack#Card_counting
count: i16,
player_chips: u32,
// Mid-game state
/// Phase of the current game
phase: Phase,
dealer_hand: Hand,
/// How many player hands need to be resolved
pending_turns: u8,
/// How many results need to be resolved
pending_results: u8,
// Settings
/// If the dealer has a 17 with an Ace, they will hit. Gives advantage to dealer
discard: Arc<RwLock<Vec<Card>>>,
hit_on_soft_17: bool,
/// Returns when hitting blackjack
blackjack_returns: f32,
}
impl Table {
pub fn new(decks: u8, player_chips: u32) -> Self {
let mut table = Self {
impl Game {
pub fn new() -> Self {
Self {
// Game settings
hit_on_soft_17: false,
blackjack_returns: 3.0 / 2.0,
// Game state
rng: rand::rng(),
phase: Phase::New,
shoe: Vec::new(),
discard: Vec::new(),
player_chips,
dealer_hand: Hand::new(),
pending_turns: 0,
pending_results: 0,
count: 0,
};
for _ in 0..decks {
table.shoe.append(&mut deck_without_jokers())
discard: Arc::new(RwLock::new(Vec::new())),
}
table.shoe.shuffle(&mut table.rng);
}
table
/// Add some number of decks to the shoe, and shuffle it
pub fn with_decks(mut self, count: u8) -> Self {
for _ in 0..count {
self.shoe.append(&mut deck_without_jokers())
}
self.shoe.shuffle(&mut self.rng);
self
}
/// Sets the dealer to hit on a soft 17 (one involving an Ace)
@ -123,395 +94,146 @@ impl Table {
self
}
pub fn player_chips(&self) -> u32 {
self.player_chips
/// Play one round of Blackjack
///
/// Takes a bet and a function, which is what the player will do during their turn. This
/// function must take the player's hand (Vec<Card>) and the card the dealer is showing (Card),
/// and returns a choice (hit, stand, etc.).
///
/// Returns the winnings of the round
pub fn play<F>(
&mut self,
money_on_table: &mut u32,
bet_amount: u32,
player_decision: F,
) -> EndState
where
F: Fn(&Hand, &Card) -> PlayerChoice,
{
// Shuffle the deck if we've played over 75% of cards
let discard_size = self.discard.read().unwrap().len();
let mut shuffled = false;
if self.shoe.len() < (self.shoe.len() + discard_size) * 1 / 4 {
self.shuffle();
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());
let mut player_hand =
Hand::from([self.deal(), self.deal()].to_vec()).with_discard(self.discard.clone());
// If dealer has blackjack, immediately lose. If player has blackjack, they win
if dealer_hand.is_blackjack() {
return EndState::new(
PlayResult::DealerBlackjack,
dealer_hand.cards(),
player_hand.cards(),
shuffled,
);
} else if player_hand.is_blackjack() {
let returns = bet_amount as f32 * self.blackjack_returns;
*money_on_table += returns as u32;
return EndState::new(
PlayResult::Blackjack,
dealer_hand.cards(),
player_hand.cards(),
shuffled,
);
}
// Player turn
loop {
let decision = player_decision(&player_hand, &dealer_hand.cards()[0]);
match decision {
PlayerChoice::Stand => break,
PlayerChoice::Hit => {
player_hand.push(self.deal());
}
PlayerChoice::DoubleDown => {
if player_hand.cards().len() >= 3 {
// Can only double-down as first move
// TODO: Provide feedback that this move is invalid
continue;
}
if *money_on_table >= bet_amount {
*money_on_table -= bet_amount;
bet_amount *= 2;
}
player_hand.push(self.deal());
// Player can only take one additional card
break;
}
}
// if player busts, immediately lose bet
if player_hand.value() > 21 {
break;
}
}
if player_hand.value() > 21 {
return EndState::new(
PlayResult::Bust,
dealer_hand.cards(),
player_hand.cards(),
shuffled,
);
}
// Dealer turn
while dealer_hand.value() < 17 {
dealer_hand.push(self.deal());
}
if dealer_hand.value() > 21 {
*money_on_table += bet_amount * 2;
EndState::new(
PlayResult::Win,
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,
dealer_hand.cards(),
player_hand.cards(),
shuffled,
)
} else if dealer_hand.value() == player_hand.value() {
*money_on_table += bet_amount;
EndState::new(
PlayResult::Push,
dealer_hand.cards(),
player_hand.cards(),
shuffled,
)
} else {
EndState::new(
PlayResult::Lose,
dealer_hand.cards(),
player_hand.cards(),
shuffled,
)
}
}
/// Deal a single card from the shoe
fn deal(self: &mut Self) -> Card {
self.shoe.pop().unwrap()
}
/// Reset the shoe - combine shoe and discard and shuffle
fn shuffle(self: &mut Self) {
self.shoe.append(&mut self.discard);
assert_eq!(self.discard.len(), 0);
let mut discard = self.discard.write().unwrap();
self.shoe.append(&mut discard);
assert_eq!(discard.len(), 0);
self.shoe.shuffle(&mut self.rng);
self.count = 0;
}
/// Deal a single card from the shoe
fn deal_card(&mut self) -> Card {
let card = self.shoe.pop().unwrap();
if u8::from(&card) < 6 {
self.count += 1;
} else if u8::from(&card) >= 10 {
self.count -= 1;
}
card
}
/// Deal out a hand and start a game
pub fn deal_hand(&mut self, bet: u32) -> PlayerTurn {
self.dealer_hand = Hand::new();
let mut player = PlayerTurn::new(bet);
// Shuffle the deck if we've played over 75% of cards
let discard_size = self.discard.len();
if self.shoe.len() < (self.shoe.len() + discard_size) * 1 / 4 {
self.shuffle();
player.shuffled = true;
}
// Sanity check to make sure we don't accidentally clone cards into the discard
let total_cards = self.shoe.len() + self.discard.len();
if total_cards % 52 != 0 {
panic!("Wrong number of cards in shoe + discard: {total_cards}");
}
self.player_chips -= bet;
// Deal cards in a circle, player(s) then dealer
for _ in 0..2 {
player.hand.push(self.deal_card());
let dealer_card = self.deal_card();
self.dealer_hand.push(dealer_card);
}
self.phase = Phase::PlayerTurn;
self.pending_turns += 1;
self.pending_results += 1;
player
}
/// Returns the card that the dealer is showing
pub fn dealer_showing(&self) -> Card {
self.dealer_hand.cards()[0].clone()
}
/// Run the dealer's turn and finish the game
pub fn dealers_turn(&mut self) -> Result<(), BlackjackError> {
if self.phase != Phase::DealerTurn {
return Err(BlackjackError::IncorrectAction(self.phase));
}
while self.dealer_hand.value() < 17 {
let dealer_card = self.deal_card();
self.dealer_hand.push(dealer_card);
}
self.phase = Phase::Results;
Ok(())
}
/// Get the results
pub fn results(&mut self, turn: PlayerTurn) -> Result<EndState, BlackjackError> {
if self.phase != Phase::Results {
return Err(BlackjackError::IncorrectAction(self.phase));
}
let mut end_state = EndState {
result: PlayResult::Lose,
dealer_hand: self.dealer_hand.clone(),
player_hand: turn.hand.clone(),
returns: 0,
};
if self.dealer_hand.is_blackjack() {
end_state.result = PlayResult::DealerBlackjack;
} else if turn.hand.is_blackjack() {
end_state.result = PlayResult::Blackjack;
let bj_winnings = (turn.bet as f32 * self.blackjack_returns) as u32;
end_state.returns = turn.bet + bj_winnings;
} else if turn.hand.value() > 21 {
end_state.result = PlayResult::Bust;
} else if self.dealer_hand.value() > 21 {
end_state.result = PlayResult::Win;
end_state.returns = turn.bet * 2;
} else if self.dealer_hand.value() < turn.hand.value() {
end_state.result = PlayResult::Win;
end_state.returns = turn.bet * 2;
} else if self.dealer_hand.value() == turn.hand.value() {
end_state.result = PlayResult::Push;
end_state.returns = turn.bet;
}
self.player_chips += end_state.returns;
self.pending_results -= 1;
// Since we're turning the hand in here (taking ownership of the PlayerTurn), we can
// discard the cards now
self.discard_hand(turn.hand);
if self.pending_results == 0 {
self.phase = Phase::Ended;
}
Ok(end_state)
}
pub fn discard_hand(&mut self, hand: Hand) {
self.discard.append(&mut hand.cards());
}
/// End the game and reset table state
pub fn end_game(&mut self) -> Result<(), BlackjackError> {
if self.phase != Phase::Ended {
return Err(BlackjackError::IncorrectAction(self.phase));
}
self.discard.append(&mut self.dealer_hand.cards());
self.dealer_hand = Hand::new();
self.phase = Phase::New;
Ok(())
}
// -- player functions --
/// Stand, ending the player's turn
pub fn stand(&mut self, turn: &mut PlayerTurn) -> Result<(), BlackjackError> {
if self.phase != Phase::PlayerTurn {
return Err(BlackjackError::IncorrectAction(self.phase));
}
if turn.stood {
return Ok(());
}
turn.stood = true;
self.pending_turns -= 1;
if self.pending_turns == 0 {
self.phase = Phase::DealerTurn;
}
Ok(())
}
pub fn hit(&mut self, turn: &mut PlayerTurn) -> Result<(), BlackjackError> {
if self.phase != Phase::PlayerTurn {
return Err(BlackjackError::IncorrectAction(self.phase));
}
turn.hand.push(self.deal_card());
Ok(())
}
/// Double the bet and take one additional card, standing afterward
pub fn double_down(&mut self, turn: &mut PlayerTurn) -> Result<(), BlackjackError> {
if self.phase != Phase::PlayerTurn {
return Err(BlackjackError::IncorrectAction(self.phase));
} else if turn.hand.cards().len() != 2 {
return Err(BlackjackError::Double(turn.hand.clone()));
} else if self.player_chips < turn.bet {
return Err(BlackjackError::InsufficientChips);
}
self.player_chips -= turn.bet;
turn.bet *= 2;
turn.hand.push(self.deal_card());
self.stand(turn)
}
/// Split the hand. Returns a second Round which was split off of this one
pub fn split(&mut self, turn: &mut PlayerTurn) -> Result<PlayerTurn, BlackjackError> {
if self.phase != Phase::PlayerTurn {
return Err(BlackjackError::IncorrectAction(self.phase));
}
// Make sure the user isn't trying to split again, and that the hand is valid to split
if turn.was_split {
return Err(BlackjackError::AlreadySplit);
} else if !turn.hand.is_pair() {
return Err(BlackjackError::Split(turn.hand.clone()));
} else if self.player_chips < turn.bet {
return Err(BlackjackError::InsufficientChips);
}
self.player_chips -= turn.bet;
let mut other_hand = turn.hand.split();
turn.hand.push(self.deal_card());
other_hand.push(self.deal_card());
self.pending_turns += 1;
self.pending_results += 1;
turn.was_split = true;
Ok(PlayerTurn {
hand: other_hand,
bet: turn.bet,
shuffled: turn.shuffled,
stood: false,
was_split: true,
})
}
}
/// Represents one round (or hand) of Blackjack. Becomes duplicated on a split.
pub struct PlayerTurn {
hand: Hand,
pub bet: u32,
pub stood: bool,
pub was_split: bool,
/// Indicates if the deck was shuffled when dealing cards for this turn
pub shuffled: bool,
}
impl PlayerTurn {
pub fn new(bet: u32) -> Self {
Self {
hand: Hand::new(),
bet,
shuffled: false,
stood: false,
was_split: false,
}
}
/// Returns a read-only copy of the player's hand
pub fn player_hand(&self) -> Hand {
Hand::from(self.hand.cards())
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Set up a game with the specified hands
fn setup_hands(
bet: u32,
dealer_cards: Vec<(&str, &str)>,
player_cards: Vec<(&str, &str)>,
) -> (Table, PlayerTurn) {
let mut table = Table::new(6, bet);
table.pending_turns = 1;
table.pending_results = 1;
table.dealer_hand = Hand::from(dealer_cards);
table.phase = Phase::PlayerTurn;
let mut turn = PlayerTurn::new(10);
turn.hand = Hand::from(player_cards);
(table, turn)
}
/// Play a game with the specified hands
fn play_hands(
bet: u32,
dealer_cards: Vec<(&str, &str)>,
player_cards: Vec<(&str, &str)>,
) -> anyhow::Result<EndState> {
let (mut table, mut turn) = setup_hands(bet, dealer_cards, player_cards);
table.stand(&mut turn)?;
table.dealers_turn()?;
Ok(table.results(turn)?)
}
#[test]
fn player_lose() {
let result = play_hands(
10,
Vec::from([("", "9"), ("", "10")]),
Vec::from([("", "5"), ("", "10")]),
)
.unwrap();
assert_eq!(result.result, PlayResult::Lose);
assert_eq!(result.returns, 0);
}
#[test]
fn player_win() {
let result = play_hands(
10,
Vec::from([("", "5"), ("", "10")]),
Vec::from([("", "9"), ("", "10")]),
)
.unwrap();
assert_eq!(result.result, PlayResult::Win);
assert_eq!(result.returns, 20);
}
#[test]
fn tie() {
let result = play_hands(
10,
Vec::from([("", "9"), ("", "10")]),
Vec::from([("", "9"), ("", "10")]),
)
.unwrap();
assert_eq!(result.result, PlayResult::Push);
assert_eq!(result.returns, 10);
}
#[test]
fn player_blackjack() {
let result = play_hands(
10,
Vec::from([("", "5"), ("", "10")]),
Vec::from([("", "A"), ("", "10")]),
)
.unwrap();
assert_eq!(result.result, PlayResult::Blackjack);
assert_eq!(result.returns, 10 + 15);
}
#[test]
fn player_blackjack_dealer_21() {
let result = play_hands(
10,
Vec::from([("", "5"), ("", "10"), ("", "6")]),
Vec::from([("", "A"), ("", "10")]),
)
.unwrap();
assert_eq!(result.result, PlayResult::Blackjack);
assert_eq!(result.returns, 10 + 15);
}
#[test]
fn dealer_blackjack() {
let result = play_hands(
10,
Vec::from([("", "A"), ("", "10")]),
Vec::from([("", "9"), ("", "10")]),
)
.unwrap();
assert_eq!(result.result, PlayResult::DealerBlackjack);
assert_eq!(result.returns, 0);
}
#[test]
fn both_blackjack() {
let result = play_hands(
10,
Vec::from([("", "A"), ("", "10")]),
Vec::from([("", "A"), ("", "10")]),
)
.unwrap();
assert_eq!(result.result, PlayResult::DealerBlackjack);
assert_eq!(result.returns, 0);
}
#[test]
/// A shuffle should leave us with the same number of cards
fn shuffle_same_cards() {
let mut table = Table::new(6, 0);
// Initial state sanity check
let discard_size = table.discard.len();
let pre = table.shoe.len() + discard_size;
println!("Pre shuffle: {}, {}", table.shoe.len(), discard_size);
table.shuffle();
let discard_size = table.discard.len();
let post = table.shoe.len() + table.discard.len();
println!("Post shuffle: {}, {}", table.shoe.len(), discard_size);
assert_eq!(pre, post);
// Deal out some cards and discard them, then shuffle to make sure we're good
let hand1 = Hand::from([table.deal_card(), table.deal_card()].to_vec());
let hand2 = Hand::from([table.deal_card(), table.deal_card()].to_vec());
table.discard_hand(hand1);
table.discard_hand(hand2);
assert_eq!(table.discard.len(), 4);
table.shuffle();
let discard_size = table.discard.len();
let post = table.shoe.len() + table.discard.len();
println!("Post shuffle: {}, {}", table.shoe.len(), discard_size);
assert_eq!(pre, post);
}
}

View file

@ -1,20 +1,32 @@
use crate::card::Card;
use itertools::Itertools;
use std::fmt;
use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)]
pub struct Hand {
cards: Vec<Card>,
discard: Option<Arc<RwLock<Vec<Card>>>>,
}
impl Hand {
pub fn new() -> Self {
Self { cards: Vec::new() }
Self {
cards: Vec::new(),
discard: None,
}
}
pub fn with_discard(mut self, discard: Arc<RwLock<Vec<Card>>>) -> Self {
self.discard = Some(discard);
self
}
/// Returns the value of the hand. If there are any aces, this is the highest value without
/// busting.
pub fn value(&self) -> u8 {
// TODO: Return the highest value without busting? And maybe let the player function
// determine if they have an ace and want to hit instead.
// Basic Strategy actually says what to do in the case of an ace
let mut num_aces = 0;
let mut sum = 0;
@ -68,12 +80,7 @@ impl Hand {
false
}
/// Returns true if the hand has a pair (can be split)
pub fn is_pair(&self) -> bool {
self.cards.len() == 2 && u8::from(&self.cards[0]) == u8::from(&self.cards[1])
}
/// Returns a read-only copy of the cards in the hand
/// Returns a copy of the cards in the hand
pub fn cards(&self) -> Vec<Card> {
self.cards.clone()
}
@ -82,25 +89,13 @@ impl Hand {
pub fn push(&mut self, card: Card) {
self.cards.push(card)
}
/// Split the hand into two
pub fn split(&mut self) -> Hand {
let other_cards: Vec<Card> = Vec::from(self.cards.pop().as_slice());
Hand { cards: other_cards }
}
}
impl From<Vec<Card>> for Hand {
fn from(cards: Vec<Card>) -> Self {
Self { cards }
}
}
impl From<Vec<(&str, &str)>> for Hand {
fn from(str_cards: Vec<(&str, &str)>) -> Self {
Self {
cards: str_cards.into_iter().map(|c| (c).into()).collect(),
cards,
discard: None,
}
}
}
@ -117,6 +112,18 @@ impl fmt::Display for Hand {
}
}
impl Drop for Hand {
fn drop(&mut self) {
match &mut self.discard {
Some(x) => {
let mut discard = x.write().unwrap();
discard.append(&mut self.cards)
}
None => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -147,4 +154,26 @@ mod tests {
h.cards.push(Card::new(Suit::Club, "A"));
assert_eq!(h.value(), 16);
}
#[test]
/// If we give the hand a discard pile, then it should throw its cards in there when it gets
/// dropped
fn discard_pile() {
let discard = Arc::new(RwLock::new(Vec::new()));
{
let hand =
Hand::from([Card::new(Suit::Heart, "1"), Card::new(Suit::Heart, "2")].to_vec())
.with_discard(discard.clone());
assert_eq!(hand.value(), 3);
// Hand still in scope, so discard pile is empty
assert_eq!(discard.read().unwrap().len(), 0);
}
let d = discard.read().unwrap();
// Hand went out of scope and got discarded
assert_eq!(d.len(), 2);
}
}

View file

@ -1,4 +1,3 @@
pub mod card;
pub mod error;
pub mod game;
pub mod hand;

View file

@ -1,45 +1,31 @@
use blackjack::card::Card;
use blackjack::game::{EndState, PlayResult, PlayerChoice, PlayerTurn, Table};
use blackjack::game::{Game, PlayResult, PlayerChoice};
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;
use std::path::{Path, PathBuf};
fn main() -> anyhow::Result<()> {
fn main() {
// TODO: Use anyhow for error handling, get rid of unwraps
let args = Args::parse();
match args.mode {
Mode::Interactive => interactive_play(),
Mode::OldMan => old_man(),
}
// TODO: CLI flags to choose how to play
interactive_play();
// old_man();
}
#[derive(ValueEnum, Clone, Debug)]
enum Mode {
Interactive,
OldMan,
}
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[arg(long, default_value = "interactive")]
mode: Mode,
}
fn interactive_play() -> anyhow::Result<()> {
fn interactive_play() {
// TODO: Persist bank between plays
// TODO: Make a way to reset bank
//
let term = Term::stdout();
let mut bank: u32 = match load_bank()? {
let mut bank: u32 = match load_bank() {
Some(b) => b,
None => 1_000,
};
let mut table = Table::new(6, bank).with_hit_on_soft_17();
let mut game = Game::new().with_decks(6).with_hit_on_soft_17();
let mut last_bet: Option<u32> = None;
loop {
@ -49,12 +35,11 @@ fn interactive_play() -> anyhow::Result<()> {
let mut bet: u32;
loop {
if last_bet.is_some() {
let last_bet = last_bet.unwrap();
print!("Your bet [{}]? ", last_bet);
print!("Your bet [{}]? ", last_bet.unwrap());
io::stdout().flush().unwrap();
let input = term.read_line()?;
let input = term.read_line().unwrap();
if input == "" {
bet = last_bet;
bet = last_bet.unwrap();
} else {
match input.parse() {
Ok(b) => bet = b,
@ -63,8 +48,8 @@ fn interactive_play() -> anyhow::Result<()> {
}
} else {
print!("Your bet? ");
io::stdout().flush()?;
match term.read_line()?.parse() {
io::stdout().flush().unwrap();
match term.read_line().unwrap().parse() {
Ok(b) => bet = b,
Err(_) => continue,
}
@ -79,274 +64,117 @@ fn interactive_play() -> anyhow::Result<()> {
last_bet = Some(bet);
println!();
let mut turn = table.deal_hand(bet);
if turn.shuffled {
println!("Deck was shuffled");
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);
term.clear_screen().unwrap();
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");
}
let split_turn = interactive_play_turn(&mut turn, &mut table)?;
if split_turn.is_none() {
table.dealers_turn()?;
let result = table.results(turn)?;
term.clear_screen()?;
print_result(&result);
} else {
let mut split_turn = split_turn.unwrap();
interactive_play_turn(&mut split_turn, &mut table)?;
table.dealers_turn()?;
let result = table.results(turn)?;
let split_result = table.results(split_turn)?;
term.clear_screen()?;
print_result(&result);
println!();
print_result(&split_result);
}
table.end_game()?;
bank = table.player_chips();
save_bank(bank)?;
save_bank(bank);
if bank == 0 {
println!("You're out of money.");
break;
return;
}
}
Ok(())
}
/// Play a turn. Returns another turn if this one was split
fn interactive_play_turn(
turn: &mut PlayerTurn,
table: &mut Table,
) -> anyhow::Result<Option<PlayerTurn>> {
let mut initial_play = true && !turn.was_split;
let mut other_turn = None;
fn interactive_decision(hand: &Hand, dealer_showing: &Card) -> PlayerChoice {
let term = Term::stdout();
term.clear_screen().unwrap();
println!("Dealer showing: {dealer_showing}");
println!("Your hand: {hand}\n");
if hand.value() == 21 {
println!("You have 21, standing.");
return PlayerChoice::Stand;
}
let choice: PlayerChoice;
let can_dd = hand.cards().len() == 2;
loop {
term.clear_screen()?;
let hand = turn.player_hand();
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 initial_play {
if can_dd {
msg += ", (d)ouble-down";
if hand.is_pair() {
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 initial_play => {
table.double_down(turn)?;
stop = true;
initial_play = false;
}
'p' if initial_play && turn.player_hand().is_pair() => {
other_turn = Some(table.split(turn)?);
initial_play = false;
}
_ => {}
io::stdout().write_all(msg.as_bytes()).unwrap();
io::stdout().flush().unwrap();
choice = match term.read_char().unwrap() {
'h' => PlayerChoice::Hit,
's' => PlayerChoice::Stand,
'd' if can_dd => PlayerChoice::DoubleDown,
_ => continue,
};
if stop {
break;
}
println!("\n");
break;
}
Ok(other_turn)
choice
}
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);
}
// TODO: Make this use a lookup table, this if logic is gnarly
fn basic_strategy(
hand: &Hand,
dealer_showing: &Card,
can_split: bool,
can_dd: bool,
) -> PlayerChoice {
fn basic_strategy(hand: &Hand, dealer_showing: &Card) -> 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.
let aces = hand.cards()[0].value == String::from("A");
match hand.value() {
12 if aces => 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 {
let can_dd = hand.cards().len() == 2;
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,
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() {
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,
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,
}
}
}
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() -> anyhow::Result<()> {
fn old_man() {
const START: u32 = 100_000;
const PER_DAY: u32 = 400;
const MIN_BET: u32 = 15;
@ -354,7 +182,7 @@ fn old_man() -> anyhow::Result<()> {
// 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 = MAX_WIN;
const MAX_LOSS: u32 = 100;
// Run sim for this many days
const DAYS: u16 = 30;
@ -363,76 +191,53 @@ fn old_man() -> anyhow::Result<()> {
println!("Starting with bank: ${bank}");
// Let's simulate this for 30 days
for day in 1..=DAYS {
bank -= PER_DAY;
let mut in_hand: u32 = PER_DAY;
bank -= in_hand;
let mut table = Table::new(6, PER_DAY).with_hit_on_soft_17();
let mut game = Game::new().with_decks(6).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 split_turn = basic_strategy_play_turn(&mut turn, &mut table)?;
if split_turn.is_none() {
table.dealers_turn()?;
table.results(turn)?;
// No need to handle result since we take chips off table at end of day
} else {
let mut split_turn = split_turn.unwrap();
basic_strategy_play_turn(&mut split_turn, &mut table)?;
table.dealers_turn()?;
// No need to handle result since we take chips off table at end of day
table.results(turn)?;
table.results(split_turn)?;
}
table.end_game()?;
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;
}
bank += table.player_chips();
println!(
"Day {day}, after {rounds} rounds, returns: ${}, bank: ${}",
table.player_chips() as i32 - PER_DAY as i32,
bank,
);
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}");
Ok(())
}
fn data_dir() -> PathBuf {
env::home_dir().unwrap().join(".local/share/blackjack")
}
fn save_bank(bank: u32) -> anyhow::Result<()> {
ensure_data_dir()?;
fn save_bank(bank: u32) {
ensure_data_dir();
let bank_path = data_dir().join("bank.txt");
Ok(fs::write(bank_path, bank.to_string())?)
fs::write(bank_path, bank.to_string()).unwrap();
}
fn load_bank() -> anyhow::Result<Option<u32>> {
fn load_bank() -> 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 fs::exists(&bank_path).unwrap() {
let bank = fs::read_to_string(&bank_path).unwrap().parse().unwrap();
if bank > 0 {
Ok(Some(bank))
Some(bank)
} else {
Ok(None)
None
}
} else {
Ok(None)
None
}
}
fn ensure_data_dir() -> anyhow::Result<()> {
if !fs::exists(data_dir())? {
fs::create_dir_all(data_dir())?;
fn ensure_data_dir() {
if !fs::exists(data_dir()).unwrap() {
fs::create_dir_all(data_dir()).unwrap();
}
Ok(())
}
#[cfg(test)]
@ -453,9 +258,7 @@ mod tests {
]
.to_vec()
),
&Card::new(Suit::Heart, "J"),
true,
true,
&Card::new(Suit::Heart, "J")
),
PlayerChoice::Stand,
)