diff --git a/Fluid-HTN.UnitTests/MyContext.cs b/Fluid-HTN.UnitTests/MyContext.cs index 8565420..4b7f910 100644 --- a/Fluid-HTN.UnitTests/MyContext.cs +++ b/Fluid-HTN.UnitTests/MyContext.cs @@ -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 MTRDebug { get; set; } = null; public override List LastMTRDebug { get; set; } = null; public override bool DebugMTR { get; } = false; diff --git a/Fluid-HTN.UnitTests/PlannerTests.cs b/Fluid-HTN.UnitTests/PlannerTests.cs index 15e7e0c..cd42e1d 100644 --- a/Fluid-HTN.UnitTests/PlannerTests.cs +++ b/Fluid-HTN.UnitTests/PlannerTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; using FluidHTN; using FluidHTN.Compounds; using FluidHTN.Conditions; @@ -13,25 +14,6 @@ namespace Fluid_HTN.UnitTests [TestClass] public class PlannerTests { - [TestMethod] - public void GetPlanReturnsClearInstanceAtStart_ExpectedBehavior() - { - var planner = new Planner(); - var plan = planner.GetPlan(); - - Assert.IsTrue(plan != null); - Assert.IsTrue(plan.Count == 0); - } - - [TestMethod] - public void GetCurrentTaskReturnsNullAtStart_ExpectedBehavior() - { - var planner = new Planner(); - var task = planner.GetCurrentTask(); - - Assert.IsTrue(task == null); - } - [TestMethod] [ExpectedException(typeof(NullReferenceException), AllowDerivedTypes = false)] public void TickWithNullParametersThrowsNRE_ExpectedBehavior() @@ -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] @@ -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] @@ -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] @@ -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] @@ -155,7 +133,7 @@ public void OnNewPlan_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnNewPlan = (p) => { test = p.Count == 1; }; + ctx.PlannerState.OnNewPlan = (p) => { test = p.Count == 1; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test" }; var task2 = new PrimitiveTask() { Name = "Sub-task" }; @@ -175,7 +153,7 @@ public void OnReplacePlan_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - 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("Test"); var task1 = new Selector() { Name = "Test1" }; var task2 = new Selector() { Name = "Test2" }; @@ -205,7 +183,7 @@ public void OnNewTask_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnNewTask = (t) => { test = t.Name == "Sub-task"; }; + ctx.PlannerState.OnNewTask = (t) => { test = t.Name == "Sub-task"; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test" }; var task2 = new PrimitiveTask() { Name = "Sub-task" }; @@ -225,7 +203,7 @@ public void OnNewTaskConditionFailed_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnNewTaskConditionFailed = (t, c) => { test = t.Name == "Sub-task1"; }; + ctx.PlannerState.OnNewTaskConditionFailed = (t, c) => { test = t.Name == "Sub-task1"; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test1" }; var task2 = new Selector() { Name = "Test2" }; @@ -260,7 +238,7 @@ public void OnStopCurrentTask_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnStopCurrentTask = (t) => { test = t.Name == "Sub-task2"; }; + ctx.PlannerState.OnStopCurrentTask = (t) => { test = t.Name == "Sub-task2"; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test1" }; var task2 = new Selector() { Name = "Test2" }; @@ -290,7 +268,7 @@ public void OnCurrentTaskCompletedSuccessfully_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnCurrentTaskCompletedSuccessfully = (t) => { test = t.Name == "Sub-task1"; }; + ctx.PlannerState.OnCurrentTaskCompletedSuccessfully = (t) => { test = t.Name == "Sub-task1"; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test1" }; var task2 = new Selector() { Name = "Test2" }; @@ -320,7 +298,7 @@ public void OnApplyEffect_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnApplyEffect = (e) => { test = e.Name == "TestEffect"; }; + ctx.PlannerState.OnApplyEffect = (e) => { test = e.Name == "TestEffect"; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test1" }; var task2 = new Selector() { Name = "Test2" }; @@ -352,7 +330,7 @@ public void OnCurrentTaskFailed_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnCurrentTaskFailed = (t) => { test = t.Name == "Sub-task"; }; + ctx.PlannerState.OnCurrentTaskFailed = (t) => { test = t.Name == "Sub-task"; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test" }; var task2 = new PrimitiveTask() { Name = "Sub-task" }; @@ -372,7 +350,7 @@ public void OnCurrentTaskContinues_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - planner.OnCurrentTaskContinues = (t) => { test = t.Name == "Sub-task"; }; + ctx.PlannerState.OnCurrentTaskContinues = (t) => { test = t.Name == "Sub-task"; }; var domain = new Domain("Test"); var task1 = new Selector() { Name = "Test" }; var task2 = new PrimitiveTask() { Name = "Sub-task" }; @@ -392,7 +370,7 @@ public void OnCurrentTaskExecutingConditionFailed_ExpectedBehavior() var ctx = new MyContext(); ctx.Init(); var planner = new Planner(); - 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("Test"); var task1 = new Selector() { Name = "Test" }; var task2 = new PrimitiveTask() { Name = "Sub-task" }; @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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"); @@ -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"); diff --git a/Fluid-HTN/Contexts/BaseContext.cs b/Fluid-HTN/Contexts/BaseContext.cs index 7002b49..cc93d0c 100644 --- a/Fluid-HTN/Contexts/BaseContext.cs +++ b/Fluid-HTN/Contexts/BaseContext.cs @@ -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 MethodTraversalRecord { get; set; } = new List(); public List LastMTR { get; } = new List(); public abstract List MTRDebug { get; set; } diff --git a/Fluid-HTN/Contexts/IContext.cs b/Fluid-HTN/Contexts/IContext.cs index 308a68f..fbcef2f 100644 --- a/Fluid-HTN/Contexts/IContext.cs +++ b/Fluid-HTN/Contexts/IContext.cs @@ -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; } /// /// The Method Traversal Record is used while decomposing a domain and diff --git a/Fluid-HTN/Fluid-HTN.csproj b/Fluid-HTN/Fluid-HTN.csproj index d07e064..ac51bce 100644 --- a/Fluid-HTN/Fluid-HTN.csproj +++ b/Fluid-HTN/Fluid-HTN.csproj @@ -49,6 +49,8 @@ + + diff --git a/Fluid-HTN/Planners/DefaultPlannerState.cs b/Fluid-HTN/Planners/DefaultPlannerState.cs new file mode 100644 index 0000000..196ee28 --- /dev/null +++ b/Fluid-HTN/Planners/DefaultPlannerState.cs @@ -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 Plan { get; set; } = new Queue(); + public TaskStatus LastStatus { get; set; } + + // ========================================================= CALLBACKS + + public Action> OnNewPlan { get; set; } + public Action, ITask, Queue> OnReplacePlan { get; set; } + public Action OnNewTask { get; set; } + public Action OnNewTaskConditionFailed { get; set; } + public Action OnStopCurrentTask { get; set; } + public Action OnCurrentTaskCompletedSuccessfully { get; set; } + public Action OnApplyEffect { get; set; } + public Action OnCurrentTaskFailed { get; set; } + public Action OnCurrentTaskContinues { get; set; } + public Action OnCurrentTaskExecutingConditionFailed { get; set; } + } +} diff --git a/Fluid-HTN/Planners/IPlannerState.cs b/Fluid-HTN/Planners/IPlannerState.cs new file mode 100644 index 0000000..0ff1373 --- /dev/null +++ b/Fluid-HTN/Planners/IPlannerState.cs @@ -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 Plan { get; set; } + TaskStatus LastStatus { get; set; } + + // ========================================================= CALLBACKS + + /// + /// OnNewPlan(newPlan) is called when we found a new plan, and there is no + /// old plan to replace. + /// + Action> OnNewPlan { get; set; } + + /// + /// OnReplacePlan(oldPlan, currentTask, newPlan) is called when we're about to replace the + /// current plan with a new plan. + /// + Action, ITask, Queue> OnReplacePlan { get; set; } + + /// + /// OnNewTask(task) is called after we popped a new task off the current plan. + /// + Action OnNewTask { get; set; } + + /// + /// OnNewTaskConditionFailed(task, failedCondition) is called when we failed to + /// validate a condition on a new task. + /// + Action OnNewTaskConditionFailed { get; set; } + + /// + /// OnStopCurrentTask(task) is called when the currently running task was stopped + /// forcefully. + /// + Action OnStopCurrentTask { get; set; } + + /// + /// OnCurrentTaskCompletedSuccessfully(task) is called when the currently running task + /// completes successfully, and before its effects are applied. + /// + Action OnCurrentTaskCompletedSuccessfully { get; set; } + + /// + /// OnApplyEffect(effect) is called for each effect of the type PlanAndExecute on a + /// completed task. + /// + Action OnApplyEffect { get; set; } + + /// + /// OnCurrentTaskFailed(task) is called when the currently running task fails to complete. + /// + Action OnCurrentTaskFailed { get; set; } + + /// + /// OnCurrentTaskContinues(task) is called every tick that a currently running task + /// needs to continue. + /// + Action OnCurrentTaskContinues { get; set; } + + /// + /// OnCurrentTaskExecutingConditionFailed(task, condition) is called if an Executing Condition + /// fails. The Executing Conditions are checked before every call to task.Operator.Update(...). + /// + Action OnCurrentTaskExecutingConditionFailed { get; set; } + } +} diff --git a/Fluid-HTN/Planners/Planner.cs b/Fluid-HTN/Planners/Planner.cs index 168077d..594b9ba 100644 --- a/Fluid-HTN/Planners/Planner.cs +++ b/Fluid-HTN/Planners/Planner.cs @@ -15,74 +15,6 @@ namespace FluidHTN /// public class Planner where T : IContext { - // ========================================================= FIELDS - - private ITask _currentTask; - private readonly Queue _plan = new Queue(); - - // ========================================================= FIELDS - public TaskStatus LastStatus { get; protected set; } - - // ========================================================= CALLBACKS - - /// - /// OnNewPlan(newPlan) is called when we found a new plan, and there is no - /// old plan to replace. - /// - public Action> OnNewPlan = null; - - /// - /// OnReplacePlan(oldPlan, currentTask, newPlan) is called when we're about to replace the - /// current plan with a new plan. - /// - public Action, ITask, Queue> OnReplacePlan = null; - - /// - /// OnNewTask(task) is called after we popped a new task off the current plan. - /// - public Action OnNewTask = null; - - /// - /// OnNewTaskConditionFailed(task, failedCondition) is called when we failed to - /// validate a condition on a new task. - /// - public Action OnNewTaskConditionFailed = null; - - /// - /// OnStopCurrentTask(task) is called when the currently running task was stopped - /// forcefully. - /// - public Action OnStopCurrentTask = null; - - /// - /// OnCurrentTaskCompletedSuccessfully(task) is called when the currently running task - /// completes successfully, and before its effects are applied. - /// - public Action OnCurrentTaskCompletedSuccessfully = null; - - /// - /// OnApplyEffect(effect) is called for each effect of the type PlanAndExecute on a - /// completed task. - /// - public Action OnApplyEffect = null; - - /// - /// OnCurrentTaskFailed(task) is called when the currently running task fails to complete. - /// - public Action OnCurrentTaskFailed = null; - - /// - /// OnCurrentTaskContinues(task) is called every tick that a currently running task - /// needs to continue. - /// - public Action OnCurrentTaskContinues = null; - - /// - /// OnCurrentTaskExecutingConditionFailed(task, condition) is called if an Executing Condition - /// fails. The Executing Conditions are checked before every call to task.Operator.Update(...). - /// - public Action OnCurrentTaskExecutingConditionFailed = null; - // ========================================================= TICK PLAN /// @@ -114,7 +46,7 @@ public void Tick(Domain domain, T ctx, bool allowImmediateReplan = true) } // If the plan has more tasks, we try to select the next one. - if (CanSelectNextTaskInPlan()) + if (CanSelectNextTaskInPlan(ctx)) { // Select the next task, but check whether the conditions of the next task failed to validate. if (SelectNextTaskInPlan(domain, ctx) == false) @@ -124,7 +56,7 @@ public void Tick(Domain domain, T ctx, bool allowImmediateReplan = true) } // If the current task is a primitive task, we try to tick its operator. - if (_currentTask is IPrimitiveTask task) + if (ctx.PlannerState.CurrentTask is IPrimitiveTask task) { if (TryTickPrimitiveTaskOperator(domain, ctx, task, allowImmediateReplan) == false) { @@ -133,9 +65,9 @@ public void Tick(Domain domain, T ctx, bool allowImmediateReplan = true) } // Check whether the planner failed to find a plan - if (HasFailedToFindPlan(isTryingToReplacePlan, decompositionStatus)) + if (HasFailedToFindPlan(isTryingToReplacePlan, decompositionStatus, ctx)) { - LastStatus = TaskStatus.Failure; + ctx.PlannerState.LastStatus = TaskStatus.Failure; } } @@ -147,13 +79,13 @@ public void Tick(Domain domain, T ctx, bool allowImmediateReplan = true) /// private bool ShouldFindNewPlan(T ctx) { - return ctx.IsDirty || (_currentTask == null && _plan.Count == 0); + return ctx.IsDirty || (ctx.PlannerState.CurrentTask == null && ctx.PlannerState.Plan.Count == 0); } private bool TryFindNewPlan(Domain domain, T ctx, out DecompositionStatus decompositionStatus) { var lastPartialPlanQueue = PrepareDirtyWorldStateForReplan(ctx); - var isTryingToReplacePlan = _plan.Count > 0; + var isTryingToReplacePlan = ctx.PlannerState.Plan.Count > 0; decompositionStatus = domain.FindPlan(ctx, out var newPlan); @@ -239,27 +171,27 @@ private bool HasFoundNewPlan(DecompositionStatus decompositionStatus) private void OnFoundNewPlan(T ctx, Queue newPlan) { - if (OnReplacePlan != null && (_plan.Count > 0 || _currentTask != null)) + if (ctx.PlannerState.OnReplacePlan != null && (ctx.PlannerState.Plan.Count > 0 || ctx.PlannerState.CurrentTask != null)) { - OnReplacePlan.Invoke(_plan, _currentTask, newPlan); + ctx.PlannerState.OnReplacePlan.Invoke(ctx.PlannerState.Plan, ctx.PlannerState.CurrentTask, newPlan); } - else if (OnNewPlan != null && _plan.Count == 0) + else if (ctx.PlannerState.OnNewPlan != null && ctx.PlannerState.Plan.Count == 0) { - OnNewPlan.Invoke(newPlan); + ctx.PlannerState.OnNewPlan.Invoke(newPlan); } - _plan.Clear(); + ctx.PlannerState.Plan.Clear(); while (newPlan.Count > 0) { - _plan.Enqueue(newPlan.Dequeue()); + ctx.PlannerState.Plan.Enqueue(newPlan.Dequeue()); } // If a task was running from the previous plan, we stop it. - if (_currentTask != null && _currentTask is IPrimitiveTask t) + if (ctx.PlannerState.CurrentTask != null && ctx.PlannerState.CurrentTask is IPrimitiveTask t) { - OnStopCurrentTask?.Invoke(t); + ctx.PlannerState.OnStopCurrentTask?.Invoke(t); t.Stop(ctx); - _currentTask = null; + ctx.PlannerState.CurrentTask = null; } // Copy the MTR into our LastMTR to represent the current plan's decomposition record @@ -327,9 +259,9 @@ private void RestoreLastMethodTraversalRecord(T ctx) /// If current task is null, we need to verify that the plan has more tasks queued. /// /// - private bool CanSelectNextTaskInPlan() + private bool CanSelectNextTaskInPlan(T ctx) { - return _currentTask == null && _plan.Count > 0; + return ctx.PlannerState.CurrentTask == null && ctx.PlannerState.Plan.Count > 0; } /// @@ -340,10 +272,10 @@ private bool CanSelectNextTaskInPlan() /// private bool SelectNextTaskInPlan(Domain domain, T ctx) { - _currentTask = _plan.Dequeue(); - if (_currentTask != null) + ctx.PlannerState.CurrentTask = ctx.PlannerState.Plan.Dequeue(); + if (ctx.PlannerState.CurrentTask != null) { - OnNewTask?.Invoke(_currentTask); + ctx.PlannerState.OnNewTask?.Invoke(ctx.PlannerState.CurrentTask); return IsConditionsValid(ctx); } @@ -368,31 +300,31 @@ private bool TryTickPrimitiveTaskOperator(Domain domain, T ctx, IPrimitiveTas return false; } - LastStatus = task.Operator.Update(ctx); + ctx.PlannerState.LastStatus = task.Operator.Update(ctx); // If the operation finished successfully, we set task to null so that we dequeue the next task in the plan the following tick. - if (LastStatus == TaskStatus.Success) + if (ctx.PlannerState.LastStatus == TaskStatus.Success) { OnOperatorFinishedSuccessfully(domain, ctx, task, allowImmediateReplan); return true; } // If the operation failed to finish, we need to fail the entire plan, so that we will replan the next tick. - if (LastStatus == TaskStatus.Failure) + if (ctx.PlannerState.LastStatus == TaskStatus.Failure) { FailEntirePlan(ctx, task); return true; } // Otherwise the operation isn't done yet and need to continue. - OnCurrentTaskContinues?.Invoke(task); + ctx.PlannerState.OnCurrentTaskContinues?.Invoke(task); return true; } // This should not really happen if a domain is set up properly. task.Aborted(ctx); - _currentTask = null; - LastStatus = TaskStatus.Failure; + ctx.PlannerState.CurrentTask = null; + ctx.PlannerState.LastStatus = TaskStatus.Failure; return true; } @@ -403,13 +335,13 @@ private bool TryTickPrimitiveTaskOperator(Domain domain, T ctx, IPrimitiveTas /// private bool IsConditionsValid(T ctx) { - foreach (var condition in _currentTask.Conditions) + foreach (var condition in ctx.PlannerState.CurrentTask.Conditions) { // If a condition failed, then the plan failed to progress! A replan is required. if (condition.IsValid(ctx) == false) { - OnNewTaskConditionFailed?.Invoke(_currentTask, condition); - AbortTask(ctx, _currentTask as IPrimitiveTask); + ctx.PlannerState.OnNewTaskConditionFailed?.Invoke(ctx.PlannerState.CurrentTask, condition); + AbortTask(ctx, ctx.PlannerState.CurrentTask as IPrimitiveTask); return false; } @@ -433,7 +365,7 @@ private bool IsExecutingConditionsValid(Domain domain, T ctx, IPrimitiveTask // If a condition failed, then the plan failed to progress! A replan is required. if (condition.IsValid(ctx) == false) { - OnCurrentTaskExecutingConditionFailed?.Invoke(task, condition); + ctx.PlannerState.OnCurrentTaskExecutingConditionFailed?.Invoke(task, condition); AbortTask(ctx, task); @@ -469,20 +401,20 @@ private void AbortTask(T ctx, IPrimitiveTask task) /// private void OnOperatorFinishedSuccessfully(Domain domain, T ctx, IPrimitiveTask task, bool allowImmediateReplan) { - OnCurrentTaskCompletedSuccessfully?.Invoke(task); + ctx.PlannerState.OnCurrentTaskCompletedSuccessfully?.Invoke(task); // All effects that is a result of running this task should be applied when the task is a success. foreach (var effect in task.Effects) { if (effect.Type == EffectType.PlanAndExecute) { - OnApplyEffect?.Invoke(effect); + ctx.PlannerState.OnApplyEffect?.Invoke(effect); effect.Apply(ctx); } } - _currentTask = null; - if (_plan.Count == 0) + ctx.PlannerState.CurrentTask = null; + if (ctx.PlannerState.Plan.Count == 0) { ctx.LastMTR.Clear(); @@ -507,7 +439,7 @@ private void OnOperatorFinishedSuccessfully(Domain domain, T ctx, IPrimitiveT /// private void FailEntirePlan(T ctx, IPrimitiveTask task) { - OnCurrentTaskFailed?.Invoke(task); + ctx.PlannerState.OnCurrentTaskFailed?.Invoke(task); task.Aborted(ctx); ClearPlanForReplan(ctx); @@ -519,8 +451,8 @@ private void FailEntirePlan(T ctx, IPrimitiveTask task) /// private void ClearPlanForReplan(T ctx) { - _currentTask = null; - _plan.Clear(); + ctx.PlannerState.CurrentTask = null; + ctx.PlannerState.Plan.Clear(); ctx.LastMTR.Clear(); @@ -540,9 +472,9 @@ private void ClearPlanForReplan(T ctx) /// /// /// - private bool HasFailedToFindPlan(bool isTryingToReplacePlan, DecompositionStatus decompositionStatus) + private bool HasFailedToFindPlan(bool isTryingToReplacePlan, DecompositionStatus decompositionStatus, T ctx) { - return _currentTask == null && _plan.Count == 0 && isTryingToReplacePlan == false && + return ctx.PlannerState.CurrentTask == null && ctx.PlannerState.Plan.Count == 0 && isTryingToReplacePlan == false && (decompositionStatus == DecompositionStatus.Failed || decompositionStatus == DecompositionStatus.Rejected); } @@ -551,34 +483,14 @@ private bool HasFailedToFindPlan(bool isTryingToReplacePlan, DecompositionStatus public void Reset(T ctx) { - _plan.Clear(); + ctx.PlannerState.Plan.Clear(); - if (_currentTask != null && _currentTask is IPrimitiveTask task) + if (ctx.PlannerState.CurrentTask != null && ctx.PlannerState.CurrentTask is IPrimitiveTask task) { task.Stop(ctx); } ClearPlanForReplan(ctx); } - - // ========================================================= GETTERS - - /// - /// Get the current plan. This is not a copy of the running plan, so treat it as read-only. - /// - /// - public Queue GetPlan() - { - return _plan; - } - - /// - /// Get the current task. - /// - /// - public ITask GetCurrentTask() - { - return _currentTask; - } } } diff --git a/README.md b/README.md index 439b245..08370cf 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A simple HTN planner based around the principles of the Builder pattern, inspire * Uses a Factory interface internally to create and free arrays/collections/objects, allowing the user to add pooling, or other memory management schemes. * Decomposition logging, for debugging. * Comes with Unity Package Module definitions for seamless integration into Unity projects. -* 150 unit tests. +* 148 unit tests. ## Support Join the [discord channel](https://discord.gg/MuccnAz) to share your experience and get support on the usage of Fluid HTN. @@ -75,7 +75,8 @@ public class MyContext : BaseContext public override Queue DecompositionLog { get; set; } = null; public override bool LogDecomposition { get; } = false; - 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(); private byte[] _worldState = new byte[Enum.GetValues(typeof(MyWorldState)).Length]; public override byte[] WorldState => _worldState; @@ -571,7 +572,7 @@ foreach(var log in ctx.LastMTRDebug) ``` The reason these debug properties are all abstract in BaseContext, is because Fluid HTN must be generic enough to be used varied environments. In Unity, for instance, a user might want to have these debug flags enabled only when in the editor, or when running the game in a special dev-mode. Or maybe the user doesn't use Unity at all, and other policies are applied for when to debug. #### Callback hooks in the planner -Sometimes these debug logs won't be enough to understand how the planner flows and gives us the results it does. Or maybe there is a need to hook up to certain events in the planner for other purposes. The planner exposes multiple callbacks that we can hook up to. +Sometimes these debug logs won't be enough to understand how the planner flows and gives us the results it does. Or maybe there is a need to hook up to certain events in the planner for other purposes. The planner state exposes multiple callbacks that we can hook up to. OnNewPlan(newPlan) is called when we found a new plan, and there is no old plan to replace. ```C#