Skip to content

Commit

Permalink
added ability to access node state during cost calculation, comparati…
Browse files Browse the repository at this point in the history
…ve preconditions, and the ability to set postconditions based on action parameters
  • Loading branch information
caesuric committed Sep 11, 2023
1 parent 1f03a51 commit 8cfefd1
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 44 deletions.
145 changes: 145 additions & 0 deletions Examples/ConsumerDemo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// <copyright file="ConsumerDemo.cs" company="Chris Muller">
// Copyright (c) Chris Muller. All rights reserved.
// </copyright>

namespace Examples {
using MountainGoap;
using MountainGoapLogging;

/// <summary>
/// Goal to create enough food to eat by working and grocery shopping.
/// </summary>
internal static class ConsumerDemo {
/// <summary>
/// Runs the demo.
/// </summary>
internal static void Run() {
_ = new DefaultLogger();
var locations = new List<string> { "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;
}
}
}
11 changes: 10 additions & 1 deletion Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@ public static async Task<int> 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);
}
Expand Down Expand Up @@ -73,5 +78,9 @@ private static void RunComparativeHappinessIncrementer() {
private static void RunCarDemo() {
CarDemo.Run();
}

private static void RunConsumerDemo() {
ConsumerDemo.Run();
}
}
}
10 changes: 8 additions & 2 deletions Examples/RpgExample/RpgUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,28 @@ internal static List<object> StartingPositionPermutations(Dictionary<string, obj
/// Gets the cost of moving to an enemy.
/// </summary>
/// <param name="action">Action for which cost is being calculated.</param>
/// <param name="state">State as it will be when cost is relevant.</param>
/// <returns>The cost of the action.</returns>
internal static float GoToEnemyCost(Action action) {
#pragma warning disable IDE0060 // Remove unused parameter
internal static float GoToEnemyCost(Action action, Dictionary<string, object?> 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

/// <summary>
/// Gets the cost of moving to food.
/// </summary>
/// <param name="action">Action for which the cost is being calculated.</param>
/// /// <param name="state">State as it will be when cost is relevant.</param>
/// <returns>The cost of the action.</returns>
internal static float GoToFoodCost(Action action) {
#pragma warning disable IDE0060 // Remove unused parameter
internal static float GoToFoodCost(Action action, Dictionary<string, object?> 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));
Expand Down
45 changes: 40 additions & 5 deletions MountainGoap/Action.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public class Action {
/// </summary>
private readonly Dictionary<string, object?> preconditions = new();

/// <summary>
/// Comnparative preconditions for the action. Indicates that a value must be greater than or less than a certain value for the action to execute.
/// </summary>
private readonly Dictionary<string, ComparisonValuePair> comparativePreconditions = new();

/// <summary>
/// Postconditions for the action. These will be set when the action has executed.
/// </summary>
Expand All @@ -51,6 +56,11 @@ public class Action {
/// </summary>
private readonly Dictionary<string, object> arithmeticPostconditions = new();

/// <summary>
/// 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.
/// </summary>
private readonly Dictionary<string, string> parameterPostconditions = new();

/// <summary>
/// Parameters to be passed to the action.
/// </summary>
Expand All @@ -65,9 +75,11 @@ public class Action {
/// <param name="cost">Cost of the action.</param>
/// <param name="costCallback">Callback for determining the cost of the action.</param>
/// <param name="preconditions">Preconditions required in the world state in order for the action to occur.</param>
/// <param name="comparativePreconditions">Preconditions indicating relative value requirements needed for the action to occur.</param>
/// <param name="postconditions">Postconditions applied after the action is successfully executed.</param>
/// <param name="arithmeticPostconditions">Arithmetic postconditions added to state after the action is successfully executed.</param>
public Action(string? name = null, Dictionary<string, PermutationSelectorCallback>? permutationSelectors = null, ExecutorCallback? executor = null, float cost = 1f, CostCallback? costCallback = null, Dictionary<string, object?>? preconditions = null, Dictionary<string, object?>? postconditions = null, Dictionary<string, object>? arithmeticPostconditions = null) {
/// <param name="parameterPostconditions">Parameter postconditions copied to state after the action is successfully executed.</param>
public Action(string? name = null, Dictionary<string, PermutationSelectorCallback>? permutationSelectors = null, ExecutorCallback? executor = null, float cost = 1f, CostCallback? costCallback = null, Dictionary<string, object?>? preconditions = null, Dictionary<string, ComparisonValuePair>? comparativePreconditions = null, Dictionary<string, object?>? postconditions = null, Dictionary<string, object>? arithmeticPostconditions = null, Dictionary<string, string>? parameterPostconditions = null) {
if (permutationSelectors == null) this.permutationSelectors = new();
else this.permutationSelectors = permutationSelectors;
if (executor == null) this.executor = DefaultExecutorCallback;
Expand All @@ -76,8 +88,10 @@ public Action(string? name = null, Dictionary<string, PermutationSelectorCallbac
this.cost = cost;
this.costCallback = costCallback ?? DefaultCostCallback;
if (preconditions != null) this.preconditions = preconditions;
if (comparativePreconditions != null) this.comparativePreconditions = comparativePreconditions;
if (postconditions != null) this.postconditions = postconditions;
if (arithmeticPostconditions != null) this.arithmeticPostconditions = arithmeticPostconditions;
if (parameterPostconditions != null) this.parameterPostconditions = parameterPostconditions;
}

/// <summary>
Expand All @@ -100,7 +114,10 @@ public Action(string? name = null, Dictionary<string, PermutationSelectorCallbac
/// </summary>
/// <returns>A copy of the action.</returns>
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;
}

/// <summary>
Expand All @@ -125,9 +142,10 @@ public void SetParameter(string key, object value) {
/// <summary>
/// Gets the cost of the action.
/// </summary>
/// <param name="currentState">State as it will be when cost is relevant.</param>
/// <returns>The cost of the action.</returns>
public float GetCost() {
return costCallback(this);
public float GetCost(Dictionary<string, object?> currentState) {
return costCallback(this, currentState);
}

/// <summary>
Expand Down Expand Up @@ -162,6 +180,17 @@ internal bool IsPossible(Dictionary<string, object?> 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;
}

Expand Down Expand Up @@ -209,6 +238,10 @@ internal void ApplyEffects(Dictionary<string, object?> 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];
}
}

/// <summary>
Expand Down Expand Up @@ -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<string, object?> currentState) {
return action.cost;
}
#pragma warning restore S1172 // Unused method parameters should be removed
}
}
3 changes: 2 additions & 1 deletion MountainGoap/CallbackDelegates/CostCallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace MountainGoap {
/// Delegate type for a callback that defines the cost of an action.
/// </summary>
/// <param name="action">Action being executed.</param>
/// <param name="currentState">State as it will be when cost is relevant.</param>
/// <returns>Cost of the action.</returns>
public delegate float CostCallback(Action action);
public delegate float CostCallback(Action action, Dictionary<string, object?> currentState);
}
Loading

0 comments on commit 8cfefd1

Please sign in to comment.