blackjack/src/game.rs

231 lines
6.7 KiB
Rust

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<Card>,
pub player_cards: Vec<Card>,
pub shuffled: bool,
}
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: Arc<RwLock<Vec<Card>>>,
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())),
}
}
/// 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<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 *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 {
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) {
let mut discard = self.discard.write().unwrap();
self.shoe.append(&mut discard);
assert_eq!(discard.len(), 0);
self.shoe.shuffle(&mut self.rng);
}
}