Browse Source

More HTN Refactoring

- Got rid of TaskArguments struct because it wasn't buying me anything. If the planner has to access it in order to function it negates its purpose
- Added const to Task virtual functions so that future users know when they can change state etc.
- Pulled the stack/stackless planning code into separate functions. I'm a little unhappy with the amount of code duplication and maintainability issues with having to always change two different places. Maybe I'll eventually get around to fixing it
- PlanStep() now pays full attention to the user's Break* settings. This means you can (with proper settings) complete a whole plan with a single PlanStep(). You can also make it break after basically every single action if necessary. I'm guessing this feature will never be used :(. I wrote a test for this as wel.
combatComponentRefactor
Macoy Madson 6 years ago
parent
commit
c1f1e7bfa2
  1. 616
      src/ai/htn/HTNPlanner.cpp
  2. 21
      src/ai/htn/HTNPlanner.hpp
  3. 2
      src/ai/htn/HTNTasks.cpp
  4. 18
      src/ai/htn/HTNTasks.hpp
  5. 6
      src/ai/htn/HTNTypes.hpp
  6. 84
      src/unitTesting/HTN_test.cpp

616
src/ai/htn/HTNPlanner.cpp

@ -11,7 +11,6 @@ bool DecomposeGoalTask(GoalDecompositionStack& decompositionStack, GoalTask* goa
int methodIndex, ParameterList& parameters, WorldState& state)
{
GoalDecomposition decomposition;
// GoalTask* goalTask = currentTask->GetGoal();
decomposition.DecomposedGoalTask = goalTask;
decomposition.MethodIndex = methodIndex;
decomposition.Parameters = parameters;
@ -29,388 +28,389 @@ bool DecomposeGoalTask(GoalDecompositionStack& decompositionStack, GoalTask* goa
}
bool DecomposeCompoundTask(TaskCallList& compoundDecompositions, CompoundTask* compoundTask,
TaskArguments& taskArguments)
const WorldState& state, const ParameterList& parameters)
{
if (!compoundTask->StateMeetsPreconditions(taskArguments))
if (!compoundTask->StateMeetsPreconditions(state, parameters))
return false;
return compoundTask->Decompose(compoundDecompositions, taskArguments);
return compoundTask->Decompose(compoundDecompositions, state, parameters);
}
// TODO: Pool various task lists?
// TODO: Pull more things out into functions, if possible. It's bad that whenever I make a change to
// something I have to change it in two places
Planner::Status Planner::PlanStep(void)
// When the stack is empty, find a goal task to push onto the task or add tasks as per usual
// If any Decompositions or Preconditions fail, we must fail the entire plan because we have
// no alternate methods
Planner::Status Planner::PlanStep_BottomLevel(void)
{
// When the stack is empty, find a goal task to push onto the task or add tasks as per usual
// If any Decompositions or Preconditions fail, we must fail the entire plan because we have
// no alternate methods
if (DecompositionStack.empty())
if (InitialCallList.empty())
return Status::Failed_NoTasks;
else if (WorkingCallList.empty() && FinalCallList.empty())
{
if (InitialCallList.empty())
return Status::Failed_NoTasks;
else if (WorkingCallList.empty() && FinalCallList.empty())
{
WorkingCallList = InitialCallList;
StacklessState = State;
}
else if (WorkingCallList.empty() && !FinalCallList.empty())
return Status::PlanComplete;
// First time the planner has been stepped
WorkingCallList = InitialCallList;
StacklessState = State;
}
else if (WorkingCallList.empty() && !FinalCallList.empty())
return Status::PlanComplete;
TaskCallList compoundDecompositions;
TaskCallList compoundDecompositions;
if (DebugPrint)
{
std::cout << "\nPlanStep()\nWorkingCallList.size() = " << WorkingCallList.size() << "\n";
printTaskCallList(WorkingCallList);
}
for (TaskCallListIterator currentTaskCallIter = WorkingCallList.begin();
currentTaskCallIter != WorkingCallList.end();
currentTaskCallIter = WorkingCallList.erase(currentTaskCallIter))
{
TaskCall currentTaskCall = (*currentTaskCallIter);
Task* currentTask = currentTaskCall.TaskToCall;
if (!currentTask)
continue;
TaskType currentTaskType = currentTask->GetType();
if (DebugPrint)
{
std::cout << "\nPlanStep()\nWorkingCallList.size() = " << WorkingCallList.size()
<< "\n";
printTaskCallList(WorkingCallList);
}
std::cout << "TaskType currentTaskType = " << (int)currentTaskType << "\n";
for (TaskCallListIterator currentTaskCallIter = WorkingCallList.begin();
currentTaskCallIter != WorkingCallList.end();
currentTaskCallIter = WorkingCallList.erase(currentTaskCallIter))
switch (currentTaskType)
{
TaskCall currentTaskCall = (*currentTaskCallIter);
Task* currentTask = currentTaskCall.TaskToCall;
if (!currentTask)
continue;
TaskType currentTaskType = currentTask->GetType();
// Prepare arguments based on our current working state & parameters
// TODO: I don't like this
TaskArguments taskArguments = {StacklessState, currentTaskCall.Parameters};
case TaskType::Goal:
{
if (DebugPrint)
std::cout << "Goal\n";
GoalTask* goalTask = currentTask->GetGoal();
if (DebugPrint)
std::cout << "TaskType currentTaskType = " << (int)currentTaskType << "\n";
if (!DecomposeGoalTask(DecompositionStack, goalTask, 0, currentTaskCall.Parameters,
StacklessState))
return Status::Failed_NoPossiblePlan;
switch (currentTaskType)
WorkingCallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulDecompositionStackPush;
}
break;
case TaskType::Primitive:
{
case TaskType::Goal:
if (DebugPrint)
std::cout << "Primitive\n";
PrimitiveTask* primitiveTask = currentTask->GetPrimitive();
if (!primitiveTask->StateMeetsPreconditions(StacklessState,
currentTaskCall.Parameters))
return Status::Failed_NoPossiblePlan;
// If at top level, apply primitive tasks immediately to the stackless state
primitiveTask->ApplyStateChange(StacklessState, currentTaskCall.Parameters);
FinalCallList.push_back(currentTaskCall);
if (BreakOnPrimitiveApply)
{
if (DebugPrint)
std::cout << "Goal\n";
GoalTask* goalTask = currentTask->GetGoal();
if (!DecomposeGoalTask(DecompositionStack, goalTask, 0,
currentTaskCall.Parameters, StacklessState))
return Status::Failed_NoPossiblePlan;
if (BreakOnStackPush)
{
WorkingCallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulDecomposition;
}
// TODO: IMPORTANT! This code will collapse horribly if I don't now start
// processing the stack
WorkingCallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulDecomposition;
}
break;
case TaskType::Primitive:
{
if (DebugPrint)
std::cout << "Primitive\n";
PrimitiveTask* primitiveTask = currentTask->GetPrimitive();
if (!primitiveTask->StateMeetsPreconditions(taskArguments))
return Status::Failed_NoPossiblePlan;
// If at top level, apply primitive tasks immediately to the stackless state
primitiveTask->ApplyStateChange(taskArguments);
StacklessState = taskArguments.worldState;
FinalCallList.push_back(currentTaskCall);
if (BreakOnPrimitiveApply)
{
WorkingCallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulPrimitive;
}
// Keep processing tasks otherwise
return Status::Running_SuccessfulPrimitive;
}
break;
case TaskType::Compound:
{
if (DebugPrint)
std::cout << "Compound\n";
CompoundTask* compoundTask = currentTask->GetCompound();
if (!DecomposeCompoundTask(compoundDecompositions, compoundTask, taskArguments))
return Status::Failed_NoPossiblePlan;
// Keep processing tasks otherwise
}
break;
case TaskType::Compound:
{
if (DebugPrint)
std::cout << "Compound\n";
CompoundTask* compoundTask = currentTask->GetCompound();
currentTaskCallIter = WorkingCallList.erase(currentTaskCallIter);
// we need to push our decomposition to our call list, but we're iterating on
// it. By pushing our decomposition to compoundDecompositions in
// DecomposeCompoundTask(), we'll then break out of the loop and tack it on there
if (!DecomposeCompoundTask(compoundDecompositions, compoundTask, StacklessState,
currentTaskCall.Parameters))
return Status::Failed_NoPossiblePlan;
// we need to push our decomposition to our call list, but we're iterating on
// it. By pushing our decomposition to compoundDecompositions, we break out of
// the loop and tack it on there
}
break;
default:
return Status::Failed_BadData;
currentTaskCallIter = WorkingCallList.erase(currentTaskCallIter);
}
break;
default:
return Status::Failed_BadData;
}
if (compoundDecompositions.size())
if (compoundDecompositions.size())
{
if (DebugPrint)
{
if (DebugPrint)
{
std::cout << "compoundDecompositions.size() = " << compoundDecompositions.size()
<< "\n";
}
WorkingCallList.insert(WorkingCallList.begin(), compoundDecompositions.begin(),
compoundDecompositions.end());
// We have to break here because we are adding things to the list we're iterating
// on; We'll process the tasks next Step
// break; // UPDATE: Eventually we'll need a way to start over the loop if the
// caller wants whole plan in one call
if (DebugPrint)
std::cout << "PlanStep Done\n";
return Status::Running_SuccessfulDecomposition;
std::cout << "compoundDecompositions.size() = " << compoundDecompositions.size()
<< "\n";
}
WorkingCallList.insert(WorkingCallList.begin(), compoundDecompositions.begin(),
compoundDecompositions.end());
if (DebugPrint)
std::cout << "Loop Done\n";
std::cout << "PlanStep Done\n";
// We have to break here because we are adding things to the list we're iterating
// on; We'll process the tasks next Step
return Status::Running_SuccessfulDecomposition;
}
}
else
{
// Remember: If goal fails to decompose and goal is bottom of stack, fail
GoalDecompositionStack::iterator currentStackFrameIter = DecompositionStack.end() - 1;
GoalDecomposition& currentStackFrame = *currentStackFrameIter;
if (DebugPrint)
{
std::cout << "\nPlanStep()\ncurrentStackFrame.CallList.size() = "
<< currentStackFrame.CallList.size() << "\n";
printTaskCallList(currentStackFrame.CallList);
std::cout << "Loop Done\n";
}
std::cout << "Stack Depth: ";
for (unsigned int i = 0; i < DecompositionStack.size(); i++)
std::cout << "=";
std::cout << "\n";
return Status::PlanComplete;
}
Planner::Status Planner::PlanStep_StackFrame(void)
{
// Remember: If goal fails to decompose and goal is bottom of stack, fail
GoalDecompositionStack::iterator currentStackFrameIter = DecompositionStack.end() - 1;
GoalDecomposition& currentStackFrame = *currentStackFrameIter;
if (DebugPrint)
{
std::cout << "\nPlanStep()\ncurrentStackFrame.CallList.size() = "
<< currentStackFrame.CallList.size() << "\n";
printTaskCallList(currentStackFrame.CallList);
std::cout << "Stack Depth: ";
for (unsigned int i = 0; i < DecompositionStack.size(); i++)
std::cout << "=";
std::cout << "\n";
{
std::cout << "----Fullstack working lists\n";
std::cout << "[0]\n";
printTaskCallList(WorkingCallList);
int i = 1;
for (GoalDecomposition& stackFrame : DecompositionStack)
{
std::cout << "----Fullstack working lists\n";
std::cout << "[0]\n";
printTaskCallList(WorkingCallList);
int i = 1;
for (GoalDecomposition& stackFrame : DecompositionStack)
{
std::cout << "[" << i++ << "]\n";
printTaskCallList(stackFrame.CallList);
}
std::cout << "----\n";
std::cout << "[" << i++ << "]\n";
printTaskCallList(stackFrame.CallList);
}
std::cout << "----\n";
}
}
TaskCallList compoundDecompositions;
bool methodFailed = false;
for (TaskCallListIterator currentTaskCallIter = currentStackFrame.CallList.begin();
currentTaskCallIter != currentStackFrame.CallList.end();
currentTaskCallIter = currentStackFrame.CallList.erase(currentTaskCallIter))
{
TaskCall currentTaskCall = (*currentTaskCallIter);
Task* currentTask = currentTaskCall.TaskToCall;
if (!currentTask)
continue;
TaskType currentTaskType = currentTask->GetType();
TaskCallList compoundDecompositions;
bool methodFailed = false;
// Prepare arguments based on our current working state & parameters
// TODO: I don't like this
TaskArguments taskArguments = {currentStackFrame.WorkingState,
currentTaskCall.Parameters};
for (TaskCallListIterator currentTaskCallIter = currentStackFrame.CallList.begin();
currentTaskCallIter != currentStackFrame.CallList.end();
currentTaskCallIter = currentStackFrame.CallList.erase(currentTaskCallIter))
{
TaskCall currentTaskCall = (*currentTaskCallIter);
Task* currentTask = currentTaskCall.TaskToCall;
if (!currentTask)
continue;
TaskType currentTaskType = currentTask->GetType();
if (DebugPrint)
std::cout << "TaskType currentTaskType = " << (int)currentTaskType << "\n";
if (DebugPrint)
std::cout << "TaskType currentTaskType = " << (int)currentTaskType << "\n";
switch (currentTaskType)
switch (currentTaskType)
{
case TaskType::Goal:
{
case TaskType::Goal:
{
if (DebugPrint)
std::cout << "Goal\n";
GoalTask* goalTask = currentTask->GetGoal();
if (DebugPrint)
std::cout << "Goal\n";
GoalTask* goalTask = currentTask->GetGoal();
// TODO erase ahead of time because fuck
// Strange things are afoot when we push to stack
currentStackFrame.CallList.erase(currentTaskCallIter);
// TODO erase ahead of time because fuck
// Strange things are afoot when we push to stack
currentStackFrame.CallList.erase(currentTaskCallIter);
// Perform the decomposition. This function only fails if the method at method
// index doesn't exist. This means that the goal task has no alternative options
if (!DecomposeGoalTask(DecompositionStack, goalTask, 0,
currentTaskCall.Parameters,
currentStackFrame.WorkingState))
{
methodFailed = true;
break;
}
// This code is wrong. I'm not sure why.
// Pushing to the stack invalidates our currentStackFrame reference; update it
// What the actual fuck (updating the ref doesn't fix the fucking thing)
/*std::cout << "Ref updating from " << &currentStackFrame << " to "
<< &(*(DecompositionStack.end() - 1)) << "\n";
currentStackFrameIter = DecompositionStack.end() - 1;
currentStackFrame = *currentStackFrameIter;*/
if (BreakOnStackPush)
{
// currentStackFrame.CallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulDecomposition;
}
// TODO: IMPORTANT! This code will collapse horribly if I don't now start
// processing the stack
// currentStackFrame.CallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulDecomposition;
}
break;
case TaskType::Primitive:
// Perform the decomposition. This function only fails if the method at method
// index doesn't exist. This means that the goal task has no alternative options
if (!DecomposeGoalTask(DecompositionStack, goalTask, 0, currentTaskCall.Parameters,
currentStackFrame.WorkingState))
{
if (DebugPrint)
std::cout << "Primitive\n";
PrimitiveTask* primitiveTask = currentTask->GetPrimitive();
if (!primitiveTask->StateMeetsPreconditions(taskArguments))
{
methodFailed = true;
break;
}
primitiveTask->ApplyStateChange(taskArguments);
currentStackFrame.WorkingState = taskArguments.worldState;
currentStackFrame.FinalCallList.push_back(currentTaskCall);
if (BreakOnPrimitiveApply)
{
currentStackFrame.CallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulPrimitive;
}
// Keep processing tasks otherwise
methodFailed = true;
break;
}
break;
case TaskType::Compound:
{
if (DebugPrint)
std::cout << "Compound\n";
CompoundTask* compoundTask = currentTask->GetCompound();
if (!DecomposeCompoundTask(compoundDecompositions, compoundTask, taskArguments))
{
methodFailed = true;
break;
}
// This code is wrong. I'm not sure why.
// Pushing to the stack invalidates our currentStackFrame reference; update it
// What the actual fuck (updating the ref doesn't fix the fucking thing)
/*std::cout << "Ref updating from " << &currentStackFrame << " to "
<< &(*(DecompositionStack.end() - 1)) << "\n";
currentStackFrameIter = DecompositionStack.end() - 1;
currentStackFrame = *currentStackFrameIter;*/
currentTaskCallIter = currentStackFrame.CallList.erase(currentTaskCallIter);
// we need to push our decomposition to our call list, but we're iterating on
// it. By pushing our decomposition to compoundDecompositions, we break out of
// the loop and tack it on there
}
break;
default:
return Status::Failed_BadData;
return Status::Running_SuccessfulDecompositionStackPush;
}
if (methodFailed)
break;
case TaskType::Primitive:
{
if (DebugPrint)
std::cout << "Method failed decomposition\n";
// Clear stack frame
currentStackFrame.CallList.clear();
currentStackFrame.FinalCallList.clear();
// currentStackFrame.Parameters.clear(); // Little weird (implicit assumption that
// GoalTask uses exact parameters)
currentStackFrame.WorkingState = currentStackFrame.InitialState;
// Try the next method
currentStackFrame.MethodIndex++;
if (!currentStackFrame.DecomposedGoalTask->DecomposeMethodAtIndex(
currentStackFrame.CallList, currentStackFrame.MethodIndex,
currentStackFrame.Parameters))
std::cout << "Primitive\n";
PrimitiveTask* primitiveTask = currentTask->GetPrimitive();
if (!primitiveTask->StateMeetsPreconditions(currentStackFrame.WorkingState,
currentTaskCall.Parameters))
{
DecompositionStack.pop_back();
return Status::Running_FailedGoalDecomposition;
methodFailed = true;
break;
}
// TODO: make this optional return
return Status::Running_FailedMethodDecomposition;
}
primitiveTask->ApplyStateChange(currentStackFrame.WorkingState,
currentTaskCall.Parameters);
if (compoundDecompositions.size())
currentStackFrame.FinalCallList.push_back(currentTaskCall);
if (BreakOnPrimitiveApply)
{
currentStackFrame.CallList.erase(currentTaskCallIter);
return Status::Running_SuccessfulPrimitive;
}
// Keep processing tasks otherwise
}
break;
case TaskType::Compound:
{
if (DebugPrint)
std::cout << "Compound\n";
CompoundTask* compoundTask = currentTask->GetCompound();
if (!DecomposeCompoundTask(compoundDecompositions, compoundTask,
currentStackFrame.WorkingState,
currentTaskCall.Parameters))
{
std::cout << "compoundDecompositions.size() = " << compoundDecompositions.size()
<< "\n";
std::cout << "currentStackFrame.CallList.size() = "
<< currentStackFrame.CallList.size() << "\n";
std::cout << "Decomposition:\n";
printTaskCallList(compoundDecompositions);
methodFailed = true;
break;
}
currentStackFrame.CallList.insert(currentStackFrame.CallList.begin(),
compoundDecompositions.begin(),
compoundDecompositions.end());
// We have to break here because we are adding things to the list we're iterating
// on; We'll process the tasks next Step
// break; // UPDATE: TODO: Eventually we'll need a way to start over the loop if the
// caller wants whole plan in one call
if (DebugPrint)
std::cout << "PlanStep Done\n";
return Status::Running_SuccessfulDecomposition;
}
if (DebugPrint)
std::cout << "Loop Done\n";
currentTaskCallIter = currentStackFrame.CallList.erase(currentTaskCallIter);
// we need to push our decomposition to our call list, but we're iterating on
// it. By pushing our decomposition to compoundDecompositions, we break out of
// the loop and tack it on there
}
break;
default:
return Status::Failed_BadData;
}
// Finished processing this stack frame
if (currentStackFrame.CallList.empty() && !currentStackFrame.FinalCallList.empty())
if (methodFailed)
{
bool onlyStackFrame = DecompositionStack.size() == 1;
TaskCallList* parentFinalCallList = nullptr;
WorldState* parentWorkingState = nullptr;
// If this is the only frame on the stack, its call list and working state goes to the
// plan, else, the previous stack
if (onlyStackFrame)
if (DebugPrint)
std::cout << "Method failed decomposition\n";
// Clear stack frame
currentStackFrame.CallList.clear();
currentStackFrame.FinalCallList.clear();
currentStackFrame.WorkingState = currentStackFrame.InitialState;
// Try the next method
currentStackFrame.MethodIndex++;
if (!currentStackFrame.DecomposedGoalTask->DecomposeMethodAtIndex(
currentStackFrame.CallList, currentStackFrame.MethodIndex,
currentStackFrame.Parameters))
{
parentFinalCallList = &FinalCallList;
parentWorkingState = &StacklessState;
DecompositionStack.pop_back();
return Status::Running_FailedGoalDecomposition;
}
else
{
GoalDecomposition& previousStackFrame = *(currentStackFrameIter - 1);
parentFinalCallList = &previousStackFrame.FinalCallList;
parentWorkingState = &previousStackFrame.WorkingState;
}
// We have failed to decompose the previous method, but successfully got the next
// method. Return to let the Planner know what just happened
return Status::Running_FailedMethodDecomposition;
}
if (compoundDecompositions.size())
{
if (DebugPrint)
{
std::cout << "Collapsing stack frame. Adding List:\n";
printTaskCallList(currentStackFrame.FinalCallList);
std::cout << "To parent:\n";
printTaskCallList(*parentFinalCallList);
std::cout << "compoundDecompositions.size() = " << compoundDecompositions.size()
<< "\n";
std::cout << "currentStackFrame.CallList.size() = "
<< currentStackFrame.CallList.size() << "\n";
std::cout << "Decomposition:\n";
printTaskCallList(compoundDecompositions);
}
// Should this be appended?
parentFinalCallList->insert(parentFinalCallList->end(),
currentStackFrame.FinalCallList.begin(),
currentStackFrame.FinalCallList.end());
currentStackFrame.CallList.insert(currentStackFrame.CallList.begin(),
compoundDecompositions.begin(),
compoundDecompositions.end());
if (DebugPrint)
std::cout << "PlanStep Done\n";
*parentWorkingState = currentStackFrame.WorkingState;
// We have to break here because we are adding things to the list we're iterating
// on; We'll process the tasks next Step
return Status::Running_SuccessfulDecomposition;
}
DecompositionStack.pop_back();
if (DebugPrint)
std::cout << "Loop Done\n";
}
if (DebugPrint)
std::cout << "Frame Done\n";
// Finished processing this stack frame
if (currentStackFrame.CallList.empty() && !currentStackFrame.FinalCallList.empty())
{
bool onlyStackFrame = DecompositionStack.size() == 1;
TaskCallList* parentFinalCallList = nullptr;
WorldState* parentWorkingState = nullptr;
if (BreakOnStackPop)
return Status::Running_SuccessfulDecomposition;
// If this is the only frame on the stack, its call list and working state goes to the
// plan, else, the previous stack
if (onlyStackFrame)
{
parentFinalCallList = &FinalCallList;
parentWorkingState = &StacklessState;
}
else
{
GoalDecomposition& previousStackFrame = *(currentStackFrameIter - 1);
// TODO: Continue processing the stack
return Status::Running_SuccessfulDecomposition;
parentFinalCallList = &previousStackFrame.FinalCallList;
parentWorkingState = &previousStackFrame.WorkingState;
}
if (DebugPrint)
{
std::cout << "Collapsing stack frame. Adding List:\n";
printTaskCallList(currentStackFrame.FinalCallList);
std::cout << "To parent:\n";
printTaskCallList(*parentFinalCallList);
}
parentFinalCallList->insert(parentFinalCallList->end(),
currentStackFrame.FinalCallList.begin(),
currentStackFrame.FinalCallList.end());
*parentWorkingState = currentStackFrame.WorkingState;
DecompositionStack.pop_back();
if (DebugPrint)
std::cout << "Frame Done\n";
return Status::Running_SuccessfulDecompositionStackPop;
}
return Status::PlanComplete;
}
// TODO: Pool various task lists?
// TODO: Pull more things out into functions, if possible. It's bad that whenever I make a change to
// something I have to change it in two places
Planner::Status Planner::PlanStep(void)
{
Status status = Status::Failed_NoPossiblePlan;
// Continue stepping the plan until a non-running state is complete or a status is returned
// which matches the user-specified break conditions
do
{
if (DecompositionStack.empty())
status = PlanStep_BottomLevel();
else
status = PlanStep_StackFrame();
// The user can decided how much planning happens in a single step; conditionally stop
// stepping here based on their settings
if ((BreakOnCompoundDecomposition && status == Status::Running_SuccessfulDecomposition) ||
(BreakOnPrimitiveApply && status == Status::Running_SuccessfulPrimitive) ||
(BreakOnStackAction && (status == Status::Running_SuccessfulDecompositionStackPush ||
status == Status::Running_SuccessfulDecompositionStackPop ||
status == Status::Running_FailedGoalDecomposition)) ||
(BreakOnFailedDecomposition && (status == Status::Running_FailedGoalDecomposition ||
status == Status::Running_FailedMethodDecomposition)))
return status;
} while (status >= Status::Running_EnumBegin && status <= Status::Running_EnumEnd);
return status;
}
}

