summaryrefslogtreecommitdiff
path: root/webapp
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-09-05 12:02:10 -0400
committerKjetil Orbekk <kj@orbekk.com>2022-09-05 12:02:10 -0400
commit19564fd7385266d6628376e743884e65e5a645e1 (patch)
tree7c66fa17fd03ddb7d7c3942c9545f5309fe32bf9 /webapp
parent42285171a2fb5caed62d1d1a5f45ea74223942f6 (diff)
Add Bid data structure
Diffstat (limited to 'webapp')
-rw-r--r--webapp/Cargo.toml2
-rw-r--r--webapp/src/bridge_engine.rs173
-rw-r--r--webapp/src/card.rs26
3 files changed, 185 insertions, 16 deletions
diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml
index 66fa3b1..d64ed32 100644
--- a/webapp/Cargo.toml
+++ b/webapp/Cargo.toml
@@ -14,6 +14,8 @@ rand = "0.8.4"
getrandom = { version = "0.2.7", features = ["js"] }
wee_alloc = "0.4.3"
anyhow = "1.0"
+regex = "1.0"
+lazy_static = "1.4"
[dev-dependencies]
env_logger = "0.8.4"
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()?;