UNPKG

goap-oriented

Version:

A TypeScript implementation of Goal-Oriented Action Planning

293 lines (292 loc) 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GOAPPlanner = void 0; const PriorityQueue_1 = require("./PriorityQueue"); /** * Goal-Oriented Action Planning (GOAP) implementation for automated planning and problem solving. * This planner uses A* search with custom heuristics to find optimal action sequences. */ class GOAPPlanner { constructor() { this.actions = []; this.maxIterations = 1000; this.resourceKeys = new Set(); } /** * Adds a new action to the planner's available action set. * @param action The action to add */ addAction(action) { this.actions.push(action); } /** * Removes an action from the planner's available action set. * @param actionName The name of the action to remove */ removeAction(actionName) { this.actions = this.actions.filter((a) => a.name !== actionName); } evaluateStateProgress(state) { // Calculate progress towards meeting preconditions for goal actions let progress = 0; const goalActions = this.actions.filter((action) => action.effects.some((effect) => effect.key === 'goal' && effect.value === true)); for (const action of goalActions) { for (const precond of action.preconditions) { if (precond.operator === 'greaterThan' || precond.operator === 'greaterThanOrEqual') { const current = state[precond.key] || 0; const target = precond.value; progress += Math.min(1, current / target); } } } return progress; } projectFutureCosts(state, goal) { // TODO: Checking if the action's effects contribute to the actual goal conditions. Use the goal parameter to calculate costs based on how actions contribute to the specific goal conditions. let projection = 0; // Find all actions that could satisfy the goal const goalActions = this.actions.filter((action) => action.effects.some((effect) => effect.key === 'goal' && effect.value === true)); // For each potential goal action, calculate minimum cost to meet its preconditions for (const action of goalActions) { let actionProjection = 0; let requiredResources = 0; for (const precond of action.preconditions) { if (precond.operator === 'greaterThan' || precond.operator === 'greaterThanOrEqual') { const current = state[precond.key] || 0; const target = precond.value; if (current <= target) { // Calculate cost of reaching target with AddResources const resourcesNeeded = target - current; const addResourceCost = Math.ceil(resourcesNeeded / 5); // AddResources adds 5 at cost 1 actionProjection += addResourceCost; requiredResources = Math.max(requiredResources, target); } } } // Add the cost of the goal action itself const finalStateCost = typeof action.cost === 'function' ? action.cost({ ...state, resources: requiredResources }) : action.cost; actionProjection += finalStateCost; // Keep track of minimum projected cost path if (projection === 0 || actionProjection < projection) { projection = actionProjection; } } return projection; } /** * Finds an optimal sequence of actions to achieve the specified goal from the initial state. * Uses A* search with custom heuristics for efficient planning. * @param initialState The starting world state * @param goal The goal conditions to achieve * @returns An array of actions that achieve the goal, or empty array if no solution is found */ findPlan(initialState, goal) { const queue = new PriorityQueue_1.PriorityQueue((a, b) => { // Prefer paths that are closer to meeting preconditions for goal actions if (a.priority === b.priority) { return this.evaluateStateProgress(b.state) - this.evaluateStateProgress(a.state); } return a.priority - b.priority; }); const visited = new Map(); let iterations = 0; let bestPlan = null; let bestCost = Infinity; queue.enqueue({ state: initialState, actions: [], cost: 0, priority: 0, }); while (!queue.isEmpty() && iterations < this.maxIterations) { iterations++; const current = queue.dequeue(); if (!current) continue; if (this.satisfiesGoal(current.state, goal)) { if (current.cost < bestCost) { bestPlan = current.actions; bestCost = current.cost; } continue; } const stateHash = this.getStateHash(current.state); const visitCount = visited.get(stateHash) || 0; if (visitCount > 10) continue; visited.set(stateHash, visitCount + 1); // Get all valid actions for current state const validActions = this.actions.filter((action) => this.checkPreconditions(current.state, action.preconditions)); for (const action of validActions) { const newState = this.applyEffects(current.state, action.effects); const actionCost = this.calculateActionCost(action, current.state); const totalCost = current.cost + actionCost; if (totalCost >= bestCost) continue; // Project future costs based on remaining requirements const projectedCost = this.projectFutureCosts(newState, goal); const priority = totalCost + projectedCost; queue.enqueue({ state: { ...newState }, actions: [...current.actions, action], cost: totalCost, priority, }); } } return bestPlan || []; } calculateHeuristic(state, goal) { let estimate = 0; let resourceNeeded = 0; // Check if we need more resources for any available actions for (const action of this.actions) { for (const precond of action.preconditions) { if (precond.operator === 'greaterThan' || precond.operator === 'greaterThanOrEqual') { const required = precond.value; const current = state[precond.key] || 0; if (current <= required) { resourceNeeded = Math.max(resourceNeeded, required - current); } } } } // Add resource gathering cost to heuristic if (resourceNeeded > 0) { // Assume we need to gather resources at base cost estimate += Math.ceil(resourceNeeded / 5); // Assuming AddResources adds 5 at cost 1 } // Add goal satisfaction cost for (const condition of goal.conditions) { if (!this.checkPrecondition(state, condition)) { if (typeof condition.value === 'number' && typeof state[condition.key] === 'number') { estimate += Math.abs((state[condition.key] || 0) - condition.value); } else { estimate += 1; } } } return estimate; } getStateHash(state) { // Include both regular state and consumption tracking in hash const stateEntries = Object.entries(state) .filter(([key]) => !key.endsWith('_consumed')) .sort(([a], [b]) => a.localeCompare(b)); const consumedEntries = Object.entries(state) .filter(([key]) => key.endsWith('_consumed')) .sort(([a], [b]) => a.localeCompare(b)); return JSON.stringify({ state: stateEntries, consumed: consumedEntries, }); } calculateActionCost(action, state) { // For dynamic cost actions, evaluate their potential future cost if (typeof action.cost === 'function') { const projectedState = this.applyEffects(state, action.effects); return action.cost(projectedState); } return action.cost; } /** * Marks a state key as representing a consumable resource. * Marked resources will be tracked for consumption status. * @param key The state key to mark as a resource */ markAsResource(key) { this.resourceKeys.add(key); } /** * Clears all resource tracking markers. */ clearResourceTracking() { this.resourceKeys.clear(); } /** * Applies a sequence of effects to a world state. * @param state Current world state * @param effects Array of effects to apply * @returns New world state after applying effects * @private */ applyEffects(state, effects) { const newState = { ...state }; for (const effect of effects) { const prevValue = newState[effect.key]; const value = typeof effect.value === 'function' ? effect.value(newState) : effect.value; // Track resource consumption if (this.resourceKeys.has(effect.key)) { if (typeof value === 'number' && typeof prevValue === 'number') { if (value < prevValue) { // Mark as consumed only when resources are actually reduced newState[`${effect.key}_consumed`] = true; } else { // Reset consumed flag when resources are replenished delete newState[`${effect.key}_consumed`]; } } } newState[effect.key] = value; } return newState; } /** * Checks if all preconditions are satisfied in the given state. * @param state Current world state * @param preconditions Array of preconditions to check * @returns True if all preconditions are satisfied * @private */ checkPreconditions(state, preconditions) { return preconditions.every((condition) => { if (this.resourceKeys.has(condition.key) && condition.operator === 'greaterThanOrEqual' && state[`${condition.key}_consumed`]) { return false; } return this.checkPrecondition(state, condition); }); } /** * Checks if a single precondition is satisfied in the given state. * @param state Current world state * @param condition Precondition to check * @returns True if the precondition is satisfied * @private */ checkPrecondition(state, condition) { const stateValue = state[condition.key]; const conditionValue = typeof condition.value === 'function' ? condition.value(state) : condition.value; switch (condition.operator) { case 'greaterThan': return stateValue > conditionValue; case 'lessThan': return stateValue < conditionValue; case 'greaterThanOrEqual': return stateValue >= conditionValue; case 'lessThanOrEqual': return stateValue <= conditionValue; case 'notEquals': return stateValue !== conditionValue; case 'equals': default: return stateValue === conditionValue; } } /** * Checks if the current state satisfies all goal conditions. * @param state Current world state * @param goal Goal to check * @returns True if all goal conditions are satisfied * @private */ satisfiesGoal(state, goal) { return goal.conditions.every((condition) => this.checkPrecondition(state, condition)); } } exports.GOAPPlanner = GOAPPlanner;