Use of Reasoner decision maker from emergent crate
Usage of Reasoner
decision maker from emergent
crate isn't much different
from what we have made by ourselves - what's different, considerations are actually
separate structs for which we implement Consideration
trait but we could also
just use scoring functions and wrap them with ClosureConsideration
, but we
want to keep ourselves on the full modularity track - the point is, there are
many ways you can organize your logic with emergent
crate, it is all for you
to decide what works best for you.
#![allow(unused)]
fn main() {
extern crate emergent;
use emergent::prelude::*;
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
enum EnemyState {
Idle,
GatherFood,
GatherWood,
AttackOpponent,
}
struct EnemyData {
hunger: f32,
distance_to_food: f32,
distance_to_trees: f32,
wood_needed: usize,
distance_to_opponent: f32,
opponent_strength: f32,
}
struct Enemy {
data: EnemyData,
reasoner: Reasoner<EnemyData, EnemyState>,
}
impl Enemy {
fn new() -> Self {
let data = EnemyData {
hunger: 0.0,
distance_to_food: 1.0,
distance_to_trees: 1.0,
wood_needed: 0,
distance_to_opponent: 1.0,
opponent_strength: 0.0,
};
let reasoner = ReasonerBuilder::default()
// Just like with conditions, Consideration trait is implemented to scalars
// so we can just use constants here.
// This translates to: `0.001`
.state(EnemyState::Idle, ReasonerState::new(0.001, EnemyIdleTask))
.state(EnemyState::GatherFood, ReasonerState::new(
// Product evaluator will multiply all its children consideration scores.
// This translates to: `hunger * (1 - food proximity)`
EvaluatorProduct::default()
.consideration(Hunger)
.consideration(ConsiderationRemap::new(
FoodProximity,
ReverseScoreMapping,
)),
EnemyGatherFoodTask,
))
.state(EnemyState::GatherWood, ReasonerState::new(
// This translates to: `(wood needed ? 1 : 0) * (1 - trees proximity)`
EvaluatorProduct::default()
.consideration(ConditionConsideration::unit(WoodNeeded))
.consideration(ConsiderationRemap::new(
TreesProximity,
ReverseScoreMapping,
)),
EnemyGatherWoodTask,
))
.state(EnemyState::AttackOpponent, ReasonerState::new(
// This translates to: `(1 - opponent proximity) + opponent strength`
EvaluatorSum::default()
.consideration(ConsiderationRemap::new(
OpponentProximity,
ReverseScoreMapping,
))
.consideration(OpponentStrength),
EnemyAttackOpponentTask,
))
.build();
Self { data, reasoner }
}
fn tick(&mut self) {
self.reasoner.process(&mut self.data);
self.reasoner.update(&mut self.data);
}
}
// Conditions
struct WoodNeeded;
impl Condition<EnemyData> for WoodNeeded {
fn validate(&self, memory: &EnemyData) -> bool {
memory.wood_needed > 0
}
}
// Considerations
struct Hunger;
impl Consideration<EnemyData> for Hunger {
fn score(&self, memory: &EnemyData) -> Scalar {
memory.hunger
}
}
struct FoodProximity;
impl Consideration<EnemyData> for FoodProximity {
fn score(&self, memory: &EnemyData) -> Scalar {
memory.distance_to_food
}
}
struct TreesProximity;
impl Consideration<EnemyData> for TreesProximity {
fn score(&self, memory: &EnemyData) -> Scalar {
memory.distance_to_trees
}
}
struct OpponentProximity;
impl Consideration<EnemyData> for OpponentProximity {
fn score(&self, memory: &EnemyData) -> Scalar {
memory.distance_to_opponent
}
}
struct OpponentStrength;
impl Consideration<EnemyData> for OpponentStrength {
fn score(&self, memory: &EnemyData) -> Scalar {
memory.opponent_strength
}
}
// Enemy state tasks
struct EnemyIdleTask;
impl Task<EnemyData> for EnemyIdleTask {}
struct EnemyGatherFoodTask;
impl Task<EnemyData> for EnemyGatherFoodTask {
fn on_enter(&mut self, memory: &mut EnemyData) {
memory.hunger = 0.0;
memory.distance_to_food = 1.0;
}
}
struct EnemyGatherWoodTask;
impl Task<EnemyData> for EnemyGatherWoodTask {
fn on_enter(&mut self, memory: &mut EnemyData) {
memory.wood_needed = memory.wood_needed.max(1) - 1;
memory.distance_to_trees = 1.0;
}
}
struct EnemyAttackOpponentTask;
impl Task<EnemyData> for EnemyAttackOpponentTask {
fn on_enter(&mut self, memory: &mut EnemyData) {
memory.distance_to_opponent = 1.0;
memory.opponent_strength = 0.0;
}
}
// Test run
let mut enemy = Enemy::new();
assert_eq!(enemy.reasoner.active_state(), None);
assert_eq!(enemy.data.hunger, 0.0);
assert_eq!(enemy.data.distance_to_food, 1.0);
assert_eq!(enemy.data.distance_to_trees, 1.0);
assert_eq!(enemy.data.wood_needed, 0);
assert_eq!(enemy.data.distance_to_opponent, 1.0);
assert_eq!(enemy.data.opponent_strength, 0.0);
enemy.data.hunger = 0.5;
enemy.data.distance_to_food = 0.9;
enemy.data.distance_to_trees = 0.5;
enemy.data.opponent_strength = 0.2;
enemy.tick();
assert_eq!(enemy.reasoner.active_state(), Some(&EnemyState::AttackOpponent));
assert_eq!(enemy.data.hunger, 0.5);
assert_eq!(enemy.data.distance_to_food, 0.9);
assert_eq!(enemy.data.distance_to_trees, 0.5);
assert_eq!(enemy.data.wood_needed, 0);
assert_eq!(enemy.data.distance_to_opponent, 1.0);
assert_eq!(enemy.data.opponent_strength, 0.0);
enemy.tick();
assert_eq!(enemy.reasoner.active_state(), Some(&EnemyState::GatherFood));
assert_eq!(enemy.data.hunger, 0.0);
assert_eq!(enemy.data.distance_to_food, 1.0);
assert_eq!(enemy.data.distance_to_trees, 0.5);
assert_eq!(enemy.data.wood_needed, 0);
assert_eq!(enemy.data.distance_to_opponent, 1.0);
assert_eq!(enemy.data.opponent_strength, 0.0);
enemy.data.wood_needed = 1;
enemy.tick();
assert_eq!(enemy.reasoner.active_state(), Some(&EnemyState::GatherWood));
assert_eq!(enemy.data.hunger, 0.0);
assert_eq!(enemy.data.distance_to_food, 1.0);
assert_eq!(enemy.data.distance_to_trees, 1.0);
assert_eq!(enemy.data.wood_needed, 0);
assert_eq!(enemy.data.distance_to_opponent, 1.0);
assert_eq!(enemy.data.opponent_strength, 0.0);
enemy.tick();
assert_eq!(enemy.reasoner.active_state(), Some(&EnemyState::Idle));
assert_eq!(enemy.data.hunger, 0.0);
assert_eq!(enemy.data.distance_to_food, 1.0);
assert_eq!(enemy.data.distance_to_trees, 1.0);
assert_eq!(enemy.data.wood_needed, 0);
assert_eq!(enemy.data.distance_to_opponent, 1.0);
assert_eq!(enemy.data.opponent_strength, 0.0);
}