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/// Execution context for behavior trees.
11///
12/// This wraps the Mecha10 Context and provides additional execution state.
13#[derive(Clone)]
14pub struct ExecutionContext {
15    /// Underlying Mecha10 context
16    ctx: Context,
17
18    /// Tick rate for the executor
19    tick_rate_hz: f32,
20}
21
22impl ExecutionContext {
23    /// Create a new execution context.
24    pub fn new(ctx: Context, tick_rate_hz: f32) -> Self {
25        Self { ctx, tick_rate_hz }
26    }
27
28    /// Get the underlying context.
29    pub fn context(&self) -> &Context {
30        &self.ctx
31    }
32
33    /// Get the tick rate in Hz.
34    pub fn tick_rate_hz(&self) -> f32 {
35        self.tick_rate_hz
36    }
37}
38
39/// Statistics about behavior execution.
40#[derive(Debug, Clone)]
41pub struct ExecutionStats {
42    /// Total number of ticks executed
43    pub tick_count: usize,
44
45    /// Total execution time
46    pub total_duration: Duration,
47
48    /// Average tick duration
49    pub avg_tick_duration: Duration,
50
51    /// Minimum tick duration
52    pub min_tick_duration: Duration,
53
54    /// Maximum tick duration
55    pub max_tick_duration: Duration,
56
57    /// Final status
58    pub final_status: Option<NodeStatus>,
59}
60
61impl ExecutionStats {
62    fn new() -> Self {
63        Self {
64            tick_count: 0,
65            total_duration: Duration::ZERO,
66            avg_tick_duration: Duration::ZERO,
67            min_tick_duration: Duration::MAX,
68            max_tick_duration: Duration::ZERO,
69            final_status: None,
70        }
71    }
72
73    fn update(&mut self, tick_duration: Duration) {
74        self.tick_count += 1;
75        self.total_duration += tick_duration;
76
77        if tick_duration < self.min_tick_duration {
78            self.min_tick_duration = tick_duration;
79        }
80        if tick_duration > self.max_tick_duration {
81            self.max_tick_duration = tick_duration;
82        }
83
84        self.avg_tick_duration = self.total_duration / self.tick_count as u32;
85    }
86
87    fn finalize(&mut self, status: NodeStatus) {
88        self.final_status = Some(status);
89    }
90}
91
92/// Executor for behavior trees with tick-based execution.
93///
94/// This provides a simple interface for running behavior trees at a fixed rate.
95///
96/// # Example
97///
98/// ```rust,no_run
99/// use mecha10_behavior_runtime::prelude::*;
100///
101/// # async fn example(behavior: BoxedBehavior, ctx: Context) -> anyhow::Result<()> {
102/// let mut executor = BehaviorExecutor::new(behavior, 30.0);
103///
104/// let (status, stats) = executor.run_until_complete(&ctx).await?;
105/// println!("Behavior completed with status: {} in {} ticks", status, stats.tick_count);
106/// # Ok(())
107/// # }
108/// ```
109pub struct BehaviorExecutor {
110    behavior: BoxedBehavior,
111    tick_rate_hz: f32,
112    max_ticks: Option<usize>,
113}
114
115/// Generate a `BehaviorExecutor` lifecycle passthrough that logs, delegates to the named
116/// `BoxedBehavior` lifecycle method, and returns `Ok(())` - `init`/`terminate` were flagged by
117/// cargo-dupes as identical bodies save for the log verb and delegate method name.
118macro_rules! lifecycle_step {
119    ($(#[$meta:meta])* $fn_name:ident, $verb:literal, $behavior_method:ident) => {
120        $(#[$meta])*
121        pub async fn $fn_name(&mut self, ctx: &Context) -> anyhow::Result<()> {
122            info!(concat!($verb, " behavior: {}"), self.behavior.name());
123            self.behavior.$behavior_method(ctx).await?;
124            Ok(())
125        }
126    };
127}
128
129impl BehaviorExecutor {
130    mecha10_core::new_with_defaults!(
131        /// Create a new behavior executor.
132        ///
133        /// # Arguments
134        ///
135        /// * `behavior` - The behavior to execute
136        /// * `tick_rate_hz` - Tick rate in Hz (e.g., 30.0 for 30 Hz)
137        pub fn new(behavior: BoxedBehavior, tick_rate_hz: f32) -> Self {
138            max_ticks: None
139        }
140    );
141
142    mecha10_core::builder_setter!(
143        /// Set the maximum number of ticks before timeout.
144        into_some, with_max_ticks, max_ticks, usize
145    );
146
147    lifecycle_step!(
148        /// Initialize the behavior.
149        init, "Initializing", on_init
150    );
151
152    /// Run the behavior until it completes (Success or Failure).
153    ///
154    /// This will tick the behavior at the configured rate until it returns
155    /// a terminal status (Success or Failure).
156    ///
157    /// # Returns
158    ///
159    /// The final status and execution statistics.
160    pub async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
161        let mut stats = ExecutionStats::new();
162        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
163
164        info!(
165            "Starting behavior execution: {} at {} Hz",
166            self.behavior.name(),
167            self.tick_rate_hz
168        );
169
170        loop {
171            interval.tick().await;
172
173            if let Some(max_ticks) = self.max_ticks {
174                if stats.tick_count >= max_ticks {
175                    warn!("Behavior exceeded maximum ticks ({})", max_ticks);
176                    return Err(anyhow::anyhow!("Exceeded maximum ticks"));
177                }
178            }
179
180            let tick_start = Instant::now();
181            let status = self.behavior.tick(ctx).await?;
182            let tick_duration = tick_start.elapsed();
183
184            stats.update(tick_duration);
185
186            debug!(
187                "Tick #{}: status={}, duration={:?}",
188                stats.tick_count, status, tick_duration
189            );
190
191            if status.is_complete() {
192                info!(
193                    "Behavior completed with status: {} after {} ticks ({:?})",
194                    status, stats.tick_count, stats.total_duration
195                );
196                stats.finalize(status);
197                return Ok((status, stats));
198            }
199        }
200    }
201
202    /// Run the behavior for a fixed duration.
203    ///
204    /// This will tick the behavior at the configured rate for the specified duration,
205    /// regardless of whether it completes.
206    pub async fn run_for_duration(
207        &mut self,
208        ctx: &Context,
209        duration: Duration,
210    ) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
211        let mut stats = ExecutionStats::new();
212        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
213        let start_time = Instant::now();
214
215        info!(
216            "Starting behavior execution: {} for {:?} at {} Hz",
217            self.behavior.name(),
218            duration,
219            self.tick_rate_hz
220        );
221
222        let mut last_status = NodeStatus::Running;
223
224        while start_time.elapsed() < duration {
225            interval.tick().await;
226
227            let tick_start = Instant::now();
228            let status = self.behavior.tick(ctx).await?;
229            let tick_duration = tick_start.elapsed();
230
231            stats.update(tick_duration);
232            last_status = status;
233
234            debug!(
235                "Tick #{}: status={}, duration={:?}",
236                stats.tick_count, status, tick_duration
237            );
238
239            if status.is_complete() {
240                info!("Behavior completed early with status: {}", status);
241                break;
242            }
243        }
244
245        info!(
246            "Behavior execution ended after {} ticks ({:?})",
247            stats.tick_count, stats.total_duration
248        );
249        stats.finalize(last_status);
250        Ok((last_status, stats))
251    }
252
253    lifecycle_step!(
254        /// Terminate the behavior.
255        terminate, "Terminating", on_terminate
256    );
257
258    /// Reset the behavior to its initial state.
259    pub async fn reset(&mut self) -> anyhow::Result<()> {
260        info!("Resetting behavior: {}", self.behavior.name());
261        self.behavior.reset().await?;
262        Ok(())
263    }
264}