goap
Import from @pierre/ecs/modules/goap.
Goal-oriented action planning: give an agent actions with
preconditions/effects and a goal, and plan() finds the least-cost
sequence that reaches it — via A* over symbolic world-states. The payoff
over authored behaviour is that the same goal yields different plans
as the world changes.
The most advanced of the AI decision modules (after modules/fsm and
modules/behavior-tree). GOAP was introduced in F.E.A.R.; see
examples/woodcutter for a worked example.
Scope: the pure planner
Section titled “Scope: the pure planner”type WorldState = Record<string, boolean>; // flat boolean facts; missing = falseinterface GoapAction { name: string; cost: number; preconditions: WorldState; // partial — keys that must match to apply effects: WorldState; // partial — keys it sets}function plan(actions, initial, goal): GoapAction[] | null;plan returns the ordered action list, [] if the goal already holds, or
null if unreachable. It is pure and symbolic — execution and
replanning stay in the consumer (map action.name to a runtime
behaviour, run it, and replan when a step fails or the world diverges).
How it works
Section titled “How it works”- Boolean facts.
WorldStateisRecord<string, boolean>; a missing key reads asfalse, sofalseand absent are the same state. This is the minimal representation. - A* over states. A node is a world state (hashed by its truthy keys,
which collapses equivalent states); an edge is an applicable action;
edge cost is
action.cost; the heuristic is the count of unsatisfied goal facts. No-op actions (effects already satisfied) are skipped to avoid cycles. - Cost shapes the plan. Distance-, risk-, or preference-based costs let one goal resolve to different plans — e.g. a consumer can make “go to the nearest resource” fall out of the planner by pricing travel actions by distance.
Consumer pattern
Section titled “Consumer pattern”// 1. Derive facts from the real world.const facts = buildFacts(agent);// 2. Plan.const steps = plan(ACTIONS, facts, GOAL);// 3. Execute the current step; on success advance; on failure replan.const status = RUNTIME[steps[i].name](agent);Replanning is a consumer decision (goal changed, action failed, world
diverged). The woodcutter example replans per delivery and on a
ChopTree{i} failure when a tree is taken — showing both plan-time
avoidance and replan-on-failure.
- Zero dependencies, ECS-decoupled.
planoperates on plainWorldStateobjects; it does not touch the ECS world. - Handle
null.planreturnsnullwhen the goal is unreachable from the current facts — including transient cases like “every resource is occupied” (no applicable action can satisfy the goal right now). The consumer decides what to do: idle and replan next tick (the woodcutter’s choice), fall back to another goal, or spawn more resources.nullis a normal signal, not an error. - A no-op / plan-runner is not shipped. The execute-and-replan loop is genuinely game-specific (it maps action names to movement/timers and decides when to replan), so it stays in the consumer per the rule of three.
- Small state spaces. The open set is a linear-scan priority queue — fine for the tens-of-nodes searches GOAP plans over. Swap in a heap only if a consumer plans over a large action set.