dds_bridge/contract/
mod.rs

1#[cfg(test)]
2mod test;
3
4/// Denomination, a suit or notrump
5///
6/// We choose this representation over `Option<Suit>` because we are not sure if
7/// the latter can be optimized to a single byte.
8///
9/// The order of the suits deviates from [`dds`][dds], but this order provides
10/// natural ordering by deriving [`PartialOrd`] and [`Ord`].
11///
12/// [dds]: https://github.com/dds-bridge/dds
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u8)]
15pub enum Strain {
16    /// ♣
17    Clubs,
18    /// ♦
19    Diamonds,
20    /// ♥
21    Hearts,
22    /// ♠
23    Spades,
24    /// NT, the strain not proposing a trump suit
25    Notrump,
26}
27
28impl Strain {
29    /// Whether this strain is a minor suit (clubs or diamonds)
30    #[must_use]
31    #[inline]
32    pub const fn is_minor(self) -> bool {
33        matches!(self, Self::Clubs | Self::Diamonds)
34    }
35
36    /// Whether this strain is a major suit (hearts or spades)
37    #[must_use]
38    #[inline]
39    pub const fn is_major(self) -> bool {
40        matches!(self, Self::Hearts | Self::Spades)
41    }
42
43    /// Whether this strain is a suit
44    #[must_use]
45    #[inline]
46    pub const fn is_suit(self) -> bool {
47        !matches!(self, Self::Notrump)
48    }
49
50    /// Whether this strain is notrump
51    #[must_use]
52    #[inline]
53    pub const fn is_notrump(self) -> bool {
54        matches!(self, Self::Notrump)
55    }
56
57    /// Strains in the ascending order, the order in this crate
58    pub const ASC: [Self; 5] = [
59        Self::Clubs,
60        Self::Diamonds,
61        Self::Hearts,
62        Self::Spades,
63        Self::Notrump,
64    ];
65
66    /// Strains in the descending order
67    pub const DESC: [Self; 5] = [
68        Self::Notrump,
69        Self::Spades,
70        Self::Hearts,
71        Self::Diamonds,
72        Self::Clubs,
73    ];
74
75    /// Strains in the order in [`dds_bridge_sys`]
76    pub const SYS: [Self; 5] = [
77        Self::Spades,
78        Self::Hearts,
79        Self::Diamonds,
80        Self::Clubs,
81        Self::Notrump,
82    ];
83}
84
85/// A call that proposes a contract
86///
87/// The order of the fields ensures natural ordering by deriving [`PartialOrd`]
88/// and [`Ord`].
89#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct Bid {
91    /// The number of tricks (adding the book of 6 tricks) to take to fulfill
92    /// the contract
93    pub level: u8,
94
95    /// The strain of the contract
96    pub strain: Strain,
97}
98
99impl Bid {
100    /// Create a bid from level and strain
101    #[must_use]
102    #[inline]
103    pub const fn new(level: u8, strain: Strain) -> Self {
104        Self { level, strain }
105    }
106}
107
108/// Any legal announcement in the bidding stage
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum Call {
111    /// A call indicating no wish to change the contract
112    Pass,
113    /// A call increasing penalties and bonuses for the contract
114    Double,
115    /// A call doubling the score to the previous double
116    Redouble,
117    /// A call proposing a contract
118    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/// Penalty inflicted on a contract
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130#[repr(u8)]
131pub enum Penalty {
132    /// No penalty
133    None,
134    /// Penalty by [`Call::Double`]
135    Doubled,
136    /// Penalty by [`Call::Redouble`]
137    Redoubled,
138}
139
140/// The statement of the pair winning the bidding that they will take at least
141/// the number of tricks (in addition to the book of 6 tricks), and the strain
142/// denotes the trump suit.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub struct Contract {
145    /// The basic part of a contract
146    pub bid: Bid,
147    /// The penalty inflicted on the contract
148    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    /// Create a contract from level, strain, and penalty
178    #[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    /// Base score for making this contract
188    ///
189    /// <https://en.wikipedia.org/wiki/Bridge_scoring#Contract_points>
190    #[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    /// Score for this contract given the number of taken tricks and
200    /// vulnerability
201    ///
202    /// The score is positive if the declarer makes the contract, and negative
203    /// if the declarer fails.
204    #[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}