using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace SynapticPro.GOAP
{
///
/// GOAP Planner - A* based action planning system
/// Finds optimal sequence of actions to achieve goals
///
public class GOAPPlanner
{
private int maxPlanningIterations = 1000;
private int maxPlanDepth = 15;
///
/// Plan node for A* search
///
private class PlanNode : IComparable
{
public WorldState State;
public GOAPActionBase Action;
public PlanNode Parent;
public float GCost; // Cost from start
public float HCost; // Heuristic cost to goal
public float FCost => GCost + HCost;
public int Depth;
public int CompareTo(PlanNode other)
{
int compare = FCost.CompareTo(other.FCost);
if (compare == 0)
{
compare = HCost.CompareTo(other.HCost);
}
return compare;
}
}
///
/// Create a plan to achieve the goal from current state
///
/// The GOAP agent
/// Actions the agent can perform
/// Current world state
/// Goal to achieve
/// Queue of actions to execute, or null if no plan found
public Queue Plan(
GOAPAgent agent,
HashSet availableActions,
WorldState currentState,
GOAPGoal goal)
{
if (goal == null || availableActions == null || availableActions.Count == 0)
{
return null;
}
// Reset actions
foreach (var action in availableActions)
{
action.Reset();
}
// Get usable actions (procedural preconditions check)
var usableActions = new HashSet();
foreach (var action in availableActions)
{
if (action.CheckProceduralPrecondition(agent))
{
usableActions.Add(action);
}
}
if (usableActions.Count == 0)
{
return null;
}
// A* search
var openList = new List();
var closedSet = new HashSet();
// Start node
var startNode = new PlanNode
{
State = currentState.Clone(),
Action = null,
Parent = null,
GCost = 0,
HCost = CalculateHeuristic(currentState, goal),
Depth = 0
};
openList.Add(startNode);
int iterations = 0;
PlanNode goalNode = null;
while (openList.Count > 0 && iterations < maxPlanningIterations)
{
iterations++;
// Get node with lowest F cost
openList.Sort();
var currentNode = openList[0];
openList.RemoveAt(0);
// Check if goal is satisfied
if (GoalSatisfied(currentNode.State, goal))
{
goalNode = currentNode;
break;
}
// Skip if max depth reached
if (currentNode.Depth >= maxPlanDepth)
{
continue;
}
// Generate state hash for closed set
string stateHash = currentNode.State.GetHashCode().ToString();
if (closedSet.Contains(stateHash))
{
continue;
}
closedSet.Add(stateHash);
// Expand node - try each action
foreach (var action in usableActions)
{
// Check if action's preconditions are met
if (!PreconditionsMet(currentNode.State, action))
{
continue;
}
// Apply action effects to create new state
var newState = currentNode.State.Clone();
ApplyEffects(newState, action);
// Create new node
var newNode = new PlanNode
{
State = newState,
Action = action,
Parent = currentNode,
GCost = currentNode.GCost + action.Cost,
HCost = CalculateHeuristic(newState, goal),
Depth = currentNode.Depth + 1
};
// Check if already in open list with better cost
string newStateHash = newState.GetHashCode().ToString();
if (!closedSet.Contains(newStateHash))
{
openList.Add(newNode);
}
}
}
// Build plan from goal node
if (goalNode != null)
{
return BuildPlan(goalNode);
}
return null;
}
///
/// Calculate heuristic (estimated cost to goal)
///
private float CalculateHeuristic(WorldState state, GOAPGoal goal)
{
if (goal.DesiredState == null || goal.DesiredState.Count == 0)
{
return 0;
}
int unsatisfiedConditions = 0;
foreach (var condition in goal.DesiredState)
{
if (!state.HasState(condition.Key) ||
!state.GetState