Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/rust-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
steps:
- name: Install libudev
run: sudo apt-get update && sudo apt-get install libudev-dev pkg-config librust-alsa-sys-dev
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.3
- uses: actions/checkout@v6.0.2
- uses: actions/setup-python@v6.2.0
- uses: pre-commit/action@v3.0.1
build:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ For example, if you have a state `A` and a state `B`:
- Do `A`, `B` repeatedly while `LoopCondition` runs: `WhileAll(LoopCondition, [A, B])`. After *All* nodes `A`, `B` are completed successfully, check the condition node.
- Run `A` and `B` in parallell and wait for both to succeed: `WhenAll([A, B])`
- Run `A` and `B` in parallell and wait for any to succeed: `WhenAny([A, B])`
- Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])`
- Run `A` and `B` in parallell, but `A` has to succeed before `B`: `After([A, B])`

See the `Behavior` enum for more information.
Expand Down
2 changes: 1 addition & 1 deletion bonsai/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ name = "bonsai-bt"
readme = "../README.md"
repository = "https://github.com/sollimann/bonsai.git"
rust-version = "1.80.0"
version = "0.10.0"
version = "0.11.0"

[lib]
name = "bonsai_bt"
Expand Down
6 changes: 6 additions & 0 deletions bonsai/src/behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ pub enum Behavior<A> {
/// Succeeds if all behaviors succeed, but only if succeeding in sequence.
/// Fails if one behavior fails.
After(Vec<Behavior<A>>),
/// Runs all behaviors in parallel until one completes (succeeds or fails).
///
/// Returns the status of the first behavior to complete,
/// whether that is `Success` or `Failure`.
/// If all behaviors remain `Running`, returns `Running`.
Race(Vec<Behavior<A>>),
}

#[cfg(test)]
Expand Down
3 changes: 2 additions & 1 deletion bonsai/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
//! - Do `A`, `B` forever: `While(WaitForever, [A, B])`
//! - Run `A` and `B` in parallell and wait for both to succeed: `WhenAll([A, B])`
//! - Run `A` and `B` in parallell and wait for any to succeed: `WhenAny([A, B])`
//! - Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])`
//! - Run `A` and `B` in parallell, but `A` has to succeed before `B`: `After([A, B])`
//!
//! See the `Behavior` enum for more information.
Expand Down Expand Up @@ -118,7 +119,7 @@
//! ```

pub use behavior::Behavior::{
self, Action, After, AlwaysSucceed, If, Invert, Select, Sequence, Wait, WaitForever, WhenAll, WhenAny, While,
self, Action, After, AlwaysSucceed, If, Invert, Race, Select, Sequence, Wait, WaitForever, WhenAll, WhenAny, While,
WhileAll,
};

