use crate::card::{deck_without_jokers, Card}; use crate::hand::Hand; use rand::prelude::*; use std::sync::{Arc, RwLock}; #[derive(Debug, PartialEq)] pub enum PlayerChoice { Hit, Stand, DoubleDown, // TODO // Split, // TODO // Surrender, } 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 Lose, // Player busts or dealer had higher without busting Push, // Dealer and player have same value DealerBlackjack, // Player lost because dealer had blackjack Bust, // Player lost because they busted } /// The state at the end of a round, useful for printing out pub struct EndState { pub result: PlayResult, pub dealer_cards: Vec, pub player_cards: Vec, pub shuffled: bool, } impl EndState { fn new( result: PlayResult, dealer_cards: Vec, player_cards: Vec, shuffled: bool, ) -> Self { Self { result, dealer_cards, player_cards, shuffled, } } } pub struct Game { rng: rand::rngs::ThreadRng, shoe: Vec, /// Discard pile. When the shoe runs out, this gets mixed with the shoe and reshuffled. discard: Arc>>, /// The current card count, see: https://en.wikipedia.org/wiki/Blackjack#Card_counting count: i16, hit_on_soft_17: bool, /// Returns when hitting blackjack blackjack_returns: f32, } 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(), shoe: Vec::new(), discard: Arc::new(RwLock::new(Vec::new())), count: 0, } } /// 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) pub fn with_hit_on_soft_17(mut self) -> Self { self.hit_on_soft_17 = true; self } /// Sets the return when getting a blackjack pub fn with_blackjack_returns(mut self, returns: f32) -> Self { self.blackjack_returns = returns; self } /// 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) and the card the dealer is showing (Card), /// and returns a choice (hit, stand, etc.). /// /// Returns the winnings of the round pub fn play( &mut self, money_on_table: &mut u32, bet_amount: u32, player_decision: F, ) -> EndState where F: Fn(&Hand, &Card, i16) -> 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], self.count); 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 { let card = self.shoe.pop().unwrap(); if u8::from(&card) < 6 { self.count += 1; } else if u8::from(&card) >= 10 { self.count -= 1; } card } /// Reset the shoe - combine shoe and discard and shuffle fn shuffle(self: &mut Self) { 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; } }