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
362    fn can_push(&self, call: Call) -> Result<(), IllegalCall> {
363        if self.has_ended() {
364            return Err(IllegalCall::AfterFinalPass);
365        }
366
367        match call {
368            Call::Pass => Ok(()),
369            Call::Double => self.can_double(),
370            Call::Redouble => self.can_redouble(),
371            Call::Bid(bid) => self.can_bid(bid),
372        }
373    }
374
375    /// Add a call to the auction
376    ///
377    /// # Panics
378    ///
379    /// Panics if the call is illegal.
380    pub fn push(&mut self, call: Call) {
381        self.try_push(call).unwrap();
382    }
383
384    /// Add a call to the auction with checks
385    ///
386    /// # Errors
387    ///
388    /// [`IllegalCall`] if the call is forbidden by [The Laws of Duplicate
389    /// Bridge][laws].
390    ///
391    /// [laws]: http://www.worldbridge.org/wp-content/uploads/2017/03/2017LawsofDuplicateBridge-nohighlights.pdf
392    pub fn try_push(&mut self, call: Call) -> Result<(), IllegalCall> {
393        self.can_push(call)?;
394        self.0.push(call);
395        Ok(())
396    }
397
398    /// Try adding calls to the auction
399    ///
400    /// # Errors
401    ///
402    /// If any call is illegal, an [`IllegalCall`] is returned.  Calls already
403    /// added to the auction are kept.  If you want to roll back the auction,
404    /// [`truncate`][Self::truncate] it to the previous length.
405    pub fn try_extend(&mut self, iter: impl IntoIterator<Item = Call>) -> Result<(), IllegalCall> {
406        let iter = iter.into_iter();
407
408        if let Some(size) = iter.size_hint().1 {
409            self.0.reserve(size);
410        }
411
412        for call in iter {
413            self.try_push(call)?;
414        }
415        Ok(())
416    }
417
418    /// Pop the last call from the auction
419    pub fn pop(&mut self) -> Option<Call> {
420        self.0.pop()
421    }
422
423    /// Truncate the auction to the first `len` calls
424    ///
425    /// If `len` is greater or equal to the current length, this has no effect.
426    pub fn truncate(&mut self, len: usize) {
427        self.0.truncate(len);
428    }
429
430    /// Find the position of the declaring bid in the call sequence
431    ///
432    /// The declarer is the first player on the declaring side to have bid the
433    /// strain of the final contract.  This method returns the index of that
434    /// bid in `self`, so `self[index]` is the declaring bid.
435    ///
436    /// The index also encodes the relative seat: `index % 2 == 0` is the
437    /// dealer's side, and `index % 2 == 1` is the other side.  To obtain the
438    /// absolute [`Seat`](crate::Seat), add the dealer's seat offset modulo 4.
439    ///
440    /// Returns [`None`] if the auction has no bid (passed out).
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use contract_bridge::auction::{Auction, Call, IllegalCall};
446    /// use contract_bridge::{Bid, Level, Strain};
447    ///
448    /// # fn main() -> Result<(), IllegalCall> {
449    /// // 1♥ by opener (index 1), raised to 4♥ — declarer bid 1♥ at index 1
450    /// let mut auction = Auction::new();
451    /// let one_heart = Call::Bid(Bid { level: Level::new(1), strain: Strain::Hearts });
452    /// let four_hearts = Call::Bid(Bid { level: Level::new(4), strain: Strain::Hearts });
453    /// auction.try_push(Call::Pass)?;  // index 0 (dealer)
454    /// auction.try_push(one_heart)?;   // index 1 (declarer)
455    /// auction.try_push(Call::Pass)?;  // index 2
456    /// auction.try_push(four_hearts)?; // index 3 (dummy)
457    /// auction.try_push(Call::Pass)?;
458    /// auction.try_push(Call::Pass)?;
459    /// auction.try_push(Call::Pass)?;
460    /// assert_eq!(auction.declarer(), Some(1));
461    /// # Ok(())
462    /// # }
463    /// ```
464    #[must_use]
465    pub fn declarer(&self) -> Option<usize> {
466        let (parity, strain) =
467            self.iter()
468                .copied()
469                .enumerate()
470                .rev()
471                .find_map(|(index, call)| match call {
472                    Call::Bid(bid) => Some((index & 1, bid.strain)),
473                    _ => None,
474                })?;
475
476        self.iter()
477            .skip(parity)
478            .step_by(2)
479            .position(|call| match call {
480                Call::Bid(bid) => bid.strain == strain,
481                _ => false,
482            })
483            .map(|position| position << 1 | parity)
484    }
485}
486
487#[cfg(test)]
488mod tests;