diff --git a/Examples/ConsumerDemo.cs b/Examples/ConsumerDemo.cs new file mode 100644 index 0000000..1493945 --- /dev/null +++ b/Examples/ConsumerDemo.cs @@ -0,0 +1,145 @@ +// +// Copyright (c) Chris Muller. All rights reserved. +// + +namespace Examples { + using MountainGoap; + using MountainGoapLogging; + + /// + /// Goal to create enough food to eat by working and grocery shopping. + /// + internal static class ConsumerDemo { + /// + /// Runs the demo. + /// + internal static void Run() { + _ = new DefaultLogger(); + var locations = new List { "home", "work", "store" }; + var agent = new Agent( + name: "Consumer Agent", + state: new() { + { "food", 4 }, + { "energy", 100 }, + { "money", 0 }, + { "inCar", false }, + { "location", "home" } + }, + goals: new() { + //new ComparativeGoal( + // name: "Get at least 5 food", + // desiredState: new() { + // { + // "food", new() { + // Operator = ComparisonOperator.GreaterThanOrEquals, + // Value = 5 + // } + // } + // }) + //new Goal( + // name: "Get 5 food", + // desiredState: new() { + // { "food", 5 } + // }) + new ExtremeGoal( + name: "Get food", + desiredState: new() { + { "food", true } + }) + }, + actions: new() { + new( + name: "Walk", + cost: 10, + executor: GenericExecutor, + permutationSelectors: new() { + { "location", PermutationSelectorGenerators.SelectFromCollection(locations) } + }, + comparativePreconditions: new() { + { "energy", new() { Operator = ComparisonOperator.GreaterThan, Value = 0 } } + }, + arithmeticPostconditions: new() { + { "energy", -1 } + }, + parameterPostconditions: new() { + { "location", "location" } + } + ), + new( + name: "Drive", + cost: 1, + preconditions: new() { + { "inCar", true } + }, + comparativePreconditions: new() { + { "energy", new() { Operator = ComparisonOperator.GreaterThan, Value = 0 } } + }, + executor: GenericExecutor, + permutationSelectors: new() { + { "location", PermutationSelectorGenerators.SelectFromCollection(locations) } + }, + arithmeticPostconditions: new() { + { "energy", -1 } + }, + parameterPostconditions: new() { + { "location", "location" } + } + ), + new( + name: "Get in Car", + cost: 1f, + preconditions: new() { + { "inCar", false } + }, + comparativePreconditions: new() { + { "energy", new() { Operator = ComparisonOperator.GreaterThan, Value = 0 } } + }, + postconditions: new() { + { "inCar", true } + }, + arithmeticPostconditions: new() { + { "energy", -1 } + }, + executor: GenericExecutor + ), + new( + name: "Work", + cost: 1f, + preconditions: new() { + { "location", "work" }, + }, + comparativePreconditions: new() { + { "energy", new() { Operator = ComparisonOperator.GreaterThan, Value = 0 } } + }, + arithmeticPostconditions: new() { + { "energy", -1 }, + { "money", 1 } + }, + executor: GenericExecutor + ), + new( + name: "Shop", + cost: 1f, + preconditions: new() { + { "location", "store" } + }, + comparativePreconditions: new() { + { "energy", new() { Operator = ComparisonOperator.GreaterThan, Value = 0 } }, + { "money", new() { Operator = ComparisonOperator.GreaterThan, Value = 0 } } + }, + arithmeticPostconditions: new() { + { "energy", -1 }, + { "money", -1 }, + { "food", 1 } + }, + executor: GenericExecutor + ) + }); + while (agent.State["food"] is int food && food < 5) agent.Step(); + } + + private static ExecutionStatus GenericExecutor(Agent agent, Action action) { + return ExecutionStatus.Succeeded; + } + } +} diff --git a/Examples/Program.cs b/Examples/Program.cs index 587b263..f929323 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -39,13 +39,18 @@ public static async Task Main(string[] args) { carCommand.SetHandler(() => { RunCarDemo(); }); + var consumerCommand = new Command("consumer", "Run the consumer demo."); + consumerCommand.SetHandler(() => { + RunConsumerDemo(); + }); var cmd = new RootCommand { happinessIncrementerCommand, rpgCommand, arithmeticHappinessIncrementerCommand, extremeHappinessIncrementerCommand, comparativeHappinessIncrementerCommand, - carCommand + carCommand, + consumerCommand }; return await cmd.InvokeAsync(args); } @@ -73,5 +78,9 @@ private static void RunComparativeHappinessIncrementer() { private static void RunCarDemo() { CarDemo.Run(); } + + private static void RunConsumerDemo() { + ConsumerDemo.Run(); + } } } diff --git a/Examples/RpgExample/RpgUtils.cs b/Examples/RpgExample/RpgUtils.cs index e013ef4..785e4eb 100644 --- a/Examples/RpgExample/RpgUtils.cs +++ b/Examples/RpgExample/RpgUtils.cs @@ -91,22 +91,28 @@ internal static List StartingPositionPermutations(Dictionary /// Action for which cost is being calculated. + /// State as it will be when cost is relevant. /// The cost of the action. - internal static float GoToEnemyCost(Action action) { +#pragma warning disable IDE0060 // Remove unused parameter + internal static float GoToEnemyCost(Action action, Dictionary state) { if (action.GetParameter("startingPosition") is not Vector2 startingPosition || action.GetParameter("target") is not Agent target) return float.MaxValue; if (target.State["position"] is not Vector2 targetPosition) return float.MaxValue; return Distance(startingPosition, targetPosition); } +#pragma warning restore IDE0060 // Remove unused parameter /// /// Gets the cost of moving to food. /// /// Action for which the cost is being calculated. + /// /// State as it will be when cost is relevant. /// The cost of the action. - internal static float GoToFoodCost(Action action) { +#pragma warning disable IDE0060 // Remove unused parameter + internal static float GoToFoodCost(Action action, Dictionary state) { if (action.GetParameter("startingPosition") is not Vector2 startingPosition || action.GetParameter("target") is not Vector2 targetPosition) return float.MaxValue; return Distance(startingPosition, targetPosition); } +#pragma warning restore IDE0060 // Remove unused parameter private static float Distance(Vector2 pos1, Vector2 pos2) { return (float)Math.Sqrt(Math.Pow(Math.Abs(pos2.X - pos1.X), 2) + Math.Pow(Math.Abs(pos2.Y - pos1.Y), 2)); diff --git a/MountainGoap/Action.cs b/MountainGoap/Action.cs index 3453d36..0f5072d 100644 --- a/MountainGoap/Action.cs +++ b/MountainGoap/Action.cs @@ -41,6 +41,11 @@ public class Action { /// private readonly Dictionary preconditions = new(); + /// + /// Comnparative preconditions for the action. Indicates that a value must be greater than or less than a certain value for the action to execute. + /// + private readonly Dictionary comparativePreconditions = new(); + /// /// Postconditions for the action. These will be set when the action has executed. /// @@ -51,6 +56,11 @@ public class Action { /// private readonly Dictionary arithmeticPostconditions = new(); + /// + /// Parameter postconditions for the action. When the action has executed, the value of the parameter given in the key will be copied to the state with the name given in the value. + /// + private readonly Dictionary parameterPostconditions = new(); + /// /// Parameters to be passed to the action. /// @@ -65,9 +75,11 @@ public class Action { /// Cost of the action. /// Callback for determining the cost of the action. /// Preconditions required in the world state in order for the action to occur. + /// Preconditions indicating relative value requirements needed for the action to occur. /// Postconditions applied after the action is successfully executed. /// Arithmetic postconditions added to state after the action is successfully executed. - public Action(string? name = null, Dictionary? permutationSelectors = null, ExecutorCallback? executor = null, float cost = 1f, CostCallback? costCallback = null, Dictionary? preconditions = null, Dictionary? postconditions = null, Dictionary? arithmeticPostconditions = null) { + /// Parameter postconditions copied to state after the action is successfully executed. + public Action(string? name = null, Dictionary? permutationSelectors = null, ExecutorCallback? executor = null, float cost = 1f, CostCallback? costCallback = null, Dictionary? preconditions = null, Dictionary? comparativePreconditions = null, Dictionary? postconditions = null, Dictionary? arithmeticPostconditions = null, Dictionary? parameterPostconditions = null) { if (permutationSelectors == null) this.permutationSelectors = new(); else this.permutationSelectors = permutationSelectors; if (executor == null) this.executor = DefaultExecutorCallback; @@ -76,8 +88,10 @@ public Action(string? name = null, Dictionary @@ -100,7 +114,10 @@ public Action(string? name = null, Dictionary /// A copy of the action. public Action Copy() { - return new Action(Name, permutationSelectors, executor, cost, costCallback, preconditions.Copy(), postconditions.Copy(), arithmeticPostconditions.CopyNonNullable()); + var newAction = new Action(Name, permutationSelectors, executor, cost, costCallback, preconditions.Copy(), comparativePreconditions.Copy(), postconditions.Copy(), arithmeticPostconditions.CopyNonNullable(), parameterPostconditions.Copy()) { + parameters = parameters.Copy() + }; + return newAction; } /// @@ -125,9 +142,10 @@ public void SetParameter(string key, object value) { /// /// Gets the cost of the action. /// + /// State as it will be when cost is relevant. /// The cost of the action. - public float GetCost() { - return costCallback(this); + public float GetCost(Dictionary currentState) { + return costCallback(this, currentState); } /// @@ -162,6 +180,17 @@ internal bool IsPossible(Dictionary state) { else if (state[kvp.Key] == null && state[kvp.Key] == kvp.Value) continue; if (state[kvp.Key] is object obj && !obj.Equals(kvp.Value)) return false; } + foreach (var kvp in comparativePreconditions) { + if (!state.ContainsKey(kvp.Key)) return false; + if (state[kvp.Key] == null) return false; + if (state[kvp.Key] is object obj && kvp.Value.Value is object obj2) { + if (kvp.Value.Operator == ComparisonOperator.LessThan && !Utils.IsLowerThan(obj, obj2)) return false; + else if (kvp.Value.Operator == ComparisonOperator.GreaterThan && !Utils.IsHigherThan(obj, obj2)) return false; + else if (kvp.Value.Operator == ComparisonOperator.LessThanOrEquals && !Utils.IsLowerThanOrEquals(obj, obj2)) return false; + else if (kvp.Value.Operator == ComparisonOperator.GreaterThanOrEquals && !Utils.IsHigherThanOrEquals(obj, obj2)) return false; + } + else return false; + } return true; } @@ -209,6 +238,10 @@ internal void ApplyEffects(Dictionary state) { else if (state[kvp.Key] is decimal stateDecimal && kvp.Value is decimal conditionDecimal) state[kvp.Key] = stateDecimal + conditionDecimal; else if (state[kvp.Key] is DateTime stateDateTime && kvp.Value is TimeSpan conditionTimeSpan) state[kvp.Key] = stateDateTime + conditionTimeSpan; } + foreach (var kvp in parameterPostconditions) { + if (!parameters.ContainsKey(kvp.Key)) continue; + state[kvp.Value] = parameters[kvp.Key]; + } } /// @@ -245,8 +278,10 @@ private static ExecutionStatus DefaultExecutorCallback(Agent agent, Action actio return ExecutionStatus.Failed; } - private static float DefaultCostCallback(Action action) { +#pragma warning disable S1172 // Unused method parameters should be removed + private static float DefaultCostCallback(Action action, Dictionary currentState) { return action.cost; } +#pragma warning restore S1172 // Unused method parameters should be removed } } \ No newline at end of file diff --git a/MountainGoap/CallbackDelegates/CostCallback.cs b/MountainGoap/CallbackDelegates/CostCallback.cs index aadc2cf..04f39e7 100644 --- a/MountainGoap/CallbackDelegates/CostCallback.cs +++ b/MountainGoap/CallbackDelegates/CostCallback.cs @@ -7,6 +7,7 @@ namespace MountainGoap { /// Delegate type for a callback that defines the cost of an action. /// /// Action being executed. + /// State as it will be when cost is relevant. /// Cost of the action. - public delegate float CostCallback(Action action); + public delegate float CostCallback(Action action, Dictionary currentState); } diff --git a/MountainGoap/Internals/ActionAStar.cs b/MountainGoap/Internals/ActionAStar.cs index d87056d..411203d 100644 --- a/MountainGoap/Internals/ActionAStar.cs +++ b/MountainGoap/Internals/ActionAStar.cs @@ -50,7 +50,7 @@ internal ActionAStar(ActionGraph graph, ActionNode start, BaseGoal goal) { break; } foreach (var next in graph.Neighbors(current)) { - float newCost = CostSoFar[current] + next.Cost(); + float newCost = CostSoFar[current] + next.Cost(current.State); if (!CostSoFar.ContainsKey(next) || newCost < CostSoFar[next]) { CostSoFar[next] = newCost; float priority = newCost + Heuristic(next, goal, current); @@ -98,47 +98,19 @@ private static float Heuristic(ActionNode actionNode, BaseGoal goal, ActionNode } private static bool IsLowerThan(object a, object b) { - if (a == null || b == null) return false; - if (a is int intA && b is int intB) return intA < intB; - if (a is long longA && b is long longB) return longA < longB; - if (a is float floatA && b is float floatB) return floatA < floatB; - if (a is double doubleA && b is double doubleB) return doubleA < doubleB; - if (a is decimal decimalA && b is decimal decimalB) return decimalA < decimalB; - if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA < dateTimeB; - return false; + return Utils.IsLowerThan(a, b); } private static bool IsHigherThan(object a, object b) { - if (a == null || b == null) return false; - if (a is int intA && b is int intB) return intA > intB; - if (a is long longA && b is long longB) return longA > longB; - if (a is float floatA && b is float floatB) return floatA > floatB; - if (a is double doubleA && b is double doubleB) return doubleA > doubleB; - if (a is decimal decimalA && b is decimal decimalB) return decimalA > decimalB; - if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA > dateTimeB; - return false; + return Utils.IsHigherThan(a, b); } private static bool IsLowerThanOrEquals(object a, object b) { - if (a == null || b == null) return false; - if (a is int intA && b is int intB) return intA <= intB; - if (a is long longA && b is long longB) return longA <= longB; - if (a is float floatA && b is float floatB) return floatA <= floatB; - if (a is double doubleA && b is double doubleB) return doubleA <= doubleB; - if (a is decimal decimalA && b is decimal decimalB) return decimalA <= decimalB; - if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA <= dateTimeB; - return false; + return Utils.IsLowerThanOrEquals(a, b); } private static bool IsHigherThanOrEquals(object a, object b) { - if (a == null || b == null) return false; - if (a is int intA && b is int intB) return intA >= intB; - if (a is long longA && b is long longB) return longA >= longB; - if (a is float floatA && b is float floatB) return floatA >= floatB; - if (a is double doubleA && b is double doubleB) return doubleA >= doubleB; - if (a is decimal decimalA && b is decimal decimalB) return decimalA >= decimalB; - if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA >= dateTimeB; - return false; + return Utils.IsHigherThanOrEquals(a, b); } private bool MeetsGoal(ActionNode actionNode, ActionNode current) { diff --git a/MountainGoap/Internals/ActionNode.cs b/MountainGoap/Internals/ActionNode.cs index 9fd570d..f2a8781 100644 --- a/MountainGoap/Internals/ActionNode.cs +++ b/MountainGoap/Internals/ActionNode.cs @@ -74,10 +74,11 @@ public override int GetHashCode() { /// /// Cost to traverse this node. /// + /// Current state after previous node is executed. /// The cost of the action to be executed. - internal float Cost() { + internal float Cost(Dictionary currentState) { if (Action == null) return float.MaxValue; - return Action.GetCost(); + return Action.GetCost(currentState); } private bool StateMatches(ActionNode otherNode) { diff --git a/MountainGoap/Internals/CopyDictionaryExtensionMethod.cs b/MountainGoap/Internals/CopyDictionaryExtensionMethod.cs index 5f8b9cd..1396a08 100644 --- a/MountainGoap/Internals/CopyDictionaryExtensionMethod.cs +++ b/MountainGoap/Internals/CopyDictionaryExtensionMethod.cs @@ -19,6 +19,24 @@ internal static class CopyDictionaryExtensionMethod { return dictionary.ToDictionary(entry => entry.Key, entry => entry.Value); } + /// + /// Copies the dictionary to a shallow clone. + /// + /// Dictionary to be copied. + /// A shallow copy of the dictionary. + internal static Dictionary Copy(this Dictionary dictionary) { + return dictionary.ToDictionary(entry => entry.Key, entry => entry.Value); + } + + /// + /// Copies the dictionary to a shallow clone. + /// + /// Dictionary to be copied. + /// A shallow copy of the dictionary. + internal static Dictionary Copy(this Dictionary dictionary) { + return dictionary.ToDictionary(entry => entry.Key, entry => entry.Value); + } + /// /// Copies the dictionary to a shallow clone. /// diff --git a/MountainGoap/Internals/Utils.cs b/MountainGoap/Internals/Utils.cs new file mode 100644 index 0000000..85d3eb7 --- /dev/null +++ b/MountainGoap/Internals/Utils.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Chris Muller. All rights reserved. +// + +namespace MountainGoap { + /// + /// Utilities for the MountainGoap library. + /// + internal static class Utils { + /// + /// Indicates whether a is lower than b. + /// + /// First element to be compared. + /// Second element to be compared. + /// True if lower, false otherwise. + internal static bool IsLowerThan(object a, object b) { + if (a == null || b == null) return false; + if (a is int intA && b is int intB) return intA < intB; + if (a is long longA && b is long longB) return longA < longB; + if (a is float floatA && b is float floatB) return floatA < floatB; + if (a is double doubleA && b is double doubleB) return doubleA < doubleB; + if (a is decimal decimalA && b is decimal decimalB) return decimalA < decimalB; + if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA < dateTimeB; + return false; + } + + /// + /// Indicates whether a is higher than b. + /// + /// First element to be compared. + /// Second element to be compared. + /// True if higher, false otherwise. + internal static bool IsHigherThan(object a, object b) { + if (a == null || b == null) return false; + if (a is int intA && b is int intB) return intA > intB; + if (a is long longA && b is long longB) return longA > longB; + if (a is float floatA && b is float floatB) return floatA > floatB; + if (a is double doubleA && b is double doubleB) return doubleA > doubleB; + if (a is decimal decimalA && b is decimal decimalB) return decimalA > decimalB; + if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA > dateTimeB; + return false; + } + + /// + /// Indicates whether a is lower than or equal to b. + /// + /// First element to be compared. + /// Second element to be compared. + /// True if lower or equal, false otherwise. + internal static bool IsLowerThanOrEquals(object a, object b) { + if (a == null || b == null) return false; + if (a is int intA && b is int intB) return intA <= intB; + if (a is long longA && b is long longB) return longA <= longB; + if (a is float floatA && b is float floatB) return floatA <= floatB; + if (a is double doubleA && b is double doubleB) return doubleA <= doubleB; + if (a is decimal decimalA && b is decimal decimalB) return decimalA <= decimalB; + if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA <= dateTimeB; + return false; + } + + /// + /// Indicates whether a is higher than or equal to b. + /// + /// First element to be compared. + /// Second element to be compared. + /// True if higher or equal, false otherwise. + internal static bool IsHigherThanOrEquals(object a, object b) { + if (a == null || b == null) return false; + if (a is int intA && b is int intB) return intA >= intB; + if (a is long longA && b is long longB) return longA >= longB; + if (a is float floatA && b is float floatB) return floatA >= floatB; + if (a is double doubleA && b is double doubleB) return doubleA >= doubleB; + if (a is decimal decimalA && b is decimal decimalB) return decimalA >= decimalB; + if (a is DateTime dateTimeA && b is DateTime dateTimeB) return dateTimeA >= dateTimeB; + return false; + } + } +} \ No newline at end of file