From 1982d27c4f9025aa6d1791df3f27880e56055805 Mon Sep 17 00:00:00 2001 From: Niklas Kapelle Date: Mon, 13 May 2024 16:12:45 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 75 +++++++++++++++ Cargo.toml | 7 ++ src/blackjack.rs | 104 ++++++++++++++++++++ src/card.rs | 18 ++++ src/card_index.rs | 102 ++++++++++++++++++++ src/card_suit.rs | 47 +++++++++ src/console_blackjack.rs | 100 ++++++++++++++++++++ src/decks.rs | 54 +++++++++++ src/hand.rs | 199 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 19 ++++ 11 files changed, 726 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/blackjack.rs create mode 100644 src/card.rs create mode 100644 src/card_index.rs create mode 100644 src/card_suit.rs create mode 100644 src/console_blackjack.rs create mode 100644 src/decks.rs create mode 100644 src/hand.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3394219 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,75 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustjack" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..552b500 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "rustjack" +version = "0.1.0" +edition = "2021" + +[dependencies] +rand = "0.8.5" diff --git a/src/blackjack.rs b/src/blackjack.rs new file mode 100644 index 0000000..f96a00f --- /dev/null +++ b/src/blackjack.rs @@ -0,0 +1,104 @@ +use crate::{decks::new_blackjack_shoe, hand::Hand}; + +pub struct BlackjackGame { + shoe: Hand, + player_hand: Hand, + dealer_hand: Hand, +} + +pub enum DealResult { + DealerBlackJack, // Dealer has a blackjack. Player does not. + Push, // Dealer has a Blackjack and so does the Player + PlayerBlackJack, // Player has a blackjack. Dealer does not. + Continue, // Game continues +} + +pub enum HitResult { + Bust, + Continue, +} + +pub enum DealerPlayResult { + Bust, + Push, // Player and dealer have the same value + StandPlayerLose, // Dealer stands and wins + StandPlayerWin, // Dealer stands and loses +} + +impl BlackjackGame { + pub fn new() -> Self { + BlackjackGame { + shoe: new_blackjack_shoe(6), + player_hand: Hand::new(), + dealer_hand: Hand::new(), + } + } + + pub fn get_dealer_hand(&self) -> &Hand { + &self.dealer_hand + } + + pub fn get_player_hand(&self) -> &Hand { + &self.player_hand + } + + pub fn deal(&mut self) -> DealResult { + self.player_hand.add_card(self.shoe.pop_card().unwrap()); + self.dealer_hand.add_card(self.shoe.pop_card().unwrap()); + + self.player_hand.add_card(self.shoe.pop_card().unwrap()); + self.dealer_hand.add_card(self.shoe.pop_card().unwrap()); + + if self.dealer_hand.is_backjack() { + if self.player_hand.is_backjack() { + return DealResult::Push; + } + + return DealResult::DealerBlackJack; + } + + if self.player_hand.is_backjack() { + return DealResult::PlayerBlackJack; + } + + DealResult::Continue + } + + pub fn hit(&mut self) -> HitResult { + self.player_hand.add_card(self.shoe.pop_card().unwrap()); + + if self.player_hand.get_blackjack_value() > 21 { + return HitResult::Bust; + } + + HitResult::Continue + } + + pub fn stand(&mut self) { + // ??? + } + + pub fn dealer_play(&mut self) -> DealerPlayResult { + // Stand on 17 or above + // Hit on 16 and below + + loop { + let dealer_value = self.dealer_hand.get_blackjack_value(); + if dealer_value >= 17 { + let player_value = self.player_hand.get_blackjack_value(); + + return match dealer_value.cmp(&player_value) { + std::cmp::Ordering::Equal => DealerPlayResult::Push, + std::cmp::Ordering::Greater => DealerPlayResult::StandPlayerLose, + std::cmp::Ordering::Less => DealerPlayResult::StandPlayerWin, + }; + } + + self.dealer_hand.add_card(self.shoe.pop_card().unwrap()); + + if self.dealer_hand.get_blackjack_value() > 21 { + return DealerPlayResult::Bust; + } + } + } +} diff --git a/src/card.rs b/src/card.rs new file mode 100644 index 0000000..ee06ba0 --- /dev/null +++ b/src/card.rs @@ -0,0 +1,18 @@ +use std::fmt::Display; + +use crate::{card_index::CardIndex, card_suit::CardSuit}; + +pub struct Card { + pub suit: CardSuit, + pub index: CardIndex, +} + +impl Display for Card { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.suit.fmt(f)?; + f.write_str(" ")?; + self.index.fmt(f)?; + + Ok(()) + } +} diff --git a/src/card_index.rs b/src/card_index.rs new file mode 100644 index 0000000..852081b --- /dev/null +++ b/src/card_index.rs @@ -0,0 +1,102 @@ +use std::fmt::Display; + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] +#[repr(u8)] +pub enum CardIndex { + A = 14, + K = 13, + Q = 12, + J = 11, + N10 = 10, + N9 = 9, + N8 = 8, + N7 = 7, + N6 = 6, + N5 = 5, + N4 = 4, + N3 = 3, + N2 = 2, +} + +impl CardIndex { + pub fn get_blackjack_value(&self, ace_as_1: bool) -> u32 { + match self { + Self::J | Self::K | Self::Q => 10, + Self::A => { + if ace_as_1 { + 1 + } else { + 11 + } + } + _ => (*self as u8) as u32, + } + } +} + +impl TryFrom for CardIndex { + type Error = &'static str; + + fn try_from(value: char) -> Result { + match value { + 'A' => Ok(CardIndex::A), + 'K' => Ok(CardIndex::K), + 'Q' => Ok(CardIndex::Q), + 'J' => Ok(CardIndex::J), + 'T' => Ok(CardIndex::N10), + '9' => Ok(CardIndex::N9), + '8' => Ok(CardIndex::N8), + '7' => Ok(CardIndex::N7), + '6' => Ok(CardIndex::N6), + '5' => Ok(CardIndex::N5), + '4' => Ok(CardIndex::N4), + '3' => Ok(CardIndex::N3), + '2' => Ok(CardIndex::N2), + _ => Err("Not a card"), + } + } +} + +impl Display for CardIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let index = match self { + Self::A => "A".to_owned(), + Self::K => "K".to_owned(), + Self::Q => "Q".to_owned(), + Self::J => "J".to_owned(), + _ => (*self as u8).to_string(), + }; + + f.write_str(&index)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn blackjack_value_ace() { + assert_eq!(CardIndex::A.get_blackjack_value(true), 1); + assert_eq!(CardIndex::A.get_blackjack_value(false), 11); + } + + #[test] + fn blackjack_value_face() { + assert_eq!(CardIndex::J.get_blackjack_value(true), 10); + assert_eq!(CardIndex::Q.get_blackjack_value(true), 10); + assert_eq!(CardIndex::K.get_blackjack_value(true), 10); + } + + #[test] + fn blackjack_value_num() { + assert_eq!(CardIndex::N10.get_blackjack_value(true), 10); + assert_eq!(CardIndex::N9.get_blackjack_value(true), 9); + assert_eq!(CardIndex::N8.get_blackjack_value(true), 8); + assert_eq!(CardIndex::N5.get_blackjack_value(true), 5); + assert_eq!(CardIndex::N3.get_blackjack_value(true), 3); + assert_eq!(CardIndex::N2.get_blackjack_value(true), 2); + } +} diff --git a/src/card_suit.rs b/src/card_suit.rs new file mode 100644 index 0000000..b28eddb --- /dev/null +++ b/src/card_suit.rs @@ -0,0 +1,47 @@ +use std::fmt::Display; + +#[derive(Clone, Copy)] +pub enum CardSuit { + Spades, + Clubs, + Hearts, + Diamonds, +} + +impl CardSuit { + pub fn is_red(&self) -> bool { + matches!(self, CardSuit::Hearts | CardSuit::Diamonds) + } + + pub fn is_black(&self) -> bool { + !self.is_red() + } +} + +impl Display for CardSuit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let icon = match self { + CardSuit::Clubs => "♣️", + CardSuit::Diamonds => "♦️", + CardSuit::Hearts => "♥️", + CardSuit::Spades => "♠️", + }; + + f.write_str(icon)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn color() { + assert!(CardSuit::Hearts.is_red()); + assert!(CardSuit::Diamonds.is_red()); + assert!(CardSuit::Clubs.is_black()); + assert!(CardSuit::Spades.is_black()); + } +} diff --git a/src/console_blackjack.rs b/src/console_blackjack.rs new file mode 100644 index 0000000..fd15443 --- /dev/null +++ b/src/console_blackjack.rs @@ -0,0 +1,100 @@ +use std::io::stdin; + +use crate::blackjack::BlackjackGame; + +pub fn play() -> Result<(), Box> { + let mut game = BlackjackGame::new(); + + let deal = game.deal(); + + println!( + "Dealer hand: {} ?", + game.get_dealer_hand().get_card(0).unwrap() + ); + println!( + "Player hand: {} ({})", + game.get_player_hand(), + game.get_player_hand().get_blackjack_value() + ); + + match deal { + crate::blackjack::DealResult::DealerBlackJack => { + println!("Dealer has a blackjack!"); + return Ok(()); + } + crate::blackjack::DealResult::PlayerBlackJack => { + println!("Player has a blackjack!"); + return Ok(()); + } + crate::blackjack::DealResult::Push => { + println!("Both Player and dealer have a blackjack!"); + return Ok(()); + } + crate::blackjack::DealResult::Continue => {} + } + + loop { + println!("(H)it (S)tand"); + + let mut buffer = String::new(); + stdin().read_line(&mut buffer)?; + + let hit = buffer.trim() == "h"; + + if hit { + let hit_result = game.hit(); + + println!( + "Dealer hand: {} ?", + game.get_dealer_hand().get_card(0).unwrap() + ); + println!( + "Player hand: {} ({})", + game.get_player_hand(), + game.get_player_hand().get_blackjack_value() + ); + + match hit_result { + crate::blackjack::HitResult::Bust => { + println!("Player busts"); + return Ok(()); + } + crate::blackjack::HitResult::Continue => {} + } + } else { + game.stand(); + + let dealer_play = game.dealer_play(); + + println!( + "Dealer hand: {} ({})", + game.get_dealer_hand(), + game.get_dealer_hand().get_blackjack_value() + ); + println!( + "Player hand: {} ({})", + game.get_player_hand(), + game.get_player_hand().get_blackjack_value() + ); + + match dealer_play { + crate::blackjack::DealerPlayResult::Bust => { + println!("Dealer bust. Player wins."); + } + crate::blackjack::DealerPlayResult::Push => { + println!("Player and dealer have same value. Push."); + } + crate::blackjack::DealerPlayResult::StandPlayerLose => { + println!("Dealer wins"); + } + crate::blackjack::DealerPlayResult::StandPlayerWin => { + println!("Player wins"); + } + } + + break; + } + } + + Ok(()) +} diff --git a/src/decks.rs b/src/decks.rs new file mode 100644 index 0000000..c95bdc3 --- /dev/null +++ b/src/decks.rs @@ -0,0 +1,54 @@ +use crate::{card::Card, hand::Hand, CardIndex as CI, CardSuit as CS}; + +pub fn new_full_deck() -> Hand { + let mut hand = Hand::new(); + + for suit in [CS::Clubs, CS::Diamonds, CS::Hearts, CS::Spades] { + for index in [ + CI::A, + CI::J, + CI::K, + CI::Q, + CI::N10, + CI::N9, + CI::N8, + CI::N7, + CI::N6, + CI::N5, + CI::N4, + CI::N3, + CI::N2, + ] { + hand.add_card(Card { suit, index }); + } + } + + hand +} + +pub fn new_blackjack_shoe(decks: u32) -> Hand { + let mut hand = Hand::new(); + + for _ in 0..decks { + hand.merge(&mut new_full_deck()); + } + + hand.shuffle(); + + hand +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn full_deck_count() { + assert_eq!(new_full_deck().count(), 52); + } + + #[test] + fn shoe_count() { + assert_eq!(new_blackjack_shoe(6).count(), 6 * 52); + } +} diff --git a/src/hand.rs b/src/hand.rs new file mode 100644 index 0000000..ea2d75a --- /dev/null +++ b/src/hand.rs @@ -0,0 +1,199 @@ +use rand::{seq::SliceRandom, thread_rng}; +use std::fmt::Display; + +use crate::{card::Card, card_index::CardIndex}; + +pub struct Hand { + cards: Vec, +} + +impl Hand { + pub fn new() -> Self { + Hand { cards: Vec::new() } + } + + pub fn add_card(&mut self, card: Card) { + self.cards.push(card); + } + + pub fn get_card(&self, i: usize) -> Option<&Card> { + return self.cards.get(i); + } + + pub fn count(&self) -> usize { + self.cards.len() + } + + pub fn remove_card(&mut self, i: usize) -> Card { + self.cards.remove(i) + } + + pub fn pop_card(&mut self) -> Option { + self.cards.pop() + } + + pub fn shuffle(&mut self) { + self.cards.shuffle(&mut thread_rng()); + } + + pub fn is_backjack(&self) -> bool { + self.cards.len() == 2 && self.get_blackjack_value() == 21 + } + + /** + * Put another hand into this. + * Leaves the other hand empty. + */ + pub fn merge(&mut self, other: &mut Hand) { + self.cards.append(&mut other.cards); + } + + pub fn get_blackjack_value(&self) -> u32 { + let mut sum: u32 = self + .cards + .iter() + .map(|card| card.index.get_blackjack_value(true)) + .sum(); + + // Add 10 for the all aces (adds up to 11) until we go over 21 + let aces = self + .cards + .iter() + .filter(|card| card.index == CardIndex::A) + .count(); + + for _ in 0..aces { + if sum + 10 > 21 { + return sum; + } + + sum += 10; + } + + sum + } +} + +impl Display for Hand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for card in self.cards.iter() { + card.fmt(f)?; + f.write_str(" ")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_blackjack_ace_first() { + let mut hand = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Hearts, + index: CardIndex::A, + }); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Diamonds, + index: CardIndex::N10, + }); + assert!(hand.is_backjack()); + } + + #[test] + fn is_blackjack_ace_last() { + let mut hand = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Diamonds, + index: CardIndex::J, + }); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Hearts, + index: CardIndex::A, + }); + assert!(hand.is_backjack()); + } + + #[test] + fn is_not_blackjack_too_many() { + let mut hand = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Diamonds, + index: CardIndex::J, + }); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Hearts, + index: CardIndex::A, + }); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Spades, + index: CardIndex::A, + }); + assert!(!hand.is_backjack()); + } + + #[test] + fn is_not_blackjack_too_few() { + let mut hand = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Hearts, + index: CardIndex::A, + }); + assert!(!hand.is_backjack()); + } + + #[test] + fn is_not_blackjack_value_21() { + let mut hand = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Hearts, + index: CardIndex::K, + }); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Clubs, + index: CardIndex::N7, + }); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Hearts, + index: CardIndex::N4, + }); + assert!(!hand.is_backjack()); + } + + #[test] + fn blackjack_value() { + let mut hand = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Hearts, + index: CardIndex::K, + }); + assert_eq!(hand.get_blackjack_value(), 10); + } + + #[test] + fn merge_hands() { + let mut hand = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Diamonds, + index: CardIndex::N5, + }); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Spades, + index: CardIndex::Q, + }); + + let mut other = Hand::new(); + hand.add_card(Card { + suit: crate::card_suit::CardSuit::Spades, + index: CardIndex::K, + }); + + hand.merge(&mut other); + + assert_eq!(other.count(), 0); + assert_eq!(hand.count(), 3); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..91a7f87 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,19 @@ +#![allow(dead_code)] + +use std::error::Error; + +use console_blackjack::play; + +use crate::{card_index::CardIndex, card_suit::CardSuit}; + +mod blackjack; +mod card; +mod card_index; +mod card_suit; +mod console_blackjack; +mod decks; +mod hand; + +fn main() -> Result<(), Box> { + play() +}