Browse Source

HTN Planner now sort of working

Nested goals still do not work and I have not tested very extensively. The code is awful and will need a big cleanup pass once it's fully working.
combatComponentRefactor
Macoy Madson 6 years ago
parent
commit
3bd9c77824
  1. 5
      Jamrules
  2. 3
      src/ai/Jamfile
  3. 559
      src/ai/htn/HTNPlanner.cpp
  4. 82
      src/ai/htn/HTNPlanner.hpp
  5. 38
      src/ai/htn/HTNTasks.cpp
  6. 78
      src/ai/htn/HTNTasks.hpp
  7. 2
      src/ai/htn/HTNTypes.hpp
  8. 17
      src/project/galavantSublime/galavant.sublime-project
  9. 125
      src/unitTesting/HTN_test.cpp

5
Jamrules

@ -8,8 +8,9 @@ LINK = g++ ;
# fPIC = position independent code. This is so the lib can be linked in other libs/executables
# g = debug symbols
# lstdc++ = standard library
C++FLAGS = -std=c++11 -fPIC -g -Wall -Wextra ;
OBJECTC++FLAGS = -std=c++11 -lstdc++ ;
# Og = compile without optimizations for debugging
C++FLAGS = -std=c++11 -fPIC -g -Og -Wall -Wextra ;
OBJECTC++FLAGS = -std=c++11 -lstdc++ -g -Og ;
HDRS = thirdParty/flatbuffers/include ;

3
src/ai/Jamfile

@ -1,6 +1,5 @@
SubDir . src ai ;
Library libGalaAi : htn/HTNTasks.cpp ;
#Library libGalaAi : htn/HTNTasks.cpp htn/HTNPlanner.cpp ;
Library libGalaAi : htn/HTNTasks.cpp htn/HTNPlanner.cpp ;
MakeLocate libGalaAi.a : lib ;

559
src/ai/htn/HTNPlanner.cpp

