use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct WorldState {
bools: HashMap<String, bool>,
floats: HashMap<String, f32>,
}
impl WorldState {
pub fn new() -> Self { Self::default() }
pub fn set_bool(&mut self, key: &str, value: bool) {
self.bools.insert(key.to_string(), value);
}
pub fn get_bool(&self, key: &str) -> bool {
*self.bools.get(key).unwrap_or(&false)
}
pub fn has_bool(&self, key: &str) -> bool {
self.bools.contains_key(key)
}
pub fn set_float(&mut self, key: &str, value: f32) {
self.floats.insert(key.to_string(), value);
}
pub fn get_float(&self, key: &str) -> f32 {
*self.floats.get(key).unwrap_or(&0.0)
}
pub fn has_float(&self, key: &str) -> bool {
self.floats.contains_key(key)
}
pub fn satisfies(&self, goal: &WorldState) -> bool {
for (k, &v) in &goal.bools {
if self.get_bool(k) != v { return false; }
}
for (k, &v) in &goal.floats {
if self.get_float(k) < v { return false; }
}
true
}
pub fn distance_to(&self, goal: &WorldState) -> usize {
let bool_unsatisfied = goal.bools.iter()
.filter(|(k, &v)| self.get_bool(k) != v)
.count();
let float_unsatisfied = goal.floats.iter()
.filter(|(k, &v)| self.get_float(k) < v)
.count();
bool_unsatisfied + float_unsatisfied
}
pub fn apply(&self, effects: &ActionEffects) -> WorldState {
let mut next = self.clone();
for (k, &v) in &effects.bools { next.bools.insert(k.clone(), v); }
for (k, &v) in &effects.floats_add {
let cur = next.get_float(k);
next.floats.insert(k.clone(), cur + v);
}
for (k, &v) in &effects.floats_set {
next.floats.insert(k.clone(), v);
}
next
}
pub fn merge_from(&mut self, other: &WorldState) {
for (k, &v) in &other.bools { self.bools.insert(k.clone(), v); }
for (k, &v) in &other.floats { self.floats.insert(k.clone(), v); }
}
pub fn is_empty(&self) -> bool {
self.bools.is_empty() && self.floats.is_empty()
}
fn snapshot_key(&self) -> StateKey {
let mut bool_pairs: Vec<(String, bool)> = self.bools.iter()
.map(|(k, &v)| (k.clone(), v)).collect();
bool_pairs.sort_by(|a, b| a.0.cmp(&b.0));
let mut float_pairs: Vec<(String, u32)> = self.floats.iter()
.map(|(k, &v)| (k.clone(), v.to_bits())).collect();
float_pairs.sort_by(|a, b| a.0.cmp(&b.0));
StateKey { bools: bool_pairs, floats: float_pairs }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct StateKey {
bools: Vec<(String, bool)>,
floats: Vec<(String, u32)>,
}
#[derive(Debug, Clone, Default)]
pub struct ActionEffects {
pub bools: HashMap<String, bool>,
pub floats_add: HashMap<String, f32>,
pub floats_set: HashMap<String, f32>,
}
impl ActionEffects {
pub fn new() -> Self { Self::default() }
pub fn set_bool(mut self, key: &str, value: bool) -> Self {
self.bools.insert(key.to_string(), value);
self
}
pub fn add_float(mut self, key: &str, delta: f32) -> Self {
self.floats_add.insert(key.to_string(), delta);
self
}
pub fn set_float(mut self, key: &str, value: f32) -> Self {
self.floats_set.insert(key.to_string(), value);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Preconditions {
pub bools: HashMap<String, bool>,
pub floats_gte: HashMap<String, f32>,
pub floats_lte: HashMap<String, f32>,
pub floats_gt: HashMap<String, f32>,
pub floats_lt: HashMap<String, f32>,
}
impl Preconditions {
pub fn new() -> Self { Self::default() }
pub fn require_bool(mut self, key: &str, value: bool) -> Self {
self.bools.insert(key.to_string(), value);
self
}
pub fn require_float_gte(mut self, key: &str, min: f32) -> Self {
self.floats_gte.insert(key.to_string(), min);
self
}
pub fn require_float_lte(mut self, key: &str, max: f32) -> Self {
self.floats_lte.insert(key.to_string(), max);
self
}
pub fn require_float_gt(mut self, key: &str, min: f32) -> Self {
self.floats_gt.insert(key.to_string(), min);
self
}
pub fn require_float_lt(mut self, key: &str, max: f32) -> Self {
self.floats_lt.insert(key.to_string(), max);
self
}
pub fn satisfied_by(&self, state: &WorldState) -> bool {
for (k, &v) in &self.bools {
if state.get_bool(k) != v { return false; }
}
for (k, &t) in &self.floats_gte { if state.get_float(k) < t { return false; } }
for (k, &t) in &self.floats_lte { if state.get_float(k) > t { return false; } }
for (k, &t) in &self.floats_gt { if state.get_float(k) <= t { return false; } }
for (k, &t) in &self.floats_lt { if state.get_float(k) >= t { return false; } }
true
}
}
#[derive(Debug, Clone)]
pub struct Action {
pub name: String,
pub cost: f32,
pub preconditions: Preconditions,
pub effects: ActionEffects,
pub duration_secs: f32,
pub interrupt_priority: u32,
pub disabled: bool,
pub tags: Vec<String>,
}
impl Action {
pub fn new(name: &str, cost: f32) -> Self {
Self {
name: name.to_string(),
cost,
preconditions: Preconditions::new(),
effects: ActionEffects::new(),
duration_secs: 0.0,
interrupt_priority: 0,
disabled: false,
tags: Vec::new(),
}
}
pub fn require_bool(mut self, key: &str, value: bool) -> Self {
self.preconditions = self.preconditions.require_bool(key, value);
self
}
pub fn require_float_gte(mut self, key: &str, min: f32) -> Self {
self.preconditions = self.preconditions.require_float_gte(key, min);
self
}
pub fn require_float_lte(mut self, key: &str, max: f32) -> Self {
self.preconditions = self.preconditions.require_float_lte(key, max);
self
}
pub fn require_float_gt(mut self, key: &str, val: f32) -> Self {
self.preconditions = self.preconditions.require_float_gt(key, val);
self
}
pub fn require_float_lt(mut self, key: &str, val: f32) -> Self {
self.preconditions = self.preconditions.require_float_lt(key, val);
self
}
pub fn effect_bool(mut self, key: &str, value: bool) -> Self {
self.effects = self.effects.set_bool(key, value);
self
}
pub fn effect_add_float(mut self, key: &str, delta: f32) -> Self {
self.effects = self.effects.add_float(key, delta);
self
}
pub fn effect_set_float(mut self, key: &str, value: f32) -> Self {
self.effects = self.effects.set_float(key, value);
self
}
pub fn with_duration(mut self, secs: f32) -> Self {
self.duration_secs = secs;
self
}
pub fn with_interrupt_priority(mut self, p: u32) -> Self {
self.interrupt_priority = p;
self
}
pub fn with_tag(mut self, tag: &str) -> Self {
self.tags.push(tag.to_string());
self
}
pub fn disabled(mut self) -> Self {
self.disabled = true;
self
}
pub fn is_applicable(&self, state: &WorldState) -> bool {
!self.disabled && self.preconditions.satisfied_by(state)
}
pub fn apply_effects(&self, state: &WorldState) -> WorldState {
state.apply(&self.effects)
}
}
#[derive(Debug, Clone)]
pub struct Goal {
pub name: String,
pub state: WorldState,
pub priority: u32,
pub ttl_secs: Option<f32>,
created_at: f32,
}
impl Goal {
pub fn new(name: &str, state: WorldState, priority: u32) -> Self {
Self { name: name.to_string(), state, priority, ttl_secs: None, created_at: 0.0 }
}
pub fn with_ttl(mut self, ttl_secs: f32) -> Self {
self.ttl_secs = Some(ttl_secs);
self
}
pub fn is_expired(&self, sim_time: f32) -> bool {
self.ttl_secs.map_or(false, |ttl| sim_time - self.created_at > ttl)
}
}
#[derive(Debug, Default)]
pub struct GoalStack {
goals: Vec<Goal>,
sim_time: f32,
}
impl GoalStack {
pub fn new() -> Self { Self::default() }
pub fn push(&mut self, mut goal: Goal) {
goal.created_at = self.sim_time;
self.goals.push(goal);
self.goals.sort_by(|a, b| b.priority.cmp(&a.priority));
}
pub fn remove(&mut self, name: &str) {
self.goals.retain(|g| g.name != name);
}
pub fn active(&self) -> Option<&Goal> {
self.goals.iter().find(|g| !g.is_expired(self.sim_time))
}
pub fn tick(&mut self, dt: f32) {
self.sim_time += dt;
let t = self.sim_time;
self.goals.retain(|g| !g.is_expired(t));
}
pub fn is_empty(&self) -> bool { self.goals.is_empty() }
pub fn len(&self) -> usize { self.goals.len() }
pub fn iter(&self) -> impl Iterator<Item = &Goal> {
self.goals.iter()
}
pub fn has_goal(&self, name: &str) -> bool {
self.goals.iter().any(|g| g.name == name && !g.is_expired(self.sim_time))
}
pub fn sim_time(&self) -> f32 { self.sim_time }
}
#[derive(Clone)]
struct SearchNode {
state: WorldState,
path: Vec<String>,
cost: f32,
heuristic: usize,
}
impl SearchNode {
fn f(&self) -> f32 { self.cost + self.heuristic as f32 }
fn f_ord(&self) -> u64 { (self.f() * 1_000_000.0) as u64 }
}
impl PartialEq for SearchNode {
fn eq(&self, other: &Self) -> bool { self.f_ord() == other.f_ord() }
}
impl Eq for SearchNode {}
impl PartialOrd for SearchNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Ord for SearchNode {
fn cmp(&self, other: &Self) -> Ordering {
other.f_ord().cmp(&self.f_ord())
}
}
pub struct GoapPlanner;
impl GoapPlanner {
pub fn plan(
start: &WorldState,
goal: &WorldState,
actions: &[Action],
max_depth: usize,
) -> Option<Vec<String>> {
if start.satisfies(goal) {
return Some(Vec::new()); }
let mut open: BinaryHeap<SearchNode> = BinaryHeap::new();
let mut closed: HashSet<StateKey> = HashSet::new();
open.push(SearchNode {
state: start.clone(),
path: Vec::new(),
cost: 0.0,
heuristic: start.distance_to(goal),
});
while let Some(node) = open.pop() {
if node.state.satisfies(goal) {
return Some(node.path);
}
if node.path.len() >= max_depth { continue; }
let key = node.state.snapshot_key();
if closed.contains(&key) { continue; }
closed.insert(key);
for action in actions {
if !action.is_applicable(&node.state) { continue; }
let next_state = action.apply_effects(&node.state);
let next_key = next_state.snapshot_key();
if closed.contains(&next_key) { continue; }
let next_cost = node.cost + action.cost;
let mut next_path = node.path.clone();
next_path.push(action.name.clone());
open.push(SearchNode {
state: next_state.clone(),
path: next_path,
cost: next_cost,
heuristic: next_state.distance_to(goal),
});
}
}
None }
pub fn plan_with_cost(
start: &WorldState,
goal: &WorldState,
actions: &[Action],
max_depth: usize,
) -> Option<(Vec<String>, f32)> {
if start.satisfies(goal) {
return Some((Vec::new(), 0.0));
}
let mut open: BinaryHeap<SearchNode> = BinaryHeap::new();
let mut closed: HashSet<StateKey> = HashSet::new();
open.push(SearchNode {
state: start.clone(),
path: Vec::new(),
cost: 0.0,
heuristic: start.distance_to(goal),
});
while let Some(node) = open.pop() {
if node.state.satisfies(goal) {
let cost = node.cost;
return Some((node.path, cost));
}
if node.path.len() >= max_depth { continue; }
let key = node.state.snapshot_key();
if closed.contains(&key) { continue; }
closed.insert(key);
for action in actions {
if !action.is_applicable(&node.state) { continue; }
let next_state = action.apply_effects(&node.state);
let next_key = next_state.snapshot_key();
if closed.contains(&next_key) { continue; }
let next_cost = node.cost + action.cost;
let mut next_path = node.path.clone();
next_path.push(action.name.clone());
open.push(SearchNode {
state: next_state.clone(),
path: next_path,
cost: next_cost,
heuristic: next_state.distance_to(goal),
});
}
}
None
}
pub fn plan_alternatives(
start: &WorldState,
goal: &WorldState,
actions: &[Action],
max_depth: usize,
max_plans: usize,
) -> Vec<(Vec<String>, f32)> {
let mut results: Vec<(Vec<String>, f32)> = Vec::new();
let mut queue: VecDeque<SearchNode> = VecDeque::new();
queue.push_back(SearchNode {
state: start.clone(),
path: Vec::new(),
cost: 0.0,
heuristic: start.distance_to(goal),
});
let mut visited: HashSet<StateKey> = HashSet::new();
while let Some(node) = queue.pop_front() {
if results.len() >= max_plans { break; }
if node.state.satisfies(goal) {
results.push((node.path.clone(), node.cost));
continue; }
if node.path.len() >= max_depth { continue; }
let key = node.state.snapshot_key();
if visited.contains(&key) { continue; }
visited.insert(key);
for action in actions {
if !action.is_applicable(&node.state) { continue; }
let next_state = action.apply_effects(&node.state);
let mut next_path = node.path.clone();
next_path.push(action.name.clone());
queue.push_back(SearchNode {
state: next_state.clone(),
path: next_path,
cost: node.cost + action.cost,
heuristic: next_state.distance_to(goal),
});
}
}
results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
results
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanStepStatus {
NotStarted,
InProgress,
Completed,
Failed,
Interrupted,
}
#[derive(Debug, Clone)]
pub struct PlanStep {
pub action_name: String,
pub status: PlanStepStatus,
pub elapsed: f32,
pub duration: f32,
}
impl PlanStep {
fn new(action_name: &str, duration: f32) -> Self {
Self {
action_name: action_name.to_string(),
status: PlanStepStatus::NotStarted,
elapsed: 0.0,
duration,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutorState {
Idle,
Executing,
Succeeded,
Replanning,
Failed,
Interrupted,
}
#[derive(Debug)]
pub struct PlanExecutor {
actions: Vec<Action>,
steps: Vec<PlanStep>,
current: usize,
sim_state: WorldState,
goal: Option<WorldState>,
pub state: ExecutorState,
pub max_depth: usize,
pub sim_time: f32,
pub replan_count: u32,
pub max_replans: u32,
step_start_state: WorldState,
pub history: Vec<String>,
}
impl PlanExecutor {
pub fn new(actions: Vec<Action>, max_depth: usize) -> Self {
Self {
actions,
steps: Vec::new(),
current: 0,
sim_state: WorldState::new(),
goal: None,
state: ExecutorState::Idle,
max_depth,
sim_time: 0.0,
replan_count: 0,
max_replans: 5,
step_start_state: WorldState::new(),
history: Vec::new(),
}
}
pub fn set_world_state(&mut self, state: WorldState) {
self.sim_state = state;
}
pub fn update_bool(&mut self, key: &str, value: bool) {
self.sim_state.set_bool(key, value);
}
pub fn update_float(&mut self, key: &str, value: f32) {
self.sim_state.set_float(key, value);
}
pub fn start(&mut self, goal: WorldState) -> Result<usize, PlanError> {
self.goal = Some(goal.clone());
self.replan_count = 0;
self.history.clear();
self.do_plan(&goal)
}
pub fn interrupt(&mut self) {
if let Some(step) = self.steps.get_mut(self.current) {
step.status = PlanStepStatus::Interrupted;
}
self.state = ExecutorState::Interrupted;
}
pub fn tick(
&mut self,
dt: f32,
observe_state: impl Fn(&WorldState) -> WorldState,
) -> Option<&str> {
self.sim_time += dt;
match self.state {
ExecutorState::Idle
| ExecutorState::Succeeded
| ExecutorState::Failed
| ExecutorState::Interrupted => return None,
ExecutorState::Replanning => {
if let Some(goal) = self.goal.clone() {
match self.do_plan(&goal) {
Ok(_) => {} Err(_) => {
self.state = ExecutorState::Failed;
return None;
}
}
} else {
self.state = ExecutorState::Idle;
return None;
}
}
ExecutorState::Executing => {}
}
if self.current >= self.steps.len() {
self.state = ExecutorState::Succeeded;
return None;
}
{
let step = &mut self.steps[self.current];
if step.status == PlanStepStatus::NotStarted {
step.status = PlanStepStatus::InProgress;
}
step.elapsed += dt;
}
self.step_start_state = self.sim_state.clone();
let observed = observe_state(&self.sim_state);
if self.state_has_drifted(&observed) {
self.sim_state.merge_from(&observed);
self.steps[self.current].status = PlanStepStatus::Interrupted;
if self.replan_count >= self.max_replans {
self.state = ExecutorState::Failed;
return None;
}
self.replan_count += 1;
self.state = ExecutorState::Replanning;
return Some(&self.steps[self.current].action_name);
}
self.sim_state.merge_from(&observed);
let step = &self.steps[self.current];
let action_name = step.action_name.clone();
let elapsed = step.elapsed;
let duration = step.duration;
if duration > 0.0 && elapsed < duration {
return Some(&self.steps[self.current].action_name);
}
if let Some(action) = self.find_action(&action_name) {
let effects = action.effects.clone();
self.sim_state = self.sim_state.apply(&effects);
}
self.steps[self.current].status = PlanStepStatus::Completed;
self.history.push(action_name);
self.current += 1;
if self.current >= self.steps.len() {
self.state = ExecutorState::Succeeded;
return None;
}
let next_name = self.steps[self.current].action_name.clone();
if let Some(action) = self.find_action(&next_name) {
if !action.is_applicable(&self.sim_state) {
if self.replan_count >= self.max_replans {
self.state = ExecutorState::Failed;
return None;
}
self.replan_count += 1;
self.state = ExecutorState::Replanning;
}
}
Some(&self.steps[self.current.saturating_sub(1)].action_name)
}
pub fn current_action(&self) -> Option<&str> {
if self.state == ExecutorState::Executing && self.current < self.steps.len() {
Some(&self.steps[self.current].action_name)
} else {
None
}
}
pub fn plan_names(&self) -> Vec<&str> {
self.steps.iter().map(|s| s.action_name.as_str()).collect()
}
pub fn progress(&self) -> (usize, usize) {
(self.current, self.steps.len())
}
pub fn has_succeeded(&self) -> bool { self.state == ExecutorState::Succeeded }
pub fn has_failed(&self) -> bool { self.state == ExecutorState::Failed }
pub fn reset(&mut self) {
self.steps = Vec::new();
self.current = 0;
self.goal = None;
self.state = ExecutorState::Idle;
self.replan_count = 0;
self.history.clear();
}
pub fn world_state(&self) -> &WorldState { &self.sim_state }
fn do_plan(&mut self, goal: &WorldState) -> Result<usize, PlanError> {
match GoapPlanner::plan(&self.sim_state, goal, &self.actions, self.max_depth) {
Some(names) => {
self.steps = names.iter().map(|n| {
let dur = self.find_action(n).map(|a| a.duration_secs).unwrap_or(0.0);
PlanStep::new(n, dur)
}).collect();
self.current = 0;
self.state = ExecutorState::Executing;
let len = self.steps.len();
Ok(len)
}
None => {
self.state = ExecutorState::Failed;
Err(PlanError::NoPlanFound)
}
}
}
fn find_action(&self, name: &str) -> Option<&Action> {
self.actions.iter().find(|a| a.name == name)
}
fn state_has_drifted(&self, observed: &WorldState) -> bool {
if self.current >= self.steps.len() { return false; }
let name = &self.steps[self.current].action_name;
if let Some(action) = self.find_action(name) {
for (k, &expected) in &action.preconditions.bools {
if observed.has_bool(k) && observed.get_bool(k) != expected {
return true;
}
}
for (k, &min) in &action.preconditions.floats_gte {
if observed.has_float(k) && observed.get_float(k) < min {
return true;
}
}
for (k, &max) in &action.preconditions.floats_lte {
if observed.has_float(k) && observed.get_float(k) > max {
return true;
}
}
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanError {
NoPlanFound,
NoActions,
AlreadySatisfied,
}
impl std::fmt::Display for PlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlanError::NoPlanFound => write!(f, "GOAP: no plan found"),
PlanError::NoActions => write!(f, "GOAP: action library is empty"),
PlanError::AlreadySatisfied => write!(f, "GOAP: goal already satisfied"),
}
}
}
impl std::error::Error for PlanError {}
#[derive(Debug, Default, Clone)]
pub struct ActionLibrary {
actions: Vec<Action>,
}
impl ActionLibrary {
pub fn new() -> Self { Self::default() }
pub fn add(&mut self, action: Action) { self.actions.push(action); }
pub fn remove(&mut self, name: &str) {
self.actions.retain(|a| a.name != name);
}
pub fn get(&self, name: &str) -> Option<&Action> {
self.actions.iter().find(|a| a.name == name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut Action> {
self.actions.iter_mut().find(|a| a.name == name)
}
pub fn enable(&mut self, name: &str) {
if let Some(a) = self.get_mut(name) { a.disabled = false; }
}
pub fn disable(&mut self, name: &str) {
if let Some(a) = self.get_mut(name) { a.disabled = true; }
}
pub fn all(&self) -> &[Action] { &self.actions }
pub fn by_tag(&self, tag: &str) -> Vec<&Action> {
self.actions.iter().filter(|a| a.tags.iter().any(|t| t == tag)).collect()
}
pub fn applicable(&self, state: &WorldState) -> Vec<&Action> {
self.actions.iter().filter(|a| a.is_applicable(state)).collect()
}
pub fn plan(
&self,
start: &WorldState,
goal: &WorldState,
max_depth: usize,
) -> Option<Vec<String>> {
GoapPlanner::plan(start, goal, &self.actions, max_depth)
}
}
#[derive(Debug)]
pub struct GoapAgent {
pub name: String,
pub goals: GoalStack,
pub library: ActionLibrary,
pub executor: PlanExecutor,
active_goal: Option<String>,
}
impl GoapAgent {
pub fn new(name: &str, actions: Vec<Action>, max_depth: usize) -> Self {
let library = ActionLibrary { actions: actions.clone() };
let executor = PlanExecutor::new(actions, max_depth);
Self {
name: name.to_string(),
goals: GoalStack::new(),
library,
executor,
active_goal: None,
}
}
pub fn push_goal(&mut self, goal: Goal) {
self.goals.push(goal);
}
pub fn set_world_state(&mut self, state: WorldState) {
self.executor.set_world_state(state);
}
pub fn set_bool(&mut self, key: &str, value: bool) {
self.executor.update_bool(key, value);
}
pub fn set_float(&mut self, key: &str, value: f32) {
self.executor.update_float(key, value);
}
pub fn tick(&mut self, dt: f32) -> Option<&str> {
self.goals.tick(dt);
let desired = self.goals.active().map(|g| g.name.clone());
if desired != self.active_goal {
self.active_goal = desired.clone();
if let Some(goal_name) = desired {
if let Some(goal) = self.goals.iter()
.find(|g| g.name == goal_name)
.map(|g| g.state.clone())
{
let _ = self.executor.start(goal);
}
} else {
self.executor.reset();
}
}
let world = self.executor.sim_state.clone();
self.executor.tick(dt, |_| world.clone())
}
pub fn current_action(&self) -> Option<&str> { self.executor.current_action() }
pub fn is_idle(&self) -> bool { self.executor.state == ExecutorState::Idle }
pub fn has_succeeded(&self) -> bool { self.executor.has_succeeded() }
pub fn has_failed(&self) -> bool { self.executor.has_failed() }
pub fn plan_names(&self) -> Vec<&str> { self.executor.plan_names() }
}
#[cfg(test)]
mod tests {
use super::*;
fn build_test_actions() -> Vec<Action> {
vec![
Action::new("pick_up_weapon", 1.0)
.require_bool("has_weapon", false)
.effect_bool("has_weapon", true),
Action::new("attack_enemy", 2.0)
.require_bool("has_weapon", true)
.require_bool("enemy_dead", false)
.effect_bool("enemy_dead", true),
Action::new("flee", 0.5)
.require_bool("enemy_dead", false)
.effect_bool("safe", true),
]
}
#[test]
fn plan_pick_up_then_attack() {
let mut start = WorldState::new();
start.set_bool("has_weapon", false);
start.set_bool("enemy_dead", false);
let mut goal = WorldState::new();
goal.set_bool("enemy_dead", true);
let actions = build_test_actions();
let plan = GoapPlanner::plan(&start, &goal, &actions, 5);
assert!(plan.is_some());
let plan = plan.unwrap();
assert_eq!(plan, vec!["pick_up_weapon", "attack_enemy"]);
}
#[test]
fn plan_already_satisfied() {
let mut start = WorldState::new();
start.set_bool("enemy_dead", true);
let mut goal = WorldState::new();
goal.set_bool("enemy_dead", true);
let actions = build_test_actions();
let plan = GoapPlanner::plan(&start, &goal, &actions, 5).unwrap();
assert!(plan.is_empty(), "Goal already satisfied — plan should be empty");
}
#[test]
fn plan_no_solution() {
let start = WorldState::new();
let mut goal = WorldState::new();
goal.set_bool("magic_flag", true);
let actions = build_test_actions();
let plan = GoapPlanner::plan(&start, &goal, &actions, 5);
assert!(plan.is_none());
}
#[test]
fn world_state_satisfies() {
let mut s = WorldState::new();
s.set_bool("a", true);
s.set_float("hp", 80.0);
let mut g = WorldState::new();
g.set_bool("a", true);
g.set_float("hp", 50.0);
assert!(s.satisfies(&g));
s.set_float("hp", 30.0);
assert!(!s.satisfies(&g));
}
#[test]
fn action_effects_applied() {
let action = Action::new("test", 1.0)
.effect_bool("door_open", true)
.effect_set_float("energy", 0.0)
.effect_add_float("gold", 10.0);
let mut state = WorldState::new();
state.set_bool("door_open", false);
state.set_float("energy", 100.0);
state.set_float("gold", 5.0);
let next = action.apply_effects(&state);
assert_eq!(next.get_bool("door_open"), true);
assert_eq!(next.get_float("energy"), 0.0);
assert_eq!(next.get_float("gold"), 15.0);
}
#[test]
fn goal_stack_priority_order() {
let mut stack = GoalStack::new();
let mut g1 = WorldState::new(); g1.set_bool("low_priority", true);
let mut g2 = WorldState::new(); g2.set_bool("high_priority", true);
stack.push(Goal::new("low", g1, 1));
stack.push(Goal::new("high", g2, 10));
assert_eq!(stack.active().map(|g| g.name.as_str()), Some("high"));
}
#[test]
fn goal_ttl_expiry() {
let mut stack = GoalStack::new();
let mut g = WorldState::new(); g.set_bool("x", true);
stack.push(Goal::new("temp", g, 1).with_ttl(0.5));
assert!(stack.active().is_some());
stack.tick(1.0); assert!(stack.active().is_none());
}
#[test]
fn executor_completes_plan() {
let actions = build_test_actions();
let mut executor = PlanExecutor::new(actions, 10);
let mut ws = WorldState::new();
ws.set_bool("has_weapon", false);
ws.set_bool("enemy_dead", false);
executor.set_world_state(ws);
let mut goal = WorldState::new();
goal.set_bool("enemy_dead", true);
let result = executor.start(goal);
assert!(result.is_ok(), "Expected a plan to be found");
assert_eq!(result.unwrap(), 2, "Expected 2-step plan");
let names = executor.plan_names();
assert_eq!(names, vec!["pick_up_weapon", "attack_enemy"]);
}
#[test]
fn plan_alternatives_returns_multiple() {
let actions = build_test_actions();
let mut start = WorldState::new();
start.set_bool("has_weapon", false);
start.set_bool("enemy_dead", false);
let mut goal = WorldState::new();
goal.set_bool("enemy_dead", true);
let alts = GoapPlanner::plan_alternatives(&start, &goal, &actions, 5, 3);
assert!(!alts.is_empty());
}
#[test]
fn action_library_applicable() {
let mut lib = ActionLibrary::new();
for a in build_test_actions() { lib.add(a); }
let mut ws = WorldState::new();
ws.set_bool("has_weapon", false);
ws.set_bool("enemy_dead", false);
let applicable = lib.applicable(&ws);
let names: Vec<&str> = applicable.iter().map(|a| a.name.as_str()).collect();
assert!(names.contains(&"pick_up_weapon"));
assert!(names.contains(&"flee"));
assert!(!names.contains(&"attack_enemy")); }
#[test]
fn float_precondition_plan() {
let actions = vec![
Action::new("heal", 1.0)
.require_float_lte("hp", 50.0)
.effect_set_float("hp", 100.0),
];
let mut start = WorldState::new();
start.set_float("hp", 30.0);
let mut goal = WorldState::new();
goal.set_float("hp", 80.0);
let plan = GoapPlanner::plan(&start, &goal, &actions, 3);
assert!(plan.is_some());
assert_eq!(plan.unwrap(), vec!["heal"]);
}
}