summaryrefslogtreecommitdiff
path: root/protocol/src/bridge_engine.rs
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/src/bridge_engine.rs
parent8fa1b37bb705371bf5dee574f1f136019d3db9d1 (diff)
Move shared bridge library code into `protocol` crate
Diffstat (limited to 'protocol/src/bridge_engine.rs')
-rw-r--r--protocol/src/bridge_engine.rs709
1 files changed, 709 insertions, 0 deletions
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"),
+ })
+ );
+ }
+}