diff options
author | Kjetil Orbekk <kj@orbekk.com> | 2022-09-05 12:02:10 -0400 |
---|---|---|
committer | Kjetil Orbekk <kj@orbekk.com> | 2022-09-05 12:02:10 -0400 |
commit | 19564fd7385266d6628376e743884e65e5a645e1 (patch) | |
tree | 7c66fa17fd03ddb7d7c3942c9545f5309fe32bf9 /webapp/src | |
parent | 42285171a2fb5caed62d1d1a5f45ea74223942f6 (diff) |
Add Bid data structure
Diffstat (limited to 'webapp/src')
-rw-r--r-- | webapp/src/bridge_engine.rs | 173 | ||||
-rw-r--r-- | webapp/src/card.rs | 26 |
2 files changed, 183 insertions, 16 deletions
diff --git a/webapp/src/bridge_engine.rs b/webapp/src/bridge_engine.rs index 873a2f0..70fd828 100644 --- a/webapp/src/bridge_engine.rs +++ b/webapp/src/bridge_engine.rs @@ -1,4 +1,12 @@ use crate::card::Card; +use crate::card::Suit; +use anyhow::anyhow; +use log::debug; +use regex::Regex; +use std::fmt; +use std::str::FromStr; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum Player { @@ -55,9 +63,174 @@ impl Turn { } } +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, EnumIter)] +pub enum ContractLevel { + One = 1, + Two, + Three, + Four, + Five, + Six, + Seven, +} + +impl fmt::Display for ContractLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{}", *self as u8) + } +} + +impl fmt::Debug for ContractLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{}", self) + } +} + +impl FromStr for ContractLevel { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { + match s { + "1" => Ok(ContractLevel::One), + "2" => Ok(ContractLevel::Two), + "3" => Ok(ContractLevel::Three), + "4" => Ok(ContractLevel::Four), + "5" => Ok(ContractLevel::Five), + "6" => Ok(ContractLevel::Six), + "7" => Ok(ContractLevel::Seven), + _ => Err(anyhow!("invalid string: {}", s)), + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub struct Bid { + level: ContractLevel, + suit: Option<Suit>, +} + +impl Bid { + pub fn all_bids() -> Vec<Bid> { + let mut result = Vec::with_capacity(7 * 5); + for level in ContractLevel::iter() { + for suit in Suit::iter() { + result.push(Bid { + level, + suit: Some(suit), + }); + } + result.push(Bid { level, suit: None }); + } + result + } +} + +impl PartialOrd<Self> for Bid { + fn partial_cmp(&self, o: &Self) -> std::option::Option<std::cmp::Ordering> { + if self.level != o.level { + return self.level.partial_cmp(&o.level); + } + if self.suit != o.suit { + if self.suit == None { + return Some(std::cmp::Ordering::Greater); + } + if o.suit == None { + return Some(std::cmp::Ordering::Less); + } + return self.suit.partial_cmp(&o.suit); + } + return Some(std::cmp::Ordering::Equal); + } +} + +impl Ord for Bid { + fn cmp(&self, o: &Self) -> std::cmp::Ordering { + self.partial_cmp(o).unwrap() + } +} + +impl fmt::Display for Bid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!( + f, + "{}{}", + self.level, + self.suit + .map_or("NT".to_string(), |suit| format!("{}", suit)) + ) + } +} + +impl fmt::Debug for Bid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{}", self) + } +} + +impl FromStr for Bid { + type Err = anyhow::Error; + fn from_str(s: &str) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { + lazy_static::lazy_static! { + static ref RE: Regex = Regex::new(r#"\s*(.[0-9]*)\s*(.*)"#).unwrap(); + }; + let caps = RE.captures(s).ok_or(anyhow!("invalid bid: {}", s))?; + debug!("caps: {:?}", caps); + let level = caps[1].parse()?; + let suit = match &caps[2] { + "NT" => None, + x => Some(x.parse()?), + }; + Ok(Bid { level, suit }) + } +} + #[cfg(test)] mod tests { use super::*; + use log::info; + + #[test] + fn bid_conversion() { + crate::tests::test_setup(); + let bid1d = Bid { + level: ContractLevel::One, + suit: Some(Suit::Diamond), + }; + assert_eq!("1♢", format!("{}", bid1d)); + assert_eq!("1♢", format!("{:?}", bid1d)); + assert_eq!(bid1d, Bid::from_str("1D").unwrap()); + + let mut checked_bids = 0; + for bid in Bid::all_bids() { + assert_eq!(bid, Bid::from_str(format!("{}", bid).as_str()).unwrap()); + checked_bids += 1; + } + assert_eq!(checked_bids, 35); + } + + #[test] + fn bid_ord() { + let bid = |s| Bid::from_str(s).unwrap(); + assert!(bid("2♦") < bid("3♦")); + assert!(bid("3♦") < bid("3♥")); + assert!(bid("1♠") < bid("2♣")); + assert!(bid("1♠") < bid("1NT")); + for bid in Bid::all_bids() { + assert_eq!(bid, bid); + } + } + + #[test] + fn contract_level_conversion() { + crate::tests::test_setup(); + assert_eq!("2", format!("{}", ContractLevel::Two)); + assert_eq!("3", format!("{:?}", ContractLevel::Three)); + + let result = ContractLevel::from_str("8"); + info!("{:?}", result); + assert!(result.unwrap_err().to_string().contains("invalid")); + assert_eq!(ContractLevel::Seven, "7".parse().unwrap()); + } #[test] fn next_player() { diff --git a/webapp/src/card.rs b/webapp/src/card.rs index 3b2fc24..248b9b5 100644 --- a/webapp/src/card.rs +++ b/webapp/src/card.rs @@ -3,6 +3,7 @@ use rand::Rng; use std::fmt; use strum::IntoEnumIterator; use strum_macros::EnumIter; +use anyhow::anyhow; #[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, EnumIter)] pub enum Suit { @@ -40,17 +41,10 @@ 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; + type Err = anyhow::Error; - fn from_str(s: &str) -> std::result::Result<Self, ParseError> { + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { match s.trim() { "♣" => Ok(Suit::Club), "C" => Ok(Suit::Club), @@ -62,7 +56,7 @@ impl std::str::FromStr for Suit { "H" => Ok(Suit::Heart), "♠" => Ok(Suit::Spade), "S" => Ok(Suit::Spade), - _ => Err(ParseError::InvalidSuit(s.to_string())), + _ => Err(anyhow!("invalid suit: {}", s)), } } } @@ -100,9 +94,9 @@ impl fmt::Debug for Rank { } impl std::str::FromStr for Rank { - type Err = ParseError; + type Err = anyhow::Error; - fn from_str(s: &str) -> std::result::Result<Self, ParseError> { + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { match s.trim().to_ascii_uppercase().as_str() { "A" => Ok(Rank::Ace), "K" => Ok(Rank::King), @@ -118,7 +112,7 @@ impl std::str::FromStr for Rank { "4" => Ok(Rank::Four), "3" => Ok(Rank::Three), "2" => Ok(Rank::Two), - _ => Err(ParseError::InvalidRank(s.to_string())), + _ => Err(anyhow!("invalid rank: {}", s)), } } } @@ -140,14 +134,14 @@ impl fmt::Debug for Card { } impl std::str::FromStr for Card { - type Err = ParseError; + type Err = anyhow::Error; - fn from_str(s: &str) -> std::result::Result<Self, ParseError> { + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { let stripped = s.replace(" ", ""); let mut chars = stripped.chars(); let suit = chars .next() - .ok_or(ParseError::MissingParts(s.to_string()))? + .ok_or(anyhow!("missing parts: {}", s))? .to_string() .parse()?; let rank = chars.collect::<String>().parse()?; |