21
src/ai/htn/HTNPlanner.hpp

@ -29,6 +29,8 @@ struct GoalDecomposition
typedef std::vector<GoalDecomposition> GoalDecompositionStack;
// TODO: Either make planner allow making multiple plans with the same instance, or make it clear
// that it is for a single plan only
class Planner
{
public:
@ -42,15 +44,17 @@ public:
//
// Settings to tweak how long you want a single PlanStep() to be
//
// Break immediately after a new method has been chosen for a goal
bool BreakOnStackPush = true;
// Break immediately after a method has failed to be decomposed
bool BreakOnStackPop = true;
// Break whenever a stack action has occurred (a goal has been decomposed, a method has finished
// decomposition, or a method failed to decompose)
bool BreakOnStackAction = true;
// Break immediately after a compound task has been decomposed
bool BreakOnCompoundDecomposition = false;
// Break immediately after a primitive task has been applied
bool BreakOnPrimitiveApply = false;
// Break whenever a goal or method failed to decompose
bool BreakOnFailedDecomposition = false;
// Print various details about the status of the stack etc.
bool DebugPrint = false;
enum class Status
@ -60,11 +64,17 @@ public:
Failed_NoPossiblePlan,
Failed_NoTasks,
Running_EnumBegin,
Running_SuccessfulDecomposition,
Running_SuccessfulDecompositionStackPush,
Running_SuccessfulDecompositionStackPop,
Running_SuccessfulPrimitive,
Running_FailedMethodDecomposition,
Running_FailedGoalDecomposition,
Running_EnumEnd,
// Done
PlanComplete
};
@ -79,5 +89,8 @@ private:
// Copy of InitialCallList that Planner can fuck with
TaskCallList WorkingCallList;
Status PlanStep_StackFrame(void);
Status PlanStep_BottomLevel(void);
};
};

