From ab68d26c6b16f973642d378900f5e6ac2be221b7 Mon Sep 17 00:00:00 2001 From: Kjetil Orbekk Date: Sun, 4 Sep 2022 19:42:57 -0400 Subject: Add turn/trick representation Includes convenience test and parsing utilities --- Cargo.lock | 119 ++++++++++++++++++++++++++++++++++++++++++++ webapp/.env | 1 + webapp/Cargo.toml | 4 ++ webapp/src/bridge_engine.rs | 108 ++++++++++++++++++++++++++++++++++++++++ webapp/src/card.rs | 86 ++++++++++++++++++++++++++++++++ webapp/src/main.rs | 9 ++++ 6 files changed, 327 insertions(+) create mode 100644 webapp/.env create mode 100644 webapp/src/bridge_engine.rs diff --git a/Cargo.lock b/Cargo.lock index d8609d6..f9d3453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,26 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -36,6 +56,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "getrandom" version = "0.2.7" @@ -170,6 +209,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "indexmap" version = "1.9.1" @@ -216,6 +270,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "once_cell" version = "1.14.0" @@ -300,6 +360,23 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "rustversion" version = "1.0.9" @@ -388,6 +465,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.34" @@ -518,6 +604,8 @@ name = "webapp" version = "0.1.0" dependencies = [ "console_error_panic_hook", + "dotenv", + "env_logger", "getrandom", "log", "rand", @@ -527,6 +615,37 @@ dependencies = [ "yew", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "yew" version = "0.19.3" diff --git a/webapp/.env b/webapp/.env new file mode 100644 index 0000000..edf7f1a --- /dev/null +++ b/webapp/.env @@ -0,0 +1 @@ +RUST_LOG=webapp,bridge diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index 78a5863..15a880a 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -12,3 +12,7 @@ strum = "0.24" strum_macros = "0.24" rand = "0.8.4" getrandom = { version = "0.2.7", features = ["js"] } + +[dev-dependencies] +env_logger = "0.8.4" +dotenv = "0.15" diff --git a/webapp/src/bridge_engine.rs b/webapp/src/bridge_engine.rs new file mode 100644 index 0000000..48c0fd2 --- /dev/null +++ b/webapp/src/bridge_engine.rs @@ -0,0 +1,108 @@ +use crate::card; + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum Player { + North, + West, + South, + East, +} + +impl Player { + pub fn next(&self) -> Self { + match self { + Self::North => Self::West, + Self::West => Self::South, + Self::South => Self::East, + Self::East => Self::North, + } + } +} + +#[derive(PartialEq, Eq, Debug)] +pub struct Trick { + pub leader: Player, + pub cards_played: Vec, +} + +#[derive(PartialEq, Eq, Debug)] +pub struct Turn { + in_progress: Trick, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum PlayResult { + InProgress(Turn), + Trick(Trick), +} + +impl Turn { + pub fn new(p: Player) -> Turn { + Turn { + in_progress: Trick { + leader: p, + cards_played: Vec::with_capacity(4), + }, + } + } + + pub fn play(mut self: Turn, card: card::Card) -> PlayResult { + self.in_progress.cards_played.push(card); + if self.in_progress.cards_played.len() >= 4 { + return PlayResult::Trick(self.in_progress); + } + PlayResult::InProgress(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn next_player() { + let next_players = vec![Player::North, Player::West, Player::South, Player::East] + .iter() + .map(Player::next) + .collect::>(); + assert_eq!( + next_players, + vec![Player::West, Player::South, Player::East, Player::North] + ); + } + + fn as_turn(p: PlayResult) -> Turn { + if let PlayResult::InProgress(t) = p { + t + } else { + panic!("expected PlayResult::InProgress(): {:?}", p); + } + } + + fn as_trick(p: PlayResult) -> Trick { + if let PlayResult::Trick(t) = p { + t + } else { + panic!("expected PlayResult::Trick(): {:?}", p); + } + } + + #[test] + fn play_turn() { + let turn = Turn::new(Player::South); + let turn = as_turn(turn.play("♣4".parse().unwrap())); + let turn = as_turn(turn.play("♥A".parse().unwrap())); + let turn = as_turn(turn.play("♣4".parse().unwrap())); + let trick = as_trick(turn.play("♣A".parse().unwrap())); + assert_eq!( + trick, + Trick { + leader: Player::South, + cards_played: ["♣4", "♥A", "♣4", "♣A"] + .into_iter() + .map(|c| c.parse().unwrap()) + .collect() + } + ); + } +} diff --git a/webapp/src/card.rs b/webapp/src/card.rs index 4fda0a7..174676f 100644 --- a/webapp/src/card.rs +++ b/webapp/src/card.rs @@ -40,6 +40,33 @@ impl fmt::Display for Suit { } } +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + InvalidSuit(String), + InvalidRank(String), + MissingParts(String), +} + +impl std::str::FromStr for Suit { + type Err = ParseError; + + fn from_str(s: &str) -> std::result::Result { + match s.trim() { + "♣" => Ok(Suit::Club), + "C" => Ok(Suit::Club), + "♢" => Ok(Suit::Diamond), + "♦" => Ok(Suit::Diamond), + "D" => Ok(Suit::Diamond), + "♡" => Ok(Suit::Heart), + "♥" => Ok(Suit::Heart), + "H" => Ok(Suit::Heart), + "♠" => Ok(Suit::Spade), + "S" => Ok(Suit::Spade), + _ => Err(ParseError::InvalidSuit(s.to_string())), + } + } +} + impl fmt::Debug for Suit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!(f, "{}", self) @@ -72,6 +99,30 @@ impl fmt::Debug for Rank { } } +impl std::str::FromStr for Rank { + type Err = ParseError; + + fn from_str(s: &str) -> std::result::Result { + match s.trim().to_ascii_uppercase().as_str() { + "A" => Ok(Rank::Ace), + "K" => Ok(Rank::King), + "Q" => Ok(Rank::Queen), + "J" => Ok(Rank::Jack), + "10" => Ok(Rank::Ten), + "T" => Ok(Rank::Ten), + "9" => Ok(Rank::Nine), + "8" => Ok(Rank::Eight), + "7" => Ok(Rank::Seven), + "6" => Ok(Rank::Six), + "5" => Ok(Rank::Five), + "4" => Ok(Rank::Four), + "3" => Ok(Rank::Three), + "2" => Ok(Rank::Two), + _ => Err(ParseError::InvalidRank(s.to_string())), + } + } +} + #[derive(PartialEq, Eq, Clone, Copy)] pub struct Card(pub Suit, pub Rank); @@ -88,6 +139,22 @@ impl fmt::Debug for Card { } } +impl std::str::FromStr for Card { + type Err = ParseError; + + fn from_str(s: &str) -> std::result::Result { + let stripped = s.replace(" ", ""); + let mut chars = stripped.chars(); + let suit = chars + .next() + .ok_or(ParseError::MissingParts(s.to_string()))? + .to_string() + .parse()?; + let rank = chars.collect::().parse()?; + Ok(Card(suit, rank)) + } +} + fn make_deck() -> Vec { let mut result = vec![]; for suit in Suit::iter() { @@ -98,6 +165,25 @@ fn make_deck() -> Vec { result } +#[cfg(test)] +mod tests { + use super::*; + use log::info; + + #[test] + fn string_conversion() { + crate::tests::test_setup(); + info!("deck: {:?}", make_deck()); + assert_eq!( + make_deck(), + make_deck() + .iter() + .map(|card| format!("{}", card).parse().unwrap()) + .collect::>(), + ); + } +} + pub fn shuffle_deck(rng: &mut R) -> (Vec, Vec, Vec, Vec) where R: Rng, diff --git a/webapp/src/main.rs b/webapp/src/main.rs index a99ea94..633f560 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -3,6 +3,7 @@ use crate::card::{Rank, Suit}; use log::{debug, error, info, warn}; use yew::prelude::*; pub mod card; +pub mod bridge_engine; fn main() { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); @@ -118,3 +119,11 @@ impl From for CardProps { CardProps { suit, rank } } } + +#[cfg(test)] +mod tests { + pub fn test_setup() { + dotenv::dotenv().ok(); + let _ = env_logger::builder().is_test(true).try_init(); + } +} -- cgit v1.2.3