1use crate::affordance_key::AffordanceKey;
21use crate::state::EncounterArgumentationState;
22use encounter::affordance::CatalogEntry;
23use encounter::scoring::{ActionScorer, ScoredAffordance};
24
25pub struct StateActionScorer<'a, S> {
28 state: &'a EncounterArgumentationState,
29 inner: S,
30 boost: f64,
31}
32
33impl<'a, S> StateActionScorer<'a, S> {
34 #[must_use]
38 pub fn new(state: &'a EncounterArgumentationState, inner: S, boost: f64) -> Self {
39 Self { state, inner, boost }
40 }
41}
42
43impl<S, P> ActionScorer<P> for StateActionScorer<'_, S>
44where
45 S: ActionScorer<P>,
46 P: Clone,
47{
48 fn score_actions(
49 &self,
50 actor: &str,
51 available: &[CatalogEntry<P>],
52 participants: &[String],
53 ) -> Vec<ScoredAffordance<P>> {
54 let mut scored = self.inner.score_actions(actor, available, participants);
55 for sa in &mut scored {
56 let key = AffordanceKey::new(actor, &sa.entry.spec.name, &sa.bindings);
57 let Some(id) = self.state.argument_id_for(&key) else {
58 continue;
59 };
60 match self.state.is_credulously_accepted(&id) {
61 Ok(true) => sa.score += self.boost,
62 Ok(false) => {}
63 Err(e) => {
64 self.state.record_error(e);
65 }
66 }
67 }
68 scored
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use argumentation_schemes::catalog::default_catalog;
76 use encounter::affordance::AffordanceSpec;
77 use std::collections::HashMap;
78
79 struct FlatInner;
80 impl<P: Clone> ActionScorer<P> for FlatInner {
81 fn score_actions(
82 &self,
83 _actor: &str,
84 available: &[CatalogEntry<P>],
85 _participants: &[String],
86 ) -> Vec<ScoredAffordance<P>> {
87 available
88 .iter()
89 .map(|e| {
90 let mut bindings = HashMap::new();
91 bindings.insert("self".to_string(), "alice".to_string());
92 bindings.insert("expert".to_string(), "alice".to_string());
93 bindings.insert("domain".to_string(), "military".to_string());
94 bindings.insert("claim".to_string(), "fortify_east".to_string());
95 ScoredAffordance {
96 entry: e.clone(),
97 score: 1.0,
98 bindings,
99 }
100 })
101 .collect()
102 }
103 }
104
105 fn catalog() -> Vec<CatalogEntry<String>> {
106 let spec = AffordanceSpec {
107 name: "argue_fortify_east".to_string(),
108 domain: "persuasion".to_string(),
109 bindings: vec![
110 "self".to_string(),
111 "expert".to_string(),
112 "domain".to_string(),
113 "claim".to_string(),
114 ],
115 considerations: Vec::new(),
116 effects_on_accept: Vec::new(),
117 effects_on_reject: Vec::new(),
118 drive_alignment: Vec::new(),
119 };
120 vec![CatalogEntry { spec, precondition: String::new() }]
121 }
122
123 #[test]
124 fn unboosted_when_no_argument_is_seeded() {
125 let state = EncounterArgumentationState::new(default_catalog());
126 let scorer = StateActionScorer::new(&state, FlatInner, 0.5);
127 let catalog_vec = catalog();
128 let scored = scorer.score_actions("alice", &catalog_vec, &["alice".to_string()]);
129 assert_eq!(scored.len(), 1);
130 assert!((scored[0].score - 1.0).abs() < 1e-9);
131 }
132
133 #[test]
134 fn boosted_when_argument_is_credulously_accepted() {
135 let registry = default_catalog();
136 let scheme = registry.by_key("argument_from_expert_opinion").unwrap();
137 let mut b = HashMap::new();
138 b.insert("expert".to_string(), "alice".to_string());
139 b.insert("domain".to_string(), "military".to_string());
140 b.insert("claim".to_string(), "fortify_east".to_string());
141 let instance = scheme.instantiate(&b).unwrap();
142 let mut state = EncounterArgumentationState::new(registry);
143 let mut affordance_b = b.clone();
144 affordance_b.insert("self".to_string(), "alice".to_string());
145 state.add_scheme_instance_for_affordance(
146 "alice",
147 "argue_fortify_east",
148 &affordance_b,
149 instance,
150 );
151
152 let scorer = StateActionScorer::new(&state, FlatInner, 0.5);
153 let catalog_vec = catalog();
154 let scored = scorer.score_actions("alice", &catalog_vec, &["alice".to_string()]);
155 assert_eq!(scored.len(), 1);
156 assert!(
158 (scored[0].score - 1.5).abs() < 1e-9,
159 "expected 1.5, got {}",
160 scored[0].score
161 );
162 }
163
164 #[test]
165 fn not_boosted_when_argument_is_attacked_above_budget() {
166 use crate::arg_id::ArgumentId;
170
171 let registry = default_catalog();
172 let scheme = registry.by_key("argument_from_expert_opinion").unwrap();
173 let mut b = HashMap::new();
174 b.insert("expert".to_string(), "alice".to_string());
175 b.insert("domain".to_string(), "military".to_string());
176 b.insert("claim".to_string(), "fortify_east".to_string());
177 let instance = scheme.instantiate(&b).unwrap();
178
179 let mut state = EncounterArgumentationState::new(registry);
180 let mut affordance_b = b.clone();
181 affordance_b.insert("self".to_string(), "alice".to_string());
182 let alice_id = state.add_scheme_instance_for_affordance(
183 "alice",
184 "argue_fortify_east",
185 &affordance_b,
186 instance,
187 );
188 state
190 .add_weighted_attack(&ArgumentId::new("attacker"), &alice_id, 0.8)
191 .unwrap();
192
193 let scorer = StateActionScorer::new(&state, FlatInner, 0.5);
194 let catalog_vec = catalog();
195 let scored = scorer.score_actions("alice", &catalog_vec, &["alice".to_string()]);
196 assert_eq!(scored.len(), 1);
197 assert!(
199 (scored[0].score - 1.0).abs() < 1e-9,
200 "expected 1.0 (no boost), got {}",
201 scored[0].score
202 );
203 }
204}