Skip to content

Commit

Permalink
Merge pull request #18 from ptrefall/stateless_planner_v2
Browse files Browse the repository at this point in the history
Stateless Planner
  • Loading branch information
ptrefall authored May 23, 2024
2 parents a8b3b23 + fb20fdb commit 607ba05
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 188 deletions.
3 changes: 2 additions & 1 deletion Fluid-HTN.UnitTests/MyContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum MyWorldState : byte
internal class MyContext : BaseContext
{
private byte[] _worldState = new byte[Enum.GetValues(typeof(MyWorldState)).Length];
public override IFactory Factory { get; set; } = new DefaultFactory();
public override IFactory Factory { get; protected set; } = new DefaultFactory();
public override IPlannerState PlannerState { get; protected set; } = new DefaultPlannerState();
public override List<string> MTRDebug { get; set; } = null;
public override List<string> LastMTRDebug { get; set; } = null;
public override bool DebugMTR { get; } = false;
Expand Down
84 changes: 31 additions & 53 deletions Fluid-HTN.UnitTests/PlannerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using FluidHTN;
using FluidHTN.Compounds;
using FluidHTN.Conditions;
Expand All @@ -13,25 +14,6 @@ namespace Fluid_HTN.UnitTests
[TestClass]
public class PlannerTests
{
[TestMethod]
public void GetPlanReturnsClearInstanceAtStart_ExpectedBehavior()
{
var planner = new Planner<MyContext>();
var plan = planner.GetPlan();

Assert.IsTrue(plan != null);
Assert.IsTrue(plan.Count == 0);
}

[TestMethod]
public void GetCurrentTaskReturnsNullAtStart_ExpectedBehavior()
{
var planner = new Planner<MyContext>();
var task = planner.GetCurrentTask();

Assert.IsTrue(task == null);
}

[TestMethod]
[ExpectedException(typeof(NullReferenceException), AllowDerivedTypes = false)]
public void TickWithNullParametersThrowsNRE_ExpectedBehavior()
Expand Down Expand Up @@ -82,10 +64,9 @@ public void TickWithPrimitiveTaskWithoutOperator_ExpectedBehavior()
domain.Add(task1, task2);

planner.Tick(domain, ctx);
var currentTask = planner.GetCurrentTask();

Assert.IsTrue(currentTask == null);
Assert.IsTrue(planner.LastStatus == TaskStatus.Failure);
Assert.IsTrue(ctx.PlannerState.CurrentTask == null);
Assert.IsTrue(ctx.PlannerState.LastStatus == TaskStatus.Failure);
}

[TestMethod]
Expand All @@ -102,10 +83,9 @@ public void TickWithFuncOperatorWithNullFunc_ExpectedBehavior()
domain.Add(task1, task2);

planner.Tick(domain, ctx);
var currentTask = planner.GetCurrentTask();

Assert.IsTrue(currentTask == null);
Assert.IsTrue(planner.LastStatus == TaskStatus.Failure);
Assert.IsTrue(ctx.PlannerState.CurrentTask == null);
Assert.IsTrue(ctx.PlannerState.LastStatus == TaskStatus.Failure);
}

[TestMethod]
Expand All @@ -122,10 +102,9 @@ public void TickWithDefaultSuccessOperatorWontStackOverflows_ExpectedBehavior()
domain.Add(task1, task2);

planner.Tick(domain, ctx);
var currentTask = planner.GetCurrentTask();

Assert.IsTrue(currentTask == null);
Assert.IsTrue(planner.LastStatus == TaskStatus.Success);
Assert.IsTrue(ctx.PlannerState.CurrentTask == null);
Assert.IsTrue(ctx.PlannerState.LastStatus == TaskStatus.Success);
}

[TestMethod]
Expand All @@ -142,10 +121,9 @@ public void TickWithDefaultContinueOperator_ExpectedBehavior()
domain.Add(task1, task2);

planner.Tick(domain, ctx);
var currentTask = planner.GetCurrentTask();

Assert.IsTrue(currentTask != null);
Assert.IsTrue(planner.LastStatus == TaskStatus.Continue);
Assert.IsTrue(ctx.PlannerState.CurrentTask != null);
Assert.IsTrue(ctx.PlannerState.LastStatus == TaskStatus.Continue);
}

