Skip to main content

dds_bridge/solver/
par.rs

1//! Par-contract results and their conversion from DDS FFI types
2
3use super::ffi;
4use crate::contract::{Bid, Contract, Penalty};
5use crate::seat::Seat;
6use core::ops::BitOr as _;
7use dds_bridge_sys as sys;
8
9/// Par contract
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct ParContract {
13    /// The contract
14    pub contract: Contract,
15
16    /// The declarer of the contract
17    pub declarer: Seat,
18
19    /// The number of overtricks (negative for undertricks)
20    pub overtricks: i8,
21}
22
23/// Par score and contracts
24#[derive(Debug, Clone)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Par {
27    /// The par score
28    pub score: i32,
29
30    /// The contracts that achieve the par score
31    pub contracts: Vec<ParContract>,
32}
33
34impl Par {
35    /// Check if two pars are equivalent
36    ///
37    /// Two pars are equivalent if they have the same par score and the same
38    /// set of (strain, declarer) pairs.  Overtricks and duplicate entries are
39    /// ignored.
40    ///
41    /// This is intentionally looser than [`PartialEq`], which compares every
42    /// field exactly.  `equivalent` exists because DDS may report the same
43    /// par result with different overtrick counts or orderings depending on
44    /// the code path (e.g. `DealerParBin` vs `SidesParBin`).  Use `==` when
45    /// you need exact structural equality; use `equivalent` when you only
46    /// care about the strategic meaning of the par result.
47    #[must_use]
48    pub fn equivalent(&self, other: &Self) -> bool {
49        // Since every contract scores the same, we can compare only the set of
50        // (`Strain`, `Seat`).  #`Strain` * #`Seat` = 5 * 4 = 20, which fits
51        // in a `u32` as a bitset.
52        fn key(contracts: &[ParContract]) -> u32 {
53            contracts
54                .iter()
55                .map(|p| 1 << ((p.contract.bid.strain as u8) << 2 | p.declarer as u8))
56                .fold(0, u32::bitor)
57        }
58        self.score == other.score && key(&self.contracts) == key(&other.contracts)
59    }
60}
61
62impl From<sys::parResultsMaster> for Par {
63    fn from(par: sys::parResultsMaster) -> Self {
64        let number = ffi::count_from_sys(par.number, par.contracts.len());
65
66        // DDS returns a zero contract for par-zero deals, but we want to filter
67        // it out for consistency.
68        let len = number * usize::from(par.contracts[0].level != 0);
69
70        let contracts = par.contracts[..len]
71            .iter()
72            .flat_map(|contract| {
73                let strain = ffi::strain_from_denom(contract.denom);
74
75                #[allow(clippy::cast_possible_truncation)]
76                let (penalty, overtricks) = if contract.underTricks > 0 {
77                    (Penalty::Doubled, -contract.underTricks as i8)
78                } else {
79                    (Penalty::Undoubled, contract.overTricks as i8)
80                };
81
82                let seat = match contract.seats & 3 {
83                    0 => Seat::North,
84                    1 => Seat::East,
85                    2 => Seat::South,
86                    3 => Seat::West,
87                    _ => unreachable!("The bitmask ensures this is always in 0..=3"),
88                };
89                let is_pair = contract.seats >= 4;
90
91                let contract = Contract {
92                    bid: Bid {
93                        level: ffi::level_from_sys(contract.level),
94                        strain,
95                    },
96                    penalty,
97                };
98
99                core::iter::once(ParContract {
100                    contract,
101                    declarer: seat,
102                    overtricks,
103                })
104                .chain(is_pair.then_some(ParContract {
105                    contract,
106                    declarer: seat.partner(),
107                    overtricks,
108                }))
109            })
110            .collect();
111
112        Self {
113            score: par.score,
114            contracts,
115        }
116    }
117}