629 lines
27 KiB
C++
629 lines
27 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "HTNBuilder.h"
|
|
#include "MockHTN.h"
|
|
#include "AITestsCommon.h"
|
|
#include "Debug/HTNDebug.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "AITestSuite_HTNTest"
|
|
|
|
// missing tests:
|
|
// testing invalid conditions
|
|
// testing specific scenarios with known plans
|
|
// test step by step generating the same plan as GeneratePlan
|
|
// using memory counters see if anything's leaking
|
|
|
|
struct FHTNTestBase : public FAITestBase
|
|
{
|
|
FHTNBuilder_Domain DomainBuilder;
|
|
FHTNWorldState WorldState;
|
|
FHTNPlanner Planner;
|
|
|
|
void PopulateWorldState()
|
|
{
|
|
for (int32 WSIndex = 0; WSIndex < int32(EMockHTNWorldState::MAX); ++WSIndex)
|
|
{
|
|
// setting every key to it's numerical index value
|
|
WorldState.SetValueUnsafe(static_cast<FHTNPolicy::FWSKey>(WSIndex), WSIndex);
|
|
}
|
|
}
|
|
|
|
void PopulateDomain(const bool bCompile = true)
|
|
{
|
|
DomainBuilder.SetRootName(TEXT("Root"));
|
|
{
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(TEXT("Root"));
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod(
|
|
TArray<FHTNCondition>({
|
|
FHTNCondition(EMockHTNWorldState::EnemyHealth, EHTNWorldStateCheck::Greater).SetRHSAsValue(0)
|
|
, FHTNCondition(EMockHTNWorldState::EnemyActor, EHTNWorldStateCheck::IsTrue)
|
|
}));
|
|
MethodsBuilder.AddTask(TEXT("AttackEnemy"));
|
|
}
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod();
|
|
MethodsBuilder.AddTask(TEXT("FindPatrolPoint"));
|
|
MethodsBuilder.AddTask(TEXT("NavigateToMoveDestination"));
|
|
}
|
|
}
|
|
{
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(TEXT("AttackEnemy"));
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod(FHTNCondition(EMockHTNWorldState::HasWeapon, EHTNWorldStateCheck::IsTrue));
|
|
MethodsBuilder.AddTask(TEXT("NavigateToEnemy"));
|
|
MethodsBuilder.AddTask(TEXT("UseWeapon"));
|
|
MethodsBuilder.AddTask(TEXT("Root"));
|
|
}
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod();
|
|
MethodsBuilder.AddTask(TEXT("FindWeapon"));
|
|
MethodsBuilder.AddTask(TEXT("NavigateToWeapon"));
|
|
MethodsBuilder.AddTask(TEXT("PickUp"));
|
|
MethodsBuilder.AddTask(TEXT("AttackEnemy"));
|
|
}
|
|
}
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(TEXT("FindPatrolPoint"));
|
|
PrimitiveTaskBuilder.SetOperator(EMockHTNTaskOperator::FindPatrolPoint, EMockHTNWorldState::MoveDestination);
|
|
}
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(TEXT("FindWeapon"));
|
|
PrimitiveTaskBuilder.SetOperator(EMockHTNTaskOperator::FindWeapon, EMockHTNWorldState::PickupLocation);
|
|
}
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(TEXT("NavigateToMoveDestination"));
|
|
PrimitiveTaskBuilder.SetOperator(EMockHTNTaskOperator::NavigateTo, EMockHTNWorldState::MoveDestination); // Local Variables?
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EMockHTNWorldState::CurrentLocation, EHTNWorldStateOperation::Set).SetRHSAsWSKey(EMockHTNWorldState::MoveDestination));
|
|
}
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(TEXT("NavigateToEnemy"));
|
|
PrimitiveTaskBuilder.SetOperator(EMockHTNTaskOperator::NavigateTo, EMockHTNWorldState::EnemyActor);
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EMockHTNWorldState::CurrentLocation, EHTNWorldStateOperation::Set).SetRHSAsWSKey(EMockHTNWorldState::EnemyActor));
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EMockHTNWorldState::CanSeeEnemy, EHTNWorldStateOperation::Set).SetRHSAsValue(1));
|
|
}
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(TEXT("NavigateToWeapon"));
|
|
PrimitiveTaskBuilder.SetOperator(EMockHTNTaskOperator::NavigateTo, EMockHTNWorldState::PickupLocation);
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EMockHTNWorldState::CurrentLocation, EHTNWorldStateOperation::Set).SetRHSAsWSKey(EMockHTNWorldState::PickupLocation));
|
|
}
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(TEXT("PickUp"));
|
|
PrimitiveTaskBuilder.SetOperator(EMockHTNTaskOperator::PickUp, EMockHTNWorldState::PickupLocation);
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EMockHTNWorldState::HasWeapon, EHTNWorldStateOperation::Set).SetRHSAsValue(1));
|
|
}
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(TEXT("UseWeapon"));
|
|
PrimitiveTaskBuilder.SetOperator(EMockHTNTaskOperator::UseWeapon, EMockHTNWorldState::EnemyActor);
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EMockHTNWorldState::Ammo, EHTNWorldStateOperation::Decrease).SetRHSAsValue(1));
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EMockHTNWorldState::EnemyHealth, EHTNWorldStateOperation::Decrease).SetRHSAsValue(1));
|
|
}
|
|
|
|
if (bCompile)
|
|
{
|
|
DomainBuilder.Compile();
|
|
}
|
|
}
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
struct FAITest_HTNDomainBuilderBasics : public FAITestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
FHTNBuilder_Domain DomainBuilder;
|
|
|
|
AITEST_TRUE("Initially DomainBuilder instance should be empty", DomainBuilder.CompositeTasks.Num() == 0 && DomainBuilder.PrimitiveTasks.Num() == 0);
|
|
|
|
const uint32 CompointTasksCount = 5;
|
|
for (int32 CompositeTaskIndex = 0; CompositeTaskIndex < CompointTasksCount; ++CompositeTaskIndex)
|
|
{
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(*FString::Printf(TEXT("c_%d"), CompositeTaskIndex));
|
|
for (int32 MethodIndex = 0; MethodIndex < (CompositeTaskIndex % 3); ++MethodIndex)
|
|
{
|
|
FHTNBuilder_Method* MethodsBuilder = nullptr;
|
|
|
|
if ((CompositeTaskIndex + MethodIndex) != 0)
|
|
{
|
|
TArray<FHTNCondition> Conditions;
|
|
for (int32 ConditionIndex = 0; ConditionIndex < (CompositeTaskIndex % 3); ++ConditionIndex)
|
|
{
|
|
Conditions.Add(FHTNCondition(ConditionIndex, EHTNWorldStateCheck(ConditionIndex)));
|
|
}
|
|
|
|
MethodsBuilder = &CompositeTaskBuilder.AddMethod(Conditions);
|
|
}
|
|
else
|
|
{
|
|
// this is testing the non-array method
|
|
MethodsBuilder = &CompositeTaskBuilder.AddMethod(FHTNCondition(0, EHTNWorldStateCheck(0)));
|
|
}
|
|
|
|
for (int32 TaskIndex = 0; TaskIndex < (CompositeTaskIndex % 4); ++TaskIndex)
|
|
{
|
|
MethodsBuilder->AddTask(*FString::Printf(TEXT("t_%d"), CompositeTaskIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 CompositeTaskIndex = 0; CompositeTaskIndex < DomainBuilder.CompositeTasks.Num(); ++CompositeTaskIndex)
|
|
{
|
|
FHTNBuilder_CompositeTask* CompositeTaskBuilder = DomainBuilder.CompositeTasks.Find(*FString::Printf(TEXT("c_%d"), CompositeTaskIndex));
|
|
AITEST_TRUE(FString::Printf(TEXT("Failed to find Composite task c_%d that has just been added"), CompositeTaskIndex), CompositeTaskBuilder != nullptr);
|
|
|
|
if (CompositeTaskBuilder != nullptr)
|
|
{
|
|
AITEST_TRUE(FString::Printf(TEXT("Method count mismatch for c_%d"), CompositeTaskIndex), CompositeTaskBuilder->Methods.Num() == (CompositeTaskIndex % 3));
|
|
for (int32 MethodIndex = 0; MethodIndex < CompositeTaskBuilder->Methods.Num(); ++MethodIndex)
|
|
{
|
|
if ((CompositeTaskIndex + MethodIndex) != 0)
|
|
{
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition count mismatch for c_%d[%d] method (array path)"), CompositeTaskIndex, MethodIndex)
|
|
, CompositeTaskBuilder->Methods[MethodIndex].Conditions.Num() == (CompositeTaskIndex % 3));
|
|
}
|
|
else
|
|
{
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition count mismatch for c_%d[%d] method (single instance)"), CompositeTaskIndex, MethodIndex)
|
|
, CompositeTaskBuilder->Methods[MethodIndex].Conditions.Num() == 1);
|
|
}
|
|
|
|
AITEST_TRUE(FString::Printf(TEXT("Task count mismatch for c_%d[%d] method"), CompositeTaskIndex, MethodIndex)
|
|
, CompositeTaskBuilder->Methods[MethodIndex].Tasks.Num() == (CompositeTaskIndex % 4));
|
|
}
|
|
}
|
|
}
|
|
|
|
const uint32 PrimitiveTasksCount = 4;
|
|
for (int32 PrimitiveTaskIndex = 0; PrimitiveTaskIndex < PrimitiveTasksCount; ++PrimitiveTaskIndex)
|
|
{
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTaskBuilder = DomainBuilder.AddPrimitiveTask(*FString::Printf(TEXT("p_%d"), PrimitiveTaskIndex));
|
|
PrimitiveTaskBuilder.SetOperator(PrimitiveTaskIndex, PrimitiveTaskIndex * 2);
|
|
for (int32 EffectIndex = 0; EffectIndex < (PrimitiveTaskIndex % 3); ++EffectIndex)
|
|
{
|
|
PrimitiveTaskBuilder.AddEffect(FHTNEffect(EffectIndex, EHTNWorldStateOperation::Set));
|
|
}
|
|
}
|
|
|
|
AITEST_TRUE("Wrong number of primitive tasks added", DomainBuilder.PrimitiveTasks.Num() == PrimitiveTasksCount);
|
|
for (int32 PrimitiveTaskIndex = 0; PrimitiveTaskIndex < PrimitiveTasksCount; ++PrimitiveTaskIndex)
|
|
{
|
|
FHTNBuilder_PrimitiveTask* PrimitiveTaskBuilder = DomainBuilder.PrimitiveTasks.Find(*FString::Printf(TEXT("p_%d"), PrimitiveTaskIndex));
|
|
AITEST_TRUE(FString::Printf(TEXT("Failed to find primitive task p_%d that has just been added"), PrimitiveTaskIndex), PrimitiveTaskBuilder != nullptr);
|
|
|
|
if (PrimitiveTaskBuilder)
|
|
{
|
|
AITEST_TRUE(FString::Printf(TEXT("Primitive task p_%d operator is wrong"), PrimitiveTasksCount), PrimitiveTaskBuilder->ActionID == PrimitiveTaskIndex);
|
|
AITEST_TRUE(FString::Printf(TEXT("Primitive task p_%d effects count is wrong"), PrimitiveTasksCount), PrimitiveTaskBuilder->Effects.Num() == (PrimitiveTaskIndex % 3));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNDomainBuilderBasics, "System.AI.HTN.DomainBuilderBasics")
|
|
|
|
struct FAITest_HTNBuildDomain : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
PopulateDomain();
|
|
|
|
AITEST_TRUE("DomainBuilder stores wrong number of primitive tasks", DomainBuilder.PrimitiveTasks.Num() == 7);
|
|
AITEST_TRUE("DomainBuilder stores wrong number of Composite tasks", DomainBuilder.CompositeTasks.Num() == 2);
|
|
|
|
FHTNBuilder_CompositeTask* RootBuilder = DomainBuilder.GetRootAsCompositeTask();
|
|
AITEST_TRUE("Root task should be set", RootBuilder != nullptr);
|
|
if (RootBuilder && RootBuilder->Methods.Num() > 0)
|
|
{
|
|
AITEST_TRUE("Root task\'s method [0] should be configured to have two conditions", RootBuilder->Methods[0].Conditions.Num() == 2);
|
|
}
|
|
|
|
TArray<FHTNBuilder_CompositeTask> CompositeTasks;
|
|
DomainBuilder.CompositeTasks.GenerateValueArray(CompositeTasks);
|
|
AITEST_TRUE("DomainBuilder stores wrong number of methods for the first Composite task", CompositeTasks[0].Methods.Num() == 2);
|
|
AITEST_TRUE("DomainBuilder stores wrong number of methods for the second Composite task", CompositeTasks[1].Methods.Num() == 2);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNBuildDomain, "System.AI.HTN.BuildDomain")
|
|
|
|
struct FAITest_HTNPlanning : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
FHTNResult Result;
|
|
|
|
Planner.GeneratePlan(*(DomainBuilder.DomainInstance), WorldState, Result);
|
|
AITEST_TRUE("Planning with an empty domain should result in an empty plan", Result.TaskIDs.Num() == 0);
|
|
|
|
PopulateDomain();
|
|
|
|
Planner.GeneratePlan(*(DomainBuilder.DomainInstance), WorldState, Result);
|
|
|
|
TArray<FHTNBuilder_CompositeTask> CompositeTasks;
|
|
DomainBuilder.CompositeTasks.GenerateValueArray(CompositeTasks);
|
|
// Patrol plan
|
|
AITEST_TRUE("Patrol plan should be same length as the last methods of the root Composite task", CompositeTasks[0].Methods.Last().Tasks.Num() == Result.TaskIDs.Num());
|
|
for (int32 TaskIndex = 0; TaskIndex < Result.TaskIDs.Num(); ++TaskIndex)
|
|
{
|
|
const FHTNPolicy::FTaskID TaskID = Result.TaskIDs[TaskIndex];
|
|
AITEST_TRUE("Patrol plan element mismatch", TaskID == DomainBuilder.DomainInstance->FindTaskID(CompositeTasks[0].Methods.Last().Tasks[TaskIndex]));
|
|
}
|
|
|
|
const FHTNDomain EmptyDomain;
|
|
Planner.GeneratePlan(EmptyDomain, WorldState, Result);
|
|
AITEST_TRUE("Reusing previous planning result with an empty domain should result in an empty plan", Result.TaskIDs.Num() == 0);
|
|
|
|
WorldState.ApplyEffect(FHTNEffect(EMockHTNWorldState::EnemyHealth, EHTNWorldStateOperation::Set).SetRHSAsValue(1));
|
|
WorldState.ApplyEffect(FHTNEffect(EMockHTNWorldState::EnemyActor, EHTNWorldStateOperation::Set).SetRHSAsValue(1));
|
|
Planner.GeneratePlan(*(DomainBuilder.DomainInstance), WorldState, Result);
|
|
AITEST_TRUE("", Result.TaskIDs.Num() > 0);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNPlanning, "System.AI.HTN.Planning")
|
|
|
|
struct FAITest_HTNPlanningRollback : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
FHTNResult Result;
|
|
|
|
// build a domain that will force rolling back
|
|
// first method should get accepted in the first planner step
|
|
// then one of the tasks it consists of should be a Composite task, that fails its condition
|
|
// Note: WorldState is populated with 0 values and has 128 keys (by default)
|
|
{
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(NAME_None); // root
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod();
|
|
MethodsBuilder.AddTask(TEXT("FailedComposite"));
|
|
}
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod();
|
|
MethodsBuilder.AddTask(TEXT("SuccessfulComposite"));
|
|
}
|
|
}
|
|
{
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(TEXT("FailedComposite"));
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod(FHTNCondition(0, EHTNWorldStateCheck::Greater).SetRHSAsValue(0));
|
|
MethodsBuilder.AddTask(TEXT("DummyPrimitive1"));
|
|
}
|
|
}
|
|
{
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(TEXT("SuccessfulComposite"));
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod(FHTNCondition(0, EHTNWorldStateCheck::Equal).SetRHSAsValue(0));
|
|
MethodsBuilder.AddTask(TEXT("DummyPrimitive2"));
|
|
}
|
|
}
|
|
DomainBuilder.AddPrimitiveTask(TEXT("DummyPrimitive1"));
|
|
const FName DummyPrimitiveName2 = TEXT("DummyPrimitive2");
|
|
DomainBuilder.AddPrimitiveTask(DummyPrimitiveName2);
|
|
|
|
DomainBuilder.Compile();
|
|
|
|
Planner.GeneratePlan(*(DomainBuilder.DomainInstance), WorldState, Result);
|
|
AITEST_TRUE("First Rollback plan should consist of one task, DummyPrimitive2", Result.TaskIDs.Num() == 1
|
|
&& FHTNDebug::GetTaskName(DomainBuilder, Result.TaskIDs[0]) == DummyPrimitiveName2);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNPlanningRollback, "System.AI.HTN.PlanningRollback")
|
|
|
|
struct FAITest_HTNDecompileDomain : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
AITEST_TRUE("Compiling an empty domain is allowed", DomainBuilder.Compile());
|
|
|
|
PopulateDomain();
|
|
|
|
FHTNBuilder_Domain DomainBuilder2(DomainBuilder.DomainInstance);
|
|
DomainBuilder2.Decompile();
|
|
const FString OriginalDescription = DomainBuilder.GetDebugDescription();
|
|
const FString DecompiledDescription = DomainBuilder2.GetDebugDescription();
|
|
|
|
AITEST_TRUE("Decompilation should result in identical DomainBuilder", OriginalDescription.Equals(DecompiledDescription));
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNDecompileDomain, "System.AI.HTN.DomainDecompilation")
|
|
|
|
struct FAITest_HTNDomainCompilationIssues : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
const FName MissingTaskName = TEXT("MissingTask");
|
|
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(TEXT("Root")); // root
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod();
|
|
MethodsBuilder.AddTask(MissingTaskName);
|
|
}
|
|
|
|
AITEST_FALSE("Domain with missing tasks should not compile", DomainBuilder.Compile());
|
|
AITEST_TRUE("Domain should be empty after a failed compilation", DomainBuilder.DomainInstance->IsEmpty());
|
|
|
|
DomainBuilder.AddPrimitiveTask(MissingTaskName);
|
|
AITEST_TRUE("After adding missing task domain should compile just fine", DomainBuilder.Compile());
|
|
AITEST_FALSE("Domain should not be empty after a successful compilation", DomainBuilder.DomainInstance->IsEmpty());
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNDomainCompilationIssues, "System.AI.HTN.DomainDecompilationIssues")
|
|
|
|
struct FAITest_HTNWorldRepresentation : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
for (int32 WSIndex = 0; WSIndex < int32(EMockHTNWorldState::MAX); ++WSIndex)
|
|
{
|
|
FHTNPolicy::FWSValue Value;
|
|
AITEST_TRUE("Retrieving known values from the WorldState instance", WorldState.GetValue(static_cast<FHTNPolicy::FWSKey>(WSIndex), Value) && Value == FHTNPolicy::DefaultValue);
|
|
}
|
|
|
|
PopulateWorldState();
|
|
|
|
const uint32 ReferenceValue = 3;
|
|
for (int32 WSIndex = 0; WSIndex < int32(EMockHTNWorldState::MAX); ++WSIndex)
|
|
{
|
|
FHTNPolicy::FWSValue Value;
|
|
AITEST_TRUE("Retrieving known values from the WorldState instance", WorldState.GetValue(static_cast<FHTNPolicy::FWSKey>(WSIndex), Value) && Value == WSIndex);
|
|
|
|
for (int32 OpIndex = 0; OpIndex < int32(EHTNWorldStateCheck::MAX); ++OpIndex)
|
|
{
|
|
const EHTNWorldStateCheck OpCode = EHTNWorldStateCheck(OpIndex);
|
|
bool bExpectedResult = false;
|
|
|
|
switch (OpCode)
|
|
{
|
|
case EHTNWorldStateCheck::Less:
|
|
bExpectedResult = Value < ReferenceValue;
|
|
break;
|
|
case EHTNWorldStateCheck::LessOrEqual:
|
|
bExpectedResult = Value <= ReferenceValue;
|
|
break;
|
|
case EHTNWorldStateCheck::Equal:
|
|
bExpectedResult = Value == ReferenceValue;
|
|
break;
|
|
case EHTNWorldStateCheck::NotEqual:
|
|
bExpectedResult = Value != ReferenceValue;
|
|
break;
|
|
case EHTNWorldStateCheck::GreaterOrEqual:
|
|
bExpectedResult = Value >= ReferenceValue;
|
|
break;
|
|
case EHTNWorldStateCheck::Greater:
|
|
bExpectedResult = Value > ReferenceValue;
|
|
break;
|
|
case EHTNWorldStateCheck::IsTrue:
|
|
bExpectedResult = (Value != 0);
|
|
break;
|
|
default:
|
|
AITEST_TRUE("Unhanled operation ID!", true);
|
|
}
|
|
|
|
FString Message = FString::Printf(TEXT("Testing %s on $d"), *FHTNDebug::HTNWorldStateCheckToString(OpCode), Value);
|
|
AITEST_TRUE(Message, WorldState.CheckCondition(FHTNCondition(WSIndex, OpCode).SetRHSAsValue(ReferenceValue)) == bExpectedResult);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNWorldRepresentation, "System.AI.HTN.WorldRepresentation")
|
|
|
|
struct FAITest_HTNCondition : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
PopulateWorldState();
|
|
|
|
const uint32 ReferenceValue = 3;
|
|
|
|
for (int32 WSIndex = 0; WSIndex < int32(EMockHTNWorldState::MAX); ++WSIndex)
|
|
{
|
|
for (int32 Value = 0; Value < int32(EMockHTNWorldState::MAX); ++Value)
|
|
{
|
|
const FHTNPolicy::FWSValue AsValue = FHTNPolicy::FWSValue(Value);
|
|
const FHTNPolicy::FWSKey AsKey = FHTNPolicy::FWSKey(Value);
|
|
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] < %d"), WSIndex, AsValue)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::Less).SetRHSAsValue(AsValue)) == (WSIndex < AsValue));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] <= %d"), WSIndex, AsValue)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::LessOrEqual).SetRHSAsValue(AsValue)) == (WSIndex <= AsValue));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] == %d"), WSIndex, AsValue)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::Equal).SetRHSAsValue(AsValue)) == (WSIndex == AsValue));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] != %d"), WSIndex, AsValue)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::NotEqual).SetRHSAsValue(AsValue)) == (WSIndex != AsValue));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] >= %d"), WSIndex, AsValue)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::GreaterOrEqual).SetRHSAsValue(AsValue)) == (WSIndex >= AsValue));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] > %d"), WSIndex, AsValue)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::Greater).SetRHSAsValue(AsValue)) == (WSIndex > AsValue));
|
|
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] < WS[%d]"), WSIndex, AsKey)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::Less).SetRHSAsWSKey(AsKey)) == (WSIndex < AsKey));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] <= WS[%d]"), WSIndex, AsKey)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::LessOrEqual).SetRHSAsWSKey(AsKey)) == (WSIndex <= AsKey));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] == WS[%d]"), WSIndex, AsKey)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::Equal).SetRHSAsWSKey(AsKey)) == (WSIndex == AsKey));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] != WS[%d]"), WSIndex, AsKey)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::NotEqual).SetRHSAsWSKey(AsKey)) == (WSIndex != AsKey));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] >= WS[%d]"), WSIndex, AsKey)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::GreaterOrEqual).SetRHSAsWSKey(AsKey)) == (WSIndex >= AsKey));
|
|
AITEST_TRUE(FString::Printf(TEXT("Condition WS[%d] > WS[%d]"), WSIndex, AsKey)
|
|
, WorldState.CheckCondition(FHTNCondition(WSIndex, EHTNWorldStateCheck::Greater).SetRHSAsWSKey(AsKey)) == (WSIndex > AsKey));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNCondition, "System.AI.HTN.Condition")
|
|
|
|
struct FAITest_HTNMethodSelection : public FAITestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
FHTNDomain Domain;
|
|
|
|
//FHTNCompositeTask& RootCompositeTask = Domain.AddCompositeTask(TEXT("Root"));
|
|
// do planning and come up with empty plan
|
|
|
|
// INJECTION
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNMethodSelection, "System.AI.HTN.MethodSelection")
|
|
|
|
struct FAITest_HTNTrivialPlanning : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
//FHTNDomain Domain;
|
|
|
|
TArray<FHTNCondition> Conditions;
|
|
//FHTNMethodSelection MethodSelection(0, Conditions);
|
|
|
|
|
|
//FHTNCompositeTask& RootCompositeTask = Domain.AddCompositeTask(TEXT("Root"));
|
|
// do planning and come up with empty plan
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNTrivialPlanning, "System.AI.HTN.TrivialPlanning")
|
|
|
|
struct FAITest_HTNExtendingDomain : public FAITestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
//FHTNDomain Domain;
|
|
|
|
//FHTNCompositeTask& RootCompositeTask = Domain.AddCompositeTask(TEXT("Root"));
|
|
// do planning and come up with empty plan
|
|
|
|
// INJECTION
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNExtendingDomain, "System.AI.HTN.ExtendingDomain")
|
|
|
|
struct FAITest_HTNCustomWSCheck : public FHTNTestBase
|
|
{
|
|
static bool bCheckFunctionRun;
|
|
static bool CustomCheck(const FHTNPolicy::FWSValue* Values, const FHTNCondition& Condition)
|
|
{
|
|
bCheckFunctionRun = true;
|
|
return true;
|
|
}
|
|
|
|
virtual bool InstantTest() override
|
|
{
|
|
const uint32 CustomCheckID = FHTNWorldStateOperations::RegisterCustomCheckType(&FAITest_HTNCustomWSCheck::CustomCheck, TEXT("CustomCheck"));
|
|
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(NAME_None); // root
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod(FHTNCondition(0, IntCastChecked<FHTNPolicy::FWSOperationID>(CustomCheckID)));
|
|
MethodsBuilder.AddTask(TEXT("Task1"));
|
|
}
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTask = DomainBuilder.AddPrimitiveTask(TEXT("Task1"));
|
|
|
|
DomainBuilder.Compile();
|
|
|
|
FHTNResult Result;
|
|
Planner.GeneratePlan(*DomainBuilder.DomainInstance, WorldState, Result);
|
|
|
|
AITEST_TRUE("Custom check has been exeduted", bCheckFunctionRun);
|
|
AITEST_TRUE("The custom check should allow for construction of the plan", Result.TaskIDs.Num() == 1);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
bool FAITest_HTNCustomWSCheck::bCheckFunctionRun = false;
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNCustomWSCheck, "System.AI.HTN.CustomWSCheck")
|
|
|
|
struct FAITest_HTNCustomWSOperation : public FHTNTestBase
|
|
{
|
|
static uint32 StaticValue;
|
|
static void CustomOperation(FHTNPolicy::FWSValue* Values, const FHTNEffect& Effect)
|
|
{
|
|
++StaticValue;
|
|
Values[Effect.KeyLeftHand] = StaticValue * 1024;
|
|
}
|
|
|
|
virtual bool InstantTest() override
|
|
{
|
|
const uint32 CustomOperationID = FHTNWorldStateOperations::RegisterCustomOperationType(&FAITest_HTNCustomWSOperation::CustomOperation, TEXT("CustomOperation"));
|
|
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(NAME_None); // root
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod();
|
|
MethodsBuilder.AddTask(TEXT("Task1"));
|
|
}
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTask = DomainBuilder.AddPrimitiveTask(TEXT("Task1"));
|
|
PrimitiveTask.AddEffect(FHTNEffect(0, IntCastChecked<FHTNPolicy::FWSOperationID>(CustomOperationID)));
|
|
PrimitiveTask.AddEffect(FHTNEffect(2, IntCastChecked<FHTNPolicy::FWSOperationID>(CustomOperationID)));
|
|
|
|
DomainBuilder.Compile();
|
|
|
|
FHTNResult Result;
|
|
Planner.GeneratePlan(*DomainBuilder.DomainInstance, WorldState, Result);
|
|
|
|
AITEST_TRUE("Custom Operation has been exeduted", StaticValue == 2);
|
|
AITEST_TRUE("Checking custom effect on key 0", Planner.GetWorldState().GetValueUnsafe(0) == 1024);
|
|
AITEST_TRUE("Checking custom effect on key 0", Planner.GetWorldState().GetValueUnsafe(2) == 1024 * 2);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
uint32 FAITest_HTNCustomWSOperation::StaticValue = 0;
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNCustomWSOperation, "System.AI.HTN.CustomWSOperation")
|
|
|
|
// compare contents of TaskIDs in HTNResult to the operators it contains
|
|
struct FAITest_HTNOperatorsOfGeneratedPlan : public FHTNTestBase
|
|
{
|
|
virtual bool InstantTest() override
|
|
{
|
|
FHTNBuilder_CompositeTask& CompositeTaskBuilder = DomainBuilder.AddCompositeTask(NAME_None);
|
|
{
|
|
FHTNBuilder_Method& MethodsBuilder = CompositeTaskBuilder.AddMethod();
|
|
MethodsBuilder.AddTask(TEXT("Task2"));
|
|
MethodsBuilder.AddTask(TEXT("Task1"));
|
|
}
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTask1 = DomainBuilder.AddPrimitiveTask(TEXT("Task1"));
|
|
PrimitiveTask1.SetOperator(1, 2);
|
|
FHTNBuilder_PrimitiveTask& PrimitiveTask2 = DomainBuilder.AddPrimitiveTask(TEXT("Task2"));
|
|
PrimitiveTask2.SetOperator(3, 4);
|
|
|
|
DomainBuilder.Compile();
|
|
|
|
FHTNResult Result;
|
|
Planner.GeneratePlan(*DomainBuilder.DomainInstance, WorldState, Result);
|
|
|
|
AITEST_TRUE("Plan should contain two elements", Result.TaskIDs.Num() == 2);
|
|
if (Result.TaskIDs.Num() >= 2)
|
|
{
|
|
const FHTNPolicy::FTaskID TaskID1 = DomainBuilder.DomainInstance->FindTaskID(TEXT("Task1"));
|
|
const FHTNPolicy::FTaskID TaskID2 = DomainBuilder.DomainInstance->FindTaskID(TEXT("Task2"));
|
|
AITEST_TRUE("Task2 should be the first one", Result.TaskIDs[0] == TaskID2);
|
|
AITEST_TRUE("Task2 action should be the first one", Result.ActionsSequence[0].ActionID == 3 && Result.ActionsSequence[0].Parameter == 4);
|
|
AITEST_TRUE("Task1 should be the second one", Result.TaskIDs[1] == TaskID1);
|
|
AITEST_TRUE("Task1 action should be the first one", Result.ActionsSequence[1].ActionID == 1 && Result.ActionsSequence[1].Parameter == 2);
|
|
|
|
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
IMPLEMENT_AI_INSTANT_TEST(FAITest_HTNOperatorsOfGeneratedPlan, "System.AI.HTN.OperatorsOfGeneratedPlan")
|
|
|
|
//----------------------------------------------------------------------//
|
|
// Component tests
|
|
//----------------------------------------------------------------------//
|
|
typedef FAITest_SimpleComponentBasedTest<UMockHTNComponent> FAITest_HTNComponentTest;
|
|
|
|
#undef LOCTEXT_NAMESPACE
|