use crate::cards::{Card, Rank, Shoe};
use crate::money::Money;
use crate::Casino;
use anyhow::Result;
use anyhow::{bail, Context};
use colored::*;
use inquire::{Confirm, Select, Text};
use num::rational::Ratio;
use serde::{Deserialize, Serialize};
use spinners::{Spinner, Spinners};
use std::fmt;
use std::thread::sleep;
use std::time::Duration;
#[derive(Clone, Debug, Default)]
pub struct Hand {
pub cards: Vec<Card>,
pub hidden_count: usize,
pub standing: bool,
pub doubling_down: bool,
}
impl Hand {
pub fn new() -> Self {
Hand::default()
}
pub fn new_hidden(hidden_count: usize) -> Self {
Hand {
hidden_count,
..Default::default()
}
}
pub fn push(&mut self, card: Card) {
self.cards.push(card);
}
pub fn face_card(&self) -> &Card {
&self.cards[1]
}
pub fn is_natural_blackjack(&self) -> bool {
self.cards.len() == 2 && self.blackjack_sum() == 21
}
pub fn blackjack_sum(&self) -> u8 {
let mut sum = 0;
for card in self.cards.iter() {
sum += card.blackjack_value();
}
let has_ace = self.cards.iter().any(|c| matches!(&c.rank, Rank::Ace));
if has_ace && sum <= 11 {
sum += 10;
}
sum
}
pub fn is_bust(&self) -> bool {
self.blackjack_sum() > 21
}
pub fn can_double_down(&self) -> bool {
let player_sum = self.blackjack_sum();
self.cards.len() == 2 && !self.doubling_down && (player_sum == 10 || player_sum == 11)
}
pub fn can_split(&self) -> bool {
self.cards.len() == 2 && self.cards[0].rank == self.cards[1].rank
}
pub fn split(&mut self) -> Hand {
let moved_card = self.cards.pop().expect("Hand needs cards to split!");
let mut other_hand = Hand::default();
other_hand.push(moved_card);
other_hand
}
pub fn is_finished(&self) -> bool {
self.standing || self.blackjack_sum() > 21
}
}
impl fmt::Display for Hand {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut hand_str: String = "".to_owned();
for (i, card) in self.cards.iter().enumerate() {
if i < self.hidden_count {
hand_str.push_str("🂠");
} else {
hand_str.push_str(&card.to_string());
}
}
hand_str.push_str(" (");
for (i, card) in self.cards.iter().enumerate() {
if i < self.hidden_count {
hand_str.push('?');
} else {
hand_str.push_str(&card.blackjack_value().to_string());
}
if i < self.cards.len() - 1 {
hand_str.push_str(" + ");
}
}
if self.hidden_count > 0 {
hand_str.push_str(" = ?)");
} else {
hand_str.push_str(&format!(" = {})", self.blackjack_sum()));
}
write!(f, "{hand_str}")
}
}
pub trait BlackjackValue {
fn blackjack_value(&self) -> u8;
}
impl BlackjackValue for Card {
fn blackjack_value(&self) -> u8 {
match &self.rank {
Rank::Ace => 1,
Rank::Two => 2,
Rank::Three => 3,
Rank::Four => 4,
Rank::Five => 5,
Rank::Six => 6,
Rank::Seven => 7,
Rank::Eight => 8,
Rank::Nine => 9,
Rank::Ten | Rank::Jack | Rank::Queen | Rank::King => 10,
}
}
}
#[derive(Debug, Default)]
pub struct Blackjack {
config: BlackjackConfig,
shoe: Shoe,
dealer_hand: Hand,
player_hands: Vec<Hand>,
bet: Money,
insurance: bool,
splitting: bool,
doubling_down: bool,
current_hand: usize,
}
impl Blackjack {
pub fn new(config: BlackjackConfig) -> Self {
Self {
config: config.clone(),
shoe: Shoe::new(config.shoe_count, config.shuffle_at_penetration),
dealer_hand: Hand::new_hidden(1),
player_hands: vec![Hand::new()],
bet: Money::ZERO,
insurance: false,
splitting: false,
doubling_down: false,
current_hand: 0,
}
}
pub fn set_shoe(&mut self, shoe: Shoe) {
self.shoe = shoe;
}
pub fn set_bet(&mut self, bet: Money) {
assert!(bet.is_sign_positive());
assert!(!bet.is_zero());
self.bet = bet;
}
pub fn current_hand_index(&self) -> usize {
self.current_hand
}
pub fn current_player_hand(&self) -> &Hand {
&self.player_hands[self.current_hand]
}
pub fn card_to_dealer(&mut self) {
let card = self.shoe.draw_card();
self.dealer_hand.push(card);
}
fn card_to_player(&mut self) {
let card = self.shoe.draw_card();
self.player_hands[self.current_hand].push(card);
}
pub fn hit(&mut self) {
self.card_to_player();
}
pub fn initial_deal(&mut self) {
assert!(
self.dealer_hand.cards.is_empty(),
"Can't do the initial deal when cards have already been dealt."
);
self.card_to_dealer();
self.card_to_player();
self.card_to_dealer();
self.card_to_player();
}
pub fn can_place_insurance_bet(&self) -> bool {
matches!(self.dealer_hand.face_card().rank, Rank::Ace)
}
pub fn place_insurance_bet(&mut self) {
self.insurance = true;
}
pub fn can_double_down(&self) -> bool {
self.player_hands[self.current_hand].can_double_down()
}
pub fn can_split(&self) -> bool {
!self.splitting && self.player_hands[self.current_hand].can_split()
}
pub fn double_down(&mut self) {
self.card_to_player();
self.doubling_down = true;
}
pub fn split(&mut self) {
self.splitting = true;
let mut new_hand = self.player_hands[self.current_hand].split();
let card = self.shoe.draw_card();
new_hand.cards.push(card);
self.player_hands.push(new_hand);
self.card_to_player();
}
pub fn stand(&mut self) {
self.player_hands[self.current_hand].standing = true;
}
pub fn reveal_hole_card(&mut self) {
self.dealer_hand.hidden_count = 0;
}
pub fn next_hand(&mut self) {
self.current_hand += 1;
}
pub fn insurance_payout(&self) -> Money {
self.bet + self.bet * self.config.insurance_payout_ratio
}
pub fn natural_blackjack_payout(&self) -> Money {
self.bet + self.bet * self.config.blackjack_payout_ratio
}
}
pub fn play_blackjack() -> Result<()> {
let mut casino = Casino::from_filesystem()?;
let mut blackjack = Blackjack::new(casino.config.blackjack.clone());
println!("Your money: {}", casino.bankroll);
loop {
let bet_text = Text::new("How much will you bet?").prompt()?;
let bet = bet_text
.trim()
.parse::<Money>()
.with_context(|| "Failed to parse prompt text into an integer")?;
if casino.bankroll >= bet {
blackjack.set_bet(bet);
break;
} else {
println!("You can't bet that amount, try again.");
}
}
casino.subtract_bankroll(blackjack.bet)?;
println!("Betting {}", blackjack.bet);
let mut sp = Spinner::new(Spinners::Dots, "Dealing cards...".into());
sleep(Duration::from_millis(1_500));
sp.stop_with_message(format!("{}", "* The dealer issues your cards.".dimmed()));
blackjack.initial_deal();
println!("Dealer's hand: {}", blackjack.dealer_hand);
println!("Your hand: {}", blackjack.player_hands[0]);
if casino.bankroll >= blackjack.bet && blackjack.can_place_insurance_bet() {
let take_insurance = Confirm::new("Insurance?").with_default(false).prompt()?;
if take_insurance {
casino.subtract_bankroll(blackjack.bet)?;
blackjack.place_insurance_bet();
println!("You make an additional {} insurance bet.", blackjack.bet,);
} else {
println!("You choose for forego making an insurance bet.");
}
}
while !blackjack.player_hands.iter().all(|hand| hand.is_finished()) {
let mut options = vec!["Hit", "Stand"];
if casino.bankroll >= blackjack.bet && blackjack.can_double_down() {
options.push("Double");
}
if casino.bankroll >= blackjack.bet && blackjack.can_split() {
options.push("Split");
}
let prompt = format!(
"What will you do with hand â„– {}?",
blackjack.current_hand_index() + 1
);
let ans = Select::new(&prompt, options).prompt()?;
match ans {
"Hit" => {
let mut sp = Spinner::new(Spinners::Dots, "Dealing another card...".into());
sleep(Duration::from_millis(1_000));
sp.stop_with_message(format!(
"{}",
"* The dealer hands you another card.".dimmed()
));
blackjack.hit();
}
"Double" => {
println!(
"Your bet is now {}, and you will only receive one more card.",
blackjack.bet * 2u32
);
let mut sp = Spinner::new(Spinners::Dots, "Dealing another card...".into());
sleep(Duration::from_millis(1_000));
sp.stop_with_message(format!(
"{}",
"* The dealer hands you another card.".dimmed()
));
casino.subtract_bankroll(blackjack.bet)?;
blackjack.double_down();
}
"Split" => {
println!(
"You split hand â„– {} and place an additional {} bet.",
blackjack.current_hand_index() + 1,
blackjack.bet
);
let mut sp = Spinner::new(Spinners::Dots, "Dealing your cards...".into());
sleep(Duration::from_millis(1_000));
sp.stop_with_message(format!(
"{}",
"* The dealer hands you another two cards.".dimmed()
));
casino.subtract_bankroll(blackjack.bet)?;
blackjack.split();
}
"Stand" => {
blackjack.stand();
}
_ => bail!("Unknown answer received"),
}
println!(
"Your hand â„– {}: {}",
blackjack.current_hand_index() + 1,
blackjack.current_player_hand(),
);
if blackjack.current_player_hand().is_bust() {
casino.stats.blackjack.record_loss(blackjack.bet);
casino.stats.update_bankroll(casino.bankroll);
println!(
"HAND â„– {} BUST! You lose {}. You now have {}",
blackjack.current_hand_index() + 1,
blackjack.bet,
casino.bankroll
);
}
if blackjack.current_player_hand().is_finished() {
blackjack.next_hand();
}
}
if blackjack.player_hands.iter().any(|hand| !hand.is_bust()) {
let mut sp = Spinner::new(Spinners::Dots, "Revealing the hole card...".into());
sleep(Duration::from_millis(1_000));
sp.stop_with_message(format!("{}", "* Hole card revealed!".dimmed()));
blackjack.reveal_hole_card();
println!("Dealer's hand: {}", blackjack.dealer_hand);
while blackjack.dealer_hand.blackjack_sum() < 17 {
let mut sp = Spinner::new(Spinners::Dots, "Dealing another card...".into());
sleep(Duration::from_millis(1_000));
sp.stop_with_message(format!(
"{}",
"* The dealer issues themself another card.".dimmed()
));
blackjack.card_to_dealer();
println!("Dealer's hand: {}", blackjack.dealer_hand);
}
let mut sp = Spinner::new(Spinners::Dots, "Determining outcome...".into());
sleep(Duration::from_millis(1_000));
sp.stop_with_message(format!("{}", "* The hand is finished!".dimmed()));
for i in 0..blackjack
.player_hands
.iter()
.filter(|h| !h.is_bust())
.count()
{
let hand = &blackjack.player_hands[i];
if blackjack.dealer_hand.is_bust() {
casino.stats.blackjack.record_win(blackjack.bet);
casino.add_bankroll(blackjack.bet * 2u32);
println!(
"DEALER BUST! You receive {}. You now have {}",
blackjack.bet, casino.bankroll
);
} else if blackjack.dealer_hand.blackjack_sum() == hand.blackjack_sum() {
casino.stats.blackjack.record_push();
casino.add_bankroll(blackjack.bet);
println!("PUSH! Nobody wins.");
} else if blackjack.dealer_hand.blackjack_sum() > hand.blackjack_sum() {
let bet = blackjack.bet;
casino.stats.blackjack.record_loss(bet);
casino.stats.update_bankroll(casino.bankroll);
println!(
"HOUSE WINS! You lose {}. You now have {}",
bet, casino.bankroll
);
} else if hand.is_natural_blackjack() {
let payout = blackjack.natural_blackjack_payout();
casino.stats.blackjack.record_win(payout);
casino.add_bankroll(payout);
println!(
"BLACKJACK! You receive {}. You now have {}",
payout, casino.bankroll
);
} else {
let bet = blackjack.bet;
casino.stats.blackjack.record_win(bet);
casino.add_bankroll(bet);
println!(
"YOU WIN! You receive {}. You now have {}",
bet, casino.bankroll
);
}
}
if blackjack.dealer_hand.is_natural_blackjack() && blackjack.insurance {
let insurance_payout = blackjack.insurance_payout();
casino.add_bankroll(insurance_payout);
println!(
"DEALER BLACKJACK! Your insurance bet pays out {}. You now have {}.",
insurance_payout, casino.bankroll
);
}
}
casino.check_for_mister_green();
casino.save();
Ok(())
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BlackjackConfig {
#[serde(default = "BlackjackConfig::default_shoe_count")]
pub shoe_count: u8,
#[serde(default = "BlackjackConfig::default_shuffle_penetration")]
pub shuffle_at_penetration: f32,
#[serde(default = "BlackjackConfig::default_payout_ratio")]
pub payout_ratio: Ratio<i64>,
#[serde(default = "BlackjackConfig::default_blackjack_payout_ratio")]
pub blackjack_payout_ratio: Ratio<i64>,
#[serde(default = "BlackjackConfig::default_insurance_payout_ratio")]
pub insurance_payout_ratio: Ratio<i64>,
}
impl Default for BlackjackConfig {
fn default() -> Self {
Self {
shoe_count: Self::default_shoe_count(),
shuffle_at_penetration: Self::default_shuffle_penetration(),
payout_ratio: Self::default_payout_ratio(),
blackjack_payout_ratio: Self::default_blackjack_payout_ratio(),
insurance_payout_ratio: Self::default_insurance_payout_ratio(),
}
}
}
impl BlackjackConfig {
pub fn shuffle_shoe_threshold_count(&self) -> usize {
let threshold_fraction = 1f32 - self.shuffle_at_penetration;
let starting_shoe_size = self.shoe_count as usize * 52;
(starting_shoe_size as f32 * threshold_fraction) as usize
}
fn default_shoe_count() -> u8 {
4
}
fn default_shuffle_penetration() -> f32 {
0.75
}
fn default_payout_ratio() -> Ratio<i64> {
Ratio::new(1, 1)
}
fn default_blackjack_payout_ratio() -> Ratio<i64> {
Ratio::new(3, 2)
}
fn default_insurance_payout_ratio() -> Ratio<i64> {
Ratio::new(2, 1)
}
}