beetry-node 0.2.0

Beetry library with reusable behavior tree nodes.
Documentation
use beetry_core::{Node, NonEmptyNodes, TickStatus};

use crate::{Indices, control::RunningNodesAborter};

/// Control node that tries children in order until one succeeds.
///
/// `Fallback` ticks children in order on each tick:
///
/// - returns [`TickStatus::Success`] as soon as a child succeeds
/// - returns [`TickStatus::Running`] as soon as a child is still running
/// - returns [`TickStatus::Failure`] only if every child fails on the same tick
///
/// This variant does not remember which child was previously running, so the
/// next tick starts again from the first child.
pub struct Fallback {
    nodes: NonEmptyNodes,
    aborter: RunningNodesAborter,
}

impl Fallback {
    #[must_use]
    pub fn new(nodes: NonEmptyNodes) -> Self {
        Self {
            nodes,
            aborter: RunningNodesAborter::new(),
        }
    }
}

impl Node for Fallback {
    fn tick(&mut self) -> TickStatus {
        let aborter = &mut self.aborter;
        for idx in self.nodes.indices() {
            let node = &mut self.nodes[idx];
            match node.tick() {
                TickStatus::Failure => {
                    aborter.untrack(idx);
                }
                TickStatus::Running => {
                    aborter.abort_if_other_running(&mut self.nodes, idx);
                    aborter.track(idx);
                    return TickStatus::Running;
                }
                TickStatus::Success => {
                    aborter.abort_if_other_running(&mut self.nodes, idx);
                    return TickStatus::Success;
                }
            }
        }
        TickStatus::Failure
    }

    fn abort(&mut self) {
        self.aborter.clear();
        for node in &mut self.nodes {
            node.abort();
        }
    }

    fn reset(&mut self) {
        self.aborter.clear();
        for node in &mut self.nodes {
            node.reset();
        }
    }
}

#[cfg(test)]
mod tests {
    use beetry_core::{Node, TickStatus};

    use super::*;
    use crate::mock_test::{boxed, mock_returns};

    #[test]
    fn success_with_first_success() {
        let nodes = NonEmptyNodes::from([
            boxed(mock_returns([TickStatus::Failure])),
            boxed(mock_returns([TickStatus::Success])),
            boxed(mock_returns([])),
        ]);
        let mut fb = Fallback::new(nodes);

        assert_eq!(fb.tick(), TickStatus::Success);
    }

    #[test]
    fn running_with_first_running() {
        let nodes = NonEmptyNodes::from([
            boxed(mock_returns([TickStatus::Failure])),
            boxed(mock_returns([TickStatus::Running])),
            boxed(mock_returns([])),
        ]);
        let mut fb = Fallback::new(nodes);
        assert_eq!(fb.tick(), TickStatus::Running);
    }

    #[test]
    fn failure_with_all_failed() {
        let nodes = NonEmptyNodes::from([
            boxed(mock_returns([TickStatus::Failure])),
            boxed(mock_returns([TickStatus::Failure])),
        ]);
        let mut fb = Fallback::new(nodes);

        assert_eq!(fb.tick(), TickStatus::Failure);
    }

    #[test]
    fn resets_running() {
        let m1 = mock_returns([
            TickStatus::Failure,
            TickStatus::Failure,
            TickStatus::Running,
        ]);
        let mut m2 = mock_returns([TickStatus::Failure, TickStatus::Running]);
        let mut m3 = mock_returns([TickStatus::Running]);
        m2.expect_abort().once().return_const(());
        m3.expect_abort().once().return_const(());

        let nodes = NonEmptyNodes::from([boxed(m1), boxed(m2), boxed(m3)]);
        let mut fb = Fallback::new(nodes);

        assert_eq!(fb.tick(), TickStatus::Running);
        assert_eq!(fb.tick(), TickStatus::Running);
        assert_eq!(fb.tick(), TickStatus::Running);
    }
}