├── .gitattributes ├── .gitignore ├── SimpleGOAP.Core ├── .gitattributes ├── DefaultDictionary.cs ├── IAction.cs ├── IStateCopier.cs ├── LambdaAction.cs ├── LambdaCopier.cs ├── Plan.cs ├── PlanParameters.cs ├── PlanStep.cs ├── Planner.cs ├── SimpleGOAP.Core.csproj └── StateNode.cs ├── SimpleGOAP.KeyValueState ├── Fact.cs ├── KeyValuePlanner.cs ├── KeyValueState.cs ├── KeyValueStateComparer.cs ├── KeyValueStateCopier.cs └── SimpleGOAP.KeyValueState.csproj ├── SimpleGOAP.Tests ├── Data │ ├── DrumStacker │ │ ├── DrumStackerPlannerFactory.cs │ │ ├── DrumStackerState.cs │ │ ├── DrumStackerStateComparer.cs │ │ └── DrumStackerStateCopier.cs │ ├── ReadmeExample │ │ ├── PotatoState.cs │ │ ├── PotatoStateCopier.cs │ │ ├── PotatoStateEqualityComparer.cs │ │ └── PotatoStatePlannerFactory.cs │ ├── RiverCrossing │ │ ├── RiverCrossingPlannerFactory.cs │ │ ├── RiverCrossingState.cs │ │ └── RiverCrossingStateComparer.cs │ └── Traveler │ │ ├── Actions │ │ ├── DriveAction.cs │ │ ├── EatAction.cs │ │ ├── PurchaseAction.cs │ │ ├── SellAction.cs │ │ ├── SleepAction.cs │ │ ├── WatchMovieAction.cs │ │ └── WorkAction.cs │ │ └── TravelerDataFactory.cs ├── PerformanceTests.cs ├── PlannerTests.cs └── SimpleGOAP.Tests.csproj ├── SimpleGOAP.sln ├── SimpleGOAP.sln.DotSettings.user ├── push-core.ps1 ├── push-core.sh ├── push-kvs.ps1 └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | **/*bin/ 3 | **/*obj/ 4 | .api_key 5 | .api_key 6 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/DefaultDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP 4 | { 5 | internal class DefaultDictionary : Dictionary 6 | { 7 | private readonly TValue defaultValue; 8 | 9 | internal DefaultDictionary(TValue defaultValue, IEqualityComparer comp) : base(comp) 10 | { 11 | this.defaultValue = defaultValue; 12 | } 13 | 14 | internal new TValue this[TKey key] 15 | { 16 | get => TryGetValue(key, out var t) ? t : base[key] = defaultValue; 17 | set => base[key] = value; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/IAction.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP 2 | { 3 | /// An action, defined relative to state (T) 4 | /// The type representing state. 5 | public interface IAction 6 | { 7 | /// The name of the action. This has no impact in the search. 8 | string Title { get; } 9 | 10 | /// The cost of taking this action. The search will prioritize paths which have a lower cost. 11 | /// 12 | int GetCost(T state); 13 | 14 | /// A function which should modify the input state and return a new state. You do not need to copy 15 | /// the state within this function, since that has already been done upstream. So, for reference types 16 | /// you may just edit them and return the same object. This requires a return type of T in case 17 | /// your state object is not passed by reference (e.g. a struct). 18 | T TakeActionOnState(T state); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/IStateCopier.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP 2 | { 3 | /// A class which is responsible for making copies of your state T. 4 | /// The type representing state. 5 | public interface IStateCopier 6 | { 7 | /// Given a state T, this function makes a copy of that state and returns it. 8 | public T Copy(T state); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/LambdaAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleGOAP 4 | { 5 | public class LambdaAction : IAction 6 | { 7 | private Func action; 8 | private readonly Func getCost; 9 | 10 | public LambdaAction(string title, int actionCost, Func action) 11 | { 12 | Title = title; 13 | getCost = () => actionCost; 14 | this.action = action; 15 | } 16 | 17 | public LambdaAction(string title, int actionCost, Action action) 18 | { 19 | Title = title; 20 | getCost = () => actionCost; 21 | this.action = state => 22 | { 23 | action(state); 24 | return state; 25 | }; 26 | } 27 | 28 | public LambdaAction(string title, Func getCost, Func action) 29 | { 30 | Title = title; 31 | this.getCost = getCost; 32 | this.action = action; 33 | } 34 | 35 | public LambdaAction(string title, Func getCost, Action action) 36 | { 37 | Title = title; 38 | this.getCost = getCost; 39 | this.action = state => 40 | { 41 | action(state); 42 | return state; 43 | }; 44 | } 45 | 46 | public LambdaAction(string title, Func action) 47 | { 48 | Title = title; 49 | getCost = () => 1; 50 | this.action = action; 51 | } 52 | 53 | public LambdaAction(string title, Action action) 54 | { 55 | Title = title; 56 | getCost = () => 1; 57 | this.action = state => 58 | { 59 | action(state); 60 | return state; 61 | }; 62 | } 63 | 64 | public string Title { get; } 65 | 66 | public int GetCost(T state) => getCost(); 67 | 68 | public T TakeActionOnState(T state) => action(state); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/LambdaCopier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleGOAP 4 | { 5 | public class LambdaCopier : IStateCopier 6 | { 7 | private readonly Func doCopy; 8 | 9 | public LambdaCopier(Func doCopy) 10 | { 11 | this.doCopy = doCopy; 12 | } 13 | 14 | public T Copy(T state) => doCopy(state); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/Plan.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP 4 | { 5 | /// The result of the GOAP planner's search. 6 | /// The type representing state. 7 | public class Plan 8 | { 9 | /// Whether the search found a path to the goal. 10 | public bool Success { get; set; } 11 | 12 | /// The steps to take to get from current state to goal state. Will be empty if the search 13 | /// failed. 14 | public List> Steps { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/PlanParameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SimpleGOAP 5 | { 6 | /// Search parameters for the planner. 7 | /// The type representing state. 8 | public class PlanParameters 9 | { 10 | /// The initial world state. 11 | public T StartingState { get; set; } 12 | 13 | /// A heuristic function that will tell the engine how close to our goal we are for any given state. 14 | /// The planner will consider lower values to be closer to the goal.

Note that this is technically 15 | /// optional; you could return 0 and the search will still work. However, it's purpose is to suggest possible 16 | /// future paths and therefore can have a drastic effect on performance.
17 | public Func HeuristicCost { get; set; } 18 | 19 | /// A function that should return true if for a given state T we have reached our goal. 20 | public Func GoalEvaluator { get; set; } 21 | 22 | /// A list of actions that can be taken to achieve the goal. 23 | public Func>> GetActions { get; set; } 24 | 25 | /// The maximum number of possible actions to check before exiting. 26 | public int MaxIterations { get; set; } = int.MaxValue; 27 | 28 | /// Prefer using a faster queue, with the downside of having a fixed queue size. 29 | public bool UseFastQueue { get; set; } = true; 30 | 31 | /// The max queue size to use. Only relevant if UseFastQueue is set to true. 32 | public int QueueMaxSize { get; set; } = 100001; 33 | 34 | /// If an action's heuristic cost exceeds this threshold, it will be ignored as 35 | /// a possible future path. Ignored if null. 36 | public int? MaxHeuristicCost { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/PlanStep.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP 2 | { 3 | /// A step in the plan which indicates an action taken, as well as the before and after states. 4 | /// The type representing state. 5 | public class PlanStep 6 | { 7 | /// The position of this step in the list. 8 | public int Index { get; set; } 9 | 10 | /// The action taken. 11 | public IAction Action { get; set; } 12 | 13 | /// The state prior to action being taken. 14 | public T BeforeState {get;set;} 15 | 16 | /// The state after the action is taken. 17 | public T AfterState {get;set;} 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/Planner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Priority_Queue; 5 | 6 | namespace SimpleGOAP 7 | { 8 | /// The GOAP planner which runs search on possible futures to find a path to a goal state. 9 | /// The type representing state. 10 | public class Planner 11 | { 12 | private readonly IStateCopier stateCopier; 13 | private readonly IEqualityComparer stateComparer; 14 | 15 | public Planner(IStateCopier stateCopier, IEqualityComparer stateComparer) 16 | { 17 | this.stateCopier = stateCopier; 18 | this.stateComparer = stateComparer; 19 | } 20 | 21 | /// Execute the plan. 22 | public Plan Execute(PlanParameters @params) 23 | { 24 | if (@params.GoalEvaluator == null) 25 | throw new ArgumentOutOfRangeException(nameof(@params.GoalEvaluator)); 26 | if (@params.HeuristicCost == null) 27 | throw new ArgumentOutOfRangeException(nameof(@params.HeuristicCost)); 28 | if (@params.GetActions == null) 29 | throw new ArgumentOutOfRangeException(nameof(@params.GetActions)); 30 | if (@params.StartingState == null) 31 | throw new ArgumentOutOfRangeException(nameof(@params.StartingState)); 32 | 33 | /* 34 | * AStar: 35 | * g score: current distance from the start measured by sum of action costs 36 | * h score: heuristic of how close the node's state is to goal state, supplied by caller 37 | * f score: sum of (g, h), used as priority of the node 38 | */ 39 | var heuristicCost = @params.HeuristicCost; 40 | var evalGoal = @params.GoalEvaluator; 41 | var maxHScore = @params.MaxHeuristicCost; 42 | 43 | var start = new StateNode(@params.StartingState, null, null); 44 | var openSet = CreateQueue(@params); 45 | openSet.Enqueue(start, 0); 46 | 47 | var distanceScores = new DefaultDictionary(int.MaxValue, stateComparer) 48 | { 49 | [start.ResultingState] = 0 50 | }; 51 | 52 | var iterations = 0; 53 | while (openSet.Any() && ++iterations < @params.MaxIterations) 54 | { 55 | var current = openSet.Dequeue(); 56 | if (evalGoal(current.ResultingState)) 57 | return ReconstructPath(current, @params.StartingState); 58 | 59 | foreach (var neighbor in GetNeighbors(current, @params.GetActions(current.ResultingState))) 60 | { 61 | var distScore = distanceScores[current.ResultingState] + neighbor.GetActionCost(current.ResultingState); 62 | if (distScore >= distanceScores[neighbor.ResultingState]) 63 | continue; 64 | 65 | distanceScores[neighbor.ResultingState] = distScore; 66 | var hCost = heuristicCost(neighbor.ResultingState); 67 | if(hCost > maxHScore) 68 | continue; 69 | var finalScore = distScore + hCost; 70 | if (!openSet.Contains(neighbor)) 71 | openSet.Enqueue(neighbor, finalScore); 72 | } 73 | } 74 | 75 | return new Plan 76 | { 77 | Success = false, 78 | Steps = new List>() 79 | }; 80 | } 81 | 82 | private static IPriorityQueue, float> CreateQueue(PlanParameters args) 83 | { 84 | if (args.UseFastQueue) 85 | return new FastPriorityQueue>(args.QueueMaxSize); 86 | return new SimplePriorityQueue>(); 87 | } 88 | 89 | private static Plan ReconstructPath(StateNode final, T startingState) 90 | { 91 | var current = final; 92 | var path = new List>(); 93 | while (current.Parent != null) 94 | { 95 | path.Add(current); 96 | current = current.Parent; 97 | } 98 | 99 | path.Reverse(); 100 | return new Plan 101 | { 102 | Success = true, 103 | Steps = path.Select((step, i) => new PlanStep 104 | { 105 | Index = i, 106 | Action = step.SourceAction, 107 | AfterState = step.ResultingState, 108 | BeforeState = step.Parent == null ? startingState : step.Parent.ResultingState 109 | }).ToList() 110 | }; 111 | } 112 | 113 | private IEnumerable> GetNeighbors(StateNode start, 114 | IEnumerable> actions) 115 | { 116 | var currentState = start.ResultingState; 117 | 118 | foreach (var action in actions) 119 | { 120 | var newState = action.TakeActionOnState(stateCopier.Copy(currentState)); 121 | 122 | // sometimes actions have no effect on state, in which case we don't want to entertain them as nodes 123 | // assuming that additional actions to get to the same state is always worse 124 | if(!stateComparer.Equals(currentState, newState)) 125 | yield return new StateNode(newState, start, action); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/SimpleGOAP.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 4 | SimpleGOAP 5 | SimpleGOAP.Core 6 | net472;net48;net5.0;net5.0-windows;net6.0;net6.0-windows;netstandard1.0;netstandard1.1;netstandard1.2;netstandard1.3;netstandard1.4;netstandard1.5;netstandard1.6;netstandard1.7;netstandard2.0;netstandard2.1 7 | 0.0.7 8 | SimpleGOAP.Core 9 | Tom Kerr 10 | SimpleGOAP is a C# implementation of goal oriented action planning. 11 | 2022 Tom Kerr 12 | https://github.com/tckerr/SimpleGOAP 13 | https://github.com/tckerr/SimpleGOAP 14 | GOAP, AI 15 | git 16 | MIT 17 | true 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SimpleGOAP.Core/StateNode.cs: -------------------------------------------------------------------------------- 1 | using Priority_Queue; 2 | 3 | namespace SimpleGOAP 4 | { 5 | internal class StateNode : StablePriorityQueueNode 6 | { 7 | internal IAction SourceAction; 8 | internal StateNode Parent; 9 | internal T ResultingState; 10 | 11 | internal StateNode(T state, StateNode parent, IAction sourceAction) 12 | { 13 | SourceAction = sourceAction; 14 | Parent = parent; 15 | ResultingState = state; 16 | } 17 | 18 | internal int GetActionCost(T state) => SourceAction?.GetCost(state) ?? 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleGOAP.KeyValueState/Fact.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP.KeyValueState 2 | { 3 | public class Fact 4 | { 5 | public readonly TKey Key; 6 | public readonly TVal Value; 7 | 8 | public Fact(TKey key, TVal value) 9 | { 10 | Key = key; 11 | Value = value; 12 | } 13 | 14 | public override int GetHashCode() 15 | { 16 | unchecked 17 | { 18 | var hash = 17; 19 | hash = hash * 23 + Key.GetHashCode(); 20 | hash = hash * 23 + Value.GetHashCode(); 21 | return hash; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SimpleGOAP.KeyValueState/KeyValuePlanner.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP.KeyValueState 2 | { 3 | public class KeyValuePlanner : Planner> 4 | { 5 | public KeyValuePlanner() : base(new KeyValueStateCopier(), new KeyValueStateComparer()) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SimpleGOAP.KeyValueState/KeyValueState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SimpleGOAP.KeyValueState 5 | { 6 | public class KeyValueState 7 | { 8 | public List> Facts { get; } = new List>(); 9 | private readonly Dictionary indices = new Dictionary(); 10 | 11 | public void Set(TKey key, TVal val) 12 | { 13 | if (!indices.TryGetValue(key, out var idx)) 14 | { 15 | indices[key] = indices.Count; 16 | Facts.Add(new Fact(key, val)); 17 | } 18 | else 19 | Facts[idx] = new Fact(key, val); 20 | } 21 | 22 | public void Set(TKey key, Func setter) where T : TVal => 23 | Set(key, setter((T) Facts[indices[key]].Value)); 24 | 25 | public void Set(Fact fact) => Set(fact.Key, fact.Value); 26 | 27 | public T2 Get(TKey key) where T2 : TVal 28 | { 29 | if (!indices.TryGetValue(key, out var idx)) 30 | throw new Exception($"Fact key '{key}' not registered"); 31 | var val = Facts[idx].Value; 32 | if (!(val is T2 tval)) 33 | throw new Exception($"Fact of type {val.GetType().FullName} is not type {typeof(T2).FullName}"); 34 | return tval; 35 | } 36 | 37 | public bool Check(Fact fact) => Check(fact.Key, fact.Value); 38 | public bool Check(TKey key, TVal val) => indices.TryGetValue(key, out var idx) && Facts[idx].Value.Equals(val); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SimpleGOAP.KeyValueState/KeyValueStateComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP.KeyValueState 4 | { 5 | public class KeyValueStateComparer : IEqualityComparer> 6 | { 7 | public bool Equals(KeyValueState x, KeyValueState y) 8 | { 9 | if (x == null || y == null || x.Facts.Count != y.Facts.Count) 10 | return false; 11 | return GetHashCode(x) == GetHashCode(y); 12 | } 13 | 14 | public int GetHashCode(KeyValueState state) 15 | { 16 | var facts = state.Facts; 17 | if (facts == null) 18 | return 0; 19 | unchecked 20 | { 21 | var hash = 17; 22 | foreach (var fact in facts) 23 | hash = hash * 23 + (fact.GetHashCode()); 24 | return hash; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SimpleGOAP.KeyValueState/KeyValueStateCopier.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP.KeyValueState 2 | { 3 | public class KeyValueStateCopier : IStateCopier> 4 | { 5 | public KeyValueState Copy(KeyValueState state) 6 | { 7 | var newState = new KeyValueState(); 8 | foreach (var fact in state.Facts) 9 | newState.Set(fact); 10 | 11 | return newState; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SimpleGOAP.KeyValueState/SimpleGOAP.KeyValueState.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | preview 5 | SimpleGOAP.KeyValueState 6 | SimpleGOAP.KeyValueState 7 | net472;net48;net5.0;net5.0-windows;net6.0;net6.0-windows;netstandard1.0;netstandard1.1;netstandard1.2;netstandard1.3;netstandard1.4;netstandard1.5;netstandard1.6;netstandard1.7;netstandard2.0;netstandard2.1 8 | true 9 | 0.0.2 10 | https://github.com/tckerr/SimpleGOAP 11 | An easy-to-use key/value data store for use with SimpleGOAP.Core. 12 | 2022 Tom Kerr 13 | https://github.com/tckerr/SimpleGOAP 14 | https://github.com/tckerr/SimpleGOAP 15 | git 16 | MIT 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/DrumStacker/DrumStackerPlannerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace SimpleGOAP.Tests.Data.DrumStacker 6 | { 7 | public static class DrumStackerPlannerFactory 8 | { 9 | public static (PlanParameters, Planner) Create() 10 | { 11 | var allSizes = (DrumSize[]) Enum.GetValues(typeof(DrumSize)); 12 | 13 | int HeuristicCost(DrumStackerState state) 14 | { 15 | var count = 0; 16 | foreach (var stack in state.Stacks) 17 | { 18 | for (var i = 0; i < stack.Drums.Count; i++) 19 | { 20 | var drum = stack.Drums[i]; 21 | if (drum.Color != stack.StackColor) 22 | count++; 23 | 24 | if (i < 3 && drum.Size != allSizes[i]) 25 | count++; 26 | } 27 | } 28 | 29 | return count * 300; 30 | } 31 | 32 | IEnumerable> GetActions(DrumStackerState state) 33 | { 34 | for (var i = 0; i < state.Stacks.Count; i++) 35 | { 36 | var stack = state.Stacks[i]; 37 | 38 | if (stack.Drums.Count == 0) 39 | continue; 40 | 41 | var drumToMove = stack.Drums[stack.Drums.Count - 1]; 42 | 43 | for (var j = 0; j < state.Stacks.Count; j++) 44 | { 45 | if (i == j || state.Stacks[j].Drums.Count >= 4) 46 | continue; 47 | 48 | var size = drumToMove.Size; 49 | var color = drumToMove.Color; 50 | var targetIdx = j; 51 | yield return new LambdaAction( 52 | $"Move {size} {color} to stack {j}", 53 | s => 54 | { 55 | var currentStack = s.Stacks 56 | .First(st => 57 | st.Drums.Any(d => d.Color == color && d.Size == size)); 58 | 59 | var me = currentStack.Drums.First(d => 60 | d.Color == color && d.Size == size); 61 | 62 | currentStack.Drums.Remove(me); 63 | s.Stacks[targetIdx].Drums.Add(me); 64 | }); 65 | } 66 | } 67 | } 68 | 69 | var initialState = new DrumStackerState 70 | { 71 | Stacks = new List 72 | { 73 | new DrumStack 74 | { 75 | StackColor = DrumColor.Blue, 76 | Drums = new List 77 | { 78 | new Drum {Color = DrumColor.Blue, Size = DrumSize.Large}, 79 | new Drum {Color = DrumColor.Red, Size = DrumSize.Large}, 80 | new Drum {Color = DrumColor.Red, Size = DrumSize.Small}, 81 | } 82 | }, 83 | new DrumStack 84 | { 85 | StackColor = DrumColor.Yellow, 86 | Drums = new List 87 | { 88 | new Drum {Color = DrumColor.Yellow, Size = DrumSize.Medium}, 89 | new Drum {Color = DrumColor.Green, Size = DrumSize.Small}, 90 | new Drum {Color = DrumColor.Blue, Size = DrumSize.Small}, 91 | } 92 | }, 93 | new DrumStack 94 | { 95 | StackColor = DrumColor.Green, 96 | Drums = new List 97 | { 98 | new Drum {Color = DrumColor.Yellow, Size = DrumSize.Large}, 99 | new Drum {Color = DrumColor.Green, Size = DrumSize.Medium}, 100 | new Drum {Color = DrumColor.Blue, Size = DrumSize.Medium}, 101 | } 102 | }, 103 | new DrumStack 104 | { 105 | StackColor = DrumColor.Red, 106 | Drums = new List 107 | { 108 | new Drum {Color = DrumColor.Red, Size = DrumSize.Medium}, 109 | new Drum {Color = DrumColor.Yellow, Size = DrumSize.Small}, 110 | new Drum {Color = DrumColor.Green, Size = DrumSize.Large}, 111 | } 112 | }, 113 | } 114 | }; 115 | 116 | var data = new PlanParameters 117 | { 118 | StartingState = initialState, 119 | HeuristicCost = HeuristicCost, 120 | GoalEvaluator = state => HeuristicCost(state) == 0, 121 | GetActions = GetActions 122 | }; 123 | 124 | var planner = new Planner( 125 | new DrumStackerStateCopier(), 126 | new DrumStackerStateComparer() 127 | ); 128 | 129 | return (data, planner); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/DrumStacker/DrumStackerState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP.Tests.Data.DrumStacker 4 | { 5 | public enum DrumColor 6 | { 7 | Blue, 8 | Yellow, 9 | Green, 10 | Red 11 | } 12 | public enum DrumSize 13 | { 14 | Large, 15 | Medium, 16 | Small, 17 | } 18 | 19 | public class Drum 20 | { 21 | public DrumColor Color; 22 | public DrumSize Size; 23 | } 24 | 25 | public class DrumStack 26 | { 27 | public DrumColor StackColor; 28 | public List Drums = new List 29 | { 30 | new Drum(), 31 | new Drum(), 32 | new Drum(), 33 | new Drum(), 34 | }; 35 | } 36 | 37 | public class DrumStackerState 38 | { 39 | public List Stacks = new List 40 | { 41 | new DrumStack{StackColor = DrumColor.Blue}, 42 | new DrumStack{StackColor = DrumColor.Yellow}, 43 | new DrumStack{StackColor = DrumColor.Green}, 44 | new DrumStack{StackColor = DrumColor.Red}, 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/DrumStacker/DrumStackerStateComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP.Tests.Data.DrumStacker 4 | { 5 | public class DrumStackerStateComparer : IEqualityComparer 6 | { 7 | public bool Equals(DrumStackerState x, DrumStackerState y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | 14 | if (x.Stacks.Count != y.Stacks.Count) 15 | return false; 16 | 17 | for (var i = 0; i < x.Stacks.Count; i++) 18 | { 19 | var xStack = x.Stacks[i]; 20 | var yStack = y.Stacks[i]; 21 | 22 | if (xStack.StackColor != yStack.StackColor) 23 | return false; 24 | 25 | if (yStack.Drums.Count != xStack.Drums.Count) 26 | return false; 27 | 28 | for (var j = 0; j < xStack.Drums.Count; j++) 29 | { 30 | var xDrum = xStack.Drums[j]; 31 | var yDrum = yStack.Drums[j]; 32 | if (xDrum.Color != yDrum.Color) 33 | return false; 34 | if (xDrum.Size != yDrum.Size) 35 | return false; 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | public int GetHashCode(DrumStackerState obj) 42 | { 43 | int Hash(IReadOnlyList drums, int idx) 44 | { 45 | if (idx >= drums.Count) 46 | return 1; 47 | var drum = drums[idx]; 48 | return drum == null ? 0 : new {drum.Color, drum.Size}.GetHashCode(); 49 | } 50 | 51 | return new 52 | { 53 | a1 = Hash(obj.Stacks[0].Drums, 0), 54 | a2 = Hash(obj.Stacks[0].Drums, 1), 55 | a3 = Hash(obj.Stacks[0].Drums, 2), 56 | a4 = Hash(obj.Stacks[0].Drums, 3), 57 | a5 = obj.Stacks[0].StackColor, 58 | b1 = Hash(obj.Stacks[1].Drums, 0), 59 | b2 = Hash(obj.Stacks[1].Drums, 1), 60 | b3 = Hash(obj.Stacks[1].Drums, 2), 61 | b4 = Hash(obj.Stacks[1].Drums, 3), 62 | b5 = obj.Stacks[1].StackColor, 63 | c1 = Hash(obj.Stacks[2].Drums, 0), 64 | c2 = Hash(obj.Stacks[2].Drums, 1), 65 | c3 = Hash(obj.Stacks[2].Drums, 2), 66 | c4 = Hash(obj.Stacks[2].Drums, 3), 67 | c5 = obj.Stacks[2].StackColor, 68 | d1 = Hash(obj.Stacks[3].Drums, 0), 69 | d2 = Hash(obj.Stacks[3].Drums, 1), 70 | d3 = Hash(obj.Stacks[3].Drums, 2), 71 | d4 = Hash(obj.Stacks[3].Drums, 3), 72 | d5 = obj.Stacks[3].StackColor, 73 | }.GetHashCode(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/DrumStacker/DrumStackerStateCopier.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace SimpleGOAP.Tests.Data.DrumStacker 4 | { 5 | public class DrumStackerStateCopier : IStateCopier 6 | { 7 | public DrumStackerState Copy(DrumStackerState state) => 8 | new DrumStackerState 9 | { 10 | Stacks = state.Stacks.Select(s => new DrumStack 11 | { 12 | StackColor = s.StackColor, 13 | Drums = s.Drums.Select(d => new Drum 14 | { 15 | Color = d.Color, 16 | Size = d.Size 17 | }).ToList() 18 | }).ToList() 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/ReadmeExample/PotatoState.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP.Tests.Data.ReadmeExample 2 | { 3 | public class PotatoState 4 | { 5 | public int RawPotatoes = 0; 6 | public int Wood = 0; 7 | public bool Fire = false; 8 | public int BakedPotatoes = 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/ReadmeExample/PotatoStateCopier.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP.Tests.Data.ReadmeExample 2 | { 3 | public class PotatoStateCopier : IStateCopier 4 | { 5 | public PotatoState Copy(PotatoState state) 6 | { 7 | return new PotatoState 8 | { 9 | RawPotatoes = state.RawPotatoes, 10 | Wood = state.Wood, 11 | Fire = state.Fire, 12 | BakedPotatoes = state.BakedPotatoes 13 | }; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/ReadmeExample/PotatoStateEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP.Tests.Data.ReadmeExample 4 | { 5 | public class PotatoStateEqualityComparer : IEqualityComparer 6 | { 7 | public bool Equals(PotatoState x, PotatoState y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | return x.RawPotatoes == y.RawPotatoes && x.Wood == y.Wood && x.Fire == y.Fire && x.BakedPotatoes == y.BakedPotatoes; 14 | } 15 | 16 | public int GetHashCode(PotatoState obj) 17 | { 18 | return new {obj.RawPotatoes, obj.Wood, obj.Fire, obj.BakedPotatoes}.GetHashCode(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/ReadmeExample/PotatoStatePlannerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SimpleGOAP.Tests.Data.ReadmeExample 5 | { 6 | public static class PotatoStatePlannerFactory 7 | { 8 | public static (PlanParameters, Planner) Create() 9 | { 10 | var planner = new Planner( 11 | new PotatoStateCopier(), 12 | new PotatoStateEqualityComparer() 13 | ); 14 | 15 | Func goalEvaluator = state => state.BakedPotatoes >= 5; 16 | Func heuristicCost = state => 5 - state.BakedPotatoes; 17 | 18 | var makeFire = new LambdaAction("Make fire", 1, 19 | state => 20 | { 21 | state.Fire = true; 22 | state.Wood -= 3; 23 | }); 24 | 25 | var cookPotato = new LambdaAction("Cook", 1, 26 | state => 27 | { 28 | state.RawPotatoes--; 29 | state.BakedPotatoes++; 30 | }); 31 | 32 | var harvestPotato = new LambdaAction("Harvest potato", 1, 33 | state => state.RawPotatoes++); 34 | 35 | var chopWood = new LambdaAction("Chop wood", 1, 36 | state => state.Wood++); 37 | 38 | IEnumerable> GetActions(PotatoState state) 39 | { 40 | yield return harvestPotato; 41 | yield return chopWood; 42 | 43 | if (state.Wood >= 3) 44 | yield return makeFire; 45 | 46 | if (state.Fire && state.RawPotatoes > 0) 47 | yield return cookPotato; 48 | } 49 | 50 | var planParameters = new PlanParameters 51 | { 52 | StartingState = new PotatoState(), 53 | GetActions = GetActions, 54 | HeuristicCost = heuristicCost, 55 | GoalEvaluator = goalEvaluator 56 | }; 57 | return (planParameters, planner); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/RiverCrossing/RiverCrossingPlannerFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP.Tests.Data.RiverCrossing 4 | { 5 | public static class RiverCrossingPlannerFactory 6 | { 7 | public static (PlanParameters, Planner) Create() 8 | { 9 | int HeuristicCost(RiverCrossingState state1) 10 | { 11 | var cost = 0; 12 | if (!state1.Wolf) cost++; 13 | if (!state1.Goat) cost++; 14 | if (!state1.Cabbage) cost++; 15 | if (!state1.Farmer) cost++; 16 | 17 | if (state1.Wolf == state1.Goat && state1.Wolf != state1.Farmer) cost += 999; 18 | 19 | if (state1.Goat == state1.Cabbage && state1.Goat != state1.Farmer) cost += 999; 20 | 21 | return cost; 22 | } 23 | 24 | var returnFarmer = new LambdaAction( 25 | "Return", 26 | state => state.Farmer = false 27 | ); 28 | 29 | var moveCabbageLeft = new LambdaAction( 30 | "Move cabbage left", 31 | state => 32 | { 33 | state.Farmer = true; 34 | state.Cabbage = true; 35 | } 36 | ); 37 | 38 | var moveCabbageRight = new LambdaAction( 39 | "Move cabbage right", 40 | state => 41 | { 42 | state.Farmer = false; 43 | state.Cabbage = false; 44 | } 45 | ); 46 | 47 | var moveWolfLeft = new LambdaAction( 48 | "Move wolf left", 49 | state => 50 | { 51 | state.Farmer = true; 52 | state.Wolf = true; 53 | } 54 | ); 55 | 56 | var moveWolfRight = new LambdaAction( 57 | "Move wolf right", 58 | state => 59 | { 60 | state.Farmer = false; 61 | state.Wolf = false; 62 | } 63 | ); 64 | 65 | var moveGoatLeft = new LambdaAction( 66 | "Move goat left", 67 | state => 68 | { 69 | state.Farmer = true; 70 | state.Goat = true; 71 | } 72 | ); 73 | 74 | var moveGoatRight = new LambdaAction( 75 | "Move goat right", 76 | state => 77 | { 78 | state.Farmer = false; 79 | state.Goat = false; 80 | } 81 | ); 82 | 83 | var moveFarmerLeft = new LambdaAction( 84 | "Move farmer left", 85 | state => state.Farmer = true 86 | ); 87 | 88 | var data = new PlanParameters 89 | { 90 | StartingState = new RiverCrossingState(), 91 | MaxHeuristicCost = 50, 92 | HeuristicCost = HeuristicCost, 93 | GoalEvaluator = state => HeuristicCost(state) == 0, 94 | GetActions = s => 95 | { 96 | var actions = new List>(); 97 | 98 | if(s.Farmer) 99 | actions.Add(returnFarmer); 100 | 101 | if(!s.Farmer) 102 | actions.Add(moveFarmerLeft); 103 | 104 | if(!s.Farmer && !s.Cabbage && s.Wolf != s.Goat) 105 | actions.Add(moveCabbageLeft); 106 | 107 | if(s.Farmer && s.Cabbage) 108 | actions.Add(moveCabbageRight); 109 | 110 | if(!s.Farmer && !s.Wolf) 111 | actions.Add(moveWolfLeft); 112 | 113 | if(s.Farmer && s.Wolf) 114 | actions.Add(moveWolfRight); 115 | 116 | if(!s.Farmer && !s.Goat) 117 | actions.Add(moveGoatLeft); 118 | 119 | if(s.Farmer && s.Goat) 120 | actions.Add(moveGoatRight); 121 | 122 | return actions; 123 | } 124 | }; 125 | 126 | var planner = new Planner( 127 | new LambdaCopier(state => new RiverCrossingState 128 | { 129 | Cabbage = state.Cabbage, 130 | Goat = state.Goat, 131 | Wolf = state.Wolf, 132 | Farmer = state.Farmer 133 | }), 134 | new RiverCrossingStateComparer() 135 | ); 136 | 137 | return (data, planner); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/RiverCrossing/RiverCrossingState.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleGOAP.Tests.Data.RiverCrossing 2 | { 3 | public class RiverCrossingState 4 | { 5 | public bool Wolf; 6 | public bool Goat; 7 | public bool Cabbage; 8 | public bool Farmer; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/RiverCrossing/RiverCrossingStateComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SimpleGOAP.Tests.Data.RiverCrossing 4 | { 5 | public class RiverCrossingStateComparer : IEqualityComparer 6 | { 7 | public bool Equals(RiverCrossingState x, RiverCrossingState y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | return x.Wolf == y.Wolf && x.Goat == y.Goat && x.Cabbage == y.Cabbage && x.Farmer == y.Farmer; 14 | } 15 | 16 | public int GetHashCode(RiverCrossingState obj) 17 | { 18 | unchecked 19 | { 20 | var hashCode = obj.Wolf.GetHashCode(); 21 | hashCode = (hashCode * 397) ^ obj.Goat.GetHashCode(); 22 | hashCode = (hashCode * 397) ^ obj.Cabbage.GetHashCode(); 23 | hashCode = (hashCode * 397) ^ obj.Farmer.GetHashCode(); 24 | return hashCode; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/Actions/DriveAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SimpleGOAP.KeyValueState; 5 | 6 | namespace SimpleGOAP.Tests.Data.Traveler.Actions 7 | { 8 | public class DriveAction : IAction> 9 | { 10 | private const double GasPerDistance = 10; 11 | private readonly string location; 12 | private readonly List<(string, int, int)> locations; 13 | 14 | public DriveAction(string location, List<(string, int, int)> locations) 15 | { 16 | this.location = location; 17 | this.locations = locations; 18 | } 19 | 20 | public string Title => $"Drive to {location}"; 21 | public int GetCost(KeyValueState state) => 10; 22 | 23 | private int GasSpentForDistance(KeyValueState state) 24 | { 25 | var currentLocation = state.Get("myLocation"); 26 | var (_, currentX, currentY) = locations.First(l => l.Item1 == currentLocation); 27 | var (_, targetX, targetY) = locations.First(l => l.Item1 == location); 28 | var distance = Math.Sqrt(Math.Pow(targetX - currentX, 2) + Math.Pow(targetY - currentY, 2)); 29 | return Convert.ToInt32(distance * GasPerDistance); 30 | } 31 | 32 | public bool IsLegalForState(KeyValueState state) 33 | { 34 | return state.Get("gas") >= GasSpentForDistance(state); 35 | } 36 | 37 | public KeyValueState TakeActionOnState(KeyValueState state) 38 | { 39 | var gasCost = GasSpentForDistance(state); 40 | state.Set("gas", state.Get("gas") - gasCost); 41 | state.Set("myLocation", location); 42 | return state; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/Actions/EatAction.cs: -------------------------------------------------------------------------------- 1 | using SimpleGOAP.KeyValueState; 2 | 3 | namespace SimpleGOAP.Tests.Data.Traveler.Actions 4 | { 5 | public class EatAction : IAction> 6 | { 7 | public string Title => $"Eat food"; 8 | public int GetCost(KeyValueState state) => 10; 9 | 10 | public bool IsLegalForState(KeyValueState state) 11 | { 12 | return state.Check("myLocation", "Restaurant") && state.Get("food") > 0; 13 | } 14 | 15 | public KeyValueState TakeActionOnState(KeyValueState state) 16 | { 17 | state.Set("food", state.Get("food") - 1); 18 | state.Set("full", true); 19 | return state; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/Actions/PurchaseAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SimpleGOAP.KeyValueState; 3 | 4 | namespace SimpleGOAP.Tests.Data.Traveler.Actions 5 | { 6 | public class PurchaseAction : IAction> 7 | { 8 | private readonly string itemName; 9 | private readonly string storeName; 10 | private readonly int cost; 11 | private readonly int amountPerPurchase; 12 | private readonly int? max; 13 | 14 | public PurchaseAction(string itemName, string storeName, int cost, int amountPerPurchase, int? max = null) 15 | { 16 | this.max = max; 17 | this.itemName = itemName; 18 | this.storeName = storeName; 19 | this.cost = cost; 20 | this.amountPerPurchase = amountPerPurchase; 21 | } 22 | 23 | public string Title => $"Purchase {itemName} x{amountPerPurchase} for ${cost}"; 24 | public int GetCost(KeyValueState state) => 10; 25 | 26 | public bool IsLegalForState(KeyValueState state) 27 | { 28 | return state.Get("money") >= cost 29 | && state.Check("myLocation", storeName) 30 | && (max == null || state.Get(itemName) < max); 31 | } 32 | 33 | public KeyValueState TakeActionOnState(KeyValueState state) 34 | { 35 | state.Set(itemName, Math.Min(max ?? int.MaxValue, state.Get(itemName) + amountPerPurchase)); 36 | state.Set("money", state.Get("money") - cost); 37 | return state; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/Actions/SellAction.cs: -------------------------------------------------------------------------------- 1 | using SimpleGOAP.KeyValueState; 2 | 3 | namespace SimpleGOAP.Tests.Data.Traveler.Actions 4 | { 5 | public class SellAction : IAction> 6 | { 7 | private string itemName; 8 | private int amountPerItem; 9 | 10 | public SellAction(string itemName, int amountPerItem) 11 | { 12 | this.itemName = itemName; 13 | this.amountPerItem = amountPerItem; 14 | } 15 | 16 | public string Title => $"Sell 1 {itemName} for ${amountPerItem} on eBay"; 17 | 18 | public int GetCost(KeyValueState state) => 10; 19 | 20 | public bool IsLegalForState(KeyValueState state) 21 | { 22 | return state.Get(itemName) > 0 && state.Check("myLocation", "Home"); 23 | } 24 | 25 | public KeyValueState TakeActionOnState(KeyValueState state) 26 | { 27 | state.Set(itemName, f => f - 1); 28 | state.Set("money", m => m + amountPerItem); 29 | return state; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/Actions/SleepAction.cs: -------------------------------------------------------------------------------- 1 | using SimpleGOAP.KeyValueState; 2 | 3 | namespace SimpleGOAP.Tests.Data.Traveler.Actions 4 | { 5 | public class SleepAction : IAction> 6 | { 7 | public string Title => "Sleep"; 8 | public int GetCost(KeyValueState state) => 10; 9 | 10 | public bool IsLegalForState(KeyValueState state) 11 | { 12 | return state.Check("myLocation", "Home") && state.Get("fatigue") > 0; 13 | } 14 | 15 | public KeyValueState TakeActionOnState(KeyValueState state) 16 | { 17 | state.Set("fatigue", 0); 18 | return state; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/Actions/WatchMovieAction.cs: -------------------------------------------------------------------------------- 1 | using SimpleGOAP.KeyValueState; 2 | 3 | namespace SimpleGOAP.Tests.Data.Traveler.Actions 4 | { 5 | public class WatchMovieAction : IAction> 6 | { 7 | public string Title => "Watch movie for $20"; 8 | public int GetCost(KeyValueState state) => 10; 9 | 10 | public bool IsLegalForState(KeyValueState state) 11 | { 12 | return state.Check("myLocation", "Theater") && state.Get("money") >= 20; 13 | } 14 | 15 | public KeyValueState TakeActionOnState(KeyValueState state) 16 | { 17 | state.Set("money", state.Get("money") - 20); 18 | state.Set("fun", state.Get("fun") + 1); 19 | return state; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/Actions/WorkAction.cs: -------------------------------------------------------------------------------- 1 | using SimpleGOAP.KeyValueState; 2 | 3 | namespace SimpleGOAP.Tests.Data.Traveler.Actions 4 | { 5 | public class WorkAction : IAction> 6 | { 7 | private readonly string workLocation; 8 | private readonly int amountEarned; 9 | 10 | public WorkAction(string workLocation, int amountEarned) 11 | { 12 | this.workLocation = workLocation; 13 | this.amountEarned = amountEarned; 14 | } 15 | 16 | public string Title => $"Earn ${amountEarned} at {workLocation}"; 17 | public int GetCost(KeyValueState state) => 10; 18 | 19 | public bool IsLegalForState(KeyValueState state) 20 | { 21 | return state.Check("myLocation", workLocation) && state.Get("fatigue") < 3; 22 | } 23 | 24 | public KeyValueState TakeActionOnState(KeyValueState state) 25 | { 26 | state.Set("money", state.Get("money") + amountEarned); 27 | state.Set("fatigue", f => f + 1); 28 | return state; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/Data/Traveler/TravelerDataFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using SimpleGOAP.KeyValueState; 4 | using SimpleGOAP.Tests.Data.Traveler.Actions; 5 | 6 | namespace SimpleGOAP.Tests.Data.Traveler 7 | { 8 | public static class TravelerDataFactory 9 | { 10 | public static (PlanParameters>, Planner>) Create() 11 | { 12 | const int COST_OF_TOY = 10; 13 | const int SELL_VALUE_OF_TOY = 30; 14 | const int COST_OF_GAS = 50; 15 | const int COST_OF_FOOD = 30; 16 | const int GAS_TANK_CAPACITY = 40; 17 | const int WAGE = 20; 18 | 19 | var currentState = new KeyValueState(); 20 | foreach (var (key, val) in new (string, object)[] 21 | { 22 | ("myLocation", "Home"), 23 | ("food", 0), 24 | ("full", false), 25 | ("money", 0), 26 | ("gas", 40), 27 | ("fun", 0), 28 | ("fatigue", 0), 29 | ("toy", 0), 30 | }) 31 | { 32 | currentState.Set(new Fact(key, val)); 33 | } 34 | 35 | 36 | var locations = new List<(string, int, int)> 37 | { 38 | ("Restaurant", 2, 2), 39 | ("Work", 1, 0), 40 | ("Gas Station", 1, 1), 41 | ("Home", 0, 0), 42 | ("Theater", 2, 0), 43 | }; 44 | 45 | var actions = new IAction>[] 46 | { 47 | new DriveAction("Restaurant", locations), 48 | new DriveAction("Work", locations), 49 | new DriveAction("Home", locations), 50 | new DriveAction("Gas Station", locations), 51 | new DriveAction("Theater", locations), 52 | new PurchaseAction("gas", "Gas Station", COST_OF_GAS, GAS_TANK_CAPACITY, GAS_TANK_CAPACITY), 53 | new PurchaseAction("toy", "Gas Station", COST_OF_TOY, 3, 6), 54 | new PurchaseAction("food", "Restaurant", COST_OF_FOOD, 1), 55 | new SellAction("toy", SELL_VALUE_OF_TOY), 56 | new WorkAction("Work", WAGE), 57 | new WatchMovieAction(), 58 | new SleepAction(), 59 | new EatAction() 60 | }; 61 | 62 | int HeuristicCost(KeyValueState state) => 63 | new[] 64 | { 65 | state.Check("full", true) ? 0 : 1, 66 | state.Check("myLocation", "Home") ? 0 : 1, 67 | state.Get("fun") >= 2 ? 0 : 1, 68 | state.Get("fatigue") <= 0 ? 0 : 1, 69 | }.Sum() * 300; 70 | 71 | var args = new PlanParameters> 72 | { 73 | GetActions = _ => actions, 74 | StartingState = currentState, 75 | HeuristicCost = HeuristicCost, 76 | GoalEvaluator = s => HeuristicCost(s) <= 0, 77 | }; 78 | 79 | return (args, new KeyValuePlanner()); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/PerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SimpleGOAP.KeyValueState; 3 | using SimpleGOAP.Tests.Data.ReadmeExample; 4 | using SimpleGOAP.Tests.Data.Traveler; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace SimpleGOAP.Tests 9 | { 10 | public class PerformanceTests 11 | { 12 | private readonly ITestOutputHelper testOutputHelper; 13 | 14 | public PerformanceTests(ITestOutputHelper testOutputHelper) 15 | { 16 | this.testOutputHelper = testOutputHelper; 17 | } 18 | 19 | [Fact] 20 | public void TestKeyValuePlannerPerformance() 21 | { 22 | var (data, subject) = TravelerDataFactory.Create(); 23 | 24 | var start = DateTime.Now; 25 | var iterations = 300; 26 | for (var i = 0; i < iterations; i++) 27 | subject.Execute(data); 28 | var duration = DateTime.Now - start; 29 | 30 | testOutputHelper.WriteLine($"Plan x{iterations} complete after {duration.TotalSeconds}s:"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/PlannerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using SimpleGOAP.KeyValueState; 4 | using SimpleGOAP.Tests.Data.DrumStacker; 5 | using SimpleGOAP.Tests.Data.ReadmeExample; 6 | using SimpleGOAP.Tests.Data.RiverCrossing; 7 | using SimpleGOAP.Tests.Data.Traveler; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace SimpleGOAP.Tests 12 | { 13 | public class PlannerTests 14 | { 15 | private readonly ITestOutputHelper testOutputHelper; 16 | 17 | public PlannerTests(ITestOutputHelper testOutputHelper) 18 | { 19 | this.testOutputHelper = testOutputHelper; 20 | } 21 | 22 | [Fact] 23 | public void TestReadmeExample() 24 | { 25 | var (args, planner) = PotatoStatePlannerFactory.Create(); 26 | foreach (var step in planner.Execute(args).Steps) 27 | testOutputHelper.WriteLine(step.Action.Title); 28 | } 29 | 30 | [Fact] 31 | public void TestTravelerExample() 32 | { 33 | var (data, subject) = TravelerDataFactory.Create(); 34 | 35 | var start = DateTime.Now; 36 | var plan = subject.Execute(data); 37 | var duration = DateTime.Now - start; 38 | 39 | testOutputHelper.WriteLine($"Plan complete after {duration.TotalMilliseconds}ms:"); 40 | foreach (var step in plan.Steps) 41 | testOutputHelper.WriteLine($"\t{step.Action.Title}"); 42 | 43 | Assert.True(plan.Success); 44 | } 45 | [Fact] 46 | public void TestRiverCrossing() 47 | { 48 | // https://en.wikipedia.org/wiki/Wolf,_goat_and_cabbage_problem 49 | var (data, subject) = RiverCrossingPlannerFactory.Create(); 50 | 51 | var start = DateTime.Now; 52 | var plan = subject.Execute(data); 53 | var duration = DateTime.Now - start; 54 | 55 | testOutputHelper.WriteLine($"Plan complete after {duration.TotalMilliseconds}ms:"); 56 | foreach (var step in plan.Steps) 57 | testOutputHelper.WriteLine($"\t{step.Action.Title}"); 58 | 59 | Assert.True(plan.Success); 60 | Assert.Equal("Move goat left", plan.Steps[0].Action.Title); 61 | Assert.Equal("Return", plan.Steps[1].Action.Title); 62 | Assert.Equal("Move cabbage left", plan.Steps[2].Action.Title); 63 | Assert.Equal("Move goat right", plan.Steps[3].Action.Title); 64 | Assert.Equal("Move wolf left", plan.Steps[4].Action.Title); 65 | Assert.Equal("Return", plan.Steps[5].Action.Title); 66 | Assert.Equal("Move goat left", plan.Steps[6].Action.Title); 67 | } 68 | 69 | [Fact] 70 | public void TestDrumStacker() 71 | { 72 | var (data, subject) = DrumStackerPlannerFactory.Create(); 73 | 74 | var start = DateTime.Now; 75 | var plan = subject.Execute(data); 76 | var duration = DateTime.Now - start; 77 | 78 | string Render(Drum drum) => $"[{drum.Color.ToString()[0]}{drum.Size.ToString()[0]}]"; 79 | 80 | testOutputHelper.WriteLine($"Plan complete after {duration.TotalMilliseconds}ms:"); 81 | for (var i = 0; i < plan.Steps.Count; i++) 82 | { 83 | var step = plan.Steps[i]; 84 | testOutputHelper.WriteLine($"\t{i}: {step.Action.Title}"); 85 | 86 | var line1 = step.AfterState.Stacks.Select(stack => stack.Drums.Count > 0 ? Render(stack.Drums[0]) : "[ ]"); 87 | var line2 = step.AfterState.Stacks.Select(stack => stack.Drums.Count > 1 ? Render(stack.Drums[1]) : "[ ]"); 88 | var line3 = step.AfterState.Stacks.Select(stack => stack.Drums.Count > 2 ? Render(stack.Drums[2]) : "[ ]"); 89 | var line4 = step.AfterState.Stacks.Select(stack => stack.Drums.Count > 3 ? Render(stack.Drums[3]) : "[ ]"); 90 | 91 | testOutputHelper.WriteLine("BLU-YEL-GRE-RED-"); 92 | testOutputHelper.WriteLine(string.Join("", line4)); 93 | testOutputHelper.WriteLine(string.Join("", line3)); 94 | testOutputHelper.WriteLine(string.Join("", line2)); 95 | testOutputHelper.WriteLine(string.Join("", line1)); 96 | } 97 | 98 | Assert.True(plan.Success); 99 | } 100 | 101 | [Fact] 102 | public void TestKeyValuePlannerFailsWhenNoActions() 103 | { 104 | var subject = new KeyValuePlanner(); 105 | 106 | var plan = subject.Execute(new PlanParameters> 107 | { 108 | GetActions = _ => Array.Empty>>(), 109 | GoalEvaluator = g => false, 110 | HeuristicCost = g => 0, 111 | StartingState = new KeyValueState() 112 | }); 113 | 114 | Assert.False(plan.Success); 115 | } 116 | 117 | [Fact] 118 | public void TestKeyValuePlannerThrowsWhenArgsNull() 119 | { 120 | var subject = new KeyValuePlanner(); 121 | Assert.Throws(() => 122 | { 123 | subject.Execute(new PlanParameters>()); 124 | }); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /SimpleGOAP.Tests/SimpleGOAP.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | 6 | false 7 | 8 | SimplGOAP.Tests 9 | 10 | SimpleGOAP.Tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SimpleGOAP.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleGOAP.Core", "SimpleGOAP.Core\SimpleGOAP.Core.csproj", "{9471CC3B-7401-40C0-9A14-646F75152C02}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleGOAP.Tests", "SimpleGOAP.Tests\SimpleGOAP.Tests.csproj", "{9E80DA48-35E1-4454-A10D-725458CA83A5}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleGOAP.KeyValueState", "SimpleGOAP.KeyValueState\SimpleGOAP.KeyValueState.csproj", "{5B5EA671-02DE-493E-B518-E4CC09ECBDB2}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {9471CC3B-7401-40C0-9A14-646F75152C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {9471CC3B-7401-40C0-9A14-646F75152C02}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {9471CC3B-7401-40C0-9A14-646F75152C02}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {9471CC3B-7401-40C0-9A14-646F75152C02}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {9E80DA48-35E1-4454-A10D-725458CA83A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {9E80DA48-35E1-4454-A10D-725458CA83A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {9E80DA48-35E1-4454-A10D-725458CA83A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {9E80DA48-35E1-4454-A10D-725458CA83A5}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {5B5EA671-02DE-493E-B518-E4CC09ECBDB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {5B5EA671-02DE-493E-B518-E4CC09ECBDB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {5B5EA671-02DE-493E-B518-E4CC09ECBDB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {5B5EA671-02DE-493E-B518-E4CC09ECBDB2}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /SimpleGOAP.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | /Users/tomkerr/.dotnet/sdk/6.0.101/MSBuild.dll 3 | 1114112 4 | <SessionState ContinuousTestingMode="0" Name="TestKeyValuePlanner" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 5 | <TestAncestor> 6 | <TestId>xUnit::9E80DA48-35E1-4454-A10D-725458CA83A5::.NETFramework,Version=v4.7.2::SimpleGOAP.Tests.PlannerTests</TestId> 7 | <TestId>xUnit::9E80DA48-35E1-4454-A10D-725458CA83A5::.NETFramework,Version=v4.7.2::SimpleGOAP.Tests.PerformanceTests</TestId> 8 | </TestAncestor> 9 | </SessionState> 10 | <SessionState ContinuousTestingMode="0" Name="TestWolfExample" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 11 | <Project Location="/Users/tomkerr/projects/SimpleGOAP/SimpleGOAP.Tests" Presentation="&lt;SimpleGOAP.Tests&gt;" /> 12 | </SessionState> 13 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="PlannerTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 14 | <TestAncestor> 15 | <TestId>xUnit::9E80DA48-35E1-4454-A10D-725458CA83A5::.NETFramework,Version=v4.7.2::SimpleGOAP.Tests.PlannerTests</TestId> 16 | </TestAncestor> 17 | </SessionState> 18 | 19 | 20 | True -------------------------------------------------------------------------------- /push-core.ps1: -------------------------------------------------------------------------------- 1 | $version=$args[0] 2 | $api_key = Get-Content -Path ./.api_key 3 | $api_url = 'https://api.nuget.org/v3/index.json' 4 | 5 | nuget push -ApiKey $api_key -Source $api_url ./SimpleGOAP.Core/bin/Release/SimpleGOAP.Core.$version.nupkg 6 | -------------------------------------------------------------------------------- /push-core.sh: -------------------------------------------------------------------------------- 1 | if [ -z "$1" ]; then 2 | echo "Error: no version number supplied" 3 | exit 1 4 | fi 5 | 6 | version=$1 7 | package_path="./SimpleGOAP.Core/bin/Release/SimpleGOAP.Core.$version.nupkg" 8 | if [ ! -f $package_path ]; then 9 | echo "Error: no .nupkg file for that version exists" 10 | exit 1 11 | fi 12 | 13 | if [ ! -f .api_key ]; then 14 | echo "Error: no .api_key file exists" 15 | exit 1 16 | fi 17 | 18 | api_key=`cat .api_key` 19 | api_url='https://api.nuget.org/v3/index.json' 20 | 21 | nuget push -ApiKey $api_key -Source $api_url $package_path 22 | -------------------------------------------------------------------------------- /push-kvs.ps1: -------------------------------------------------------------------------------- 1 | $version=$args[0] 2 | $api_key = Get-Content -Path ./.api_key 3 | $api_url = 'https://api.nuget.org/v3/index.json' 4 | 5 | nuget push -ApiKey $api_key -Source $api_url ./SimpleGOAP.KeyValueState/bin/Release/SimpleGOAP.KeyValueState.$version.nupkg 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # What is SimpleGOAP? 2 | 3 | SimpleGOAP is a C# implementation of goal oriented action planning. There are some great resources on the topic [for your reading here](https://alumni.media.mit.edu/~jorkin/goap.html). The objectives of this repository are twofold: 4 | 5 | 1. Provide a simple implementation for anyone to use across a variety of platforms like Unity, Godot, ASP.net, etc. 6 | 2. Serve as a reference implementation for GOAP. 7 | 8 | # Installation 9 | 10 | SimpleGOAP is available on nuget.org through the package ID `SimpleGOAP.Core`. If you want to utilize the `KeyValueState` classes shown in examples below, you'll also need to install the `SimpleGOAP.KeyValueState` package: 11 | 12 | 1. [SimpleGOAP.Core on nuget.org](https://www.nuget.org/packages/SimpleGOAP.Core/) 13 | 1. [SimpleGOAP.KeyValueState on nuget.org](https://www.nuget.org/packages/SimpleGOAP.KeyValueState/) 14 | 15 | # Usage 16 | 17 | There are 4 steps to using the GOAP planner: 18 | 19 | 1. **Establishing state**: Define a "state" class that represents the parameters of your world state. Create an object of this type that represents the current world state. 20 | 2. **Defining actions**: Create a function which returns a list of actions that can be taken for a given state. 21 | 3. **Setting a goal**: Write a function that evaluates whether any permutation of that state adequately satisfies your end goal. 22 | 4. **Running the planner**: Pass all of the above into the planner to get a list of actions that can be taken to get from the current world state to a state that meets the goal. 23 | 24 | ## Example: Baking potatoes 25 | 26 | We are a farmer. Our goal is to harvest and cook 5 baked potatoes. Here are the actions we can take: 27 | 28 | 1. Harvest potato (+1 raw potato) 29 | 2. Chop wood (+1 wood) 30 | 3. Make fire (-3 wood, fire = true) 31 | 4. Cook potato (-1 raw potato, +1 baked potato) 32 | 33 | ### Step 1: Defining state 34 | 35 | Let's start by creating our state class: 36 | 37 | ```c# 38 | public class PotatoState 39 | { 40 | public int RawPotatoes = 0; 41 | public int Wood = 0; 42 | public bool Fire = false; 43 | public int BakedPotatoes = 0; 44 | } 45 | ``` 46 | 47 | In order for the algorithm to function, it needs to be able to copy a state as well as compare two states to see if they are the same. Define two classes for each of theses purposes. 48 | 49 | *Copying states is required because the planner must apply actions to a state in order to create possible futures from which a solution can be found. We don't want to modify the object if it is a reference type, and therefore we must generate a copy each time.* 50 | 51 | *Equality checks are required because there could be more than one way to reach any given state. If actions arrive at another state which already has a shorter path, that branch will be discarded.* 52 | 53 | 54 | ```c# 55 | 56 | public class PotatoStateCopier : IStateCopier 57 | { 58 | public PotatoState Copy(PotatoState state) 59 | { 60 | return new PotatoState 61 | { 62 | Potatoes = state.RawPotatoes, 63 | Wood = state.Wood, 64 | Fire = state.Fire, 65 | BakedPotatoes = state.BakedPotatoes 66 | }; 67 | } 68 | } 69 | 70 | public class PotatoStateEqualityComparer : IEqualityComparer 71 | { 72 | public bool Equals(PotatoState x, PotatoState y) 73 | { 74 | if (ReferenceEquals(x, y)) return true; 75 | if (ReferenceEquals(x, null)) return false; 76 | if (ReferenceEquals(y, null)) return false; 77 | if (x.GetType() != y.GetType()) return false; 78 | return x.RawPotatoes == y.RawPotatoes && x.Wood == y.Wood && x.Fire == y.Fire && x.BakedPotatoes == y.BakedPotatoes; 79 | } 80 | 81 | public int GetHashCode(PotatoState obj) 82 | { 83 | return new {obj.RawPotatoes, obj.Wood, obj.Fire, obj.BakedPotatoes}.GetHashCode(); 84 | } 85 | } 86 | ``` 87 | 88 | ### Step 2: Defining actions 89 | 90 | As outlined above, there are 4 actions the user can take: harvest potatoes, chop wood, make fire, and cook potatoes. There are a few properties that define an action: 91 | 92 | 1. A name. 93 | 2. An action cost. The algorithm prioritizes paths with lower costs. The cost can be dynamic depending on the state. 94 | 3. An "effect": a function that takes in a state object and returns a modified object. This represents the impact of taking that action. 95 | 96 | In code, our actions must implement `IAction`. You can choose to implement this interface with your own classes, but for simplicity there is an existing implementation -- `LambdaAction` -- which we can use for now. It takes all of the 3 parameters from above in its constructor. 97 | 98 | For now, we'll set all actions costs to 1: 99 | 100 | ```c# 101 | var harvestPotato = new LambdaAction( 102 | "Harvest potato", 1, state => state.RawPotatoes++); 103 | 104 | var chopWood = new LambdaAction( 105 | "Chop wood", 1, state => state.Wood++); 106 | 107 | var makeFire = new LambdaAction( 108 | "Make fire", 1, state => 109 | { 110 | state.Fire = true; 111 | state.Wood -= 3; 112 | }); 113 | 114 | var cookPotato = new LambdaAction( 115 | "Cook", 1, state => 116 | { 117 | state.RawPotatoes--; 118 | state.BakedPotatoes++; 119 | }); 120 | ``` 121 | 122 | Now that we have actions defined, let's create our function that takes in a `PotatoState` and returns a list of eligible actions: 123 | 124 | ```c# 125 | IEnumerable> GetActions(PotatoState state) 126 | { 127 | yield return harvestPotato; 128 | yield return chopWood; 129 | 130 | if (state.Wood >= 3) 131 | yield return makeFire; 132 | 133 | if (state.Fire && state.RawPotatoes > 0) 134 | yield return cookPotato; 135 | } 136 | ``` 137 | 138 | ### Step 3: Setting the goal 139 | Now, we define a function that will tell the engine whether we have reached our goal. In the case of our potato example, we simply want more that 5 baked potatoes: 140 | 141 | ```c# 142 | Func goalEvaluator = (state) => state.BakedPotatoes >= 5; 143 | ``` 144 | We also must define a heuristic function that will tell the engine how close to our goal we are for any given state. The planner will consider lower values to be closer to the goal. In this case, let's use the distance from 5 (our goal) as a heuristic: 145 | 146 | ```c# 147 | Func heuristicCost = state => 5 - state.BakedPotatoes; 148 | ``` 149 | 150 | *Note: the function above is technically optional; you could always return 0 and the search will still work. However, it's purpose is to suggest possible future paths and therefore can have a drastic effect on performance.* 151 | 152 | 153 | ### Step 4: Running the planner 154 | 155 | Finally, instantiate the planner and execute the plan: 156 | 157 | ```c# 158 | var planner = new Planner( 159 | new PotatoStateCopier(), 160 | new PotatoStateEqualityComparer() 161 | ); 162 | 163 | var plan = planner.Execute(new PlanParameters 164 | { 165 | StartingState = new PotatoState(), 166 | GetActions = GetActions, 167 | HeuristicCost = heuristicCost, 168 | GoalEvaluator = goalEvaluator 169 | }); 170 | 171 | foreach (var step in plan.Steps) 172 | Console.WriteLine(step.Action.Title); 173 | ``` 174 | 175 | The output: 176 | 177 | ``` 178 | Chop wood 179 | Chop wood 180 | Chop wood 181 | Make fire 182 | Harvest potato 183 | Cook 184 | Harvest potato 185 | Cook 186 | Harvest potato 187 | Cook 188 | Harvest potato 189 | Cook 190 | Harvest potato 191 | Cook 192 | ``` 193 | 194 | #### Review 195 | 196 | Our final code looks like so: 197 | 198 | ```c# 199 | public class PotatoState 200 | { 201 | public int RawPotatoes = 0; 202 | public int Wood = 0; 203 | public bool Fire = false; 204 | public int BakedPotatoes = 0; 205 | } 206 | 207 | public class PotatoStateCopier : IStateCopier 208 | { 209 | public PotatoState Copy(PotatoState state) 210 | { 211 | return new PotatoState 212 | { 213 | Potatoes = state.RawPotatoes, 214 | Wood = state.Wood, 215 | Fire = state.Fire, 216 | BakedPotatoes = state.BakedPotatoes 217 | }; 218 | } 219 | } 220 | 221 | public class PotatoStateEqualityComparer : IEqualityComparer 222 | { 223 | public bool Equals(PotatoState x, PotatoState y) 224 | { 225 | if (ReferenceEquals(x, y)) return true; 226 | if (ReferenceEquals(x, null)) return false; 227 | if (ReferenceEquals(y, null)) return false; 228 | if (x.GetType() != y.GetType()) return false; 229 | return x.RawPotatoes == y.RawPotatoes && x.Wood == y.Wood && x.Fire == y.Fire && x.BakedPotatoes == y.BakedPotatoes; 230 | } 231 | 232 | public int GetHashCode(PotatoState obj) 233 | { 234 | return new {obj.RawPotatoes, obj.Wood, obj.Fire, obj.BakedPotatoes}.GetHashCode(); 235 | } 236 | } 237 | 238 | public static class PotatoExample { 239 | 240 | public static void Main() { 241 | var initialState = new PotatoState(); 242 | 243 | var harvestPotato = new LambdaAction( 244 | "Harvest potato", 1, state => state.RawPotatoes++); 245 | 246 | var chopWood = new LambdaAction( 247 | "Chop wood", 1, state => state.Wood++); 248 | 249 | var makeFire = new LambdaAction( 250 | "Make fire", 1, state => 251 | { 252 | state.Fire = true; 253 | state.Wood -= 3; 254 | }); 255 | 256 | var cookPotato = new LambdaAction( 257 | "Cook", 1, state => 258 | { 259 | state.RawPotatoes--; 260 | state.BakedPotatoes++; 261 | }); 262 | 263 | IEnumerable> GetActions(PotatoState state) 264 | { 265 | yield return harvestPotato; 266 | yield return chopWood; 267 | 268 | if (state.Wood >= 3) 269 | yield return makeFire; 270 | 271 | if (state.Fire && state.RawPotatoes > 0) 272 | yield return cookPotato; 273 | } 274 | 275 | var planner = new Planner( 276 | new PotatoStateCopier(), 277 | new PotatoStateEqualityComparer() 278 | ); 279 | 280 | var plan = planner.Execute(new PlanParameters 281 | { 282 | StartingState = initialState, 283 | GetActions = GetActions, 284 | HeuristicCost = heuristicCost, 285 | GoalEvaluator = goalEvaluator 286 | }); 287 | 288 | foreach (var step in plan.Steps) 289 | Console.WriteLine(step.Action.Title); 290 | } 291 | } 292 | ``` 293 | 294 | 295 | 296 | # Some comments on implementation 297 | 298 | Many implementations of GOAP prefer an expression of actions and state that can be driven from a configuration file. These are more or less static and limit your options. However the approach SimpleGOAP takes is code-first. For example, precondition checks have been eliminated in favor of a user-defined function that takes a state object and returns all possible actions. This allows for more dynamic action lists that morph as state changes. 299 | 300 | Note that you could build a more static system on top of SimpleGOAP. In the case of defining actions externally, you could simply have an implementation of `IAction` which returns actions from your master list which pass a precondition check also defined in the config file. 301 | --------------------------------------------------------------------------------