Skip to main content

contract_bridge/
auction.rs

1//! Auction lingo: calls, legal-call enforcement, and the sequence of calls
2//! that make up the bidding phase of a deal.
3//!
4//! [`Call`] is the exhaustive set of legal announcements during the bidding —
5//! pass, double, redouble, or a [`Bid`]. [`Auction`] stores a sequence of
6//! `Call`s and enforces the laws of duplicate bridge on each insertion,
7//! reporting violations via [`IllegalCall`].
8//!
9//! [`RelativeVulnerability`] is a 2-bit set (WE / THEY) for vulnerability
10//! viewed from one partnership's perspective. The four valid combinations are
11//! exposed as the constants [`NONE`](RelativeVulnerability::NONE),
12//! [`WE`](RelativeVulnerability::WE), [`THEY`](RelativeVulnerability::THEY),
13//! and [`ALL`](RelativeVulnerability::ALL).
14
15use crate::{Bid, Penalty};
16use core::borrow::Borrow;
17use core::fmt::{self, Write as _};
18use core::ops::Deref;
19use core::str::FromStr;
20use thiserror::Error;
21
22/// Any legal announcement in the bidding stage
23///
24/// This enum is intentionally exhaustive: the laws of contract bridge define
25/// exactly these call types, so no future variants are possible.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27#[cfg_attr(
28    feature = "serde",
29    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
30)]
31pub enum Call {
32    /// A call indicating no wish to change the contract
33    Pass,
34    /// A call increasing penalties and bonuses for the contract
35    Double,
36    /// A call doubling the score to the previous double
37    Redouble,
38    /// A call proposing a contract
39    Bid(Bid),
40}
41
42impl From<Bid> for Call {
43    fn from(bid: Bid) -> Self {
44        Self::Bid(bid)
45    }
46}
47
48impl fmt::Display for Call {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::Pass => f.write_char('P'),
52            Self::Double => f.write_char('X'),
53            Self::Redouble => f.write_str("XX"),
54            Self::Bid(bid) => bid.fmt(f),
55        }
56    }
57}
58
59/// Error returned when parsing a [`Call`] fails
60#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
61#[error("Invalid call: expected pass, double, redouble, or a bid like '1NT' or '3♠'")]
62pub struct ParseCallError;
63
64impl FromStr for Call {
65    type Err = ParseCallError;
66
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        match s.to_ascii_uppercase().as_str() {
69            "P" | "PASS" => Ok(Self::Pass),
70            "X" | "DBL" | "DOUBLE" => Ok(Self::Double),
71            "XX" | "RDBL" | "REDOUBLE" => Ok(Self::Redouble),
72            _ => s.parse::<Bid>().map(Self::Bid).map_err(|_| ParseCallError),
73        }
74    }
75}
76
77bitflags::bitflags! {
78    /// Vulnerability of sides
79    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81    pub struct RelativeVulnerability: u8 {
82        /// We are vulnerable
83        const WE = 1;
84        /// Opponents are vulnerable
85        const THEY = 2;
86    }
87}
88
89impl RelativeVulnerability {
90    /// No player is vulnerable
91    pub const NONE: Self = Self::empty();
92    /// All players are vulnerable
93    pub const ALL: Self = Self::all();
94}
95
96impl fmt::Display for RelativeVulnerability {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match *self {
99            Self::NONE => f.write_str("none"),
100            Self::WE => f.write_str("we"),
101            Self::THEY => f.write_str("they"),
102            Self::ALL => f.write_str("both"),
103            _ => unreachable!("RelativeVulnerability has only 4 valid bit combinations"),
104        }
105    }
106}
107
108/// Error returned when parsing a [`RelativeVulnerability`] fails
109#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
110#[error("Invalid relative vulnerability: expected one of none, we, they, both, all")]
111pub struct ParseRelativeVulnerabilityError;
112
113impl FromStr for RelativeVulnerability {
114    type Err = ParseRelativeVulnerabilityError;
115
116    fn from_str(s: &str) -> Result<Self, Self::Err> {
117        match s.to_ascii_lowercase().as_str() {
118            "none" => Ok(Self::NONE),
119            "we" => Ok(Self::WE),
120            "they" => Ok(Self::THEY),
121            "both" | "all" => Ok(Self::ALL),
122            _ => Err(ParseRelativeVulnerabilityError),
123        }
124    }
125}
126
127/// Types of illegal calls
128///
129/// The laws mentioned in the variants are from [The Laws of Duplicate Bridge
130/// 2017][laws].
131///
132/// [laws]: http://www.worldbridge.org/wp-content/uploads/2017/03/2017LawsofDuplicateBridge-nohighlights.pdf
133#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135#[non_exhaustive]
136pub enum IllegalCall {
137    /// Law 27: insufficient bid
138    #[error("Law 27: insufficient bid")]
139    InsufficientBid {
140        /// The offending bid
141        this: Bid,
142        /// The last bid in the auction
143        last: Option<Bid>,
144    },
145
146    /// Law 36: inadmissible doubles and redoubles
147    #[error("Law 36: inadmissible doubles and redoubles")]
148    InadmissibleDouble(Penalty),
149
150    /// Law 39: call after the final pass
151    #[error("Law 39: call after the final pass")]
152    AfterFinalPass,
153}
154
155/// A sequence of [`Call`]s
156#[derive(Debug, Clone, Default, PartialEq, Eq)]
157#[cfg_attr(
158    feature = "serde",
159    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
160)]
161pub struct Auction(Vec<Call>);
162
163impl fmt::Display for Auction {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        let mut iter = self.0.iter();
166        if let Some(first) = iter.next() {
167            first.fmt(f)?;
168            for call in iter {
169                f.write_char(' ')?;
170                call.fmt(f)?;
171            }
172        }
173        Ok(())
174    }
175}
176
177/// Error returned when parsing an [`Auction`] fails
178#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
179pub enum ParseAuctionError {
180    /// A token could not be parsed as a [`Call`]
181    #[error(transparent)]
182    Call(#[from] ParseCallError),
183    /// A parsed call would violate the laws of bidding
184    #[error(transparent)]
185    Illegal(#[from] IllegalCall),
186}
187
188impl FromStr for Auction {
189    type Err = ParseAuctionError;
190
191    fn from_str(s: &str) -> Result<Self, Self::Err> {
192        let mut auction = Self::new();
193        for token in s.split_ascii_whitespace() {
194            auction.try_push(token.parse()?)?;
195        }
196        Ok(auction)
197    }
198}
199
200/// View the auction as a slice of calls
201impl Deref for Auction {
202    type Target = [Call];
203
204    fn deref(&self) -> &[Call] {
205        &self.0
206    }
207}
208
209impl AsRef<[Call]> for Auction {
210    fn as_ref(&self) -> &[Call] {
211        self
212    }
213}
214
215impl Borrow<[Call]> for Auction {
216    fn borrow(&self) -> &[Call] {
217        self
218    }
219}
220
221impl From<Auction> for Vec<Call> {
222    fn from(auction: Auction) -> Self {
223        auction.0
224    }
225}
226
227impl IntoIterator for Auction {
228    type Item = Call;
229    type IntoIter = std::vec::IntoIter<Call>;
230
231    fn into_iter(self) -> Self::IntoIter {
232        self.0.into_iter()
233    }
234}
235
236impl<'a> IntoIterator for &'a Auction {
237    type Item = &'a Call;
238    type IntoIter = core::slice::Iter<'a, Call>;
239
240    fn into_iter(self) -> Self::IntoIter {
241        self.0.iter()
242    }
243}
244
245impl Auction {
246    /// Construct an empty auction
247    #[must_use]
248    pub const fn new() -> Self {
249        Self(Vec::new())
250    }
251
252    /// Check if the auction is terminated (by 3 consecutive passes following
253    /// a call)
254    #[must_use]
255    pub fn has_ended(&self) -> bool {
256        self.len() >= 4 && self[self.len() - 3..] == [Call::Pass; 3]
257    }
258
259    /// Test doubling the last bid
260    fn can_double(&self) -> Result<(), IllegalCall> {
261        let admissible = self
262            .iter()
263            .rev()
264            .copied()
265            .enumerate()
266            .find(|&(_, call)| call != Call::Pass)
267            .is_some_and(|(index, call)| index & 1 == 0 && matches!(call, Call::Bid(_)));
268
269        if !admissible {
270            return Err(IllegalCall::InadmissibleDouble(Penalty::Doubled));
271        }
272        Ok(())
273    }
274
275    /// Test redoubling the last double (dry run)
276    fn can_redouble(&self) -> Result<(), IllegalCall> {
277        let admissible = self
278            .iter()
279            .rev()
280            .copied()
281            .enumerate()
282            .find(|&(_, call)| call != Call::Pass)
283            .is_some_and(|(index, call)| index & 1 == 0 && call == Call::Double);
284
285        if !admissible {
286            return Err(IllegalCall::InadmissibleDouble(Penalty::Redoubled));
287        }
288        Ok(())
289    }
290
291    /// Test bidding a contract (dry run)
292    fn can_bid(&self, bid: Bid) -> Result<(), IllegalCall> {
293        let last = self.iter().rev().find_map(|&call| match call {
294            Call::Bid(bid) => Some(bid),
295            _ => None,
296        });
297
298        if last >= Some(bid) {
299            return Err(IllegalCall::InsufficientBid { this: bid, last });
300        }
301        Ok(())
302    }
303
304    /// Test adding a call to the auction
305    fn can_push(&self, call: Call) -> Result<(), IllegalCall> {
306        if self.has_ended() {
307            return Err(IllegalCall::AfterFinalPass);
308        }
309
310        match call {
311            Call::Pass => Ok(()),
312            Call::Double => self.can_double(),
313            Call::Redouble => self.can_redouble(),
314            Call::Bid(bid) => self.can_bid(bid),
315        }
316    }
317
318    /// Add a call to the auction
319    ///
320    /// # Panics
321    ///
322    /// Panics if the call is illegal.
323    pub fn push(&mut self, call: Call) {
324        self.try_push(call).unwrap();
325    }
326
327    /// Add a call to the auction with checks
328    ///
329    /// # Errors
330    ///
331    /// [`IllegalCall`] if the call is forbidden by [The Laws of Duplicate
332    /// Bridge][laws].
333    ///
334    /// [laws]: http://www.worldbridge.org/wp-content/uploads/2017/03/2017LawsofDuplicateBridge-nohighlights.pdf
335    pub fn try_push(&mut self, call: Call) -> Result<(), IllegalCall> {
336        self.can_push(call)?;
337        self.0.push(call);
338        Ok(())
339    }
340
341    /// Try adding calls to the auction
342    ///
343    /// # Errors
344    ///
345    /// If any call is illegal, an [`IllegalCall`] is returned.  Calls already
346    /// added to the auction are kept.  If you want to roll back the auction,
347    /// [`truncate`][Self::truncate] it to the previous length.
348    pub fn try_extend(&mut self, iter: impl IntoIterator<Item = Call>) -> Result<(), IllegalCall> {
349        let iter = iter.into_iter();
350
351        if let Some(size) = iter.size_hint().1 {
352            self.0.reserve(size);
353        }
354
355        for call in iter {
356            self.try_push(call)?;
357        }
358        Ok(())
359    }
360
361    /// Pop the last call from the auction
362    pub fn pop(&mut self) -> Option<Call> {
363        self.0.pop()
364    }
365
366    /// Truncate the auction to the first `len` calls
367    ///
368    /// If `len` is greater or equal to the current length, this has no effect.
369    pub fn truncate(&mut self, len: usize) {
370        self.0.truncate(len);
371    }
372
373    /// Find the position of the declaring bid in the call sequence
374    ///
375    /// The declarer is the first player on the declaring side to have bid the
376    /// strain of the final contract.  This method returns the index of that
377    /// bid in `self`, so `self[index]` is the declaring bid.
378    ///
379    /// The index also encodes the relative seat: `index % 2 == 0` is the
380    /// dealer's side, and `index % 2 == 1` is the other side.  To obtain the
381    /// absolute [`Seat`](crate::Seat), add the dealer's seat offset modulo 4.
382    ///
383    /// Returns [`None`] if the auction has no bid (passed out).
384    ///
385    /// # Examples
386    ///
387    /// ```
388    /// use contract_bridge::auction::{Auction, Call, IllegalCall};
389    /// use contract_bridge::{Bid, Level, Strain};
390    ///
391    /// # fn main() -> Result<(), IllegalCall> {
392    /// // 1♥ by opener (index 1), raised to 4♥ — declarer bid 1♥ at index 1
393    /// let mut auction = Auction::new();
394    /// let one_heart = Call::Bid(Bid { level: Level::new(1), strain: Strain::Hearts });
395    /// let four_hearts = Call::Bid(Bid { level: Level::new(4), strain: Strain::Hearts });
396    /// auction.try_push(Call::Pass)?;  // index 0 (dealer)
397    /// auction.try_push(one_heart)?;   // index 1 (declarer)
398    /// auction.try_push(Call::Pass)?;  // index 2
399    /// auction.try_push(four_hearts)?; // index 3 (dummy)
400    /// auction.try_push(Call::Pass)?;
401    /// auction.try_push(Call::Pass)?;
402    /// auction.try_push(Call::Pass)?;
403    /// assert_eq!(auction.declarer(), Some(1));
404    /// # Ok(())
405    /// # }
406    /// ```
407    #[must_use]
408    pub fn declarer(&self) -> Option<usize> {
409        let (parity, strain) =
410            self.iter()
411                .copied()
412                .enumerate()
413                .rev()
414                .find_map(|(index, call)| match call {
415                    Call::Bid(bid) => Some((index & 1, bid.strain)),
416                    _ => None,
417                })?;
418
419        self.iter()
420            .skip(parity)
421            .step_by(2)
422            .position(|call| match call {
423                Call::Bid(bid) => bid.strain == strain,
424                _ => false,
425            })
426            .map(|position| position << 1 | parity)
427    }
428}
429
430#[cfg(test)]
431mod tests;