using System; using System.Collections.Generic; using FluidHTN.Compounds; using FluidHTN.Conditions; using FluidHTN.PrimitiveTasks; namespace FluidHTN { /// /// A planner is a responsible for handling the management of finding plans in a domain, replan when the state of the /// running plan /// demands it, or look for a new potential plan if the world state gets dirty. /// /// 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 /// /// Call this with a domain and context instance to have the planner manage plan and task handling for the domain at /// runtime. /// If the plan completes or fails, the planner will find a new plan, or if the context is marked dirty, the planner /// will attempt /// a replan to see whether we can find a better plan now that the state of the world has changed. /// This planner can also be used as a blueprint for writing a custom planner. /// /// /// public void Tick(Domain domain, T ctx, bool allowImmediateReplan = true) { if (ctx.IsInitialized == false) throw new Exception("Context was not initialized!"); DecompositionStatus decompositionStatus = DecompositionStatus.Failed; bool isTryingToReplacePlan = false; // Check whether state has changed or the current plan has finished running. // and if so, try to find a new plan. if (_currentTask == null && (_plan.Count == 0) || ctx.IsDirty) { Queue lastPartialPlanQueue = null; var worldStateDirtyReplan = ctx.IsDirty; ctx.IsDirty = false; if (worldStateDirtyReplan) { // If we're simply re-evaluating whether to replace the current plan because // some world state got dirt, then we do not intend to continue a partial plan // right now, but rather see whether the world state changed to a degree where // we should pursue a better plan. Thus, if this replan fails to find a better // plan, we have to add back the partial plan temps cached above. if (ctx.HasPausedPartialPlan) { ctx.HasPausedPartialPlan = false; lastPartialPlanQueue = ctx.Factory.CreateQueue(); while (ctx.PartialPlanQueue.Count > 0) { lastPartialPlanQueue.Enqueue(ctx.PartialPlanQueue.Dequeue()); } // We also need to ensure that the last mtr is up to date with the on-going MTR of the partial plan, // so that any new potential plan that is decomposing from the domain root has to beat the currently // running partial plan. ctx.LastMTR.Clear(); foreach (var record in ctx.MethodTraversalRecord) ctx.LastMTR.Add(record); if (ctx.DebugMTR) { ctx.LastMTRDebug.Clear(); foreach (var record in ctx.MTRDebug) ctx.LastMTRDebug.Add(record); } } } decompositionStatus = domain.FindPlan(ctx, out var newPlan); isTryingToReplacePlan = _plan.Count > 0; if (decompositionStatus == DecompositionStatus.Succeeded || decompositionStatus == DecompositionStatus.Partial) { if (OnReplacePlan != null && (_plan.Count > 0 || _currentTask != null)) { OnReplacePlan.Invoke(_plan, _currentTask, newPlan); } else if (OnNewPlan != null && _plan.Count == 0) { OnNewPlan.Invoke(newPlan); } _plan.Clear(); while (newPlan.Count > 0) _plan.Enqueue(newPlan.Dequeue()); if (_currentTask != null && _currentTask is IPrimitiveTask t) { OnStopCurrentTask?.Invoke(t); t.Stop(ctx); _currentTask = null; } // Copy the MTR into our LastMTR to represent the current plan's decomposition record // that must be beat to replace the plan. if (ctx.MethodTraversalRecord != null) { ctx.LastMTR.Clear(); foreach (var record in ctx.MethodTraversalRecord) ctx.LastMTR.Add(record); if (ctx.DebugMTR) { ctx.LastMTRDebug.Clear(); foreach (var record in ctx.MTRDebug) ctx.LastMTRDebug.Add(record); } } } else if (lastPartialPlanQueue != null) { ctx.HasPausedPartialPlan = true; ctx.PartialPlanQueue.Clear(); while (lastPartialPlanQueue.Count > 0) { ctx.PartialPlanQueue.Enqueue(lastPartialPlanQueue.Dequeue()); } ctx.Factory.FreeQueue(ref lastPartialPlanQueue); if (ctx.LastMTR.Count > 0) { ctx.MethodTraversalRecord.Clear(); foreach (var record in ctx.LastMTR) ctx.MethodTraversalRecord.Add(record); ctx.LastMTR.Clear(); if (ctx.DebugMTR) { ctx.MTRDebug.Clear(); foreach (var record in ctx.LastMTRDebug) ctx.MTRDebug.Add(record); ctx.LastMTRDebug.Clear(); } } } } if (_currentTask == null && _plan.Count > 0) { _currentTask = _plan.Dequeue(); if (_currentTask != null) { OnNewTask?.Invoke(_currentTask); foreach (var condition in _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); _currentTask = null; _plan.Clear(); ctx.LastMTR.Clear(); if (ctx.DebugMTR) ctx.LastMTRDebug.Clear(); ctx.HasPausedPartialPlan = false; ctx.PartialPlanQueue.Clear(); ctx.IsDirty = false; return; } } } if (_currentTask != null) if (_currentTask is IPrimitiveTask task) { if (task.Operator != null) { foreach (var condition in task.ExecutingConditions) // If a condition failed, then the plan failed to progress! A replan is required. if (condition.IsValid(ctx) == false) { OnCurrentTaskExecutingConditionFailed?.Invoke(task, condition); _currentTask = null; _plan.Clear(); ctx.LastMTR.Clear(); if (ctx.DebugMTR) ctx.LastMTRDebug.Clear(); ctx.HasPausedPartialPlan = false; ctx.PartialPlanQueue.Clear(); ctx.IsDirty = false; return; } 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) { 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); effect.Apply(ctx); } } _currentTask = null; if (_plan.Count == 0) { ctx.LastMTR.Clear(); if (ctx.DebugMTR) ctx.LastMTRDebug.Clear(); ctx.IsDirty = false; if (allowImmediateReplan) Tick(domain, ctx, allowImmediateReplan: false); } } // If the operation failed to finish, we need to fail the entire plan, so that we will replan the next tick. else if (LastStatus == TaskStatus.Failure) { OnCurrentTaskFailed?.Invoke(task); _currentTask = null; _plan.Clear(); ctx.LastMTR.Clear(); if (ctx.DebugMTR) ctx.LastMTRDebug.Clear(); ctx.HasPausedPartialPlan = false; ctx.PartialPlanQueue.Clear(); ctx.IsDirty = false; } // Otherwise the operation isn't done yet and need to continue. else { OnCurrentTaskContinues?.Invoke(task); } } else { // This should not really happen if a domain is set up properly. _currentTask = null; LastStatus = TaskStatus.Failure; } } if (_currentTask == null && _plan.Count == 0 && isTryingToReplacePlan == false && (decompositionStatus == DecompositionStatus.Failed || decompositionStatus == DecompositionStatus.Rejected)) { LastStatus = TaskStatus.Failure; } } // ========================================================= RESET public void Reset(IContext ctx) { _plan.Clear(); if (_currentTask != null && _currentTask is IPrimitiveTask task) { task.Stop(ctx); } _currentTask = null; } // ========================================================= 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; } } }