Skip to main content

lineage/
behavior.rs

1//! # Minimal Behavior Loop
2//!
3//! ## What This Enforces
4//! - One behavior (pulse)
5//! - One contract (minimum energy threshold)
6//! - One injury path (weak pulse strain)
7//! - One scar consequence (increased future cost)
8//!
9//! ## What This Demonstrates
10//! - Behaviors consume energy
11//! - Contracts detect violations
12//! - Violations cause permanent injury
13//! - Scars reduce future capability
14//!
15//! This is not useful. This is consequential.
16
17use crate::lineage::{Lineage, OperationError, OperationResult};
18use crate::scar::ScarSeverity;
19
20/// Output of a pulse behavior execution.
21#[derive(Debug, PartialEq)]
22pub struct PulseOutput {
23    /// Energy level when pulse was executed
24    pub energy_at_pulse: u64,
25    /// Whether the pulse was strong enough
26    pub is_strong: bool,
27    /// Whether strain occurred (contract violation)
28    pub strain_occurred: bool,
29}
30
31/// The single behavior: PULSE
32/// 
33/// A pulse is a basic operation that consumes energy.
34/// The pulse must be executed with sufficient energy or it causes strain.
35/// 
36/// **CONSEQUENCE**: Weak pulses cause permanent strain scars.
37/// **CONSEQUENCE**: Strain scars increase future pulse costs.
38pub struct PulseBehavior {
39    /// Minimum energy required for a healthy pulse
40    threshold: u64,
41    /// Base energy cost per pulse
42    base_cost: u64,
43}
44
45impl PulseBehavior {
46    /// Creates the pulse behavior with fixed parameters.
47    /// 
48    /// **INVARIANT**: Threshold and cost are permanent and non-configurable.
49    pub fn new() -> Self {
50        PulseBehavior {
51            threshold: 30,  // Minimum 30 energy for healthy pulse
52            base_cost: 10,   // Base cost is 10 energy
53        }
54    }
55
56    /// Executes a single pulse.
57    /// 
58    /// **CONTRACT**: Pulse must be executed with energy >= threshold.
59    /// **CONSEQUENCE**: If energy < threshold during pulse, strain occurs.
60    /// **CONSEQUENCE**: Strain inflicts a permanent scar.
61    /// **CONSEQUENCE**: Each strain scar increases future pulse cost by 5.
62    /// 
63    /// This demonstrates:
64    /// - Behavior consumes energy
65    /// - Contract is checked based on observable state
66    /// - Violation triggers injury, not correction
67    /// - Scar permanently reduces capability
68    pub fn execute_pulse(&self, lineage: &mut Lineage) -> PulseOutput {
69        // Check if lineage is operational
70        if !lineage.is_alive() {
71            return PulseOutput {
72                energy_at_pulse: 0,
73                is_strong: false,
74                strain_occurred: false,
75            };
76        }
77
78        // Calculate actual cost based on accumulated strain scars
79        let strain_scars = self.count_strain_scars(lineage);
80        let actual_cost = self.base_cost + (strain_scars * 5);
81
82        // Observe energy level BEFORE pulse
83        let energy_before = lineage.metabolism().energy();
84
85        // Perform the pulse operation
86        let result = lineage.perform_operation(
87            format!("Pulse (cost: {})", actual_cost),
88            actual_cost,
89        );
90
91        // Check if pulse completed
92        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        // CONTRACT VERIFICATION: Was energy sufficient during pulse?
103        let is_strong = energy_before >= self.threshold;
104
105        // INJURY PATH: Contract violation causes strain
106        let mut strain_occurred = false;
107        if !is_strong {
108            // Weak pulse detected - this is a contract violation
109            // Trigger injury: inflict permanent strain scar
110            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    /// INJURY PATH: Inflicts a strain scar.
122    /// 
123    /// **CONSEQUENCE**: This scar is permanent.
124    /// **CONSEQUENCE**: Future pulses cost more energy.
125    /// **CONSEQUENCE**: Eventually, pulses become impossible.
126    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    /// Counts strain scars to calculate increased cost.
140    /// 
141    /// Each strain scar adds 5 to the pulse cost.
142    /// This is the permanent capability reduction.
143    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    /// Returns the current pulse cost based on accumulated strain.
153    /// 
154    /// This demonstrates how scars reduce capability:
155    /// - 0 strain scars: cost = 10
156    /// - 1 strain scar: cost = 15
157    /// - 2 strain scars: cost = 20
158    /// - etc.
159    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    /// Returns the energy threshold for healthy pulses.
165    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        // Create lineage with barely enough energy for pulse but below threshold
196        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        // Initial cost is 10
212        assert_eq!(behavior.current_pulse_cost(&lineage), 10);
213
214        // Deplete energy to cause strain
215        lineage.perform_operation("Deplete".to_string(), 75);
216        
217        // First weak pulse - causes strain
218        behavior.execute_pulse(&mut lineage);
219        
220        // Cost should now be 15 (base 10 + 5 for one strain scar)
221        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        // Test that multiple strain scars compound the cost
230        // Use separate lineages to isolate each strain accumulation
231        
232        // First lineage: 1 strain
233        let mut lineage1 = Lineage::create(100);
234        lineage1.perform_operation("Deplete".to_string(), 75); // 25 left
235        behavior.execute_pulse(&mut lineage1); // Strain! Cost was 10
236        assert_eq!(behavior.count_strain_scars(&lineage1), 1);
237        assert_eq!(behavior.current_pulse_cost(&lineage1), 15); // 10 + 5
238        
239        // Second lineage: 2 strains
240        let mut lineage2 = Lineage::create(100);
241        lineage2.perform_operation("Deplete".to_string(), 75); // 25 left
242        behavior.execute_pulse(&mut lineage2); // Strain 1
243        lineage2.perform_operation("Deplete2".to_string(), 10); // Down to 5, then wait...
244        // Can't do another pulse without more energy, so test with 3rd lineage
245        
246        // Third lineage: Create with scars already (simulate via multiple weak pulses)
247        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        // Execute weak pulses repeatedly to accumulate strain
270        for i in 0..15 {
271            if !lineage.is_alive() {
272                break;
273            }
274            
275            let current_energy = lineage.metabolism().energy();
276            
277            // Deplete to 25 (below threshold of 30) to trigger strain
278            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            // Break if energy too low to continue
288            if lineage.metabolism().energy() < 15 {
289                break;
290            }
291        }
292
293        // Verify death spiral occurred - at least some strains
294        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        // Verify cost increased due to scars
298        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        // Inflict first strain scar
307        lineage.perform_operation("Deplete".to_string(), 175); // Energy: 25
308        behavior.execute_pulse(&mut lineage); // Cost: 10, Energy after: 15, Strain inflicted
309        
310        let strain_count = behavior.count_strain_scars(&lineage);
311        let pulse_cost = behavior.current_pulse_cost(&lineage);
312        
313        // INVARIANT: Pulse cost MUST account for ALL strain scars
314        assert_eq!(strain_count, 1);
315        assert_eq!(pulse_cost, 10 + (strain_count * 5));
316        assert_eq!(pulse_cost, 15);
317        
318        // INVARIANT: There should be NO way to:
319        // behavior.reset_cost()
320        // behavior.ignore_scars()
321        // behavior.set_base_cost_only()
322        // 
323        // If such methods exist, scar consequence has been violated.
324    }
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        // Cause first strain
334        lineage.perform_operation("Deplete1".to_string(), 475); // Energy: 25
335        behavior.execute_pulse(&mut lineage); // Cost: 10, Energy after: 15
336        let cost1 = behavior.current_pulse_cost(&lineage);
337        
338        // Verify cost increased
339        let strain_count_1 = behavior.count_strain_scars(&lineage);
340        
341        // INVARIANT: Pulse cost is monotonically increasing
342        assert_eq!(cost0, 10);
343        assert!(cost1 > cost0);  // 10 + (strains * 5)
344        assert_eq!(cost1, 10 + (strain_count_1 * 5));
345        
346        // INVARIANT: Cost increase is permanent
347        // Once strain scars exist, cost never goes back down
348        
349        // Verify cost stays increased even after more operations
350        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        // Cause one strain
360        lineage.perform_operation("Deplete".to_string(), 475);
361        behavior.execute_pulse(&mut lineage);
362        
363        // INVARIANT: Cost is now permanently increased
364        let increased_cost = behavior.current_pulse_cost(&lineage);
365        assert_eq!(increased_cost, 15);
366        
367        // Execute many more pulses
368        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        // INVARIANT: Cost remains increased (or increases further, but never decreases)
379        let final_cost = behavior.current_pulse_cost(&lineage);
380        assert!(final_cost >= increased_cost);
381        
382        // INVARIANT: Past strain scars affect ALL future pulses
383        // There is NO "forgetting" or "healing" of strain consequences
384    }
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        // Output should reflect exact state at moment of pulse
394        assert_eq!(output.energy_at_pulse, 50);
395        assert!(output.is_strong); // 50 >= 30 threshold
396        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        // Deplete energy to death
405        lineage.perform_operation("Death".to_string(), 10);
406        assert!(!lineage.is_alive());
407
408        let output = behavior.execute_pulse(&mut lineage);
409
410        // Should return failed pulse output
411        assert_eq!(output.energy_at_pulse, 0);
412        assert!(!output.is_strong);
413        assert!(!output.strain_occurred); // No strain because pulse didn't execute
414    }
415
416    #[test]
417    fn strain_scars_are_permanent() {
418        let mut lineage = Lineage::create(100);
419        let behavior = PulseBehavior::new();
420
421        // Cause strain
422        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        // Perform more operations
429        lineage.perform_operation("More ops".to_string(), 5);
430        lineage.perform_operation("Even more".to_string(), 3);
431
432        // Scars and cost should remain
433        assert_eq!(lineage.scars().scar_count(), scars_after_strain);
434        assert_eq!(behavior.current_pulse_cost(&lineage), cost_after_strain);
435    }
436}