1use crate::affinity::Affinity;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct GhostSignals {
10 pub message_count: i64,
11 pub hours_since_last_ghost: Option<f64>,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum GhostDecision {
16 Ghost,
17 Reply,
18}
19
20pub fn score(a: &Affinity) -> f64 {
22 (1.0 - a.intrigue) * 0.4 + (1.0 - a.patience) * 0.4 + a.tension * 0.2
23}
24
25pub fn ghost_permitted(a: &Affinity, s: GhostSignals) -> bool {
31 if s.message_count < 10 {
32 return false;
33 }
34 if a.ghost_streak >= 2 {
35 return false;
36 }
37 if matches!(s.hours_since_last_ghost, Some(h) if h < 1.0) {
38 return false;
39 }
40 true
41}
42
43pub fn decide(a: &Affinity, s: GhostSignals) -> GhostDecision {
46 if !ghost_permitted(a, s) {
47 return GhostDecision::Reply;
48 }
49 let threshold = if s.hours_since_last_ghost.is_some() {
50 0.85
51 } else {
52 0.65
53 };
54 if score(a) > threshold {
55 GhostDecision::Ghost
56 } else {
57 GhostDecision::Reply
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::affinity::Affinity;
65 use chrono::Utc;
66 use uuid::Uuid;
67
68 fn aff(intrigue: f64, patience: f64, tension: f64, ghost_streak: i32) -> Affinity {
69 let now = Utc::now();
70 Affinity {
71 id: Uuid::new_v4(),
72 session_id: Uuid::new_v4(),
73 user_id: Uuid::new_v4(),
74 instance_id: Uuid::new_v4(),
75 warmth: 0.3,
76 trust: 0.2,
77 intrigue,
78 intimacy: 0.0,
79 patience,
80 tension,
81 ghost_streak,
82 last_ghost_at: None,
83 total_ghosts: 0,
84 relationship_label: None,
85 created_at: now,
86 updated_at: now,
87 }
88 }
89
90 #[test]
91 fn never_ghost_when_message_count_below_10() {
92 let a = aff(0.0, 0.0, 1.0, 0); let s = GhostSignals {
94 message_count: 5,
95 hours_since_last_ghost: None,
96 };
97 assert_eq!(decide(&a, s), GhostDecision::Reply);
98 }
99
100 #[test]
101 fn never_ghost_two_in_a_row() {
102 let a = aff(0.0, 0.0, 1.0, 2);
103 let s = GhostSignals {
104 message_count: 50,
105 hours_since_last_ghost: Some(0.5),
106 };
107 assert_eq!(decide(&a, s), GhostDecision::Reply);
108 }
109
110 #[test]
111 fn cooldown_blocks_ghost_within_one_hour() {
112 let a = aff(0.0, 0.0, 1.0, 1);
113 let s = GhostSignals {
114 message_count: 50,
115 hours_since_last_ghost: Some(0.5),
116 };
117 assert_eq!(decide(&a, s), GhostDecision::Reply);
118 }
119
120 #[test]
121 fn ghost_when_score_above_threshold_post_protection() {
122 let a = aff(0.1, 0.1, 0.5, 0);
125 let s = GhostSignals {
126 message_count: 50,
127 hours_since_last_ghost: None,
128 };
129 assert_eq!(decide(&a, s), GhostDecision::Ghost);
130 }
131
132 #[test]
133 fn raised_threshold_after_recent_ghost_blocks_mid_score() {
134 let a = aff(0.5, 0.5, 0.0, 1);
137 let s = GhostSignals {
138 message_count: 50,
139 hours_since_last_ghost: Some(2.0),
140 };
141 assert_eq!(decide(&a, s), GhostDecision::Reply);
142 }
143
144 #[test]
145 fn high_score_blocked_by_post_ghost_higher_threshold() {
146 let a = aff(0.05, 0.05, 0.0, 1);
149 let s = GhostSignals {
150 message_count: 50,
151 hours_since_last_ghost: Some(2.0),
152 };
153 assert_eq!(decide(&a, s), GhostDecision::Reply);
154 }
155
156 #[test]
157 fn ghost_score_formula() {
158 let a = aff(0.4, 0.6, 0.5, 0);
159 let expected = (1.0 - 0.4) * 0.4 + (1.0 - 0.6) * 0.4 + 0.5 * 0.2;
160 assert!((score(&a) - expected).abs() < 1e-9);
161 }
162
163 #[test]
164 fn ghost_permitted_false_when_message_count_below_10() {
165 let a = aff(0.1, 0.1, 0.5, 0);
166 let s = GhostSignals {
167 message_count: 5,
168 hours_since_last_ghost: None,
169 };
170 assert!(!ghost_permitted(&a, s));
171 }
172
173 #[test]
174 fn ghost_permitted_false_on_streak() {
175 let a = aff(0.1, 0.1, 0.5, 2); let s = GhostSignals {
177 message_count: 50,
178 hours_since_last_ghost: Some(5.0),
179 };
180 assert!(!ghost_permitted(&a, s));
181 }
182
183 #[test]
184 fn ghost_permitted_false_within_cooldown() {
185 let a = aff(0.1, 0.1, 0.5, 0);
186 let s = GhostSignals {
187 message_count: 50,
188 hours_since_last_ghost: Some(0.5),
189 };
190 assert!(!ghost_permitted(&a, s));
191 }
192
193 #[test]
194 fn ghost_permitted_true_when_clear() {
195 let a = aff(0.1, 0.1, 0.5, 0);
196 let s = GhostSignals {
197 message_count: 50,
198 hours_since_last_ghost: Some(5.0),
199 };
200 assert!(ghost_permitted(&a, s));
201 }
202}