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#[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 Clubs,
37 Diamonds,
39 Hearts,
41 Spades,
43 Notrump,
45}
46
47impl Strain {
48 #[must_use]
50 #[inline]
51 pub const fn is_minor(self) -> bool {
52 matches!(self, Self::Clubs | Self::Diamonds)
53 }
54
55 #[must_use]
57 #[inline]
58 pub const fn is_major(self) -> bool {
59 matches!(self, Self::Hearts | Self::Spades)
60 }
61
62 #[must_use]
64 #[inline]
65 pub const fn is_suit(self) -> bool {
66 !matches!(self, Self::Notrump)
67 }
68
69 #[must_use]
71 #[inline]
72 pub const fn is_notrump(self) -> bool {
73 matches!(self, Self::Notrump)
74 }
75
76 #[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 #[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 pub const ASC: [Self; 5] = [
118 Self::Clubs,
119 Self::Diamonds,
120 Self::Hearts,
121 Self::Spades,
122 Self::Notrump,
123 ];
124
125 pub const DESC: [Self; 5] = [
127 Self::Notrump,
128 Self::Spades,
129 Self::Hearts,
130 Self::Diamonds,
131 Self::Clubs,
132 ];
133}
134
135#[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 Clubs,
144 Diamonds,
146 Hearts,
148 Spades,
150}
151
152impl Suit {
153 pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
155
156 pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
158
159 #[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#[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
213const EMOJI_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
217
218#[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#[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}