diff --git a/tests/testBehavior.cpp b/tests/testBehavior.cpp new file mode 100644 index 0000000..4b51140 --- /dev/null +++ b/tests/testBehavior.cpp @@ -0,0 +1,158 @@ +#include "testHarness.h" +#include "vf/Behavior.h" + +#include +#include +#include + +using namespace vf; + +namespace { + +World flatWorld(int halfSpan = 24) { + World world; + for (int x = -halfSpan; x <= halfSpan; ++x) { + for (int z = -halfSpan; z <= halfSpan; ++z) world.setBlock(x, 0, z, Block::Stone); + } + return world; +} + +World grassWorld(int halfSpan = 12) { + World world; + for (int x = -halfSpan; x <= halfSpan; ++x) { + for (int z = -halfSpan; z <= halfSpan; ++z) world.setBlock(x, 0, z, Block::Grass); + } + return world; +} + +std::unique_ptr constantAction(const char* name, BehaviorStatus status, int* counter) { + return std::unique_ptr(new ActionNode(name, [status, counter](BehaviorContext&) { + if (counter != nullptr) ++(*counter); + return status; + })); +} + +std::unique_ptr flagCondition(const char* name, const bool* source) { + return std::unique_ptr( + new ConditionNode(name, [source](BehaviorContext&) { return *source; })); +} + +// Reports Running for `runningTicks` calls, then Success once, then restarts. +std::unique_ptr slowAction(const char* name, int runningTicks, int* counter, + int* progress) { + return std::unique_ptr( + new ActionNode(name, [runningTicks, counter, progress](BehaviorContext&) { + ++(*counter); + ++(*progress); + if (*progress <= runningTicks) return BehaviorStatus::Running; + *progress = 0; + return BehaviorStatus::Success; + })); +} + +BehaviorContext freshContext(const World* world) { + BehaviorContext ctx; + ctx.world = world; + return ctx; +} + +BehaviorAgent makeAgent(Species species, const Vec3& position) { + BehaviorAgent agent; + agent.species = species; + agent.position = position; + agent.homePosition = position; + const SpeciesStats& stats = speciesStats(species); + agent.health = stats.maxHealth; + agent.maxHealth = stats.maxHealth; + agent.moveStep = 0.25f; + return agent; +} + +} // namespace + +VF_TEST(blackboardKeepsEachValueTypeInItsOwnDrawer) { + Blackboard board; + board.setNumber("threat", 4.5f); + board.setCount("meals", 3); + board.setFlag("engaged", true); + board.setPoint("lastSeen", Vec3{1.0f, 2.0f, 3.0f}); + board.setCell("target", Vec3i{4, 5, 6}); + board.setRoute("path", {Vec3i{0, 1, 0}, Vec3i{1, 1, 0}}); + + CHECK_NEAR(board.number("threat"), 4.5f, 1e-6f); + CHECK_EQ(board.count("meals"), 3); + CHECK(board.flag("engaged")); + CHECK_NEAR(board.point("lastSeen").y, 2.0f, 1e-6f); + CHECK(board.cell("target") == Vec3i(4, 5, 6)); + CHECK(board.route("path") != nullptr); + CHECK_EQ(board.route("path")->size(), 2u); + CHECK_EQ(board.entryCount(), 6u); + + // A key written as one type is invisible to every other type. + CHECK(!board.hasCount("threat")); + CHECK(!board.hasNumber("meals")); + CHECK(board.route("threat") == nullptr); + CHECK_NEAR(board.number("missing", -1.0f), -1.0f, 1e-6f); + + board.eraseNumber("threat"); + CHECK(!board.hasNumber("threat")); + CHECK_EQ(board.entryCount(), 5u); + board.eraseRoute("path"); + CHECK(!board.hasRoute("path")); + + board.clear(); + CHECK_EQ(board.entryCount(), 0u); +} + +VF_TEST(sequenceStopsAtTheChildThatFailsAndSkipsTheRest) { + int firstRuns = 0; + int lastRuns = 0; + bool gateOpen = false; + + auto root = std::unique_ptr(new SequenceNode("sequence")); + root->add(constantAction("first", BehaviorStatus::Success, &firstRuns)); + root->add(flagCondition("gate", &gateOpen)); + root->add(constantAction("last", BehaviorStatus::Success, &lastRuns)); + + BehaviorTree tree(std::move(root)); + BehaviorContext ctx; + + CHECK(tree.tick(ctx) == BehaviorStatus::Failure); + CHECK_EQ(firstRuns, 1); + CHECK_EQ(lastRuns, 0); + + gateOpen = true; + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(firstRuns, 2); + CHECK_EQ(lastRuns, 1); +} + +VF_TEST(selectorTakesTheFirstBranchThatSucceedsAndFailsWhenNoneDo) { + int firstRuns = 0; + int secondRuns = 0; + int thirdRuns = 0; + + auto root = std::unique_ptr(new SelectorNode("selector")); + root->add(constantAction("first", BehaviorStatus::Failure, &firstRuns)); + root->add(constantAction("second", BehaviorStatus::Success, &secondRuns)); + root->add(constantAction("third", BehaviorStatus::Success, &thirdRuns)); + + BehaviorTree tree(std::move(root)); + BehaviorContext ctx; + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(firstRuns, 1); + CHECK_EQ(secondRuns, 1); + CHECK_EQ(thirdRuns, 0); + + auto allFail = std::unique_ptr(new SelectorNode("selector")); + allFail->add(constantAction("a", BehaviorStatus::Failure, nullptr)); + allFail->add(constantAction("b", BehaviorStatus::Failure, nullptr)); + BehaviorTree hopeless(std::move(allFail)); + CHECK(hopeless.tick(ctx) == BehaviorStatus::Failure); +} + +VF_TEST(aRunningNodeResumesNextTickInsteadOfRestartingTheSequence) { + int firstRuns = 0; + int slowRuns = 0; + int slowProgress = 0; +