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