[TestMethod]
Expand All @@ -155,7 +133,7 @@ public void OnNewPlan_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnNewPlan = (p) => { test = p.Count == 1; };
ctx.PlannerState.OnNewPlan = (p) => { test = p.Count == 1; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test" };
var task2 = new PrimitiveTask() { Name = "Sub-task" };
Expand All @@ -175,7 +153,7 @@ public void OnReplacePlan_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnReplacePlan = (op, ct, p) => { test = op.Count == 0 && ct != null && p.Count == 1; };
ctx.PlannerState.OnReplacePlan = (op, ct, p) => { test = op.Count == 0 && ct != null && p.Count == 1; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test1" };
var task2 = new Selector() { Name = "Test2" };
Expand Down Expand Up @@ -205,7 +183,7 @@ public void OnNewTask_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnNewTask = (t) => { test = t.Name == "Sub-task"; };
ctx.PlannerState.OnNewTask = (t) => { test = t.Name == "Sub-task"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test" };
var task2 = new PrimitiveTask() { Name = "Sub-task" };
Expand All @@ -225,7 +203,7 @@ public void OnNewTaskConditionFailed_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnNewTaskConditionFailed = (t, c) => { test = t.Name == "Sub-task1"; };
ctx.PlannerState.OnNewTaskConditionFailed = (t, c) => { test = t.Name == "Sub-task1"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test1" };
var task2 = new Selector() { Name = "Test2" };
Expand Down Expand Up @@ -260,7 +238,7 @@ public void OnStopCurrentTask_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnStopCurrentTask = (t) => { test = t.Name == "Sub-task2"; };
ctx.PlannerState.OnStopCurrentTask = (t) => { test = t.Name == "Sub-task2"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test1" };
var task2 = new Selector() { Name = "Test2" };
Expand Down Expand Up @@ -290,7 +268,7 @@ public void OnCurrentTaskCompletedSuccessfully_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnCurrentTaskCompletedSuccessfully = (t) => { test = t.Name == "Sub-task1"; };
ctx.PlannerState.OnCurrentTaskCompletedSuccessfully = (t) => { test = t.Name == "Sub-task1"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test1" };
var task2 = new Selector() { Name = "Test2" };
Expand Down Expand Up @@ -320,7 +298,7 @@ public void OnApplyEffect_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnApplyEffect = (e) => { test = e.Name == "TestEffect"; };
ctx.PlannerState.OnApplyEffect = (e) => { test = e.Name == "TestEffect"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test1" };
var task2 = new Selector() { Name = "Test2" };
Expand Down Expand Up @@ -352,7 +330,7 @@ public void OnCurrentTaskFailed_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnCurrentTaskFailed = (t) => { test = t.Name == "Sub-task"; };
ctx.PlannerState.OnCurrentTaskFailed = (t) => { test = t.Name == "Sub-task"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test" };
var task2 = new PrimitiveTask() { Name = "Sub-task" };
Expand All @@ -372,7 +350,7 @@ public void OnCurrentTaskContinues_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnCurrentTaskContinues = (t) => { test = t.Name == "Sub-task"; };
ctx.PlannerState.OnCurrentTaskContinues = (t) => { test = t.Name == "Sub-task"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test" };
var task2 = new PrimitiveTask() { Name = "Sub-task" };
Expand All @@ -392,7 +370,7 @@ public void OnCurrentTaskExecutingConditionFailed_ExpectedBehavior()
var ctx = new MyContext();
ctx.Init();
var planner = new Planner<MyContext>();
planner.OnCurrentTaskExecutingConditionFailed = (t, c) => { test = t.Name == "Sub-task" && c.Name == "TestCondition"; };
ctx.PlannerState.OnCurrentTaskExecutingConditionFailed = (t, c) => { test = t.Name == "Sub-task" && c.Name == "TestCondition"; };
var domain = new Domain<MyContext>("Test");
var task1 = new Selector() { Name = "Test" };
var task2 = new PrimitiveTask() { Name = "Sub-task" };
Expand Down Expand Up @@ -433,8 +411,8 @@ public void FindPlanIfConditionChangeAndOperatorIsContinuous_ExpectedBehavior()
ITask currentTask;

planner.Tick(domain, ctx, false);
plan = planner.GetPlan();
currentTask = planner.GetCurrentTask();
plan = ctx.PlannerState.Plan;
currentTask = ctx.PlannerState.CurrentTask;
Assert.IsTrue(plan != null);
Assert.IsTrue(plan.Count == 0);
Assert.IsTrue(currentTask.Name == "Test Action B");
Expand All @@ -446,8 +424,8 @@ public void FindPlanIfConditionChangeAndOperatorIsContinuous_ExpectedBehavior()
ctx.Done = true;

planner.Tick(domain, ctx, true);
plan = planner.GetPlan();
currentTask = planner.GetCurrentTask();
plan = ctx.PlannerState.Plan;
currentTask = ctx.PlannerState.CurrentTask;
Assert.IsTrue(plan != null);
Assert.IsTrue(plan.Count == 0);
Assert.IsTrue(currentTask.Name == "Test Action A");
Expand Down Expand Up @@ -481,8 +459,8 @@ public void FindPlanIfWorldStateChangeAndOperatorIsContinuous_ExpectedBehavior()
ITask currentTask;

planner.Tick(domain, ctx, false);
plan = planner.GetPlan();
currentTask = planner.GetCurrentTask();
plan = ctx.PlannerState.Plan;
currentTask = ctx.PlannerState.CurrentTask;
Assert.IsTrue(plan != null);
Assert.IsTrue(plan.Count == 0);
Assert.IsTrue(currentTask.Name == "Test Action B");
Expand All @@ -494,8 +472,8 @@ public void FindPlanIfWorldStateChangeAndOperatorIsContinuous_ExpectedBehavior()
ctx.SetState(MyWorldState.HasA, true, EffectType.Permanent);

planner.Tick(domain, ctx, true);
plan = planner.GetPlan();
currentTask = planner.GetCurrentTask();
plan = ctx.PlannerState.Plan;
currentTask = ctx.PlannerState.CurrentTask;
Assert.IsTrue(plan != null);
Assert.IsTrue(plan.Count == 0);
Assert.IsTrue(currentTask.Name == "Test Action A");
Expand Down Expand Up @@ -531,8 +509,8 @@ public void FindPlanIfWorldStateChangeToWorseMRTAndOperatorIsContinuous_Expected
ITask currentTask;

planner.Tick(domain, ctx, false);
plan = planner.GetPlan();
currentTask = planner.GetCurrentTask();
plan = ctx.PlannerState.Plan;
currentTask = ctx.PlannerState.CurrentTask;
Assert.IsTrue(plan != null);
Assert.IsTrue(plan.Count == 0);
Assert.IsTrue(currentTask.Name == "Test Action A");
Expand All @@ -544,8 +522,8 @@ public void FindPlanIfWorldStateChangeToWorseMRTAndOperatorIsContinuous_Expected
ctx.SetState(MyWorldState.HasA, true, EffectType.Permanent);

planner.Tick(domain, ctx, true);
plan = planner.GetPlan();
currentTask = planner.GetCurrentTask();
plan = ctx.PlannerState.Plan;
currentTask = ctx.PlannerState.CurrentTask;
Assert.IsTrue(plan != null);
Assert.IsTrue(plan.Count == 0);
Assert.IsTrue(currentTask.Name == "Test Action B");
Expand Down
3 changes: 2 additions & 1 deletion Fluid-HTN/Contexts/BaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public abstract class BaseContext : IContext
public bool IsDirty { get; set; }
public ContextState ContextState { get; set; } = ContextState.Executing;
public int CurrentDecompositionDepth { get; set; } = 0;
public abstract IFactory Factory { get; set; }
public abstract IFactory Factory { get; protected set; }
public abstract IPlannerState PlannerState { get; protected set; }
public List<int> MethodTraversalRecord { get; set; } = new List<int>();
public List<int> LastMTR { get; } = new List<int>();
public abstract List<string> MTRDebug { get; set; }
Expand Down
4 changes: 3 additions & 1 deletion Fluid-HTN/Contexts/IContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public interface IContext
ContextState ContextState { get; set; }
int CurrentDecompositionDepth { get; set; }

IFactory Factory { get; set; }
IFactory Factory { get; }

IPlannerState PlannerState { get; }

/// <summary>
/// The Method Traversal Record is used while decomposing a domain and
Expand Down
2 changes: 2 additions & 0 deletions Fluid-HTN/Fluid-HTN.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<Compile Include="Factory\DefaultFactory.cs" />
<Compile Include="Factory\IFactory.cs" />
<Compile Include="IDomain.cs" />
<Compile Include="Planners\DefaultPlannerState.cs" />
<Compile Include="Planners\IPlannerState.cs" />
<Compile Include="Tasks\CompoundTasks\DecompositionStatus.cs" />
<Compile Include="Tasks\CompoundTasks\IDecomposeAll.cs" />
<Compile Include="Tasks\CompoundTasks\PausePlanTask.cs" />
Expand Down
29 changes: 29 additions & 0 deletions Fluid-HTN/Planners/DefaultPlannerState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using FluidHTN.Conditions;
using FluidHTN.PrimitiveTasks;
using System;
using System.Collections.Generic;

namespace FluidHTN
{
public class DefaultPlannerState : IPlannerState
{
// ========================================================= PROPERTIES

public ITask CurrentTask { get; set; }
public Queue<ITask> Plan { get; set; } = new Queue<ITask>();
public TaskStatus LastStatus { get; set; }

// ========================================================= CALLBACKS

public Action<Queue<ITask>> OnNewPlan { get; set; }
public Action<Queue<ITask>, ITask, Queue<ITask>> OnReplacePlan { get; set; }
public Action<ITask> OnNewTask { get; set; }
public Action<ITask, ICondition> OnNewTaskConditionFailed { get; set; }
public Action<IPrimitiveTask> OnStopCurrentTask { get; set; }
public Action<IPrimitiveTask> OnCurrentTaskCompletedSuccessfully { get; set; }
public Action<IEffect> OnApplyEffect { get; set; }
public Action<IPrimitiveTask> OnCurrentTaskFailed { get; set; }
public Action<IPrimitiveTask> OnCurrentTaskContinues { get; set; }
public Action<IPrimitiveTask, ICondition> OnCurrentTaskExecutingConditionFailed { get; set; }
}
}
76 changes: 76 additions & 0 deletions Fluid-HTN/Planners/IPlannerState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using FluidHTN.Conditions;
using FluidHTN.PrimitiveTasks;
using System;
using System.Collections.Generic;

namespace FluidHTN
{
public interface IPlannerState
{
// ========================================================= PROPERTIES

ITask CurrentTask { get; set; }
Queue<ITask> Plan { get; set; }
TaskStatus LastStatus { get; set; }

// ========================================================= CALLBACKS

/// <summary>
/// OnNewPlan(newPlan) is called when we found a new plan, and there is no
/// old plan to replace.
/// </summary>
Action<Queue<ITask>> OnNewPlan { get; set; }

/// <summary>
/// OnReplacePlan(oldPlan, currentTask, newPlan) is called when we're about to replace the
/// current plan with a new plan.
/// </summary>
Action<Queue<ITask>, ITask, Queue<ITask>> OnReplacePlan { get; set; }

/// <summary>
/// OnNewTask(task) is called after we popped a new task off the current plan.
/// </summary>
Action<ITask> OnNewTask { get; set; }

/// <summary>
/// OnNewTaskConditionFailed(task, failedCondition) is called when we failed to
/// validate a condition on a new task.
/// </summary>
Action<ITask, ICondition> OnNewTaskConditionFailed { get; set; }

/// <summary>
/// OnStopCurrentTask(task) is called when the currently running task was stopped
/// forcefully.
/// </summary>
Action<IPrimitiveTask> OnStopCurrentTask { get; set; }

/// <summary>
/// OnCurrentTaskCompletedSuccessfully(task) is called when the currently running task
/// completes successfully, and before its effects are applied.
/// </summary>
Action<IPrimitiveTask> OnCurrentTaskCompletedSuccessfully { get; set; }

/// <summary>
/// OnApplyEffect(effect) is called for each effect of the type PlanAndExecute on a
/// completed task.
/// </summary>
Action<IEffect> OnApplyEffect { get; set; }

/// <summary>
/// OnCurrentTaskFailed(task) is called when the currently running task fails to complete.
/// </summary>
Action<IPrimitiveTask> OnCurrentTaskFailed { get; set; }

/// <summary>
/// OnCurrentTaskContinues(task) is called every tick that a currently running task
/// needs to continue.
/// </summary>
Action<IPrimitiveTask> OnCurrentTaskContinues { get; set; }

/// <summary>
/// OnCurrentTaskExecutingConditionFailed(task, condition) is called if an Executing Condition
/// fails. The Executing Conditions are checked before every call to task.Operator.Update(...).
/// </summary>
Action<IPrimitiveTask, ICondition> OnCurrentTaskExecutingConditionFailed { get; set; }
}
}
Loading

5 comments on commit 607ba05

@fnaith
Copy link

@fnaith fnaith commented on 607ba05 May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit surprising that a PlannerState belongs to Context.
But thinking twice, make planner statless is kind of cool !

@ptrefall
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing your opinion on this commit fnaith! The reasoning is from the perspective of being able to trigger planning for agents on multiple threads and being guaranteed that the agent's context is respected. This is why it made sense to put PlannerState in the Agent's Context, as the planner's state relates to the plan, current task and current status of planning for "the last agent context is processed". This does of course mean that in a situation where you don't need planning to run in parallel, we spend more memory than is strictly needed. This is where the setter on the PlannerState property can be used in your own Context definition to override, so you could set it to a common PlannerState instance if you so wanted to. I am of course open to making alterations here, so feel free to suggest improvements.

@fnaith
Copy link

@fnaith fnaith commented on 607ba05 May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I noticed that you import System.Threading.Tasks and System.Security.Cryptography.X509Certificates for multiple threads.
It all make scese now, thanks for your explanation.

@ptrefall
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, those probably shouldn't be in there. Sometimes this auto-import stuff gets overly "excited". Thanks for pointing that out! About 5 years ago TotallyGatsby successfully used the stateless planner branch with Unity DOTS. He has several thousand agents running some basic domains in real-time.

@fnaith
Copy link

@fnaith fnaith commented on 607ba05 May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the performance is very promising.
I wish the godot can support hundreds of agent.

Please sign in to comment.