Skip to main content

encounter/
scoring.rs

1//! Scoring traits and scored affordance types for encounter resolution.
2//!
3//! This module provides [`ScoredAffordance`], [`AcceptanceEval`], and
4//! [`ActionScorer`] — the core interfaces consumers implement to plug in
5//! utility scoring and acceptance logic.
6
7use crate::affordance::CatalogEntry;
8use std::collections::HashMap;
9
10/// An affordance that has been sifted and scored. Input to resolution protocols.
11/// Generic over precondition type P (from `CatalogEntry<P>`). Default is String.
12#[derive(Debug, Clone)]
13pub struct ScoredAffordance<P = String> {
14    /// The catalog entry being scored.
15    pub entry: CatalogEntry<P>,
16    /// The computed utility score for this affordance.
17    pub score: f64,
18    /// Resolved slot bindings for this affordance instance.
19    pub bindings: HashMap<String, String>,
20}
21
22/// Evaluates whether a responder accepts an action.
23///
24/// Generic over `P` to match `ScoredAffordance<P>`. Consumers that need to
25/// share an `AcceptanceEval` across threads should add `+ Send + Sync` at
26/// the use site (`&(dyn AcceptanceEval<P> + Send + Sync)`); the trait itself
27/// does not require it, so single-threaded scorers holding `Rc<T>` /
28/// `Cell<T>` / `RefCell<T>` are fine.
29pub trait AcceptanceEval<P = String> {
30    /// Returns true if the responder accepts the given scored action.
31    fn evaluate(&self, responder: &str, action: &ScoredAffordance<P>) -> bool;
32}
33
34/// Scores available affordances for an actor.
35///
36/// Same threading note as [`AcceptanceEval`]: the trait does not require
37/// `Send + Sync`; add the bound at the use site if cross-thread sharing is
38/// needed.
39pub trait ActionScorer<P = String> {
40    /// Returns a scored and ordered list of affordances available to the actor.
41    fn score_actions(
42        &self,
43        actor: &str,
44        available: &[CatalogEntry<P>],
45        participants: &[String],
46    ) -> Vec<ScoredAffordance<P>>;
47}
48
49/// Test helper: always accepts any action for any responder.
50pub struct AlwaysAccept;
51
52impl<P> AcceptanceEval<P> for AlwaysAccept {
53    fn evaluate(&self, _responder: &str, _action: &ScoredAffordance<P>) -> bool {
54        true
55    }
56}
57
58/// Test helper: always rejects any action for any responder.
59pub struct AlwaysReject;
60
61impl<P> AcceptanceEval<P> for AlwaysReject {
62    fn evaluate(&self, _responder: &str, _action: &ScoredAffordance<P>) -> bool {
63        false
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::affordance::AffordanceSpec;
71
72    fn test_scored(name: &str, score: f64) -> ScoredAffordance<String> {
73        let spec = AffordanceSpec {
74            name: name.to_string(),
75            domain: "test".to_string(),
76            bindings: vec![],
77            considerations: vec![],
78            effects_on_accept: vec![],
79            effects_on_reject: vec![],
80            drive_alignment: vec![],
81        };
82        let entry = CatalogEntry {
83            spec,
84            precondition: String::new(),
85        };
86        ScoredAffordance {
87            entry,
88            score,
89            bindings: HashMap::new(),
90        }
91    }
92
93    #[test]
94    fn always_accept_accepts() {
95        let eval = AlwaysAccept;
96        let action = test_scored("greet", 0.9);
97        assert!(eval.evaluate("alice", &action));
98    }
99
100    #[test]
101    fn always_reject_rejects() {
102        let eval = AlwaysReject;
103        let action = test_scored("threaten", 0.5);
104        assert!(!eval.evaluate("bob", &action));
105    }
106
107    #[test]
108    fn always_accept_works_with_unit_precondition() {
109        let spec = AffordanceSpec {
110            name: "wave".to_string(),
111            domain: "social".to_string(),
112            bindings: vec![],
113            considerations: vec![],
114            effects_on_accept: vec![],
115            effects_on_reject: vec![],
116            drive_alignment: vec![],
117        };
118        let entry: CatalogEntry<()> = CatalogEntry {
119            spec,
120            precondition: (),
121        };
122        let action: ScoredAffordance<()> = ScoredAffordance {
123            entry,
124            score: 1.0,
125            bindings: HashMap::new(),
126        };
127        let eval = AlwaysAccept;
128        assert!(eval.evaluate("carol", &action));
129    }
130}