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