Skip to main content

dds_bridge/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4pub mod contract;
5pub mod deal;
6pub mod hand;
7pub mod seat;
8pub mod solver;
9
10pub use contract::{Bid, Contract, Level, Penalty};
11pub use deal::{Builder, FullDeal, PartialDeal};
12pub use hand::{Card, Hand, Holding, Rank};
13pub use seat::{Seat, SeatFlags};
14pub use solver::Solver;
15
16use core::fmt::{self, Write as _};
17use core::str::FromStr;
18use thiserror::Error;
19
20/// Denomination, a suit or notrump
21///
22/// We choose this representation over `Option<Suit>` because we are not sure if
23/// the latter can be optimized to a single byte.
24///
25/// The order of the suits deviates from [`dds`][dds], but this order provides
26/// natural ordering by deriving [`PartialOrd`] and [`Ord`].
27///
28/// [dds]: https://github.com/dds-bridge/dds
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[repr(u8)]
32pub enum Strain {
33    /// ♣
34    Clubs,
35    /// ♦
36    Diamonds,
37    /// ♥
38    Hearts,
39    /// ♠
40    Spades,
41    /// NT, the strain not proposing a trump suit
42    Notrump,
43}
44
45impl Strain {
46    /// Whether this strain is a minor suit (clubs or diamonds)
47    #[must_use]
48    #[inline]
49    pub const fn is_minor(self) -> bool {
50        matches!(self, Self::Clubs | Self::Diamonds)
51    }
52
53    /// Whether this strain is a major suit (hearts or spades)
54    #[must_use]
55    #[inline]
56    pub const fn is_major(self) -> bool {
57        matches!(self, Self::Hearts | Self::Spades)
58    }
59
60    /// Whether this strain is a suit
61    #[must_use]
62    #[inline]
63    pub const fn is_suit(self) -> bool {
64        !matches!(self, Self::Notrump)
65    }
66
67    /// Whether this strain is notrump
68    #[must_use]
69    #[inline]
70    pub const fn is_notrump(self) -> bool {
71        matches!(self, Self::Notrump)
72    }
73
74    /// Convert to a [`Suit`], returning `None` for notrump
75    #[must_use]
76    #[inline]
77    pub const fn suit(self) -> Option<Suit> {
78        match self {
79            Self::Clubs => Some(Suit::Clubs),
80            Self::Diamonds => Some(Suit::Diamonds),
81            Self::Hearts => Some(Suit::Hearts),
82            Self::Spades => Some(Suit::Spades),
83            Self::Notrump => None,
84        }
85    }
86
87    /// Uppercase letter
88    #[must_use]
89    #[inline]
90    pub const fn letter(self) -> char {
91        match self {
92            Self::Clubs => 'C',
93            Self::Diamonds => 'D',
94            Self::Hearts => 'H',
95            Self::Spades => 'S',
96            Self::Notrump => 'N',
97        }
98    }
99}
100
101impl fmt::Display for Strain {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::Clubs => f.write_char('♣'),
105            Self::Diamonds => f.write_char('♦'),
106            Self::Hearts => f.write_char('♥'),
107            Self::Spades => f.write_char('♠'),
108            Self::Notrump => f.write_str("NT"),
109        }
110    }
111}
112
113impl Strain {
114    /// Strains in the ascending order, the order in this crate
115    pub const ASC: [Self; 5] = [
116        Self::Clubs,
117        Self::Diamonds,
118        Self::Hearts,
119        Self::Spades,
120        Self::Notrump,
121    ];
122
123    /// Strains in the descending order
124    pub const DESC: [Self; 5] = [
125        Self::Notrump,
126        Self::Spades,
127        Self::Hearts,
128        Self::Diamonds,
129        Self::Clubs,
130    ];
131}
132
133/// A suit of playing cards
134///
135/// Suits are convertible to [`Strain`]s since suits form a subset of strains.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138#[repr(u8)]
139pub enum Suit {
140    /// ♣, convertible to [`Strain::Clubs`]
141    Clubs,
142    /// ♦, convertible to [`Strain::Diamonds`]
143    Diamonds,
144    /// ♥, convertible to [`Strain::Hearts`]
145    Hearts,
146    /// ♠, convertible to [`Strain::Spades`]
147    Spades,
148}
149
150impl Suit {
151    /// Suits in the ascending order, the order in this crate
152    pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
153
154    /// Suits in the descending order, the order in [`dds_bridge_sys`]
155    pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
156
157    /// Uppercase letter
158    #[must_use]
159    #[inline]
160    pub const fn letter(self) -> char {
161        match self {
162            Self::Clubs => 'C',
163            Self::Diamonds => 'D',
164            Self::Hearts => 'H',
165            Self::Spades => 'S',
166        }
167    }
168}
169
170impl fmt::Display for Suit {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.write_char(match self {
173            Self::Clubs => '♣',
174            Self::Diamonds => '♦',
175            Self::Hearts => '♥',
176            Self::Spades => '♠',
177        })
178    }
179}
180
181impl From<Suit> for Strain {
182    fn from(suit: Suit) -> Self {
183        match suit {
184            Suit::Clubs => Self::Clubs,
185            Suit::Diamonds => Self::Diamonds,
186            Suit::Hearts => Self::Hearts,
187            Suit::Spades => Self::Spades,
188        }
189    }
190}
191
192/// Error raised when converting [`Strain::Notrump`] to a suit
193#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
194#[error("Notrump is not a suit")]
195pub struct SuitFromNotrumpError;
196
197impl TryFrom<Strain> for Suit {
198    type Error = SuitFromNotrumpError;
199
200    fn try_from(strain: Strain) -> Result<Self, Self::Error> {
201        match strain {
202            Strain::Clubs => Ok(Self::Clubs),
203            Strain::Diamonds => Ok(Self::Diamonds),
204            Strain::Hearts => Ok(Self::Hearts),
205            Strain::Spades => Ok(Self::Spades),
206            Strain::Notrump => Err(SuitFromNotrumpError),
207        }
208    }
209}
210
211/// Unicode variation selectors that may appear after suit emojis
212///
213/// We want to ignore these suffixes when parsing suits.
214const EMOJI_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
215
216/// Error returned when parsing a [`Suit`] fails
217#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
218#[error("Invalid suit: expected one of C, D, H, S, ♣, ♦, ♥, ♠, ♧, ♢, ♡, ♤")]
219pub struct ParseSuitError;
220
221impl FromStr for Suit {
222    type Err = ParseSuitError;
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        match s
225            .to_ascii_uppercase()
226            .as_str()
227            .trim_end_matches(EMOJI_SELECTORS)
228        {
229            "C" | "♣" | "♧" => Ok(Self::Clubs),
230            "D" | "♦" | "♢" => Ok(Self::Diamonds),
231            "H" | "♥" | "♡" => Ok(Self::Hearts),
232            "S" | "♠" | "♤" => Ok(Self::Spades),
233            _ => Err(ParseSuitError),
234        }
235    }
236}
237
238/// Error returned when parsing a [`Strain`] fails
239#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
240#[error("Invalid strain: expected one of C, D, H, S, N, NT, ♣, ♦, ♥, ♠, ♧, ♢, ♡, ♤")]
241pub struct ParseStrainError;
242
243impl FromStr for Strain {
244    type Err = ParseStrainError;
245    fn from_str(s: &str) -> Result<Self, Self::Err> {
246        match s
247            .to_ascii_uppercase()
248            .as_str()
249            .trim_end_matches(EMOJI_SELECTORS)
250        {
251            "C" | "♣" | "♧" => Ok(Self::Clubs),
252            "D" | "♦" | "♢" => Ok(Self::Diamonds),
253            "H" | "♥" | "♡" => Ok(Self::Hearts),
254            "S" | "♠" | "♤" => Ok(Self::Spades),
255            "N" | "NT" => Ok(Self::Notrump),
256            _ => Err(ParseStrainError),
257        }
258    }
259}