1use crate::Strain;
16use core::fmt::{self, Write as _};
17use core::num::NonZero;
18use core::str::FromStr;
19use thiserror::Error;
20
21#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25#[error("{0} is not a valid level (1..=7)")]
26pub struct InvalidLevel(u8);
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[cfg_attr(feature = "serde", serde(transparent))]
35#[repr(transparent)]
36pub struct Level(NonZero<u8>);
37
38impl Level {
39 #[must_use]
46 #[inline]
47 pub const fn new(level: u8) -> Self {
48 match Self::try_new(level) {
49 Ok(l) => l,
50 Err(_) => panic!("level must be in 1..=7"),
51 }
52 }
53
54 #[inline]
60 pub const fn try_new(level: u8) -> Result<Self, InvalidLevel> {
61 match NonZero::new(level) {
62 Some(nonzero) if level <= 7 => Ok(Self(nonzero)),
63 _ => Err(InvalidLevel(level)),
64 }
65 }
66
67 #[must_use]
69 #[inline]
70 pub const fn get(self) -> u8 {
71 self.0.get()
72 }
73}
74
75impl fmt::Display for Level {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 self.get().fmt(f)
78 }
79}
80
81#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
83#[error("Invalid level: expected 1-7")]
84pub struct ParseLevelError;
85
86impl FromStr for Level {
87 type Err = ParseLevelError;
88
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 let s = s.as_bytes();
91
92 match (s.len(), s.first()) {
93 (1, Some(b'1'..=b'7')) => Ok(Self::new(s[0] - b'0')),
94 _ => Err(ParseLevelError),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
104#[cfg_attr(
105 feature = "serde",
106 derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
107)]
108pub struct Bid {
109 pub level: Level,
111 pub strain: Strain,
113}
114
115impl Bid {
116 #[must_use]
123 #[inline]
124 pub const fn new(level: u8, strain: Strain) -> Self {
125 Self {
126 level: Level::new(level),
127 strain,
128 }
129 }
130}
131
132impl fmt::Display for Bid {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}{}", self.level, self.strain)
135 }
136}
137
138#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
140#[error("Invalid bid: expected <level><strain>, e.g. 1N, 3♠, 7NT")]
141pub struct ParseBidError;
142
143impl FromStr for Bid {
144 type Err = ParseBidError;
145
146 fn from_str(s: &str) -> Result<Self, Self::Err> {
147 if s.len() < 2 {
148 return Err(ParseBidError);
149 }
150 let (level, strain) = s.split_at(1);
152 let level: Level = level.parse().map_err(|_| ParseBidError)?;
153 let strain: Strain = strain.parse().map_err(|_| ParseBidError)?;
154 Ok(Self { level, strain })
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
161#[repr(u8)]
162pub enum Penalty {
163 Undoubled,
165 Doubled,
167 Redoubled,
169}
170
171impl fmt::Display for Penalty {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 match self {
174 Self::Undoubled => Ok(()),
175 Self::Doubled => f.write_char('x'),
176 Self::Redoubled => f.write_str("xx"),
177 }
178 }
179}
180
181#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
183#[error("Invalid penalty: expected '', 'x'/'X', or 'xx'/'XX'")]
184pub struct ParsePenaltyError;
185
186impl FromStr for Penalty {
187 type Err = ParsePenaltyError;
188
189 fn from_str(s: &str) -> Result<Self, Self::Err> {
190 match s.to_ascii_lowercase().as_str() {
191 "" => Ok(Self::Undoubled),
192 "x" => Ok(Self::Doubled),
193 "xx" => Ok(Self::Redoubled),
194 _ => Err(ParsePenaltyError),
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
203#[cfg_attr(
204 feature = "serde",
205 derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
206)]
207pub struct Contract {
208 pub bid: Bid,
210 pub penalty: Penalty,
212}
213
214impl fmt::Display for Contract {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 write!(f, "{}{}", self.bid, self.penalty)
217 }
218}
219
220impl From<Bid> for Contract {
221 fn from(bid: Bid) -> Self {
222 Self {
223 bid,
224 penalty: Penalty::Undoubled,
225 }
226 }
227}
228
229const fn compute_doubled_penalty(undertricks: i32, vulnerable: bool) -> i32 {
230 match undertricks + vulnerable as i32 {
231 1 => 100,
232 2 => {
233 if vulnerable {
234 200
235 } else {
236 300
237 }
238 }
239 many => 300 * many - 400,
240 }
241}
242
243impl Contract {
244 #[must_use]
251 #[inline]
252 pub const fn new(level: u8, strain: Strain, penalty: Penalty) -> Self {
253 Self {
254 bid: Bid::new(level, strain),
255 penalty,
256 }
257 }
258
259 #[must_use]
263 #[inline]
264 pub const fn contract_points(self) -> i32 {
265 let level = self.bid.level.get() as i32;
266 let per_trick = self.bid.strain.is_minor() as i32 * -10 + 30;
267 let notrump = self.bid.strain.is_notrump() as i32 * 10;
268 (per_trick * level + notrump) << (self.penalty as u8)
269 }
270
271 #[must_use]
298 #[inline]
299 pub const fn score(self, tricks: u8, vulnerable: bool) -> i32 {
300 let overtricks = tricks as i32 - self.bid.level.get() as i32 - 6;
301
302 if overtricks >= 0 {
303 let base = self.contract_points();
304 let game = if base < 100 {
305 50
306 } else if vulnerable {
307 500
308 } else {
309 300
310 };
311 let doubled = self.penalty as i32 * 50;
312
313 let slam = match self.bid.level.get() {
314 6 => (vulnerable as i32 + 2) * 250,
315 7 => (vulnerable as i32 + 2) * 500,
316 _ => 0,
317 };
318
319 let per_trick = match self.penalty {
320 Penalty::Undoubled => self.bid.strain.is_minor() as i32 * -10 + 30,
321 penalty => penalty as i32 * if vulnerable { 200 } else { 100 },
322 };
323
324 base + game + slam + doubled + overtricks * per_trick
325 } else {
326 match self.penalty {
327 Penalty::Undoubled => overtricks * if vulnerable { 100 } else { 50 },
328 penalty => penalty as i32 * -compute_doubled_penalty(-overtricks, vulnerable),
329 }
330 }
331 }
332}
333
334#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
336#[error("Invalid contract: expected <bid><penalty>, e.g. 1♥, 3♠x, 7NTxx")]
337pub struct ParseContractError;
338
339impl FromStr for Contract {
340 type Err = ParseContractError;
341
342 fn from_str(s: &str) -> Result<Self, Self::Err> {
343 let x_count = s
344 .bytes()
345 .rev()
346 .take_while(|c| b'x'.eq_ignore_ascii_case(c))
347 .take(3)
348 .count();
349
350 let penalty = match x_count {
351 0 => Penalty::Undoubled,
352 1 => Penalty::Doubled,
353 2 => Penalty::Redoubled,
354 _ => return Err(ParseContractError),
355 };
356
357 s[..s.len() - x_count]
358 .parse()
359 .map_or(Err(ParseContractError), |bid| Ok(Self { bid, penalty }))
360 }
361}