From 6c736374b4afd60af592a357ad2403304d3638d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kat=20March=C3=A1n?= <kzm@zkat.tech>
Date: Mon, 26 Apr 2021 19:16:00 -0700
Subject: [PATCH] feat(actions): Add new Concurrently composite action

Fixes: https://github.com/zkat/big-brain/issues/22
---
 src/actions.rs | 141 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs     |   3 +-
 2 files changed, 143 insertions(+), 1 deletion(-)

diff --git a/src/actions.rs b/src/actions.rs
index b569886..675044f 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -232,3 +232,144 @@ pub fn steps_system(
         }
     }
 }
+
+/**
+[`ActionBuilder`] for the [`Concurrent`] component. Constructed through `Concurrent::build()`.
+*/
+#[derive(Debug)]
+pub struct ConcurrentlyBuilder {
+    actions: Vec<Arc<dyn ActionBuilder>>,
+}
+
+impl ConcurrentlyBuilder {
+    /**
+    Add an action to execute. Order does not matter.
+    */
+    pub fn push(mut self, action_builder: impl ActionBuilder + 'static) -> Self {
+        self.actions.push(Arc::new(action_builder));
+        self
+    }
+}
+
+impl ActionBuilder for ConcurrentlyBuilder {
+    fn build(&self, cmd: &mut Commands, action: Entity, actor: Entity) {
+        let children: Vec<Entity> = self
+            .actions
+            .iter()
+            .map(|action| action.attach(cmd, actor))
+            .collect();
+        cmd.entity(action)
+            .insert(Name::new("Concurrent Action"))
+            .push_children(&children[..])
+            .insert(Concurrently {
+                actions: children.into_iter().map(ActionEnt).collect(),
+            });
+    }
+}
+
+/**
+Composite Action that executes a number of Actions concurrently, as long as they all result in a `Success`ful [`ActionState`].
+
+### Example
+
+```ignore
+Thinker::build()
+    .when(
+        MyScorer,
+        Concurrent::build()
+            .push(MyAction::build())
+            .push(MyOtherAction::build())
+        )
+```
+*/
+#[derive(Debug)]
+pub struct Concurrently {
+    actions: Vec<ActionEnt>,
+}
+
+impl Concurrently {
+    /**
+    Construct a new [`ConcurrentBuilder`] to define the actions to take.
+    */
+    pub fn build() -> ConcurrentlyBuilder {
+        ConcurrentlyBuilder {
+            actions: Vec::new(),
+        }
+    }
+}
+
+/**
+System that takes care of executing any existing [`Concurrent`] Actions.
+*/
+pub fn concurrent_system(
+    concurrent_q: Query<(Entity, &Concurrently)>,
+    mut states_q: Query<&mut ActionState>,
+) {
+    use ActionState::*;
+    for (seq_ent, concurrent_action) in concurrent_q.iter() {
+        let current_state = states_q.get_mut(seq_ent).expect("uh oh").clone();
+        match current_state {
+            Requested => {
+                // Begin at the beginning
+                let mut current_state = states_q.get_mut(seq_ent).expect("uh oh");
+                *current_state = Executing;
+                for ActionEnt(child_ent) in concurrent_action.actions.iter() {
+                    let mut child_state = states_q.get_mut(*child_ent).expect("uh oh");
+                    *child_state = Requested;
+                }
+            }
+            Executing => {
+                let mut all_success = true;
+                let mut failed_idx = None;
+                for (idx, ActionEnt(child_ent)) in concurrent_action.actions.iter().enumerate() {
+                    let mut child_state = states_q.get_mut(*child_ent).expect("uh oh");
+                    match *child_state {
+                        Failure => {
+                            failed_idx = Some(idx);
+                            all_success = false;
+                        }
+                        Success => {}
+                        _ => {
+                            all_success = false;
+                            if failed_idx.is_some() {
+                                *child_state = Cancelled;
+                            }
+                        }
+                    }
+                }
+                if all_success {
+                    let mut state_var = states_q.get_mut(seq_ent).expect("uh oh");
+                    *state_var = Success;
+                } else if let Some(idx) = failed_idx {
+                    for ActionEnt(child_ent) in concurrent_action.actions.iter().take(idx) {
+                        let mut child_state = states_q.get_mut(*child_ent).expect("uh oh");
+                        match *child_state {
+                            Failure | Success => {}
+                            _ => {
+                                *child_state = Cancelled;
+                            }
+                        }
+                    }
+                }
+
+            }
+            Cancelled => {
+                // Cancel all actions
+                for ActionEnt(child_ent) in concurrent_action.actions.iter() {
+                    let mut child_state = states_q.get_mut(*child_ent).expect("uh oh");
+                    match *child_state {
+                        Init | Success | Failure => {
+                            // Do nothing
+                        }
+                        _ => {
+                            *child_state = Cancelled;
+                        }
+                    }
+                }
+            }
+            Init | Success | Failure => {
+                // Do nothing.
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 5174ba6..a6d2eed 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -163,7 +163,7 @@ pub mod prelude {
     use super::*;
 
     pub use super::BigBrainPlugin;
-    pub use actions::{ActionBuilder, ActionState, Steps};
+    pub use actions::{ActionBuilder, ActionState, Steps, Concurrently};
     pub use pickers::{FirstToScore, Picker};
     pub use scorers::{AllOrNothing, FixedScore, Score, ScorerBuilder, SumOfScorers, WinningScorer};
     pub use thinker::{Actor, Thinker, ThinkerBuilder};
@@ -196,6 +196,7 @@ impl Plugin for BigBrainPlugin {
         app.add_system(thinker::thinker_component_detach_system.system());
         app.add_system(thinker::actor_gone_cleanup.system());
         app.add_system(actions::steps_system.system());
+        app.add_system(actions::concurrent_system.system());
         app.add_system(scorers::fixed_score_system.system());
         app.add_system(scorers::all_or_nothing_system.system());
         app.add_system(scorers::sum_of_scorers_system.system());
-- 
GitLab