From 027eb31551a2448ebc327ffc04269bc9736d9311 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 24 Mar 2022 11:08:24 +0100 Subject: [PATCH] Add a blackjack client for validating expected results from url --- Cargo.lock | 323 +++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + client/Cargo.toml | 21 +++ client/src/lib.rs | 161 ++++++++++++++++++ client/src/main.rs | 22 +++ logic/src/card.rs | 8 +- logic/src/deck_generator.rs | 26 +-- logic/src/error.rs | 18 +- 8 files changed, 561 insertions(+), 19 deletions(-) create mode 100644 client/Cargo.toml create mode 100644 client/src/lib.rs create mode 100644 client/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8008d3e..bb4b4d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + [[package]] name = "cfg-if" version = "1.0.0" @@ -126,10 +132,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", - "textwrap", + "textwrap 0.11.0", "unicode-width", ] +[[package]] +name = "clap" +version = "3.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap 0.15.0", +] + +[[package]] +name = "clap_derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "client" +version = "0.1.0" +dependencies = [ + "clap 3.1.6", + "lazy_static", + "logic", + "rand", + "reqwest", + "serde", + "serde_json", + "strum", + "strum_macros", + "tokio", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.1" @@ -156,7 +224,7 @@ checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" dependencies = [ "atty", "cast", - "clap", + "clap 2.34.0", "criterion-plot", "csv", "itertools", @@ -285,6 +353,15 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.9.0" @@ -325,6 +402,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -551,6 +643,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.2.3" @@ -581,6 +686,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" + [[package]] name = "itertools" version = "0.10.3" @@ -741,6 +852,24 @@ dependencies = [ "twoway", ] +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -787,6 +916,48 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -873,6 +1044,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "plotters" version = "0.3.1" @@ -907,6 +1084,30 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -1064,6 +1265,42 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwest" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1100,6 +1337,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -1112,6 +1359,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.6" @@ -1241,6 +1511,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.24.0" @@ -1303,6 +1579,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.30" @@ -1379,6 +1661,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.8" @@ -1538,6 +1830,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1637,6 +1935,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.79" @@ -1749,3 +2059,12 @@ name = "windows_x86_64_msvc" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml index 3ee9647..5747777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "logic", "server", + "client", ] exclude = [".cargo"] diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..5d0c098 --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "client" +version = "0.1.0" +edition = "2018" + +[lib] +name = "client" + +[dependencies] +lazy_static = "1.4.0" +rand = "0.8.5" +serde = { version = "1.0.136", features = ["derive"] } +serde_json = "1.0.79" +strum = "0.24.0" +strum_macros = "0.24.0" +reqwest = { version = "0.11.10", features = ["json"] } +clap = { version = "3.1.6", features = ["derive"] } +tokio = { version = "1", features = ["full"] } + +[dependencies.logic] +path = "../logic" diff --git a/client/src/lib.rs b/client/src/lib.rs new file mode 100644 index 0000000..88bc7cd --- /dev/null +++ b/client/src/lib.rs @@ -0,0 +1,161 @@ +use logic::card::{Card, Rank}; +use reqwest::get; +use serde::{Deserialize, Serialize}; +type Deck = Vec; + +async fn fetch_deck(url: String) -> reqwest::Result { + get(url).await?.json::().await +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct PlayerResult { + name: String, + hand: Deck, + score: u8, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GameResult { + deck: Deck, + players: Vec, + winner: String, +} + +pub trait ToInt { + fn to_int(&self) -> u8; +} + +impl ToInt for Card { + fn to_int(&self) -> u8 { + self.value.to_int() + } +} + +impl ToInt for Rank { + fn to_int(&self) -> u8 { + match &self { + Rank::Two => 2, + Rank::Three => 3, + Rank::Four => 4, + Rank::Five => 5, + Rank::Six => 6, + Rank::Seven => 7, + Rank::Eight => 8, + Rank::Nine => 9, + Rank::Ace => 11, + _ => 10, + } + } +} + +trait Blackjack { + fn has_blackjack(&self) -> bool; +} +trait Scorable { + fn score(&self) -> u8; +} +trait Bust { + fn is_bust(&self) -> bool; +} + +trait Draw { + fn hit_me(&self, other_score: u8) -> bool; +} + +trait ToPlayerResult { + fn to_result(&self) -> PlayerResult; +} + +pub struct Player { + name: String, + hand: Vec, + strategy: Box bool>, +} +impl ToPlayerResult for Player { + fn to_result(&self) -> PlayerResult { + PlayerResult { + name: self.name.clone(), + hand: self.hand.clone(), + score: self.score(), + } + } +} +impl Scorable for Player { + fn score(&self) -> u8 { + self.hand.iter().map(|x| x.to_int()).sum() + } +} +impl Draw for Player { + fn hit_me(&self, other_score: u8) -> bool { + (self.strategy)(self.score(), other_score) + } +} +impl Blackjack for Player { + fn has_blackjack(&self) -> bool { + self.hand.len() == 2 && self.score() == 21 + } +} +impl Bust for Player { + fn is_bust(&self) -> bool { + self.score() > 21 + } +} + +fn player_wins(original: Deck, player: Player, dealer: Player) -> GameResult { + GameResult { + winner: player.name.clone(), + players: vec![player.to_result(), dealer.to_result()], + deck: original, + } +} + +fn dealer_wins(original: Deck, player: Player, dealer: Player) -> GameResult { + GameResult { + winner: dealer.name.clone(), + players: vec![player.to_result(), dealer.to_result()], + deck: original, + } +} + +fn play_game(deck: Deck, player_name: String) -> GameResult { + println!("Playing as {:?}", player_name); + println!("{:#?}", deck); + let original = deck.clone(); + let mut playing_deck = deck.clone(); + let starting_hand = vec![playing_deck.remove(0), playing_deck.remove(0)]; + let mut player = Player { + name: player_name, + hand: starting_hand, + strategy: Box::new(|own_score, _| own_score < 17), + }; + let dealer_hand = vec![playing_deck.remove(0), playing_deck.remove(0)]; + let mut dealer = Player { + name: "Dealer".into(), + hand: dealer_hand, + strategy: Box::new(|own_score, player_score| own_score <= player_score), + }; + if dealer.has_blackjack() { + return dealer_wins(original, player, dealer); + } else if player.has_blackjack() { + return player_wins(original, player, dealer); + } + while (player.hit_me(dealer.score())) { + player.hand.push(playing_deck.remove(0)); + } + if (player.is_bust()) { + return dealer_wins(original, player, dealer); + } + let p_score = player.score(); + while (dealer.hit_me(p_score)) { + dealer.hand.push(playing_deck.remove(0)); + } + if (dealer.is_bust()) { + return player_wins(original, player, dealer); + } + dealer_wins(original, player, dealer) +} + +pub async fn play_blackjack(url: String, player_name: String) -> GameResult { + let deck = fetch_deck(url).await.expect("Could not parse deck"); + play_game(deck, player_name) +} diff --git a/client/src/main.rs b/client/src/main.rs new file mode 100644 index 0000000..e5fe2f3 --- /dev/null +++ b/client/src/main.rs @@ -0,0 +1,22 @@ +use clap::Parser; +use client::play_blackjack; + +#[derive(Parser, Debug)] +#[clap(version, about)] +pub struct ClientArgs { + #[clap( + short, + long, + default_value = "https://sandbox.getunleash.io/blackjack/shuffle" + )] + url: String, + #[clap(short, long, default_value = "Sam")] + player_name: String, +} + +#[tokio::main] +async fn main() { + let client_args = ClientArgs::parse(); + let result = play_blackjack(client_args.url, client_args.player_name).await; + println!("{:#?}", result) +} diff --git a/logic/src/card.rs b/logic/src/card.rs index 24e3468..148d590 100644 --- a/logic/src/card.rs +++ b/logic/src/card.rs @@ -69,11 +69,15 @@ impl Card { ((self.suit.clone() as u8) * 13 + self.value.clone() as u8) as usize } } +pub trait FromAnswer: Sized { + type Err; + fn from_answer(s: &str) -> Result; +} -impl FromStr for Card { +impl FromAnswer for Card { type Err = (); - fn from_str(s: &str) -> Result { + fn from_answer(s: &str) -> Result { let mut chars = s.chars(); if let Some(suit) = { match chars.next() { diff --git a/logic/src/deck_generator.rs b/logic/src/deck_generator.rs index 4df3bd0..ca0c21a 100644 --- a/logic/src/deck_generator.rs +++ b/logic/src/deck_generator.rs @@ -176,16 +176,16 @@ pub struct BlackjackQuery { #[cfg(test)] mod blackjack { use super::*; - use std::str::FromStr; + use crate::card::FromAnswer; #[test] fn four_aces_returns_four_aces_as_first_four_cards() { let f = four_aces(); let four_aces = [ - Card::from_str("SA").unwrap(), - Card::from_str("HA").unwrap(), - Card::from_str("CA").unwrap(), - Card::from_str("DA").unwrap(), + Card::from_answer("SA").unwrap(), + Card::from_answer("HA").unwrap(), + Card::from_answer("CA").unwrap(), + Card::from_answer("DA").unwrap(), ]; assert_eq!(four_aces, f.chunks(4).next().unwrap()) } @@ -196,8 +196,8 @@ mod blackjack { let first_player_card = b.get(0).unwrap(); let second_player_card = b.get(1).unwrap(); - assert_eq!(first_player_card, &Card::from_str("SA").unwrap()); - assert_eq!(second_player_card, &Card::from_str("SJ").unwrap()) + assert_eq!(first_player_card, &Card::from_answer("SA").unwrap()); + assert_eq!(second_player_card, &Card::from_answer("SJ").unwrap()) } #[test] @@ -206,17 +206,17 @@ mod blackjack { let first_dealer_card = b.get(2).unwrap(); let second_dealer_card = b.get(3).unwrap(); - assert_eq!(first_dealer_card, &Card::from_str("SA").unwrap()); - assert_eq!(second_dealer_card, &Card::from_str("SJ").unwrap()) + assert_eq!(first_dealer_card, &Card::from_answer("SA").unwrap()); + assert_eq!(second_dealer_card, &Card::from_answer("SJ").unwrap()) } #[test] fn complete_deck_returns_complete_and_correct_deck() { let four_aces = [ - Card::from_str("SA").unwrap(), - Card::from_str("HA").unwrap(), - Card::from_str("CA").unwrap(), - Card::from_str("DA").unwrap(), + Card::from_answer("SA").unwrap(), + Card::from_answer("HA").unwrap(), + Card::from_answer("CA").unwrap(), + Card::from_answer("DA").unwrap(), ]; let new_deck = complete_deck(four_aces.to_vec()); diff --git a/logic/src/error.rs b/logic/src/error.rs index 2945f76..b016234 100644 --- a/logic/src/error.rs +++ b/logic/src/error.rs @@ -1,7 +1,21 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt::{Display, Formatter}; -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct ErrorMessage { pub code: u16, pub message: String, } + +impl Display for ErrorMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &self) + } +} + +impl Error for ErrorMessage { + fn description(&self) -> &str { + &self.message + } +}