Skip to main content

mecha10_behavior_runtime/
execution.rs

1//! Execution engine for behavior trees
2//!
3//! This module provides the executor that runs behavior trees with tick-based execution.
4
5use crate::{BoxedBehavior, NodeStatus};
6use mecha10_core::Context;
7use std::time::{Duration, Instant};
8use tracing::{debug, info, warn};
9
10#[derive(Clone)]
11pub struct ExecutionContext {
12    ctx: Context,
13
14    tick_rate_hz: f32,
15}
16
17impl ExecutionContext {
18    /// Create a new execution context.
19    pub fn new(ctx: Context, tick_rate_hz: f32) -> Self {
20        Self { ctx, tick_rate_hz }
21    }
22
23    /// Get the underlying context.
24    pub fn context(&self) -> &Context {
25        &self.ctx
26    }
27
28    /// Get the tick rate in Hz.
29    pub fn tick_rate_hz(&self) -> f32 {
30        self.tick_rate_hz
31    }
32}
33
34#[derive(Debug, Clone)]
35pub struct ExecutionStats {
36    pub tick_count: usize,
37
38    pub total_duration: Duration,
39
40    pub avg_tick_duration: Duration,
41
42    pub min_tick_duration: Duration,
43
44    pub max_tick_duration: Duration,
45
46    pub final_status: Option<NodeStatus>,
47}
48
49impl ExecutionStats {
50    fn new() -> Self {
51        Self {
52            tick_count: 0,
53            total_duration: Duration::ZERO,
54            avg_tick_duration: Duration::ZERO,
55            min_tick_duration: Duration::MAX,
56            max_tick_duration: Duration::ZERO,
57            final_status: None,
58        }
59    }
60
61    fn update(&mut self, tick_duration: Duration) {
62        self.tick_count += 1;
63        self.total_duration += tick_duration;
64
65        if tick_duration < self.min_tick_duration {
66            self.min_tick_duration = tick_duration;
67        }
68        if tick_duration > self.max_tick_duration {
69            self.max_tick_duration = tick_duration;
70        }
71
72        self.avg_tick_duration = self.total_duration / self.tick_count as u32;
73    }
74
75    fn finalize(&mut self, status: NodeStatus) {
76        self.final_status = Some(status);
77    }
78}
79
80pub struct BehaviorExecutor {
81    behavior: BoxedBehavior,
82    tick_rate_hz: f32,
83    max_ticks: Option<usize>,
84}
85
86macro_rules! lifecycle_step {
87    ($(#[$meta:meta])* $fn_name:ident, $verb:literal, $behavior_method:ident) => {
88        $(#[$meta])*
89        pub async fn $fn_name(&mut self, ctx: &Context) -> anyhow::Result<()> {
90            info!(concat!($verb, " behavior: {}"), self.behavior.name());
91            self.behavior.$behavior_method(ctx).await?;
92            Ok(())
93        }
94    };
95}
96
97impl BehaviorExecutor {
98    mecha10_core::new_with_defaults!(
99        /// Create a new behavior executor.
100        ///
101        /// # Arguments
102        ///
103        /// * `behavior` - The behavior to execute
104        /// * `tick_rate_hz` - Tick rate in Hz (e.g., 30.0 for 30 Hz)
105        pub fn new(behavior: BoxedBehavior, tick_rate_hz: f32) -> Self {
106            max_ticks: None
107        }
108    );
109
110    mecha10_core::builder_setter!(into_some, with_max_ticks, max_ticks, usize);
111
112    lifecycle_step!(init, "Initializing", on_init);
113
114    /// Run the behavior until it completes (Success or Failure).
115    ///
116    /// This will tick the behavior at the configured rate until it returns
117    /// a terminal status (Success or Failure).
118    ///
119    /// # Returns
120    ///
121    /// The final status and execution statistics.
122    pub async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
123        let mut stats = ExecutionStats::new();
124        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
125
126        info!(
127            "Starting behavior execution: {} at {} Hz",
128            self.behavior.name(),
129            self.tick_rate_hz
130        );
131
132        loop {
133            interval.tick().await;
134
135            if let Some(max_ticks) = self.max_ticks {
136                if stats.tick_count >= max_ticks {
137                    warn!("Behavior exceeded maximum ticks ({})", max_ticks);
138                    return Err(anyhow::anyhow!("Exceeded maximum ticks"));
139                }
140            }
141
142            let tick_start = Instant::now();
143            let status = self.behavior.tick(ctx).await?;
144            let tick_duration = tick_start.elapsed();
145
146            stats.update(tick_duration);
147
148            debug!(
149                "Tick #{}: status={}, duration={:?}",
150                stats.tick_count, status, tick_duration
151            );
152
153            if status.is_complete() {
154                info!(
155                    "Behavior completed with status: {} after {} ticks ({:?})",
156                    status, stats.tick_count, stats.total_duration
157                );
158                stats.finalize(status);
159                return Ok((status, stats));
160            }
161        }
162    }
163
164    /// Run the behavior for a fixed duration.
165    ///
166    /// This will tick the behavior at the configured rate for the specified duration,
167    /// regardless of whether it completes.
168    pub async fn run_for_duration(
169        &mut self,
170        ctx: &Context,
171        duration: Duration,
172    ) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
173        let mut stats = ExecutionStats::new();
174        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
175        let start_time = Instant::now();
176
177        info!(
178            "Starting behavior execution: {} for {:?} at {} Hz",
179            self.behavior.name(),
180            duration,
181            self.tick_rate_hz
182        );
183
184        let mut last_status = NodeStatus::Running;
185
186        while start_time.elapsed() < duration {
187            interval.tick().await;
188
189            let tick_start = Instant::now();
190            let status = self.behavior.tick(ctx).await?;
191            let tick_duration = tick_start.elapsed();
192
193            stats.update(tick_duration);
194            last_status = status;
195
196            debug!(
197                "Tick #{}: status={}, duration={:?}",
198                stats.tick_count, status, tick_duration
199            );
200
201            if status.is_complete() {
202                info!("Behavior completed early with status: {}", status);
203                break;
204            }
205        }
206
207        info!(
208            "Behavior execution ended after {} ticks ({:?})",
209            stats.tick_count, stats.total_duration
210        );
211        stats.finalize(last_status);
212        Ok((last_status, stats))
213    }
214
215    lifecycle_step!(terminate, "Terminating", on_terminate);
216
217    /// Reset the behavior to its initial state.
218    pub async fn reset(&mut self) -> anyhow::Result<()> {
219        info!("Resetting behavior: {}", self.behavior.name());
220        self.behavior.reset().await?;
221        Ok(())
222    }
223}