2
src/ai/htn/HTNTasks.cpp

@ -11,7 +11,7 @@ int GoalTask::GetNumMethods(void)
}
bool GoalTask::DecomposeMethodAtIndex(TaskCallList& decomposition, int index,
ParameterList& parameters)
const ParameterList& parameters)
{
Task* methodToDecompose = GetMethodAtIndex(index);
if (!methodToDecompose)

18
src/ai/htn/HTNTasks.hpp

@ -33,7 +33,8 @@ public:
int GetNumMethods(void);
Task* GetMethodAtIndex(int index);
bool DecomposeMethodAtIndex(TaskCallList& decomposition, int index, ParameterList& parameters);
bool DecomposeMethodAtIndex(TaskCallList& decomposition, int index,
const ParameterList& parameters);
void SetMethods(TaskList* newMethods);
};
@ -43,8 +44,10 @@ class CompoundTask
public:
CompoundTask(void) = default;
virtual ~CompoundTask(void) = default;
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0;
virtual bool Decompose(TaskCallList& taskCallList, const TaskArguments& arguments) = 0;
virtual bool StateMeetsPreconditions(const WorldState& state,
const ParameterList& parameters) const = 0;
virtual bool Decompose(TaskCallList& taskCallList, const WorldState& state,
const ParameterList& parameters) = 0;
};
class PrimitiveTask
@ -52,10 +55,13 @@ class PrimitiveTask
public:
PrimitiveTask(void) = default;
virtual ~PrimitiveTask(void) = default;
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0;
virtual void ApplyStateChange(TaskArguments& arguments) = 0;
virtual bool StateMeetsPreconditions(const WorldState& state,
const ParameterList& parameters) const = 0;
virtual void ApplyStateChange(WorldState& state, const ParameterList& parameters) = 0;
// Returns whether or not starting the task was successful (NOT whether the task completed)
virtual bool Execute(const TaskArguments& arguments) = 0;
// Execution should (when completed etc.) modify the world state the same as ApplyStateChange
// would. Call that function for extra safety
virtual bool Execute(WorldState& state, const ParameterList& parameters) = 0;
};
enum class TaskType

