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#[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 Clubs,
35 Diamonds,
37 Hearts,
39 Spades,
41 Notrump,
43}
44
45impl Strain {
46 #[must_use]
48 #[inline]
49 pub const fn is_minor(self) -> bool {
50 matches!(self, Self::Clubs | Self::Diamonds)
51 }
52
53 #[must_use]
55 #[inline]
56 pub const fn is_major(self) -> bool {
57 matches!(self, Self::Hearts | Self::Spades)
58 }
59
60 #[must_use]
62 #[inline]
63 pub const fn is_suit(self) -> bool {
64 !matches!(self, Self::Notrump)
65 }
66
67 #[must_use]
69 #[inline]
70 pub const fn is_notrump(self) -> bool {
71 matches!(self, Self::Notrump)
72 }
73
74 #[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 #[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 pub const ASC: [Self; 5] = [
116 Self::Clubs,
117 Self::Diamonds,
118 Self::Hearts,
119 Self::Spades,
120 Self::Notrump,
121 ];
122
123 pub const DESC: [Self; 5] = [
125 Self::Notrump,
126 Self::Spades,
127 Self::Hearts,
128 Self::Diamonds,
129 Self::Clubs,
130 ];
131}
132
133#[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 Clubs,
142 Diamonds,
144 Hearts,
146 Spades,
148}
149
150impl Suit {
151 pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
153
154 pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
156
157 #[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#[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
211const EMOJI_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
215
216#[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#[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}