1use crate::affinity::Affinity;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum InsightMode {
12 Off,
13 Neutral,
15 Full,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
21#[serde(rename_all = "snake_case")]
22pub enum MemoryScope {
23 Full,
24 #[default]
25 NeutralAndRelationship,
26 RelationshipOnly,
27 NeutralOnly,
28 InsightsOnly,
29 None,
30}
31
32impl MemoryScope {
33 pub fn resolve(self) -> (InsightMode, bool, bool) {
35 match self {
36 MemoryScope::Full => (InsightMode::Full, true, true),
37 MemoryScope::NeutralAndRelationship => (InsightMode::Neutral, true, true),
38 MemoryScope::RelationshipOnly => (InsightMode::Off, false, true),
39 MemoryScope::NeutralOnly => (InsightMode::Neutral, false, false),
40 MemoryScope::InsightsOnly => (InsightMode::Full, false, false),
41 MemoryScope::None => (InsightMode::Off, false, false),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum AffinityAxis {
49 Warmth,
50 Trust,
51 Intrigue,
52 Intimacy,
53 Patience,
54 Tension,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub struct AffinityScope {
60 pub warmth: bool,
61 pub trust: bool,
62 pub intrigue: bool,
63 pub intimacy: bool,
64 pub patience: bool,
65 pub tension: bool,
66}
67
68impl Default for AffinityScope {
69 fn default() -> Self {
70 Self::bond()
71 }
72}
73
74impl AffinityScope {
75 pub fn none() -> Self {
76 Self {
77 warmth: false,
78 trust: false,
79 intrigue: false,
80 intimacy: false,
81 patience: false,
82 tension: false,
83 }
84 }
85 pub fn full() -> Self {
86 Self {
87 warmth: true,
88 trust: true,
89 intrigue: true,
90 intimacy: true,
91 patience: true,
92 tension: true,
93 }
94 }
95 pub fn bond() -> Self {
97 Self {
98 warmth: true,
99 intimacy: true,
100 tension: true,
101 trust: false,
102 intrigue: false,
103 patience: false,
104 }
105 }
106 pub fn chemistry() -> Self {
108 Self {
109 trust: true,
110 intrigue: true,
111 patience: true,
112 warmth: false,
113 intimacy: false,
114 tension: false,
115 }
116 }
117 pub fn from_axes(axes: &[AffinityAxis]) -> Self {
118 let mut s = Self::none();
119 for a in axes {
120 match a {
121 AffinityAxis::Warmth => s.warmth = true,
122 AffinityAxis::Trust => s.trust = true,
123 AffinityAxis::Intrigue => s.intrigue = true,
124 AffinityAxis::Intimacy => s.intimacy = true,
125 AffinityAxis::Patience => s.patience = true,
126 AffinityAxis::Tension => s.tension = true,
127 }
128 }
129 s
130 }
131 pub fn any_bond_axis(&self) -> bool {
135 self.warmth || self.intimacy || self.tension
136 }
137 pub fn any_chemistry_axis(&self) -> bool {
139 self.trust || self.intrigue || self.patience
140 }
141 pub fn contains(self, axis: AffinityAxis) -> bool {
142 match axis {
143 AffinityAxis::Warmth => self.warmth,
144 AffinityAxis::Trust => self.trust,
145 AffinityAxis::Intrigue => self.intrigue,
146 AffinityAxis::Intimacy => self.intimacy,
147 AffinityAxis::Patience => self.patience,
148 AffinityAxis::Tension => self.tension,
149 }
150 }
151 pub fn is_empty(self) -> bool {
152 !(self.warmth
153 || self.trust
154 || self.intrigue
155 || self.intimacy
156 || self.patience
157 || self.tension)
158 }
159
160 pub fn active_count(self) -> usize {
162 [
163 self.warmth,
164 self.trust,
165 self.intrigue,
166 self.intimacy,
167 self.patience,
168 self.tension,
169 ]
170 .into_iter()
171 .filter(|b| *b)
172 .count()
173 }
174
175 pub fn length_score(self, a: &Affinity) -> Option<f64> {
178 let warm01 = clamp01(a.warmth);
181 let bond = clamp01((warm01 + a.intimacy + a.tension) / 3.0);
182 let chemistry = clamp01((a.trust + a.intrigue + a.patience) / 3.0);
183 let bond_active = self.warmth || self.intimacy || self.tension;
184 let chem_active = self.trust || self.intrigue || self.patience;
185 match (bond_active, chem_active) {
186 (true, true) => Some((bond + chemistry) / 2.0),
187 (true, false) => Some(bond),
188 (false, true) => Some(chemistry),
189 (false, false) => None,
190 }
191 }
192}
193
194fn clamp01(x: f64) -> f64 {
195 x.clamp(0.0, 1.0)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use chrono::Utc;
202 use uuid::Uuid;
203
204 fn affinity(
205 warmth: f64,
206 trust: f64,
207 intrigue: f64,
208 intimacy: f64,
209 patience: f64,
210 tension: f64,
211 ) -> Affinity {
212 let now = Utc::now();
213 Affinity {
214 id: Uuid::new_v4(),
215 session_id: Uuid::new_v4(),
216 user_id: Uuid::new_v4(),
217 instance_id: Uuid::new_v4(),
218 warmth,
219 trust,
220 intrigue,
221 intimacy,
222 patience,
223 tension,
224 warmth_grade: 2,
225 patience_grade: 2,
226 ghost_streak: 0,
227 last_ghost_at: None,
228 total_ghosts: 0,
229 relationship_label: None,
230 created_at: now,
231 updated_at: now,
232 }
233 }
234
235 #[test]
236 fn memory_scope_resolution_table() {
237 use InsightMode::*;
238 assert_eq!(MemoryScope::Full.resolve(), (Full, true, true));
239 assert_eq!(
240 MemoryScope::NeutralAndRelationship.resolve(),
241 (Neutral, true, true)
242 );
243 assert_eq!(MemoryScope::RelationshipOnly.resolve(), (Off, false, true));
244 assert_eq!(MemoryScope::NeutralOnly.resolve(), (Neutral, false, false));
245 assert_eq!(MemoryScope::InsightsOnly.resolve(), (Full, false, false));
246 assert_eq!(MemoryScope::None.resolve(), (Off, false, false));
247 }
248
249 #[test]
250 fn memory_scope_default_is_neutral_and_relationship() {
251 assert_eq!(MemoryScope::default(), MemoryScope::NeutralAndRelationship);
252 }
253
254 #[test]
255 fn memory_scope_serde_snake_case() {
256 let s: MemoryScope = serde_json::from_str("\"relationship_only\"").unwrap();
257 assert_eq!(s, MemoryScope::RelationshipOnly);
258 let n: MemoryScope = serde_json::from_str("\"neutral_and_relationship\"").unwrap();
260 assert_eq!(n, MemoryScope::NeutralAndRelationship);
261 assert_eq!(
262 serde_json::to_string(&MemoryScope::NeutralAndRelationship).unwrap(),
263 "\"neutral_and_relationship\""
264 );
265 assert!(serde_json::from_str::<MemoryScope>("\"bogus\"").is_err());
266 }
267
268 #[test]
269 fn affinity_scope_contains_matches_fields() {
270 let s = AffinityScope::bond();
271 assert!(s.contains(AffinityAxis::Warmth));
272 assert!(s.contains(AffinityAxis::Intimacy));
273 assert!(s.contains(AffinityAxis::Tension));
274 assert!(!s.contains(AffinityAxis::Trust));
275 assert!(!s.contains(AffinityAxis::Intrigue));
276 assert!(!s.contains(AffinityAxis::Patience));
277 }
278
279 #[test]
280 fn affinity_scope_default_is_bond() {
281 let d = AffinityScope::default();
282 assert_eq!(d, AffinityScope::bond());
283 assert!(d.warmth && d.intimacy && d.tension);
284 assert!(!d.trust && !d.intrigue && !d.patience);
285 }
286
287 #[test]
288 fn affinity_scope_chemistry_and_full() {
289 let c = AffinityScope::chemistry();
290 assert!(c.trust && c.intrigue && c.patience);
291 assert!(!c.warmth && !c.intimacy && !c.tension);
292 let f = AffinityScope::full();
293 assert!(!f.is_empty());
294 assert!(f.warmth && f.trust && f.intrigue && f.intimacy && f.patience && f.tension);
295 }
296
297 #[test]
298 fn affinity_scope_from_axes_and_empty() {
299 let s = AffinityScope::from_axes(&[AffinityAxis::Warmth, AffinityAxis::Trust]);
300 assert!(s.warmth && s.trust);
301 assert!(!s.intrigue && !s.intimacy && !s.patience && !s.tension);
302 assert!(AffinityScope::from_axes(&[]).is_empty());
303 assert!(AffinityScope::none().is_empty());
304 }
305
306 #[test]
307 fn affinity_scope_active_count() {
308 assert_eq!(AffinityScope::none().active_count(), 0);
309 assert_eq!(AffinityScope::bond().active_count(), 3);
310 assert_eq!(AffinityScope::full().active_count(), 6);
311 let one = AffinityScope::from_axes(&[AffinityAxis::Warmth]);
312 assert_eq!(one.active_count(), 1);
313 }
314
315 #[test]
316 fn affinity_axis_serde_snake_case() {
317 let a: AffinityAxis = serde_json::from_str("\"warmth\"").unwrap();
318 assert_eq!(a, AffinityAxis::Warmth);
319 assert!(serde_json::from_str::<AffinityAxis>("\"warm\"").is_err());
320 }
321
322 #[test]
323 fn length_score_named_cases() {
324 let a = affinity(0.5, 0.9, 0.9, 0.5, 0.9, 0.5);
327 let bond = AffinityScope::bond().length_score(&a).unwrap();
328 let chem = AffinityScope::chemistry().length_score(&a).unwrap();
329 let full = AffinityScope::full().length_score(&a).unwrap();
330 assert!((bond - 0.5).abs() < 1e-9);
331 assert!((chem - 0.9).abs() < 1e-9);
332 assert!((full - 0.7).abs() < 1e-9); assert_eq!(AffinityScope::none().length_score(&a), None);
334 }
335
336 #[test]
337 fn length_score_array_activates_both_triads() {
338 let a = affinity(0.5, 0.9, 0.9, 0.5, 0.9, 0.5);
339 let s = AffinityScope::from_axes(&[AffinityAxis::Warmth, AffinityAxis::Trust]);
341 assert!((s.length_score(&a).unwrap() - 0.7).abs() < 1e-9);
342 }
343
344 #[test]
345 fn affinity_scope_half_detection() {
346 assert!(AffinityScope::bond().any_bond_axis());
347 assert!(!AffinityScope::bond().any_chemistry_axis());
348 assert!(AffinityScope::chemistry().any_chemistry_axis());
349 assert!(!AffinityScope::chemistry().any_bond_axis());
350 assert!(AffinityScope::full().any_bond_axis());
351 assert!(AffinityScope::full().any_chemistry_axis());
352 assert!(!AffinityScope::none().any_bond_axis());
353 assert!(!AffinityScope::none().any_chemistry_axis());
354 let w = AffinityScope::from_axes(&[AffinityAxis::Warmth]);
356 assert!(w.any_bond_axis() && !w.any_chemistry_axis());
357 let t = AffinityScope::from_axes(&[AffinityAxis::Trust]);
358 assert!(t.any_chemistry_axis() && !t.any_bond_axis());
359 }
360}