Refactor the game flow and support splits
Behaves more like a client/server, rather than just taking a decision function. The CLI logic is more complex now, but the game is more flexible and and support splits (basically branching the hand off)
This commit is contained in:
parent
cb70077f5a
commit
69a4239f90
8 changed files with 820 additions and 334 deletions
28
Cargo.lock
generated
28
Cargo.lock
generated
|
|
@ -52,6 +52,12 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.9.1"
|
version = "2.9.1"
|
||||||
|
|
@ -62,10 +68,12 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
name = "blackjack"
|
name = "blackjack"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"console",
|
"console",
|
||||||
"itertools",
|
"itertools",
|
||||||
"rand",
|
"rand",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -275,6 +283,26 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.98"
|
||||||
clap = { version = "4.5.40", features = ["derive"] }
|
clap = { version = "4.5.40", features = ["derive"] }
|
||||||
console = "0.16.0"
|
console = "0.16.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
rand = "0.9.1"
|
rand = "0.9.1"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
|
|
||||||
27
src/card.rs
27
src/card.rs
|
|
@ -1,7 +1,8 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Suit {
|
pub enum Suit {
|
||||||
|
Unknown,
|
||||||
Club,
|
Club,
|
||||||
Diamond,
|
Diamond,
|
||||||
Heart,
|
Heart,
|
||||||
|
|
@ -15,12 +16,13 @@ impl fmt::Display for Suit {
|
||||||
Suit::Diamond => "♦",
|
Suit::Diamond => "♦",
|
||||||
Suit::Heart => "♥",
|
Suit::Heart => "♥",
|
||||||
Suit::Spade => "♠",
|
Suit::Spade => "♠",
|
||||||
|
Suit::Unknown => "",
|
||||||
};
|
};
|
||||||
write!(f, "{c}")
|
write!(f, "{c}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Card {
|
pub struct Card {
|
||||||
pub suit: Suit,
|
pub suit: Suit,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
|
@ -35,6 +37,27 @@ 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 {
|
impl From<&Card> for u8 {
|
||||||
fn from(card: &Card) -> u8 {
|
fn from(card: &Card) -> u8 {
|
||||||
match card.value.as_ref() {
|
match card.value.as_ref() {
|
||||||
|
|
|
||||||
17
src/error.rs
Normal file
17
src/error.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
605
src/game.rs
605
src/game.rs
|
|
@ -1,19 +1,36 @@
|
||||||
use crate::card::{deck_without_jokers, Card};
|
use crate::card::{deck_without_jokers, Card};
|
||||||
|
use crate::error::BlackjackError;
|
||||||
use crate::hand::Hand;
|
use crate::hand::Hand;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum PlayerChoice {
|
pub enum PlayerChoice {
|
||||||
Hit,
|
Hit,
|
||||||
Stand,
|
Stand,
|
||||||
DoubleDown,
|
DoubleDown,
|
||||||
// TODO
|
Split,
|
||||||
// Split,
|
|
||||||
// TODO
|
// TODO
|
||||||
// Surrender,
|
// 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 {
|
pub enum PlayResult {
|
||||||
Win, // Player didn't bust, and either has higher value or dealer busted
|
Win, // Player didn't bust, and either has higher value or dealer busted
|
||||||
Blackjack, // Player won with a face card and ace
|
Blackjack, // Player won with a face card and ace
|
||||||
|
|
@ -23,67 +40,75 @@ pub enum PlayResult {
|
||||||
Bust, // Player lost because they busted
|
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
|
/// The state at the end of a round, useful for printing out
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct EndState {
|
pub struct EndState {
|
||||||
pub result: PlayResult,
|
pub result: PlayResult,
|
||||||
pub dealer_cards: Vec<Card>,
|
pub dealer_hand: Hand,
|
||||||
pub player_cards: Vec<Card>,
|
pub player_hand: Hand,
|
||||||
pub shuffled: bool,
|
pub returns: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EndState {
|
pub struct Table {
|
||||||
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,
|
rng: rand::rngs::ThreadRng,
|
||||||
shoe: Vec<Card>,
|
shoe: Vec<Card>,
|
||||||
/// Discard pile. When the shoe runs out, this gets mixed with the shoe and reshuffled.
|
/// Discard pile. When the shoe runs out, this gets mixed with the shoe and reshuffled.
|
||||||
discard: Arc<RwLock<Vec<Card>>>,
|
discard: Vec<Card>,
|
||||||
/// The current card count, see: https://en.wikipedia.org/wiki/Blackjack#Card_counting
|
/// The current card count, see: https://en.wikipedia.org/wiki/Blackjack#Card_counting
|
||||||
count: i16,
|
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
|
||||||
hit_on_soft_17: bool,
|
hit_on_soft_17: bool,
|
||||||
|
|
||||||
/// Returns when hitting blackjack
|
/// Returns when hitting blackjack
|
||||||
blackjack_returns: f32,
|
blackjack_returns: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Table {
|
||||||
pub fn new() -> Self {
|
pub fn new(decks: u8, player_chips: u32) -> Self {
|
||||||
Self {
|
let mut table = Self {
|
||||||
// Game settings
|
// Game settings
|
||||||
hit_on_soft_17: false,
|
hit_on_soft_17: false,
|
||||||
blackjack_returns: 3.0 / 2.0,
|
blackjack_returns: 3.0 / 2.0,
|
||||||
|
|
||||||
// Game state
|
// Game state
|
||||||
rng: rand::rng(),
|
rng: rand::rng(),
|
||||||
|
phase: Phase::New,
|
||||||
shoe: Vec::new(),
|
shoe: Vec::new(),
|
||||||
discard: Arc::new(RwLock::new(Vec::new())),
|
discard: Vec::new(),
|
||||||
|
player_chips,
|
||||||
|
dealer_hand: Hand::new(),
|
||||||
|
pending_turns: 0,
|
||||||
|
pending_results: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/// Add some number of decks to the shoe, and shuffle it
|
for _ in 0..decks {
|
||||||
pub fn with_decks(mut self, count: u8) -> Self {
|
table.shoe.append(&mut deck_without_jokers())
|
||||||
for _ in 0..count {
|
|
||||||
self.shoe.append(&mut deck_without_jokers())
|
|
||||||
}
|
}
|
||||||
self.shoe.shuffle(&mut self.rng);
|
table.shoe.shuffle(&mut table.rng);
|
||||||
|
|
||||||
self
|
table
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the dealer to hit on a soft 17 (one involving an Ace)
|
/// Sets the dealer to hit on a soft 17 (one involving an Ace)
|
||||||
|
|
@ -98,138 +123,20 @@ impl Game {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Play one round of Blackjack
|
pub fn player_chips(&self) -> u32 {
|
||||||
///
|
self.player_chips
|
||||||
/// 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, 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;
|
/// Reset the shoe - combine shoe and discard and shuffle
|
||||||
*money_on_table -= bet_amount;
|
fn shuffle(self: &mut Self) {
|
||||||
|
self.shoe.append(&mut self.discard);
|
||||||
// Deal cards
|
assert_eq!(self.discard.len(), 0);
|
||||||
let mut dealer_hand =
|
self.shoe.shuffle(&mut self.rng);
|
||||||
Hand::from([self.deal(), self.deal()].to_vec()).with_discard(self.discard.clone());
|
self.count = 0;
|
||||||
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
|
/// Deal a single card from the shoe
|
||||||
fn deal(self: &mut Self) -> Card {
|
fn deal_card(&mut self) -> Card {
|
||||||
let card = self.shoe.pop().unwrap();
|
let card = self.shoe.pop().unwrap();
|
||||||
|
|
||||||
if u8::from(&card) < 6 {
|
if u8::from(&card) < 6 {
|
||||||
|
|
@ -241,12 +148,370 @@ impl Game {
|
||||||
card
|
card
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the shoe - combine shoe and discard and shuffle
|
/// Deal out a hand and start a game
|
||||||
fn shuffle(self: &mut Self) {
|
pub fn deal_hand(&mut self, bet: u32) -> PlayerTurn {
|
||||||
let mut discard = self.discard.write().unwrap();
|
self.dealer_hand = Hand::new();
|
||||||
self.shoe.append(&mut discard);
|
let mut player = PlayerTurn::new(bet);
|
||||||
assert_eq!(discard.len(), 0);
|
|
||||||
self.shoe.shuffle(&mut self.rng);
|
// Shuffle the deck if we've played over 75% of cards
|
||||||
self.count = 0;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
src/hand.rs
73
src/hand.rs
|
|
@ -1,32 +1,20 @@
|
||||||
use crate::card::Card;
|
use crate::card::Card;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct Hand {
|
pub struct Hand {
|
||||||
cards: Vec<Card>,
|
cards: Vec<Card>,
|
||||||
discard: Option<Arc<RwLock<Vec<Card>>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hand {
|
impl Hand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self { cards: Vec::new() }
|
||||||
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
|
/// Returns the value of the hand. If there are any aces, this is the highest value without
|
||||||
/// busting.
|
/// busting.
|
||||||
pub fn value(&self) -> u8 {
|
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 num_aces = 0;
|
||||||
let mut sum = 0;
|
let mut sum = 0;
|
||||||
|
|
||||||
|
|
@ -80,7 +68,12 @@ impl Hand {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a copy of the cards in the hand
|
/// 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
|
||||||
pub fn cards(&self) -> Vec<Card> {
|
pub fn cards(&self) -> Vec<Card> {
|
||||||
self.cards.clone()
|
self.cards.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -89,13 +82,25 @@ impl Hand {
|
||||||
pub fn push(&mut self, card: Card) {
|
pub fn push(&mut self, card: Card) {
|
||||||
self.cards.push(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 {
|
impl From<Vec<Card>> for Hand {
|
||||||
fn from(cards: Vec<Card>) -> Self {
|
fn from(cards: Vec<Card>) -> Self {
|
||||||
|
Self { cards }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<(&str, &str)>> for Hand {
|
||||||
|
fn from(str_cards: Vec<(&str, &str)>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cards,
|
cards: str_cards.into_iter().map(|c| (c).into()).collect(),
|
||||||
discard: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,18 +117,6 @@ 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -154,26 +147,4 @@ mod tests {
|
||||||
h.cards.push(Card::new(Suit::Club, "A"));
|
h.cards.push(Card::new(Suit::Club, "A"));
|
||||||
assert_eq!(h.value(), 16);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod card;
|
pub mod card;
|
||||||
|
pub mod error;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
pub mod hand;
|
pub mod hand;
|
||||||
|
|
|
||||||
411
src/main.rs
411
src/main.rs
|
|
@ -1,5 +1,5 @@
|
||||||
use blackjack::card::Card;
|
use blackjack::card::Card;
|
||||||
use blackjack::game::{Game, PlayResult, PlayerChoice};
|
use blackjack::game::{EndState, PlayResult, PlayerChoice, PlayerTurn, Table};
|
||||||
use blackjack::hand::Hand;
|
use blackjack::hand::Hand;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use console::Term;
|
use console::Term;
|
||||||
|
|
@ -7,12 +7,11 @@ use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn main() {
|
fn main() -> anyhow::Result<()> {
|
||||||
// TODO: Use anyhow for error handling, get rid of unwraps
|
// TODO: Use anyhow for error handling, get rid of unwraps
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
println!("{:?}", args);
|
|
||||||
match args.mode {
|
match args.mode {
|
||||||
Mode::Interactive => interactive_play(),
|
Mode::Interactive => interactive_play(),
|
||||||
Mode::OldMan => old_man(),
|
Mode::OldMan => old_man(),
|
||||||
|
|
@ -32,16 +31,15 @@ struct Args {
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interactive_play() {
|
fn interactive_play() -> anyhow::Result<()> {
|
||||||
// TODO: Make a way to reset bank
|
// TODO: Make a way to reset bank
|
||||||
//
|
|
||||||
let term = Term::stdout();
|
let term = Term::stdout();
|
||||||
let mut bank: u32 = match load_bank() {
|
let mut bank: u32 = match load_bank()? {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => 1_000,
|
None => 1_000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut game = Game::new().with_decks(6).with_hit_on_soft_17();
|
let mut table = Table::new(6, bank).with_hit_on_soft_17();
|
||||||
let mut last_bet: Option<u32> = None;
|
let mut last_bet: Option<u32> = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -51,11 +49,12 @@ fn interactive_play() {
|
||||||
let mut bet: u32;
|
let mut bet: u32;
|
||||||
loop {
|
loop {
|
||||||
if last_bet.is_some() {
|
if last_bet.is_some() {
|
||||||
print!("Your bet [{}]? ", last_bet.unwrap());
|
let last_bet = last_bet.unwrap();
|
||||||
|
print!("Your bet [{}]? ", last_bet);
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
let input = term.read_line().unwrap();
|
let input = term.read_line()?;
|
||||||
if input == "" {
|
if input == "" {
|
||||||
bet = last_bet.unwrap();
|
bet = last_bet;
|
||||||
} else {
|
} else {
|
||||||
match input.parse() {
|
match input.parse() {
|
||||||
Ok(b) => bet = b,
|
Ok(b) => bet = b,
|
||||||
|
|
@ -64,8 +63,8 @@ fn interactive_play() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print!("Your bet? ");
|
print!("Your bet? ");
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush()?;
|
||||||
match term.read_line().unwrap().parse() {
|
match term.read_line()?.parse() {
|
||||||
Ok(b) => bet = b,
|
Ok(b) => bet = b,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
}
|
}
|
||||||
|
|
@ -80,13 +79,123 @@ fn interactive_play() {
|
||||||
last_bet = Some(bet);
|
last_bet = Some(bet);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let result = game.play(&mut bank, bet, interactive_decision);
|
let mut turn = table.deal_hand(bet);
|
||||||
let dealer_hand = Hand::from(result.dealer_cards);
|
if turn.shuffled {
|
||||||
let player_hand = Hand::from(result.player_cards);
|
println!("Deck was shuffled");
|
||||||
|
}
|
||||||
|
|
||||||
term.clear_screen().unwrap();
|
let split_turn = interactive_play_turn(&mut turn, &mut table)?;
|
||||||
println!("Dealer's hand: {} = {}", dealer_hand, dealer_hand.value());
|
if split_turn.is_none() {
|
||||||
println!("Your hand: {} = {}", player_hand, player_hand.value());
|
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)?;
|
||||||
|
|
||||||
|
if bank == 0 {
|
||||||
|
println!("You're out of money.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
let term = Term::stdout();
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
if stop {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(other_turn)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
match result.result {
|
||||||
PlayResult::Win => println!("You won!"),
|
PlayResult::Win => println!("You won!"),
|
||||||
|
|
@ -96,102 +205,148 @@ fn interactive_play() {
|
||||||
PlayResult::DealerBlackjack => println!("Dealer got blackjack"),
|
PlayResult::DealerBlackjack => println!("Dealer got blackjack"),
|
||||||
PlayResult::Bust => println!("You busted"),
|
PlayResult::Bust => println!("You busted"),
|
||||||
}
|
}
|
||||||
if result.shuffled {
|
println!("You got: ${}", result.returns);
|
||||||
println!("Deck was shuffled at beginning of round");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save_bank(bank);
|
// TODO: Make this use a lookup table, this if logic is gnarly
|
||||||
|
fn basic_strategy(
|
||||||
if bank == 0 {
|
hand: &Hand,
|
||||||
println!("You're out of money.");
|
dealer_showing: &Card,
|
||||||
return;
|
can_split: bool,
|
||||||
}
|
can_dd: bool,
|
||||||
}
|
) -> PlayerChoice {
|
||||||
}
|
|
||||||
|
|
||||||
fn interactive_decision(hand: &Hand, dealer_showing: &Card, _count: i16) -> PlayerChoice {
|
|
||||||
let term = Term::stdout();
|
|
||||||
term.clear_screen().unwrap();
|
|
||||||
|
|
||||||
println!("Dealer showing: {dealer_showing}");
|
|
||||||
println!("Your hand: {hand}");
|
|
||||||
println!();
|
|
||||||
|
|
||||||
if hand.value() == 21 {
|
|
||||||
println!("You have 21, standing.");
|
|
||||||
return PlayerChoice::Stand;
|
|
||||||
}
|
|
||||||
|
|
||||||
let choice: PlayerChoice;
|
|
||||||
let can_dd = hand.cards().len() == 2;
|
|
||||||
loop {
|
|
||||||
let mut msg = String::from("(h)it, (s)tand");
|
|
||||||
if can_dd {
|
|
||||||
msg += ", (d)ouble-down";
|
|
||||||
}
|
|
||||||
msg += "? ";
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
println!("\n");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
choice
|
|
||||||
}
|
|
||||||
|
|
||||||
fn basic_strategy(hand: &Hand, dealer_showing: &Card, _count: i16) -> PlayerChoice {
|
|
||||||
// Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy
|
// Source: https://en.wikipedia.org/wiki/Blackjack#Basic_strategy
|
||||||
let dv = u8::from(dealer_showing);
|
let dv = u8::from(dealer_showing);
|
||||||
let can_dd = hand.cards().len() == 2;
|
|
||||||
if hand.has_ace() && hand.cards().len() == 2 {
|
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 {
|
||||||
// Ace and some other card
|
// Ace and some other card
|
||||||
match hand.value() {
|
match hand.value() {
|
||||||
v if v >= 19 => PlayerChoice::Stand,
|
20 => PlayerChoice::Stand,
|
||||||
v if v == 18 && (7..=8).contains(&dv) => PlayerChoice::Stand,
|
19 if dv == 6 && can_dd => PlayerChoice::DoubleDown,
|
||||||
v if v == 18 && dv < 7 && can_dd => PlayerChoice::DoubleDown,
|
19 => PlayerChoice::Stand,
|
||||||
v if v == 18 && dv < 7 => PlayerChoice::Stand,
|
18 => match dv {
|
||||||
v if v == 17 && (3..=6).contains(&dv) && can_dd => PlayerChoice::DoubleDown,
|
2..=6 if can_dd => PlayerChoice::DoubleDown,
|
||||||
v if v == 17 && (3..=6).contains(&dv) => PlayerChoice::Hit,
|
9..=11 => PlayerChoice::Hit,
|
||||||
v if (15..=16).contains(&v) && (4..=6).contains(&dv) && can_dd => {
|
_ => PlayerChoice::Stand,
|
||||||
PlayerChoice::DoubleDown
|
},
|
||||||
}
|
17 => match dv {
|
||||||
v if (15..=16).contains(&v) && (4..=6).contains(&dv) => PlayerChoice::Hit,
|
3..=6 if can_dd => PlayerChoice::DoubleDown,
|
||||||
v if (13..=14).contains(&v) && (5..=6).contains(&dv) && can_dd => {
|
_ => PlayerChoice::Hit,
|
||||||
PlayerChoice::DoubleDown
|
},
|
||||||
}
|
15..=16 => match dv {
|
||||||
v if (13..=14).contains(&v) && (5..=6).contains(&dv) => PlayerChoice::Hit,
|
4..=6 if can_dd => PlayerChoice::DoubleDown,
|
||||||
v if v == 12 && dv == 6 && can_dd => PlayerChoice::DoubleDown,
|
_ => PlayerChoice::Hit,
|
||||||
v if v == 12 && dv == 6 => PlayerChoice::Hit,
|
},
|
||||||
|
13..=14 => match dv {
|
||||||
|
5..=6 if can_dd => PlayerChoice::DoubleDown,
|
||||||
|
_ => PlayerChoice::Hit,
|
||||||
|
},
|
||||||
|
12 if dv == 6 && can_dd => PlayerChoice::DoubleDown,
|
||||||
_ => PlayerChoice::Hit,
|
_ => PlayerChoice::Hit,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match hand.value() {
|
match hand.value() {
|
||||||
v if v >= 17 => PlayerChoice::Stand,
|
17..=21 => PlayerChoice::Stand,
|
||||||
v if v > 12 && dv < 7 => PlayerChoice::Stand,
|
13..=16 if dv >= 7 => PlayerChoice::Hit,
|
||||||
v if v == 12 && (4..=6).contains(&dv) => PlayerChoice::Stand,
|
13..=16 => PlayerChoice::Stand,
|
||||||
v if v == 11 && can_dd => PlayerChoice::DoubleDown,
|
12 => match dv {
|
||||||
v if v == 11 => PlayerChoice::Hit,
|
4..=6 => PlayerChoice::Stand,
|
||||||
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,
|
_ => 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
/// 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
|
/// 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
|
/// 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.
|
/// addition to points/comps which he would use to eat brunch for free.
|
||||||
///
|
///
|
||||||
/// Does this actually work, or was the driver full of it?
|
/// Does this actually work, or was the driver full of it?
|
||||||
fn old_man() {
|
fn old_man() -> anyhow::Result<()> {
|
||||||
const START: u32 = 100_000;
|
const START: u32 = 100_000;
|
||||||
const PER_DAY: u32 = 400;
|
const PER_DAY: u32 = 400;
|
||||||
const MIN_BET: u32 = 15;
|
const MIN_BET: u32 = 15;
|
||||||
|
|
@ -199,7 +354,7 @@ fn old_man() {
|
||||||
// Walk away when we're up by this much
|
// Walk away when we're up by this much
|
||||||
const MAX_WIN: u32 = 100;
|
const MAX_WIN: u32 = 100;
|
||||||
// Walk away when we're down by this much
|
// Walk away when we're down by this much
|
||||||
const MAX_LOSS: u32 = 100;
|
const MAX_LOSS: u32 = MAX_WIN;
|
||||||
// Run sim for this many days
|
// Run sim for this many days
|
||||||
const DAYS: u16 = 30;
|
const DAYS: u16 = 30;
|
||||||
|
|
||||||
|
|
@ -208,53 +363,76 @@ fn old_man() {
|
||||||
println!("Starting with bank: ${bank}");
|
println!("Starting with bank: ${bank}");
|
||||||
// Let's simulate this for 30 days
|
// Let's simulate this for 30 days
|
||||||
for day in 1..=DAYS {
|
for day in 1..=DAYS {
|
||||||
let mut in_hand: u32 = PER_DAY;
|
bank -= PER_DAY;
|
||||||
bank -= in_hand;
|
|
||||||
|
|
||||||
let mut game = Game::new().with_decks(6).with_hit_on_soft_17();
|
let mut table = Table::new(6, PER_DAY).with_hit_on_soft_17();
|
||||||
let mut rounds = 0;
|
let mut rounds = 0;
|
||||||
while in_hand > MIN_BET && in_hand < (PER_DAY + MAX_WIN) && in_hand > (PER_DAY - MAX_LOSS) {
|
while table.player_chips() > MIN_BET
|
||||||
let bet = MIN_BET;
|
&& table.player_chips() < (PER_DAY + MAX_WIN)
|
||||||
game.play(&mut in_hand, bet, basic_strategy);
|
&& 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()?;
|
||||||
rounds += 1;
|
rounds += 1;
|
||||||
}
|
}
|
||||||
println!("Day {day}, after {rounds} rounds, in-hand: ${in_hand}");
|
bank += table.player_chips();
|
||||||
bank += in_hand;
|
println!(
|
||||||
|
"Day {day}, after {rounds} rounds, returns: ${}, bank: ${}",
|
||||||
|
table.player_chips() as i32 - PER_DAY as i32,
|
||||||
|
bank,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pl: i64 = bank as i64 - START as i64;
|
let pl: i64 = bank as i64 - START as i64;
|
||||||
println!("Ending with: ${bank}");
|
println!("Ending with: ${bank}");
|
||||||
println!("Profit/Loss: ${pl}");
|
println!("Profit/Loss: ${pl}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data_dir() -> PathBuf {
|
fn data_dir() -> PathBuf {
|
||||||
env::home_dir().unwrap().join(".local/share/blackjack")
|
env::home_dir().unwrap().join(".local/share/blackjack")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_bank(bank: u32) {
|
fn save_bank(bank: u32) -> anyhow::Result<()> {
|
||||||
ensure_data_dir();
|
ensure_data_dir()?;
|
||||||
let bank_path = data_dir().join("bank.txt");
|
let bank_path = data_dir().join("bank.txt");
|
||||||
fs::write(bank_path, bank.to_string()).unwrap();
|
Ok(fs::write(bank_path, bank.to_string())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_bank() -> Option<u32> {
|
fn load_bank() -> anyhow::Result<Option<u32>> {
|
||||||
let bank_path = data_dir().join("bank.txt");
|
let bank_path = data_dir().join("bank.txt");
|
||||||
if fs::exists(&bank_path).unwrap() {
|
if fs::exists(&bank_path)? {
|
||||||
let bank = fs::read_to_string(&bank_path).unwrap().parse().unwrap();
|
let bank = fs::read_to_string(&bank_path)?.parse()?;
|
||||||
if bank > 0 {
|
if bank > 0 {
|
||||||
Some(bank)
|
Ok(Some(bank))
|
||||||
} else {
|
} else {
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_data_dir() {
|
fn ensure_data_dir() -> anyhow::Result<()> {
|
||||||
if !fs::exists(data_dir()).unwrap() {
|
if !fs::exists(data_dir())? {
|
||||||
fs::create_dir_all(data_dir()).unwrap();
|
fs::create_dir_all(data_dir())?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -276,7 +454,8 @@ mod tests {
|
||||||
.to_vec()
|
.to_vec()
|
||||||
),
|
),
|
||||||
&Card::new(Suit::Heart, "J"),
|
&Card::new(Suit::Heart, "J"),
|
||||||
0,
|
true,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
PlayerChoice::Stand,
|
PlayerChoice::Stand,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue