use super::Map;
use super::array::Logits;
use super::constraint::{Constraint, Description};
use super::context::Context;
use super::inference::Inference;
use super::trie::Classifier;
use contract_bridge::Hand;
use contract_bridge::auction::Call;
use core::fmt;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Alert(pub &'static str);
#[derive(Clone)]
pub struct Rule {
call: Call,
weight: f32,
when: Arc<dyn Constraint>,
label: &'static str,
alert: Option<Alert>,
}
impl Rule {
#[must_use]
pub const fn call(&self) -> Call {
self.call
}
#[must_use]
pub const fn weight(&self) -> f32 {
self.weight
}
#[must_use]
pub const fn label(&self) -> &'static str {
self.label
}
#[must_use]
pub const fn alert(&self) -> Option<Alert> {
self.alert
}
#[must_use]
pub fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
self.weight + self.when.eval(hand, context)
}
#[must_use]
pub fn describe(&self) -> Description {
self.when.describe()
}
#[must_use]
pub fn project(&self, context: &Context<'_>) -> Inference {
self.when.project(context)
}
}
impl fmt::Debug for Rule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rule")
.field("call", &self.call)
.field("weight", &self.weight)
.field("label", &self.label)
.field("alert", &self.alert)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Default)]
pub struct Rules {
rules: Vec<Rule>,
}
impl Rules {
#[must_use]
pub const fn new() -> Self {
Self { rules: Vec::new() }
}
#[must_use]
pub fn rule(
mut self,
call: impl Into<Call>,
weight: f32,
when: impl Constraint + 'static,
) -> Self {
self.rules.push(Rule {
call: call.into(),
weight,
when: Arc::new(when),
label: "",
alert: None,
});
self
}
#[must_use]
pub fn note(mut self, label: &'static str) -> Self {
self.rules
.last_mut()
.expect("note() requires a preceding rule()")
.label = label;
self
}
#[must_use]
pub fn alert(mut self, alert: Alert) -> Self {
self.rules
.last_mut()
.expect("alert() requires a preceding rule()")
.alert = Some(alert);
self
}
#[must_use]
pub fn chain(mut self, other: Rules) -> Self {
self.rules.extend(other.rules);
self
}
#[must_use]
pub fn gated(mut self, active: impl Fn(Alert) -> bool) -> Self {
self.rules.retain(|rule| rule.alert.is_none_or(&active));
self
}
#[must_use]
pub fn rules(&self) -> &[Rule] {
&self.rules
}
#[must_use]
pub fn explain(&self, hand: Hand, context: &Context<'_>) -> Map<(usize, f32)> {
let mut best = Map::new();
for (index, rule) in self.rules.iter().enumerate() {
let logit = rule.eval(hand, context);
let entry = best.entry(rule.call);
if logit > f32::NEG_INFINITY && entry.is_none_or(|(_, incumbent)| logit > incumbent) {
entry.replace((index, logit));
}
}
best
}
}
impl Classifier for Rules {
fn classify(&self, hand: Hand, context: &Context<'_>) -> Logits {
let mut logits = Logits::new();
for rule in &self.rules {
let slot = logits.0.get_mut(rule.call);
*slot = slot.max(rule.eval(hand, context));
}
logits
}
fn as_rules(&self) -> Option<&Rules> {
Some(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::constraint::{balanced, hcp, len};
use contract_bridge::auction::RelativeVulnerability;
use contract_bridge::{Bid, Strain, Suit};
fn opening_rules() -> Rules {
Rules::new()
.rule(Bid::new(1, Strain::Notrump), 1.0, hcp(15..=17) & balanced())
.rule(
Bid::new(1, Strain::Spades),
1.0,
hcp(11..=21) & len(Suit::Spades, 5..),
)
.rule(Call::Pass, 0.0, hcp(..11))
}
fn best_call(logits: &Logits) -> Call {
(&logits.0)
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("array is never empty")
}
#[test]
fn test_classification() {
let rules = opening_rules();
let context = Context::new(RelativeVulnerability::NONE, &[]);
let notrump = "AKQ2.K53.QJ4.T92".parse().expect("valid hand");
assert_eq!(
best_call(&rules.classify(notrump, &context)),
Call::Bid(Bid::new(1, Strain::Notrump)),
);
let spades = "AKQ32.K532.QJ4.9".parse().expect("valid hand");
assert_eq!(
best_call(&rules.classify(spades, &context)),
Call::Bid(Bid::new(1, Strain::Spades)),
);
let weak = "98432.K53.QJ4.92".parse().expect("valid hand");
assert_eq!(best_call(&rules.classify(weak, &context)), Call::Pass);
}
#[test]
fn test_note_labels_last_rule_and_downcasts() {
let rules = Rules::new()
.rule(Bid::new(1, Strain::Notrump), 1.0, hcp(15..=17) & balanced())
.note("15-17 BAL")
.rule(Call::Pass, 0.0, hcp(..11));
assert_eq!(rules.rules()[0].label(), "15-17 BAL");
assert_eq!(rules.rules()[1].label(), "");
let erased: &dyn Classifier = &rules;
let recovered = erased.as_rules().expect("Rules downcasts to itself");
assert_eq!(recovered.rules().len(), 2);
assert_eq!(recovered.rules()[0].label(), "15-17 BAL");
}
#[test]
fn test_alert_marks_block_and_gated_filters() {
const PUPPET: Alert = Alert("puppet");
const EUROPEAN: Alert = Alert("european");
let rules = Rules::new()
.rule(Call::Pass, 0.0, hcp(..8))
.chain(
Rules::new()
.rule(Bid::new(3, Strain::Clubs), 1.0, hcp(9..))
.alert(PUPPET),
)
.chain(
Rules::new()
.rule(Bid::new(3, Strain::Clubs), 1.0, hcp(9..))
.alert(EUROPEAN),
);
assert_eq!(rules.rules()[0].alert(), None);
assert_eq!(rules.rules()[1].alert(), Some(PUPPET));
assert_eq!(rules.rules()[2].alert(), Some(EUROPEAN));
let puppet = rules.clone().gated(|alert| alert == PUPPET);
assert_eq!(puppet.rules().len(), 2);
assert_eq!(puppet.rules()[0].alert(), None);
assert_eq!(puppet.rules()[1].alert(), Some(PUPPET));
let european = rules.gated(|alert| alert == EUROPEAN);
assert_eq!(european.rules().len(), 2);
assert_eq!(european.rules()[1].alert(), Some(EUROPEAN));
}
#[test]
fn test_explain() {
let rules = opening_rules();
let context = Context::new(RelativeVulnerability::NONE, &[]);
let hand = "AKQ32.K532.QJ4.9".parse().expect("valid hand");
let explanation = rules.explain(hand, &context);
let spades = Call::Bid(Bid::new(1, Strain::Spades));
assert_eq!(explanation.get(spades), Some(&(1, 1.0)));
assert_eq!(explanation.get(Call::Pass), None);
assert_eq!(explanation.get(Call::Double), None);
}
}