1use crate::lineage::{Lineage, OperationError, OperationResult};
18use crate::scar::ScarSeverity;
19
20#[derive(Debug, PartialEq)]
22pub struct PulseOutput {
23 pub energy_at_pulse: u64,
25 pub is_strong: bool,
27 pub strain_occurred: bool,
29}
30
31pub struct PulseBehavior {
39 threshold: u64,
41 base_cost: u64,
43}
44
45impl PulseBehavior {
46 pub fn new() -> Self {
50 PulseBehavior {
51 threshold: 30, base_cost: 10, }
54 }
55
56 pub fn execute_pulse(&self, lineage: &mut Lineage) -> PulseOutput {
69 if !lineage.is_alive() {
71 return PulseOutput {
72 energy_at_pulse: 0,
73 is_strong: false,
74 strain_occurred: false,
75 };
76 }
77
78 let strain_scars = self.count_strain_scars(lineage);
80 let actual_cost = self.base_cost + (strain_scars * 5);
81
82 let energy_before = lineage.metabolism().energy();
84
85 let result = lineage.perform_operation(
87 format!("Pulse (cost: {})", actual_cost),
88 actual_cost,
89 );
90
91 let pulse_completed = matches!(result, OperationResult::Success { .. });
93
94 if !pulse_completed {
95 return PulseOutput {
96 energy_at_pulse: energy_before,
97 is_strong: false,
98 strain_occurred: false,
99 };
100 }
101
102 let is_strong = energy_before >= self.threshold;
104
105 let mut strain_occurred = false;
107 if !is_strong {
108 self.inflict_strain_injury(lineage, energy_before);
111 strain_occurred = true;
112 }
113
114 PulseOutput {
115 energy_at_pulse: energy_before,
116 is_strong,
117 strain_occurred,
118 }
119 }
120
121 fn inflict_strain_injury(&self, lineage: &mut Lineage, energy_at_violation: u64) {
127 let strain_scar = OperationError::new(
128 ScarSeverity::Moderate,
129 format!("Pulse strain: insufficient energy (had {}, need {})",
130 energy_at_violation, self.threshold),
131 ).with_context(format!(
132 "Contract violation: pulse executed below threshold. \
133 This permanently increases future pulse cost by 5 energy."
134 ));
135
136 lineage.record_error(strain_scar);
137 }
138
139 fn count_strain_scars(&self, lineage: &Lineage) -> u64 {
144 lineage
145 .scars()
146 .all_scars()
147 .iter()
148 .filter(|scar| scar.description().contains("Pulse strain"))
149 .count() as u64
150 }
151
152 pub fn current_pulse_cost(&self, lineage: &Lineage) -> u64 {
160 let strain_scars = self.count_strain_scars(lineage);
161 self.base_cost + (strain_scars * 5)
162 }
163
164 pub fn threshold(&self) -> u64 {
166 self.threshold
167 }
168}
169
170impl Default for PulseBehavior {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::lineage::Lineage;
180
181 #[test]
182 fn pulse_with_sufficient_energy_is_strong() {
183 let mut lineage = Lineage::create(100);
184 let behavior = PulseBehavior::new();
185
186 let output = behavior.execute_pulse(&mut lineage);
187
188 assert!(output.is_strong, "Pulse should be strong with 100 energy");
189 assert!(!output.strain_occurred, "No strain should occur");
190 assert_eq!(lineage.scars().scar_count(), 0, "No scars should be inflicted");
191 }
192
193 #[test]
194 fn pulse_below_threshold_causes_strain() {
195 let mut lineage = Lineage::create(25);
197 let behavior = PulseBehavior::new();
198
199 let output = behavior.execute_pulse(&mut lineage);
200
201 assert!(!output.is_strong, "Pulse should be weak with 25 energy");
202 assert!(output.strain_occurred, "Strain should occur");
203 assert_eq!(lineage.scars().scar_count(), 1, "Strain scar should be inflicted");
204 }
205
206 #[test]
207 fn strain_scars_increase_future_pulse_cost() {
208 let mut lineage = Lineage::create(100);
209 let behavior = PulseBehavior::new();
210
211 assert_eq!(behavior.current_pulse_cost(&lineage), 10);
213
214 lineage.perform_operation("Deplete".to_string(), 75);
216
217 behavior.execute_pulse(&mut lineage);
219
220 assert_eq!(behavior.current_pulse_cost(&lineage), 15);
222 assert_eq!(lineage.scars().scar_count(), 1);
223 }
224
225 #[test]
226 fn multiple_strain_scars_compound_cost() {
227 let behavior = PulseBehavior::new();
228
229 let mut lineage1 = Lineage::create(100);
234 lineage1.perform_operation("Deplete".to_string(), 75); behavior.execute_pulse(&mut lineage1); assert_eq!(behavior.count_strain_scars(&lineage1), 1);
237 assert_eq!(behavior.current_pulse_cost(&lineage1), 15); let mut lineage2 = Lineage::create(100);
241 lineage2.perform_operation("Deplete".to_string(), 75); behavior.execute_pulse(&mut lineage2); lineage2.perform_operation("Deplete2".to_string(), 10); let mut lineage3 = Lineage::create(200);
248 for i in 0..3 {
249 let energy = lineage3.metabolism().energy();
250 if energy > 25 {
251 lineage3.perform_operation(format!("Deplete #{}", i), energy - 25);
252 }
253 behavior.execute_pulse(&mut lineage3);
254 if !lineage3.is_alive() || lineage3.metabolism().energy() < 25 {
255 break;
256 }
257 }
258
259 let final_strains = behavior.count_strain_scars(&lineage3);
260 assert!(final_strains >= 1, "Should have at least 1 strain");
261 assert_eq!(behavior.current_pulse_cost(&lineage3), 10 + (final_strains * 5));
262 }
263
264 #[test]
265 fn strain_accumulation_leads_to_death_spiral() {
266 let mut lineage = Lineage::create(200);
267 let behavior = PulseBehavior::new();
268
269 for i in 0..15 {
271 if !lineage.is_alive() {
272 break;
273 }
274
275 let current_energy = lineage.metabolism().energy();
276
277 if current_energy > 25 {
279 lineage.perform_operation(
280 format!("Deplete #{}", i + 1),
281 current_energy - 25
282 );
283 }
284
285 behavior.execute_pulse(&mut lineage);
286
287 if lineage.metabolism().energy() < 15 {
289 break;
290 }
291 }
292
293 let final_strain_count = behavior.count_strain_scars(&lineage);
295 assert!(final_strain_count >= 1, "Should have accumulated at least 1 strain, got {}", final_strain_count);
296
297 assert_eq!(behavior.current_pulse_cost(&lineage), 10 + (final_strain_count * 5));
299 }
300
301 #[test]
302 fn behavior_cannot_ignore_strain_scars() {
303 let behavior = PulseBehavior::new();
304 let mut lineage = Lineage::create(200);
305
306 lineage.perform_operation("Deplete".to_string(), 175); behavior.execute_pulse(&mut lineage); let strain_count = behavior.count_strain_scars(&lineage);
311 let pulse_cost = behavior.current_pulse_cost(&lineage);
312
313 assert_eq!(strain_count, 1);
315 assert_eq!(pulse_cost, 10 + (strain_count * 5));
316 assert_eq!(pulse_cost, 15);
317
318 }
325
326 #[test]
327 fn pulse_cost_only_increases() {
328 let behavior = PulseBehavior::new();
329 let mut lineage = Lineage::create(500);
330
331 let cost0 = behavior.current_pulse_cost(&lineage);
332
333 lineage.perform_operation("Deplete1".to_string(), 475); behavior.execute_pulse(&mut lineage); let cost1 = behavior.current_pulse_cost(&lineage);
337
338 let strain_count_1 = behavior.count_strain_scars(&lineage);
340
341 assert_eq!(cost0, 10);
343 assert!(cost1 > cost0); assert_eq!(cost1, 10 + (strain_count_1 * 5));
345
346 let cost1_again = behavior.current_pulse_cost(&lineage);
351 assert_eq!(cost1, cost1_again);
352 }
353
354 #[test]
355 fn strain_scars_affect_all_future_pulses() {
356 let behavior = PulseBehavior::new();
357 let mut lineage = Lineage::create(500);
358
359 lineage.perform_operation("Deplete".to_string(), 475);
361 behavior.execute_pulse(&mut lineage);
362
363 let increased_cost = behavior.current_pulse_cost(&lineage);
365 assert_eq!(increased_cost, 15);
366
367 for _ in 0..5 {
369 let current = lineage.metabolism().energy();
370 if current > 40 {
371 lineage.perform_operation("Work".to_string(), current - 40);
372 }
373 if lineage.is_alive() {
374 behavior.execute_pulse(&mut lineage);
375 }
376 }
377
378 let final_cost = behavior.current_pulse_cost(&lineage);
380 assert!(final_cost >= increased_cost);
381
382 }
385
386 #[test]
387 fn pulse_output_records_observable_state() {
388 let mut lineage = Lineage::create(50);
389 let behavior = PulseBehavior::new();
390
391 let output = behavior.execute_pulse(&mut lineage);
392
393 assert_eq!(output.energy_at_pulse, 50);
395 assert!(output.is_strong); assert!(!output.strain_occurred);
397 }
398
399 #[test]
400 fn cannot_pulse_after_death() {
401 let mut lineage = Lineage::create(10);
402 let behavior = PulseBehavior::new();
403
404 lineage.perform_operation("Death".to_string(), 10);
406 assert!(!lineage.is_alive());
407
408 let output = behavior.execute_pulse(&mut lineage);
409
410 assert_eq!(output.energy_at_pulse, 0);
412 assert!(!output.is_strong);
413 assert!(!output.strain_occurred); }
415
416 #[test]
417 fn strain_scars_are_permanent() {
418 let mut lineage = Lineage::create(100);
419 let behavior = PulseBehavior::new();
420
421 lineage.perform_operation("Deplete".to_string(), 75);
423 behavior.execute_pulse(&mut lineage);
424
425 let scars_after_strain = lineage.scars().scar_count();
426 let cost_after_strain = behavior.current_pulse_cost(&lineage);
427
428 lineage.perform_operation("More ops".to_string(), 5);
430 lineage.perform_operation("Even more".to_string(), 3);
431
432 assert_eq!(lineage.scars().scar_count(), scars_after_strain);
434 assert_eq!(behavior.current_pulse_cost(&lineage), cost_after_strain);
435 }
436}