From 1f623cca3ea0937508b8c50f4c32a0972271e8f4 Mon Sep 17 00:00:00 2001 From: Kjetil Orbekk Date: Tue, 15 Nov 2022 15:06:07 -0500 Subject: Move shared bridge library code into `protocol` crate --- Cargo.lock | 11 +- protocol/Cargo.toml | 11 + protocol/src/bridge_engine.rs | 709 +++++++++++++++++++++++++++++++++ protocol/src/card.rs | 277 +++++++++++++ protocol/src/lib.rs | 10 + webapp/Cargo.toml | 4 +- webapp/src/bridge_engine.rs | 709 --------------------------------- webapp/src/card.rs | 277 ------------- webapp/src/components.rs | 2 +- webapp/src/components/bidding.rs | 2 +- webapp/src/components/bidding_box.rs | 2 +- webapp/src/components/bidding_table.rs | 2 +- webapp/src/components/card.rs | 2 +- webapp/src/components/game.rs | 8 +- webapp/src/components/hand.rs | 2 +- webapp/src/components/show_bid.rs | 2 +- webapp/src/components/trick_in_play.rs | 2 +- webapp/src/components/tricks_played.rs | 2 +- webapp/src/main.rs | 9 - 19 files changed, 1030 insertions(+), 1013 deletions(-) create mode 100644 protocol/src/bridge_engine.rs create mode 100644 protocol/src/card.rs delete mode 100644 webapp/src/bridge_engine.rs delete mode 100644 webapp/src/card.rs diff --git a/Cargo.lock b/Cargo.lock index b852fd1..fa97c01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1435,8 +1435,17 @@ dependencies = [ name = "protocol" version = "0.1.0" dependencies = [ + "anyhow", + "dotenv", + "env_logger", + "lazy_static", + "log", + "rand", + "regex", "serde", "serde_json", + "strum", + "strum_macros", "uuid", ] @@ -2566,8 +2575,6 @@ dependencies = [ "protocol", "rand", "regex", - "strum", - "strum_macros", "uuid", "wasm-bindgen-futures", "wasm-logger", 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 { + 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, +} + +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 { + 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, + in_progress: TurnInPlay, +} + +#[derive(Debug)] +pub enum DealInPlayResult { + InProgress(DealInPlay), + PlayFinished(Vec), +} + +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 { + &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 { + 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::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 { + 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::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, +} + +impl Raise { + pub fn all_raises() -> Vec { + 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 for Raise { + fn partial_cmp(&self, o: &Self) -> Option { + 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::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, +} + +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 { + 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 { + 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 { + // 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, 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 { + 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::>(); + 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 { + 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 { + 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 { + 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 { + 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::().parse()?; + Ok(Card(suit, rank)) + } +} + +fn make_deck() -> Vec { + 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::().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::>(), + ); + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Deal { + pub north: Vec, + pub west: Vec, + pub south: Vec, + pub east: Vec, +} + +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(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(); + } +} diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index fd048ab..fe1d53b 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -8,9 +8,6 @@ yew = "0.19" console_error_panic_hook = "0.1" wasm-logger = "0.2" log = "0.4" -strum = "0.24" -strum_macros = "0.24" -rand = "0.8.4" getrandom = { version = "0.2.7", features = ["js"] } wee_alloc = "0.4.3" anyhow = "1.0" @@ -22,6 +19,7 @@ protocol = { path = "../protocol" } yew-router = "0.16.0" web-sys = { version = "0.3.60", features = ["Location", "Document"] } uuid = { version = "1.2.1", features = ["serde"] } +rand = "0.8.4" [dev-dependencies] env_logger = "0.8.4" diff --git a/webapp/src/bridge_engine.rs b/webapp/src/bridge_engine.rs deleted file mode 100644 index 808045d..0000000 --- a/webapp/src/bridge_engine.rs +++ /dev/null @@ -1,709 +0,0 @@ -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 { - 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, -} - -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 { - 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, - in_progress: TurnInPlay, -} - -#[derive(Debug)] -pub enum DealInPlayResult { - InProgress(DealInPlay), - PlayFinished(Vec), -} - -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 { - &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 { - 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::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 { - 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::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, -} - -impl Raise { - pub fn all_raises() -> Vec { - 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 for Raise { - fn partial_cmp(&self, o: &Self) -> Option { - 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::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, -} - -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 { - 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 { - 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 { - // 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, 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 { - 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::>(); - 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 { - 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/webapp/src/card.rs b/webapp/src/card.rs deleted file mode 100644 index 621bae1..0000000 --- a/webapp/src/card.rs +++ /dev/null @@ -1,277 +0,0 @@ -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 { - 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 { - 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 { - 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::().parse()?; - Ok(Card(suit, rank)) - } -} - -fn make_deck() -> Vec { - 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::().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::>(), - ); - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Deal { - pub north: Vec, - pub west: Vec, - pub south: Vec, - pub east: Vec, -} - -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(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/webapp/src/components.rs b/webapp/src/components.rs index 018b1c3..aa3636f 100644 --- a/webapp/src/components.rs +++ b/webapp/src/components.rs @@ -1,4 +1,4 @@ -use crate::card::Suit; +use protocol::card::Suit; mod app_context_provider; mod bidding; diff --git a/webapp/src/components/bidding.rs b/webapp/src/components/bidding.rs index 2825cbd..2c48ca3 100644 --- a/webapp/src/components/bidding.rs +++ b/webapp/src/components/bidding.rs @@ -1,4 +1,4 @@ -use crate::bridge_engine::{self, BiddingResult, Contract, Player}; +use protocol::bridge_engine::{self, BiddingResult, Contract, Player}; use crate::components::{BiddingBox, BiddingTable}; use log::error; use yew::prelude::*; diff --git a/webapp/src/components/bidding_box.rs b/webapp/src/components/bidding_box.rs index 1d12369..0b384ec 100644 --- a/webapp/src/components/bidding_box.rs +++ b/webapp/src/components/bidding_box.rs @@ -1,4 +1,4 @@ -use crate::bridge_engine::{Bid, Raise}; +use protocol::bridge_engine::{Bid, Raise}; use crate::components::bid_css_class; use yew::prelude::*; diff --git a/webapp/src/components/bidding_table.rs b/webapp/src/components/bidding_table.rs index 8576ed6..0f1a824 100644 --- a/webapp/src/components/bidding_table.rs +++ b/webapp/src/components/bidding_table.rs @@ -1,4 +1,4 @@ -use crate::bridge_engine::{Bid, Bidding, Player}; +use protocol::bridge_engine::{Bid, Bidding, Player}; use crate::components::bid_css_class; use yew::prelude::*; diff --git a/webapp/src/components/card.rs b/webapp/src/components/card.rs index ff321e1..48d53e3 100644 --- a/webapp/src/components/card.rs +++ b/webapp/src/components/card.rs @@ -1,4 +1,4 @@ -use crate::card; +use protocol::card; use crate::components::suit_css_class; use yew::prelude::*; diff --git a/webapp/src/components/game.rs b/webapp/src/components/game.rs index 34e073c..7ade948 100644 --- a/webapp/src/components/game.rs +++ b/webapp/src/components/game.rs @@ -1,7 +1,7 @@ -use crate::bridge_engine::{self, Contract, DealInPlay, DealInPlayResult, Player}; -use crate::card; -use crate::card::Deal; -use crate::card::Suit; +use protocol::bridge_engine::{self, Contract, DealInPlay, DealInPlayResult, Player}; +use protocol::card; +use protocol::card::Deal; +use protocol::card::Suit; use crate::components::{Bidding, Hand, ShowBid, TrickInPlay, TricksPlayed}; use log::{error, info}; use yew::prelude::*; diff --git a/webapp/src/components/hand.rs b/webapp/src/components/hand.rs index 4a01508..7946a5c 100644 --- a/webapp/src/components/hand.rs +++ b/webapp/src/components/hand.rs @@ -1,4 +1,4 @@ -use crate::card; +use protocol::card; use crate::components::card::Card; use yew::prelude::*; diff --git a/webapp/src/components/show_bid.rs b/webapp/src/components/show_bid.rs index 5914710..81cc7aa 100644 --- a/webapp/src/components/show_bid.rs +++ b/webapp/src/components/show_bid.rs @@ -1,4 +1,4 @@ -use crate::bridge_engine::{Bidding, Contract}; +use protocol::bridge_engine::{Bidding, Contract}; use yew::prelude::*; #[derive(PartialEq, Properties, Clone)] diff --git a/webapp/src/components/trick_in_play.rs b/webapp/src/components/trick_in_play.rs index 720fec0..c68239e 100644 --- a/webapp/src/components/trick_in_play.rs +++ b/webapp/src/components/trick_in_play.rs @@ -1,4 +1,4 @@ -use crate::bridge_engine::TurnInPlay; +use protocol::bridge_engine::TurnInPlay; use crate::components::Card; use yew::prelude::*; diff --git a/webapp/src/components/tricks_played.rs b/webapp/src/components/tricks_played.rs index 97a7cd1..d8f5a04 100644 --- a/webapp/src/components/tricks_played.rs +++ b/webapp/src/components/tricks_played.rs @@ -1,4 +1,4 @@ -use crate::bridge_engine::Trick; +use protocol::bridge_engine::Trick; use yew::prelude::*; #[function_component(TricksPlayed)] diff --git a/webapp/src/main.rs b/webapp/src/main.rs index 35e69fd..25d12f1 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -4,8 +4,6 @@ use std::rc::Rc; use log::{debug, error, info, warn}; use yew::prelude::*; use yew_router::prelude::*; -pub mod bridge_engine; -pub mod card; pub mod components; use components::{AppContext, AppContextProvider, ErrorInfo, Game, Table}; use gloo_net::http::Request; @@ -100,10 +98,3 @@ fn switch(routes: &Route) -> Html { } } -#[cfg(test)] -mod tests { - pub fn test_setup() { - dotenv::dotenv().ok(); - let _ = env_logger::builder().is_test(true).try_init(); - } -} -- cgit v1.2.3