encounter_argumentation/
value_scorer.rs1use crate::state::EncounterArgumentationState;
25use encounter::affordance::CatalogEntry;
26use encounter::scoring::{ActionScorer, ScoredAffordance};
27
28pub struct ValueAwareScorer<'a, S> {
31 inner: S,
32 state: &'a EncounterArgumentationState,
33 max_boost: f64,
34}
35
36impl<'a, S> ValueAwareScorer<'a, S> {
37 pub fn new(inner: S, state: &'a EncounterArgumentationState, max_boost: f64) -> Self {
46 Self {
47 inner,
48 state,
49 max_boost,
50 }
51 }
52}
53
54impl<'a, S, P> ActionScorer<P> for ValueAwareScorer<'a, S>
55where
56 S: ActionScorer<P>,
57 P: Clone,
58{
59 fn score_actions(
60 &self,
61 actor: &str,
62 available: &[CatalogEntry<P>],
63 participants: &[String],
64 ) -> Vec<ScoredAffordance<P>> {
65 let mut scored = self.inner.score_actions(actor, available, participants);
66
67 let Some(audience) = self.state.audience_for(actor) else {
68 return scored;
70 };
71
72 for sa in &mut scored {
74 sa.score += value_boost_for_affordance(
75 &sa.bindings,
76 &audience,
77 self.max_boost,
78 );
79 }
80 scored.sort_by(|a, b| {
81 b.score
82 .partial_cmp(&a.score)
83 .unwrap_or(std::cmp::Ordering::Equal)
84 });
85 scored
86 }
87}
88
89fn value_boost_for_affordance(
98 bindings: &std::collections::HashMap<String, String>,
99 audience: &argumentation_values::Audience,
100 max_boost: f64,
101) -> f64 {
102 let Some(value_str) = bindings.get("value") else {
103 return 0.0;
104 };
105 let value = argumentation_values::Value::new(value_str.clone());
106 let tier_count = audience.tier_count();
107 if tier_count == 0 {
108 return 0.0;
109 }
110 let Some(rank) = audience.rank(&value) else {
111 return 0.0;
112 };
113 let normalised = (tier_count - rank) as f64 / tier_count as f64;
114 max_boost * normalised
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use argumentation_schemes::catalog::default_catalog;
121 use argumentation_values::{Audience, Value};
122 use encounter::affordance::AffordanceSpec;
123 use encounter::scoring::ScoredAffordance;
124 use std::collections::HashMap;
125
126 struct StubScorer {
129 value_promoted: String,
130 }
131
132 impl<P: Clone> ActionScorer<P> for StubScorer {
133 fn score_actions(
134 &self,
135 actor: &str,
136 available: &[CatalogEntry<P>],
137 _participants: &[String],
138 ) -> Vec<ScoredAffordance<P>> {
139 available
140 .iter()
141 .map(|entry| {
142 let mut bindings = HashMap::new();
143 bindings.insert("self".into(), actor.into());
144 bindings.insert("value".into(), self.value_promoted.clone());
145 ScoredAffordance {
146 entry: entry.clone(),
147 score: 1.0,
148 bindings,
149 }
150 })
151 .collect()
152 }
153 }
154
155 fn dummy_entry() -> CatalogEntry<()> {
156 CatalogEntry {
157 spec: AffordanceSpec {
158 name: "test_affordance".into(),
159 domain: "test".into(),
160 bindings: vec!["self".into(), "value".into()],
161 considerations: Vec::new(),
162 effects_on_accept: Vec::new(),
163 effects_on_reject: Vec::new(),
164 drive_alignment: Vec::new(),
165 },
166 precondition: (),
167 }
168 }
169
170 #[test]
171 fn no_audience_means_no_boost() {
172 let registry = default_catalog();
173 let state = EncounterArgumentationState::new(registry);
174 let inner = StubScorer { value_promoted: "honor".into() };
175 let scorer = ValueAwareScorer::new(inner, &state, 0.5);
176 let entries = vec![dummy_entry()];
177 let scored = scorer.score_actions("alice", &entries, &["alice".into()]);
178 assert_eq!(scored.len(), 1);
179 assert!((scored[0].score - 1.0).abs() < 1e-9);
180 }
181
182 #[test]
183 fn boost_proportional_to_tier_position() {
184 let registry = default_catalog();
185 let state = EncounterArgumentationState::new(registry);
186 state.set_audience(
187 "alice",
188 Audience::total([Value::new("honor"), Value::new("safety")]),
189 );
190 let inner = StubScorer { value_promoted: "honor".into() };
191 let scorer = ValueAwareScorer::new(inner, &state, 0.5);
192 let entries = vec![dummy_entry()];
193 let scored = scorer.score_actions("alice", &entries, &["alice".into()]);
194 assert!((scored[0].score - 1.5).abs() < 1e-9, "got {}", scored[0].score);
197 }
198
199 #[test]
200 fn unranked_value_gets_no_boost() {
201 let registry = default_catalog();
202 let state = EncounterArgumentationState::new(registry);
203 state.set_audience(
204 "alice",
205 Audience::total([Value::new("life")]),
206 );
207 let inner = StubScorer { value_promoted: "tradition".into() };
208 let scorer = ValueAwareScorer::new(inner, &state, 0.5);
209 let entries = vec![dummy_entry()];
210 let scored = scorer.score_actions("alice", &entries, &["alice".into()]);
211 assert!((scored[0].score - 1.0).abs() < 1e-9);
212 }
213
214 #[test]
215 fn lower_tier_gets_smaller_boost() {
216 let registry = default_catalog();
217 let state = EncounterArgumentationState::new(registry);
218 state.set_audience(
219 "alice",
220 Audience::total([
221 Value::new("honor"),
222 Value::new("safety"),
223 Value::new("comfort"),
224 ]),
225 );
226 let inner = StubScorer { value_promoted: "comfort".into() };
227 let scorer = ValueAwareScorer::new(inner, &state, 0.5);
228 let entries = vec![dummy_entry()];
229 let scored = scorer.score_actions("alice", &entries, &["alice".into()]);
230 assert!((scored[0].score - 1.1666666666).abs() < 1e-3, "got {}", scored[0].score);
233 }
234}