A flexible Hierarchical Task Network (HTN) planner for creating complex AI behaviors in JavaScript.
npm install stella-htn-jsAI GENERATED README
A flexible and powerful Hierarchical Task Network (HTN) planner for creating complex and emergent AI behaviors in JavaScript. This library is designed for game developers and AI enthusiasts looking for a goal-oriented planning system that is easy to integrate and extend.
* Hierarchical Planning: Define complex behaviors by decomposing them into smaller, manageable tasks.
* Dynamic & Flexible: Plans are generated at runtime, allowing AI to adapt to a changing world.
* Smart Objects: A system for creating context-aware interactions with world objects.
* Plan Interruption & Replanning: Robustly handle unexpected events and replan on the fly.
* Performance-Oriented: Designed with performance in mind, including features like plan caching and an incremental planner.
* ESM Native: Built as a native ES Module.
You can install stella-htn-js via npm or your favorite package manager:
``bash`
npm install stella-htn-js`
Or with bun:bash`
bun add stella-htn-jsCore Concepts
stella-htn-js is based on a few core concepts:
* Tasks: The building blocks of behavior.
* PrimitiveTask: Represents a single, executable action that directly affects the world state (e.g., Attack, MoveTo).
CompoundTask: Represents a high-level goal that is achieved by completing a sequence of sub-tasks (e.g., EngageEnemy). It contains multiple methods*, which are different ways to accomplish the goal.
* Planner: The "brain" of the system. It takes a high-level task and a world state, and finds a sequence of primitive tasks (a "plan") to achieve it.
* WorldStateProxy: A flexible interface that allows the planner to interact with your game's world state, no matter how it's structured.
* PlanExecutor: Executes a plan step-by-step and handles interruptions.
Here's a simple example of how to set up and run the planner.
First, define the primitive and compound tasks that your AI can perform.
`js
import { PrimitiveTask, CompoundTask } from 'stella-htn-js';
// A primitive task to attack an enemy
const attackEnemy = new PrimitiveTask('AttackEnemy', {
conditions: (ws) => ws.get('agentHasWeapon') && ws.get('enemyIsVisible'),
effects: (ws) => ws.set('enemyDefeated', true),
operator: () => console.log("AI is attacking the enemy!")
});
// A compound task to get a weapon
const getWeapon = new CompoundTask('GetWeapon', [
{
name: 'Pick up a nearby weapon',
conditions: (ws) => !ws.get('agentHasWeapon'),
subtasks: ['PickupWeapon_scattergun_01'] // This would be a SmartObjectTask
}
]);
// A high-level goal to engage an enemy
const engageEnemy = new CompoundTask('EngageEnemy', [
{
name: 'Attack if ready',
priority: 10,
conditions: (ws) => ws.get('agentHasWeapon'),
subtasks: [attackEnemy]
},
{
name: 'Get a weapon first',
priority: 5,
subtasks: [getWeapon, 'EngageEnemy'] // Recursively call this task
}
]);
`$3
The planner needs to know about the current state of the world. WorldStateProxy lets you connect your game's state.
`js
import { WorldStateProxy } from 'stella-htn-js';
let gameState = {
agentHasWeapon: false,
enemyIsVisible: true,
enemyDefeated: false,
// ... other game state
};
const worldState = new WorldStateProxy({
getState: (key) => (key ? gameState[key] : gameState),
setState: (key, value) => (gameState[key] = value),
clone: () => {
const clonedState = structuredClone(gameState);
return new WorldStateProxy({
getState: (key) => (key ? clonedState[key] : clonedState),
setState: (key, value) => (clonedState[key] = value),
clone: () => { /...recursive clone setup.../ }
});
}
});
`$3
Now, create a Planner instance, register your tasks, and find a plan.
`js
import { Planner } from 'stella-htn-js';
const planner = new Planner();
planner.registerTask(attackEnemy);
planner.registerTask(getWeapon);
planner.registerTask(engageEnemy);
// ... register other tasks
const planResult = await runPlannerToCompletion(planner, {
tasks: engageEnemy,
worldState: worldState,
context: { agentId: 'agent_01' }
});
if (planResult) {
console.log('Plan found:', planResult.plan.map(task => task.name));
// Now you can execute this plan!
} else {
console.log('No plan found.');
}
// Helper to run the async generator planner
async function runPlannerToCompletion(planner, options) {
const generator = planner.findPlan(options);
let result = null;
while (true) {
const { value, done } = await generator.next();
if (done) {
result = value;
break;
}
}
return result;
}
`API Reference
The main class for finding plans.
* new Planner(config): Creates a new planner. config.maxIterations
* : Max planning steps. config.enablePlanCaching
* : Caches plans for given states. registerTask(task)
* : Registers a task with the planner. findPlan({ tasks, worldState, context })
* : Asynchronously finds a plan. Returns a generator.
Executes a plan and handles interruptions.
* new PlanExecutor(plan, context, options) tick(worldState)
* : Executes the next step of the plan. isDone()
* : Returns true if the plan is complete.
Manages conditions that can interrupt a plan.
* registerInterruptor(id, interruptor)`: Adds a condition that can interrupt the plan.
Contributions are welcome! Please feel free to submit a pull request or open an issue.
This project is licensed under the MIT License. See the LICENSE file for details.