Skip to main content

argumentation_weighted/
weight_source.rs

1//! `WeightSource` trait for computing attack weights from external
2//! state (relationship metadata, personality traits, etc.).
3//!
4//! This is a deliberately minimal abstraction for v0.1.0 — it exists
5//! so consumers like the future `encounter` crate can pass a policy
6//! closure for deriving weights, without coupling this crate to any
7//! specific narrative-stack types. The full `encounter`-specific
8//! integration lives in whatever bridge crate the narrative team
9//! ships; this trait is just the hook.
10
11use crate::error::Error;
12use crate::framework::WeightedFramework;
13use std::hash::Hash;
14
15/// A source of attack weights. Given an attacker and a target (and
16/// whatever context `Self` carries), produce the weight for the
17/// corresponding attack edge.
18///
19/// Implementations might read participant relationship metadata,
20/// personality compatibility, recent interaction history, or any other
21/// external state. The trait itself is stateless from this crate's
22/// perspective.
23pub trait WeightSource<A> {
24    /// Compute the weight for an attack from `attacker` to `target`.
25    /// Returns `None` if this source has no opinion (i.e., the attack
26    /// should not be added). Returns `Some(w)` otherwise.
27    fn weight_for(&self, attacker: &A, target: &A) -> Option<f64>;
28}
29
30/// A closure-based `WeightSource` that wraps any `Fn(&A, &A) -> Option<f64>`.
31pub struct ClosureWeightSource<F>(pub F);
32
33impl<A, F> WeightSource<A> for ClosureWeightSource<F>
34where
35    F: Fn(&A, &A) -> Option<f64>,
36{
37    fn weight_for(&self, attacker: &A, target: &A) -> Option<f64> {
38        (self.0)(attacker, target)
39    }
40}
41
42/// Populate a `WeightedFramework` from a list of attack pairs, pulling
43/// each weight from the provided `WeightSource`. Pairs for which the
44/// source returns `None` are skipped. Pairs for which the source
45/// returns an invalid weight propagate an [`Error::InvalidWeight`].
46///
47/// This is a convenience builder. Consumers that need more control
48/// (e.g., different sources for different attack types) should call
49/// `add_weighted_attack` directly.
50pub fn populate_from_source<A, W, I>(
51    framework: &mut WeightedFramework<A>,
52    pairs: I,
53    source: &W,
54) -> Result<(), Error>
55where
56    A: Clone + Eq + Hash,
57    W: WeightSource<A>,
58    I: IntoIterator<Item = (A, A)>,
59{
60    for (attacker, target) in pairs {
61        if let Some(weight) = source.weight_for(&attacker, &target) {
62            framework.add_weighted_attack(attacker, target, weight)?;
63        }
64    }
65    Ok(())
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    struct FixedSource(f64);
73
74    impl WeightSource<&'static str> for FixedSource {
75        fn weight_for(&self, _attacker: &&'static str, _target: &&'static str) -> Option<f64> {
76            Some(self.0)
77        }
78    }
79
80    #[test]
81    fn closure_weight_source_returns_closure_output() {
82        let src = ClosureWeightSource(|_a: &&str, _b: &&str| Some(0.42));
83        assert_eq!(src.weight_for(&"x", &"y"), Some(0.42));
84    }
85
86    #[test]
87    fn populate_from_source_adds_all_attacks() {
88        let mut wf: WeightedFramework<&str> = WeightedFramework::new();
89        let src = FixedSource(0.5);
90        populate_from_source(&mut wf, vec![("a", "b"), ("c", "d")], &src).unwrap();
91        assert_eq!(wf.attack_count(), 2);
92    }
93
94    #[test]
95    fn populate_skips_none_weights() {
96        let src = ClosureWeightSource(
97            |_a: &&str, target: &&str| if *target == "b" { Some(0.5) } else { None },
98        );
99        let mut wf: WeightedFramework<&str> = WeightedFramework::new();
100        populate_from_source(&mut wf, vec![("x", "b"), ("x", "c")], &src).unwrap();
101        assert_eq!(wf.attack_count(), 1);
102    }
103
104    #[test]
105    fn populate_propagates_invalid_weights() {
106        let src = ClosureWeightSource(|_a: &&str, _b: &&str| Some(-1.0));
107        let mut wf: WeightedFramework<&str> = WeightedFramework::new();
108        let err = populate_from_source(&mut wf, vec![("x", "y")], &src).unwrap_err();
109        assert!(matches!(err, Error::InvalidWeight { .. }));
110    }
111}