diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..b4475da --- /dev/null +++ b/.clang-format @@ -0,0 +1,26 @@ +--- +BasedOnStyle: Microsoft +AccessModifierOffset : -4 +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: true +#AllowShortFunctionsOnASingleLine: Inline +AllowShortFunctionsOnASingleLine: InlineOnly +AlwaysBreakTemplateDeclarations : true +BreakBeforeBraces: Custom +BraceWrapping : + AfterEnum : true + AfterCaseLabel : true +BreakConstructorInitializers: BeforeComma +ColumnLimit: 132 +#IndentCaseBlocks: 'true' +IndentCaseLabels: 'true' +PointerAlignment: Left +UseTab : 'Never' + +SortIncludes : false + +AlignAfterOpenBracket: Align +AllowAllArgumentsOnNextLine: false +BinPackParameters : false +BinPackArguments : false +... diff --git a/.gitignore b/.gitignore index fd8715a..b7a2193 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # NuGet packages /packages/ +Debug/ diff --git a/Fluid-HTN.UnitTests/Fluid-HTN.UnitTests.csproj b/Fluid-HTN.UnitTests/Fluid-HTN.UnitTests.csproj index d2b9c3e..29c5000 100644 --- a/Fluid-HTN.UnitTests/Fluid-HTN.UnitTests.csproj +++ b/Fluid-HTN.UnitTests/Fluid-HTN.UnitTests.csproj @@ -1,6 +1,6 @@  - + Debug @@ -41,10 +41,10 @@ - ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\packages\MSTest.TestFramework.2.1.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\packages\MSTest.TestFramework.2.1.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll @@ -63,23 +63,23 @@ - - - {B6908CED-5C0B-415C-9564-85F66A8B5025} Fluid-HTN + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + - + \ No newline at end of file diff --git a/Fluid-HTN.UnitTests/packages.config b/Fluid-HTN.UnitTests/packages.config index 2f7c5a1..f84cb10 100644 --- a/Fluid-HTN.UnitTests/packages.config +++ b/Fluid-HTN.UnitTests/packages.config @@ -1,5 +1,5 @@  - - + + \ No newline at end of file diff --git a/Fluid-HTN.sln b/Fluid-HTN.sln index 07a3505..f8d42c8 100644 --- a/Fluid-HTN.sln +++ b/Fluid-HTN.sln @@ -7,20 +7,68 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluid-HTN", "Fluid-HTN\Flui EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluid-HTN.UnitTests", "Fluid-HTN.UnitTests\Fluid-HTN.UnitTests.csproj", "{A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Fluid-HTNCPP", "Fluid-HTNCPP\Fluid-HTNCPP.vcxproj", "{0C469B65-6D39-4667-8D00-9EC963C518E3}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Fluid-HTNCPP.UnitTests", "Fluid-HTNCPP.UnitTests\Fluid-HTNCPP.UnitTests.vcxproj", "{402394C5-2D69-49F0-B2E2-239FAB4CC252}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B6908CED-5C0B-415C-9564-85F66A8B5025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B6908CED-5C0B-415C-9564-85F66A8B5025}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Debug|x64.ActiveCfg = Debug|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Debug|x64.Build.0 = Debug|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Debug|x86.ActiveCfg = Debug|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Debug|x86.Build.0 = Debug|Any CPU {B6908CED-5C0B-415C-9564-85F66A8B5025}.Release|Any CPU.ActiveCfg = Release|Any CPU {B6908CED-5C0B-415C-9564-85F66A8B5025}.Release|Any CPU.Build.0 = Release|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Release|x64.ActiveCfg = Release|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Release|x64.Build.0 = Release|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Release|x86.ActiveCfg = Release|Any CPU + {B6908CED-5C0B-415C-9564-85F66A8B5025}.Release|x86.Build.0 = Release|Any CPU {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Debug|x64.Build.0 = Debug|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Debug|x86.Build.0 = Debug|Any CPU {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Release|Any CPU.Build.0 = Release|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Release|x64.ActiveCfg = Release|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Release|x64.Build.0 = Release|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Release|x86.ActiveCfg = Release|Any CPU + {A6DAA5DB-FC2B-4D08-87A1-0E667A39CF21}.Release|x86.Build.0 = Release|Any CPU + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Debug|Any CPU.ActiveCfg = Debug|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Debug|Any CPU.Build.0 = Debug|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Debug|x64.ActiveCfg = Debug|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Debug|x64.Build.0 = Debug|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Debug|x86.ActiveCfg = Debug|Win32 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Debug|x86.Build.0 = Debug|Win32 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Release|Any CPU.ActiveCfg = Release|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Release|Any CPU.Build.0 = Release|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Release|x64.ActiveCfg = Release|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Release|x64.Build.0 = Release|x64 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Release|x86.ActiveCfg = Release|Win32 + {0C469B65-6D39-4667-8D00-9EC963C518E3}.Release|x86.Build.0 = Release|Win32 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Debug|Any CPU.ActiveCfg = Debug|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Debug|Any CPU.Build.0 = Debug|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Debug|x64.ActiveCfg = Debug|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Debug|x64.Build.0 = Debug|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Debug|x86.ActiveCfg = Debug|Win32 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Debug|x86.Build.0 = Debug|Win32 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Release|Any CPU.ActiveCfg = Release|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Release|Any CPU.Build.0 = Release|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Release|x64.ActiveCfg = Release|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Release|x64.Build.0 = Release|x64 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Release|x86.ActiveCfg = Release|Win32 + {402394C5-2D69-49F0-B2E2-239FAB4CC252}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Fluid-HTN/Fluid-HTN.csproj b/Fluid-HTN/Fluid-HTN.csproj index d07e064..68165b7 100644 --- a/Fluid-HTN/Fluid-HTN.csproj +++ b/Fluid-HTN/Fluid-HTN.csproj @@ -1,5 +1,6 @@  + Debug @@ -13,6 +14,8 @@ 512 true + + true @@ -75,6 +78,16 @@ - + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Fluid-HTN/packages.config b/Fluid-HTN/packages.config new file mode 100644 index 0000000..651e442 --- /dev/null +++ b/Fluid-HTN/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Fluid-HTNCPP.UnitTests/ActionEffectsTest.cpp b/Fluid-HTNCPP.UnitTests/ActionEffectsTest.cpp new file mode 100644 index 0000000..0296d21 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/ActionEffectsTest.cpp @@ -0,0 +1,72 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "Effects/Effect.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +class TestContext : public BaseContext +{ + bool _Done = false; +public: + bool& Done() { return _Done; } +}; + +namespace Microsoft::VisualStudio::CppUnitTestFramework +{ + template<> + std::wstring ToString(const EffectType& eff) + { + switch(eff) + { + case EffectType::Permanent: + return L"EffectType::Permanent"; + case EffectType::PlanOnly: + return L"EffectType::PlanOnly"; + case EffectType::PlanAndExecute: + return L"EffectType::PlanAndExecute"; + } + return L"Unknown value"; + } +} +namespace FluidHTNCPPUnitTests +{ + TEST_CLASS(ActionEffectTests) + { + public: + + TEST_METHOD(SetsName_ExpectedBehavior) + { + ActionEffect a("Name", EffectType::PlanOnly, nullptr); + + Assert::AreEqual("Name"s, a.Name()); + } + TEST_METHOD(SetsType_ExpectedBehavior) + { + ActionEffect e("Name", EffectType::PlanOnly, nullptr); + + Assert::AreEqual(EffectType::PlanOnly, e.Type()); + } + + TEST_METHOD(ApplyDoesNothingWithoutFunctionPtr_ExpectedBehavior) + { + TestContext ctx; + ActionEffect e("Name", EffectType::PlanOnly, nullptr); + + e.Apply(ctx); + } + + TEST_METHOD(ApplyCallsInternalFunctionPtr_ExpectedBehavior) + { + TestContext ctx; + ActionEffect e("Name", EffectType::PlanOnly, [=](IContext& c, EffectType ) {static_cast(c).Done() = true; }); + + e.Apply(ctx); + + Assert::AreEqual(true, ctx.Done()); + } + }; +} diff --git a/Fluid-HTNCPP.UnitTests/BaseContextTests.cpp b/Fluid-HTNCPP.UnitTests/BaseContextTests.cpp new file mode 100644 index 0000000..33103c0 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/BaseContextTests.cpp @@ -0,0 +1,178 @@ + +#include "pch.h" +#include "CppUnitTest.h" +#include "Effects/EffectType.h" +#include "Contexts/BaseContext.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace std::string_literals; + +using namespace FluidHTN; + +namespace FluidHTNCPPUnitTests +{ + TEST_CLASS(BaseContextTests) + { + TEST_METHOD( DefaultContextStateIsExecuting_ExpectedBehavior) + { + DomainTestContext ctx; + Assert::IsTrue(ctx.GetContextState() == ContextState::Executing); + } + TEST_METHOD(InitInitializeCollections_ExpectedBehavior) + { + DomainTestContext ctx; + + ctx.Init(); + + Assert::AreEqual(false, ctx.DebugMTR()); + Assert::AreEqual(false, ctx.LogDecomposition()); + Assert::AreEqual(true, ctx.MTRDebug().size() == 0); + Assert::AreEqual(true, ctx.LastMTRDebug().size() == 0); + Assert::AreEqual(true, ctx.DecompositionLog().size() == 0); + } + TEST_METHOD(InitInitializeDebugCollections_ExpectedBehavior) + { + MyDebugContext ctx; + + ctx.Init(); + + Assert::AreEqual(true, ctx.DebugMTR()); + Assert::AreEqual(true, ctx.LogDecomposition()); + } + TEST_METHOD(HasState_ExpectedBehavior) + { + DomainTestContext ctx; + ctx.Init(); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + + Assert::AreEqual(false, ctx.HasStateOneParam(DomainTestState::HasA)); + Assert::AreEqual(true, ctx.HasStateOneParam(DomainTestState::HasB)); + } + TEST_METHOD(SetStatePlanningContext_ExpectedBehavior) + { + DomainTestContext ctx; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + + Assert::AreEqual(true, (bool)ctx.GetStateDTS(DomainTestState::HasB)); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasA].size() == 0); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasB].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasB].top().First() == EffectType::Permanent); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasB].top().Second() == 1); + Assert::IsTrue(ctx.GetWorldState().GetState(DomainTestState::HasB) == 0); + } + TEST_METHOD(SetStateExecutingContext_ExpectedBehavior) + { + DomainTestContext ctx; + + ctx.Init(); + ctx.SetContextState(ContextState::Executing); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + + Assert::AreEqual(true, ctx.HasStateOneParam(DomainTestState::HasB)); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasB].size() == 0); + Assert::IsTrue(ctx.GetWorldState().GetState( DomainTestState::HasB) == 1); + } + + TEST_METHOD(GetStatePlanningContext_ExpectedBehavior) + { + DomainTestContext ctx; + + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + + Assert::AreEqual(0,(int) ctx.GetStateDTS(DomainTestState::HasA)); + Assert::AreEqual(1, (int)ctx.GetStateDTS(DomainTestState::HasB)); + } + + TEST_METHOD(GetStateExecutingContext_ExpectedBehavior) + { + DomainTestContext ctx; + + ctx.Init(); + ctx.SetContextState(ContextState::Executing); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + + Assert::AreEqual(0,(int) ctx.GetStateDTS(DomainTestState::HasA)); + Assert::AreEqual(1, (int)ctx.GetStateDTS(DomainTestState::HasB)); + } + + TEST_METHOD(GetWorldStateChangeDepth_ExpectedBehavior) + { + DomainTestContext ctx; + + ctx.Init(); + ctx.SetContextState(ContextState::Executing); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + auto changeDepthExecuting = ctx.GetWorldStateChangeDepth(); + + ctx.SetContextState(ContextState::Planning); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + auto changeDepthPlanning = ctx.GetWorldStateChangeDepth(); + + Assert::AreEqual(ctx.GetWorldStateChangeStack().size(), changeDepthExecuting.size()); + Assert::AreEqual(0, changeDepthExecuting[(int) DomainTestState::HasA]); + Assert::AreEqual(0, changeDepthExecuting[(int) DomainTestState::HasB]); + + Assert::AreEqual(ctx.GetWorldStateChangeStack().size(), changeDepthPlanning.size()); + Assert::AreEqual(0, changeDepthPlanning[(int) DomainTestState::HasA]); + Assert::AreEqual(1, changeDepthPlanning[(int) DomainTestState::HasB]); + } + + TEST_METHOD(TrimForExecution_ExpectedBehavior) + { + DomainTestContext ctx; + + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + ctx.SetStateDTS(DomainTestState::HasA, true,true, EffectType::PlanAndExecute); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + ctx.SetStateDTS(DomainTestState::HasC, true,true, EffectType::PlanOnly); + ctx.TrimForExecution(); + + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasA].size() == 0); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasB].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasC].size() == 0); + } + + TEST_METHOD(TrimForExecutionThrowsExceptionIfWrongContextState_ExpectedBehavior) + { + DomainTestContext ctx; + + ctx.Init(); + ctx.SetContextState(ContextState::Executing); + Assert::ExpectException([&]() { ctx.TrimForExecution(); }); + } + TEST_METHOD(TrimToStackDepth_ExpectedBehavior) + { + DomainTestContext ctx; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + ctx.SetStateDTS(DomainTestState::HasA, true,true, EffectType::PlanAndExecute); + ctx.SetStateDTS(DomainTestState::HasB, true,true, EffectType::Permanent); + ctx.SetStateDTS(DomainTestState::HasC, true,true, EffectType::PlanOnly); + auto stackDepth = ctx.GetWorldStateChangeDepth(); + + ctx.SetStateDTS(DomainTestState::HasA, false, true, EffectType::PlanAndExecute); + ctx.SetStateDTS(DomainTestState::HasB, false, true, EffectType::Permanent); + ctx.SetStateDTS(DomainTestState::HasC, false, true, EffectType::PlanOnly); + ctx.TrimToStackDepth(stackDepth); + + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasA].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasB].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasC].size() == 1); + } + + TEST_METHOD(TrimToStackDepthThrowsExceptionIfWrongContextState_ExpectedBehavior) + { + DomainTestContext ctx; + ctx.Init(); + ctx.SetContextState(ContextState::Executing); + auto stackDepth = ctx.GetWorldStateChangeDepth(); + Assert::ExpectException([&]() { ctx.TrimToStackDepth(stackDepth); }); + } + } ; +} diff --git a/Fluid-HTNCPP.UnitTests/DomainBuilderTests.cpp b/Fluid-HTNCPP.UnitTests/DomainBuilderTests.cpp new file mode 100644 index 0000000..74d8a62 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/DomainBuilderTests.cpp @@ -0,0 +1,413 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "CoreIncludes/BaseDomainBuilder.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +class DomainBuilder final : public BaseDomainBuilder + { +public: + DomainBuilder(StringType n): BaseDomainBuilder(n){} +}; +namespace FluidHTNCPPUnitTests +{ + TEST_CLASS(DomainBuilderTests) + { + TEST_METHOD(Build_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + auto ptr = builder.Pointer(); + auto domain = *(builder.Build()); + + Assert::IsTrue(domain.Root() != nullptr); + Assert::IsTrue(ptr == domain.Root()); + Assert::AreEqual("Test"s, domain.Root()->Name()); + } + + TEST_METHOD(BuildInvalidatesPointer_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + auto domain = *builder.Build(); + + Assert::ExpectException([&]() { + bool bRet = (builder.Pointer() == domain.Root()); + bRet; + }); + } + + TEST_METHOD(Selector_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSelector("select test"); + builder.End(); + + // Assert + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(Selector_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSelector("select test"); + + // Assert + Assert::AreEqual(false, builder.Pointer()->IsTypeOf( ITaskDerivedClassName::TaskRoot)); + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::SelectorCompoundTask)); + } + + TEST_METHOD(SelectorBuild_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSelector("select test"); + Assert::ExpectException([&]() { auto domain = builder.Build(); }); + } + TEST_METHOD(Selector_CompoundTask) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + SharedPtr ctask = MakeSharedPtr("compound task"); + builder.AddCompoundTask("compound task",ctask); + + // Assert + Assert::AreEqual(false, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::SelectorCompoundTask)); + } + TEST_METHOD(Sequence_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSequence("Sequence test"); + builder.End(); + + // Assert + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(Sequence_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSequence("Sequence test"); + + // Assert + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::SequenceCompoundTask)); + } + + TEST_METHOD(Sequence_CompoundTask) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + SharedPtr ctask = MakeSharedPtr("sequence task"); + builder.AddCompoundTask("compound task",ctask); + + // Assert + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::SequenceCompoundTask)); + } + + TEST_METHOD(Action_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("sequence test"); + builder.End(); + + // Assert + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(Action_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("sequence test"); + + // Assert + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)); + } + TEST_METHOD(Action_PrimitiveTask) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddPrimitiveTask("sequence test"); + + // Assert + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)); + } + + TEST_METHOD(PausePlanThrowsWhenPointerIsNotDecomposeAll) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + Assert::ExpectException([&]() { builder.PausePlan(); }); + } + + TEST_METHOD(PausePlan_ExpectedBehaviour) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSequence("sequence test"); + builder.PausePlan(); + builder.End(); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + TEST_METHOD(PausePlan_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSequence("sequence test"); + builder.PausePlan(); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::SequenceCompoundTask)); + } + + TEST_METHOD(Condition_ExpectedBehaviour) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddCondition("test", [](IContext&) { return true; }); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(ExecutingCondition_ThrowsIfNotPrimitiveTaskPointer) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + Assert::ExpectException( + [&]() { builder.AddExecutingCondition("test", [](IContext&) { return true; }); }); + } + TEST_METHOD(ExecutingCondition_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("test"); + builder.AddExecutingCondition("test", [](IContext&) { return true; }); + builder.End(); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(ExecutingCondition_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("test"); + builder.AddExecutingCondition("test", [](IContext&) { return true; }); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)); + } + + TEST_METHOD(Do_ThrowsIfNotPrimitiveTaskPointer) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + Assert::ExpectException( + [&]() { + builder.AddOperator([](IContext&) -> TaskStatus { return TaskStatus::Success; }); + }); + } + + TEST_METHOD(Do_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("test"); + builder.AddOperator([](IContext&) -> TaskStatus { return TaskStatus::Success; }); + builder.End(); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(Do_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("test"); + builder.AddOperator([](IContext&) -> TaskStatus { return TaskStatus::Success; }); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)); + } + + TEST_METHOD(Effect_ThrowsIfNotPrimitiveTaskPointer) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + Assert::ExpectException([&]() { builder.AddEffect("test", EffectType::Permanent, [](IContext&, EffectType){}); }); + } + + TEST_METHOD( Effect_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("test"); + builder.AddEffect("test", EffectType::Permanent, [](IContext&, EffectType){}); + builder.End(); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(Effect_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("test"); + builder.AddEffect("test", EffectType::Permanent, [](IContext&, EffectType){}); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)); + } + TEST_METHOD(Splice_ThrowsIfNotCompoundPointer) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + auto domain = *DomainBuilder("sub-domain").Build(); + builder.AddAction("test"); + Assert::ExpectException([&]() { builder.Splice(domain); }); + } + + TEST_METHOD(Splice_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + auto domain = *DomainBuilder("sub-domain").Build(); + builder.Splice(domain); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + } + + TEST_METHOD(Splice_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + auto domain = *DomainBuilder("sub-domain").Build(); + builder.AddSelector("test"); + builder.Splice(domain); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::SelectorCompoundTask)); + } + + TEST_METHOD(Slot_ThrowsIfNotCompoundPointer) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddAction("test"); + Assert::ExpectException([&]() { builder.AddSlot(1); }); + } + + TEST_METHOD(Slot_ThrowsIfSlotIdAlreadyDefined) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSlot(1); + Assert::ExpectException([&]() { builder.AddSlot(1); }); + } + + TEST_METHOD(Slot_ExpectedBehavior) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSlot(1); + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + + auto domain = *builder.Build(); + + auto subDomain = *DomainBuilder("sub-domain").Build(); + Assert::IsTrue(domain.TrySetSlotDomain(1, subDomain)); // Its valid to add a sub-domain to a slot we have defined in our domain definition, and that is not currently occupied. + Assert::IsTrue(domain.TrySetSlotDomain(1, subDomain) == false); // Need to clear slot before we can attach sub-domain to a currently occupied slot. + Assert::IsTrue(domain.TrySetSlotDomain(99, subDomain) == false); // Need to define slotId in domain definition before we can attach sub-domain to that slot. + + Assert::IsTrue(domain.Root()->Subtasks().size() == 1); + Assert::IsTrue(domain.Root()->Subtasks()[0]->IsTypeOf(ITaskDerivedClassName::Slot)); + + auto slot = StaticCastPtr(domain.Root()->Subtasks()[0]); + Assert::IsTrue(slot->Subtask() != nullptr); + Assert::IsTrue(slot->Subtask()->IsTypeOf(ITaskDerivedClassName::TaskRoot)); + Assert::IsTrue(slot->Subtask()->Name() == "sub-domain"s); + + domain.ClearSlot(1); + Assert::IsTrue(slot->Subtask() == nullptr); + } + + TEST_METHOD(Slot_ForgotEnd) + { + // Arrange + DomainBuilder builder("Test"s); + + // Act + builder.AddSelector("test"); + builder.AddSlot(1); + + Assert::AreEqual(true, builder.Pointer()->IsTypeOf(ITaskDerivedClassName::SelectorCompoundTask)); + } + }; +} \ No newline at end of file diff --git a/Fluid-HTNCPP.UnitTests/DomainTestContext.h b/Fluid-HTNCPP.UnitTests/DomainTestContext.h new file mode 100644 index 0000000..eaca46d --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/DomainTestContext.h @@ -0,0 +1,65 @@ +#pragma once +#include "Contexts/BaseContext.h" + +using namespace FluidHTN; + +enum class DomainTestState +{ + HasA, + HasB, + HasC +}; +class DomainTestWorldState : public IWorldState +{ + uint8_t MyState[3]; + +public: + bool HasState(DomainTestState state, uint8_t value) + { + return (MyState[(int)state] == value); + } + + uint8_t& GetState(DomainTestState state) { return MyState[(int)state]; } + + void SetState(DomainTestState state, uint8_t value) { MyState[(int)state] = value; } + + int GetMaxPropertyCount() { return 3; } +}; +class DomainTestContext : public BaseContext +{ + bool _done = false; + +public: + DomainTestContext() { _WorldState = MakeSharedPtr(); } + + bool& Done() { return _done; } + + bool HasStateOneParam(DomainTestState state) + { + uint8_t one = 1; + return BaseContext::HasState(state, one); + } + + void SetStateDTS(DomainTestState state, int value) + { + _WorldState->SetState(static_cast(state), static_cast(value)); + } + void SetStateDTS(DomainTestState state, int value, bool dirty, EffectType eff) + { + BaseContext::SetState(static_cast(state), static_cast(value),dirty,eff); + } + + uint8_t GetStateDTS(DomainTestState state) { return BaseContext::GetState(static_cast(state)); } + +}; +typedef BaseContext BaseContextType; + +class MyDebugContext : public DomainTestContext +{ +public: + MyDebugContext() + { + _DebugMTR = true; + _LogDecomposition = true; + } +}; diff --git a/Fluid-HTNCPP.UnitTests/DomainTests.cpp b/Fluid-HTNCPP.UnitTests/DomainTests.cpp new file mode 100644 index 0000000..4479c50 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/DomainTests.cpp @@ -0,0 +1,513 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "CoreIncludes/Domain.h" +#include "Tasks/Task.h" +#include "Tasks/CompoundTasks/CompoundTask.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "Tasks/CompoundTasks/PausePlanTask.h" +#include "Tasks/CompoundTasks/Selector.h" +#include "Tasks/CompoundTasks/Sequence.h" +#include "Tasks/CompoundTasks/DecompositionStatus.h" +#include "Effects/Effect.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +namespace Microsoft::VisualStudio::CppUnitTestFramework +{ +template <> +std::wstring ToString(ITask* eff) +{ + if (eff) + { + switch (eff->GetType()) + { + case ITaskDerivedClassName::CompoundTask: + return L"ITaskDerivedClassName::CompoundTask"; + case ITaskDerivedClassName::ITaskType: + return L"ITaskDerivedClassName::ITaskType"; + case ITaskDerivedClassName::PausePlanTask: + return L"ITaskDerivedClassName::PausePlanTask"; + case ITaskDerivedClassName::PrimitiveTask: + return L"ITaskDerivedClassName::PrimitiveTask"; + case ITaskDerivedClassName::SelectorCompoundTask: + return L"ITaskDerivedClassName::SelectorCompoundTask"; + case ITaskDerivedClassName::SequenceCompoundTask: + return L"ITaskDerivedClassName::SequenceCompoundTask"; + case ITaskDerivedClassName::Slot: + return L"ITaskDerivedClassName::Slot"; + case ITaskDerivedClassName::TaskRoot: + return L"ITaskDerivedClassName::TaskRoot"; + } + } + return L"Unknown value"; +} +} // namespace Microsoft::VisualStudio::CppUnitTestFramework + +namespace FluidHTNCPPUnitTests +{ +TEST_CLASS(DomainTests) +{ + TEST_METHOD(DomainHasRootWithDomainName_ExpectedBehavior) + { + Domain domain("Test"); + Assert::IsTrue(domain.Root() != nullptr); + Assert::IsTrue(domain.Root()->Name() == "Test"s); + } + TEST_METHOD(AddSubtaskToParent_ExpectedBehavior) + { + Domain domain("Test"); + SharedPtr task1 = MakeSharedPtr("Test"); + SharedPtr task2 = MakeSharedPtr("Test2"); + domain.Add(task1, task2); + //Assert::IsTrue(std::find(task1->Subtasks().begin(), task1->Subtasks().end(), task2) != task1->Subtasks().end()); + Assert::IsTrue(task2->Parent().get() == task1.get()); + } + TEST_METHOD(FindPlanUninitializedContextThrowsException_ExpectedBehavior) + { + auto domain = MakeSharedPtr("Test"); + SharedPtr ctx = MakeSharedPtr(); + + Assert::ExpectException([=]() -> DecompositionStatus { + TaskQueueType plan; + return domain->FindPlan(*ctx, plan); + }); + } + TEST_METHOD(FindPlanNoTasksThenNullPlan_ExpectedBehavior) + { + SharedPtr ctx = MakeSharedPtr(); + Domain domain("Test"); + TaskQueueType plan; + ctx->Init(); + auto status = domain.FindPlan(*ctx, plan); + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + } + TEST_METHOD(AfterFindPlanContextStateIsExecuting_ExpectedBehavior) + { + SharedPtr ctx = MakeSharedPtr(); + Domain domain("Test"); + TaskQueueType plan; + ctx->Init(); + domain.FindPlan(*ctx, plan); + Assert::IsTrue(ctx->GetContextState() == ContextState::Executing); + } + TEST_METHOD(FindPlan_ExpectedBehavior) + { + SharedPtr bctx = MakeSharedPtr(); + Domain domain("Test"); + TaskQueueType plan; + + bctx->Init(); + + SharedPtr task1 = MakeSharedPtr("Test"); + + SharedPtr task2 = MakeSharedPtr("Sub-task"); + + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::IsTrue(plan.front()->Name() == "Sub-task"s); + } + TEST_METHOD(FindPlanTrimsNonPermanentStateChange_ExpectedBehavior) + { + SharedPtr bctx = MakeSharedPtr(); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + SharedPtr task1 = MakeSharedPtr("Test"); + + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr effect1 = + MakeSharedPtr("TestEffect1"s, EffectType::PlanOnly, [=](IContext& ctx, EffectType t) { + static_cast(ctx).SetState(DomainTestState::HasA, true, true, t); + }); + task2->AddEffect(effect1); + + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + SharedPtr effect2 = MakeSharedPtr( + "TestEffect2"s, + EffectType::PlanAndExecute, + [=](IContext& ctx, EffectType t) {static_cast(ctx).SetState(DomainTestState::HasB, true, true, t); }); + task3->AddEffect(effect2); + + SharedPtr task4 = MakeSharedPtr("Sub-task3"); + SharedPtr effect3 = + MakeSharedPtr("TestEffect3"s, EffectType::Permanent, [=](IContext& ctx, EffectType t) { + static_cast(ctx).SetState(DomainTestState::HasC, true, true, t); + }); + task4->AddEffect(effect3); + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + domain.Add(task1, task3); + domain.Add(task1, task4); + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(bctx->GetWorldStateChangeStack()[(int)DomainTestState::HasA].size() == 0); + Assert::IsTrue(bctx->GetWorldStateChangeStack()[(int)DomainTestState::HasB].size() == 0); + Assert::IsTrue(bctx->GetWorldStateChangeStack()[(int)DomainTestState::HasC].size() == 0); + Assert::IsTrue(bctx->GetWorldState().GetState(DomainTestState::HasA) == 0); + Assert::IsTrue(bctx->GetWorldState().GetState(DomainTestState::HasB) == 0); + Assert::IsTrue(bctx->GetWorldState().GetState(DomainTestState::HasC) == 1); + Assert::IsTrue(plan.size() == 3); + } + + TEST_METHOD(FindPlanClearsStateChangeWhenPlanIsNull_ExpectedBehavior) + { + auto bctx = MakeSharedPtr(); + SharedPtr ctx = StaticCastPtr(bctx); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + SharedPtr task1 = MakeSharedPtr("Test"); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr effect1 = + MakeSharedPtr("TestEffect1"s, EffectType::PlanOnly, [=](IContext& ctx, EffectType t) { + static_cast(ctx).SetState(DomainTestState::HasA, true, true, t); + }); + task2->AddEffect(effect1); + + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + SharedPtr effect2 = MakeSharedPtr( + "TestEffect2"s, + EffectType::PlanAndExecute, + [=](IContext& ctx, EffectType t) { static_cast(ctx).SetState(DomainTestState::HasB, true, true, t); }); + task3->AddEffect(effect2); + + SharedPtr task4 = MakeSharedPtr("Sub-task3"); + SharedPtr effect3 = + MakeSharedPtr("TestEffect3"s, EffectType::Permanent, [=](IContext& ctx, EffectType t) { + static_cast(ctx).SetState(DomainTestState::HasC, true, true, t); + }); + task4->AddEffect(effect3); + + SharedPtr task5 = MakeSharedPtr("Sub-task4"); + SharedPtr condition = MakeSharedPtr("TestCondition"s, [=](IContext& ctx) { + DomainTestContext& d = (DomainTestContext&)ctx; + return (d.Done() == true); + }); + task5->AddCondition(condition); + + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + domain.Add(task1, task3); + domain.Add(task1, task4); + domain.Add(task1, task5); + auto status = domain.FindPlan(*bctx, plan); + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(bctx->GetWorldStateChangeStack()[(int)DomainTestState::HasA].size() == 0); + Assert::IsTrue(bctx->GetWorldStateChangeStack()[(int)DomainTestState::HasB].size() == 0); + Assert::IsTrue(bctx->GetWorldStateChangeStack()[(int)DomainTestState::HasC].size() == 0); + Assert::IsTrue(bctx->GetWorldState().GetState(DomainTestState::HasA) == 0); + Assert::IsTrue(bctx->GetWorldState().GetState(DomainTestState::HasB) == 0); + Assert::IsTrue(bctx->GetWorldState().GetState(DomainTestState::HasC) == 0); + Assert::IsTrue(plan.size() == 0); + } + TEST_METHOD(FindPlanIfMTRsAreEqualThenReturnNullPlan_ExpectedBehavior) + { + auto bctx = MakeSharedPtr(); + SharedPtr ctx = StaticCastPtr(bctx); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + ctx->LastMTR().Add(1); + + // Root is a Selector that branch off into task1 selector or task2 sequence. + // MTR only tracks decomposition of compound tasks, so our MTR is only 1 layer deep here, + // Since both compound tasks decompose into primitive tasks. + SharedPtr task1 = MakeSharedPtr("Test1"); + SharedPtr task2 = MakeSharedPtr("Test2"); + + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr condition = MakeSharedPtr("TestCondition"s, [=](IContext& ctx) { + DomainTestContext& d = (DomainTestContext&)ctx; + return (d.Done() == true); + }); + task3->AddCondition(condition); + + SharedPtr task4 = MakeSharedPtr("Sub-task1"); + + SharedPtr task5 = MakeSharedPtr("Sub-task2"); + SharedPtr condition2 = MakeSharedPtr("TestCondition"s, [=](IContext& ctx) { + DomainTestContext& d = (DomainTestContext&)ctx; + return (d.Done() == true); + }); + task5->AddCondition(condition); + + domain.Add(domain.Root(), task1); + domain.Add(domain.Root(), task2); + domain.Add(task1, task3); + domain.Add(task2, task4); + domain.Add(task2, task5); + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx->MethodTraversalRecord().size() == 1); + Assert::IsTrue(ctx->MethodTraversalRecord()[0] == ctx->LastMTR()[0]); + } + + TEST_METHOD(PausePlan_ExpectedBehavior) + { + auto bctx = MakeSharedPtr(); + SharedPtr ctx = StaticCastPtr(bctx); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + + SharedPtr task1 = MakeSharedPtr("Test1"); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + SharedPtr task4 = MakeSharedPtr(); + + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + domain.Add(task1, task4); + domain.Add(task1, task3); + + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx->HasPausedPartialPlan()); + Assert::IsTrue(ctx->PartialPlanQueue().size() == 1); + auto tx = StaticCastPtr(task1); + ITask* t1ptr = tx.get(); + ITask* t2ptr = ctx->PartialPlanQueue().front().Task.get(); + Assert::AreEqual(t1ptr, t2ptr); + Assert::AreEqual(2, ctx->PartialPlanQueue().front().TaskIndex); + } + + TEST_METHOD(ContinuePausedPlan_ExpectedBehavior) + { + auto bctx = MakeSharedPtr(); + SharedPtr ctx = StaticCastPtr(bctx); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + + SharedPtr task1 = MakeSharedPtr("Test1"); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + SharedPtr task4 = MakeSharedPtr(); + + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + domain.Add(task1, task4); + domain.Add(task1, task3); + + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx->HasPausedPartialPlan()); + Assert::IsTrue(ctx->PartialPlanQueue().size() == 1); + auto tx = StaticCastPtr(task1); + ITask* t1ptr = tx.get(); + ITask* t2ptr = ctx->PartialPlanQueue().front().Task.get(); + Assert::AreEqual(t1ptr, t2ptr); + Assert::AreEqual(2, ctx->PartialPlanQueue().front().TaskIndex); + + status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + } + + TEST_METHOD(NestedPausePlan_ExpectedBehavior) + { + auto bctx = MakeSharedPtr(); + SharedPtr ctx = StaticCastPtr(bctx); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + SharedPtr task = MakeSharedPtr("Test1"); + SharedPtr task2 = MakeSharedPtr("Test2"); + SharedPtr task3 = MakeSharedPtr("Test3"); + + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + SharedPtr pausePlan = MakeSharedPtr(); + + domain.Add(domain.Root(), task); + domain.Add(task, task2); + domain.Add(task, subtask4); + + domain.Add(task2, task3); + domain.Add(task2, subtask3); + + domain.Add(task3, subtask1); + domain.Add(task3, pausePlan); + domain.Add(task3, subtask2); + + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx->HasPausedPartialPlan()); + Assert::IsTrue(ctx->PartialPlanQueue().size() == 2); + + auto theQueue = ctx->PartialPlanQueue(); + + ITask* t1ptr = task3.get(); + ITask* t2ptr = theQueue.front().Task.get(); + Assert::AreEqual(t1ptr, t2ptr); + Assert::AreEqual(2, theQueue.front().TaskIndex); + + theQueue.pop(); + t1ptr = task.get(); + t2ptr = theQueue.front().Task.get(); + Assert::AreEqual(t1ptr, t2ptr); + Assert::AreEqual(1, theQueue.front().TaskIndex); + } + TEST_METHOD(ContinueNestedPausePlan_ExpectedBehavior) + { + auto bctx = MakeSharedPtr(); + SharedPtr ctx = StaticCastPtr(bctx); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + + SharedPtr task = MakeSharedPtr("Test1"); + SharedPtr task2 = MakeSharedPtr("Test2"); + SharedPtr task3 = MakeSharedPtr("Test3"); + + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + SharedPtr pausePlan = MakeSharedPtr(); + + domain.Add(domain.Root(), task); + domain.Add(task, task2); + domain.Add(task, subtask4); + + domain.Add(task2, task3); + domain.Add(task2, subtask3); + + domain.Add(task3, subtask1); + domain.Add(task3, pausePlan); + domain.Add(task3, subtask2); + + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx->HasPausedPartialPlan()); + Assert::IsTrue(ctx->PartialPlanQueue().size() == 2); + + PartialPlanQueueType queueCopy = ctx->PartialPlanQueue(); + ITask* t1ptr = task3.get(); + ITask* t2ptr = queueCopy.front().Task.get(); + Assert::AreEqual(t1ptr, t2ptr); + Assert::AreEqual(2, queueCopy.front().TaskIndex); + + queueCopy.pop(); + t1ptr = task.get(); + t2ptr = queueCopy.front().Task.get(); + Assert::AreEqual(t1ptr, t2ptr); + Assert::AreEqual(1, queueCopy.front().TaskIndex); + + status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 2); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task4"s, plan.front()->Name()); + } + TEST_METHOD(ContinueMultipleNestedPausePlan_ExpectedBehavior) + { + auto bctx = MakeSharedPtr(); + SharedPtr ctx = StaticCastPtr(bctx); + Domain domain("Test"); + TaskQueueType plan; + bctx->Init(); + + SharedPtr task = MakeSharedPtr("Test1"); + SharedPtr task2 = MakeSharedPtr("Test2"); + SharedPtr task3 = MakeSharedPtr("Test3"); + SharedPtr task4 = MakeSharedPtr("Test4"); + + domain.Add(domain.Root(), task); + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + SharedPtr pausePlan1 = MakeSharedPtr(); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + domain.Add(task3, subtask1); + domain.Add(task3, pausePlan1); + domain.Add(task3, subtask2); + + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + domain.Add(task2, task3); + domain.Add(task2, subtask3); + + SharedPtr subtask5 = MakeSharedPtr("Sub-task5"); + SharedPtr pausePlan2 = MakeSharedPtr(); + SharedPtr subtask6 = MakeSharedPtr("Sub-task6"); + domain.Add(task4, subtask5); + domain.Add(task4, pausePlan2); + domain.Add(task4, subtask6); + + domain.Add(task, task2); + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + domain.Add(task, subtask4); + domain.Add(task, task4); + SharedPtr subtask7 = MakeSharedPtr("Sub-task7"); + domain.Add(task, subtask7); + + auto status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx->HasPausedPartialPlan()); + Assert::IsTrue(ctx->PartialPlanQueue().size() == 2); + + PartialPlanQueueType queueCopy = ctx->PartialPlanQueue(); + + ITask* t1ptr = task3.get(); + ITask* t2ptr = queueCopy.front().Task.get(); + Assert::AreEqual(t1ptr, t2ptr); + Assert::AreEqual(2, queueCopy.front().TaskIndex); + queueCopy.pop(); + t1ptr = task.get(); + Assert::AreEqual(t1ptr, queueCopy.front().Task.get()); + Assert::AreEqual(1, queueCopy.front().TaskIndex); + + status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 3); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task4"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task5"s, plan.front()->Name()); + + status = domain.FindPlan(*bctx, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 2); + Assert::AreEqual("Sub-task6"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task7"s, plan.front()->Name()); + } + }; +} // namespace FluidHTNCPPUnitTests \ No newline at end of file diff --git a/Fluid-HTNCPP.UnitTests/Fluid-HTNCPP.UnitTests.vcxproj b/Fluid-HTNCPP.UnitTests/Fluid-HTNCPP.UnitTests.vcxproj new file mode 100644 index 0000000..98322f0 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/Fluid-HTNCPP.UnitTests.vcxproj @@ -0,0 +1,205 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {402394C5-2D69-49F0-B2E2-239FAB4CC252} + Win32Proj + FluidHTNCPPUnitTests + 10.0 + NativeUnitTestProject + + + + DynamicLibrary + true + v142 + Unicode + false + + + DynamicLibrary + false + v142 + true + Unicode + false + + + DynamicLibrary + true + v142 + Unicode + false + + + DynamicLibrary + false + v142 + true + Unicode + false + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)$(Configuration)\$(Platform)\Output\Test\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\Test\ + + + true + $(SolutionDir)$(Configuration)\$(Platform)\Output\Test\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\Test\ + + + false + $(SolutionDir)$(Configuration)\$(Platform)\Output\Test\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\Test\ + + + false + $(SolutionDir)$(Configuration)\$(Platform)\Output\Test\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\Test\ + + + + Use + Level4 + true + $(SolutionDir)Fluid-HTNCPP;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;%(PreprocessorDefinitions) + true + pch.h + true + stdcpp17 + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Use + Level4 + true + $(SolutionDir)Fluid-HTNCPP;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + _DEBUG;%(PreprocessorDefinitions) + true + pch.h + true + stdcpp17 + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Use + Level4 + true + true + true + $(SolutionDir)Fluid-HTNCPP;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;%(PreprocessorDefinitions) + true + pch.h + true + stdcpp17 + + + Windows + true + true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Use + Level4 + true + true + true + $(SolutionDir)Fluid-HTNCPP;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + NDEBUG;%(PreprocessorDefinitions) + true + pch.h + true + stdcpp17 + + + Windows + true + true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + + + + + + + {0c469b65-6d39-4667-8d00-9ec963c518e3} + + + + + + \ No newline at end of file diff --git a/Fluid-HTNCPP.UnitTests/Fluid-HTNCPP.UnitTests.vcxproj.filters b/Fluid-HTNCPP.UnitTests/Fluid-HTNCPP.UnitTests.vcxproj.filters new file mode 100644 index 0000000..0953159 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/Fluid-HTNCPP.UnitTests.vcxproj.filters @@ -0,0 +1,63 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/Fluid-HTNCPP.UnitTests/FuncConditionTests.cpp b/Fluid-HTNCPP.UnitTests/FuncConditionTests.cpp new file mode 100644 index 0000000..485cf72 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/FuncConditionTests.cpp @@ -0,0 +1,43 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "Conditions/Condition.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + + +namespace FluidHTNCPPUnitTests +{ +TEST_CLASS(FuncConditionTests) +{ + TEST_METHOD(SetsName_ExpectedBehavior) + { + auto c = MakeSharedPtr("Name"s, nullptr); + + Assert::AreEqual("Name"s, c->Name()); + } + + TEST_METHOD(IsValidFailsWithoutFunctionPtr_ExpectedBehavior) + { + auto ctx = MakeSharedPtr(); + auto c = MakeSharedPtr("Name"s, nullptr); + + auto result = c->IsValid(*ctx); + + Assert::AreEqual(false, result); + } + + TEST_METHOD(IsValidCallsInternalFunctionPtr_ExpectedBehavior) + { + DomainTestContext ctx; + auto c = MakeSharedPtr("Name"s, + [](IContext& ctx) { return (static_cast(ctx).Done() == false); }); + auto result = c->IsValid(ctx); + + Assert::AreEqual(true, result); + } +} ; +} // namespace FluidHTNCPPUnitTests diff --git a/Fluid-HTNCPP.UnitTests/FuncOperatorTests.cpp b/Fluid-HTNCPP.UnitTests/FuncOperatorTests.cpp new file mode 100644 index 0000000..c9cd75f --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/FuncOperatorTests.cpp @@ -0,0 +1,52 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "Operators/Operator.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + + + +namespace FluidHTNCPPUnitTests +{ +TEST_CLASS(FuncOperatorTests) +{ + TEST_METHOD(UpdateDoesNothingWithoutFunctionPtr_ExpectedBehavior) + { + DomainTestContext ctx; + auto e = MakeSharedPtr(nullptr, nullptr); + e->Update(ctx); + } + +TEST_METHOD(StopDoesNothingWithoutFunctionPtr_ExpectedBehavior) +{ + DomainTestContext ctx; + auto e = MakeSharedPtr(nullptr, nullptr); + + e->Stop(ctx); +} +TEST_METHOD(UpdateReturnsStatusInternalFunctionPtr_ExpectedBehavior) +{ + DomainTestContext ctx; + auto e = MakeSharedPtr([=](IContext&) { return TaskStatus::Success; }, nullptr); + + auto status = e->Update(ctx); + + Assert::AreEqual((int)TaskStatus::Success, (int)status); +} + +TEST_METHOD(StopCallsInternalFunctionPtr_ExpectedBehavior) +{ + DomainTestContext ctx; + auto e = MakeSharedPtr(nullptr, [](IContext&ctx ) { static_cast(ctx).Done() = true; }); + + e->Stop(ctx); + + Assert::AreEqual(true, ctx.Done()); +} +} +; +} // namespace FluidHTNCPPUnitTests diff --git a/Fluid-HTNCPP.UnitTests/PlannerTests.cpp b/Fluid-HTNCPP.UnitTests/PlannerTests.cpp new file mode 100644 index 0000000..785647e --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/PlannerTests.cpp @@ -0,0 +1,461 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "CoreIncludes/Domain.h" +#include "Planners/Planner.h" +#include "Tasks/CompoundTasks/Selector.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +namespace FluidHTNCPPUnitTests +{ + TEST_CLASS(PlannerTests) + { + TEST_METHOD(GetPlanReturnsClearInstanceAtStart_ExpectedBehavior) + { + Planner planner; + auto plan = planner.GetPlan(); + + Assert::IsTrue(plan.size() == 0); + } + TEST_METHOD(GetCurrentTaskReturnsNullAtStart_ExpectedBehavior) + { + Planner planner; + auto task = planner.GetCurrentTask(); + + Assert::IsTrue(task == nullptr); + } + TEST_METHOD(TickWithoutInitializedContextThrowsException_ExpectedBehavior) + { + DomainTestContext ctx; + Domain domain("Test"s); + Planner planner; + Assert::ExpectException([&]() { planner.Tick(domain, ctx); }); + } + TEST_METHOD(TickWithEmptyDomain_ExpectedBehavior) + { + DomainTestContext ctx; + Domain domain("Test"s); + Planner planner; + ctx.Init(); + planner.Tick(domain, ctx); + } + + TEST_METHOD(TickWithPrimitiveTaskWithoutOperator_ExpectedBehavior) + { + DomainTestContext ctx; + Domain domain("Test"s); + Planner planner; + ctx.Init(); + SharedPtr task1 = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + auto currentTask = planner.GetCurrentTask(); + + Assert::IsTrue(currentTask == nullptr); + Assert::IsTrue(planner.LastStatus() == TaskStatus::Failure); + } + + TEST_METHOD(TickWithFuncOperatorWithNullFunc_ExpectedBehavior) + { + DomainTestContext ctx; + Domain domain("Test"s); + Planner planner; + ctx.Init(); + + SharedPtr task1 = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f = MakeSharedPtr(nullptr); + + task2->SetOperator(f); + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + auto currentTask = planner.GetCurrentTask(); + + Assert::IsTrue(currentTask == nullptr); + Assert::IsTrue(planner.LastStatus() == TaskStatus::Failure); + } + TEST_METHOD(TickWithDefaultSuccessOperatorWontStackOverflows_ExpectedBehavior) + { + DomainTestContext ctx; + Domain domain("Test"s); + Planner planner; + ctx.Init(); + SharedPtr task1 = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f = MakeSharedPtr([](IContext& ) { return TaskStatus::Success; }); + task2->SetOperator(f); + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + auto currentTask = planner.GetCurrentTask(); + + Assert::IsTrue(currentTask == nullptr); + Assert::IsTrue(planner.LastStatus() == TaskStatus::Success); + } + + TEST_METHOD(TickWithDefaultContinueOperator_ExpectedBehavior) + { + DomainTestContext ctx; + Domain domain("Test"s); + Planner planner; + ctx.Init(); + SharedPtr task1 = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f = MakeSharedPtr([](IContext& ) { return TaskStatus::Continue; }); + + task2->SetOperator(f); + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + auto currentTask = planner.GetCurrentTask(); + + Assert::IsTrue(currentTask != nullptr); + Assert::IsTrue(planner.LastStatus() == TaskStatus::Continue); + } + + TEST_METHOD(OnNewPlan_ExpectedBehavior) + { + DomainTestContext ctx; + Domain domain("Test"s); + Planner planner; + bool test = false; + ctx.Init(); + planner.OnNewPlan = [&](TaskQueueType p) { test = (p.size() == 1); }; + SharedPtr task1 = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + task2->SetOperator(f); + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + TEST_METHOD(OnReplacePlan_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnReplacePlan = [&](TaskQueueType op, SharedPtr ct, TaskQueueType p) { + test = ((op.size() == 0) && (ct != nullptr) && (p.size() == 1)); + }; + SharedPtr task1 = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + task3->AddCondition(c); + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + + SharedPtr f1 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + + task3->SetOperator(f1); + task4->SetOperator(f2); + domain.Add(domain.Root(), task1); + domain.Add(domain.Root(), task2); + domain.Add(task1, task3); + domain.Add(task2, task4); + + ctx.Done() = true; + planner.Tick(domain, ctx); + + ctx.Done() = false; + ctx.IsDirty() = true; + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + TEST_METHOD(OnNewTask_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnNewTask = [&](SharedPtr&t) { test = (t->Name() == "Sub-task"); }; + SharedPtr task1 = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + + task2->SetOperator(f); + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + Assert::IsTrue(test); + } + TEST_METHOD(OnNewTaskConditionFailed_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnNewTaskConditionFailed = [&](SharedPtr& t, SharedPtr&) { + test = (t->Name() == "Sub-task1"s); + }; + SharedPtr task1 = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + task3->AddCondition(c); + + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + + SharedPtr f = MakeSharedPtr([](IContext&) { return TaskStatus::Success; }); + task3->SetOperator(f); + // Note that one should not use AddEffect on types that's not part of WorldState unless you + // know what you're doing. Outside of the WorldState, we don't get automatic trimming of + // state change. This method is used here only to invoke the desired callback, not because + // its correct practice. + SharedPtr effect = + MakeSharedPtr("TestEffect"s, EffectType::PlanAndExecute, [](IContext& context, EffectType ) { + static_cast(context).Done() = true; + }); + task3->AddEffect(effect); + + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + task4->SetOperator(f2); + domain.Add(domain.Root(), task1); + domain.Add(domain.Root(), task2); + domain.Add(task1, task3); + domain.Add(task2, task4); + + ctx.Done() = true; + planner.Tick(domain, ctx); + + ctx.Done() = false; + ctx.IsDirty() = true; + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + TEST_METHOD(OnStopCurrentTask_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnStopCurrentTask = [&](SharedPtr& t) { test = (t->Name() == "Sub-task2"); }; + + SharedPtr task1 = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + task3->AddCondition(c); + + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + + SharedPtr f1 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + task3->SetOperator(f1); + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + task4->SetOperator(f2); + + domain.Add(domain.Root(), task1); + domain.Add(domain.Root(), task2); + domain.Add(task1, task3); + domain.Add(task2, task4); + + ctx.Done() = true; + planner.Tick(domain, ctx); + + ctx.Done() = false; + ctx.IsDirty() = true; + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + TEST_METHOD(OnCurrentTaskCompletedSuccessfully_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnCurrentTaskCompletedSuccessfully = [&](SharedPtr& t) { + test = (t->Name() == "Sub-task1"s); + }; + SharedPtr task1 = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + task3->AddCondition(c); + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + + SharedPtr f1 = MakeSharedPtr([](IContext&) { return TaskStatus::Success; }); + task3->SetOperator(f1); + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + task4->SetOperator(f2); + + domain.Add(domain.Root(), task1); + domain.Add(domain.Root(), task2); + domain.Add(task1, task3); + domain.Add(task2, task4); + + ctx.Done() = true; + planner.Tick(domain, ctx); + + ctx.Done() = false; + ctx.IsDirty() = true; + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + + TEST_METHOD(OnApplyEffect_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnApplyEffect = [&](SharedPtr& e) { test = (e->Name() == "TestEffect"s); }; + + SharedPtr task1 = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& context) { + uint8_t trudat = 1; + return static_cast(context).HasState( + DomainTestState::HasA, + trudat); + }); + + task3->AddCondition(c); + + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + + SharedPtr f1 = MakeSharedPtr([](IContext&) { return TaskStatus::Success; }); + task3->SetOperator(f1); + + SharedPtr eff = + MakeSharedPtr("TestEffect"s, EffectType::PlanAndExecute, [](IContext& context, EffectType type) { + static_cast(context).SetState(DomainTestState::HasA, true, true, type); + }); + + task3->AddEffect(eff); + + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + task4->SetOperator(f2); + + domain.Add(domain.Root(), task1); + domain.Add(domain.Root(), task2); + domain.Add(task1, task3); + domain.Add(task2, task4); + + ctx.SetContextState(ContextState::Executing); + ctx.SetState(DomainTestState::HasA, true, true, EffectType::Permanent); + planner.Tick(domain, ctx); + + ctx.SetContextState(ContextState::Executing); + ctx.SetState(DomainTestState::HasA, false, true, EffectType::Permanent); + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + + TEST_METHOD(OnCurrentTaskFailed_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnCurrentTaskFailed = [&](SharedPtr& t) { test = (t->Name() == "Sub-task"s); }; + SharedPtr task1 = MakeSharedPtr("Test1"s); + + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Failure; }); + + task2->SetOperator(f2); + + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + + TEST_METHOD(OnCurrentTaskContinues_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnCurrentTaskContinues = [&](SharedPtr& t) { test = (t->Name() == "Sub-task"s); }; + SharedPtr task1 = MakeSharedPtr("Test1"s); + + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + + task2->SetOperator(f2); + + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + + TEST_METHOD(OnCurrentTaskExecutingConditionFailed_ExpectedBehavior) + { + bool test = false; + Domain domain("Test"s); + DomainTestContext ctx; + Planner planner; + ctx.Init(); + planner.OnCurrentTaskExecutingConditionFailed = [&](SharedPtr& t, SharedPtr& c) { + test = ((t->Name() == "Sub-task"s) && (c->Name() == "TestCondition"s)); + }; + SharedPtr task1 = MakeSharedPtr("Test1"s); + + SharedPtr task2 = MakeSharedPtr("Sub-task"); + SharedPtr f2 = MakeSharedPtr([](IContext&) { return TaskStatus::Continue; }); + task2->SetOperator(f2); + + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& context) { + return static_cast(context).Done(); + }); + + task2->AddExecutingCondition(c); + domain.Add(domain.Root(), task1); + domain.Add(task1, task2); + + planner.Tick(domain, ctx); + + Assert::IsTrue(test); + } + }; +} diff --git a/Fluid-HTNCPP.UnitTests/PrimitiveTaskTests.cpp b/Fluid-HTNCPP.UnitTests/PrimitiveTaskTests.cpp new file mode 100644 index 0000000..e95a270 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/PrimitiveTaskTests.cpp @@ -0,0 +1,132 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "Effects/Effect.h" +#include "Conditions/Condition.h" +#include "Operators/Operator.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +namespace FluidHTNCPPUnitTests +{ + TEST_CLASS(PrimitiveTaskTests) + { + TEST_METHOD(AddCondition_ExpectedBehavior) + { + auto task = MakeSharedPtr("Test"s); + SharedPtr c = MakeSharedPtr("Name"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + bool bRet = task->AddCondition(c); + + Assert::IsTrue(bRet); + Assert::IsTrue(task->Conditions().size() == 1); + } + TEST_METHOD(AddExecutingCondition_ExpectedBehavior) + { + auto task = MakeSharedPtr("Test"s); + SharedPtr c = MakeSharedPtr("Name"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + + bool bRet = task->AddExecutingCondition(c); + Assert::IsTrue(bRet); + Assert::IsTrue(task->ExecutingConditions().size() == 1); + } + + TEST_METHOD(AddEffect_ExpectedBehavior) + { + auto task = MakeSharedPtr("Test"s); + SharedPtr e = MakeSharedPtr("Name"s, EffectType::Permanent , [](IContext& ctx, EffectType eff) { + (void)eff; + static_cast(ctx).Done() = true; + }); + + bool bRet = task->AddEffect(e); + Assert::IsTrue(bRet); + Assert::IsTrue(task->Effects().size() == 1); + } + TEST_METHOD(SetOperator_ExpectedBehavior) + { + auto task = MakeSharedPtr("Test"s); + SharedPtr o = MakeSharedPtr(nullptr, nullptr); + + task->SetOperator(o); + + Assert::IsTrue(task->Operator() != nullptr); + } + + TEST_METHOD(SetOperatorThrowsExceptionIfAlreadySet_ExpectedBehavior) + { + auto task = MakeSharedPtr("Test"s); + + SharedPtr o = MakeSharedPtr(nullptr, nullptr); + + task->SetOperator(o); + + Assert::ExpectException([=]() { + SharedPtr o2 = MakeSharedPtr(nullptr, nullptr); + task->SetOperator(o2); + }); + } + + TEST_METHOD(ApplyEffects_ExpectedBehavior) + { + DomainTestContext ctx; + auto task = MakeSharedPtr("Test"s); + SharedPtr e = MakeSharedPtr("Name"s, EffectType::Permanent, [](IContext& ctx, EffectType e) { + (void)e; + static_cast(ctx).Done() = true; + }); + + task->AddEffect(e); + task->ApplyEffects(ctx); + + Assert::AreEqual(true, ctx.Done()); + } + TEST_METHOD(StopWithValidOperator_ExpectedBehavior) + { + DomainTestContext ctx; + auto task = MakeSharedPtr("Test"s); + SharedPtr o = + MakeSharedPtr(nullptr, [](IContext& ctx) { static_cast(ctx).Done() = true; }); + + task->SetOperator(o); + task->Stop(ctx); + + Assert::IsTrue(task->Operator() != nullptr); + Assert::AreEqual(true, ctx.Done()); + } + + TEST_METHOD(StopWithNullOperator_ExpectedBehavior) + { + DomainTestContext ctx; + auto task = MakeSharedPtr("Test"s); + task->Stop(ctx); + } + TEST_METHOD( IsValid_ExpectedBehavior) + { + DomainTestContext ctx; + auto task = MakeSharedPtr("Test"s); + SharedPtr c = MakeSharedPtr("Name"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + SharedPtr c2 = MakeSharedPtr("Name"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + + task->AddCondition(c); + bool expectTrue = task->IsValid(ctx); + + task->AddCondition(c2); + bool expectFalse = task->IsValid(ctx); + + Assert::IsTrue(expectTrue); + Assert::IsFalse(expectFalse); + } + }; +} diff --git a/Fluid-HTNCPP.UnitTests/RandomSelectorTests.cpp b/Fluid-HTNCPP.UnitTests/RandomSelectorTests.cpp new file mode 100644 index 0000000..b92639c --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/RandomSelectorTests.cpp @@ -0,0 +1,71 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Tasks/CompoundTasks/RandomSelector.h" +#include "CoreIncludes/BaseDomainBuilder.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +namespace FluidHTNCPPUnitTests +{ +TEST_CLASS(RandomSelectorTest) +{ + + TEST_METHOD(RandomSelect_ExpectedBehavior) + { + BaseDomainBuilder builder("tests"); + DomainTestContext ctx; + ctx.Init(); + + builder.AddRandomSelector("random"); + + builder.AddAction("get a"); + builder.AddCondition("has not A", [](IContext& ctx) { + return (static_cast(ctx).HasStateOneParam(DomainTestState::HasA) == false); + }); + builder.AddOperator([](IContext&) { return TaskStatus::Success; }); + builder.End(); + builder.AddAction("get b"); + builder.AddCondition("has not B", [](IContext& ctx) { + return (static_cast(ctx).HasStateOneParam(DomainTestState::HasB) == false); + }); + builder.AddOperator([](IContext&) { return TaskStatus::Success; }); + builder.End(); + builder.AddAction("get c"); + builder.AddCondition("has not C", [](IContext& ctx) { + return (static_cast(ctx).HasStateOneParam(DomainTestState::HasC) == false); + }); + builder.AddOperator([](IContext&) { return TaskStatus::Success; }); + builder.End(); + builder.End(); + auto domain = builder.Build(); + + int aCount = 0; + int bCount = 0; + int cCount = 0; + for (int i = 0; i < 1000; i++) + { + TaskQueueType plan; + auto status = domain->FindPlan(ctx, plan); + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + + auto name = plan.front()->Name(); + if (name == "get a"s) + aCount++; + if (name == "get b"s) + bCount++; + if (name == "get c"s) + cCount++; + + Assert::IsTrue(name == "get a" || name == "get b" || name == "get c"); + plan = TaskQueueType(); + } + + // With 1000 iterations, the chance of any of these counts being 0 is suuuper slim. + Assert::IsTrue(aCount > 0 && bCount > 0 && cCount > 0); + } +}; +} // namespace FluidHTNCPPUnitTests diff --git a/Fluid-HTNCPP.UnitTests/SelectorTests.cpp b/Fluid-HTNCPP.UnitTests/SelectorTests.cpp new file mode 100644 index 0000000..1423fc4 --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/SelectorTests.cpp @@ -0,0 +1,477 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "CoreIncludes/Domain.h" +#include "Planners/Planner.h" +#include "Tasks/CompoundTasks/Selector.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +namespace FluidHTNCPPUnitTests +{ + TEST_CLASS(SelectorTests) + { + TEST_METHOD(AddCondition_ExpectedBehavior) + { + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + bool bRet = task->AddCondition(c); + Assert::IsTrue(bRet); + Assert::IsTrue(task->Conditions().size() == 1); + } + + TEST_METHOD(AddSubtask_ExpectedBehavior) + { + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + bool bRet = task->AddSubTask(task2); + + Assert::IsTrue(bRet); + Assert::IsTrue(task->Subtasks().size() == 1); + } + + TEST_METHOD(IsValidFailsWithoutSubtasks_ExpectedBehavior) + { + DomainTestContext ctx; + SharedPtr task = MakeSharedPtr("Test"s); + + Assert::IsFalse(task->IsValid(ctx)); + } + + TEST_METHOD(IsValid_ExpectedBehavior) + { + DomainTestContext ctx; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + task->AddSubTask(task2); + + Assert::IsTrue(task->IsValid(ctx)); + } + + TEST_METHOD(DecomposeWithNoSubtasks_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Failed); + Assert::IsTrue(plan.size() == 0); + } + + TEST_METHOD(DecomposeWithSubtasks_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + task->AddSubTask(task2); + task->AddSubTask(task3); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + } + + TEST_METHOD(DecomposeWithSubtasks2_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"s); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + task->AddSubTask(task2); + task->AddSubTask(task3); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + } + + TEST_METHOD(DecomposeWithSubtasks3_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + + task2->AddCondition(c); + + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + task->AddSubTask(task2); + task->AddSubTask(task3); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + } + + TEST_METHOD(DecomposeMTRFails_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task2->AddCondition(c); + + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + task->AddSubTask(task2); + task->AddSubTask(task3); + ctx.LastMTR().Add(0); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 1); + Assert::AreEqual(-1, ctx.MethodTraversalRecord()[0]); + } + TEST_METHOD( DecomposeDebugMTRFails_ExpectedBehavior) + { + MyDebugContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task2->AddCondition(c); + task->AddSubTask(task2); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + task->AddSubTask(task3); + + ctx.LastMTR().Add(0); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.MTRDebug().size() == 1); + Assert::IsTrue(ctx.MTRDebug()[0].find("REPLAN FAIL"s) != StringType::npos); + Assert::IsTrue(ctx.MTRDebug()[0].find("Sub-task2"s) != StringType::npos); + } + + TEST_METHOD(DecomposeMTRSucceedsWhenEqual_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task2->AddCondition(c); + + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + task->AddSubTask(task2); + task->AddSubTask(task3); + ctx.LastMTR().Add(1); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 0); + Assert::IsTrue(plan.size() == 1); + } + + TEST_METHOD(DecomposeCompoundSubtaskSucceeds_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task3->AddCondition(c); + task2->AddSubTask(task3); + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + task2->AddSubTask(task4); + + task->AddSubTask(task2); + SharedPtr task5 = MakeSharedPtr("Sub-task3"); + task->AddSubTask(task5); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 1); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 0); + } + + TEST_METHOD(DecomposeCompoundSubtaskFails_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task3->AddCondition(c); + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + SharedPtr c2 = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task4->AddCondition(c2); + + task2->AddSubTask(task3); + task2->AddSubTask(task4); + + task->AddSubTask(task2); + SharedPtr task5 = MakeSharedPtr("Sub-task3"); + task->AddSubTask(task5); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task3"s, plan.front()->Name()); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 0); + } + + TEST_METHOD(DecomposeNestedCompoundSubtaskFails_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + + SharedPtr task4 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task4->AddCondition(c); + SharedPtr task5 = MakeSharedPtr("Sub-task2"); + task5->AddCondition(c); + + task3->AddSubTask(task4); + task3->AddSubTask(task5); + + task2->AddSubTask(task3); + SharedPtr task6 = MakeSharedPtr("Sub-task3"); + task6->AddCondition(c); + task2->AddSubTask(task6); + + task->AddSubTask(task2); + SharedPtr task7 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(task7); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task4"s, plan.front()->Name()); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 0); + } + + TEST_METHOD(DecomposeCompoundSubtaskBeatsLastMTR_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task3->AddCondition(c); + + task2->AddSubTask(task3); + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + task2->AddSubTask(task4); + + task->AddSubTask(task2); + SharedPtr task5 = MakeSharedPtr("Sub-task3"); + task->AddSubTask(task5); + + ctx.LastMTR().Add(1); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 1); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 0); + } + + TEST_METHOD( DecomposeCompoundSubtaskEqualToLastMTR_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task3->AddCondition(c); + task2->AddSubTask(task3); + + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + task2->AddSubTask(task4); + + task->AddSubTask(task2); + SharedPtr task5 = MakeSharedPtr("Sub-task3"); + task->AddSubTask(task5); + + ctx.LastMTR().Add(0); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 1); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 0); + } + + TEST_METHOD(DecomposeCompoundSubtaskLoseToLastMTR_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task3->AddCondition(c); + task2->AddSubTask(task3); + + SharedPtr task4 = MakeSharedPtr("Sub-task2"); + task2->AddSubTask(task4); + + SharedPtr task5 = MakeSharedPtr("Sub-task3"); + task5->AddCondition(c); + + task->AddSubTask(task5); + task->AddSubTask(task2); + + ctx.LastMTR().Add(0); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 1); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == -1); + } + TEST_METHOD(DecomposeCompoundSubtaskWinOverLastMTR_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr rootTask = MakeSharedPtr("Root"s); + SharedPtr task = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr subTask31 = MakeSharedPtr("Sub-task3-1"); + SharedPtr subTask32 = MakeSharedPtr("Sub-task3-2"); + SharedPtr subTask21 = MakeSharedPtr("Sub-task2-1"); + SharedPtr subTask22 = MakeSharedPtr("Sub-task2-2"); + SharedPtr subTask11 = MakeSharedPtr("Sub-task1-1"); + SharedPtr ctrue = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + SharedPtr cfalse = MakeSharedPtr("Done == false"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + + subTask31->AddCondition(ctrue); + task3->AddSubTask(subTask31); + task3->AddSubTask(subTask32); + + subTask21->AddCondition(ctrue); + task2->AddSubTask(subTask21); + task2->AddSubTask(subTask22); + + task->AddSubTask(task2); + task->AddSubTask(task3); + subTask11->AddCondition(cfalse); + task->AddSubTask(subTask11); + + rootTask->AddSubTask(task); + + ctx.LastMTR().Add(0); + ctx.LastMTR().Add(1); + ctx.LastMTR().Add(0); + + // In this test, we prove that [0, 0, 1] beats [0, 1, 0] + auto status = rootTask->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + } + + TEST_METHOD( DecomposeCompoundSubtaskLoseToLastMTR2_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + SharedPtr rootTask = MakeSharedPtr("Root"s); + SharedPtr task = MakeSharedPtr("Test1"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr subTask21 = MakeSharedPtr("Sub-task2-1"); + SharedPtr subTask11 = MakeSharedPtr("Sub-task1-1"); + SharedPtr ctrue = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + + subTask21->AddCondition(ctrue); + task2->AddSubTask(subTask21); + + subTask11->AddCondition(ctrue); + + task->AddSubTask(subTask11); + task->AddSubTask(task); + + rootTask->AddSubTask(task); + + ctx.LastMTR().Add(0); + ctx.LastMTR().Add(1); + ctx.LastMTR().Add(0); + + // We expect this test to be rejected, because [0,1,1] shouldn't beat [0,1,0] + auto status = rootTask->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 3); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 0); + Assert::IsTrue(ctx.MethodTraversalRecord()[1] == 1); + Assert::IsTrue(ctx.MethodTraversalRecord()[2] == -1); + } + }; +} \ No newline at end of file diff --git a/Fluid-HTNCPP.UnitTests/SequenceTests.cpp b/Fluid-HTNCPP.UnitTests/SequenceTests.cpp new file mode 100644 index 0000000..33e139e --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/SequenceTests.cpp @@ -0,0 +1,776 @@ + +#include "pch.h" +#include "CppUnitTest.h" +#include "Contexts/BaseContext.h" +#include "CoreIncludes/Domain.h" +#include "Planners/Planner.h" +#include "Tasks/CompoundTasks/Sequence.h" +#include "Tasks/CompoundTasks/Selector.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "Tasks/CompoundTasks/PausePlanTask.h" +#include "Effects/Effect.h" +#include "DomainTestContext.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +using namespace FluidHTN; + +namespace Microsoft::VisualStudio::CppUnitTestFramework +{ +template <> +std::wstring ToString(ITask* eff) +{ + if (eff) + { + switch (eff->GetType()) + { + case ITaskDerivedClassName::CompoundTask: + return L"ITaskDerivedClassName::CompoundTask"; + case ITaskDerivedClassName::ITaskType: + return L"ITaskDerivedClassName::ITaskType"; + case ITaskDerivedClassName::PausePlanTask: + return L"ITaskDerivedClassName::PausePlanTask"; + case ITaskDerivedClassName::PrimitiveTask: + return L"ITaskDerivedClassName::PrimitiveTask"; + case ITaskDerivedClassName::SelectorCompoundTask: + return L"ITaskDerivedClassName::SelectorCompoundTask"; + case ITaskDerivedClassName::SequenceCompoundTask: + return L"ITaskDerivedClassName::SequenceCompoundTask"; + case ITaskDerivedClassName::Slot: + return L"ITaskDerivedClassName::Slot"; + case ITaskDerivedClassName::TaskRoot: + return L"ITaskDerivedClassName::TaskRoot"; + } + } + return L"Unknown value"; +} +} // namespace Microsoft::VisualStudio::CppUnitTestFramework +namespace FluidHTNCPPUnitTests +{ + TEST_CLASS(SequenceTests) + { + TEST_METHOD(AddCondition_ExpectedBehavior) + { + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr c = MakeSharedPtr("TestCondition"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == false); + }); + bool bRet = task->AddCondition(c); + Assert::IsTrue(bRet); + Assert::IsTrue(task->Conditions().size() == 1); + } + + TEST_METHOD(AddSubtask_ExpectedBehavior) + { + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + bool bRet = task->AddSubTask(task2); + + Assert::IsTrue(bRet); + Assert::IsTrue(task->Subtasks().size() == 1); + } + + TEST_METHOD(IsValidFailsWithoutSubtasks_ExpectedBehavior) + { + DomainTestContext ctx; + SharedPtr task = MakeSharedPtr("Test"s); + + Assert::IsFalse(task->IsValid(ctx)); + } + + TEST_METHOD(IsValid_ExpectedBehavior) + { + DomainTestContext ctx; + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task"); + task->AddSubTask(task2); + + Assert::IsTrue(task->IsValid(ctx)); + } + + TEST_METHOD(DecomposeRequiresContextInitFails_ExpectedBehavior) + { + DomainTestContext ctx; + SharedPtr task = MakeSharedPtr("Test"s); + Assert::ExpectException([&]() { + TaskQueueType plan; + task->Decompose(ctx, 0, plan); + }); + } + + TEST_METHOD(DecomposeWithNoSubtasks_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + SharedPtr task = MakeSharedPtr("Test"s); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Failed); + Assert::IsTrue(plan.size() == 0); + } + + TEST_METHOD(DecomposeWithSubtasks_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + + task->AddSubTask(task2); + task->AddSubTask(task3); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 2); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + } + + TEST_METHOD(DecomposeNestedSubtasks_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + + SharedPtr task4 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task4->AddCondition(c); + task3->AddSubTask(task4); + SharedPtr task5 = MakeSharedPtr("Sub-task2"); + task3->AddSubTask(task5); + + task2->AddSubTask(task3); + SharedPtr task6 = MakeSharedPtr("Sub-task3"); + task2->AddSubTask(task6); + + task->AddSubTask(task2); + SharedPtr task7 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(task7); + + auto status = task->Decompose(ctx, 0,plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 2); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task4"s, plan.front()->Name()); + } + + TEST_METHOD(DecomposeWithSubtasksOneFail_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + task->AddSubTask(task2); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task3->AddCondition(c); + task->AddSubTask(task3); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Failed); + Assert::IsTrue(plan.size() == 0); + } + + TEST_METHOD(DecomposeWithSubtasksCompoundSubtaskFails_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr task3 = MakeSharedPtr("Sub-task2"); + task->AddSubTask(task2); + task->AddSubTask(task3); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Failed); + Assert::IsTrue(plan.size() == 0); + } + + TEST_METHOD(DecomposeFailureReturnToPreviousWorldState_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + ctx.SetState(DomainTestState::HasA, true,true, EffectType::PlanAndExecute); + ctx.SetState(DomainTestState::HasB, true,true, EffectType::Permanent); + ctx.SetState(DomainTestState::HasC, true,true, EffectType::PlanOnly); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Sub-task1"); + SharedPtr eff = + MakeSharedPtr("TestEffect"s, EffectType::Permanent, [](IContext& context, EffectType ) { + static_cast(context).SetState(DomainTestState::HasA, + false, + true, + EffectType::PlanOnly); + }); + task2->AddEffect(eff) ; + task->AddSubTask(task2); + SharedPtr task3 = MakeSharedPtr("Sub-task2"s); + task->AddSubTask(task3); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Failed); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasA].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int)DomainTestState::HasB].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int)DomainTestState::HasC].size() == 1); + Assert::AreEqual(1,(int) ctx.GetStateDTS(DomainTestState::HasA)); + Assert::AreEqual(1, (int)ctx.GetStateDTS(DomainTestState::HasB)); + Assert::AreEqual(1, (int)ctx.GetStateDTS(DomainTestState::HasC)); + } + + TEST_METHOD(DecomposeNestedCompoundSubtaskLoseToMTR_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr task4 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task4->AddCondition(c); + task3->AddSubTask(task4); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + task3->AddSubTask(subtask2); + + task2->AddSubTask(task3); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + task2->AddSubTask(subtask3); + + task->AddSubTask(task2); + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(subtask4); + + ctx.LastMTR().Add(0); + ctx.LastMTR().Add(0); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 2); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 0); + Assert::IsTrue(ctx.MethodTraversalRecord()[1] == -1); + } + + TEST_METHOD(DecomposeNestedCompoundSubtaskLoseToMTR2_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr task4 = MakeSharedPtr("Sub-task1"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + task4->AddCondition(c); + task3->AddSubTask(task4); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + task3->AddSubTask(subtask2); + + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + subtask3->AddCondition(c); + task2->AddSubTask(subtask3); + task2->AddSubTask(task3); + + task->AddSubTask(task2); + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(subtask4); + + ctx.LastMTR().Add(1); + ctx.LastMTR().Add(0); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 2); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 1); + Assert::IsTrue(ctx.MethodTraversalRecord()[1] == -1); + } + + TEST_METHOD(DecomposeNestedCompoundSubtaskEqualToMTR_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + subtask2->AddCondition(c); + task3->AddSubTask(subtask2); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + task3->AddSubTask(subtask3); + + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + subtask1->AddCondition(c); + + task2->AddSubTask(subtask1); + task2->AddSubTask(task3); + + task->AddSubTask(task2); + + SharedPtr task6 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(task6); + + ctx.LastMTR().Add(1); + ctx.LastMTR().Add(1); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Succeeded); + Assert::IsTrue(plan.size() == 2); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 1); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 1); + Assert::AreEqual("Sub-task3"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task4"s, plan.front()->Name()); + } + + TEST_METHOD(DecomposeNestedCompoundSubtaskLoseToMTRReturnToPreviousWorldState_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + ctx.SetState(DomainTestState::HasA, true,true, EffectType::PlanAndExecute); + ctx.SetState(DomainTestState::HasB, true,true, EffectType::Permanent); + ctx.SetState(DomainTestState::HasC, true,true, EffectType::PlanOnly); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + subtask2->AddCondition(c); + task3->AddSubTask(subtask2); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + SharedPtr eff = + MakeSharedPtr("TestEffect"s, EffectType::Permanent, [](IContext& ctx, EffectType ) { + static_cast(ctx).SetState(DomainTestState::HasA, false, true, EffectType::PlanOnly); + }); + subtask3->AddEffect(eff); + task3->AddSubTask(subtask3); + + task2->AddSubTask(task3); + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + SharedPtr eff2 = + MakeSharedPtr("TestEffect2"s, EffectType::Permanent, [](IContext& ctx, EffectType ) { + static_cast(ctx).SetState(DomainTestState::HasB, false, true, EffectType::PlanOnly); + }); + subtask4->AddEffect(eff2); + task2->AddSubTask(subtask4); + + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + subtask1->AddEffect(eff); + task->AddSubTask(subtask1); + task->AddSubTask(task2); + + SharedPtr subtask5 = MakeSharedPtr("Sub-task5"); + SharedPtr eff3 = + MakeSharedPtr("TestEffect3"s, EffectType::Permanent, [](IContext& ctx, EffectType ) { + static_cast(ctx).SetState(DomainTestState::HasC, false, true, EffectType::PlanOnly); + }); + subtask5->AddEffect(eff3); + task->AddSubTask(subtask5); + + ctx.LastMTR().Add(0); + ctx.LastMTR().Add(0); + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Rejected); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.MethodTraversalRecord().size() == 2); + Assert::IsTrue(ctx.MethodTraversalRecord()[0] == 0); + Assert::IsTrue(ctx.MethodTraversalRecord()[1] == -1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasA].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasB].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int) DomainTestState::HasC].size() == 1); + Assert::AreEqual(1, (int)ctx.GetState(DomainTestState::HasA)); + Assert::AreEqual(1, (int)ctx.GetState(DomainTestState::HasB)); + Assert::AreEqual(1, (int)ctx.GetState(DomainTestState::HasC)); + } + + TEST_METHOD(DecomposeNestedCompoundSubtaskFailReturnToPreviousWorldState_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + ctx.SetContextState(ContextState::Planning); + ctx.SetState(DomainTestState::HasA, true, true, EffectType::PlanAndExecute); + ctx.SetState(DomainTestState::HasB, true, true, EffectType::Permanent); + ctx.SetState(DomainTestState::HasC, true, true, EffectType::PlanOnly); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + SharedPtr c = MakeSharedPtr("Done == true"s, [](IContext& ctx) { + return (static_cast(ctx).Done() == true); + }); + subtask2->AddCondition(c); + task3->AddSubTask(subtask2); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + SharedPtr eff = + MakeSharedPtr("TestEffect"s, EffectType::Permanent, [](IContext& ctx, EffectType ) { + static_cast(ctx).SetState(DomainTestState::HasA, false, true, EffectType::PlanOnly); + }); + subtask3->AddEffect(eff); + task3->AddSubTask(subtask3); + task2->AddSubTask(task3); + + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + SharedPtr eff2 = + MakeSharedPtr("TestEffect2"s, EffectType::Permanent, [](IContext& ctx, EffectType ) { + static_cast(ctx).SetState(DomainTestState::HasB, false, true, EffectType::PlanOnly); + }); + subtask4->AddEffect(eff2); + + task2->AddSubTask(subtask4); + + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + subtask1->AddEffect(eff); + task->AddSubTask(subtask1); + + task->AddSubTask(task2); + + SharedPtr subtask5 = MakeSharedPtr("Sub-task5"); + SharedPtr eff3 = + MakeSharedPtr("TestEffect3"s, EffectType::Permanent, [](IContext& ctx, EffectType ) { + static_cast(ctx).SetState(DomainTestState::HasC, false, true, EffectType::PlanOnly); + }); + subtask5->AddEffect(eff3); + task->AddSubTask(subtask5); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Failed); + Assert::IsTrue(plan.size() == 0); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int)DomainTestState::HasA].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int)DomainTestState::HasB].size() == 1); + Assert::IsTrue(ctx.GetWorldStateChangeStack()[(int)DomainTestState::HasC].size() == 1); + Assert::AreEqual(1, (int)ctx.GetState(DomainTestState::HasA)); + Assert::AreEqual(1, (int)ctx.GetState(DomainTestState::HasB)); + Assert::AreEqual(1, (int)ctx.GetState(DomainTestState::HasC)); + } + + TEST_METHOD(PausePlan_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + task->AddSubTask(subtask1); + SharedPtr pause = MakeSharedPtr(); + task->AddSubTask(pause); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + task->AddSubTask(subtask2); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx.HasPausedPartialPlan()); + Assert::IsTrue(ctx.PartialPlanQueue().size() == 1); + ITask* tptr1 = static_cast(task.get()); + Assert::AreEqual(tptr1, ctx.PartialPlanQueue().front().Task.get()); + Assert::AreEqual(2, ctx.PartialPlanQueue().front().TaskIndex); + } + + TEST_METHOD(ContinuePausedPlan_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + task->AddSubTask(subtask1); + SharedPtr pause = MakeSharedPtr(); + task->AddSubTask(pause); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + task->AddSubTask(subtask2); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx.HasPausedPartialPlan()); + Assert::IsTrue(ctx.PartialPlanQueue().size() == 1); + ITask* tptr1 = static_cast(task.get()); + Assert::AreEqual(tptr1, ctx.PartialPlanQueue().front().Task.get()); + Assert::AreEqual(2, ctx.PartialPlanQueue().front().TaskIndex); + + ctx.HasPausedPartialPlan() = false; + plan = TaskQueueType(); + while (ctx.PartialPlanQueue().size() > 0) + { + auto kvp = ctx.PartialPlanQueue().front(); + ctx.PartialPlanQueue().pop(); + TaskQueueType p; + auto s = StaticCastPtr(kvp.Task)->Decompose(ctx, kvp.TaskIndex, p); + if (s == DecompositionStatus::Succeeded || s == DecompositionStatus::Partial) + { + while (p.size() > 0) + { + plan.push(p.front()); + p.pop(); + } + } + } + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + } + + TEST_METHOD(NestedPausePlan_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + task3->AddSubTask(subtask1); + SharedPtr pause = MakeSharedPtr(); + task3->AddSubTask(pause); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + task3->AddSubTask(subtask2); + + task2->AddSubTask(task3); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + task2->AddSubTask(subtask3); + + task->AddSubTask(task2); + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(subtask4); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx.HasPausedPartialPlan()); + Assert::IsTrue(ctx.PartialPlanQueue().size() == 2); + PartialPlanQueueType queueCopy = ctx.PartialPlanQueue(); + ITask* tptr1 = static_cast(task3.get()); + Assert::AreEqual(tptr1, queueCopy.front().Task.get()); + Assert::AreEqual(2, queueCopy.front().TaskIndex); + queueCopy.pop(); + Assert::AreEqual(static_cast(task.get()), queueCopy.front().Task.get()); + Assert::AreEqual(1, queueCopy.front().TaskIndex); + } + + TEST_METHOD(ContinueNestedPausePlan_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + task3->AddSubTask(subtask1); + SharedPtr pause = MakeSharedPtr(); + task3->AddSubTask(pause); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + task3->AddSubTask(subtask2); + + task2->AddSubTask(task3); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + task2->AddSubTask(subtask3); + + task->AddSubTask(task2); + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(subtask4); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx.HasPausedPartialPlan()); + Assert::IsTrue(ctx.PartialPlanQueue().size() == 2); + PartialPlanQueueType queueCopy = ctx.PartialPlanQueue(); + ITask* tptr1 = static_cast(task3.get()); + Assert::AreEqual(tptr1, queueCopy.front().Task.get()); + Assert::AreEqual(2, queueCopy.front().TaskIndex); + queueCopy.pop(); + Assert::AreEqual(static_cast(task.get()), queueCopy.front().Task.get()); + Assert::AreEqual(1, queueCopy.front().TaskIndex); + + ctx.HasPausedPartialPlan() = false; + plan = TaskQueueType(); + while (ctx.PartialPlanQueue().size() > 0) + { + auto kvp = ctx.PartialPlanQueue().front(); + ctx.PartialPlanQueue().pop(); + TaskQueueType p; + auto s = StaticCastPtr(kvp.Task)->Decompose(ctx, kvp.TaskIndex, p); + + if (s == DecompositionStatus::Succeeded || s == DecompositionStatus::Partial) + { + while (p.size() > 0) + { + plan.push(p.front()); + p.pop(); + } + } + + if (ctx.HasPausedPartialPlan()) + break; + } + + Assert::IsTrue(plan.size() == 2); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task4"s, plan.front()->Name()); + } + + TEST_METHOD(ContinueMultipleNestedPausePlan_ExpectedBehavior) + { + DomainTestContext ctx; + TaskQueueType plan; + ctx.Init(); + + SharedPtr task = MakeSharedPtr("Test"s); + SharedPtr task2 = MakeSharedPtr("Test2"s); + SharedPtr task3 = MakeSharedPtr("Test3"s); + SharedPtr task4 = MakeSharedPtr("Test3"s); + + SharedPtr subtask1 = MakeSharedPtr("Sub-task1"); + task3->AddSubTask(subtask1); + SharedPtr pause = MakeSharedPtr(); + task3->AddSubTask(pause); + SharedPtr subtask2 = MakeSharedPtr("Sub-task2"); + task3->AddSubTask(subtask2); + + task2->AddSubTask(task3); + SharedPtr subtask3 = MakeSharedPtr("Sub-task3"); + task2->AddSubTask(subtask3); + + SharedPtr subtask5 = MakeSharedPtr("Sub-task5"); + task4->AddSubTask(subtask5); + task4->AddSubTask(pause); + SharedPtr subtask6 = MakeSharedPtr("Sub-task6"); + task4->AddSubTask(subtask6); + + task->AddSubTask(task2); + SharedPtr subtask4 = MakeSharedPtr("Sub-task4"); + task->AddSubTask(subtask4); + task->AddSubTask(task4); + SharedPtr subtask7 = MakeSharedPtr("Sub-task7"); + task->AddSubTask(subtask7); + + auto status = task->Decompose(ctx, 0, plan); + + Assert::IsTrue(status == DecompositionStatus::Partial); + Assert::IsTrue(plan.size() == 1); + Assert::AreEqual("Sub-task1"s, plan.front()->Name()); + Assert::IsTrue(ctx.HasPausedPartialPlan()); + Assert::IsTrue(ctx.PartialPlanQueue().size() == 2); + PartialPlanQueueType queueCopy = ctx.PartialPlanQueue(); + ITask* tptr1 = static_cast(task3.get()); + Assert::AreEqual(tptr1, queueCopy.front().Task.get()); + Assert::AreEqual(2, queueCopy.front().TaskIndex); + queueCopy.pop(); + Assert::AreEqual(static_cast(task.get()), queueCopy.front().Task.get()); + Assert::AreEqual(1, queueCopy.front().TaskIndex); + + ctx.HasPausedPartialPlan() = false; + plan = TaskQueueType(); + while (ctx.PartialPlanQueue().size() > 0) + { + auto kvp = ctx.PartialPlanQueue().front(); + ctx.PartialPlanQueue().pop(); + TaskQueueType p; + auto s = StaticCastPtr(kvp.Task)->Decompose(ctx, kvp.TaskIndex, p); + + if (s == DecompositionStatus::Succeeded || s == DecompositionStatus::Partial) + { + while (p.size() > 0) + { + plan.push(p.front()); + p.pop(); + } + } + + if (ctx.HasPausedPartialPlan()) + break; + } + + Assert::IsTrue(plan.size() == 3); + Assert::AreEqual("Sub-task2"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task4"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task5"s, plan.front()->Name()); + + ctx.HasPausedPartialPlan() = false; + plan = TaskQueueType(); + while (ctx.PartialPlanQueue().size() > 0) + { + auto kvp = ctx.PartialPlanQueue().front(); + ctx.PartialPlanQueue().pop(); + TaskQueueType p; + auto s = StaticCastPtr(kvp.Task)->Decompose(ctx, kvp.TaskIndex, p); + + if (s == DecompositionStatus::Succeeded || s == DecompositionStatus::Partial) + { + while (p.size() > 0) + { + plan.push(p.front()); + p.pop(); + } + } + + if (ctx.HasPausedPartialPlan()) + break; + } + + Assert::IsTrue(plan.size() == 2); + Assert::AreEqual("Sub-task6"s, plan.front()->Name()); + plan.pop(); + Assert::AreEqual("Sub-task7"s, plan.front()->Name()); + } + }; +} diff --git a/Fluid-HTNCPP.UnitTests/pch.cpp b/Fluid-HTNCPP.UnitTests/pch.cpp new file mode 100644 index 0000000..64b7eef --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/Fluid-HTNCPP.UnitTests/pch.h b/Fluid-HTNCPP.UnitTests/pch.h new file mode 100644 index 0000000..ff5568f --- /dev/null +++ b/Fluid-HTNCPP.UnitTests/pch.h @@ -0,0 +1,44 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +// add headers that you want to pre-compile here +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#define VC_EXTRALEAN +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CoreIncludes/STLTypes.h" + +#ifndef FHTN_FATAL_EXCEPTION +#define FHTN_FATAL_EXCEPTION(condition, msg) \ + if (!(condition)) \ + { \ + throw std::exception(msg); \ + } + +#endif + +#ifndef FHTN_FATAL_EXCEPTION_V +#define FHTN_FATAL_EXCEPTION_V(condition, fmt, ...) this is for UE4 checkf, verifymsg etc. do not t use elsewhere +#endif + +using namespace std::string_literals; + +#endif //PCH_H diff --git a/Fluid-HTNCPP/Conditions/Condition.h b/Fluid-HTNCPP/Conditions/Condition.h new file mode 100644 index 0000000..9ccf69d --- /dev/null +++ b/Fluid-HTNCPP/Conditions/Condition.h @@ -0,0 +1,48 @@ +#pragma once +#include "Contexts/Context.h" + +namespace FluidHTN +{ +class ICondition : public EnableSharedFromThis +{ +protected: + StringType _Name; + +public: + virtual ~ICondition(){} + StringType& Name() { return _Name; } + virtual bool IsValid(class IContext&) = 0; +}; + +typedef IDecompositionLogEntry DecomposedConditionEntry; +typedef std::function FunctionConditionType; + +class FuncCondition : public ICondition +{ + FunctionConditionType _func; + +public: + FuncCondition(const StringType& name, FunctionConditionType func) + : _func(func) + { + _Name = name; + } + bool IsValid(IContext& ctx) + { + bool result = false; + if (_func) + { + result = _func(ctx); + } + if (ctx.LogDecomposition()) + { + ctx.Log(_Name, + "FuncCondition.IsValid:"s + ToString(result), + ctx.CurrentDecompositionDepth() + 1, + SharedFromThis()); + } + + return result; + } +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Contexts/BaseContext.h b/Fluid-HTNCPP/Contexts/BaseContext.h new file mode 100644 index 0000000..fff7914 --- /dev/null +++ b/Fluid-HTNCPP/Contexts/BaseContext.h @@ -0,0 +1,214 @@ +#pragma once +#include "Contexts/Context.h" +#include "DebugInterfaces/DecompositionLogEntry.h" +#include "Conditions/Condition.h" +#include "Effects/Effect.h" +#include "Tasks/CompoundTasks/CompoundTask.h" + +namespace FluidHTN +{ + +template +class BaseContext : public IContext +{ + void throw_if_not_intialized() { FHTN_FATAL_EXCEPTION(_IsInitialized, "Context is not initialized"); } + +protected: + bool _IsInitialized = false; + bool _IsDirty = false; + ContextState _ContextState = ContextState::Executing; + int _CurrentDecompositionDepth = 0; + bool _DebugMTR = false; + Queue _DecompositionLog; + bool _LogDecomposition = false; + bool _RealTimeLog = false; + ArrayType _MethodTraversalRecord; + ArrayType _MTRDebug; + + ArrayType _LastMTR; + ArrayType _LastMTRDebug; + + PartialPlanQueueType _PartialPlanQueue; + bool _HasPausedPartialPlan = false; + SharedPtr> _WorldState; + + // An array of stacks per property of the world state. + typedef Stack> WorldStateStackType; + typedef ArrayType WorldStateStackArrayType; + + WorldStateStackArrayType _WorldStateChangeStackArray; + +public: + virtual bool IsInitialized() const override final { return _IsInitialized; } + virtual bool& IsDirty() override final { return _IsDirty; } + virtual ContextState GetContextState() const override final { return _ContextState; } + virtual void SetContextState(ContextState s) override final { _ContextState = s; } + virtual int& CurrentDecompositionDepth() override final { return _CurrentDecompositionDepth; } + virtual ArrayType& MethodTraversalRecord() override final { return _MethodTraversalRecord; } + virtual ArrayType& MTRDebug() override final { return _MTRDebug; } + virtual ArrayType& LastMTR() override final { return _LastMTR; } + virtual ArrayType& LastMTRDebug() override final { return _LastMTRDebug; } + virtual bool& DebugMTR() override final { return _DebugMTR; } + virtual Queue& DecompositionLog() override final { return _DecompositionLog; } + virtual bool LogDecomposition() override final { return _LogDecomposition; } + virtual void SetLogDecomposition(bool decomp) override final { _LogDecomposition = decomp; } + virtual void SetRealTimeLog(bool dolog) final {_RealTimeLog = dolog; } + virtual PartialPlanQueueType& PartialPlanQueue() override final { return _PartialPlanQueue; } + virtual void PartialPlanQueue(PartialPlanQueueType p) override final { _PartialPlanQueue = p; } + virtual void ClearPartialPlanQueue() override final { _PartialPlanQueue = PartialPlanQueueType(); } + virtual bool& HasPausedPartialPlan() override final { return _HasPausedPartialPlan; } + + IWorldState& GetWorldState() { return *_WorldState; } + /// + /// A stack of changes applied to each world state entry during planning. + /// This is necessary if one wants to support planner-only and plan&execute effects. + /// + WorldStateStackArrayType& GetWorldStateChangeStack() { return _WorldStateChangeStackArray; } + + virtual void Init() override + { + if (_WorldState != nullptr) + { + for(int i =0 ; i < _WorldState->GetMaxPropertyCount(); i++) + { + _WorldStateChangeStackArray.Add(WorldStateStackType()); + } + } + _IsInitialized = true; + } + + virtual bool HasState(WSIDTYPE state, WSVALTYPE value) { return (GetState(state) == value); } + virtual WSVALTYPE GetState(WSIDTYPE state) + { + if (_ContextState == ContextState::Executing) + { + return _WorldState->GetState(state); + } + if (_WorldStateChangeStackArray[(int)state].size() == 0) + { + return _WorldState->GetState(state); + } + return _WorldStateChangeStackArray[(int)state].top().Second(); + } + virtual void SetState(WSIDTYPE state, + WSVALTYPE value, + bool setAsDirty /* = true */, + EffectType e /* = EffectType::Permanent */) + { + if (_ContextState == ContextState::Executing) + { + // Prevent setting the world state dirty if we're not changing anything. + if (_WorldState->GetState(state) == value) + { + return; + } + _WorldState->SetState(state, value); + if (setAsDirty) + { + _IsDirty = true; + } + } + else + { + Pair p(e, value); + _WorldStateChangeStackArray[(int)state].push(p); + } + } + virtual ArrayType GetWorldStateChangeDepth() override + { + throw_if_not_intialized(); + ArrayType stackDepth(_WorldStateChangeStackArray.size()); + for (size_t i = 0; i < _WorldStateChangeStackArray.size(); i++) + { + //stackDepth.Add((int)_WorldStateChangeStackArray[i].size()); + // Nodifyed by kaminaritukane@163.com, to pass the DomainTests + stackDepth[i] = (int)_WorldStateChangeStackArray[i].size(); + } + return stackDepth; + } + virtual void TrimForExecution() override + { + FHTN_FATAL_EXCEPTION(_ContextState != ContextState::Executing, "Can not trim a context when in execution mode"); + + for (size_t si = 0; si < _WorldStateChangeStackArray.size();si++) + { + auto& stack = _WorldStateChangeStackArray[si]; + while (stack.size() != 0 && stack.top().First() != EffectType::Permanent) + { + stack.pop(); + } + } + } + + virtual void TrimToStackDepth(ArrayType& stackDepth) override + { + FHTN_FATAL_EXCEPTION(_ContextState != ContextState::Executing, "Can not trim a context when in execution mode"); + + for (size_t i = 0; i < stackDepth.size(); i++) + { + auto& stack = _WorldStateChangeStackArray[i]; + while (stack.size() > stackDepth[i]) + { + stack.pop(); + } + } + } + virtual void Reset() override + { + _MethodTraversalRecord.clear(); + _LastMTR.clear(); + + _IsInitialized = false; + } + // ========================================================= DECOMPOSITION LOGGING + virtual void RealTimeLog(StringType Name, StringType description) override{} + void Log(StringType name, StringType description, int depth, SharedPtr task, ConsoleColor color = ConsoleColor::White) + { + if(_RealTimeLog) + { + RealTimeLog(name,description); + } + if (_LogDecomposition == false) + return; + + _DecompositionLog.push(DecomposedCompoundTaskEntry{ + {name, description, depth, color}, + StaticCastPtr(task), + }); + } + void Log(StringType name, + StringType description, + int depth, + SharedPtr condition, + ConsoleColor color = ConsoleColor::DarkGreen) + { + if(_RealTimeLog) + { + RealTimeLog(name,description); + } + if (_LogDecomposition == false) + return; + + _DecompositionLog.push(DecomposedConditionEntry{{name, description, depth, color}, condition}); + } + void Log(StringType name, + StringType description, + int depth, + SharedPtr effect, + ConsoleColor color = ConsoleColor::DarkYellow) + { + if(_RealTimeLog) + { + RealTimeLog(name,description); + } + if (_LogDecomposition == false) + return; + + _DecompositionLog.push(DecomposedEffectEntry{ + {name, description, depth, color}, + effect, + }); + } +}; + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Contexts/Context.h b/Fluid-HTNCPP/Contexts/Context.h new file mode 100644 index 0000000..981d60a --- /dev/null +++ b/Fluid-HTNCPP/Contexts/Context.h @@ -0,0 +1,107 @@ +#pragma once +#include "Effects/EffectType.h" +#include "CoreIncludes/WorldState.h" +#include "DebugInterfaces/DecompositionLogEntry.h" + +namespace FluidHTN +{ + +enum class ContextState +{ + Planning, + Executing +}; + +struct PartialPlanEntry +{ + SharedPtr Task; + int TaskIndex; +}; + +typedef Queue PartialPlanQueueType; +class IContext +{ +public: + virtual ~IContext(){} + virtual void Init() = 0; + virtual bool IsInitialized() const = 0; + virtual bool& IsDirty() = 0; + + virtual ContextState GetContextState() const = 0; + virtual void SetContextState(ContextState s) = 0; + + virtual int& CurrentDecompositionDepth() = 0; + + /// + /// The Method Traversal Record is used while decomposing a domain and + /// records the valid decomposition indices as we go through our + /// decomposition process. + /// It "should" be enough to only record decomposition traversal in Selectors. + /// This can be used to compare LastMTR with the MTR, and reject + /// a new plan early if it is of lower priority than the last plan. + /// It is the user's responsibility to set the instance of the MTR, so that + /// the user is free to use pooled instances, or whatever optimization they + /// see fit. + /// + virtual ArrayType& MethodTraversalRecord() = 0; + virtual ArrayType& MTRDebug() = 0; + + /// + /// The Method Traversal Record that was recorded for the currently + /// running plan. + /// If a plan completes successfully, this should be cleared. + /// It is the user's responsibility to set the instance of the MTR, so that + /// the user is free to use pooled instances, or whatever optimization they + /// see fit. + /// + virtual ArrayType& LastMTR() = 0; + virtual ArrayType& LastMTRDebug() = 0; + + /// + /// Whether the planning system should collect debug information about our Method Traversal Record. + /// + virtual bool& DebugMTR() = 0; + + /// + /// + virtual Queue& DecompositionLog() = 0; + /// + /// Whether our planning system should log our decomposition. Specially condition success vs failure. + /// + virtual bool LogDecomposition() = 0; + virtual void SetLogDecomposition(bool) = 0; + + virtual PartialPlanQueueType& PartialPlanQueue() = 0; + virtual void PartialPlanQueue(PartialPlanQueueType p) = 0; + virtual void ClearPartialPlanQueue() = 0; + + virtual bool& HasPausedPartialPlan() = 0; + + /// + /// Reset the context state to default values. + /// + virtual void Reset() = 0; + + virtual void TrimForExecution() = 0; + virtual void TrimToStackDepth(ArrayType& stackDepth) = 0; + + virtual ArrayType GetWorldStateChangeDepth() = 0; + + virtual void RealTimeLog(StringType name, StringType description){} + virtual void Log(StringType name, + StringType description, + int depth, + SharedPtr task, + ConsoleColor color = ConsoleColor::White) = 0; + virtual void Log(StringType name, + StringType description, + int depth, + SharedPtr condition, + ConsoleColor color = ConsoleColor::DarkGreen) = 0; + virtual void Log(StringType name, + StringType description, + int depth, + SharedPtr effect, + ConsoleColor color = ConsoleColor::DarkYellow) = 0; +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/CoreIncludes/BaseDomainBuilder.h b/Fluid-HTNCPP/CoreIncludes/BaseDomainBuilder.h new file mode 100644 index 0000000..3f82cd7 --- /dev/null +++ b/Fluid-HTNCPP/CoreIncludes/BaseDomainBuilder.h @@ -0,0 +1,188 @@ +#pragma once +#include "Domain.h" +#include "Tasks/CompoundTasks/PausePlanTask.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "Tasks/CompoundTasks/Sequence.h" +#include "Tasks/CompoundTasks/Selector.h" +#include "Tasks/CompoundTasks/RandomSelector.h" +#include "Tasks/OtherTasks/Slot.h" +#include "Conditions/Condition.h" +#include "Operators/Operator.h" +#include "Effects/Effect.h" + +namespace FluidHTN +{ + +class BaseDomainBuilder +{ +protected: + SharedPtr _domain; + ArrayType> _pointers; + bool _PointersValid = true; + +public: + BaseDomainBuilder(const StringType& domainName) + { + _domain = MakeSharedPtr(domainName); + _pointers.Add(_domain->Root()); + } + const SharedPtr Pointer() + { + FHTN_FATAL_EXCEPTION(_PointersValid, "Pointers are null"); + if (_pointers.size() == 0) + { + return nullptr; + } + return _pointers.Back(); + } + // ========================================================= HIERARCHY HANDLING + + /// + /// Compound tasks are where HTN get their hierarchical nature. You can think of a compound task as + /// a high level task that has multiple ways of being accomplished. There are primarily two types of + /// compound tasks. Selectors and Sequencers. A Selector must be able to decompose a single sub-task, + /// while a Sequence must be able to decompose all its sub-tasks successfully for itself to have decomposed + /// successfully. There is nothing stopping you from extending this toolset with RandomSelect, UtilitySelect, + /// etc. These tasks are decomposed until we're left with only Primitive Tasks, which represent a final plan. + /// Compound tasks are comprised of a set of subtasks and a set of conditions. + /// http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf + /// + /// The type of compound task + /// The name given to the task, mainly for debug/display purposes + /// + bool AddCompoundTask(StringType name, SharedPtr task) + { + task->Name() = name; + + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf(ITaskDerivedClassName::CompoundTask), "Pointer() is not compound task"); + + auto baseTask = StaticCastPtr(task); + auto compoundTask = StaticCastPtr(Pointer()); + + if (_domain->Add(compoundTask, baseTask)) + { + _pointers.Add(task); + return true; + } + + return false; + } + template + bool AddCompoundTask(StringType name) + { + SharedPtr ptr = MakeSharedPtr(name); + return AddCompoundTask(name, ptr); + }; + /// + /// Primitive tasks represent a single step that can be performed by our AI. A set of primitive tasks is + /// the plan that we are ultimately getting out of the HTN. Primitive tasks are comprised of an operator, + /// a set of effects, a set of conditions and a set of executing conditions. + /// http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf + /// + /// The type of primitive task + /// The name given to the task, mainly for debug/display purposes + /// + bool AddPrimitiveTask(const StringType& name) + { + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf( ITaskDerivedClassName::CompoundTask), "Pointer() is not compound task"); + + auto parent = MakeSharedPtr(); + parent->Name() = name; + + auto baseTask = StaticCastPtr(parent); + auto compoundTask = StaticCastPtr(Pointer()); + + if (_domain->Add(compoundTask, baseTask)) + { + + _pointers.Add(parent); + return true; + } + return false; + } + bool AddPausePlanTask() + { + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf(ITaskDerivedClassName::SequenceCompoundTask), + "Pointer is not a Sequence. Maybe you tried to Pause Plan a " + "Selector, or forget an End() after a Primitive Task Action was defined?"); + + auto parent = MakeSharedPtr(); + parent->Name() = "Pause Plan"s; + auto baseTask = StaticCastPtr(parent); + auto compoundTask = StaticCastPtr(Pointer()); + return _domain->Add(compoundTask, baseTask); + } + bool AddSequence(const StringType& name) + { + return AddCompoundTask(name); + } + bool AddAction(const StringType& name) { return AddPrimitiveTask(name); } + bool AddSelector(const StringType& name) + { + return AddCompoundTask(name); + } + bool AddCondition(const StringType& name, FunctionConditionType func) + { + auto condition = MakeSharedPtr(name, func); + auto base = StaticCastPtr(condition); + return Pointer()->AddCondition(base); + } + bool AddExecutingCondition(const StringType& name, FunctionConditionType func) + { + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask), + "Tried to add an Executing Condition, but the Pointer is not a Primitive Task!"); + auto condition = MakeSharedPtr(name, func); + auto base = StaticCastPtr(condition); + auto task = StaticCastPtr(Pointer()); + return task->AddExecutingCondition(base); + } + bool AddOperator(FuncOperatorType action, StopOperatorType stopAction= nullptr) + { + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask), + "Tried to add Operator, but the Pointer is not a Primitive Task!"); + auto op = MakeSharedPtr(action, stopAction); + auto base = StaticCastPtr(op); + auto task = StaticCastPtr(Pointer()); + return task->SetOperator(base); + } + bool AddEffect(const StringType& name, EffectType effectType, ActionType action) + { + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf(ITaskDerivedClassName::PrimitiveTask), + "Tried to add an Effect, but the Pointer is not a Primitive Task!"); + auto effect = MakeSharedPtr(name, effectType, action); + auto base = StaticCastPtr(effect); + auto task = StaticCastPtr(Pointer()); + return task->AddEffect(base); + } + + bool AddSlot(int slotId) + { + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf(ITaskDerivedClassName::CompoundTask), "Pointer() is not compound task"); + auto slot = MakeSharedPtr(); + auto compoundTask = StaticCastPtr(Pointer()); + slot->SlotId(slotId); + return _domain->Add(compoundTask, slot); + } + bool AddRandomSelector(const StringType& name) { return AddCompoundTask(name); } + void End() { _pointers.PopBack(); } + bool Splice(Domain& domain) + { + FHTN_FATAL_EXCEPTION(Pointer()->IsTypeOf(ITaskDerivedClassName::CompoundTask), + "Pointer is not a compound task type. Did you forget an End()?"); + + auto compoundTask = StaticCastPtr(Pointer()); + _domain->Add(compoundTask, domain.Root()); + + return true; + } + bool PausePlan() { return AddPausePlanTask(); } + SharedPtr Build() + { + FHTN_FATAL_EXCEPTION(Pointer() == _domain->Root(), "Domain definition lacks one or more End() statements"); + _pointers.clear(); + _PointersValid = false; // C# code frees the pointers so that further access to Pointer() throws null reference + return _domain; + } +}; + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/CoreIncludes/Domain.h b/Fluid-HTNCPP/CoreIncludes/Domain.h new file mode 100644 index 0000000..d556c9a --- /dev/null +++ b/Fluid-HTNCPP/CoreIncludes/Domain.h @@ -0,0 +1,270 @@ +#pragma once +#include "Tasks/Task.h" +#include "Tasks/CompoundTasks/DecompositionStatus.h" +#include "Contexts/Context.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "Tasks/CompoundTasks/CompoundTask.h" +#include "Tasks/CompoundTasks/Selector.h" +#include "Tasks/OtherTasks/Slot.h" +#include "Domain.h" + +namespace FluidHTN +{ + +template +class BaseContext; + +//===================================================================================== +// Base class +class Domain +{ +protected: + SharedPtr _Root; + Map> _slots; + +public: + Domain(const StringType& name) + { + _Root = MakeSharedPtr(); + _Root->Name() = name; + } + virtual ~Domain() {} + + virtual SharedPtr& Root() { return _Root; } + + bool Add(SharedPtr& parent, SharedPtr& slot) + { + FHTN_FATAL_EXCEPTION(StaticCastPtr(parent) != StaticCastPtr(slot), "Parent and slot cannot be the same"); + + FHTN_FATAL_EXCEPTION(_slots.Find(slot->SlotId()) == _slots.End(), "slot already exists in domain definition"); + parent->AddSubTask(StaticCastPtr(slot)); + slot->Parent() = parent; + _slots.Insert(MakePair(slot->SlotId(), slot)); + return true; + } + + bool Add(SharedPtr& root, SharedPtr& subtask) + { + auto compound = StaticCastPtr(root); + return Add(compound, subtask); + } + + bool Add(SharedPtr& parent, SharedPtr& subtask) + { + auto s = StaticCastPtr(subtask); + return Add(parent, s); + } + bool Add(SharedPtr& parent, SharedPtr& pt) + { + auto s = StaticCastPtr(pt); + return Add(parent, s); + } + + bool Add(SharedPtr& parent, SharedPtr& root) + { + auto s = StaticCastPtr(root); + return Add(parent, s); + } + + bool Add(SharedPtr& parent, SharedPtr& subtask) + { + FHTN_FATAL_EXCEPTION(subtask != parent, "parent and subtask cannot be the same"); + parent->AddSubTask(subtask); + subtask->Parent() = parent; + return true; + } + + + // ========================================================= SLOTS + + /// + /// At runtime, set a sub-domain to the slot with the given id. + /// This can be used with Smart Objects, to extend the behavior + /// of an agent at runtime. + /// + bool TrySetSlotDomain(int slotId, Domain& subDomain) + { + auto slot = _slots.Find(slotId); + if (slot != _slots.End()) + { + return slot->second->Set(subDomain.Root()); + } + return false; + } + + /// + /// At runtime, clear the sub-domain from the slot with the given id. + /// This can be used with Smart Objects, to extend the behavior + /// of an agent at runtime. + /// + void ClearSlot(int slotId) + { + auto iter = _slots.Find(slotId); + if (iter != _slots.End()) + { + iter->second->clear(); + } + } + template + DecompositionStatus FindPlan(BaseContext& ctx, TaskQueueType& plan) + { + FHTN_FATAL_EXCEPTION(ctx.IsInitialized(), "Context was uninitialized"); + + ctx.SetContextState(ContextState::Planning); + + plan.clear(); + + auto status = DecompositionStatus::Rejected; + + // We first check whether we have a stored start task. This is true + // if we had a partial plan pause somewhere in our plan, and we now + // want to continue where we left off. + // If this is the case, we don't erase the MTR, but continue building it. + // However, if we have a partial plan, but LastMTR is not 0, that means + // that the partial plan is still running, but something triggered a replan. + // When this happens, we have to plan from the domain root (we're not + // continuing the current plan), so that we're open for other plans to replace + // the running partial plan. + if (ctx.HasPausedPartialPlan() && ctx.LastMTR().size() == 0) + { + ctx.HasPausedPartialPlan() = false; + while (ctx.PartialPlanQueue().size() > 0) + { + auto& pair = ctx.PartialPlanQueue().front(); + ctx.PartialPlanQueue().pop(); + + FHTN_FATAL_EXCEPTION(pair.Task->IsTypeOf(ITaskDerivedClassName::CompoundTask), + "PartialPlanEntry task must be a compound task"); + + auto compoundTask = StaticCastPtr(pair.Task); + if (plan.size() == 0) + { + status = compoundTask->Decompose(ctx, pair.TaskIndex, plan); + } + else + { + TaskQueueType p; + status = compoundTask->Decompose(ctx, pair.TaskIndex, p); + if (status == DecompositionStatus::Succeeded || status == DecompositionStatus::Partial) + { + while (p.size() > 0) + { + plan.push(p.front()); + p.pop(); + } + } + } + // While continuing a partial plan, we might encounter + // a new pause. + if (ctx.HasPausedPartialPlan()) + { + break; + } + } + // If we failed to continue the paused partial plan, + // then we have to start planning from the root. + if (status == DecompositionStatus::Rejected || status == DecompositionStatus::Failed) + { + ctx.MethodTraversalRecord().clear(); + if (ctx.DebugMTR()) + { + ctx.MTRDebug().clear(); + } + + status = _Root->Decompose(ctx, 0, plan); + } + } + else + { + Queue lastPartialPlanQueue; + if (ctx.HasPausedPartialPlan()) + { + ctx.HasPausedPartialPlan() = false; + while (ctx.PartialPlanQueue().size() > 0) + { + lastPartialPlanQueue.push(ctx.PartialPlanQueue().front()); + ctx.PartialPlanQueue().pop(); + } + } + // We only erase the MTR if we start from the root task of the domain. + ctx.MethodTraversalRecord().clear(); + if (ctx.DebugMTR()) + { + ctx.MTRDebug().clear(); + } + + status = _Root->Decompose(ctx, 0, plan); + + // If we failed to find a new plan, we have to restore the old plan, + // if it was a partial plan. + if (lastPartialPlanQueue.empty() != true) + { + if (status == DecompositionStatus::Rejected || status == DecompositionStatus::Failed) + { + ctx.HasPausedPartialPlan() = true; + ctx.PartialPlanQueue().clear(); + while (lastPartialPlanQueue.size() > 0) + { + ctx.PartialPlanQueue().push(lastPartialPlanQueue.front()); + lastPartialPlanQueue.pop(); + } + } + } + // If this MTR equals the last MTR, then we need to double check whether we ended up + // just finding the exact same plan. During decomposition each compound task can't check + // for equality, only for less than, so this case needs to be treated after the fact. + auto isMTRsEqual = (ctx.MethodTraversalRecord().size() == ctx.LastMTR().size()); + if (isMTRsEqual) + { + for (auto i = 0; i < ctx.MethodTraversalRecord().size(); i++) + if (ctx.MethodTraversalRecord()[i] < ctx.LastMTR()[i]) + { + isMTRsEqual = false; + break; + } + + if (isMTRsEqual) + { + plan = TaskQueueType(); + status = DecompositionStatus::Rejected; + } + } + if (status == DecompositionStatus::Succeeded || status == DecompositionStatus::Partial) + { + // Trim away any plan-only or plan&execute effects from the world state change stack, that only + // permanent effects on the world state remains now that the planning is done. + ctx.TrimForExecution(); + + // Apply permanent world state changes to the actual world state used during plan execution. + for (size_t i = 0; i < ctx.GetWorldStateChangeStack().size(); i++) + { + auto& stack = ctx.GetWorldStateChangeStack()[i]; + if (stack.size() != 0) + { + ctx.GetWorldState().SetState(static_cast(i), stack.top().Second()); + stack.clear(); + } + } + } + else + { + // Clear away any changes that might have been applied to the stack + // No changes should be made or tracked further when the plan failed. + for (size_t i = 0; i < ctx.GetWorldStateChangeStack().size(); i++) + { + auto& stack = ctx.GetWorldStateChangeStack()[i]; + if (stack.size() != 0) + { + stack.clear(); + } + } + } + ctx.SetContextState(ContextState::Executing); + } + + return status; + } +}; +//===================================================================================== + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/CoreIncludes/STLTypes.h b/Fluid-HTNCPP/CoreIncludes/STLTypes.h new file mode 100644 index 0000000..7ced201 --- /dev/null +++ b/Fluid-HTNCPP/CoreIncludes/STLTypes.h @@ -0,0 +1,158 @@ +// +// Define aliases and wrappers for STL types here so that custom implementations can override them (UE4 for example). +// +#pragma once + +#if !FHTN_USING_CUSTOM_STL + +template +using SharedPtr = std::shared_ptr; + +template +SharedPtr MakeSharedPtr(ARGS&&... args) +{ + return std::make_shared(std::forward(args)...); +} +template +using EnableSharedFromThis = std::enable_shared_from_this; + +#define SharedFromThis() shared_from_this() + +template +SharedPtr StaticCastPtr(const SharedPtr& Other) +{ + return std::static_pointer_cast(Other); +} + +template +class ArrayType +{ +private: + std::vector vec; +public: + ArrayType(){} + ArrayType(size_t s) : vec(s){} + void Add(const T& x) {vec.push_back(x);} + size_t size() const {return vec.size();} + void clear() {return vec.clear();} + void PopBack(){vec.pop_back();} + void resize(size_t n) {vec.resize(n);} + + T& Back() {return vec.back();} + + T* begin() {return vec.begin();} + auto end() {return vec.end();} + + T& operator[](size_t index){return vec[index];} + const T& operator[](size_t index)const {return vec[index];} +}; + +template +class Queue +{ +private: + std::queue q; + +public: + void push(const T& x) { q.push(x); } + void push(T&& x) { q.push(std::move(x)); } + void pop() { q.pop(); } + T& front() { return q.front(); } + size_t size() { return q.size(); } + bool empty() { return q.empty(); } + void clear() { q = std::queue(); } +}; +template +class Stack +{ +private: + std::stack s; + +public: + void push(const T& x) { s.push(x); } + void push(T&& x) { s.push(std::move(x)); } + void pop() { return s.pop(); } + T& top() { return s.top(); } + size_t size() { return s.size(); } + bool empty() { return s.empty(); } + void clear() { s = std::stack(); } +}; + +template +class Map +{ +private: + std::unordered_map m; +public: + template + auto Insert(V&& x) -> decltype(m.insert(std::forward(x))) { return m.insert(std::forward(x)); } + + auto Find(T x) -> decltype(m.find(std::forward(x))) { return m.find(std::forward(x)); } + + auto End() { return m.end(); } +}; +template +class Set +{ +private: + std::unordered_set s; +public: + template + auto Insert(V&& x) -> decltype(s.insert(std::forward(x))) { return s.insert(std::forward(x)); } + + auto Find(T x) -> decltype(s.find(std::forward(x))) { return s.find(std::forward(x)); } + + auto Contains(T x) { return (s.find(x) != s.end()); } +}; + +template +auto MakePair(T&& A, U&& B) -> decltype(std::make_pair(std::forward(A), std::forward(B)) ) +{ + return std::make_pair(std::forward(A), std::forward(B)); +} + +template +class Pair +{ +private: + std::pair p; + +public: + Pair(P1 X, P2 Y): p(X,Y){} + P1& First() { return p.first; } + P2& Second() { return p.second; } + +}; + +using StringType = std::string; + +template +StringType ToString(const T& arg) +{ + return std::to_string(arg); +} + +#ifndef FHTN_FATAL_EXCEPTION +#define FHTN_FATAL_EXCEPTION(condition, msg) \ + if (!(condition)) \ + { \ + throw std::exception(msg); \ + } + +#endif + +#ifndef FHTN_FATAL_EXCEPTION_V +#define FHTN_FATAL_EXCEPTION_V(condition, fmt, ...) this is for UE4 checkf, verifymsg etc. do not t use elsewhere +#endif + +inline void InitializeRandom() +{ + std::srand((unsigned int)std::time(nullptr)); +} +inline int NextRandom() +{ + return std::rand(); +} +#else +#include "STLReplacementTypes.h" +#endif !FHTN_USING_CUSTOM_STL diff --git a/Fluid-HTNCPP/CoreIncludes/WorldState.h b/Fluid-HTNCPP/CoreIncludes/WorldState.h new file mode 100644 index 0000000..eb76e74 --- /dev/null +++ b/Fluid-HTNCPP/CoreIncludes/WorldState.h @@ -0,0 +1,29 @@ +#pragma once + +namespace FluidHTN +{ + +template +class IWorldState +{ + static_assert(std::is_enum::value, "WorldState Id must be an enum type"); + +public: + typedef IDTYPE IdType; + typedef VALUETYPE ValueType; + + bool HasState(IdType state, ValueType value) + { + return static_cast(this)->HasState(state, value); + } + ValueType GetState(IdType state) + { + return static_cast(this)->GetState(state); + } + void SetState(IdType state, ValueType value) + { + return static_cast(this)->SetState(state, value); + } + int GetMaxPropertyCount() { return static_cast(this)->GetMaxPropertyCount(); } +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/DebugInterfaces/DecompositionLogEntry.h b/Fluid-HTNCPP/DebugInterfaces/DecompositionLogEntry.h new file mode 100644 index 0000000..9eea45f --- /dev/null +++ b/Fluid-HTNCPP/DebugInterfaces/DecompositionLogEntry.h @@ -0,0 +1,48 @@ +#pragma once +#include "CoreIncludes/STLTypes.h" + +namespace FluidHTN +{ +enum class ConsoleColor +{ + Black, + Red, + DarkRed, + Blue, + DarkBlue, + Green, + DarkGreen, + White, + Yellow, + DarkYellow +}; +class Debug +{ +public: + static StringType DepthToString(int depth) + { + StringType s = ""s; + for (auto i = 0; i < depth; i++) + { + s += "\t"s; + } + + s += "- "s; + return s; + } +}; +struct IBaseDecompositionLogEntry +{ + StringType Name; + StringType Description; + int Depth; + ConsoleColor Color; +}; + +template +struct IDecompositionLogEntry : public IBaseDecompositionLogEntry +{ +public: + SharedPtr _Entry; +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Effects/Effect.h b/Fluid-HTNCPP/Effects/Effect.h new file mode 100644 index 0000000..aa17a12 --- /dev/null +++ b/Fluid-HTNCPP/Effects/Effect.h @@ -0,0 +1,49 @@ +#pragma once +#include "Effects/EffectType.h" +#include "DebugInterfaces/DecompositionLogEntry.h" +#include "Contexts/Context.h" + +namespace FluidHTN +{ +class IEffect : public EnableSharedFromThis +{ +protected: + StringType _Name; + EffectType _Type; + + +public: + virtual ~IEffect(){} + const StringType& Name() { return _Name; } + EffectType Type() { return _Type; } + virtual void Apply(class IContext& ctx) = 0; +}; + +typedef IDecompositionLogEntry DecomposedEffectEntry; +typedef std::function ActionType; + +class ActionEffect : public IEffect +{ + ActionType _action; + +public: + ActionEffect() =delete; + ActionEffect(const StringType name, EffectType type, ActionType action) + { + _Name = name; + _Type = type; + _action = action; + } + void Apply(IContext& ctx) + { + if (ctx.LogDecomposition()) + { + ctx.Log(_Name, "ActionEffect"s + ToString((int)_Type), ctx.CurrentDecompositionDepth() + 1, SharedFromThis()); + } + if (_action) + { + _action(ctx, _Type); + } + } +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Effects/EffectType.h b/Fluid-HTNCPP/Effects/EffectType.h new file mode 100644 index 0000000..80c6d1d --- /dev/null +++ b/Fluid-HTNCPP/Effects/EffectType.h @@ -0,0 +1,11 @@ +#pragma once +namespace FluidHTN +{ + +enum class EffectType +{ + PlanAndExecute, + PlanOnly, + Permanent +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Fluid-HTNCPP.vcxproj b/Fluid-HTNCPP/Fluid-HTNCPP.vcxproj new file mode 100644 index 0000000..1e6d81f --- /dev/null +++ b/Fluid-HTNCPP/Fluid-HTNCPP.vcxproj @@ -0,0 +1,212 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {0c469b65-6d39-4667-8d00-9ec963c518e3} + FluidHTNCPP + 10.0 + + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)$(Configuration)\$(Platform)\Output\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\ + + + false + $(SolutionDir)$(Configuration)\$(Platform)\Output\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\ + + + true + $(SolutionDir)$(Configuration)\$(Platform)\Output\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\ + + + false + $(SolutionDir)$(Configuration)\$(Platform)\Output\ + $(SolutionDir)$(Configuration)\$(Platform)\Intermediate\ + + + + Level4 + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir);%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + stdcpp17 + true + false + + + + + true + + + + + Level4 + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir);%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + stdcpp17 + true + false + + + + + true + true + true + + + + + Level4 + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir);%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + stdcpp17 + true + false + + + + + true + + + + + Level4 + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir);%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + stdcpp17 + true + false + + + + + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + \ No newline at end of file diff --git a/Fluid-HTNCPP/Fluid-HTNCPP.vcxproj.filters b/Fluid-HTNCPP/Fluid-HTNCPP.vcxproj.filters new file mode 100644 index 0000000..e67ec6a --- /dev/null +++ b/Fluid-HTNCPP/Fluid-HTNCPP.vcxproj.filters @@ -0,0 +1,113 @@ + + + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {e5af4f3f-467c-4bae-a275-1a6734468ddb} + + + {12f97d9c-b8b9-49b3-baa1-1412b9945678} + + + {4c8bdc48-7b43-4420-8cf3-df5cb6f5d372} + + + {0c2118ed-97fb-4d61-bfd7-b1924b2f3bb1} + + + {ceb9a360-f3ae-45cc-b094-6d403f97dea6} + + + {a14ac7e8-ba7f-4798-b2dc-e48d20ab8477} + + + {2a062dd9-748c-457e-9702-35d2fd6fa9a2} + + + {f29103bf-38ef-4b13-8058-89b7ae0601bd} + + + {748ca950-191e-4380-bb90-e46b4bb9ad92} + + + {c0e3a0ae-50db-4b97-b5a4-330aedacdbbe} + + + {68d5697f-c87f-416a-8e45-02e397cfa67b} + + + + + Header Files\Tasks + + + Header Files\Tasks\CompoundTasks + + + Header Files\Tasks\CompoundTasks + + + Header Files\Tasks\CompoundTasks + + + Header Files\Tasks\CompoundTasks + + + Header Files\Tasks\CompoundTasks + + + Header Files\Tasks\OtherTasks + + + Header Files\Tasks\PrimitiveTasks + + + Header Files\Contexts + + + Header Files\Effects + + + Header Files\Conditions + + + Header Files\Operators + + + Header Files\Planners + + + Header Files\Contexts + + + Header Files\DebugInterfaces + + + Header Files\Effects + + + Header Files\Tasks\CompoundTasks + + + Header Files\CoreIncludes + + + Header Files\CoreIncludes + + + Header Files\CoreIncludes + + + Header Files\CoreIncludes + + + Header Files + + + + + + \ No newline at end of file diff --git a/Fluid-HTNCPP/Operators/Operator.h b/Fluid-HTNCPP/Operators/Operator.h new file mode 100644 index 0000000..6ddccb3 --- /dev/null +++ b/Fluid-HTNCPP/Operators/Operator.h @@ -0,0 +1,45 @@ +#pragma once +#include "Tasks/Task.h" + +namespace FluidHTN +{ +class IOperator +{ +public: + virtual TaskStatus Update(class IContext& ctx) = 0; + virtual void Stop(IContext& ctx) = 0; +}; + +typedef std::function FuncOperatorType; +typedef std::function StopOperatorType; + +class FuncOperator : public IOperator +{ + FuncOperatorType _func; + StopOperatorType _funcStop; + +public: + virtual ~FuncOperator(){} + FuncOperator(FuncOperatorType func, StopOperatorType stp = nullptr) + { + _func = func; + _funcStop = stp; + } + virtual TaskStatus Update(IContext& ctx) override + { + if (!_func) + { + return TaskStatus::Failure; + } + return _func(ctx); + } + virtual void Stop(IContext& ctx) override + { + if (_funcStop != nullptr) + { + + _funcStop(ctx); + } + } +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Planners/Planner.h b/Fluid-HTNCPP/Planners/Planner.h new file mode 100644 index 0000000..769eb0d --- /dev/null +++ b/Fluid-HTNCPP/Planners/Planner.h @@ -0,0 +1,388 @@ +#pragma once +#include "Tasks/Task.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" + +namespace FluidHTN +{ + +class Planner +{ + SharedPtr _currentTask; + TaskQueueType _plan; + TaskStatus _LastStatus; + +public: + TaskStatus LastStatus() { return _LastStatus; } + + /// + /// OnNewPlan(newPlan) is called when we found a new plan, and there is no + /// old plan to replace. + /// + std::function OnNewPlan; + + /// + /// OnReplacePlan(oldPlan, currentTask, newPlan) is called when we're about to replace the + /// current plan with a new plan. + /// + std::function&, TaskQueueType)> OnReplacePlan; + + /// + /// OnNewTask(task) is called after we popped a new task off the current plan. + /// + std::function&)> OnNewTask; + + /// + /// OnNewTaskConditionFailed(task, failedCondition) is called when we failed to + /// validate a condition on a new task. + /// + std::function&, SharedPtr&)> OnNewTaskConditionFailed; + + /// + /// OnStopCurrentTask(task) is called when the currently running task was stopped + /// forcefully. + /// + std::function&)> OnStopCurrentTask; + + /// + /// OnCurrentTaskCompletedSuccessfully(task) is called when the currently running task + /// completes successfully, and before its effects are applied. + /// + std::function&)> OnCurrentTaskCompletedSuccessfully; + + /// + /// OnApplyEffect(effect) is called for each effect of the type PlanAndExecute on a + /// completed task. + /// + std::function&)> OnApplyEffect; + + /// + /// OnCurrentTaskFailed(task) is called when the currently running task fails to complete. + /// + std::function&)> OnCurrentTaskFailed; + + /// + /// OnCurrentTaskContinues(task) is called every tick that a currently running task + /// needs to continue. + /// + std::function&)> OnCurrentTaskContinues; + + /// + /// OnCurrentTaskExecutingConditionFailed(task, condition) is called if an Executing Condition + /// fails. The Executing Conditions are checked before every call to task.Operator.Update(...). + /// + std::function&, SharedPtr&)> OnCurrentTaskExecutingConditionFailed; + + void Reset(IContext& ctx) + { + _plan = TaskQueueType(); + if (_currentTask != nullptr) + { + if (_currentTask->IsTypeOf(ITaskDerivedClassName::PrimitiveTask) ) + { + auto task = StaticCastPtr(_currentTask); + task->Stop(ctx); + } + _currentTask = nullptr; + } + } + const TaskQueueType& GetPlan() { return _plan; } + const SharedPtr& GetCurrentTask() { return _currentTask; } + + // ========================================================= TICK PLAN + template + void Tick(Domain& domain, IContext& ctx, bool allowImmediateReplan = true) + { + FHTN_FATAL_EXCEPTION(ctx.IsInitialized(), "Context was not initialized"); + + DecompositionStatus decompositionStatus = DecompositionStatus::Failed; + bool isTryingToReplacePlan = false; + + // Check whether state has changed or the current plan has finished running. + // and if so, try to find a new plan. + if (((_currentTask == nullptr) && (_plan.size() == 0)) || ctx.IsDirty()) + { + Queue lastPartialPlanQueue; + bool worldStateDirtyReplan = ctx.IsDirty(); + + ctx.IsDirty() = false; + if (worldStateDirtyReplan) + { + // If we're simply re-evaluating whether to replace the current plan because + // some world state got dirt, then we do not intend to continue a partial plan + // right now, but rather see whether the world state changed to a degree where + // we should pursue a better plan. Thus, if this replan fails to find a better + // plan, we have to add back the partial plan temps cached above. + if (ctx.HasPausedPartialPlan()) + { + ctx.HasPausedPartialPlan() = false; + while (ctx.PartialPlanQueue().size() > 0) + { + lastPartialPlanQueue.push(ctx.PartialPlanQueue().front()); + ctx.PartialPlanQueue().pop(); + } + // We also need to ensure that the last mtr is up to date with the on-going MTR of the partial plan, + // so that any new potential plan that is decomposing from the domain root has to beat the currently + // running partial plan. + ctx.LastMTR().clear(); + for (size_t si = 0; si < ctx.MethodTraversalRecord().size();si++) + { + auto record = ctx.MethodTraversalRecord()[si]; + ctx.LastMTR().Add(record); + } + if (ctx.DebugMTR()) + { + ctx.LastMTRDebug().clear(); + for (size_t si =0 ; si < ctx.MTRDebug().size();si++) + { + auto record = ctx.MTRDebug()[si]; + ctx.LastMTRDebug().Add(record); + } + } + } + } + TaskQueueType newPlan; + decompositionStatus = domain.FindPlan(static_cast&>(ctx), newPlan); + isTryingToReplacePlan = (_plan.size() > 0); + if (decompositionStatus == DecompositionStatus::Succeeded || decompositionStatus == DecompositionStatus::Partial) + { + if (OnReplacePlan != nullptr && (_plan.size() > 0 || _currentTask != nullptr)) + { + OnReplacePlan(_plan, _currentTask, newPlan); + } + else if (OnNewPlan != nullptr && _plan.size() == 0) + { + OnNewPlan(newPlan); + } + + _plan = newPlan; + + if (_currentTask != nullptr && _currentTask->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)) + { + auto tPrimitive = StaticCastPtr(_currentTask); + if (OnStopCurrentTask != nullptr) + { + OnStopCurrentTask(tPrimitive); + } + tPrimitive->Stop(ctx); + _currentTask = nullptr; + } + + // Copy the MTR into our LastMTR to represent the current plan's decomposition record + // that must be beat to replace the plan. + if (ctx.MethodTraversalRecord().size() != 0) + { + ctx.LastMTR().clear(); + for (size_t si = 0; si < ctx.MethodTraversalRecord().size() ;si++) + { + auto& record = ctx.MethodTraversalRecord()[si]; + ctx.LastMTR().Add(record); + } + if (ctx.DebugMTR()) + { + ctx.LastMTRDebug().clear(); + for (size_t si =0 ; si < ctx.MTRDebug().size();si++) + { + auto record = ctx.MTRDebug()[si]; + ctx.LastMTRDebug().Add(record); + } + } + } + } + else if (lastPartialPlanQueue.size() != 0) + { + ctx.HasPausedPartialPlan() = true; + ctx.ClearPartialPlanQueue(); + while (lastPartialPlanQueue.size() > 0) + { + ctx.PartialPlanQueue().push(lastPartialPlanQueue.front()); + lastPartialPlanQueue.pop(); + } + if (ctx.LastMTR().size() > 0) + { + ctx.MethodTraversalRecord().clear(); + for (size_t si =0 ; si < ctx.LastMTR().size();si++) + { + auto& record = ctx.LastMTR()[si]; + ctx.MethodTraversalRecord().Add(record); + } + ctx.LastMTR().clear(); + if (ctx.DebugMTR()) + { + ctx.LastMTRDebug().clear(); + for (size_t si =0 ; si < ctx.MTRDebug().size();si++) + { + auto record = ctx.MTRDebug()[si]; + ctx.LastMTRDebug().Add(record); + } + } + } + } + } + if (_currentTask == nullptr && _plan.size() > 0) + { + _currentTask = _plan.front(); + _plan.pop(); + ctx.RealTimeLog(_currentTask->Name(), "Popped task for execution"); + + if (_currentTask != nullptr) + { + if (OnNewTask != nullptr) + { + OnNewTask(_currentTask); + } + for (size_t si = 0; si < _currentTask->Conditions().size();si++) + { + auto& condition = _currentTask->Conditions()[si]; + // If a condition failed, then the plan failed to progress! A replan is required. + if (condition->IsValid(ctx) == false) + { + if (OnNewTaskConditionFailed) + { + OnNewTaskConditionFailed(_currentTask, condition); + } + + _currentTask = nullptr; + _plan = TaskQueueType(); + + ctx.LastMTR().clear(); + if (ctx.DebugMTR()) + { + ctx.LastMTRDebug().clear(); + } + + ctx.HasPausedPartialPlan() = false; + ctx.ClearPartialPlanQueue(); + ctx.IsDirty() = false; + + return; + } + } + } + } + if (_currentTask != nullptr) + { + if (_currentTask->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)) + { + auto task = StaticCastPtr(_currentTask); + if (task->Operator() != nullptr) + { + for (size_t si = 0; si < task->ExecutingConditions().size();si++) + { + auto condition = task->ExecutingConditions()[si]; + // If a condition failed, then the plan failed to progress! A replan is required. + if (condition->IsValid(ctx) == false) + { + if (OnCurrentTaskExecutingConditionFailed) + { + OnCurrentTaskExecutingConditionFailed(task, condition); + } + + _currentTask = nullptr; + _plan = TaskQueueType(); + + ctx.LastMTR().clear(); + if (ctx.DebugMTR()) + { + ctx.LastMTRDebug().clear(); + } + + ctx.HasPausedPartialPlan() = false; + ctx.ClearPartialPlanQueue(); + ctx.IsDirty() = false; + + return; + } + } + + ctx.RealTimeLog(task->Name(), "Executing action for primitive task"); + _LastStatus = task->Operator()->Update(ctx); + + // If the operation finished successfully, we set task to null so that we dequeue the next task in the plan the + // following tick. + if (_LastStatus == TaskStatus::Success) + { + if (OnCurrentTaskCompletedSuccessfully) + { + OnCurrentTaskCompletedSuccessfully(task); + } + + // All effects that is a result of running this task should be applied when the task is a success. + for (size_t si = 0; si < task->Effects().size();si++) + { + auto effect = task->Effects()[si]; + if (effect->Type() == EffectType::PlanAndExecute) + { + if (OnApplyEffect) + { + OnApplyEffect(effect); + } + effect->Apply(ctx); + } + } + _currentTask = nullptr; + if (_plan.size() == 0) + { + ctx.LastMTR().clear(); + if (ctx.DebugMTR()) + { + ctx.LastMTRDebug().clear(); + } + + ctx.IsDirty() = false; + + if (allowImmediateReplan) + { + ctx.RealTimeLog("Planner"s," Executing immediate replan"); + Tick(domain, static_cast&>(ctx), false); + } + } + } + + // If the operation failed to finish, we need to fail the entire plan, so that we will replan the next tick. + else if (_LastStatus == TaskStatus::Failure) + { + if (OnCurrentTaskFailed) + { + OnCurrentTaskFailed(task); + } + + _currentTask = nullptr; + _plan = TaskQueueType(); + + ctx.LastMTR().clear(); + if (ctx.DebugMTR()) + { + ctx.LastMTRDebug().clear(); + } + + ctx.HasPausedPartialPlan() = false; + ctx.ClearPartialPlanQueue(); + ctx.IsDirty() = false; + } + + // Otherwise the operation isn't done yet and need to continue. + else + { + if (OnCurrentTaskContinues) + { + OnCurrentTaskContinues(task); + } + } + } + else + { + // This should not really happen if a domain is set up properly. + _currentTask = nullptr; + _LastStatus = TaskStatus::Failure; + } + } + } + + if (_currentTask == nullptr && _plan.size() == 0 && isTryingToReplacePlan == false && + (decompositionStatus == DecompositionStatus::Failed || decompositionStatus == DecompositionStatus::Rejected)) + { + _LastStatus = TaskStatus::Failure; + } + } +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/CompoundTasks/CompoundTask.h b/Fluid-HTNCPP/Tasks/CompoundTasks/CompoundTask.h new file mode 100644 index 0000000..f3f3172 --- /dev/null +++ b/Fluid-HTNCPP/Tasks/CompoundTasks/CompoundTask.h @@ -0,0 +1,92 @@ +#pragma once +#include "CoreIncludes/STLTypes.h" +#include "Tasks/Task.h" +#include "Conditions/Condition.h" +#include "DebugInterfaces/DecompositionLogEntry.h" +#include "Contexts/Context.h" + +namespace FluidHTN +{ + +class Slot; + +class CompoundTask : public ITask +{ + ArrayType> _Tasks; + +protected: + CompoundTask(ITaskDerivedClassName t) + : ITask(t) + { + _SubTypes.Insert(ITaskDerivedClassName::CompoundTask); + } + +public: + CompoundTask() + : ITask(ITaskDerivedClassName::CompoundTask) + { + } + + ArrayType>& Subtasks() { return _Tasks; } + + bool AddSubTask(SharedPtr subtask) + { + _Tasks.Add(subtask); + return true; + } + + virtual DecompositionStatus OnIsValidFailed(IContext&) override { return DecompositionStatus::Failed; } + + DecompositionStatus Decompose(IContext& ctx, int startIndex, TaskQueueType& result) + { + if (ctx.LogDecomposition()) + { + ctx.CurrentDecompositionDepth() += 1; + } + auto status = OnDecompose(ctx, startIndex, result); + if (ctx.LogDecomposition()) + { + ctx.CurrentDecompositionDepth() -= 1; + } + + return status; + } + + virtual bool IsValid(IContext& ctx) override + { + for(size_t si = 0; si < _Conditions.size();si++) + { + auto& condition = _Conditions[si]; + bool result = condition->IsValid(ctx); + if (ctx.LogDecomposition()) + { + Log(ctx, + "CompoundTask.IsValid: "s + ToString(result) + " for "s + condition->Name(), + result ? ConsoleColor::DarkGreen : ConsoleColor::DarkRed); + } + if (!result) + { + return false; + } + } + return true; + } + +protected: + virtual DecompositionStatus OnDecompose(IContext& ctx, int startIndex, TaskQueueType& result) = 0; + virtual DecompositionStatus OnDecomposeTask( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) = 0; + virtual DecompositionStatus OnDecomposeCompoundTask( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) = 0; + virtual DecompositionStatus OnDecomposeSlot( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) = 0; + + virtual void Log(IContext& ctx, StringType description, ConsoleColor color = ConsoleColor::White) + { + ctx.Log(_Name, description, ctx.CurrentDecompositionDepth(), SharedFromThis(), color); + } +}; + +typedef IDecompositionLogEntry DecomposedCompoundTaskEntry; + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/CompoundTasks/DecompositionStatus.h b/Fluid-HTNCPP/Tasks/CompoundTasks/DecompositionStatus.h new file mode 100644 index 0000000..805408d --- /dev/null +++ b/Fluid-HTNCPP/Tasks/CompoundTasks/DecompositionStatus.h @@ -0,0 +1,32 @@ + +#pragma once + +namespace FluidHTN +{ + +enum class DecompositionStatus +{ + Succeeded, + Partial, + Failed, + Rejected +}; + +inline StringType DecompositionStatusToString(DecompositionStatus st) +{ + switch (st) + { + case DecompositionStatus::Failed: + return "DecompositionStatus::Failed"s; + case DecompositionStatus::Partial: + return "DecompositionStatus::Partial"s; + case DecompositionStatus::Rejected: + return "DecompositionStatus::Rejected"s; + case DecompositionStatus::Succeeded: + return "DecompositionStatus::Succeded"s; + default: + return "ThisSatisifesCompilerUselessWarnings"s; + } +} + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/CompoundTasks/PausePlanTask.h b/Fluid-HTNCPP/Tasks/CompoundTasks/PausePlanTask.h new file mode 100644 index 0000000..ce27b0d --- /dev/null +++ b/Fluid-HTNCPP/Tasks/CompoundTasks/PausePlanTask.h @@ -0,0 +1,43 @@ +#pragma once + +#include "Tasks/Task.h" +#include "Contexts/Context.h" + +namespace FluidHTN +{ + +class PausePlanTask : public ITask +{ +protected: + virtual void Log(IContext& ctx, StringType description) + { + ctx.Log(_Name, description, ctx.CurrentDecompositionDepth(), SharedFromThis(), ConsoleColor::Green); + } + +public: + PausePlanTask() + : ITask(ITaskDerivedClassName::PausePlanTask) + { + } + virtual DecompositionStatus OnIsValidFailed(IContext&) { return DecompositionStatus::Failed; } + + virtual bool AddCondition(SharedPtr&) override + { + FHTN_FATAL_EXCEPTION(false, "PausePlan Tasks do not support conditions"); + return false; + } + + bool AddEffect(SharedPtr&) { FHTN_FATAL_EXCEPTION(false, "Pause Plan tasks do not support effects"); } + + void ApplyEffects(IContext&) {} + + virtual bool IsValid(IContext& ctx) override + { + if (ctx.LogDecomposition()) + { + Log(ctx, "PausePlanTask.IsValid:Success!"); + } + return true; + }; +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/CompoundTasks/RandomSelector.h b/Fluid-HTNCPP/Tasks/CompoundTasks/RandomSelector.h new file mode 100644 index 0000000..a3c44e8 --- /dev/null +++ b/Fluid-HTNCPP/Tasks/CompoundTasks/RandomSelector.h @@ -0,0 +1,56 @@ +#pragma once +#include "Tasks/CompoundTasks/Selector.h" + +namespace FluidHTN +{ + +class RandomSelector : public Selector +{ + // ========================================================= FIELDS + +protected: + RandomSelector(ITaskDerivedClassName t) + : Selector(t) + { + InitializeRandom(); + } + + // ========================================================= DECOMPOSITION + + /// + /// In a Random Selector decomposition, we simply select a sub-task randomly, and stick with it for the duration of the + /// plan as if it was the only sub-task. + /// So if the sub-task fail to decompose, that means the entire Selector failed to decompose (we don't try to decompose + /// any other sub-tasks). + /// Because of the nature of the Random Selector, we don't do any MTR tracking for it, since it doesn't do any real + /// branching. + /// + /// + /// +protected: + virtual DecompositionStatus OnDecompose(IContext& ctx, int startIndex, TaskQueueType& result) override + { + _Plan = TaskQueueType(); + + int taskIndex = startIndex + NextRandom() %(Subtasks().size() - startIndex); + auto task = Subtasks()[taskIndex]; + + ArrayType td; + return OnDecomposeTask(ctx, task, taskIndex, td, result); + } + +public: + RandomSelector() + : Selector(ITaskDerivedClassName::RandomSelector) + { + InitializeRandom(); + } + RandomSelector(const StringType& name) + : Selector(ITaskDerivedClassName::RandomSelector) + { + _Name = name; + InitializeRandom(); + } +}; + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/CompoundTasks/Selector.h b/Fluid-HTNCPP/Tasks/CompoundTasks/Selector.h new file mode 100644 index 0000000..9662bfb --- /dev/null +++ b/Fluid-HTNCPP/Tasks/CompoundTasks/Selector.h @@ -0,0 +1,366 @@ +#pragma once +#include "CoreIncludes/STLTypes.h" +#include "Tasks/CompoundTasks/CompoundTask.h" +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "Tasks/OtherTasks/Slot.h" +#include "Contexts/Context.h" + +namespace FluidHTN +{ +class Selector : public CompoundTask +{ + bool BeatsLastMTR(IContext& ctx, int taskIndex, size_t currentDecompositionIndex) + { + // If the last plan's traversal record for this decomposition layer + // has a smaller index than the current task index we're about to + // decompose, then the new decomposition can't possibly beat the + // running plan, so we cancel finding a new plan. + if (ctx.LastMTR()[currentDecompositionIndex] < taskIndex) + { + // But, if any of the earlier records beat the record in LastMTR, we're still good, as we're on a higher priority + // branch. This ensures that [0,0,1] can beat [0,1,0] + for (auto i = 0; i < ctx.MethodTraversalRecord().size(); i++) + { + auto diff = ctx.MethodTraversalRecord()[i] - ctx.LastMTR()[i]; + if (diff < 0) + { + return true; + } + if (diff > 0) + { + // We should never really be able to get here, but just in case. + return false; + } + } + + return false; + } + + return true; + } + +protected: + Selector(ITaskDerivedClassName t) + : CompoundTask(t) + { + } + TaskQueueType _Plan; + + virtual DecompositionStatus OnDecompose(IContext& ctx, int startIndex, TaskQueueType& result) override + { + _Plan = TaskQueueType(); + + for (auto taskIndex = startIndex; taskIndex < Subtasks().size(); taskIndex++) + { + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecompose:Task index: "s + ToString(taskIndex) + ": "s + + (Subtasks()[taskIndex] ? Subtasks()[taskIndex]->Name() : " no task"s)); + } + // If the last plan is still running, we need to check whether the + // new decomposition can possibly beat it. + if (ctx.LastMTR().size() > 0) + { + if (ctx.MethodTraversalRecord().size() < ctx.LastMTR().size()) + { + // If the last plan's traversal record for this decomposition layer + // has a smaller index than the current task index we're about to + // decompose, then the new decomposition can't possibly beat the + // running plan, so we cancel finding a new plan. + auto currentDecompositionIndex = ctx.MethodTraversalRecord().size(); + if (BeatsLastMTR(ctx,taskIndex,currentDecompositionIndex) == false) + { + ctx.MethodTraversalRecord().Add(-1); + if (ctx.DebugMTR()) + { + ctx.MTRDebug().Add("REPLAN FAIL "s + Subtasks()[taskIndex]->Name()); + } + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecompose:Rejected:Index "s + ToString(currentDecompositionIndex) + + " is beat by last method traversal record!"s, + ConsoleColor::Red); + } + result = TaskQueueType(); + return DecompositionStatus::Rejected; + } + } + } + + auto task = Subtasks()[taskIndex]; + + auto status = OnDecomposeTask(ctx, task, taskIndex, ArrayType(), result); + switch (status) + { + case DecompositionStatus::Rejected: + case DecompositionStatus::Succeeded: + case DecompositionStatus::Partial: + return status; + case DecompositionStatus::Failed: + default: + continue; + } + } + + result = _Plan; + return (result.size() == 0 ? DecompositionStatus::Failed : DecompositionStatus::Succeeded); + } + virtual DecompositionStatus OnDecomposeTask( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) override + { + if (task->IsValid(ctx) == false) + { + + if (ctx.LogDecomposition()) + { + Log(ctx, "Selector.OnDecomposeTask:Failed:Task "s + task->Name() + ".IsValid returned false!"s, ConsoleColor::Red); + } + result = _Plan; + return task->OnIsValidFailed(ctx); + } + + if (task->IsTypeOf(ITaskDerivedClassName::CompoundTask)) + { + auto compoundTask = StaticCastPtr(task); + return OnDecomposeCompoundTask(ctx, compoundTask, taskIndex, ArrayType(), result); + } + + if (task->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)) + { + auto primitiveTask = StaticCastPtr(task); + if (ctx.LogDecomposition()) + { + Log(ctx, "Selector.OnDecomposeTask:Pushed "s + primitiveTask->Name() + "to plan!"s, ConsoleColor::Blue); + } + primitiveTask->ApplyEffects(ctx); + _Plan.push(task); + } + + if (task->IsTypeOf(ITaskDerivedClassName::Slot)) + { + auto slot = StaticCastPtr(task); + return OnDecomposeSlot(ctx, slot, taskIndex, ArrayType(), result); + } + + result = _Plan; + auto status = (result.size() == 0 ? DecompositionStatus::Failed : DecompositionStatus::Succeeded); + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeTask " + ToString((int)status) + "!"s, + status == DecompositionStatus::Succeeded ? ConsoleColor::Green : ConsoleColor::Red); + } + return status; + } + virtual DecompositionStatus OnDecomposeCompoundTask( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) override + { + // We need to record the task index before we decompose the task, + // so that the traversal record is set up in the right order. + ctx.MethodTraversalRecord().Add(taskIndex); + if (ctx.DebugMTR()) + { + ctx.MTRDebug().Add(task->Name()); + } + + TaskQueueType subPlan; + auto status = task->Decompose(ctx, 0, subPlan); + + // If status is rejected, that means the entire planning procedure should cancel. + if (status == DecompositionStatus::Rejected) + { + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeCompoundTask:"s + ToString((int)status) + ": Decomposing "s + task->Name() + + " was rejected."s, + ConsoleColor::Red); + } + result = TaskQueueType(); + return DecompositionStatus::Rejected; + } + + // If the decomposition failed + if (status == DecompositionStatus::Failed) + { + // Remove the taskIndex (pushed at top of function) if it failed to decompose. + ctx.MethodTraversalRecord().PopBack(); + if (ctx.DebugMTR()) + { + ctx.MTRDebug().PopBack(); + } + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeCompoundTask:"s + ToString((int)status) + ": Decomposing "s + task->Name() + " failed."s, + ConsoleColor::Red); + } + result = _Plan; + return DecompositionStatus::Failed; + } + + while (subPlan.size() > 0) + { + auto p = subPlan.front(); + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeCompoundTask:Decomposing "s + task->Name() + ":Pushed " + p->Name() + " to plan!"s, + ConsoleColor::Blue); + } + _Plan.push(p); + subPlan.pop(); + } + if (ctx.HasPausedPartialPlan()) + { + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeCompoundTask:Return partial plan at index "s + ToString(taskIndex) + "!"s, + ConsoleColor::DarkBlue); + } + result = _Plan; + return DecompositionStatus::Partial; + } + + result = _Plan; + auto s = (result.size() == 0 ? DecompositionStatus::Failed : DecompositionStatus::Succeeded); + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeCompoundTask:"s + ToString((int)s), + s == DecompositionStatus::Succeeded ? ConsoleColor::Green : ConsoleColor::Red); + } + return s; + } + virtual DecompositionStatus OnDecomposeSlot( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) override + { + // We need to record the task index before we decompose the task, + // so that the traversal record is set up in the right order. + ctx.MethodTraversalRecord().Add(taskIndex); + if (ctx.DebugMTR()) + { + ctx.MTRDebug().Add(task->Name()); + } + + TaskQueueType subPlan; + auto status = task->Decompose(ctx, 0, subPlan); + + // If status is rejected, that means the entire planning procedure should cancel. + if (status == DecompositionStatus::Rejected) + { + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeSlot:"s + ToString((int)status) + ": Decomposing "s + task->Name() + " was rejected."s, + ConsoleColor::Red); + } + result = TaskQueueType(); + return DecompositionStatus::Rejected; + } + + // If the decomposition failed + if (status == DecompositionStatus::Failed) + { + // Remove the taskIndex if it failed to decompose. + ctx.MethodTraversalRecord().PopBack(); + if (ctx.DebugMTR()) + { + ctx.MTRDebug().PopBack(); + } + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeSlot:"s + ToString((int)status) + ": Decomposing "s + task->Name() + " failed."s, + ConsoleColor::Red); + } + result = _Plan; + return DecompositionStatus::Failed; + } + + while (subPlan.size() > 0) + { + auto p = subPlan.front(); + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeSlot:Decomposing "s + task->Name() + ":Pushed "s + p->Name() + " to plan!"s, + ConsoleColor::Blue); + } + _Plan.push(p); + subPlan.pop(); + } + + if (ctx.HasPausedPartialPlan()) + { + if (ctx.LogDecomposition()) + { + Log(ctx, "Selector.OnDecomposeSlot:Return partial plan!"s, ConsoleColor::DarkBlue); + } + result = _Plan; + return DecompositionStatus::Partial; + } + + result = _Plan; + auto s = (result.size() == 0 ? DecompositionStatus::Failed : DecompositionStatus::Succeeded); + if (ctx.LogDecomposition()) + { + Log(ctx, + "Selector.OnDecomposeSlot:"s + DecompositionStatusToString(s) + "!"s, + s == DecompositionStatus::Succeeded ? ConsoleColor::Green : ConsoleColor::Red); + } + return s; + } + +public: + Selector() + : CompoundTask(ITaskDerivedClassName::SelectorCompoundTask) + { + } + explicit Selector(const StringType& name) + : CompoundTask(ITaskDerivedClassName::SelectorCompoundTask) + { + _Name = name; + } + virtual bool IsValid(IContext& ctx) override + { + if (CompoundTask::IsValid(ctx) == false) + { + if (ctx.LogDecomposition()) + { + Log(ctx, "Selector.IsValid:Failed:Preconditions not met!"s, ConsoleColor::Red); + } + return false; + } + if (Subtasks().size() == 0) + { + + if (ctx.LogDecomposition()) + { + Log(ctx, "Selector.IsValid:Failed:No sub-tasks!"s, ConsoleColor::Red); + } + return false; + } + if (ctx.LogDecomposition()) + { + Log(ctx, "Selector.IsValid:Success!"s, ConsoleColor::Green); + } + return true; + } +}; + +class TaskRoot : public Selector +{ +public: + TaskRoot() + : Selector(ITaskDerivedClassName::TaskRoot) + { + } +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/CompoundTasks/Sequence.h b/Fluid-HTNCPP/Tasks/CompoundTasks/Sequence.h new file mode 100644 index 0000000..157e929 --- /dev/null +++ b/Fluid-HTNCPP/Tasks/CompoundTasks/Sequence.h @@ -0,0 +1,311 @@ +#pragma once +#include "Tasks/PrimitiveTasks/PrimitiveTask.h" +#include "Tasks/CompoundTasks/PausePlanTask.h" +#include "Tasks/CompoundTasks/CompoundTask.h" +#include "Contexts/Context.h" + +namespace FluidHTN +{ + +// IDecompose all does not exist in C++ because it causes a diamond inheritance and Sequence seems to be the only class that uses it. +// If more classes that need IDecomposeAll are created in the future we can virtually inherit from a common ICompoundTask +class Sequence : public CompoundTask +{ +protected: + TaskQueueType _Plan; + + virtual DecompositionStatus OnDecompose(IContext& ctx, int startIndex, TaskQueueType& result) override + { + _Plan = TaskQueueType(); + + auto oldStackDepth = ctx.GetWorldStateChangeDepth(); + + for (auto taskIndex = startIndex; taskIndex < Subtasks().size(); taskIndex++) + { + auto task = Subtasks()[taskIndex]; + + if (ctx.LogDecomposition()) + { + Log(ctx, "Selection::OnDecompose task index "s + ToString(taskIndex)); + } + + auto status = OnDecomposeTask(ctx, task, taskIndex, oldStackDepth, result); + switch (status) + { + case DecompositionStatus::Rejected: + case DecompositionStatus::Failed: + case DecompositionStatus::Partial: + { + return status; + } + } + } + + result = _Plan; + return (result.size() == 0 ? DecompositionStatus::Failed : DecompositionStatus::Succeeded); + } + virtual DecompositionStatus OnDecomposeTask( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) override + { + if (task->IsValid(ctx) == false) + { + + if (ctx.LogDecomposition()) + { + Log(ctx, "Sequence.OnDecomposeTask:Failed:Task"s + task->Name() + ".IsValid returned false!"s, ConsoleColor::Red); + } + _Plan = TaskQueueType(); + ctx.TrimToStackDepth(oldStackDepth); + result = _Plan; + return task->OnIsValidFailed(ctx); + } + + if (task->IsTypeOf(ITaskDerivedClassName::CompoundTask)) + { + auto compoundTask = StaticCastPtr(task); + return OnDecomposeCompoundTask(ctx, compoundTask, taskIndex, oldStackDepth, result); + } + else if (task->IsTypeOf(ITaskDerivedClassName::PrimitiveTask)) + { + auto primitiveTask = StaticCastPtr(task); + + if (ctx.LogDecomposition()) + { + Log(ctx, "Sequence.OnDecomposeTask:Pushed"s + primitiveTask->Name() + " to plan!"s, ConsoleColor::Blue); + } + primitiveTask->ApplyEffects(ctx); + _Plan.push(task); + } + else if (task->IsTypeOf(ITaskDerivedClassName::PausePlanTask)) + { + auto pausePlanTask = StaticCastPtr(task); + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeTask:Return partial plan at index "s + ToString(taskIndex) + "!"s, + ConsoleColor::DarkBlue); + } + + PartialPlanEntry pentry; + pentry.Task = SharedFromThis(); + pentry.TaskIndex = taskIndex + 1; + ctx.HasPausedPartialPlan() = true; + ctx.PartialPlanQueue().push(pentry); + + result = _Plan; + return DecompositionStatus::Partial; + } + else if (task->IsTypeOf(ITaskDerivedClassName::Slot)) + { + auto slot = StaticCastPtr(task); + return OnDecomposeSlot(ctx, slot, taskIndex, oldStackDepth, result); + } + + result = _Plan; + auto s = (result.size() == 0 ? DecompositionStatus::Failed : DecompositionStatus::Succeeded); + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeTask:"s + ToString((int)s), + s == DecompositionStatus::Succeeded ? ConsoleColor::Green : ConsoleColor::Red); + } + return s; + } + virtual DecompositionStatus OnDecomposeCompoundTask( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) override + { + TaskQueueType subPlan; + auto status = task->Decompose(ctx, 0, subPlan); + + // If result is null, that means the entire planning procedure should cancel. + if (status == DecompositionStatus::Rejected) + { + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeCompoundTask:"s + ToString((int)status) + ": Decomposing "s + task->Name() + + " was rejected."s, + ConsoleColor::Red); + } + _Plan = TaskQueueType(); + ctx.TrimToStackDepth(oldStackDepth); + result = TaskQueueType(); + return DecompositionStatus::Rejected; + } + + // If the decomposition failed + if (status == DecompositionStatus::Failed) + { + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeCompoundTask:"s + ToString((int)status) + ": Decomposing "s + task->Name() + " failed.", + ConsoleColor::Red); + } + _Plan = TaskQueueType(); + ctx.TrimToStackDepth(oldStackDepth); + result = _Plan; + return DecompositionStatus::Failed; + } + + while (subPlan.size() > 0) + { + auto p = subPlan.front(); + _Plan.push(p); + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeCompoundTask:Decomposing "s + task->Name() + " :Pushed "s + p->Name() + " to plan!"s, + ConsoleColor::Blue); + } + subPlan.pop(); + } + + if (ctx.HasPausedPartialPlan()) + { + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeCompoundTask:Return partial plan at index "s + ToString(taskIndex) + "!"s, + ConsoleColor::DarkBlue); + } + if (taskIndex < Subtasks().size() - 1) + { + PartialPlanEntry pentry; + pentry.Task = SharedFromThis(); + pentry.TaskIndex = taskIndex + 1; + ctx.PartialPlanQueue().push(pentry); + } + + result = _Plan; + return DecompositionStatus::Partial; + } + + result = _Plan; + if (ctx.LogDecomposition()) + { + Log(ctx, "Sequence.OnDecomposeCompoundTask:Succeeded!", ConsoleColor::Green); + } + return DecompositionStatus::Succeeded; + } + virtual DecompositionStatus OnDecomposeSlot( + IContext& ctx, SharedPtr& task, int taskIndex, ArrayType oldStackDepth, TaskQueueType& result) override + { + TaskQueueType subPlan; + auto status = task->Decompose(ctx, 0, subPlan); + + // If result is null, that means the entire planning procedure should cancel. + if (status == DecompositionStatus::Rejected) + { + + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeSlot: "s + ToString((int)status) + ": Decomposing "s + task->Name() + " was rejected."s, + ConsoleColor::Red); + } + _Plan = TaskQueueType(); + ctx.TrimToStackDepth(oldStackDepth); + + result = TaskQueueType(); + return DecompositionStatus::Rejected; + } + + // If the decomposition failed + if (status == DecompositionStatus::Failed) + { + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeSlot: "s + ToString((int)status) + ": Decomposing "s + task->Name() + " was failed."s, + ConsoleColor::Red); + } + _Plan = TaskQueueType(); + ctx.TrimToStackDepth(oldStackDepth); + result = _Plan; + return DecompositionStatus::Failed; + } + + while (subPlan.size() > 0) + { + auto p = subPlan.front(); + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeSlot:Return partial plan at index "s + ToString(taskIndex) + "!"s, + ConsoleColor::Blue); + } + _Plan.push(p); + subPlan.pop(); + } + + if (ctx.HasPausedPartialPlan()) + { + if (ctx.LogDecomposition()) + { + Log(ctx, + "Sequence.OnDecomposeSlot:Return partial plan at index "s + ToString(taskIndex) + "!"s, + ConsoleColor::DarkBlue); + } + if (taskIndex < Subtasks().size() - 1) + { + PartialPlanEntry pentry; + pentry.Task = SharedFromThis(); + pentry.TaskIndex = taskIndex + 1; + ctx.PartialPlanQueue().push(pentry); + } + + result = _Plan; + return DecompositionStatus::Partial; + } + + result = _Plan; + if (ctx.LogDecomposition()) + { + Log(ctx, "Sequence.OnDecomposeSlot:Succeeded!", ConsoleColor::Green); + } + return DecompositionStatus::Succeeded; + } + +public: + Sequence() + : CompoundTask(ITaskDerivedClassName::SequenceCompoundTask) + { + } + explicit Sequence(const StringType& name) + : CompoundTask(ITaskDerivedClassName::SequenceCompoundTask) + { + _Name = name; + } + virtual bool IsValid(IContext& ctx) override + { + if (CompoundTask::IsValid(ctx) == false) + { + if (ctx.LogDecomposition()) + { + Log(ctx, "Sequence.IsValid failed, preconditions not met!"s, ConsoleColor::Red); + } + return false; + } + if (Subtasks().size() == 0) + { + if (ctx.LogDecomposition()) + { + Log(ctx, "Sequence.IsValid failed: No sub-tasks!"s, ConsoleColor::Red); + } + return false; + } + if (ctx.LogDecomposition()) + { + Log(ctx, "Sequence.IsValid Success!"s, ConsoleColor::Green); + } + return true; + } +}; + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/OtherTasks/Slot.h b/Fluid-HTNCPP/Tasks/OtherTasks/Slot.h new file mode 100644 index 0000000..f4d00cc --- /dev/null +++ b/Fluid-HTNCPP/Tasks/OtherTasks/Slot.h @@ -0,0 +1,65 @@ +#pragma once +#include "Contexts/Context.h" +#include "Tasks/Task.h" +#include "Tasks/CompoundTasks/CompoundTask.h" + +namespace FluidHTN +{ + +class Slot : public ITask +{ + int _SlotId; + + SharedPtr _Subtask; + +public: + Slot():ITask(ITaskDerivedClassName::Slot){} + int SlotId() { return _SlotId; } + void SlotId(int s) { _SlotId = s; } + const SharedPtr Subtask() { return _Subtask; } + + virtual DecompositionStatus OnIsValidFailed(IContext& ) override { return DecompositionStatus::Failed; } + + virtual bool AddCondition(SharedPtr&) override + { + FHTN_FATAL_EXCEPTION(false,"Slot Tasks do not support conditions"); + } + bool Set(SharedPtr subtask) + { + if (_Subtask != nullptr) + { + return false; + } + + _Subtask = subtask; + return true; + } + void clear() { _Subtask = nullptr; } + + DecompositionStatus Decompose(IContext& ctx, int startIndex, TaskQueueType& result) + { + if (_Subtask != nullptr) + { + return _Subtask->Decompose(ctx, startIndex, result); + } + result = TaskQueueType(); + return DecompositionStatus::Failed; + } + + virtual bool IsValid(IContext& ctx) override + { + bool result = (_Subtask != nullptr); + if (ctx.LogDecomposition()) + { + Log(ctx, "Slot.IsValid:"s + ToString(result) + "!"s, result ? ConsoleColor::Green : ConsoleColor::Red); + } + return result; + } +protected: + virtual void Log(IContext& ctx, StringType description, ConsoleColor color = ConsoleColor::White) + { + ctx.Log(_Name, description, ctx.CurrentDecompositionDepth(), SharedFromThis(), color); + } +}; + +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/PrimitiveTasks/PrimitiveTask.h b/Fluid-HTNCPP/Tasks/PrimitiveTasks/PrimitiveTask.h new file mode 100644 index 0000000..d15b460 --- /dev/null +++ b/Fluid-HTNCPP/Tasks/PrimitiveTasks/PrimitiveTask.h @@ -0,0 +1,123 @@ +#pragma once +#include "CoreIncludes/STLTypes.h" +#include "Tasks/Task.h" +#include "Effects/Effect.h" +#include "Operators/Operator.h" +#include "Conditions/Condition.h" +#include "Contexts/Context.h" + +namespace FluidHTN +{ +class PrimitiveTask : public ITask +{ +protected: + ArrayType> _ExecutingConditions; + SharedPtr _Operator; + ArrayType> _Effects; + +public: + PrimitiveTask() : ITask(ITaskDerivedClassName::PrimitiveTask){} + explicit PrimitiveTask(const StringType& name):ITask(ITaskDerivedClassName::PrimitiveTask) { _Name = name; } + virtual const ArrayType>& ExecutingConditions() const { return _ExecutingConditions; } + + virtual SharedPtr Operator() { return _Operator; } + virtual ArrayType>& Effects() { return _Effects; } + virtual DecompositionStatus OnIsValidFailed(IContext&) override { return DecompositionStatus::Failed; } + + // If these functions took non-reference parameters, they could be construction inline in the function call. + // However, we probably want to share implementations of conditions across multiple domains, so avoiding the + // shared_ptr copy costs per function call seems reasonable. + bool AddExecutingCondition(SharedPtr& condition) + { + _ExecutingConditions.Add(condition); + return true; + } + bool AddEffect(SharedPtr& effect) + { + _Effects.Add(effect); + return true; + } + bool SetOperator(SharedPtr& action) + { + FHTN_FATAL_EXCEPTION(_Operator == nullptr, "A Primitive Task can only contain a single operator"); + _Operator = action; + return true; + } + + void ApplyEffects(class IContext& ctx) + { + if (ctx.GetContextState() == ContextState::Planning) + { + if (ctx.LogDecomposition()) + { + Log(ctx, "PrimitiveTask.ApplyEffects", ConsoleColor::Yellow); + } + } + if (ctx.LogDecomposition()) + { + ctx.CurrentDecompositionDepth() += 1; + } + for(size_t si = 0; si < _Effects.size();si++) + { + auto& effect = _Effects[si]; + effect->Apply(ctx); + } + if (ctx.LogDecomposition()) + { + ctx.CurrentDecompositionDepth() -= 1; + } + } + void Stop(IContext& ctx) + { + if (_Operator) + { + _Operator->Stop(ctx); + } + } + + virtual bool IsValid(IContext& ctx) override + { + if (ctx.LogDecomposition()) + { + Log(ctx, "PrimitiveTask.IsValid check"); + } + for (size_t si = 0; si < _Conditions.size(); si++) + { + auto& condition = _Conditions[si]; + if (ctx.LogDecomposition()) + { + ctx.CurrentDecompositionDepth() += 1; + } + + bool result = condition->IsValid(ctx); + + if (ctx.LogDecomposition()) + { + ctx.CurrentDecompositionDepth() -= 1; + } + + if (ctx.LogDecomposition()) + { + Log(ctx, + "PrimitiveTask.IsValid:"s + ToString(result) + " for condition "s + condition->Name(), + result ? ConsoleColor::DarkGreen : ConsoleColor::DarkRed); + } + if (!result) + { + return false; + } + } + if (ctx.LogDecomposition()) + { + Log(ctx, "PrimitiveTask.IsValid:Success!"s, ConsoleColor::Green); + } + return true; + } + +protected: + virtual void Log(IContext& ctx, StringType description, ConsoleColor color = ConsoleColor::White) + { + ctx.Log(_Name, description, ctx.CurrentDecompositionDepth() + 1, SharedFromThis(), color); + } +}; +} // namespace FluidHTN diff --git a/Fluid-HTNCPP/Tasks/Task.h b/Fluid-HTNCPP/Tasks/Task.h new file mode 100644 index 0000000..7384f98 --- /dev/null +++ b/Fluid-HTNCPP/Tasks/Task.h @@ -0,0 +1,84 @@ +#pragma once +#include "CoreIncludes/STLTypes.h" +#include "Tasks/CompoundTasks/DecompositionStatus.h" + +namespace FluidHTN +{ + +class CompoundTask; + +class IContext; + +class ICondition; + +enum class TaskStatus +{ + Continue, + Success, + Failure +}; + +// custom RTTI, because most game engines don't like C++ RTTI +enum class ITaskDerivedClassName +{ + ITaskType, + CompoundTask, + PrimitiveTask, + SelectorCompoundTask, + TaskRoot, + SequenceCompoundTask, + Slot, + PausePlanTask, + RandomSelector +}; + +// not strictly an "interface", but design patterns are so 1999 +class ITask : public EnableSharedFromThis +{ + ITask() {} + + ITaskDerivedClassName _Type = ITaskDerivedClassName::ITaskType; + +protected: + Set _SubTypes; + explicit ITask(ITaskDerivedClassName n) + { + _Type = n; + _SubTypes.Insert(n); + } + StringType _Name; + SharedPtr _Parent; + ArrayType> _Conditions; + TaskStatus _LastStatus = TaskStatus::Failure; + +public: + virtual ~ITask(){} + ITaskDerivedClassName GetType() const { return _Type; } + bool IsTypeOf(ITaskDerivedClassName thetype) + { + return ((thetype == _Type) || (thetype == ITaskDerivedClassName::ITaskType) || + (_SubTypes.Contains(thetype))); + } + + virtual StringType& Name() { return _Name; } + + virtual SharedPtr& Parent() { return _Parent; } + + virtual ArrayType>& Conditions() { return _Conditions; } + + virtual TaskStatus LastStatus() { return _LastStatus; } + + virtual bool AddCondition(SharedPtr& Condition) + { + _Conditions.Add(Condition); + return true; + } + + virtual bool IsValid(IContext& ctx) = 0; + + virtual DecompositionStatus OnIsValidFailed(IContext& ctx) = 0; +}; + +typedef Queue> TaskQueueType; + +} // namespace FluidHTN \ No newline at end of file diff --git a/Fluid-HTNCPP/pch.cpp b/Fluid-HTNCPP/pch.cpp new file mode 100644 index 0000000..7454cfc --- /dev/null +++ b/Fluid-HTNCPP/pch.cpp @@ -0,0 +1,4 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/Fluid-HTNCPP/pch.h b/Fluid-HTNCPP/pch.h new file mode 100644 index 0000000..32444fe --- /dev/null +++ b/Fluid-HTNCPP/pch.h @@ -0,0 +1,27 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +#if !FHTN_USING_CUSTOM_STL + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif //!FHTN_USING_CUSTOM_STL + +using namespace std::string_literals; + +#endif // PCH_H