95 lines
2.7 KiB
C#
95 lines
2.7 KiB
C#
using SpaceGame.Api.Simulation.Engine;
|
|
using SpaceGame.Api.Simulation.Model;
|
|
|
|
namespace SpaceGame.Api.Simulation.AI;
|
|
|
|
public abstract class GoapAction<TState>
|
|
{
|
|
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<TState>
|
|
{
|
|
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<TState>
|
|
{
|
|
public static readonly GoapPlan<TState> Empty = new() { Actions = [], TotalCost = 0f };
|
|
|
|
public required IReadOnlyList<GoapAction<TState>> Actions { get; init; }
|
|
public required float TotalCost { get; init; }
|
|
public int CurrentStep { get; set; }
|
|
|
|
public GoapAction<TState>? CurrentAction => CurrentStep < Actions.Count ? Actions[CurrentStep] : null;
|
|
public bool IsComplete => CurrentStep >= Actions.Count;
|
|
public void Advance() => CurrentStep++;
|
|
}
|
|
|
|
public sealed class GoapPlanner<TState>
|
|
{
|
|
private readonly Func<TState, TState> cloneState;
|
|
|
|
public GoapPlanner(Func<TState, TState> cloneState)
|
|
{
|
|
this.cloneState = cloneState;
|
|
}
|
|
|
|
public GoapPlan<TState>? Plan(
|
|
TState initialState,
|
|
GoapGoal<TState> goal,
|
|
IReadOnlyList<GoapAction<TState>> availableActions)
|
|
{
|
|
if (goal.IsSatisfied(initialState))
|
|
{
|
|
return GoapPlan<TState>.Empty;
|
|
}
|
|
|
|
var openSet = new PriorityQueue<PlanNode, float>();
|
|
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<TState>
|
|
{
|
|
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<GoapAction<TState>>(current.Actions) { action };
|
|
openSet.Enqueue(new PlanNode(newState, newActions, newCost), newCost);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private sealed record PlanNode(
|
|
TState State,
|
|
IReadOnlyList<GoapAction<TState>> Actions,
|
|
float Cost);
|
|
}
|