Skip to main content

contract_bridge/
eval.rs

1//! Hand evaluation: HCP, shortness, Fifths, BUM-RAP, losing trick count, NLTC,
2//! and Zar points.
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.
10
11use crate::{Hand, Holding, Rank, Suit};
12use core::cmp::Ord;
13use core::iter::Sum;
14
15/// Trait for hand evaluators
16pub trait HandEvaluator<T> {
17    /// Evaluate a hand
18    #[must_use]
19    fn eval(&self, hand: Hand) -> T;
20
21    /// Evaluate a pair
22    #[must_use]
23    fn eval_pair(&self, pair: [Hand; 2]) -> T
24    where
25        T: core::ops::Add<Output = T>,
26    {
27        self.eval(pair[0]) + self.eval(pair[1])
28    }
29}
30
31/// Functions are natural evaluators
32impl<F: Fn(Hand) -> T, T> HandEvaluator<T> for F {
33    fn eval(&self, hand: Hand) -> T {
34        self(hand)
35    }
36}
37
38/// Evaluator summing values of suit holdings
39#[derive(Debug)]
40pub struct SimpleEvaluator<T: Sum, F: Fn(Holding) -> T>(
41    /// The per-suit kernel: invoked once per holding, its results are summed.
42    pub F,
43);
44
45impl<T: Sum, F: Fn(Holding) -> T> HandEvaluator<T> for SimpleEvaluator<T, F> {
46    fn eval(&self, hand: Hand) -> T {
47        Suit::ASC.into_iter().map(|s| (self.0)(hand[s])).sum()
48    }
49}
50
51impl<T: Sum, F: Clone + Fn(Holding) -> T> Clone for SimpleEvaluator<T, F> {
52    fn clone(&self) -> Self {
53        Self(self.0.clone())
54    }
55}
56
57impl<T: Sum, F: Copy + Fn(Holding) -> T> Copy for SimpleEvaluator<T, F> {}
58
59/// High card points
60///
61/// This is the well-known 4-3-2-1 point count by Milton Work.
62#[must_use]
63pub fn hcp<T: From<u8>>(holding: Holding) -> T {
64    T::from(
65        4 * u8::from(holding.contains(Rank::A))
66            + 3 * u8::from(holding.contains(Rank::K))
67            + 2 * u8::from(holding.contains(Rank::Q))
68            + u8::from(holding.contains(Rank::J)),
69    )
70}
71
72/// Short suit points
73#[must_use]
74// SAFETY: the integer to cast is in 0..=3, so the cast is safe.
75#[allow(clippy::cast_possible_truncation)]
76pub fn shortness<T: From<u8>>(holding: Holding) -> T {
77    T::from(3 - holding.len().min(3) as u8)
78}
79
80/// The [Fifths] evaluator for 3NT
81///
82/// This function is the kernel of [`FIFTHS`].
83///
84/// [Fifths]: https://bridge.thomasoandrews.com/valuations/cardvaluesfor3nt.html
85#[must_use]
86pub fn fifths(holding: Holding) -> f64 {
87    f64::from(
88        40 * i32::from(holding.contains(Rank::A))
89            + 28 * i32::from(holding.contains(Rank::K))
90            + 18 * i32::from(holding.contains(Rank::Q))
91            + 10 * i32::from(holding.contains(Rank::J))
92            + 4 * i32::from(holding.contains(Rank::T)),
93    ) / 10.0
94}
95
96/// The BUM-RAP evaluator
97///
98/// This function is the kernel of [`BUMRAP`].
99#[must_use]
100pub fn bumrap(holding: Holding) -> f64 {
101    f64::from(
102        18 * i32::from(holding.contains(Rank::A))
103            + 12 * i32::from(holding.contains(Rank::K))
104            + 6 * i32::from(holding.contains(Rank::Q))
105            + 3 * i32::from(holding.contains(Rank::J))
106            + i32::from(holding.contains(Rank::T)),
107    ) * 0.25
108}
109
110/// Plain old losing trick count
111#[must_use]
112pub fn ltc<T: From<u8>>(holding: Holding) -> T {
113    let len = holding.len();
114
115    T::from(
116        u8::from(len >= 1 && !holding.contains(Rank::A))
117            + u8::from(len >= 2 && !holding.contains(Rank::K))
118            + u8::from(len >= 3 && !holding.contains(Rank::Q)),
119    )
120}
121
122/// New Losing Trick Count
123///
124/// This function is the kernel of [`NLTC`].
125#[must_use]
126pub fn nltc(holding: Holding) -> f64 {
127    let len = holding.len();
128
129    f64::from(
130        3 * i32::from(len >= 1 && !holding.contains(Rank::A))
131            + 2 * i32::from(len >= 2 && !holding.contains(Rank::K))
132            + i32::from(len >= 3 && !holding.contains(Rank::Q)),
133    ) * 0.5
134}
135
136/// High card points plus useful shortness
137///
138/// For each suit, we count max([HCP][hcp], shortness, HCP + shortness &minus; 1).
139/// This method avoids double counting of short honors.  This evaluator is
140/// particularly useful for suit contracts.
141#[must_use]
142pub fn hcp_plus<T: From<u8>>(holding: Holding) -> T {
143    let count: u8 = hcp(holding);
144    let short: u8 = shortness(holding);
145
146    T::from(if count > 0 && short > 0 {
147        count + short - 1
148    } else {
149        count.max(short)
150    })
151}
152
153/// The [Fifths] evaluator for 3NT
154///
155/// This is Thomas Andrews's computed point count for 3NT.  This evaluator calls
156/// [`fifths`] for each suit.
157///
158/// [Fifths]: https://bridge.thomasoandrews.com/valuations/cardvaluesfor3nt.html
159pub const FIFTHS: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(fifths);
160
161/// The BUM-RAP evaluator
162///
163/// This is the BUM-RAP point count (4.5-3-1.5-0.75-0.25).  This evaluator calls
164/// [`bumrap`] for each suit.
165pub const BUMRAP: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(bumrap);
166
167/// BUM-RAP with shortness
168///
169/// For each suit, we count max([BUM-RAP][BUMRAP], shortness, BUM-RAP +
170/// shortness &minus; 1).  This method avoids double counting of short honors.
171/// This evaluator is particularly useful for suit contracts.
172pub const BUMRAP_PLUS: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(|x| {
173    let b: f64 = bumrap(x);
174    let s: f64 = shortness(x);
175    b.max(s).max(b + s - 1.0)
176});
177
178/// New Losing Trick Count
179///
180/// [NLTC](https://en.wikipedia.org/wiki/Losing-Trick_Count#New_Losing-Trick_Count_(NLTC))
181/// is a variant of losing trick count that gives different weights to missing
182/// honors.  A missing A/K/Q is worth 1.5/1.0/0.5 tricks respectively.
183///
184/// This evaluator calls [`nltc`] for each suit.
185pub const NLTC: SimpleEvaluator<f64, fn(Holding) -> f64> = SimpleEvaluator(nltc);
186
187/// [Zar points][zar], an evaluation by by Zar Petkov
188///
189/// [zar]: https://en.wikipedia.org/wiki/Zar_Points
190pub fn zar<T: From<u8>>(hand: Hand) -> T {
191    let holdings = Suit::ASC.map(|s| hand[s]);
192    let mut lengths = holdings.map(Holding::len);
193    lengths.sort_unstable();
194
195    // SAFETY: the lengths are at most 13, so the cast is safe.
196    #[allow(clippy::cast_possible_truncation)]
197    let sum = (lengths[3] + lengths[2]) as u8;
198
199    // SAFETY: `lengths` is already sorted, so the result is non-negative.
200    #[allow(clippy::cast_possible_truncation)]
201    let diff = (lengths[3] - lengths[0]) as u8;
202
203    let honors: u8 = holdings
204        .into_iter()
205        .map(|holding| {
206            let [a, k, q, j] = [Rank::A, Rank::K, Rank::Q, Rank::J].map(|r| holding.contains(r));
207            let count = 6 * u8::from(a) + 4 * u8::from(k) + 2 * u8::from(q) + u8::from(j);
208            let waste = match holding.len() {
209                1 => k || q || j,
210                2 => q || j,
211                _ => false,
212            };
213            count - u8::from(waste)
214        })
215        .sum();
216
217    T::from(honors + sum + diff)
218}