Skip to main content

dds_bridge/
lib.rs

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