6
src/ai/htn/HTNTypes.hpp

@ -40,10 +40,4 @@ typedef std::vector<Parameter>::reverse_iterator ParameterListReverseIterator;
// The arguments passed to most all Task functions
typedef int WorldState; // TODO
// TODO: Should this struct exist, especially considering the user can't pass it to planner anyways?
struct TaskArguments
{
WorldState worldState;
ParameterList Parameters;
};
}

84
src/unitTesting/HTN_test.cpp

@ -13,20 +13,21 @@ public:
AlwaysFailPrimitiveTask(void) = default;
virtual ~AlwaysFailPrimitiveTask(void) = default;
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
virtual bool StateMeetsPreconditions(const Htn::WorldState& state,
const Htn::ParameterList& parameters) const
{
std::cout << "\tStateMeetsPreconditions AlwaysFailPrimitiveTask\n";
return false;
}
virtual void ApplyStateChange(Htn::TaskArguments& arguments)
virtual void ApplyStateChange(Htn::WorldState& state, const Htn::ParameterList& parameters)
{
std::cout << "\tApplyStateChange AlwaysFailPrimitiveTask\n";
}
virtual bool Execute(const Htn::TaskArguments& args)
virtual bool Execute(Htn::WorldState& state, const Htn::ParameterList& parameters)
{
std::cout << "\texecute AlwaysFailPrimitiveTask: " << args.Parameters[0].IntValue << "\n";
std::cout << "\texecute AlwaysFailPrimitiveTask: " << parameters[0].IntValue << "\n";
return false;
}
};
@ -37,22 +38,22 @@ public:
RequiresStatePrimitiveTask(void) = default;
virtual ~RequiresStatePrimitiveTask(void) = default;
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
virtual bool StateMeetsPreconditions(const Htn::WorldState& state,
const Htn::ParameterList& parameters) const
{
std::cout << "\tStateMeetsPreconditions RequiresStatePrimitiveTask state = "
<< args.worldState << "\n";
return args.worldState == 1;
std::cout << "\tStateMeetsPreconditions RequiresStatePrimitiveTask state = " << state
<< "\n";
return state == 1;
}
virtual void ApplyStateChange(Htn::TaskArguments& arguments)
virtual void ApplyStateChange(Htn::WorldState& state, const Htn::ParameterList& parameters)
{
std::cout << "\tApplyStateChange RequiresStatePrimitiveTask\n";
}
virtual bool Execute(const Htn::TaskArguments& args)
virtual bool Execute(Htn::WorldState& state, const Htn::ParameterList& parameters)
{
std::cout << "\texecute RequiresStatePrimitiveTask: " << args.Parameters[0].IntValue
<< "\n";
std::cout << "\texecute RequiresStatePrimitiveTask: " << parameters[0].IntValue << "\n";
return true;
}
};
@ -63,21 +64,22 @@ public:
TestPrimitiveTask(void) = default;
virtual ~TestPrimitiveTask(void) = default;
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
virtual bool StateMeetsPreconditions(const Htn::WorldState& state,
const Htn::ParameterList& parameters) const
{
std::cout << "\tStateMeetsPreconditions TestPrimitiveTask\n";
return true;
}
virtual void ApplyStateChange(Htn::TaskArguments& args)
virtual void ApplyStateChange(Htn::WorldState& state, const Htn::ParameterList& parameters)
{
std::cout << "\tApplyStateChange TestPrimitiveTask\n";
args.worldState = 1;
state = 1;
}
virtual bool Execute(const Htn::TaskArguments& args)
virtual bool Execute(Htn::WorldState& state, const Htn::ParameterList& parameters)
{
std::cout << "\texecute TestPrimitiveTask: " << args.Parameters[0].IntValue << "\n";
std::cout << "\texecute TestPrimitiveTask: " << parameters[0].IntValue << "\n";
return true;
}
};
@ -89,18 +91,20 @@ public:
virtual ~TestCompoundTaskA(void) = default;
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
virtual bool StateMeetsPreconditions(const Htn::WorldState& state,
const Htn::ParameterList& parameters) const
{
std::cout << "\tStateMeetsPreconditions TestCompoundTaskA\n";
return true;
}
virtual bool Decompose(Htn::TaskCallList& taskCallList, const Htn::TaskArguments& args)
virtual bool Decompose(Htn::TaskCallList& taskCallList, const Htn::WorldState& state,
const Htn::ParameterList& parameters)
{
static TestPrimitiveTask testPrimitiveTask;
static Htn::Task primitiveTask(&testPrimitiveTask);
std::cout << "\tDecompose TestCompoundTaskA: " << args.Parameters[0].IntValue << "\n";
Htn::TaskCall taskCall = {&primitiveTask, args.Parameters};
std::cout << "\tDecompose TestCompoundTaskA: " << parameters[0].IntValue << "\n";
Htn::TaskCall taskCall = {&primitiveTask, parameters};
taskCallList.push_back(taskCall);
return true;
}
@ -147,9 +151,8 @@ TEST_CASE("Hierarchical Task Networks Planner")
Htn::Planner testPlan;
testPlan.InitialCallList.push_back(taskCall);
testPlan.InitialCallList.push_back(taskCall);
Htn::TaskArguments args;
args.Parameters = params;
testCompoundTaskAA.Decompose(testPlan.InitialCallList, args);
Htn::WorldState nullState;
testCompoundTaskAA.Decompose(testPlan.InitialCallList, nullState, params);
Htn::Planner::Status status;
for (int i = 0; i < 10; i++)
@ -292,4 +295,37 @@ TEST_CASE("Hierarchical Task Networks Planner")
printTaskCallList(testPlan.FinalCallList);
std::cout << "\n\n";
}
SECTION("Test user-specified early break settings")
{
std::cout << "TEST: User-specified early break settings\n\n";
Htn::Planner testPlan;
testPlan.InitialCallList.push_back(nestedGoalTaskCall);
testPlan.InitialCallList.push_back(requiresStateTaskCall);
testPlan.State = 0;
testPlan.BreakOnStackAction = false;
testPlan.BreakOnCompoundDecomposition = false;
testPlan.BreakOnPrimitiveApply = false;
testPlan.BreakOnFailedDecomposition = false;
int numIterations = 0;
Htn::Planner::Status status;
for (int i = 0; i < 12; i++)
{
numIterations++;
status = testPlan.PlanStep();
std::cout << "[" << i << "] Returned Status " << (int)status << "\n";
if (status == Htn::Planner::Status::PlanComplete ||
status == Htn::Planner::Status::Failed_NoPossiblePlan)
break;
}
REQUIRE(status == Htn::Planner::Status::PlanComplete);
REQUIRE(testPlan.FinalCallList.size() == 2);
REQUIRE(numIterations == 1);
std::cout << "\n\nFinal Plan length: " << testPlan.FinalCallList.size() << "\n";
printTaskCallList(testPlan.FinalCallList);
std::cout << "\n\n";
}
}
Loading…
Cancel
Save