Skip to main content

contract_bridge/
lib.rs

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