@ -1,164 +1,111 @@
#include "HTNPlanner.hpp"
#include "HTNTypes.hpp"
#include "HTNTasks.hpp"
#include <iostream>
namespace Htn
{
bool DecomposeGoalTask(TaskList& taskList, Task* task, const TaskArguments& taskArguments)
/*bool DecomposeGoalTask(TaskList& taskList, Task* task, const TaskArguments& taskArguments)
{
GoalTask* goalTask = static_cast<GoalTask*>(task);
GoalTask* goalTask = task->Goal;
return false; // TODO
}
bool DecomposeCompoundTask(TaskList& taskList, Task* task, const TaskArguments& taskArguments)
{
CompoundTask* compoundTask = static_cast<CompoundTask*>(task);
CompoundTask* compoundTask = task->Compound;
if (compoundTask->StateMeetsPreconditions(taskArguments))
// Decomposition is determined by the task itself
return compoundTask->Decompose(taskList, taskArguments);
return false;
}
struct TaskCall
{
Task* TaskToCall;
ParameterList Parameters;
};
typedef std::vector<TaskCall> TaskCallList;
typedef TaskCallList::iterator TaskCallListIterator;
struct GoalDecomposition
{
// The GoalTask this decomposition came from
GoalTask* DecomposedGoalTask;
// The index to the Method used for the current decomposition
int MethodIndex;
// The tasks themselves created by the Method
TaskCallList CallList;
// The state and parameters at the time this decomposition took place
WorldState State;
ParameterList Parameters;
};
typedef std::vector<GoalDecomposition> GoalDecompositionStack;
enum class PlanStepStatus
{
Failed_BadData = 0,
Failed_NoPossiblePlan,
SuccessfulDecomposition,
SuccessfulPrimitive,
NoTasks,
PlanComplete
};
struct PlanState
{
WorldState State;
TaskCallList InitialCallList;
// Filled out by PlanStep(); Should only be used once PlanStep returns PlanComplete
TaskCallList FinalCallList;
//
// 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 immediately after a compound task has been decomposed
bool BreakOnCompoundDecomposition = false;
bool BreakOnPrimitiveApply = false;
//
// Used by PlanStep() only
//
GoalDecompositionStack DecompositionStack;
// Used for when all goals have been decomposed to manage mutable state
WorldState StacklessState;
// Copy of InitialCallList that PlanState can fuck with
TaskCallList WorkingCallList;
};
}*/
PlanStepStatus PlanStep(PlanState& PlanState)
PlanStepStatus PlanStep(PlanState& planState)
{
if (InitialCallList.empty())
return PlanStepStatus::NoTasks;
else if (PlanState.WorkingCallList.empty())
PlanState.WorkingCallList = InitialCallList;
if (PlanState.DecompositionStack.empty())
{
TaskCallList compoundDecompositions;
// 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
for (TaskCallListIterator currentTaskCallIter = PlanState.WorkingCallList.begin();
currentTaskCallIter != PlanState.WorkingCallList.end();
currentTaskCallIter = PlanState.WorkingCallList.erase(currentTaskCallIter);)
// 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 (planState.DecompositionStack.empty())
{
if (planState.InitialCallList.empty())
return PlanStepStatus::NoTasks;
else if (planState.WorkingCallList.empty() && planState.FinalCallList.empty())
planState.WorkingCallList = planState.InitialCallList;
else if (planState.WorkingCallList.empty() && !planState.FinalCallList.empty())
return PlanStepStatus::PlanComplete;
TaskCallList compoundDecompositions;
std::cout << "\nPlanStep()\nplanState.WorkingCallList.size() = " << planState.WorkingCallList.size() << "\n";
for (TaskCallListIterator currentTaskCallIter = planState.WorkingCallList.begin();
currentTaskCallIter != planState.WorkingCallList.end();
currentTaskCallIter = planState.WorkingCallList.erase(currentTaskCallIter), std::cout << "Erased " << "planState.WorkingCallList.size() = " << planState.WorkingCallList.size() << "\n")
{
TaskCall currentTaskCall = (*currentTaskCallIter)Task* currentTask =
currentTaskCall.TaskToCall;
TaskCall currentTaskCall = (*currentTaskCallIter);
Task* currentTask = currentTaskCall.TaskToCall;
if (!currentTask)
continue;
TaskType currentTaskType = currentTask->GetType();
TaskArguments taskArguments = {PlanState.StacklessState, currentTaskCall.Parameters};
// NullEntity = TODO
TaskArguments taskArguments = {0, planState.StacklessState, currentTaskCall.Parameters};
std::cout << "TaskType currentTaskType = " << (int)currentTaskType << "\n";
switch (currentTaskType)
{
case TaskType::Goal:
{
GoalTask* goalTask = static_cast<GoalTask*>(currentTask);
std::cout << "Goal\n";
GoalTask* goalTask = currentTask->Goal;
GoalDecomposition decomposition;
decomposition.DecomposedGoalTask = goalTask;
decomposition.MethodIndex = 0; // -- CHANGE for stacked
// How the hell are parameters off of goals used? Fuck
// Well, a method could be restricted to a single Primitive Task or a CompoundTask
decomposition.Parameters = currentTaskCall.Parameters;
decomposition.State = PlanState.StacklessState; // -- CHANGE for stacked
decomposition.InitialState = planState.StacklessState; // -- CHANGE for stacked
// Perform the decomposition
goalTask->DecomposeMethodAtIndex(decomposition.CallList,
decomposition.MethodIndex);
if (!goalTask->DecomposeMethodAtIndex(decomposition.CallList,
decomposition.MethodIndex,
decomposition.Parameters))
return PlanStepStatus::Failed_NoPossiblePlan;
PlanState.Stack.push_back(decomposition);
planState.DecompositionStack.push_back(decomposition);
std::cout << "planState.DecompositionStack.push_back(decomposition);\n";
if (PlanState.BreakOnStackPush)
if (planState.BreakOnStackPush)
{
WorkingCallList.erase(currentTaskCallIter);
planState.WorkingCallList.erase(currentTaskCallIter);
return PlanStepStatus::SuccessfulDecomposition;
}
// TODO: IMPORTANT! This code will collapse horribly if I don't now start
// processing the stack
planState.WorkingCallList.erase(currentTaskCallIter);
return PlanStepStatus::SuccessfulDecomposition;
}
break;
case TaskType::Primitive:
{
PrimitiveTask* primitiveTask = static_cast<PrimitiveTask*>(currentTask);
if (!primitiveTask.StateMeetsPreconditions(taskArguments))
std::cout << "Primitive\n";
PrimitiveTask* primitiveTask = currentTask->Primitive;
if (!primitiveTask->StateMeetsPreconditions(taskArguments))
return PlanStepStatus::Failed_NoPossiblePlan;
primitiveTask->ApplyStateChange(taskArguments) PlanState.StacklessState =
taskArguments.State;
// If at top level, apply primitive tasks immediately to the stackless state
primitiveTask->ApplyStateChange(taskArguments);
planState.StacklessState = taskArguments.worldState;
FinalCallList.push_back(currentTaskCall);
planState.FinalCallList.push_back(currentTaskCall);
if (PlanState.BreakOnPrimitiveApply)
if (planState.BreakOnPrimitiveApply)
{
WorkingCallList.erase(currentTaskCallIter);
planState.WorkingCallList.erase(currentTaskCallIter);
return PlanStepStatus::SuccessfulPrimitive;
}
// Keep processing tasks otherwise
@ -166,11 +113,16 @@ PlanStepStatus PlanStep(PlanState& PlanState)
break;
case TaskType::Compound:
{
CompoundTask* compoundTask = static_cast<CompoundTask*>(currentTask);
std::cout << "Compound\n";
CompoundTask* compoundTask = currentTask->Compound;
if (!compoundTask->StateMeetsPreconditions(taskArguments))
return PlanStepStatus::Failed_NoPossiblePlan;
compoundTask->Decompose(compoundDecompositions, taskArguments);
if (!compoundTask->Decompose(compoundDecompositions, taskArguments))
return PlanStepStatus::Failed_NoPossiblePlan;
currentTaskCallIter = planState.WorkingCallList.erase(currentTaskCallIter);
// we need to push our decomposition to our call list, but we're iterating on
// it. Break out of the loop and tack it on there
}
@ -181,271 +133,212 @@ PlanStepStatus PlanStep(PlanState& PlanState)
if (compoundDecompositions.size())
{
PlanState.WorkingCallList.insert(PlanState.WorkingCallList.begin(),
std::cout << "compoundDecompositions.size() = " << compoundDecompositions.size() << "\n";
planState.WorkingCallList.insert(planState.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;
//break; // UPDATE: Eventually we'll need a way to start over the loop if the caller wants whole plan in one call
std::cout << "PlanStep Done\n";
return PlanStepStatus::SuccessfulDecomposition;
}
std::cout << "Loop Done\n";
}
}
else
{
// Remember: If goal fails to decompose and goal is bottom of stack, fail
}
}
/*
Goal Task // Set Agent StateA and StateB = true
Method1 = Compound1(AgentState and Params from initial call)
Method2 = Compound2
Method3 = Compound3
Compound1
StateMeetsPrecondtion(AgentState, Params from Goal) then
return {primitiveA(paramA, paramB), primitiveB(ParamA, ParamB, ParamC)}
primitiveA
StateMeetsPrecondition(AgentState, paramA, paramB)
then AgentState.StateA = true
primitiveB
StateMeetsPrecondition(AgentState, paramA, paramB, paramC)
then AgentState.StateB = true
PlanStep(stack, taskList)
if not stack
// startup
stack.push(decompose(taskList[0]))
else
decomposition = stack.pop()
allPrimitives = true
for task in decomposition
if task == primitive
task.StateMeetsPrecondition then
modify currentState
// don't break here
else
goto fail
if task == compound
allPrimitives = false
task.StateMeetsPrecondition then
stack.push(decompose(task))
break
else
goto fail
if task == goal
allPrimitives = false
methodIndex = 0
decomposedTasks = task.GetMethodAtIndex(0), currentState
stack.push(decomposition)
break
fail:
GoalDecompositionStack::iterator currentStackFrameIter = planState.DecompositionStack.end() - 1;
GoalDecomposition& currentStackFrame = *currentStackFrameIter;
std::cout << "\nPlanStep()\ncurrentStackFrame.CallList.size() = " << currentStackFrame.CallList.size() << "\n";
*/
/*PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList,
const TaskArguments& taskArguments)
{
if (!stack.empty())
{
// Check to see if the current stack item is all primitive tasks with met preconditions. If
// so, we can add them to the tasklist and walk all the way back up
// TODO: naming
TaskDecomposition currentDecomposition = stack.end();
bool onlyPrimitiveTasksRemain = true;
bool failedToDecompose = false;
for (DecomposeTaskState* currentTaskState : currentDecomposition.Decomposition)
TaskCallList compoundDecompositions;
bool methodFailed = false;
for (TaskCallListIterator currentTaskCallIter = currentStackFrame.CallList.begin();
currentTaskCallIter != currentStackFrame.CallList.end();
currentTaskCallIter = currentStackFrame.CallList.erase(currentTaskCallIter), std::cout << "Erased " << "currentStackFrame.CallList.size() = " << planState.WorkingCallList.size() << "\n")
{
if (!currentTaskState || !currentTaskState->DecomposeTask)
TaskCall currentTaskCall = (*currentTaskCallIter);
Task* currentTask = currentTaskCall.TaskToCall;
if (!currentTask)
continue;
Task* currentTask = currentTaskState->DecomposeTask;
TaskType currentTaskType = currentTask->GetType();
TaskArguments& currentTaskArguments = currentDecomposition->Arguments;
// NullEntity = TODO
TaskArguments taskArguments = {0, currentStackFrame.WorkingState, currentTaskCall.Parameters};
if (currentTaskType == TaskType::Primitive)
{
PrimitiveTask* primitiveTask = static_cast<PrimitiveTask*>(currentTask);
std::cout << "TaskType currentTaskType = " << (int)currentTaskType << "\n";
if (!primitiveTask->StateMeetsPreconditions(currentTaskArguments))
switch (currentTaskType)
{
case TaskType::Goal:
{
failedToDecompose = true;
break;
std::cout << "Goal\n";
GoalTask* goalTask = currentTask->Goal;
GoalDecomposition decomposition;
decomposition.DecomposedGoalTask = goalTask;
decomposition.MethodIndex = 0;
// How the hell are parameters off of goals used? Fuck
// Well, a method could be restricted to a single Primitive Task or a CompoundTask
decomposition.Parameters = currentTaskCall.Parameters;
decomposition.InitialState = currentStackFrame.WorkingState;
// Perform the decomposition
if (!goalTask->DecomposeMethodAtIndex(decomposition.CallList,
decomposition.MethodIndex,
decomposition.Parameters))
return PlanStepStatus::Failed_NoPossiblePlan;
currentStackFrame.CallList.erase(currentTaskCallIter);
planState.DecompositionStack.push_back(decomposition);
std::cout << "planState.DecompositionStack.push_back(decomposition);\n";
// Pushing to the stack invalidates our currentStackFrame reference; update it
// What the actual fuck
std::cout << "Ref updating from " << &currentStackFrame << " to " << &(*currentStackFrameIter) << "\n";
currentStackFrameIter = planState.DecompositionStack.end() - 1;
currentStackFrame = *currentStackFrameIter;
if (planState.BreakOnStackPush)
{
//currentStackFrame.CallList.erase(currentTaskCallIter);
return PlanStepStatus::SuccessfulDecomposition;
}
// TODO: IMPORTANT! This code will collapse horribly if I don't now start
// processing the stack
//currentStackFrame.CallList.erase(currentTaskCallIter);
return PlanStepStatus::SuccessfulDecomposition;
}
break;
case TaskType::Primitive:
{
std::cout << "Primitive\n";
PrimitiveTask* primitiveTask = currentTask->Primitive;
if (!primitiveTask->StateMeetsPreconditions(taskArguments))
{
methodFailed = true;
break;
}
// Modify state
}
else
{
onlyPrimitiveTasksRemain = false;
// TODO
//failedToDecompose = DecomposeTask()
}
}
// TODO: Goal tasks trying new decompositions
if (failedToDecompose)
{
stack.pop_back();
delete currentDecomposition;
return PlanStepStatus::FailedDecomposition;
}
}
else if (!taskList.empty())
{
// No stack; we must be just starting
Task* currentTask = *taskList.data();
if (!currentTask)
return PlanStepStatus::Failed;
primitiveTask->ApplyStateChange(taskArguments);
currentStackFrame.WorkingState = taskArguments.worldState;
TaskType currentTaskType = currentTask->GetType();
currentStackFrame.FinalCallList.push_back(currentTaskCall);
// What if taskList just has primitives? Multiple goals?
if (currentTaskType == TaskType::Goal)
{
GoalTask* goalTask = static_cast<GoalTask*>(currentTask);
int numMethods = goalTask->GetNumMethods();
TaskDecomposition newDecomposition;
newDecomposition.DecomposedTask = currentTask;
newDecomposition.DecompositionIndex = 0;
newDecomposition.Decomposition = nullptr;
// Goal tasks are decomposed via methods. Methods are Tasks which can be used to achieve
// the goal. Goal tasks do not have conditonals - they just blindly pick methods in
// order until they find one which works
for (int methodIndex = newDecomposition.DecompositionIndex;
!newDecomposition.Decomposition && methodIndex < numMethods; methodIndex++)
{
Task* method = goalTask->GetMethodAtIndex(newDecomposition.DecompositionIndex);
if (!method)
continue;
TaskType methodType = method->GetType();
if (methodType == TaskType::GoalTask)
{
newDecomposition.newDecomposition = new TaskList(1);
newDecomposition.Decomposition->push_back(method);
newDecomposition.DecompositionIndex = methodIndex;
}
else if (methodType == TaskType::Primitive)
{
PrimitiveTask* primitiveTask = static_cast<PrimitiveTask*>(method);
if (primitiveTask->StateMeetsPreconditions(taskArguments))
if (planState.BreakOnPrimitiveApply)
{
newDecomposition.newDecomposition = new TaskList(1);
newDecomposition.Decomposition->push_back(method);
newDecomposition.DecompositionIndex = methodIndex;
currentStackFrame.CallList.erase(currentTaskCallIter);
return PlanStepStatus::SuccessfulPrimitive;
}
// Keep processing tasks otherwise
}
else if (methodType == TaskType::CompoundTask)
break;
case TaskType::Compound:
{
CompoundTask* compoundTask = static_cast<CompoundTask*>(method);
if (compoundTask->StateMeetsPreconditions(taskArguments))
std::cout << "Compound\n";
CompoundTask* compoundTask = currentTask->Compound;
if (!compoundTask->StateMeetsPreconditions(taskArguments))
{
newDecomposition.newDecomposition = new TaskList();
compoundTask->Decompose(&newDecomposition.newDecomposition, taskArguments);
newDecomposition.DecompositionIndex = methodIndex;
methodFailed = true;
break;
}
if (!compoundTask->Decompose(compoundDecompositions, taskArguments))
{
methodFailed = true;
break;
}
currentTaskCallIter = currentStackFrame.CallList.erase(currentTaskCallIter);
// we need to push our decomposition to our call list, but we're iterating on
// it. Break out of the loop and tack it on there
}
else
// We got something we don't know how to handle
return PlanStepStatus::Failed;
break;
default:
return PlanStepStatus::Failed_BadData;
}
stack.push_back(newDecomposition);
return PlanStepStatus::SuccessfulDecomposition;
}
}
else
return PlanStepStatus::NoTasks;
}*/
if (methodFailed)
{
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))
{
planState.DecompositionStack.pop_back();
return PlanStepStatus::FailedGoalDecomposition;
}
/*PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList,
const TaskArguments& taskArguments)
{
bool planComplete = true;
// TODO: make this optional return
return PlanStepStatus::FailedMethodDecomposition;
}
// Check to see if there are tasks which need to be decomposed
for (Task* currentTask : taskList)
{
if (!currentTask)
continue;
if (compoundDecompositions.size())
{
std::cout << "compoundDecompositions.size() = " << compoundDecompositions.size() << "\n";
std::cout << "currentStackFrame.CallList.size() = " << currentStackFrame.CallList.size() << "\n";
std::cout << "First task type: " << (int)(*compoundDecompositions.begin()).TaskToCall->GetType() << "\n";
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
std::cout << "PlanStep Done\n";
return PlanStepStatus::SuccessfulDecomposition;
}
TaskType currentTaskType = currentTask->GetType();
std::cout << "Loop Done\n";
// The plan is complete once all Goal and Compound Tasks have been decomposed into Primitive
// Tasks, which we can actually execute
if (currentTaskType != TaskType::Primitive)
{
planComplete = false;
break;
}
}
if (!planComplete)
{
Task* currentTask = *taskList.data();
if (!currentTask)
return PlanStepStatus::Failed;
TaskType currentTaskType = currentTask->GetType();
if (currentTaskType == TaskType::Goal)
// Finished processing this stack frame
if (currentStackFrame.CallList.empty() && !currentStackFrame.FinalCallList.empty())
{
GoalTask* goalTask = static_cast<GoalTask*>(currentTask);
bool onlyStackFrame = planState.DecompositionStack.size() == 1;
TaskCallList* parentFinalCallList = nullptr;
WorldState* parentWorkingState = nullptr;
}
}
else
return PlanStepStatus::PlanComplete;
}*/
// 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 = &planState.FinalCallList;
parentWorkingState = &planState.StacklessState;
}
else
{
GoalDecomposition& previousStackFrame = *(currentStackFrameIter - 1);
/*// Take the first task in the list and decompose it or make sure it's a valid part of the plan
PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList,
const TaskArguments& taskArguments)
{
if (taskList.size())
{
Task* currentTask = *taskList.data();
TaskType taskType = currentTask->GetType();
parentFinalCallList = &previousStackFrame.FinalCallList;
parentWorkingState = &previousStackFrame.WorkingState;
}
// If it's a goal, we want to find a compound task that will accomplish it
if (taskType == TaskType::Goal)
{
if (DecomposeGoalTask(taskList, currentTask, taskArguments))
return PlanStepStatus::SuccessfulDecomposition;
else
return PlanStepStatus::FailedDecomposition;
}
// Should this be appended?
parentFinalCallList->insert(parentFinalCallList->end(),
currentStackFrame.FinalCallList.begin(),
currentStackFrame.FinalCallList.end());
// If it's a compound task, we want to decompose it
if (taskType == TaskType::Compound)
{
if (DecomposeCompoundTask(taskList, currentTask, taskArguments))
return PlanStepStatus::SuccessfulDecomposition;
else
return PlanStepStatus::FailedDecomposition;
}
*parentWorkingState = currentStackFrame.WorkingState;
// If it's a primitive task, we want to make sure we can actually execute it with our given
// state
if (taskType == TaskType::Primitive)
{
// TODO
}
planState.DecompositionStack.pop_back();
return PlanStepStatus::Failed;
}
std::cout << "Frame Done\n";
}
}
return PlanStepStatus::NoTasks;
}*/
return PlanStepStatus::PlanComplete;
}
}

82
src/ai/htn/HTNPlanner.hpp

@ -0,0 +1,82 @@
#pragma once
#include "HTNTypes.hpp"
#include "HTNTasks.hpp"
namespace Htn
{
struct GoalDecomposition
{
// The GoalTask this decomposition came from
GoalTask* DecomposedGoalTask;
// The index to the Method used for the current decomposition
int MethodIndex;
// The tasks themselves created by the Method
TaskCallList CallList;
// The state and parameters at the time this decomposition took place
WorldState InitialState;
ParameterList Parameters;
// State while the method is being decomposed
WorldState WorkingState;
// The result of this stack frame decomposition (only primitive tasks remaining)
TaskCallList FinalCallList;
};
typedef std::vector<GoalDecomposition> GoalDecompositionStack;
enum class PlanStepStatus
{
// Bad
Failed_BadData = 0,
Failed_NoPossiblePlan,
NoTasks,
// Running
SuccessfulDecomposition,
SuccessfulPrimitive,
FailedMethodDecomposition,
FailedGoalDecomposition,
// Done
PlanComplete
};
struct PlanState
{
WorldState State;
TaskCallList InitialCallList;
// Filled out by PlanStep(); Should only be used once PlanStep returns PlanComplete
TaskCallList FinalCallList;
//
// 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 immediately after a compound task has been decomposed
bool BreakOnCompoundDecomposition = false;
bool BreakOnPrimitiveApply = false;
//
// Used by PlanStep() only
//
GoalDecompositionStack DecompositionStack;
// Used for when all goals have been decomposed to manage mutable state
WorldState StacklessState;
// Copy of InitialCallList that PlanState can fuck with
TaskCallList WorkingCallList;
};
PlanStepStatus PlanStep(PlanState& PlanState);
};

38
src/ai/htn/HTNTasks.cpp

@ -4,12 +4,12 @@ namespace Htn
{
/*Task::Task(void)
{
Type = TaskType::None;
Type = TaskType::None;
}
TaskType Task::GetType(void)
{
return Type;
return Type;
}*/
int GoalTask::GetNumMethods(void)
@ -17,6 +17,17 @@ int GoalTask::GetNumMethods(void)
return Methods ? Methods->size() : 0;
}
bool GoalTask::DecomposeMethodAtIndex(TaskCallList& decomposition, int index, ParameterList& parameters)
{
Task* methodToDecompose = GetMethodAtIndex(index);
if (!methodToDecompose)
return false;
// TODO Fix this is screwy
decomposition.push_back({methodToDecompose, parameters});
return true;
}
Task* GoalTask::GetMethodAtIndex(int index)
{
if (index < 0 || index >= GetNumMethods() || !Methods)
@ -32,11 +43,30 @@ void GoalTask::SetMethods(TaskList* newMethods)
CompoundTask::CompoundTask(void)
{
//Type = TaskType::Compound;
// Type = TaskType::Compound;
}
PrimitiveTask::PrimitiveTask(void)
{
//Type = TaskType::Primitive;
// Type = TaskType::Primitive;
}
CompoundTask::~CompoundTask(void)
{
}
PrimitiveTask::~PrimitiveTask(void)
{
}
TaskType Task::GetType(void)
{
if (Goal)
return TaskType::Goal;
if (Compound)
return TaskType::Compound;
if (Primitive)
return TaskType::Primitive;
return TaskType::None;
}
}

78
src/ai/htn/HTNTasks.hpp

@ -4,38 +4,17 @@
namespace Htn
{
/*enum class TaskType
{
None = 0,
Goal,
Compound,
Primitive
};
// Task only exists so that we can store all our Task types in a flat list
class Task
{
public:
Task(void);
// Type is used to know what to cast this Task to
TaskType GetType(void);
protected:
// This is set by the constructors of each different Task
TaskType Type;
};*/
struct Task;
typedef std::vector<Task*> TaskList;
// Instead of the commented code, just use a simple struct which stores all types of tasks but
// only hope to allow only one thing to be filled in for it
struct Task
struct TaskCall
{
GoalTask* Goal;
CompoundTask* Compound;
PrimitiveTask* Primitive;
Task* TaskToCall;
ParameterList Parameters;
};
typedef std::vector<Task*> TaskList;
typedef std::vector<TaskCall> TaskCallList;
typedef TaskCallList::iterator TaskCallListIterator;
//
// Different types of tasks
@ -47,11 +26,13 @@ private:
TaskList* Methods;
public:
GoalTask(void);
GoalTask(void)=default;
int GetNumMethods(void);
Task* GetMethodAtIndex(int index);
bool DecomposeMethodAtIndex(TaskCallList& decomposition, int index, ParameterList& parameters);
void SetMethods(TaskList* newMethods);
};
@ -59,17 +40,52 @@ class CompoundTask //: public Task
{
public:
CompoundTask(void);
virtual ~CompoundTask(void);
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0;
virtual bool Decompose(TaskList& taskList, const TaskArguments& arguments) = 0;
virtual bool Decompose(TaskCallList& taskCallList, const TaskArguments& arguments) = 0;
};
class PrimitiveTask : public Task
class PrimitiveTask //: public Task
{
public:
PrimitiveTask(void);
virtual ~PrimitiveTask(void);
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0;
virtual void ApplyStateChange(TaskArguments& arguments) = 0;
// Returns whether or not starting the task was successful (NOT whether the task completed)
virtual bool Execute(const TaskArguments& arguments) = 0;
};
enum class TaskType
{
None = 0,
Goal,
Compound,
Primitive
};
/*
// Task only exists so that we can store all our Task types in a flat list
class Task
{
public:
Task(void);
// Type is used to know what to cast this Task to
TaskType GetType(void);
protected:
// This is set by the constructors of each different Task
TaskType Type;
};*/
// Instead of the commented code, just use a simple struct which stores all types of tasks but
// only hope to allow only one thing to be filled in for it
struct Task
{
GoalTask* Goal = nullptr;
CompoundTask* Compound = nullptr;
PrimitiveTask* Primitive = nullptr;
TaskType GetType(void);
};
}

2
src/ai/htn/HTNTypes.hpp

@ -39,7 +39,7 @@ typedef std::vector<Parameter>::const_iterator ParameterListConstIterator;
typedef std::vector<Parameter>::reverse_iterator ParameterListReverseIterator;
// The arguments passed to most all Task functions
typedef int WorldState; // TODO
typedef int WorldState; // TODO
struct TaskArguments
{
Entity Agent;

17
src/project/galavantSublime/galavant.sublime-project

@ -55,9 +55,18 @@
"settings":
{
//"sublimegdb_workingdir": "${folder:${project_path:your_executable_name}}",
"sublimegdb_workingdir": "../../../../galavant/bin/entityComponentTest",
"sublimegdb_commandline": "gdb --interpreter=mi ./entityComponentTest"
"sublimegdb_executables":
{
"htnTest":
{
"workingdir": "/home/macoy/Development/code/repositories/galavant/src/unitTesting/bin",
"commandline": "gdb --interpreter=mi ./htnTest"
},
// "second_executable_name":
// {
// "workingdir": "${folder:${project_path:second_executable_name}}",
// "commandline": "gdb --interpreter=mi ./second_executable"
// }
}
}
}

125
src/unitTesting/HTN_test.cpp

@ -2,51 +2,65 @@
#include "../ai/htn/HTNTypes.hpp"
#include "../ai/htn/HTNTasks.hpp"
#include "../ai/htn/HTNPlanner.hpp"
class TestCompoundTaskA : public Htn::CompoundTask
class TestPrimitiveTask : public Htn::PrimitiveTask
{
public:
TestCompoundTaskA(void)
TestPrimitiveTask(void)
{
//Type = Htn::TaskType::Primitive;
// Type = Htn::TaskType::Primitive;
}
virtual ~TestCompoundTaskA(void)
virtual ~TestPrimitiveTask(void)
{
}
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
{
std::cout << "\tStateMeetsPreconditions TestPrimitiveTask\n";
return true;
}
virtual bool Decompose(Htn::TaskList& taskList, const Htn::TaskArguments& args)
virtual void ApplyStateChange(Htn::TaskArguments& arguments)
{
std::cout << "\tApplyStateChange TestPrimitiveTask\n";
}
virtual bool Execute(const Htn::TaskArguments& args)
{
std::cout << "Decompose TestCompoundTaskA: " << args.Parameters[0].IntValue << "\n";
std::cout << "\texecute TestPrimitiveTask: " << args.Parameters[0].IntValue << "\n";
return true;
}
};
class TestPrimitiveTask : public Htn::PrimitiveTask
class TestCompoundTaskA : public Htn::CompoundTask
{
public:
TestPrimitiveTask(void)
TestCompoundTaskA(void)
{
//Type = Htn::TaskType::Primitive;
// Type = Htn::TaskType::Primitive;
}
virtual ~TestPrimitiveTask(void)
virtual ~TestCompoundTaskA(void)
{
}
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
{
std::cout << "\tStateMeetsPreconditions TestCompoundTaskA\n";
return true;
}
virtual bool Execute(const Htn::TaskArguments& args)
virtual bool Decompose(Htn::TaskCallList& taskCallList, const Htn::TaskArguments& args)
{
std::cout << "execute TestPrimitiveTask: " << args.Parameters[0].IntValue << "\n";
static TestPrimitiveTask testPrimitiveTask;
std::cout << "\tDecompose TestCompoundTaskA: " << args.Parameters[0].IntValue << "\n";
static Htn::Task primitiveTask;
primitiveTask.Primitive = &testPrimitiveTask;
primitiveTask.Goal = nullptr;
Htn::TaskCall taskCall = {&primitiveTask, args.Parameters};
taskCallList.push_back(taskCall);
return true;
}
};
@ -57,14 +71,87 @@ int main()
Htn::Parameter testParam = {Htn::Parameter::ParamType::Int, 123};
std::cout << testParam.FloatValue << "\n";
// Test Task types
TestPrimitiveTask testPrimitive;
std::cout << (int)testPrimitive.GetType() << "\n";
// Goal task setup
// Compound task setup
TestCompoundTaskA testCompoundTaskAA;
TestCompoundTaskA testCompoundTaskAB;
const Htn::TaskList goalTaskMethods = {&testCompoundTaskAA, &testCompoundTaskAB};
Htn::Task compoundTask;
compoundTask.Compound = &testCompoundTaskAA;
Htn::ParameterList params;
params.push_back(testParam);
Htn::TaskCall taskCall = {&compoundTask, params};
// Goal task setup (single)
Htn::GoalTask goalTask;
Htn::TaskList methods;
methods.push_back(&compoundTask);
goalTask.SetMethods(&methods);
Htn::Task goalTaskTask;
goalTaskTask.Goal = &goalTask;
Htn::TaskCall goalTaskCall = {&goalTaskTask, params};
// Goal task setup (nested)
Htn::GoalTask nestedGoalTask;
Htn::TaskList nestedMethods;
nestedMethods.push_back(&goalTaskTask);
nestedGoalTask.SetMethods(&nestedMethods);
Htn::Task nestedGoalTaskTask;
nestedGoalTaskTask.Goal = &nestedGoalTask;
Htn::TaskCall nestedGoalTaskCall = {&nestedGoalTaskTask, params};
// Compounds and primitives, but no goals
{
Htn::PlanState testPlan;
testPlan.InitialCallList.push_back(taskCall);
testPlan.InitialCallList.push_back(taskCall);
Htn::TaskArguments args;
args.Parameters = params;
testCompoundTaskAA.Decompose(testPlan.InitialCallList, args);
for (int i = 0; i < 10; i++)
{
Htn::PlanStepStatus status = Htn::PlanStep(testPlan);
std::cout << "[" << i << "] Returned Status " << (int)status << "\n";
if (status == Htn::PlanStepStatus::PlanComplete)
break;
}
std::cout << "Final Plan length: " << testPlan.FinalCallList.size()
<< (testPlan.FinalCallList.size() == 3 ? " (Pass)" : " (Fail)") << "\n";
}
// One goal (one stack frame)
{
Htn::PlanState testPlan;
testPlan.InitialCallList.push_back(goalTaskCall);
for (int i = 0; i < 10; i++)
{
Htn::PlanStepStatus status = Htn::PlanStep(testPlan);
std::cout << "[" << i << "] Returned Status " << (int)status << "\n";
if (status == Htn::PlanStepStatus::PlanComplete)
break;
}
std::cout << "Final Plan length: " << testPlan.FinalCallList.size()
<< (testPlan.FinalCallList.size() == 1 ? " (Pass)" : " (Fail)") << "\n";
}
// Nested goal (two stack frames)
{
Htn::PlanState testPlan;
testPlan.InitialCallList.push_back(nestedGoalTaskCall);
for (int i = 0; i < 10; i++)
{
Htn::PlanStepStatus status = Htn::PlanStep(testPlan);
std::cout << "[" << i << "] Returned Status " << (int)status << "\n";
if (status == Htn::PlanStepStatus::PlanComplete)
break;
}
std::cout << "Final Plan length: " << testPlan.FinalCallList.size()
<< (testPlan.FinalCallList.size() == 1 ? " (Pass)" : " (Fail)") << "\n";
}
return 0;
}
Loading…
Cancel
Save