diff --git a/tests/testBehavior.cpp b/tests/testBehavior.cpp index c07c083..10a2950 100644 --- a/tests/testBehavior.cpp +++ b/tests/testBehavior.cpp @@ -313,3 +313,165 @@ VF_TEST(timeLimitCutsOffAChildThatNeverSettles) { CHECK_EQ(runs, 4); } +VF_TEST(parallelSettlesOnItsSuccessAndFailureThresholds) { + int slowRuns = 0; + int slowProgress = 0; + + auto wide = std::unique_ptr(new ParallelNode("parallel", 2, 2)); + wide->add(constantAction("ok", BehaviorStatus::Success, nullptr)); + wide->add(slowAction("slow", 2, &slowRuns, &slowProgress)); + wide->add(constantAction("nope", BehaviorStatus::Failure, nullptr)); + + BehaviorTree tree(std::move(wide)); + BehaviorContext ctx; + + // One success, one failure, one still running: neither threshold is met. + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + // The slow child succeeds on its third visit, which is the second success. + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(slowRuns, 3); + + auto doomed = std::unique_ptr(new ParallelNode("parallel", 3, 2)); + doomed->add(constantAction("no", BehaviorStatus::Failure, nullptr)); + doomed->add(constantAction("alsoNo", BehaviorStatus::Failure, nullptr)); + doomed->add(constantAction("yes", BehaviorStatus::Success, nullptr)); + BehaviorTree unlucky(std::move(doomed)); + CHECK(unlucky.tick(ctx) == BehaviorStatus::Failure); +} + +VF_TEST(anOverDeepTreeIsRejectedInsteadOfOverflowingTheStack) { + std::unique_ptr node = constantAction("leaf", BehaviorStatus::Success, nullptr); + for (int level = 0; level < maxBehaviorDepth + 12; ++level) { + node = std::unique_ptr(new InverterNode("wrap", std::move(node))); + } + + BehaviorTree tree(std::move(node)); + CHECK(!tree.structureValid()); + CHECK(tree.stats().depth > maxBehaviorDepth); + CHECK_EQ(tree.stats().nodeCount, maxBehaviorDepth + 13); + + BehaviorContext ctx; + CHECK(tree.tick(ctx) == BehaviorStatus::Failure); + CHECK_EQ(ctx.tick, 0); + CHECK_EQ(tree.tickCount(), 0); +} + +VF_TEST(aTreeExactlyAtTheDepthLimitStillRuns) { + std::unique_ptr node = constantAction("leaf", BehaviorStatus::Success, nullptr); + for (int level = 0; level < maxBehaviorDepth - 1; ++level) { + node = std::unique_ptr(new InverterNode("wrap", std::move(node))); + } + + BehaviorTree tree(std::move(node)); + CHECK_EQ(tree.stats().depth, maxBehaviorDepth); + CHECK(tree.structureValid()); + + BehaviorContext ctx; + tree.tick(ctx); + CHECK_EQ(ctx.tick, 1); +} + +VF_TEST(theTraceRecordsEveryNodeVisitedInOrder) { + auto root = std::unique_ptr(new SequenceNode("root")); + root->add(constantAction("first", BehaviorStatus::Success, nullptr)); + root->add(constantAction("second", BehaviorStatus::Failure, nullptr)); + root->add(constantAction("third", BehaviorStatus::Success, nullptr)); + + BehaviorTree tree(std::move(root)); + BehaviorContext ctx; + ctx.traceEnabled = true; + tree.tick(ctx); + + std::vector expected{"root", "first", "second"}; + CHECK_EQ(ctx.trace.size(), expected.size()); + for (std::size_t i = 0; i < expected.size() && i < ctx.trace.size(); ++i) { + CHECK_EQ(ctx.trace[i], expected[i]); + } +} + +VF_TEST(theHostileTreePathsToThePlayerAndLandsRepeatedBlows) { + World world = flatWorld(); + BehaviorAgent agent = makeAgent(Species::Zombie, {2.5f, 1.0f, 2.5f}); + + BehaviorContext ctx = freshContext(&world); + ctx.agent = &agent; + ctx.playerPresent = true; + ctx.playerPosition = {10.5f, 1.0f, 2.5f}; + + BehaviorTree tree = makeHostileHunterTree(); + CHECK(tree.structureValid()); + + float startDistance = behaviorHorizontalDistance(agent.position, ctx.playerPosition); + for (int step = 0; step < 120; ++step) tree.tick(ctx); + + CHECK(behaviorHorizontalDistance(agent.position, ctx.playerPosition) < startDistance); + CHECK(behaviorHorizontalDistance(agent.position, ctx.playerPosition) <= + speciesStats(Species::Zombie).attackRange); + CHECK(agent.attacksLanded >= 2); + CHECK(ctx.blackboard.flag("engaged")); + CHECK_NEAR(ctx.blackboard.number("lastAttackDamage"), + speciesStats(Species::Zombie).attackDamage, 1e-6f); + CHECK(agent.routeGoal == behaviorCellOf(ctx.playerPosition)); + CHECK(agent.routePlannedTick > 0); +} + +VF_TEST(theHostileTreeWalksHomeOnceTheTargetIsGone) { + World world = flatWorld(); + BehaviorAgent agent = makeAgent(Species::Zombie, {9.5f, 1.0f, 2.5f}); + agent.homePosition = {2.5f, 1.0f, 2.5f}; + + BehaviorContext ctx = freshContext(&world); + ctx.agent = &agent; + ctx.playerPresent = false; + + BehaviorTree tree = makeHostileHunterTree(); + for (int step = 0; step < 120; ++step) tree.tick(ctx); + + CHECK(behaviorHorizontalDistance(agent.position, agent.homePosition) <= + ctx.tuning.homeReturnRadius); + CHECK_EQ(agent.attacksLanded, 0); +} + +VF_TEST(theAnimalTreeRunsAwayFromAnEmptyHandedPlayer) { + World world = grassWorld(); + BehaviorAgent agent = makeAgent(Species::Sheep, {0.5f, 1.0f, 0.5f}); + + BehaviorContext ctx = freshContext(&world); + ctx.agent = &agent; + ctx.playerPresent = true; + ctx.playerPosition = {2.5f, 1.0f, 0.5f}; + ctx.playerHeldItem = ItemId::None; + + BehaviorTree tree = makeGrazingAnimalTree(); + float before = behaviorHorizontalDistance(agent.position, ctx.playerPosition); + for (int step = 0; step < 20; ++step) tree.tick(ctx); + + CHECK(behaviorHorizontalDistance(agent.position, ctx.playerPosition) > before + 2.0f); + CHECK_EQ(agent.breedCount, 0); +} + +VF_TEST(theAnimalTreeFollowsHeldFoodAndBreedsOnlyOncePerCooldown) { + World world = grassWorld(); + BehaviorAgent agent = makeAgent(Species::Sheep, {0.5f, 1.0f, 0.5f}); + + BehaviorContext ctx = freshContext(&world); + ctx.agent = &agent; + ctx.playerPresent = true; + ctx.playerPosition = {6.5f, 1.0f, 0.5f}; + ctx.playerHeldItem = breedingFoodFor(Species::Sheep); + ctx.matePresent = true; + ctx.matePosition = {5.0f, 1.0f, 0.5f}; + + BehaviorTree tree = makeGrazingAnimalTree(); + + std::vector breedTicks; + int lastCount = 0; + for (int step = 0; step < 400; ++step) { + tree.tick(ctx); + if (agent.breedCount != lastCount) { + lastCount = agent.breedCount; + breedTicks.push_back(ctx.tick); + } + } +