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 contains(self, axis: AffinityAxis) -> bool {
132 match axis {
133 AffinityAxis::Warmth => self.warmth,
134 AffinityAxis::Trust => self.trust,
135 AffinityAxis::Intrigue => self.intrigue,
136 AffinityAxis::Intimacy => self.intimacy,
137 AffinityAxis::Patience => self.patience,
138 AffinityAxis::Tension => self.tension,
139 }
140 }
141 pub fn is_empty(self) -> bool {
142 !(self.warmth
143 || self.trust
144 || self.intrigue
145 || self.intimacy
146 || self.patience
147 || self.tension)
148 }
149
150 pub fn active_count(self) -> usize {
152 [
153 self.warmth,
154 self.trust,
155 self.intrigue,
156 self.intimacy,
157 self.patience,
158 self.tension,
159 ]
160 .into_iter()
161 .filter(|b| *b)
162 .count()
163 }
164
165 pub fn length_score(self, a: &Affinity) -> Option<f64> {
168 let warm01 = clamp01((a.warmth + 1.0) / 2.0);
169 let bond = clamp01((warm01 + a.intimacy + a.tension) / 3.0);
170 let chemistry = clamp01((a.trust + a.intrigue + a.patience) / 3.0);
171 let bond_active = self.warmth || self.intimacy || self.tension;
172 let chem_active = self.trust || self.intrigue || self.patience;
173 match (bond_active, chem_active) {
174 (true, true) => Some((bond + chemistry) / 2.0),
175 (true, false) => Some(bond),
176 (false, true) => Some(chemistry),
177 (false, false) => None,
178 }
179 }
180}
181
182fn clamp01(x: f64) -> f64 {
183 x.clamp(0.0, 1.0)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use chrono::Utc;
190 use uuid::Uuid;
191
192 fn affinity(
193 warmth: f64,
194 trust: f64,
195 intrigue: f64,
196 intimacy: f64,
197 patience: f64,
198 tension: f64,
199 ) -> Affinity {
200 let now = Utc::now();
201 Affinity {
202 id: Uuid::new_v4(),
203 session_id: Uuid::new_v4(),
204 user_id: Uuid::new_v4(),
205 instance_id: Uuid::new_v4(),
206 warmth,
207 trust,
208 intrigue,
209 intimacy,
210 patience,
211 tension,
212 ghost_streak: 0,
213 last_ghost_at: None,
214 total_ghosts: 0,
215 relationship_label: None,
216 created_at: now,
217 updated_at: now,
218 }
219 }
220
221 #[test]
222 fn memory_scope_resolution_table() {
223 use InsightMode::*;
224 assert_eq!(MemoryScope::Full.resolve(), (Full, true, true));
225 assert_eq!(
226 MemoryScope::NeutralAndRelationship.resolve(),
227 (Neutral, true, true)
228 );
229 assert_eq!(MemoryScope::RelationshipOnly.resolve(), (Off, false, true));
230 assert_eq!(MemoryScope::NeutralOnly.resolve(), (Neutral, false, false));
231 assert_eq!(MemoryScope::InsightsOnly.resolve(), (Full, false, false));
232 assert_eq!(MemoryScope::None.resolve(), (Off, false, false));
233 }
234
235 #[test]
236 fn memory_scope_default_is_neutral_and_relationship() {
237 assert_eq!(MemoryScope::default(), MemoryScope::NeutralAndRelationship);
238 }
239
240 #[test]
241 fn memory_scope_serde_snake_case() {
242 let s: MemoryScope = serde_json::from_str("\"relationship_only\"").unwrap();
243 assert_eq!(s, MemoryScope::RelationshipOnly);
244 let n: MemoryScope = serde_json::from_str("\"neutral_and_relationship\"").unwrap();
246 assert_eq!(n, MemoryScope::NeutralAndRelationship);
247 assert_eq!(
248 serde_json::to_string(&MemoryScope::NeutralAndRelationship).unwrap(),
249 "\"neutral_and_relationship\""
250 );
251 assert!(serde_json::from_str::<MemoryScope>("\"bogus\"").is_err());
252 }
253
254 #[test]
255 fn affinity_scope_contains_matches_fields() {
256 let s = AffinityScope::bond();
257 assert!(s.contains(AffinityAxis::Warmth));
258 assert!(s.contains(AffinityAxis::Intimacy));
259 assert!(s.contains(AffinityAxis::Tension));
260 assert!(!s.contains(AffinityAxis::Trust));
261 assert!(!s.contains(AffinityAxis::Intrigue));
262 assert!(!s.contains(AffinityAxis::Patience));
263 }
264
265 #[test]
266 fn affinity_scope_default_is_bond() {
267 let d = AffinityScope::default();
268 assert_eq!(d, AffinityScope::bond());
269 assert!(d.warmth && d.intimacy && d.tension);
270 assert!(!d.trust && !d.intrigue && !d.patience);
271 }
272
273 #[test]
274 fn affinity_scope_chemistry_and_full() {
275 let c = AffinityScope::chemistry();
276 assert!(c.trust && c.intrigue && c.patience);
277 assert!(!c.warmth && !c.intimacy && !c.tension);
278 let f = AffinityScope::full();
279 assert!(!f.is_empty());
280 assert!(f.warmth && f.trust && f.intrigue && f.intimacy && f.patience && f.tension);
281 }
282
283 #[test]
284 fn affinity_scope_from_axes_and_empty() {
285 let s = AffinityScope::from_axes(&[AffinityAxis::Warmth, AffinityAxis::Trust]);
286 assert!(s.warmth && s.trust);
287 assert!(!s.intrigue && !s.intimacy && !s.patience && !s.tension);
288 assert!(AffinityScope::from_axes(&[]).is_empty());
289 assert!(AffinityScope::none().is_empty());
290 }
291
292 #[test]
293 fn affinity_scope_active_count() {
294 assert_eq!(AffinityScope::none().active_count(), 0);
295 assert_eq!(AffinityScope::bond().active_count(), 3);
296 assert_eq!(AffinityScope::full().active_count(), 6);
297 let one = AffinityScope::from_axes(&[AffinityAxis::Warmth]);
298 assert_eq!(one.active_count(), 1);
299 }
300
301 #[test]
302 fn affinity_axis_serde_snake_case() {
303 let a: AffinityAxis = serde_json::from_str("\"warmth\"").unwrap();
304 assert_eq!(a, AffinityAxis::Warmth);
305 assert!(serde_json::from_str::<AffinityAxis>("\"warm\"").is_err());
306 }
307
308 #[test]
309 fn length_score_named_cases() {
310 let a = affinity(0.0, 0.9, 0.9, 0.5, 0.9, 0.5);
313 let bond = AffinityScope::bond().length_score(&a).unwrap();
314 let chem = AffinityScope::chemistry().length_score(&a).unwrap();
315 let full = AffinityScope::full().length_score(&a).unwrap();
316 assert!((bond - 0.5).abs() < 1e-9);
317 assert!((chem - 0.9).abs() < 1e-9);
318 assert!((full - 0.7).abs() < 1e-9); assert_eq!(AffinityScope::none().length_score(&a), None);
320 }
321
322 #[test]
323 fn length_score_array_activates_both_triads() {
324 let a = affinity(0.0, 0.9, 0.9, 0.5, 0.9, 0.5);
325 let s = AffinityScope::from_axes(&[AffinityAxis::Warmth, AffinityAxis::Trust]);
327 assert!((s.length_score(&a).unwrap() - 0.7).abs() < 1e-9);
328 }
329}