initial commit - basic TUI blackjack game
This commit is contained in:
commit
0a72da768e
9 changed files with 738 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
173
Cargo.lock
generated
Normal file
173
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "blackjack"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "blackjack"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
itertools = "0.14.0"
|
||||
rand = "0.9.1"
|
||||
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Blackjack CLI tool and simulator
|
||||
|
||||
I built this just for fun, and because I wanted to test some blackjack strategies
|
||||
70
src/card.rs
Normal file
70
src/card.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use std::fmt;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Suit {
|
||||
Club,
|
||||
Diamond,
|
||||
Heart,
|
||||
Spade,
|
||||
}
|
||||
|
||||
impl fmt::Display for Suit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let c = match self {
|
||||
Suit::Club => "♣",
|
||||
Suit::Diamond => "♦",
|
||||
Suit::Heart => "♥",
|
||||
Suit::Spade => "♠",
|
||||
};
|
||||
write!(f, "{c}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Card {
|
||||
pub suit: Suit,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl Card {
|
||||
pub fn new(suit: Suit, value: &str) -> Self {
|
||||
Self {
|
||||
suit,
|
||||
value: value.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Card> for u8 {
|
||||
fn from(card: &Card) -> u8 {
|
||||
match card.value.as_ref() {
|
||||
"A" => 11,
|
||||
"K" => 10,
|
||||
"Q" => 10,
|
||||
"J" => 10,
|
||||
x => x.parse().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Card {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", self.suit, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new deck of cards, without jokers
|
||||
pub(crate) fn deck_without_jokers() -> Vec<Card> {
|
||||
let mut deck = Vec::new();
|
||||
|
||||
for suit in [Suit::Club, Suit::Diamond, Suit::Heart, Suit::Spade] {
|
||||
for num_card in 2..=10 {
|
||||
deck.push(Card::new(suit.clone(), &(num_card.to_string())))
|
||||
}
|
||||
for face_card in ["J", "Q", "K", "A"] {
|
||||
deck.push(Card::new(suit.clone(), face_card))
|
||||
}
|
||||
}
|
||||
|
||||
deck
|
||||
}
|
||||
208
src/game.rs
Normal file
208
src/game.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
use crate::card::{deck_without_jokers, Card};
|
||||
use crate::hand::Hand;
|
||||
use rand::prelude::*;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub enum PlayerChoice {
|
||||
Hit,
|
||||
Stand,
|
||||
// TODO
|
||||
// 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 player_winnings: u64,
|
||||
pub dealer_cards: Vec<Card>,
|
||||
pub player_cards: Vec<Card>,
|
||||
}
|
||||
|
||||
impl EndState {
|
||||
fn new(
|
||||
result: PlayResult,
|
||||
player_winnings: u64,
|
||||
dealer_cards: Vec<Card>,
|
||||
player_cards: Vec<Card>,
|
||||
) -> Self {
|
||||
Self {
|
||||
result,
|
||||
player_winnings,
|
||||
dealer_cards,
|
||||
player_cards,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>(self: &mut Self, bet: 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();
|
||||
if self.shoe.len() < (self.shoe.len() + discard_size) * 1 / 4 {
|
||||
self.shuffle();
|
||||
}
|
||||
|
||||
// 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,
|
||||
0,
|
||||
dealer_hand.cards(),
|
||||
player_hand.cards(),
|
||||
);
|
||||
} else if player_hand.is_blackjack() {
|
||||
let returns = bet as f32 * self.blackjack_returns;
|
||||
return EndState::new(
|
||||
PlayResult::Blackjack,
|
||||
returns as u64,
|
||||
dealer_hand.cards(),
|
||||
player_hand.cards(),
|
||||
);
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
// if player busts, immediately lose bet
|
||||
if player_hand.value() > 21 {
|
||||
return EndState::new(
|
||||
PlayResult::Bust,
|
||||
0,
|
||||
dealer_hand.cards(),
|
||||
player_hand.cards(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Dealer turn
|
||||
while dealer_hand.value() < 17 {
|
||||
dealer_hand.push(self.deal());
|
||||
}
|
||||
|
||||
if dealer_hand.value() > 21 {
|
||||
EndState::new(
|
||||
PlayResult::Win,
|
||||
(bet * 2).into(),
|
||||
dealer_hand.cards(),
|
||||
player_hand.cards(),
|
||||
)
|
||||
} else if dealer_hand.value() < player_hand.value() {
|
||||
EndState::new(
|
||||
PlayResult::Win,
|
||||
(bet * 2).into(),
|
||||
dealer_hand.cards(),
|
||||
player_hand.cards(),
|
||||
)
|
||||
} else if dealer_hand.value() == player_hand.value() {
|
||||
EndState::new(
|
||||
PlayResult::Push,
|
||||
bet.into(),
|
||||
dealer_hand.cards(),
|
||||
player_hand.cards(),
|
||||
)
|
||||
} else {
|
||||
EndState::new(
|
||||
PlayResult::Lose,
|
||||
0,
|
||||
dealer_hand.cards(),
|
||||
player_hand.cards(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
179
src/hand.rs
Normal file
179
src/hand.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
use crate::card::Card;
|
||||
use itertools::Itertools;
|
||||
use std::fmt;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub struct Hand {
|
||||
cards: Vec<Card>,
|
||||
discard: Option<Arc<RwLock<Vec<Card>>>>,
|
||||
}
|
||||
|
||||
impl Hand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
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
|
||||
/// busting.
|
||||
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 sum = 0;
|
||||
|
||||
// count up the value of everything that's not an ace
|
||||
for card in &self.cards {
|
||||
if card.value == "A" {
|
||||
num_aces += 1;
|
||||
} else {
|
||||
sum += u8::from(card);
|
||||
}
|
||||
}
|
||||
|
||||
if num_aces == 0 {
|
||||
return sum;
|
||||
}
|
||||
|
||||
// Figure out the possible ace sums based on how many aces we have
|
||||
let mut values: Vec<u8> = [1, 11]
|
||||
.into_iter()
|
||||
.combinations_with_replacement(num_aces)
|
||||
.map(|a| a.iter().sum())
|
||||
.collect();
|
||||
|
||||
// Find the highest value without busting, adding in the rest of the cards. If they all
|
||||
// bust, then take the lowest value.
|
||||
values.sort();
|
||||
values.reverse();
|
||||
let mut total = 0;
|
||||
for v in values.iter() {
|
||||
total = v + sum;
|
||||
if total <= 21 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
total
|
||||
}
|
||||
|
||||
/// Returns true if the hand is a blackjack, meaning we have an ace and a 10-valued card
|
||||
pub fn is_blackjack(&self) -> bool {
|
||||
self.cards.len() == 2 && self.value() == 21
|
||||
}
|
||||
|
||||
/// Returns true if hand has an ace
|
||||
pub fn has_ace(&self) -> bool {
|
||||
for card in &self.cards {
|
||||
if card.value == "A" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns a copy of the cards in the hand
|
||||
pub fn cards(&self) -> Vec<Card> {
|
||||
self.cards.clone()
|
||||
}
|
||||
|
||||
/// Add a card to the hand
|
||||
pub fn push(&mut self, card: Card) {
|
||||
self.cards.push(card)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Card>> for Hand {
|
||||
fn from(cards: Vec<Card>) -> Self {
|
||||
Self {
|
||||
cards,
|
||||
discard: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Hand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for (i, c) in self.cards.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
write!(f, "{c}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::card::{Card, Suit};
|
||||
|
||||
#[test]
|
||||
fn no_aces() {
|
||||
let mut h = Hand::new();
|
||||
h.cards.push(Card::new(Suit::Club, "K"));
|
||||
h.cards.push(Card::new(Suit::Club, "7"));
|
||||
assert_eq!(h.value(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_ace() {
|
||||
let mut h = Hand::new();
|
||||
h.cards.push(Card::new(Suit::Club, "K"));
|
||||
h.cards.push(Card::new(Suit::Club, "A"));
|
||||
assert_eq!(h.value(), 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_aces() {
|
||||
let mut h = Hand::new();
|
||||
h.cards.push(Card::new(Suit::Club, "3"));
|
||||
h.cards.push(Card::new(Suit::Club, "A"));
|
||||
h.cards.push(Card::new(Suit::Club, "A"));
|
||||
h.cards.push(Card::new(Suit::Club, "A"));
|
||||
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);
|
||||
}
|
||||
}
|
||||
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod card;
|
||||
pub mod game;
|
||||
pub mod hand;
|
||||
93
src/main.rs
Normal file
93
src/main.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use blackjack::card::Card;
|
||||
use blackjack::game::{Game, PlayResult, PlayerChoice};
|
||||
use blackjack::hand::Hand;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
|
||||
fn main() {
|
||||
interactive_play();
|
||||
}
|
||||
|
||||
fn interactive_play() {
|
||||
let mut bank: u64 = 1_000;
|
||||
let mut game = Game::new().with_decks(6);
|
||||
let mut last_bet: Option<u32> = None;
|
||||
|
||||
loop {
|
||||
println!("\nMoney in the bank: {bank}");
|
||||
|
||||
// Bet checking loop
|
||||
let mut bet: u32;
|
||||
loop {
|
||||
if last_bet.is_some() {
|
||||
print!("Your bet [{}]? ", last_bet.unwrap());
|
||||
io::stdout().flush().unwrap();
|
||||
let input = read_input();
|
||||
if input == "" {
|
||||
bet = last_bet.unwrap();
|
||||
} else {
|
||||
bet = input.parse().unwrap();
|
||||
}
|
||||
} else {
|
||||
print!("Your bet? ");
|
||||
io::stdout().flush().unwrap();
|
||||
bet = read_input().parse().unwrap();
|
||||
}
|
||||
if bet as u64 <= bank {
|
||||
break;
|
||||
} else {
|
||||
println!("You don't have enough money!\n");
|
||||
}
|
||||
}
|
||||
|
||||
bank -= bet as u64;
|
||||
last_bet = Some(bet);
|
||||
println!();
|
||||
|
||||
let result = game.play(bet, interactive_decision);
|
||||
let dealer_hand = Hand::from(result.dealer_cards);
|
||||
let player_hand = Hand::from(result.player_cards);
|
||||
println!("Dealer's hand: {} = {}", dealer_hand, dealer_hand.value());
|
||||
println!("Your hand: {} = {}", player_hand, player_hand.value());
|
||||
|
||||
match result.result {
|
||||
PlayResult::Win => println!("You won!"),
|
||||
PlayResult::Blackjack => println!("Blackjack!"),
|
||||
PlayResult::Lose => println!("You lost"),
|
||||
PlayResult::Push => println!("Push"),
|
||||
PlayResult::DealerBlackjack => println!("Dealer got blackjack"),
|
||||
PlayResult::Bust => println!("You busted"),
|
||||
}
|
||||
|
||||
bank += result.player_winnings;
|
||||
}
|
||||
}
|
||||
|
||||
fn interactive_decision(hand: &Hand, dealer_showing: &Card) -> PlayerChoice {
|
||||
println!("\nDealer showing: {dealer_showing}");
|
||||
println!("Your hand: {hand}\n");
|
||||
|
||||
if hand.value() == 21 {
|
||||
println!("You have 21, standing.");
|
||||
return PlayerChoice::Stand;
|
||||
}
|
||||
|
||||
let choice: PlayerChoice;
|
||||
loop {
|
||||
print!("(h)it or (s)tand? ");
|
||||
io::stdout().flush().unwrap();
|
||||
choice = match read_input().to_lowercase().as_ref() {
|
||||
"h" => PlayerChoice::Hit,
|
||||
"s" => PlayerChoice::Stand,
|
||||
_ => continue,
|
||||
};
|
||||
break;
|
||||
}
|
||||
choice
|
||||
}
|
||||
|
||||
fn read_input() -> String {
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf).unwrap();
|
||||
buf.trim().to_owned()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue