using SpaceGame.Api.Simulation.Engine; using SpaceGame.Api.Simulation.Model; namespace SpaceGame.Api.Simulation.AI; public abstract class GoapAction { public abstract string Name { get; } public abstract float Cost { get; } public abstract bool CheckPreconditions(TState state); public abstract TState ApplyEffects(TState state); public abstract void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander); } public abstract class GoapGoal { public abstract string Name { get; } public abstract bool IsSatisfied(TState state); public abstract float ComputePriority(TState state, SimulationWorld world, CommanderRuntime commander); } public sealed class GoapPlan { public static readonly GoapPlan Empty = new() { Actions = [], TotalCost = 0f }; public required IReadOnlyList> Actions { get; init; } public required float TotalCost { get; init; } public int CurrentStep { get; set; } public GoapAction? CurrentAction => CurrentStep < Actions.Count ? Actions[CurrentStep] : null; public bool IsComplete => CurrentStep >= Actions.Count; public void Advance() => CurrentStep++; } public sealed class GoapPlanner { private readonly Func cloneState; public GoapPlanner(Func cloneState) { this.cloneState = cloneState; } public GoapPlan? Plan( TState initialState, GoapGoal goal, IReadOnlyList> availableActions) { if (goal.IsSatisfied(initialState)) { return GoapPlan.Empty; } var openSet = new PriorityQueue(); openSet.Enqueue(new PlanNode(cloneState(initialState), [], 0f), 0f); const int MaxIterations = 256; var iterations = 0; while (openSet.Count > 0 && iterations++ < MaxIterations) { var current = openSet.Dequeue(); if (goal.IsSatisfied(current.State)) { return new GoapPlan { Actions = current.Actions, TotalCost = current.Cost, }; } foreach (var action in availableActions) { if (!action.CheckPreconditions(current.State)) { continue; } var newState = action.ApplyEffects(cloneState(current.State)); var newCost = current.Cost + action.Cost; var newActions = new List>(current.Actions) { action }; openSet.Enqueue(new PlanNode(newState, newActions, newCost), newCost); } } return null; } private sealed record PlanNode( TState State, IReadOnlyList> Actions, float Cost); }