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
220/// Format a sequence of calls as an auction: space-joined, with
221/// [`Pass`](Call::Pass) rendered as `-` so [`Double`](Call::Double) /
222/// [`Redouble`](Call::Redouble) stand out when scanning. A lone [`Call`]'s own
223/// [`Display`](fmt::Display) still shows `P`; only the auction view substitutes
224/// `-`. The `-` form round-trips through [`Call`]'s [`FromStr`].
225pub fn display_calls(calls: &[Call]) -> impl fmt::Display + '_ {
226    CallsAsAuction(calls)
227}
228
229struct CallsAsAuction<'a>(&'a [Call]);
230
231impl fmt::Display for CallsAsAuction<'_> {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        let mut iter = self.0.iter();
234        if let Some(first) = iter.next() {
235            write_call_in_auction(first, f)?;
236            for call in iter {
237                f.write_char(' ')?;
238                write_call_in_auction(call, f)?;
239            }
240        }
241        Ok(())
242    }
243}
244
245/// Write one call in auction context: [`Pass`](Call::Pass) as `-`, everything
246/// else via [`Call`]'s own [`Display`](fmt::Display).
247fn write_call_in_auction(call: &Call, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248    match call {
249        Call::Pass => f.write_char('-'),
250        other => fmt::Display::fmt(other, f),
251    }
252}
253
254impl fmt::Display for Auction {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        display_calls(&self.0).fmt(f)
257    }
258}
259
260/// Error returned when parsing an [`Auction`] fails
261#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
262pub enum ParseAuctionError {
263    /// A token could not be parsed as a [`Call`]
264    #[error(transparent)]
265    Call(#[from] ParseCallError),
266    /// A parsed call would violate the laws of bidding
267    #[error(transparent)]
268    Illegal(#[from] IllegalCall),
269}
270
271impl FromStr for Auction {
272    type Err = ParseAuctionError;
273
274    fn from_str(s: &str) -> Result<Self, Self::Err> {
275        let mut auction = Self::new();
276        for token in s.split_ascii_whitespace() {
277            auction.try_push(token.parse()?)?;
278        }
279        Ok(auction)
280    }
281}
282
283/// View the auction as a slice of calls
284impl Deref for Auction {
285    type Target = [Call];
286
287    fn deref(&self) -> &[Call] {
288        &self.0
289    }
290}
291
292impl AsRef<[Call]> for Auction {
293    fn as_ref(&self) -> &[Call] {
294        self
295    }
296}
297
298impl Borrow<[Call]> for Auction {
299    fn borrow(&self) -> &[Call] {
300        self
301    }
302}
303
304impl From<Auction> for Vec<Call> {
305    fn from(auction: Auction) -> Self {
306        auction.0
307    }
308}
309
310impl IntoIterator for Auction {
311    type Item = Call;
312    type IntoIter = std::vec::IntoIter<Call>;
313
314    fn into_iter(self) -> Self::IntoIter {
315        self.0.into_iter()
316    }
317}
318
319impl<'a> IntoIterator for &'a Auction {
320    type Item = &'a Call;
321    type IntoIter = core::slice::Iter<'a, Call>;
322
323    fn into_iter(self) -> Self::IntoIter {
324        self.0.iter()
325    }
326}
327
328impl Auction {
329    /// Construct an empty auction
330    #[must_use]
331    pub const fn new() -> Self {
332        Self(Vec::new())
333    }
334
335    /// Check if the auction is terminated (by 3 consecutive passes following
336    /// a call)
337    #[must_use]
338    pub fn has_ended(&self) -> bool {
339        self.len() >= 4 && self[self.len() - 3..] == [Call::Pass; 3]
340    }
341
342    /// Test doubling the last bid
343    fn can_double(&self) -> Result<(), IllegalCall> {
344        let admissible = self
345            .iter()
346            .rev()
347            .copied()
348            .enumerate()
349            .find(|&(_, call)| call != Call::Pass)
350            .is_some_and(|(index, call)| index & 1 == 0 && matches!(call, Call::Bid(_)));
351
352        if !admissible {
353            return Err(IllegalCall::InadmissibleDouble(Penalty::Doubled));
354        }
355        Ok(())
356    }
357
358    /// Test redoubling the last double (dry run)
359    fn can_redouble(&self) -> Result<(), IllegalCall> {
360        let admissible = self
361            .iter()
362            .rev()
363            .copied()
364            .enumerate()
365            .find(|&(_, call)| call != Call::Pass)
366            .is_some_and(|(index, call)| index & 1 == 0 && call == Call::Double);
367
368        if !admissible {
369            return Err(IllegalCall::InadmissibleDouble(Penalty::Redoubled));
370        }
371        Ok(())
372    }
373
374    /// Test bidding a contract (dry run)
375    fn can_bid(&self, bid: Bid) -> Result<(), IllegalCall> {
376        let last = self.iter().rev().find_map(|&call| match call {
377            Call::Bid(bid) => Some(bid),
378            _ => None,
379        });
380
381        if last >= Some(bid) {
382            return Err(IllegalCall::InsufficientBid { this: bid, last });
383        }
384        Ok(())
385    }
386
387    /// Test adding a call to the auction (dry run)
388    ///
389    /// This is the check behind [`try_push`][Self::try_push] without the push,
390    /// for filtering candidate calls without cloning the auction.
391    ///
392    /// # Errors
393    ///
394    /// [`IllegalCall`] if the call is forbidden by [The Laws of Duplicate
395    /// Bridge][laws].
396    ///
397    /// [laws]: http://www.worldbridge.org/wp-content/uploads/2017/03/2017LawsofDuplicateBridge-nohighlights.pdf
398    pub fn can_push(&self, call: Call) -> Result<(), IllegalCall> {
399        if self.has_ended() {
400            return Err(IllegalCall::AfterFinalPass);
401        }
402
403        match call {
404            Call::Pass => Ok(()),
405            Call::Double => self.can_double(),
406            Call::Redouble => self.can_redouble(),
407            Call::Bid(bid) => self.can_bid(bid),
408        }
409    }
410
411    /// Add a call to the auction
412    ///
413    /// # Panics
414    ///
415    /// Panics if the call is illegal.
416    pub fn push(&mut self, call: Call) {
417        self.try_push(call).unwrap();
418    }
419
420    /// Add a call to the auction with checks
421    ///
422    /// # Errors
423    ///
424    /// [`IllegalCall`] if the call is forbidden by [The Laws of Duplicate
425    /// Bridge][laws].
426    ///
427    /// [laws]: http://www.worldbridge.org/wp-content/uploads/2017/03/2017LawsofDuplicateBridge-nohighlights.pdf
428    pub fn try_push(&mut self, call: Call) -> Result<(), IllegalCall> {
429        self.can_push(call)?;
430        self.0.push(call);
431        Ok(())
432    }
433
434    /// Try adding calls to the auction
435    ///
436    /// # Errors
437    ///
438    /// If any call is illegal, an [`IllegalCall`] is returned.  Calls already
439    /// added to the auction are kept.  If you want to roll back the auction,
440    /// [`truncate`][Self::truncate] it to the previous length.
441    pub fn try_extend(&mut self, iter: impl IntoIterator<Item = Call>) -> Result<(), IllegalCall> {
442        let iter = iter.into_iter();
443
444        if let Some(size) = iter.size_hint().1 {
445            self.0.reserve(size);
446        }
447
448        for call in iter {
449            self.try_push(call)?;
450        }
451        Ok(())
452    }
453
454    /// Pop the last call from the auction
455    pub fn pop(&mut self) -> Option<Call> {
456        self.0.pop()
457    }
458
459    /// Truncate the auction to the first `len` calls
460    ///
461    /// If `len` is greater or equal to the current length, this has no effect.
462    pub fn truncate(&mut self, len: usize) {
463        self.0.truncate(len);
464    }
465
466    /// Find the position of the declaring bid in the call sequence
467    ///
468    /// The declarer is the first player on the declaring side to have bid the
469    /// strain of the final contract.  This method returns the index of that
470    /// bid in `self`, so `self[index]` is the declaring bid.
471    ///
472    /// The index also encodes the relative seat: `index % 2 == 0` is the
473    /// dealer's side, and `index % 2 == 1` is the other side.  To obtain the
474    /// absolute [`Seat`](crate::Seat), add the dealer's seat offset modulo 4.
475    ///
476    /// Returns [`None`] if the auction has no bid (passed out).
477    ///
478    /// # Examples
479    ///
480    /// ```
481    /// use contract_bridge::auction::{Auction, Call, IllegalCall};
482    /// use contract_bridge::{Bid, Level, Strain};
483    ///
484    /// # fn main() -> Result<(), IllegalCall> {
485    /// // 1♥ by opener (index 1), raised to 4♥ — declarer bid 1♥ at index 1
486    /// let mut auction = Auction::new();
487    /// let one_heart = Call::Bid(Bid { level: Level::new(1), strain: Strain::Hearts });
488    /// let four_hearts = Call::Bid(Bid { level: Level::new(4), strain: Strain::Hearts });
489    /// auction.try_push(Call::Pass)?;  // index 0 (dealer)
490    /// auction.try_push(one_heart)?;   // index 1 (declarer)
491    /// auction.try_push(Call::Pass)?;  // index 2
492    /// auction.try_push(four_hearts)?; // index 3 (dummy)
493    /// auction.try_push(Call::Pass)?;
494    /// auction.try_push(Call::Pass)?;
495    /// auction.try_push(Call::Pass)?;
496    /// assert_eq!(auction.declarer(), Some(1));
497    /// # Ok(())
498    /// # }
499    /// ```
500    #[must_use]
501    pub fn declarer(&self) -> Option<usize> {
502        let (parity, strain) =
503            self.iter()
504                .copied()
505                .enumerate()
506                .rev()
507                .find_map(|(index, call)| match call {
508                    Call::Bid(bid) => Some((index & 1, bid.strain)),
509                    _ => None,
510                })?;
511
512        self.iter()
513            .skip(parity)
514            .step_by(2)
515            .position(|call| match call {
516                Call::Bid(bid) => bid.strain == strain,
517                _ => false,
518            })
519            .map(|position| position << 1 | parity)
520    }
521}
522
523#[cfg(test)]
524mod tests;