231 lines
6.7 KiB
Rust
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);
|
|
}
|
|
}
|