diff options
author | Kjetil Orbekk <kj@orbekk.com> | 2022-11-15 15:06:07 -0500 |
---|---|---|
committer | Kjetil Orbekk <kj@orbekk.com> | 2022-11-15 15:06:07 -0500 |
commit | 1f623cca3ea0937508b8c50f4c32a0972271e8f4 (patch) | |
tree | 3f5a36296ffc2afb1199cd65f9758c9667115450 /protocol | |
parent | 8fa1b37bb705371bf5dee574f1f136019d3db9d1 (diff) |
Move shared bridge library code into `protocol` crate
Diffstat (limited to 'protocol')
-rw-r--r-- | protocol/Cargo.toml | 11 | ||||
-rw-r--r-- | protocol/src/bridge_engine.rs | 709 | ||||
-rw-r--r-- | protocol/src/card.rs | 277 | ||||
-rw-r--r-- | protocol/src/lib.rs | 10 |
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(); + } +} |