test: extend coverage of testBehavior
This commit is contained in:
1 parent
8103be3f96
commit
e7781bb5ca
1 file changed
+162
@@ -313,3 +313,165 @@ VF_TEST(timeLimitCutsOffAChildThatNeverSettles) {
|
|||||||
CHECK_EQ(runs, 4);
|
CHECK_EQ(runs, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VF_TEST(parallelSettlesOnItsSuccessAndFailureThresholds) {
|
||||||
|
int slowRuns = 0;
|
||||||
|
int slowProgress = 0;
|
||||||
|
|
||||||
|
auto wide = std::unique_ptr<ParallelNode>(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<ParallelNode>(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<BehaviorNode> node = constantAction("leaf", BehaviorStatus::Success, nullptr);
|
||||||
|
for (int level = 0; level < maxBehaviorDepth + 12; ++level) {
|
||||||
|
node = std::unique_ptr<BehaviorNode>(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<BehaviorNode> node = constantAction("leaf", BehaviorStatus::Success, nullptr);
|
||||||
|
for (int level = 0; level < maxBehaviorDepth - 1; ++level) {
|
||||||
|
node = std::unique_ptr<BehaviorNode>(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<SequenceNode>(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<std::string> 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<long long> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user