1use crate::affordance_key::AffordanceKey;
24use crate::state::EncounterArgumentationState;
25use encounter::scoring::{AcceptanceEval, ScoredAffordance};
26
27pub struct StateAcceptanceEval<'a> {
37 state: &'a EncounterArgumentationState,
38}
39
40impl<'a> StateAcceptanceEval<'a> {
41 #[must_use]
43 pub fn new(state: &'a EncounterArgumentationState) -> Self {
44 Self { state }
45 }
46
47 fn proposer_key<P>(&self, action: &ScoredAffordance<P>) -> Option<AffordanceKey> {
50 let proposer = action.bindings.get("self")?;
51 Some(AffordanceKey::new(
52 proposer,
53 &action.entry.spec.name,
54 &action.bindings,
55 ))
56 }
57}
58
59impl<P> AcceptanceEval<P> for StateAcceptanceEval<'_> {
60 fn evaluate(&self, responder: &str, action: &ScoredAffordance<P>) -> bool {
61 let Some(key) = self.proposer_key(action) else {
62 self.state.record_error(crate::error::Error::MissingProposerBinding {
63 affordance_name: action.entry.spec.name.clone(),
64 });
65 return true;
66 };
67 let Some(target) = self.state.argument_id_for(&key) else {
68 return true;
69 };
70 match self.state.has_accepted_counter_by(responder, &target) {
71 Ok(true) => false,
72 Ok(false) => true,
73 Err(e) => {
74 self.state.record_error(e);
75 true
76 }
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use argumentation_schemes::catalog::default_catalog;
85 use encounter::affordance::{AffordanceSpec, CatalogEntry};
86 use encounter::scoring::ScoredAffordance;
87 use std::collections::HashMap;
88
89 fn make_affordance(
90 name: &str,
91 self_actor: &str,
92 expert: &str,
93 domain: &str,
94 claim: &str,
95 ) -> ScoredAffordance<String> {
96 let spec = AffordanceSpec {
97 name: name.to_string(),
98 domain: "persuasion".to_string(),
99 bindings: vec![
100 "self".to_string(),
101 "expert".to_string(),
102 "domain".to_string(),
103 "claim".to_string(),
104 ],
105 considerations: Vec::new(),
106 effects_on_accept: Vec::new(),
107 effects_on_reject: Vec::new(),
108 drive_alignment: Vec::new(),
109 };
110 let entry = CatalogEntry { spec, precondition: String::new() };
111 let mut bindings = HashMap::new();
112 bindings.insert("self".to_string(), self_actor.to_string());
113 bindings.insert("expert".to_string(), expert.to_string());
114 bindings.insert("domain".to_string(), domain.to_string());
115 bindings.insert("claim".to_string(), claim.to_string());
116 ScoredAffordance { entry, score: 1.0, bindings }
117 }
118
119 #[test]
120 fn accepts_when_no_argument_is_seeded_for_the_affordance() {
121 let state = EncounterArgumentationState::new(default_catalog());
122 let eval = StateAcceptanceEval::new(&state);
123 let action = make_affordance("unseeded_action", "alice", "alice", "military", "x");
124 assert!(eval.evaluate("bob", &action));
125 }
126
127 #[test]
128 fn accepts_when_responder_has_no_counter() {
129 let registry = default_catalog();
130 let scheme = registry.by_key("argument_from_expert_opinion").unwrap();
131 let mut proposer_bindings = HashMap::new();
132 proposer_bindings.insert("expert".to_string(), "alice".to_string());
133 proposer_bindings.insert("domain".to_string(), "military".to_string());
134 proposer_bindings.insert("claim".to_string(), "fortify_east".to_string());
135 let instance = scheme.instantiate(&proposer_bindings).unwrap();
136
137 let mut state = EncounterArgumentationState::new(registry);
138 let mut affordance_bindings = proposer_bindings.clone();
139 affordance_bindings.insert("self".to_string(), "alice".to_string());
140 state.add_scheme_instance_for_affordance(
141 "alice",
142 "argue_fortify_east",
143 &affordance_bindings,
144 instance,
145 );
146 let eval = StateAcceptanceEval::new(&state);
147 let action = make_affordance(
148 "argue_fortify_east",
149 "alice",
150 "alice",
151 "military",
152 "fortify_east",
153 );
154 assert!(eval.evaluate("bob", &action));
155 }
156
157 #[test]
158 fn rejects_when_responder_asserts_an_accepted_counter() {
159 let registry = default_catalog();
160 let scheme = registry.by_key("argument_from_expert_opinion").unwrap();
161
162 let mut alice_bindings = HashMap::new();
163 alice_bindings.insert("expert".to_string(), "alice".to_string());
164 alice_bindings.insert("domain".to_string(), "military".to_string());
165 alice_bindings.insert("claim".to_string(), "fortify_east".to_string());
166 let alice_instance = scheme.instantiate(&alice_bindings).unwrap();
167
168 let mut bob_bindings = HashMap::new();
169 bob_bindings.insert("expert".to_string(), "bob".to_string());
170 bob_bindings.insert("domain".to_string(), "logistics".to_string());
171 bob_bindings.insert("claim".to_string(), "abandon_east".to_string());
172 let bob_instance = scheme.instantiate(&bob_bindings).unwrap();
173
174 let mut state = EncounterArgumentationState::new(registry);
175 let mut alice_af_bindings = alice_bindings.clone();
176 alice_af_bindings.insert("self".to_string(), "alice".to_string());
177 let alice_id = state.add_scheme_instance_for_affordance(
178 "alice",
179 "argue_fortify_east",
180 &alice_af_bindings,
181 alice_instance,
182 );
183 let bob_id = state.add_scheme_instance("bob", bob_instance);
184 state.add_weighted_attack(&bob_id, &alice_id, 0.5).unwrap();
185
186 let eval = StateAcceptanceEval::new(&state);
187 let action = make_affordance(
188 "argue_fortify_east",
189 "alice",
190 "alice",
191 "military",
192 "fortify_east",
193 );
194 assert!(!eval.evaluate("bob", &action), "bob should reject alice's claim");
195 }
196
197 #[test]
198 fn default_permissive_on_missing_self_binding() {
199 let state = EncounterArgumentationState::new(default_catalog());
200 let eval = StateAcceptanceEval::new(&state);
201 let spec = AffordanceSpec {
202 name: "anon".to_string(),
203 domain: "x".to_string(),
204 bindings: vec![],
205 considerations: Vec::new(),
206 effects_on_accept: Vec::new(),
207 effects_on_reject: Vec::new(),
208 drive_alignment: Vec::new(),
209 };
210 let entry = CatalogEntry { spec, precondition: String::new() };
211 let action = ScoredAffordance {
212 entry,
213 score: 1.0,
214 bindings: HashMap::new(),
215 };
216 assert!(eval.evaluate("anyone", &action));
217 let errors = state.drain_errors();
218 assert_eq!(errors.len(), 1);
219 assert!(matches!(
220 &errors[0],
221 crate::error::Error::MissingProposerBinding { affordance_name }
222 if affordance_name == "anon"
223 ));
224 }
225}