Expand Down
17 changes: 17 additions & 0 deletions bonsai/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ pub(crate) enum State<A> {
/// Keeps track of a `WhenAny` behavior. As the states finish, they are set
/// to [`None`].
WhenAny(Vec<Option<State<A>>>),
/// Keeps track of a `Race` behavior.
Race(Vec<Option<State<A>>>),
/// Keeps track of an `After` behavior.
After {
/// The index of the next state that must succeed.
Expand Down Expand Up @@ -166,6 +168,7 @@ impl<A: Clone> State<A> {
}
Behavior::WhenAll(all) => State::WhenAll(all.into_iter().map(|ev| Some(State::new(ev))).collect()),
Behavior::WhenAny(any) => State::WhenAny(any.into_iter().map(|ev| Some(State::new(ev))).collect()),
Behavior::Race(behaviors) => State::Race(behaviors.into_iter().map(|ev| Some(State::new(ev))).collect()),
Behavior::After(after_all) => State::After {
next_success_index: 0,
states: after_all.into_iter().map(State::new).collect(),
Expand Down Expand Up @@ -404,6 +407,20 @@ impl<A: Clone> State<A> {
let any = true;
when_all(any, upd, cursors, e, f, blackboard)
}
(_, &mut Race(ref mut cursors)) => {
// return the result of the first child to complete,
// regardless of whether it succeeds or fails.
for cur in cursors.iter_mut() {
match *cur {
None => {}
Some(ref mut state) => match state.tick(e, blackboard, f) {
(Running, _) => continue,
(status, dt) => return (status, dt),
},
}
}
RUNNING
}
(
_,
&mut After {
Expand Down
8 changes: 8 additions & 0 deletions bonsai/src/visualizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub(crate) enum NodeType<A> {
WhenAll,
WhenAny,
After,
Race,
}

impl<A: Clone + Debug, K: Debug> BT<A, K> {
Expand Down Expand Up @@ -125,6 +126,13 @@ impl<A: Clone + Debug, K: Debug> BT<A, K> {
Self::dfs_recursive(graph, b, node_id)
}
}
Behavior::Race(behaviors) => {
let node_id = graph.add_node(NodeType::Race);
graph.add_edge(parent_node, node_id, 1);
for b in behaviors {
Self::dfs_recursive(graph, b, node_id)
}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions bonsai/src/when_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ where
terminated += 1;
*cur = None;
}
#[allow(clippy::manual_unwrap_or)]
match terminated {
// If there are no events, there is a whole 'dt' left.
0 if cursors.is_empty() => (
Expand Down
96 changes: 94 additions & 2 deletions bonsai/tests/behavior_tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::behavior_tests::TestActions::{Dec, Inc, LessThan, LessThanRunningSuccess};
use bonsai_bt::{
Action, ActionArgs, After, AlwaysSucceed, Event, Failure, Float, If, Invert, Select, Sequence, Status::Running,
Success, UpdateArgs, Wait, WaitForever, WhenAll, While, WhileAll, BT,
Action, ActionArgs, After, AlwaysSucceed, Event, Failure, Float, If, Invert, Race, Select, Sequence,
Status::Running, Success, UpdateArgs, Wait, WaitForever, WhenAll, WhenAny, While, WhileAll, BT,
};

/// Some test actions.
Expand Down Expand Up @@ -610,3 +610,95 @@ fn test_repeat_sequence_empty() {
// panics because no behaviors...
let _state = BT::new(after, ());
}

#[test]
fn race_returns_first_success() {
let a: i32 = 0;
// Inc succeeds immediately, Wait is still running
let behavior = Race(vec![Action(Inc), Wait(10.0)]);
let mut state = BT::new(behavior, ());
let (a, s, _) = tick(a, 0.1, &mut state);
assert_eq!(a, 1);
assert_eq!(s, Success);
}

#[test]
fn race_returns_first_failure() {
let a: i32 = 5;
// LessThan(1) fails immediately since 5 >= 1, Wait is still running
let behavior = Race(vec![Action(LessThan(1)), Wait(10.0)]);
let mut state = BT::new(behavior, ());
let (a, s, _) = tick(a, 0.1, &mut state);
assert_eq!(a, 5);
assert_eq!(s, Failure);
}

#[test]
fn race_running_until_first_completes() {
let a: i32 = 0;
// Both children are time-based, neither completes on first tick
let behavior = Race(vec![Wait(1.0), Wait(2.0)]);
let mut state = BT::new(behavior, ());

// After 0.5s, both still running
let (_a, s, _) = tick(a, 0.5, &mut state);
assert_eq!(s, Running);

// After another 0.5s (total 1.0s), first Wait completes with Success
let (_a, s, _) = tick(_a, 0.5, &mut state);
assert_eq!(s, Success);
}

#[test]
fn race_second_child_wins_if_first_is_running() {
let a: i32 = 0;
let behavior = Race(vec![WaitForever, Action(Inc)]);
let mut state = BT::new(behavior, ());
let (a, s, _) = tick(a, 0.1, &mut state);
assert_eq!(a, 1);
assert_eq!(s, Success);
}

#[test]
fn race_failure_short_circuits_unlike_when_any() {
// the main difference from WhenAny:
// WhenAny would swallow the failure and keep running.
// Race returns the failure immediately.
let a: i32 = 5;
// LessThan(1) fails immediately (5 >= 1), Wait(10.0) is still running
let behavior = Race(vec![Action(LessThan(1)), Wait(10.0)]);
let mut state = BT::new(behavior, ());
let (a, s, _) = tick(a, 0.1, &mut state);
assert_eq!(s, Failure);

// for WhenAny: same children, but failure is swallowed
let behavior_any = WhenAny(vec![Action(LessThan(1)), Wait(10.0)]);
let mut state_any = BT::new(behavior_any, ());
let (_, s_any, _) = tick(a, 0.1, &mut state_any);
assert_eq!(s_any, Running);
}

#[test]
fn race_timeout_pattern() {
let a: i32 = 0;
// Simulate a "slow action" using WaitForever with a 1-second timeout.
// The timeout (Wait) fires first.
let behavior = Race(vec![WaitForever, Wait(1.0)]);
let mut state = BT::new(behavior, ());

let (_, s, _) = tick(a, 0.5, &mut state);
assert_eq!(s, Running);

let (_, s, _) = tick(a, 0.5, &mut state);
assert_eq!(s, Success);
}

#[test]
fn race_empty() {
let a: i32 = 0;
let behavior = Race(vec![]);
let mut state = BT::new(behavior, ());
let (_, s, _) = tick(a, 0.1, &mut state);
// No children means nothing can complete, stays Running
assert_eq!(s, Running);
}
1 change: 1 addition & 0 deletions docs/concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ For example, if you have a state `A` and a state `B`:
- Do `A`, `B` forever: `While(WaitForever, [A, B])`
- Run `A` and `B` in parallel and wait for both to succeed: `WhenAll([A, B])`
- Run `A` and `B` in parallel and wait for any to succeed: `WhenAny([A, B])`
- Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])`
- Run `A` and `B` in parallel, but `A` has to succeed before `B`: `After([A, B])`

See the `Behavior` enum for more information.
Expand Down
Binary file added docs/resources/images/race_timeout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ path = "src/graphviz/main.rs"
[[bin]]
name = "simple_npc_ai"
path = "src/simple_npc_ai/main.rs"

[[bin]]
name = "race_timeout"
path = "src/race_timeout/main.rs"
10 changes: 10 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,13 @@ Compile the behavior tree into a [graphviz](https://graphviz.org/) compatible [D
<p align="center">
<img src="https://github.com/Sollimann/bonsai/blob/main/docs/resources/images/attack_drone.png" width="700">
</p>

## Behavior Timeout (example for Race behavior and long-running jobs)

This simple example shows an example of using the Race behavior to time out a long-running job that takes a random amount of time to complete.

`cargo run --bin race_timeout`

<p align="center">
<img src="https://github.com/Sollimann/bonsai/blob/main/docs/resources/images/race_timeout.png" width="700">
</p>
110 changes: 110 additions & 0 deletions examples/src/race_timeout/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use bonsai_bt::Behavior::Wait;
use bonsai_bt::{
Behavior::Action, Behavior::Race, Behavior::Sequence, Event, Float, Status, Timer, UpdateArgs, BT, RUNNING,
};
use futures::FutureExt;
use rand::Rng;
use std::collections::HashMap;
use std::sync::mpsc::{channel, Receiver};
use std::thread::sleep;
use std::time::Duration;
use tokio::time::sleep as async_sleep;

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub enum MissionAction {
/// The main job that finishes after a random delay
DoWork,
/// Hard deadline that fires after a fixed delay
OnTimeout,
}

pub struct MissionState {
pub work: Option<Receiver<Status>>,
}

/// Simulates a unit of work whose duration is random.
/// Sometimes it finishes before the timeout and sometimes it doesn't.
async fn do_work_task(tx: std::sync::mpsc::Sender<Status>) {
let work_ms: u64 = rand::thread_rng().gen_range(200..=1200);
println!("[do_work] started.");

let step = Duration::from_millis(100);
let mut elapsed = 0u64;
while elapsed < work_ms {
if tx.send(Status::Running).is_err() {
println!("[do_work] preempted by timeout, stopping.");
return;
}
async_sleep(step).await;
elapsed += step.as_millis() as u64;
}

println!("[do_work] finished after {elapsed} ms");
let _ = tx.send(Status::Success);
}

async fn tick(
timer: &mut Timer,
state: &mut MissionState,
bt: &mut BT<MissionAction, HashMap<String, serde_json::Value>>,
) -> std::option::Option<(Status, Float)> {
let dt: Float = timer.get_dt();
let e: Event = UpdateArgs { dt }.into();

bt.tick(
&e,
&mut |args: bonsai_bt::ActionArgs<Event, MissionAction>, _| match *args.action {
MissionAction::DoWork => {
if let Some(rx) = &state.work {
match rx.recv() {
Ok(Status::Running) => RUNNING,
Ok(Status::Success) => {
state.work = None;
(Status::Success, args.dt)
}
Ok(Status::Failure) | Err(_) => {
state.work = None;
(Status::Failure, args.dt)
}
}
} else {
let (tx, rx) = channel();
let (job, handle) = do_work_task(tx).remote_handle();
handle.forget();
tokio::spawn(job);
state.work = Some(rx);
match state.work.as_ref().unwrap().recv().unwrap() {
Status::Running => RUNNING,
s => (s, args.dt),
}
}
}

MissionAction::OnTimeout => {
eprintln!("do_work timed out!");
(Status::Failure, args.dt)
}
},
)
}

#[tokio::main]
async fn main() {
const TIMEOUT_S: Float = 0.6;

let behavior = Sequence(vec![Race(vec![
Action(MissionAction::DoWork),
Sequence(vec![Wait(TIMEOUT_S), Action(MissionAction::OnTimeout)]),
])]);

let mut bt = BT::new(behavior, HashMap::new());
let mut timer = Timer::init_time();
let mut state = MissionState { work: None };

loop {
sleep(Duration::from_millis(50));
if tick(&mut timer, &mut state, &mut bt).await.is_none() {
break;
}
}
}
6 changes: 3 additions & 3 deletions examples/src/simple_npc_ai/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ impl EnemyNPCState {
/// 6. check if action points are available (yes)
/// 7. run
/// 8. shoot (notice that we don't have action points
/// here but we try anyway and move on the sequence)
/// here but we try anyway and move on the sequence)
/// 9. check if action points are available (no)
/// 10. exit the subprogram
/// 11. rest and regain action points
/// (this action returns [Running] until fully rested
/// so control flow is returned to main loop)
/// (this action returns [Running] until fully rested
/// so control flow is returned to main loop)
/// 12. kill the NPC
/// 13. check if the NPC is dead (yes)
/// 14. quit
Expand Down
Loading