summaryrefslogtreecommitdiff
path: root/protocol
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-11-15 15:06:07 -0500
committerKjetil Orbekk <kj@orbekk.com>2022-11-15 15:06:07 -0500
commit1f623cca3ea0937508b8c50f4c32a0972271e8f4 (patch)
tree3f5a36296ffc2afb1199cd65f9758c9667115450 /protocol
parent8fa1b37bb705371bf5dee574f1f136019d3db9d1 (diff)
Move shared bridge library code into `protocol` crate
Diffstat (limited to 'protocol')
-rw-r--r--protocol/Cargo.toml11
-rw-r--r--protocol/src/bridge_engine.rs709
-rw-r--r--protocol/src/card.rs277
-rw-r--r--protocol/src/lib.rs10
4 files changed, 1007 insertions, 0 deletions
diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml
index b217d93..473e0e3 100644
--- a/protocol/Cargo.toml
+++ b/protocol/Cargo.toml
@@ -9,3 +9,14 @@ edition = "2021"
serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.0.85"
uuid = { version = "1.2.0", features = ["serde", "wasm-bindgen", "v4", "fast-rng"] }
+rand = "0.8.4"
+anyhow = "1.0"
+strum = "0.24"
+strum_macros = "0.24"
+log = "0.4"
+regex = "1.0"
+lazy_static = "1.4"
+
+[dev-dependencies]
+env_logger = "0.8.4"
+dotenv = "0.15"
diff --git a/protocol/src/bridge_engine.rs b/protocol/src/bridge_engine.rs
new file mode 100644
index 0000000..808045d
--- /dev/null
+++ b/protocol/src/bridge_engine.rs
@@ -0,0 +1,709 @@
+use crate::card::{Card, Deal, Suit};
+use anyhow::{anyhow, bail};
+use log::{debug, error, info};
+use regex::Regex;
+use std::cmp::Ordering;
+use std::fmt;
+use std::str::FromStr;
+use strum::{EnumCount, IntoEnumIterator};
+use strum_macros::{EnumCount as EnumCountMacro, EnumIter, FromRepr};
+
+#[derive(PartialEq, Eq, Clone, Copy, Debug, FromRepr, EnumCountMacro)]
+#[repr(u8)]
+pub enum Player {
+ West = 0,
+ North,
+ East,
+ South,
+}
+
+impl Player {
+ pub fn next(&self) -> Self {
+ self.many_next(1)
+ }
+
+ pub fn many_next(self, i: usize) -> Self {
+ Player::from_repr(((self as usize + i) % Player::COUNT) as u8).unwrap()
+ }
+
+ pub fn short_str(&self) -> &str {
+ match self {
+ Self::West => "W",
+ Self::North => "N",
+ Self::East => "E",
+ Self::South => "W",
+ }
+ }
+
+ pub fn get_cards<'a>(&self, deal: &'a mut Deal) -> &'a mut Vec<Card> {
+ match self {
+ Self::West => &mut deal.west,
+ Self::North => &mut deal.north,
+ Self::East => &mut deal.east,
+ Self::South => &mut deal.south,
+ }
+ }
+}
+
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct Trick {
+ pub leader: Player,
+ pub cards_played: Vec<Card>,
+}
+
+impl Trick {
+ pub fn winner(&self) -> Player {
+ error!("XXX: Returning incorrect result for winner");
+ self.leader
+ }
+}
+
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct TurnInPlay {
+ trick: Trick,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum TurnInPlayResult {
+ InProgress(TurnInPlay),
+ Trick(Trick),
+}
+
+impl TurnInPlay {
+ pub fn new(p: Player) -> TurnInPlay {
+ TurnInPlay {
+ trick: Trick {
+ leader: p,
+ cards_played: Vec::with_capacity(4),
+ },
+ }
+ }
+
+ pub fn suit(&self) -> Option<Suit> {
+ self.trick
+ .cards_played
+ .iter()
+ .next()
+ .map(|&Card(suit, _)| suit)
+ }
+
+ pub fn leader(&self) -> Player {
+ self.trick.leader
+ }
+
+ pub fn cards_played(&self) -> &[Card] {
+ &self.trick.cards_played[..]
+ }
+
+ pub fn play(mut self: TurnInPlay, card: Card) -> TurnInPlayResult {
+ self.trick.cards_played.push(card);
+ if self.trick.cards_played.len() >= 4 {
+ return TurnInPlayResult::Trick(self.trick);
+ }
+ TurnInPlayResult::InProgress(self)
+ }
+
+ pub fn next_player(&self) -> Player {
+ self.trick.leader.many_next(self.trick.cards_played.len())
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct DealInPlay {
+ deal: Deal,
+ tricks_played: Vec<Trick>,
+ in_progress: TurnInPlay,
+}
+
+#[derive(Debug)]
+pub enum DealInPlayResult {
+ InProgress(DealInPlay),
+ PlayFinished(Vec<Trick>),
+}
+
+impl DealInPlay {
+ pub fn new(leader: Player, deal: Deal) -> DealInPlay {
+ DealInPlay {
+ deal,
+ tricks_played: Vec::with_capacity(13),
+ in_progress: TurnInPlay::new(leader),
+ }
+ }
+
+ pub fn tricks(&self) -> &Vec<Trick> {
+ &self.tricks_played
+ }
+
+ pub fn trick_in_play(&self) -> &TurnInPlay {
+ &self.in_progress
+ }
+
+ pub fn deal(&self) -> &Deal {
+ &self.deal
+ }
+
+ pub fn play(mut self: Self, card: Card) -> Result<DealInPlayResult, anyhow::Error> {
+ let player = self.in_progress.next_player();
+ let player_cards = player.get_cards(&mut self.deal);
+
+ info!(
+ "Next player is {:?}, playing card {} from {:?}",
+ player, card, player_cards
+ );
+ let i = player_cards.iter().position(|&c| c == card).ok_or(anyhow!(
+ "{:?} does not have {}",
+ player,
+ card
+ ))?;
+ player_cards.remove(i);
+
+ Ok(match self.in_progress.play(card) {
+ TurnInPlayResult::InProgress(turn) => DealInPlayResult::InProgress(Self {
+ in_progress: turn,
+ ..self
+ }),
+ TurnInPlayResult::Trick(trick) => DealInPlayResult::InProgress(Self {
+ in_progress: TurnInPlay::new(trick.winner()),
+ tricks_played: {
+ let mut tricks = self.tricks_played;
+ tricks.push(trick);
+ tricks
+ },
+ deal: self.deal,
+ }),
+ })
+ }
+}
+
+#[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 enum Bid {
+ Pass,
+ Double,
+ Redouble,
+ Raise(Raise),
+}
+
+impl Bid {
+ pub fn as_raise(&self) -> Option<Raise> {
+ match self {
+ Bid::Raise(raise) => Some(*raise),
+ _ => None,
+ }
+ }
+}
+
+impl fmt::Display for Bid {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ match self {
+ Bid::Pass => write!(f, "Pass"),
+ Bid::Double => write!(f, "Double"),
+ Bid::Redouble => write!(f, "Redouble"),
+ Bid::Raise(x) => write!(f, "{}", x),
+ }
+ }
+}
+
+impl fmt::Debug for Bid {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ match self {
+ Bid::Pass => write!(f, "Pass"),
+ Bid::Double => write!(f, "Double"),
+ Bid::Redouble => write!(f, "Redouble"),
+ Bid::Raise(x) => write!(f, "Raise({})", x),
+ }
+ }
+}
+
+impl FromStr for Bid {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> {
+ match s.trim().to_ascii_lowercase().as_str() {
+ "pass" => Ok(Bid::Pass),
+ "double" => Ok(Bid::Double),
+ "redouble" => Ok(Bid::Redouble),
+ x => Ok(Bid::Raise(x.parse()?)),
+ }
+ }
+}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub struct Raise {
+ pub level: ContractLevel,
+ pub suit: Option<Suit>,
+}
+
+impl Raise {
+ pub fn all_raises() -> Vec<Raise> {
+ let mut result = Vec::with_capacity(7 * 5);
+ for level in ContractLevel::iter() {
+ for suit in Suit::iter() {
+ result.push(Raise {
+ level,
+ suit: Some(suit),
+ });
+ }
+ result.push(Raise { level, suit: None });
+ }
+ result
+ }
+}
+
+impl PartialOrd<Self> for Raise {
+ fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
+ if self.level != o.level {
+ return self.level.partial_cmp(&o.level);
+ }
+ if self.suit != o.suit {
+ if self.suit == None {
+ return Some(Ordering::Greater);
+ }
+ if o.suit == None {
+ return Some(Ordering::Less);
+ }
+ return self.suit.partial_cmp(&o.suit);
+ }
+ return Some(Ordering::Equal);
+ }
+}
+
+impl Ord for Raise {
+ fn cmp(&self, o: &Self) -> std::cmp::Ordering {
+ self.partial_cmp(o).unwrap()
+ }
+}
+
+impl fmt::Display for Raise {
+ 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 Raise {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ write!(f, "{}", self)
+ }
+}
+
+impl FromStr for Raise {
+ 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 raise: {}", s))?;
+ info!("caps: {:?}", caps);
+ let level = caps[1].parse()?;
+ let suit = match caps[2].to_ascii_uppercase().as_str() {
+ "NT" => None,
+ x => Some(x.parse()?),
+ };
+ Ok(Raise { level, suit })
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub enum ContractModifier {
+ None,
+ Doubled,
+ Redoubled,
+}
+
+impl fmt::Display for ContractModifier {
+ fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
+ match self {
+ ContractModifier::None => Ok(()),
+ ContractModifier::Doubled => write!(f, "x"),
+ ContractModifier::Redoubled => write!(f, "xx"),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct Contract {
+ declarer: Player,
+ highest_bid: Raise,
+ modifier: ContractModifier,
+}
+
+impl fmt::Display for Contract {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
+ write!(
+ f,
+ "{}{}{}",
+ self.highest_bid,
+ self.declarer.short_str(),
+ self.modifier
+ )
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Bidding {
+ pub dealer: Player,
+ pub bids: Vec<Bid>,
+}
+
+impl Bidding {
+ pub fn new(dealer: Player) -> Bidding {
+ Bidding {
+ dealer,
+ bids: vec![],
+ }
+ }
+
+ fn declarer(&self) -> Player {
+ // Bids are: [..., winning bid, pass, pass, pass].
+ self.dealer.many_next(self.bids.len() - 4)
+ }
+
+ pub fn highest_bid(&self) -> Option<Raise> {
+ for bid in self.bids.iter().rev() {
+ if let Some(raise) = bid.as_raise() {
+ return Some(raise);
+ }
+ }
+ None
+ }
+
+ fn passed_out(&self) -> bool {
+ let mut passes = 0;
+ for b in self.bids.iter().rev().take(3) {
+ if b == &Bid::Pass {
+ passes += 1
+ }
+ }
+ passes == 3
+ }
+
+ fn contract(&self) -> Option<Contract> {
+ match self.highest_bid() {
+ None => None,
+ Some(highest_bid) => Some(Contract {
+ declarer: self.declarer(),
+ highest_bid,
+ modifier: ContractModifier::None,
+ }),
+ }
+ }
+
+ pub fn bid(mut self, bid: Bid) -> Result<BiddingResult, anyhow::Error> {
+ // TODO: Need logic for double and redouble here.
+ if bid.as_raise().is_some() && bid.as_raise() <= self.highest_bid() {
+ bail!(
+ "bid too low: {:?} <= {:?}",
+ bid.as_raise(),
+ self.highest_bid()
+ );
+ }
+ self.bids.push(bid);
+ if self.passed_out() {
+ Ok(BiddingResult::Contract(self.contract(), self))
+ } else {
+ Ok(BiddingResult::InProgress(self))
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum BiddingResult {
+ InProgress(Bidding),
+ Contract(Option<Contract>, Bidding),
+}
+
+impl BiddingResult {
+ pub fn new(dealer: Player) -> Self {
+ BiddingResult::InProgress(Bidding::new(dealer))
+ }
+
+ pub fn bidding(&self) -> &Bidding {
+ match self {
+ BiddingResult::InProgress(bidding) => bidding,
+ BiddingResult::Contract(_, bidding) => bidding,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use log::info;
+
+ fn as_bidding(r: BiddingResult) -> Bidding {
+ match r {
+ BiddingResult::InProgress(bidding) => bidding,
+ _ => panic!("expected BiddingResult::InProgress(): {:?}", r),
+ }
+ }
+
+ fn as_contract(r: BiddingResult) -> Option<Contract> {
+ match r {
+ BiddingResult::Contract(contract, _) => contract,
+ _ => panic!("expected BiddingResult::Contract(): {:?}", r),
+ }
+ }
+
+ #[test]
+ fn bidding() {
+ crate::tests::test_setup();
+ let bidding = Bidding::new(Player::South);
+ let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap());
+ let bidding = as_bidding(bidding.bid(Bid::Raise("1♦".parse().unwrap())).unwrap());
+ let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap());
+ let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap());
+ let contract = as_contract(bidding.bid(Bid::Pass).unwrap());
+ assert_eq!(
+ Some(Contract {
+ declarer: Player::West,
+ highest_bid: "1♦".parse().unwrap(),
+ modifier: ContractModifier::None
+ }),
+ contract
+ );
+ }
+
+ #[test]
+ fn bid_conversion() {
+ crate::tests::test_setup();
+ let bid1d = Raise {
+ level: ContractLevel::One,
+ suit: Some(Suit::Diamond),
+ };
+ assert_eq!("1♢", format!("{}", bid1d));
+ assert_eq!("1♢", format!("{:?}", bid1d));
+ assert_eq!(bid1d, Raise::from_str("1D").unwrap());
+
+ assert_eq!(Bid::Pass, Bid::from_str("pass").unwrap());
+
+ let mut checked_raises = 0;
+ for bid in Raise::all_raises() {
+ assert_eq!(bid, Raise::from_str(format!("{}", bid).as_str()).unwrap());
+ assert_eq!(
+ Bid::Raise(bid),
+ Bid::from_str(format!("{}", bid).as_str()).unwrap()
+ );
+ checked_raises += 1;
+ }
+ assert_eq!(checked_raises, 35);
+ }
+
+ #[test]
+ fn fmt_contract() {
+ assert_eq!(
+ format!(
+ "{}",
+ Contract {
+ declarer: Player::West,
+ highest_bid: "1♥".parse().unwrap(),
+ modifier: ContractModifier::None
+ }
+ ),
+ "1♡W"
+ );
+
+ assert_eq!(
+ format!(
+ "{}",
+ Contract {
+ declarer: Player::East,
+ highest_bid: "1♥".parse().unwrap(),
+ modifier: ContractModifier::Doubled
+ }
+ ),
+ "1♡Ex"
+ );
+ }
+
+ #[test]
+ fn bid_ord() {
+ let bid = |s| Raise::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 Raise::all_raises() {
+ 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() {
+ let next_players = vec![Player::North, Player::East, Player::South, Player::West]
+ .iter()
+ .map(Player::next)
+ .collect::<Vec<_>>();
+ assert_eq!(
+ next_players,
+ vec![Player::East, Player::South, Player::West, Player::North]
+ );
+ }
+
+ #[test]
+ fn many_next_player() {
+ assert_eq!(Player::South, Player::South.many_next(4 * 1234567890));
+ }
+
+ fn as_turn(p: TurnInPlayResult) -> TurnInPlay {
+ if let TurnInPlayResult::InProgress(t) = p {
+ t
+ } else {
+ panic!("expected PlayResult::InProgress(): {:?}", p);
+ }
+ }
+
+ fn as_trick(p: TurnInPlayResult) -> Trick {
+ if let TurnInPlayResult::Trick(t) = p {
+ t
+ } else {
+ panic!("expected PlayResult::Trick(): {:?}", p);
+ }
+ }
+
+ #[test]
+ fn play_turn() {
+ let turn = TurnInPlay::new(Player::South);
+ assert_eq!(turn.next_player(), Player::South);
+ let turn = as_turn(turn.play("♣4".parse().unwrap()));
+ assert_eq!(turn.next_player(), Player::West);
+ let turn = as_turn(turn.play("♥A".parse().unwrap()));
+ assert_eq!(turn.next_player(), Player::North);
+ let turn = as_turn(turn.play("♣4".parse().unwrap()));
+ assert_eq!(turn.next_player(), Player::East);
+ 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()
+ }
+ );
+ }
+
+ #[test]
+ fn lead_suit() {
+ let turn = TurnInPlay::new(Player::South);
+ assert_eq!(turn.suit(), None);
+ let turn = as_turn(turn.play("♣4".parse().unwrap()));
+ assert_eq!(turn.suit(), Some("♣".parse().unwrap()));
+ }
+
+ fn mkcard(s: &str) -> Card {
+ Card::from_str(s).unwrap()
+ }
+
+ fn mkcards(s: &str) -> Vec<Card> {
+ s.split(" ").map(mkcard).collect()
+ }
+
+ fn _example_deal() -> Deal {
+ Deal {
+ west: mkcards("♠5 ♢10 ♡K ♣4 ♡J ♣5 ♢5 ♠9 ♢3 ♠2 ♣2 ♡4 ♠Q"),
+ north: mkcards("♢Q ♡9 ♠7 ♠8 ♠A ♡A ♡5 ♠6 ♢9 ♣3 ♡3 ♣9 ♢J"),
+ east: mkcards("♣10 ♡7 ♢A ♣6 ♡8 ♣Q ♠K ♡10 ♣K ♠3 ♡Q ♣J ♢4"),
+ south: mkcards("♢K ♡6 ♣8 ♢6 ♢7 ♢8 ♣A ♡2 ♣7 ♠10 ♠4 ♠J ♢2"),
+ }
+ }
+
+ fn mini_deal() -> Deal {
+ Deal {
+ west: mkcards("♢A ♡Q"),
+ north: mkcards("♢Q ♡9"),
+ east: mkcards("♢7 ♡K"),
+ south: mkcards("♢9 ♠9"),
+ }
+ }
+
+ fn as_playing_hand(result: DealInPlayResult) -> DealInPlay {
+ match result {
+ DealInPlayResult::InProgress(r) => r,
+ DealInPlayResult::PlayFinished(_) => {
+ panic!("expected PlayingDealResult::InProgress(): {:?}", result)
+ }
+ }
+ }
+
+ #[test]
+ fn play_hand() {
+ let deal = DealInPlay::new(Player::West, mini_deal());
+ assert_eq!(deal.tricks_played, vec!());
+ {
+ let err = deal.clone().play(mkcard("♥9")).unwrap_err().to_string();
+ assert_eq!(err, "West does not have ♡9");
+ }
+
+ let deal = as_playing_hand(deal.play(mkcard("♢A")).unwrap());
+ assert_eq!(deal.in_progress.trick.cards_played, vec!(mkcard("♢A")));
+
+ let deal = as_playing_hand(deal.play(mkcard("♢Q")).unwrap());
+ let deal = as_playing_hand(deal.play(mkcard("♥K")).unwrap());
+ let deal = as_playing_hand(deal.play(mkcard("♢9")).unwrap());
+ assert_eq!(deal.in_progress.trick.cards_played, []);
+ assert_eq!(
+ deal.tricks_played,
+ vec!(Trick {
+ leader: Player::West,
+ cards_played: mkcards("♢A ♢Q ♡K ♢9"),
+ })
+ );
+ }
+}
diff --git a/protocol/src/card.rs b/protocol/src/card.rs
new file mode 100644
index 0000000..621bae1
--- /dev/null
+++ b/protocol/src/card.rs
@@ -0,0 +1,277 @@
+use anyhow::anyhow;
+use rand::prelude::SliceRandom;
+use rand::Rng;
+use std::fmt;
+use strum::EnumCount;
+use strum::IntoEnumIterator;
+use strum_macros::EnumCount;
+use strum_macros::EnumIter;
+
+#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, EnumIter, EnumCount)]
+pub enum Suit {
+ Club,
+ Diamond,
+ Heart,
+ Spade,
+}
+
+#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, EnumIter)]
+pub enum Rank {
+ Two = 2,
+ Three,
+ Four,
+ Five,
+ Six,
+ Seven,
+ Eight,
+ Nine,
+ Ten,
+ Jack,
+ Queen,
+ King,
+ Ace,
+}
+
+impl fmt::Display for Suit {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ f.write_str(match self {
+ Suit::Club => "♣",
+ Suit::Diamond => "♢",
+ Suit::Heart => "♡",
+ Suit::Spade => "♠",
+ })
+ }
+}
+
+impl std::str::FromStr for Suit {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+ 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(anyhow!("invalid suit: {}", s)),
+ }
+ }
+}
+
+impl fmt::Debug for Suit {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ write!(f, "{}", self)
+ }
+}
+
+impl fmt::Display for Rank {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ f.write_str(match self {
+ Rank::Ace => "A",
+ Rank::King => "K",
+ Rank::Queen => "Q",
+ Rank::Jack => "J",
+ Rank::Ten => "10",
+ Rank::Nine => "9",
+ Rank::Eight => "8",
+ Rank::Seven => "7",
+ Rank::Six => "6",
+ Rank::Five => "5",
+ Rank::Four => "4",
+ Rank::Three => "3",
+ Rank::Two => "2",
+ })
+ }
+}
+
+impl fmt::Debug for Rank {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ write!(f, "{}", self)
+ }
+}
+
+impl std::str::FromStr for Rank {
+ type Err = anyhow::Error;
+
+ 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),
+ "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(anyhow!("invalid rank: {}", s)),
+ }
+ }
+}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub struct Card(pub Suit, pub Rank);
+
+impl fmt::Display for Card {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ let Card(suit, rank) = self;
+ write!(f, "{}{}", suit, rank)
+ }
+}
+
+impl fmt::Debug for Card {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ write!(f, "{}", self)
+ }
+}
+
+impl std::str::FromStr for Card {
+ type Err = anyhow::Error;
+
+ 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(anyhow!("missing parts: {}", s))?
+ .to_string()
+ .parse()?;
+ let rank = chars.collect::<String>().parse()?;
+ Ok(Card(suit, rank))
+ }
+}
+
+fn make_deck() -> Vec<Card> {
+ let mut result = vec![];
+ for suit in Suit::iter() {
+ for rank in Rank::iter() {
+ result.push(Card(suit, rank));
+ }
+ }
+ result
+}
+
+#[derive(Default, PartialEq, Eq, Copy, Clone)]
+pub enum RankOrder {
+ #[default]
+ Descending,
+ Ascending,
+}
+
+pub fn sort_cards(suits: &[Suit; 4], ord: RankOrder, cards: &mut [Card]) {
+ let mut score: [u8; Suit::COUNT] = [0; Suit::COUNT];
+ for (i, suit) in suits.iter().enumerate() {
+ score[*suit as usize] = i as u8;
+ }
+ cards.sort_by(|&Card(s1, r1), &Card(s2, r2)| {
+ let order = {
+ if s1 == s2 {
+ r1.cmp(&r2)
+ } else {
+ score[s1 as usize].cmp(&score[s2 as usize])
+ }
+ };
+ if ord == RankOrder::Descending {
+ order.reverse()
+ } else {
+ order
+ }
+ });
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use log::info;
+
+ #[test]
+ fn sorting_cards() {
+ let card = |s: &str| s.parse::<Card>().unwrap();
+ assert_eq!([card("♥2"), card("♥3"), card("♥4"),], {
+ let mut cards = [card("♥2"), card("♥4"), card("♥3")];
+ sort_cards(
+ &[Suit::Heart, Suit::Spade, Suit::Club, Suit::Diamond],
+ RankOrder::Ascending,
+ &mut cards,
+ );
+ cards
+ });
+ assert_eq!([card("♥A"), card("♥3"), card("♥2"),], {
+ let mut cards = [card("♥2"), card("♥A"), card("♥3")];
+ sort_cards(
+ &[Suit::Heart, Suit::Spade, Suit::Club, Suit::Diamond],
+ RankOrder::Descending,
+ &mut cards,
+ );
+ cards
+ });
+ assert_eq!([card("♠A"), card("♥A"), card("♣A"), card("♦A"),], {
+ let mut cards = [card("♣A"), card("♠A"), card("♥A"), card("♦A")];
+ sort_cards(
+ &[Suit::Diamond, Suit::Club, Suit::Heart, Suit::Spade],
+ RankOrder::Descending,
+ &mut cards,
+ );
+ cards
+ });
+ }
+
+ #[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::<Vec<Card>>(),
+ );
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Deal {
+ pub north: Vec<Card>,
+ pub west: Vec<Card>,
+ pub south: Vec<Card>,
+ pub east: Vec<Card>,
+}
+
+impl Deal {
+ pub fn sort(&mut self, suits: &[Suit; 4], ord: RankOrder) {
+ sort_cards(suits, ord, self.north.as_mut_slice());
+ sort_cards(suits, ord, self.west.as_mut_slice());
+ sort_cards(suits, ord, self.south.as_mut_slice());
+ sort_cards(suits, ord, self.east.as_mut_slice());
+ }
+}
+
+pub fn deal<R>(rng: &mut R) -> Deal
+where
+ R: Rng,
+{
+ let mut deck = make_deck();
+ deck.shuffle(rng);
+ let mut deck = deck.iter();
+ let north = deck.by_ref().take(13).cloned().collect();
+ let west = deck.by_ref().take(13).cloned().collect();
+ let south = deck.by_ref().take(13).cloned().collect();
+ let east = deck.by_ref().take(13).cloned().collect();
+ Deal {
+ north,
+ west,
+ south,
+ east,
+ }
+}
diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs
index b10e7e1..41268a9 100644
--- a/protocol/src/lib.rs
+++ b/protocol/src/lib.rs
@@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
+pub mod card;
+pub mod bridge_engine;
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct UserInfo {
@@ -16,3 +18,11 @@ pub struct Table {
pub struct TableView {
pub m: String,
}
+
+#[cfg(test)]
+mod tests {
+ pub fn test_setup() {
+ dotenv::dotenv().ok();
+ let _ = env_logger::builder().is_test(true).try_init();
+ }
+}