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#[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 Clubs,
36 Diamonds,
38 Hearts,
40 Spades,
42 Notrump,
44}
45
46impl Strain {
47 #[must_use]
49 #[inline]
50 pub const fn is_minor(self) -> bool {
51 matches!(self, Self::Clubs | Self::Diamonds)
52 }
53
54 #[must_use]
56 #[inline]
57 pub const fn is_major(self) -> bool {
58 matches!(self, Self::Hearts | Self::Spades)
59 }
60
61 #[must_use]
63 #[inline]
64 pub const fn is_suit(self) -> bool {
65 !matches!(self, Self::Notrump)
66 }
67
68 #[must_use]
70 #[inline]
71 pub const fn is_notrump(self) -> bool {
72 matches!(self, Self::Notrump)
73 }
74
75 #[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 #[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 pub const ASC: [Self; 5] = [
117 Self::Clubs,
118 Self::Diamonds,
119 Self::Hearts,
120 Self::Spades,
121 Self::Notrump,
122 ];
123
124 pub const DESC: [Self; 5] = [
126 Self::Notrump,
127 Self::Spades,
128 Self::Hearts,
129 Self::Diamonds,
130 Self::Clubs,
131 ];
132}
133
134#[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 Clubs,
143 Diamonds,
145 Hearts,
147 Spades,
149}
150
151impl Suit {
152 pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
154
155 pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
157
158 #[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#[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
212const EMOJI_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
216
217#[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#[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}