Skip to main content

contract_bridge/
eval.rs

1//! Hand evaluation: HCP, shortness, Fifths, BUM-RAP, losing trick count, NLTC,
2//! Zar points, and Kaplan–Rubens CCCC.
3//!
4//! The [`HandEvaluator`] trait abstracts over any function that maps a [`Hand`]
5//! to a numeric score. The standard schemes ([`hcp`], [`shortness`],
6//! [`fifths`], [`bumrap`], [`ltc`], [`nltc`], [`zar`], [`hcp_plus`]) operate on
7//! individual [`Holding`]s and are bundled into [`SimpleEvaluator`] constants
8//! ([`FIFTHS`], [`BUMRAP`], [`BUMRAP_PLUS`], [`NLTC`]) that evaluate a full
9//! hand by summing per-suit results.  [`zar`] and [`cccc`] evaluate the whole
10//! hand at once because they mix per-suit values with hand-wide shape terms.
11
12use crate::{Hand, Holding, Rank, Suit};
13use core::cmp::Ord;
14use core::iter::Sum;
15
16/// Trait for hand evaluators
17pub trait HandEvaluator<T> {
18    /// Evaluate a hand
19    #[must_use]
20    fn eval(&self, hand: Hand) -> T;
21
22    /// Evaluate a pair
23    #[must_use]
24    fn eval_pair(&self, pair: [Hand; 2]) -> T
25    where
26        T: core::ops::Add<Output = T>,
27    {
28        self.eval(pair[0]) + self.eval(pair[1])
29    }
30}
31
32/// Functions are natural evaluators
33impl<F: Fn(Hand) -> T, T> HandEvaluator<T> for F {
34    fn eval(&self, hand: Hand) -> T {
35        self(hand)
36    }
37}
38
39/// Evaluator summing values of suit holdings
40#[derive(Debug)]
41pub struct SimpleEvaluator<T: Sum, F: Fn(Holding) -> T>(
42    /// The per-suit kernel: invoked once per holding, its results are summed.
43    pub F,
44);
45
46impl<T: Sum, F: Fn(Holding) -> T> HandEvaluator<T> for SimpleEvaluator<T, F> {
47    fn eval(&self, hand: Hand) -> T {
48        Suit::ASC.into_iter().map(|s| (self.0)(hand[s])).sum()
49    }
50}
51
52impl<T: Sum, F: Clone + Fn(Holding) -> T> Clone for SimpleEvaluator<T, F> {
53    fn clone(&self) -> Self {
54        Self(self.0.clone())
55    }
56}
57
58impl<T: Sum, F: Copy + Fn(Holding) -> T> Copy for SimpleEvaluator<T, F> {}
59
60/// High card points
61///
62/// This is the well-known 4-3-2-1 point count by Milton Work.
63#[must_use]
64pub fn hcp<T: From<u8>>(holding: Holding) -> T {
65    T::from(
66        4 * u8::from(holding.contains(Rank::A))
67            + 3 * u8::from(holding.contains(Rank::K))
68            + 2 * u8::from(holding.contains(Rank::Q))
69            + u8::from(holding.contains(Rank::J)),
70    )
71}
72
73/// Short suit points
74#[must_use]
75// SAFETY: the integer to cast is in 0..=3, so the cast is safe.
76#[allow(clippy::cast_possible_truncation)]
77pub fn shortness<T: From<u8>>(holding: Holding) -> T {
78    T::from(3 - holding.len().min(3) as u8)
79}
80
81/// The [Fifths] evaluator for 3NT
82///
83/// This function is the kernel of [`FIFTHS`].
84///
85/// [Fifths]: https://bridge.thomasoandrews.com/valuations/cardvaluesfor3nt.html
86#[must_use]
87pub fn fifths(holding: Holding) -> f64 {
88    f64::from(
89        40 * i32::from(holding.contains(Rank::A))
90            + 28 * i32::from(holding.contains(Rank::K))
91            + 18 * i32::from(holding.contains(Rank::Q))
92            + 10 * i32::from(holding.contains(Rank::J))
93            + 4 * i32::from(holding.contains(Rank::T)),
94    ) / 10.0
95}
96
97/// The BUM-RAP evaluator
98///
99/// This function is the kernel of [`BUMRAP`].
100#[must_use]
101pub fn bumrap(holding: Holding) -> f64 {
102    f64::from(
103        18 * i32::from(holding.contains(Rank::A))
104            + 12 * i32::from(holding.contains(Rank::K))
105            + 6 * i32::from(holding.contains(Rank::Q))
106            + 3 * i32::from(holding.contains(Rank::J))
107            + i32::from(holding.contains(Rank::T)),
108    ) * 0.25
109}
110
111/// Plain old losing trick count
112#[must_use]
113pub fn ltc<T: From<u8>>(holding: Holding) -> T {
114    let len = holding.len();
115
116    T::from(
117        u8::from(len >= 1 && !holding.contains(Rank::A))
118            + u8::from(len >= 2 && !holding.contains(Rank::K))
119            + u8::from(len >= 3 && !holding.contains(Rank::Q)),
120    )
121}
122
123/// New Losing Trick Count
124///
125/// This function is the kernel of [`NLTC`].
126#[must_use]
127pub fn nltc(holding: Holding) -> f64 {
128    let len = holding.len();
129
130    f64::from(
131        3 * i32::from(len >= 1 && !holding.contains(Rank::A))
132            + 2 * i32::from(len >= 2 && !holding.contains(Rank::K))
133            + i32::from(len >= 3 && !holding.contains(Rank::Q)),
134    ) * 0.5
135}
136
137/// High card points plus useful shortness
138///
139/// For each suit, we count max([HCP][hcp], shortness, HCP + shortness &minus; 1).
140/// This method avoids double counting of short honors.  This evaluator is
141/// particularly useful for suit contracts.
142#[must_use]
143pub fn hcp_plus<T: From<u8>>(holding: Holding) -> T {
144    let count: u8 = hcp(holding);
145    let short: u8 = shortness(holding);
146
147    T::from(if count > 0 && short > 0 {
148        count + short - 1
149    } else {
150        count.max(short)
151    })
152}
153
154/// The [Fifths] evaluator for 3NT
155///
156/// This is Thomas Andrews's computed point count for 3NT.  This evaluator calls
157/// [`fifths`] for each suit.
158///
159/// [Fifths]: https://bridge.thomasoandrews.com/valuations/cardvaluesfor3nt.html
160pub const FIFTHS: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(fifths);
161
162/// The BUM-RAP evaluator
163///
164/// This is the BUM-RAP point count (4.5-3-1.5-0.75-0.25).  This evaluator calls
165/// [`bumrap`] for each suit.
166pub const BUMRAP: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(bumrap);
167
168/// BUM-RAP with shortness
169///
170/// For each suit, we count max([BUM-RAP][BUMRAP], shortness, BUM-RAP +
171/// shortness &minus; 1).  This method avoids double counting of short honors.
172/// This evaluator is particularly useful for suit contracts.
173pub const BUMRAP_PLUS: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(|x| {
174    let b: f64 = bumrap(x);
175    let s: f64 = shortness(x);
176    b.max(s).max(b + s - 1.0)
177});
178
179/// New Losing Trick Count
180///
181/// [NLTC](https://en.wikipedia.org/wiki/Losing-Trick_Count#New_Losing-Trick_Count_(NLTC))
182/// is a variant of losing trick count that gives different weights to missing
183/// honors.  A missing A/K/Q is worth 1.5/1.0/0.5 tricks respectively.
184///
185/// This evaluator calls [`nltc`] for each suit.
186pub const NLTC: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(nltc);
187
188/// [Zar points][zar], an evaluation by by Zar Petkov
189///
190/// [zar]: https://en.wikipedia.org/wiki/Zar_Points
191pub fn zar<T: From<u8>>(hand: Hand) -> T {
192    let holdings = Suit::ASC.map(|s| hand[s]);
193    let mut lengths = holdings.map(Holding::len);
194    lengths.sort_unstable();
195
196    // SAFETY: the lengths are at most 13, so the cast is safe.
197    #[allow(clippy::cast_possible_truncation)]
198    let sum = (lengths[3] + lengths[2]) as u8;
199
200    // SAFETY: `lengths` is already sorted, so the result is non-negative.
201    #[allow(clippy::cast_possible_truncation)]
202    let diff = (lengths[3] - lengths[0]) as u8;
203
204    let honors: u8 = holdings
205        .into_iter()
206        .map(|holding| {
207            let [a, k, q, j] = [Rank::A, Rank::K, Rank::Q, Rank::J].map(|r| holding.contains(r));
208            let count = 6 * u8::from(a) + 4 * u8::from(k) + 2 * u8::from(q) + u8::from(j);
209            let waste = match holding.len() {
210                1 => k || q || j,
211                2 => q || j,
212                _ => false,
213            };
214            count - u8::from(waste)
215        })
216        .sum();
217
218    T::from(honors + sum + diff)
219}
220
221/// Per-suit contribution to [`cccc`] in centipoints, including shortness
222fn cccc_suit(holding: Holding) -> i32 {
223    // SAFETY: a holding has at most 13 cards.
224    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
225    let len = holding.len() as i32;
226    let [a, k, q, j, t] =
227        [Rank::A, Rank::K, Rank::Q, Rank::J, Rank::T].map(|r| holding.contains(r));
228    let nine = holding.contains(Rank::new(9));
229    let eight = holding.contains(Rank::new(8));
230    let akq = i32::from(a) + i32::from(k) + i32::from(q);
231    let akqj = akq + i32::from(j);
232
233    // Suit quality (steps 1–12)
234    let mut quality = 400 * i32::from(a)
235        + 300 * i32::from(k)
236        + 200 * i32::from(q)
237        + 100 * i32::from(j)
238        + 50 * i32::from(t);
239
240    if (2..=6).contains(&len) {
241        quality += 50 * i32::from(t && (j || akq >= 2));
242        quality += 50 * i32::from(nine && (eight || t || akqj == 2));
243    }
244    quality += 50 * i32::from((4..=6).contains(&len) && nine && !eight && !t && akqj == 3);
245    quality += 100 * i32::from(len >= 7 && !(q && j));
246    quality += 100 * i32::from(len >= 8 && !q);
247    quality += 100 * i32::from(len >= 9 && !q && !j);
248
249    // Body and defensive values (steps 13–23)
250    let body = 300 * i32::from(a)
251        + if k {
252            if len >= 2 { 200 } else { 50 }
253        } else {
254            0
255        }
256        + if q {
257            match (len, a || k) {
258                (..=1, _) => 0,
259                (2, true) => 50,
260                (2, false) => 25,
261                (_, true) => 100,
262                (_, false) => 75,
263            }
264        } else {
265            0
266        }
267        + if j {
268            match akq {
269                2 => 50,
270                1 => 25,
271                _ => 0,
272            }
273        } else {
274            0
275        }
276        + 25 * i32::from(t && (akqj == 2 || (akqj == 1 && nine)));
277
278    // Shortness (steps 24–26)
279    let shortness = match len {
280        0 => 300,
281        1 => 200,
282        2 => 100,
283        _ => 0,
284    };
285
286    quality * len / 10 + body + shortness
287}
288
289/// [Kaplan–Rubens CCCC][knr] ("Four C's") hand evaluation
290///
291/// This is the algorithmic evaluation by Edgar Kaplan and Jeff Rubens from
292/// *The Bridge World*, October 1982, implemented after [the 26-step
293/// description reproduced by Richard Pavlicek][knr].  Scores range from
294/// &minus;0.50 to 35.60 in steps of 0.05 on a scale comparable to HCP
295/// (mean 10.77).
296///
297/// CCCC weighs honor placement together with shape, which makes it
298/// particularly accurate for suit contracts.  Prefer [`FIFTHS`] when
299/// evaluating toward notrump, especially 3NT.
300///
301/// [knr]: https://www.rpbridge.net/8j19.htm
302#[must_use]
303pub fn cccc(hand: Hand) -> f64 {
304    let suits: i32 = Suit::ASC.into_iter().map(|s| cccc_suit(hand[s])).sum();
305    let flat = Suit::ASC.into_iter().all(|s| hand[s].len() >= 3);
306    let total = suits - 100 + 50 * i32::from(flat);
307    debug_assert_eq!(total.rem_euclid(5), 0);
308    f64::from(total) / 100.0
309}