diff --git a/big-brain-derive/src/action.rs b/big-brain-derive/src/action.rs index e1c01c6da5c64da72573ce814534961f9eaeea3c..e023bdd8a8ac126a048ebc62c8ace6a441905547 100644 --- a/big-brain-derive/src/action.rs +++ b/big-brain-derive/src/action.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::quote; #[derive(Debug, FromDeriveInput)] -#[darling(attributes(action), supports(struct_named))] +#[darling(attributes(action))] pub struct Action { ident: syn::Ident, generics: syn::Generics, diff --git a/examples/basic.ron b/examples/basic.ron index 2b4addb86bb95c1aef5aef59b6b4ef275ee02dc1..1dfd451c11e3bbf389a985ee347fcb8fb35ecdb4 100644 --- a/examples/basic.ron +++ b/examples/basic.ron @@ -1,19 +1,23 @@ ( + // We have to use these special "externally tagged" specifiers instead of, + // say, `FirstToScore(threshold: 80.0)` due to a bug in RON itself: + // https://github.com/ron-rs/ron/issues/123 picker: {"FirstToScore": (threshold: 80.0)}, choices: [( - consider: [{"Bladder": (weight: 3.0)}], + when: {"Bladder": ()}, + // Thinkers happen to also be actions, so you can nest theem! then: {"Thinker": ( picker: {"FirstToScore": (threshold: 80.0)}, choices: [( - consider: [{"Bladder": (weight: 3.0)}], + when: [{"Bladder": ()}], then: {"Pee": ()} )] )} ), ( - consider: [{"Thirst": (weight: 2.0)}], + when: {"Thirst": ()}, then: {"Drink": ()}, ), ( - consider: [{"Hunger": (weight: 2.0)}], + when: {"Hunger": ()}, then: {"Eat": ()}, )], otherwise: Some({"Meander": ()}), diff --git a/examples/thirst.rs b/examples/thirst.rs index 7c30b46be7f72f0071389ffb233a1410978002a1..8d6d28dd3449e6f95ee4caf3f0a6944a479e02d9 100644 --- a/examples/thirst.rs +++ b/examples/thirst.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use big_brain::{evaluators::*, *}; +use big_brain::*; // First, we define a "Thirst" component and associated system. This is NOT // THE AI. It's a plain old system that just makes an entity "thirstier" over @@ -21,6 +21,7 @@ impl Thirst { pub fn thirst_system(time: Res<Time>, mut thirsts: Query<&mut Thirst>) { for mut thirst in thirsts.iter_mut() { thirst.thirst += thirst.per_second * (time.delta().as_micros() as f32 / 1000000.0); + println!("Thirst: {}", thirst.thirst); } } @@ -33,7 +34,7 @@ pub fn thirst_system(time: Res<Time>, mut thirsts: Query<&mut Thirst>) { // These actions will be spawned and queued by the game engine when their // conditions trigger (we'll configure what these are later). #[derive(Debug, Action)] -pub struct DrinkAction {} +pub struct DrinkAction; // Associated with that DrinkAction, you then need to have a system that will // actually execute those actions when they're "spawned" by the Big Brain @@ -52,7 +53,7 @@ fn drink_action_system( // is in. You can think of Actions as state machines. They get requested, // they can be cancelled, they can run to completion, etc. Cancellations // usually happen because the target action changed (due to a different - // Consideration winning). But you can also cancel the actions yourself by + // Scorer winning). But you can also cancel the actions yourself by // setting the state in the Action system. mut query: Query<(&Parent, &DrinkAction, &mut ActionState)>, ) { @@ -83,36 +84,22 @@ fn drink_action_system( // in the docs (later), but for now, just put them in there and trust the // system. :) #[derive(Debug, Scorer)] -pub struct ScoreThirst { - #[scorer(default)] - pub evaluator: PowerEvaluator, - #[scorer(param)] - pub weight: f32, -} +pub struct ScoreThirst; // Look familiar? Similar dance to Actions here. pub fn score_thirst_system( thirsts: Query<&Thirst>, // Same dance with the Parent here, but now we've added a Utility! - mut query: Query<(&Parent, &ScoreThirst, &mut Score)>, + mut query: Query<(&Parent, &mut Score), With<ScoreThirst>>, ) { - for (Parent(actor), scorer, mut score) in query.iter_mut() { + for (Parent(actor), mut score) in query.iter_mut() { if let Ok(thirst) = thirsts.get(*actor) { - // This is really what the job of a Consideration is. To calculate + // This is really what the job of a Scorer is. To calculate // a generic Utility value that the Big Brain engine will compare // against others, over time, and use to make decisions. This is // generally "the higher the better", and "first across the finish - // line", but that's all configurable! You can customize that to - // your heart's extent using Measures and Pickers. - // - // A note: You don't actually *need* evaluators/weights. You can - // literally just use linear values here and set thresholds - // accordingly. The evaluator is just there to give the value a - // bit of a curve. - *score = Score { - value: scorer.evaluator.evaluate(thirst.thirst), - weight: scorer.weight, - }; + // line", but that's all configurable using Pickers! + *score = Score(thirst.thirst); } } } @@ -123,15 +110,15 @@ pub fn score_thirst_system( // Thinkers are configured using RON right now, with a DSL that makes it easy // to define, in data, the actual behavior you want. pub fn init_entities(mut cmd: Commands) { - let actor = cmd.spawn().insert(Thirst::new(80.0, 2.0)).id(); + let actor = cmd.spawn().insert(Thirst::new(70.0, 2.0)).id(); - // Here's a very simple one that only has one consideration and one + // Here's a very simple one that only has one scorer and one // associated action. But you can have more of them, and even nest them by // using more Thinkers (which are actually themselves Actions). See // basic.ron in examples/ for a more involved Thinker definition. // // Ultimately, these Thinkers are meant to be usable by non-programmers: - // You, the developer, create Actions and Considerations, and someone else + // You, the developer, create Actions and Scorers, and someone else // is then able to put them all together like LEGOs into all sorts of // intricate logic. Thinker::load_from_str( @@ -139,7 +126,8 @@ pub fn init_entities(mut cmd: Commands) { ( picker: {"FirstToScore": (threshold: 80.0)}, choices: [( - consider: [{"ThirstConsideration": (weight: 2.0)}], + when: {"ScoreThirst": ()}, + // This action will fire when (and as long as) ScoreThirst scores >=80.0. then: {"DrinkAction": ()}, )], ) diff --git a/src/choices.rs b/src/choices.rs index 8bd5103c219293e7b4ba14457debe358ca722fb1..bdc756f73aa56528648168b5480c3f88d1cad951 100644 --- a/src/choices.rs +++ b/src/choices.rs @@ -4,47 +4,31 @@ use serde::Deserialize; use crate::{ actions::{Action, ActionState}, scorers::{Scorer, Score}, - measures::{Measure, WeightedMeasure}, thinker::{ActionEnt, ScorerEnt}, }; // Contains different types of Considerations and Actions #[derive(Debug)] pub struct Choice { - pub measure: Box<dyn Measure>, - pub utilities: Vec<ScorerEnt>, + pub scorer: ScorerEnt, pub action_state: ActionEnt, } impl Choice { - pub fn calculate(&self, utilities: &Query<&Score>) -> f32 { - self.measure.calculate( - self.utilities - .iter() - .map(|choice_cons| { - utilities - .get(choice_cons.0) - .expect("Where did the utility go?") - }) - .collect(), - ) + pub fn calculate(&self, scores: &Query<&Score>) -> f32 { + scores.get(self.scorer.0).expect("Where did the score go?").0 } } #[derive(Debug, Deserialize)] pub struct ChoiceBuilder { - pub consider: Vec<Box<dyn Scorer>>, + pub when: Box<dyn Scorer>, pub then: Box<dyn Action>, } impl ChoiceBuilder { pub fn build(self, actor: Entity, cmd: &mut Commands) -> Choice { let action = self.then; Choice { - measure: Box::new(WeightedMeasure), - utilities: self - .consider - .iter() - .map(|cons| cons.build(actor, cmd)) - .collect(), + scorer: self.when.build(actor, cmd), action_state: ActionState::build(action, actor, cmd), } } diff --git a/src/lib.rs b/src/lib.rs index 7aa262de456350631786e9d198819388a83e296b..a6f21184eea27e63a0625d430d3fb5f0c297d3b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ pub use scorers::*; pub use thinker::*; pub mod evaluators; -pub mod measures; pub mod pickers; mod actions; diff --git a/src/measures.rs b/src/measures.rs deleted file mode 100644 index 57f914473f3c4d4bdba1797c0c122c0999593316..0000000000000000000000000000000000000000 --- a/src/measures.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::Score; - -#[typetag::serde] -pub trait Measure: std::fmt::Debug + Sync + Send { - fn calculate(&self, utilities: Vec<&Score>) -> f32; -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct WeightedMeasure; - -#[typetag::serde] -impl Measure for WeightedMeasure { - fn calculate(&self, utilities: Vec<&Score>) -> f32 { - let wsum: f32 = utilities.iter().map(|el| el.weight).sum(); - if wsum == 0.0 { - 0.0 - } else { - utilities - .iter() - .map(|el| el.weight / wsum * el.value.powf(2.0)) - .sum::<f32>() - .powf(1.0 / 2.0) - } - } -} diff --git a/src/scorers.rs b/src/scorers.rs index 7f80f24c64f8db6b3ec4055a9b59a5b2220c29f9..1d17d84d76a3e43238ed9284ddcfa1b7863a5370 100644 --- a/src/scorers.rs +++ b/src/scorers.rs @@ -3,13 +3,10 @@ use bevy::prelude::*; use crate::ScorerEnt; #[derive(Debug, Clone, Default)] -pub struct Score { - pub value: f32, - pub weight: f32, -} +pub struct Score(pub f32); /** -This trait defines new considerations. In general, you should use the [derive macro](derive.Consideration.html) instead. +This trait defines new Scorers. In general, you should use the [derive macro](derive.Scorer.html) instead. */ #[typetag::deserialize] pub trait Scorer: std::fmt::Debug + Sync + Send {