1#[cfg(test)]
2mod test;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u8)]
15pub enum Strain {
16 Clubs,
18 Diamonds,
20 Hearts,
22 Spades,
24 Notrump,
26}
27
28impl Strain {
29 #[must_use]
31 #[inline]
32 pub const fn is_minor(self) -> bool {
33 matches!(self, Self::Clubs | Self::Diamonds)
34 }
35
36 #[must_use]
38 #[inline]
39 pub const fn is_major(self) -> bool {
40 matches!(self, Self::Hearts | Self::Spades)
41 }
42
43 #[must_use]
45 #[inline]
46 pub const fn is_suit(self) -> bool {
47 !matches!(self, Self::Notrump)
48 }
49
50 #[must_use]
52 #[inline]
53 pub const fn is_notrump(self) -> bool {
54 matches!(self, Self::Notrump)
55 }
56
57 pub const ASC: [Self; 5] = [
59 Self::Clubs,
60 Self::Diamonds,
61 Self::Hearts,
62 Self::Spades,
63 Self::Notrump,
64 ];
65
66 pub const DESC: [Self; 5] = [
68 Self::Notrump,
69 Self::Spades,
70 Self::Hearts,
71 Self::Diamonds,
72 Self::Clubs,
73 ];
74
75 pub const SYS: [Self; 5] = [
77 Self::Spades,
78 Self::Hearts,
79 Self::Diamonds,
80 Self::Clubs,
81 Self::Notrump,
82 ];
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct Bid {
91 pub level: u8,
94
95 pub strain: Strain,
97}
98
99impl Bid {
100 #[must_use]
102 #[inline]
103 pub const fn new(level: u8, strain: Strain) -> Self {
104 Self { level, strain }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum Call {
111 Pass,
113 Double,
115 Redouble,
117 Bid(Bid),
119}
120
121impl From<Bid> for Call {
122 #[inline]
123 fn from(bid: Bid) -> Self {
124 Self::Bid(bid)
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130#[repr(u8)]
131pub enum Penalty {
132 None,
134 Doubled,
136 Redoubled,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub struct Contract {
145 pub bid: Bid,
147 pub penalty: Penalty,
149}
150
151impl From<Bid> for Contract {
152 #[inline]
153 fn from(bid: Bid) -> Self {
154 Self {
155 bid,
156 penalty: Penalty::None,
157 }
158 }
159}
160
161#[inline]
162const fn compute_doubled_penalty(undertricks: i32, vulnerable: bool) -> i32 {
163 match undertricks + vulnerable as i32 {
164 1 => 100,
165 2 => {
166 if vulnerable {
167 200
168 } else {
169 300
170 }
171 }
172 many => 300 * many - 400,
173 }
174}
175
176impl Contract {
177 #[must_use]
179 #[inline]
180 pub const fn new(level: u8, strain: Strain, penalty: Penalty) -> Self {
181 Self {
182 bid: Bid::new(level, strain),
183 penalty,
184 }
185 }
186
187 #[must_use]
191 #[inline]
192 pub const fn contract_points(self) -> i32 {
193 let level = self.bid.level as i32;
194 let per_trick = self.bid.strain.is_minor() as i32 * -10 + 30;
195 let notrump = self.bid.strain.is_notrump() as i32 * 10;
196 (per_trick * level + notrump) << (self.penalty as u8)
197 }
198
199 #[must_use]
205 #[inline]
206 pub const fn score(self, tricks: u8, vulnerable: bool) -> i32 {
207 let overtricks = tricks as i32 - self.bid.level as i32 - 6;
208
209 if overtricks >= 0 {
210 let base = self.contract_points();
211 let game = if base < 100 {
212 50
213 } else if vulnerable {
214 500
215 } else {
216 300
217 };
218 let doubled = self.penalty as i32 * 50;
219
220 let slam = match self.bid.level {
221 6 => (vulnerable as i32 + 2) * 250,
222 7 => (vulnerable as i32 + 2) * 500,
223 _ => 0,
224 };
225
226 let per_trick = match self.penalty {
227 Penalty::None => self.bid.strain.is_minor() as i32 * -10 + 30,
228 penalty => penalty as i32 * if vulnerable { 200 } else { 100 },
229 };
230
231 base + game + slam + doubled + overtricks * per_trick
232 } else {
233 match self.penalty {
234 Penalty::None => overtricks * if vulnerable { 100 } else { 50 },
235 penalty => penalty as i32 * -compute_doubled_penalty(-overtricks, vulnerable),
236 }
237 }
238 }
239}