mecha10-behavior-runtime 0.1.0

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Execution engine for behavior trees
//!
//! This module provides the executor that runs behavior trees with tick-based execution.

use crate::{BoxedBehavior, NodeStatus};
use mecha10_core::Context;
use std::time::{Duration, Instant};
use tracing::{debug, info, warn};

/// Execution context for behavior trees.
///
/// This wraps the Mecha10 Context and provides additional execution state.
#[derive(Clone)]
pub struct ExecutionContext {
    /// Underlying Mecha10 context
    ctx: Context,

    /// Tick rate for the executor
    tick_rate_hz: f32,
}

impl ExecutionContext {
    /// Create a new execution context.
    pub fn new(ctx: Context, tick_rate_hz: f32) -> Self {
        Self { ctx, tick_rate_hz }
    }

    /// Get the underlying context.
    pub fn context(&self) -> &Context {
        &self.ctx
    }

    /// Get the tick rate in Hz.
    pub fn tick_rate_hz(&self) -> f32 {
        self.tick_rate_hz
    }

    /// Get the tick period as a duration.
    pub fn tick_period(&self) -> Duration {
        Duration::from_secs_f32(1.0 / self.tick_rate_hz)
    }
}

/// Statistics about behavior execution.
#[derive(Debug, Clone)]
pub struct ExecutionStats {
    /// Total number of ticks executed
    pub tick_count: usize,

    /// Total execution time
    pub total_duration: Duration,

    /// Average tick duration
    pub avg_tick_duration: Duration,

    /// Minimum tick duration
    pub min_tick_duration: Duration,

    /// Maximum tick duration
    pub max_tick_duration: Duration,

    /// Final status
    pub final_status: Option<NodeStatus>,
}

impl ExecutionStats {
    fn new() -> Self {
        Self {
            tick_count: 0,
            total_duration: Duration::ZERO,
            avg_tick_duration: Duration::ZERO,
            min_tick_duration: Duration::MAX,
            max_tick_duration: Duration::ZERO,
            final_status: None,
        }
    }

    fn update(&mut self, tick_duration: Duration) {
        self.tick_count += 1;
        self.total_duration += tick_duration;

        if tick_duration < self.min_tick_duration {
            self.min_tick_duration = tick_duration;
        }
        if tick_duration > self.max_tick_duration {
            self.max_tick_duration = tick_duration;
        }

        self.avg_tick_duration = self.total_duration / self.tick_count as u32;
    }

    fn finalize(&mut self, status: NodeStatus) {
        self.final_status = Some(status);
    }
}

/// Executor for behavior trees with tick-based execution.
///
/// This provides a simple interface for running behavior trees at a fixed rate.
///
/// # Example
///
/// ```rust,no_run
/// use mecha10_behavior_runtime::prelude::*;
///
/// # async fn example(behavior: BoxedBehavior, ctx: Context) -> anyhow::Result<()> {
/// let mut executor = BehaviorExecutor::new(behavior, 30.0);
///
/// let (status, stats) = executor.run_until_complete(&ctx).await?;
/// println!("Behavior completed with status: {} in {} ticks", status, stats.tick_count);
/// # Ok(())
/// # }
/// ```
pub struct BehaviorExecutor {
    behavior: BoxedBehavior,
    tick_rate_hz: f32,
    max_ticks: Option<usize>,
}

impl BehaviorExecutor {
    /// Create a new behavior executor.
    ///
    /// # Arguments
    ///
    /// * `behavior` - The behavior to execute
    /// * `tick_rate_hz` - Tick rate in Hz (e.g., 30.0 for 30 Hz)
    pub fn new(behavior: BoxedBehavior, tick_rate_hz: f32) -> Self {
        Self {
            behavior,
            tick_rate_hz,
            max_ticks: None,
        }
    }

    /// Set the maximum number of ticks before timeout.
    pub fn with_max_ticks(mut self, max_ticks: usize) -> Self {
        self.max_ticks = Some(max_ticks);
        self
    }

    /// Initialize the behavior.
    pub async fn init(&mut self, ctx: &Context) -> anyhow::Result<()> {
        info!("Initializing behavior: {}", self.behavior.name());
        self.behavior.on_init(ctx).await?;
        Ok(())
    }

    /// Run the behavior until it completes (Success or Failure).
    ///
    /// This will tick the behavior at the configured rate until it returns
    /// a terminal status (Success or Failure).
    ///
    /// # Returns
    ///
    /// The final status and execution statistics.
    pub async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
        let mut stats = ExecutionStats::new();
        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));

        info!(
            "Starting behavior execution: {} at {} Hz",
            self.behavior.name(),
            self.tick_rate_hz
        );

        loop {
            // Wait for next tick
            interval.tick().await;

            // Check max ticks limit
            if let Some(max_ticks) = self.max_ticks {
                if stats.tick_count >= max_ticks {
                    warn!("Behavior exceeded maximum ticks ({})", max_ticks);
                    return Err(anyhow::anyhow!("Exceeded maximum ticks"));
                }
            }

            // Execute one tick
            let tick_start = Instant::now();
            let status = self.behavior.tick(ctx).await?;
            let tick_duration = tick_start.elapsed();

            stats.update(tick_duration);

            debug!(
                "Tick #{}: status={}, duration={:?}",
                stats.tick_count, status, tick_duration
            );

            // Check for completion
            if status.is_complete() {
                info!(
                    "Behavior completed with status: {} after {} ticks ({:?})",
                    status, stats.tick_count, stats.total_duration
                );
                stats.finalize(status);
                return Ok((status, stats));
            }
        }
    }

    /// Run the behavior for a fixed duration.
    ///
    /// This will tick the behavior at the configured rate for the specified duration,
    /// regardless of whether it completes.
    pub async fn run_for_duration(
        &mut self,
        ctx: &Context,
        duration: Duration,
    ) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
        let mut stats = ExecutionStats::new();
        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
        let start_time = Instant::now();

        info!(
            "Starting behavior execution: {} for {:?} at {} Hz",
            self.behavior.name(),
            duration,
            self.tick_rate_hz
        );

        let mut last_status = NodeStatus::Running;

        while start_time.elapsed() < duration {
            interval.tick().await;

            let tick_start = Instant::now();
            let status = self.behavior.tick(ctx).await?;
            let tick_duration = tick_start.elapsed();

            stats.update(tick_duration);
            last_status = status;

            debug!(
                "Tick #{}: status={}, duration={:?}",
                stats.tick_count, status, tick_duration
            );

            if status.is_complete() {
                info!("Behavior completed early with status: {}", status);
                break;
            }
        }

        info!(
            "Behavior execution ended after {} ticks ({:?})",
            stats.tick_count, stats.total_duration
        );
        stats.finalize(last_status);
        Ok((last_status, stats))
    }

    /// Terminate the behavior.
    pub async fn terminate(&mut self, ctx: &Context) -> anyhow::Result<()> {
        info!("Terminating behavior: {}", self.behavior.name());
        self.behavior.on_terminate(ctx).await?;
        Ok(())
    }

    /// Reset the behavior to its initial state.
    pub async fn reset(&mut self) -> anyhow::Result<()> {
        info!("Resetting behavior: {}", self.behavior.name());
        self.behavior.reset().await?;
        Ok(())
    }
}