1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4pub mod contract;
6pub mod deal;
8pub 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#[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 Clubs,
34 Diamonds,
36 Hearts,
38 Spades,
40 Notrump,
42}
43
44impl Strain {
45 #[must_use]
47 #[inline]
48 pub const fn is_minor(self) -> bool {
49 matches!(self, Self::Clubs | Self::Diamonds)
50 }
51
52 #[must_use]
54 #[inline]
55 pub const fn is_major(self) -> bool {
56 matches!(self, Self::Hearts | Self::Spades)
57 }
58
59 #[must_use]
61 #[inline]
62 pub const fn is_suit(self) -> bool {
63 !matches!(self, Self::Notrump)
64 }
65
66 #[must_use]
68 #[inline]
69 pub const fn is_notrump(self) -> bool {
70 matches!(self, Self::Notrump)
71 }
72
73 #[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 #[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 pub const ASC: [Self; 5] = [
115 Self::Clubs,
116 Self::Diamonds,
117 Self::Hearts,
118 Self::Spades,
119 Self::Notrump,
120 ];
121
122 pub const DESC: [Self; 5] = [
124 Self::Notrump,
125 Self::Spades,
126 Self::Hearts,
127 Self::Diamonds,
128 Self::Clubs,
129 ];
130}
131
132#[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 Clubs,
141 Diamonds,
143 Hearts,
145 Spades,
147}
148
149impl Suit {
150 pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
152
153 pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
155
156 #[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#[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
210const EMOJI_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
214
215#[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#[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}