1use std::collections::{HashMap, BinaryHeap, HashSet};
9use std::cmp::Ordering;
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
15pub struct WorldState(HashMap<String, bool>);
16
17impl WorldState {
18 pub fn new() -> Self { Self::default() }
19
20 pub fn set(&mut self, key: &str, value: bool) {
21 self.0.insert(key.to_string(), value);
22 }
23
24 pub fn get(&self, key: &str) -> bool {
25 *self.0.get(key).unwrap_or(&false)
26 }
27
28 pub fn satisfies(&self, goal: &WorldState) -> bool {
30 goal.0.iter().all(|(k, &v)| self.get(k) == v)
31 }
32
33 pub fn apply(&self, effects: &WorldState) -> WorldState {
35 let mut next = self.clone();
36 for (k, &v) in &effects.0 {
37 next.0.insert(k.clone(), v);
38 }
39 next
40 }
41
42 pub fn distance_to(&self, goal: &WorldState) -> usize {
44 goal.0.iter().filter(|(k, &v)| self.get(k) != v).count()
45 }
46}
47
48#[derive(Debug, Clone)]
52pub struct GoapAction {
53 pub name: String,
54 pub cost: f32,
55 pub preconditions: WorldState,
56 pub effects: WorldState,
57 pub requires_in_range: Option<String>,
59}
60
61impl GoapAction {
62 pub fn new(name: &str, cost: f32) -> Self {
63 Self {
64 name: name.to_string(),
65 cost,
66 preconditions: WorldState::new(),
67 effects: WorldState::new(),
68 requires_in_range: None,
69 }
70 }
71
72 pub fn with_precondition(mut self, key: &str, value: bool) -> Self {
73 self.preconditions.set(key, value);
74 self
75 }
76
77 pub fn with_effect(mut self, key: &str, value: bool) -> Self {
78 self.effects.set(key, value);
79 self
80 }
81
82 pub fn requires_range(mut self, target: &str) -> Self {
83 self.requires_in_range = Some(target.to_string());
84 self
85 }
86
87 pub fn is_applicable(&self, state: &WorldState) -> bool {
88 state.satisfies(&self.preconditions)
89 }
90}
91
92#[derive(Clone)]
95struct SearchNode {
96 state: WorldState,
97 path: Vec<String>, cost: f32,
99 heuristic: usize,
100}
101
102impl PartialEq for SearchNode {
103 fn eq(&self, other: &Self) -> bool { self.f() == other.f() }
104}
105impl Eq for SearchNode {}
106
107impl PartialOrd for SearchNode {
108 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
109}
110
111impl Ord for SearchNode {
112 fn cmp(&self, other: &Self) -> Ordering {
113 other.f_ord().cmp(&self.f_ord()) }
115}
116
117impl SearchNode {
118 fn f(&self) -> f32 { self.cost + self.heuristic as f32 }
119 fn f_ord(&self) -> u64 { (self.f() * 1000.0) as u64 }
120}
121
122pub struct GoapPlanner;
126
127impl GoapPlanner {
128 pub fn plan(
130 start: &WorldState,
131 goal: &WorldState,
132 actions: &[GoapAction],
133 max_depth: usize,
134 ) -> Option<Vec<String>> {
135 let mut open: BinaryHeap<SearchNode> = BinaryHeap::new();
136 let mut closed: Vec<WorldState> = Vec::new();
137
138 open.push(SearchNode {
139 state: start.clone(),
140 path: Vec::new(),
141 cost: 0.0,
142 heuristic: start.distance_to(goal),
143 });
144
145 while let Some(node) = open.pop() {
146 if node.state.satisfies(goal) {
147 return Some(node.path);
148 }
149 if node.path.len() >= max_depth { continue; }
150 if closed.contains(&node.state) { continue; }
151 closed.push(node.state.clone());
152
153 for action in actions {
154 if !action.is_applicable(&node.state) { continue; }
155 let next_state = node.state.apply(&action.effects);
156 if closed.iter().any(|s| s == &next_state) { continue; }
157 let mut path = node.path.clone();
158 path.push(action.name.clone());
159 open.push(SearchNode {
160 heuristic: next_state.distance_to(goal),
161 state: next_state,
162 path,
163 cost: node.cost + action.cost,
164 });
165 }
166 }
167 None
168 }
169}
170
171pub struct GoapAgent<W> {
175 pub name: String,
176 pub world_state: WorldState,
177 pub goal: WorldState,
178 pub actions: Vec<GoapAction>,
179 current_plan: Vec<String>,
180 plan_step: usize,
181 pub max_depth: usize,
182 executors: HashMap<String, Box<dyn Fn(&mut W, &mut WorldState) -> ActionResult + Send + Sync>>,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq)]
187pub enum ActionResult {
188 InProgress,
190 Done,
192 Failed,
194}
195
196impl<W> GoapAgent<W> {
197 pub fn new(name: &str) -> Self {
198 Self {
199 name: name.to_string(),
200 world_state: WorldState::new(),
201 goal: WorldState::new(),
202 actions: Vec::new(),
203 current_plan: Vec::new(),
204 plan_step: 0,
205 max_depth: 10,
206 executors: HashMap::new(),
207 }
208 }
209
210 pub fn add_action(&mut self, action: GoapAction) {
211 self.actions.push(action);
212 }
213
214 pub fn add_executor(
215 &mut self,
216 name: &str,
217 func: impl Fn(&mut W, &mut WorldState) -> ActionResult + Send + Sync + 'static,
218 ) {
219 self.executors.insert(name.to_string(), Box::new(func));
220 }
221
222 pub fn set_state(&mut self, key: &str, value: bool) {
223 self.world_state.set(key, value);
224 }
225
226 pub fn set_goal(&mut self, key: &str, value: bool) {
227 self.goal.set(key, value);
228 self.current_plan.clear();
229 self.plan_step = 0;
230 }
231
232 pub fn tick(&mut self, world: &mut W) -> Option<String> {
234 if self.plan_step >= self.current_plan.len() {
236 if self.world_state.satisfies(&self.goal) {
237 return None; }
239 match GoapPlanner::plan(&self.world_state, &self.goal, &self.actions, self.max_depth) {
240 Some(plan) => { self.current_plan = plan; self.plan_step = 0; }
241 None => return None,
242 }
243 }
244
245 let action_name = self.current_plan[self.plan_step].clone();
246
247 if let Some(executor) = self.executors.get(&action_name) {
248 let result = executor(world, &mut self.world_state);
249 match result {
250 ActionResult::Done => { self.plan_step += 1; }
251 ActionResult::Failed => {
252 self.current_plan.clear();
253 self.plan_step = 0;
254 }
255 ActionResult::InProgress => {}
256 }
257 } else {
258 self.plan_step += 1;
260 }
261
262 Some(action_name)
263 }
264
265 pub fn current_plan(&self) -> &[String] { &self.current_plan }
266 pub fn has_goal(&self) -> bool { !self.goal.0.is_empty() }
267 pub fn plan_length(&self) -> usize { self.current_plan.len() }
268}
269
270pub fn melee_combat_actions() -> Vec<GoapAction> {
274 vec![
275 GoapAction::new("move_to_target", 1.0)
276 .with_precondition("has_target", true)
277 .with_effect("in_range", true),
278 GoapAction::new("attack", 1.0)
279 .with_precondition("has_target", true)
280 .with_precondition("in_range", true)
281 .with_precondition("weapon_ready", true)
282 .with_effect("target_dead", true),
283 GoapAction::new("equip_weapon", 2.0)
284 .with_precondition("has_weapon", true)
285 .with_effect("weapon_ready", true),
286 GoapAction::new("pick_up_weapon", 1.5)
287 .with_precondition("weapon_nearby", true)
288 .with_effect("has_weapon", true),
289 GoapAction::new("flee", 1.0)
290 .with_precondition("low_health", true)
291 .with_effect("safe", true),
292 GoapAction::new("heal", 2.0)
293 .with_precondition("has_potion", true)
294 .with_effect("low_health", false),
295 ]
296}
297
298pub fn guard_actions() -> Vec<GoapAction> {
300 vec![
301 GoapAction::new("patrol", 1.0)
302 .with_effect("patrolling", true),
303 GoapAction::new("investigate_noise", 1.5)
304 .with_precondition("heard_noise", true)
305 .with_effect("area_clear", true)
306 .with_effect("heard_noise", false),
307 GoapAction::new("sound_alarm", 2.0)
308 .with_precondition("sees_intruder", true)
309 .with_effect("alarm_raised", true),
310 GoapAction::new("chase_intruder", 1.0)
311 .with_precondition("sees_intruder", true)
312 .with_effect("in_range", true),
313 GoapAction::new("return_to_post", 0.5)
314 .with_effect("at_post", true),
315 ]
316}
317
318#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_world_state_satisfies() {
326 let mut state = WorldState::new();
327 state.set("alive", true);
328 state.set("armed", false);
329
330 let mut goal = WorldState::new();
331 goal.set("alive", true);
332 assert!(state.satisfies(&goal));
333
334 goal.set("armed", true);
335 assert!(!state.satisfies(&goal));
336 }
337
338 #[test]
339 fn test_world_state_apply() {
340 let mut state = WorldState::new();
341 state.set("alive", true);
342 let mut effects = WorldState::new();
343 effects.set("armed", true);
344 let next = state.apply(&effects);
345 assert!(next.get("alive"));
346 assert!(next.get("armed"));
347 }
348
349 #[test]
350 fn test_planner_finds_plan() {
351 let actions = melee_combat_actions();
352
353 let mut start = WorldState::new();
354 start.set("has_target", true);
355 start.set("in_range", false);
356 start.set("weapon_ready", true);
357
358 let mut goal = WorldState::new();
359 goal.set("target_dead", true);
360
361 let plan = GoapPlanner::plan(&start, &goal, &actions, 5);
362 assert!(plan.is_some(), "should find a plan");
363 let plan = plan.unwrap();
364 assert!(plan.contains(&"move_to_target".to_string()));
365 assert!(plan.contains(&"attack".to_string()));
366 }
367
368 #[test]
369 fn test_planner_longer_chain() {
370 let actions = melee_combat_actions();
371
372 let mut start = WorldState::new();
373 start.set("has_target", true);
374 start.set("weapon_nearby", true);
375
376 let mut goal = WorldState::new();
377 goal.set("target_dead", true);
378
379 let plan = GoapPlanner::plan(&start, &goal, &actions, 8);
380 assert!(plan.is_some(), "should plan pick_up → equip → move → attack chain");
381 }
382
383 #[test]
384 fn test_planner_no_possible_plan() {
385 let mut start = WorldState::new();
386 start.set("has_target", false);
387
388 let mut goal = WorldState::new();
389 goal.set("target_dead", true);
390
391 let actions = vec![
393 GoapAction::new("attack", 1.0)
394 .with_precondition("has_target", true)
395 .with_effect("target_dead", true),
396 ];
397
398 let plan = GoapPlanner::plan(&start, &goal, &actions, 5);
399 assert!(plan.is_none());
400 }
401
402 #[test]
403 fn test_agent_executes_plan() {
404 let mut agent: GoapAgent<Vec<String>> = GoapAgent::new("test_agent");
405
406 agent.add_action(
407 GoapAction::new("do_thing", 1.0)
408 .with_effect("thing_done", true),
409 );
410
411 agent.add_executor("do_thing", |world, state| {
412 world.push("did_thing".to_string());
413 state.set("thing_done", true);
414 ActionResult::Done
415 });
416
417 agent.set_goal("thing_done", true);
418
419 let mut world: Vec<String> = Vec::new();
420 let action = agent.tick(&mut world);
421 assert_eq!(action, Some("do_thing".to_string()));
422 assert!(world.contains(&"did_thing".to_string()));
423 }
424
425 #[test]
426 fn test_agent_no_replan_at_goal() {
427 let mut agent: GoapAgent<()> = GoapAgent::new("agent");
428 let mut state = agent.world_state.clone();
429 state.set("done", true);
430 agent.world_state = state;
431 agent.set_goal("done", true);
432 let mut world = ();
433 let action = agent.tick(&mut world);
434 assert!(action.is_none(), "already at goal — no action needed");
435 }
436
437 #[test]
438 fn test_guard_actions_plan() {
439 let actions = guard_actions();
440 let mut start = WorldState::new();
441 start.set("heard_noise", true);
442 let mut goal = WorldState::new();
443 goal.set("area_clear", true);
444 let plan = GoapPlanner::plan(&start, &goal, &actions, 3);
445 assert!(plan.is_some());
446 assert!(plan.unwrap().contains(&"investigate_noise".to_string()));
447 }
448}