647 lines
22 KiB
C++
647 lines
22 KiB
C++
#include "testHarness.h"
|
|
#include "vf/Behavior.h"
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<BehaviorNode> constantAction(const char* name, BehaviorStatus status, int* counter) {
|
|
return std::unique_ptr<BehaviorNode>(new ActionNode(name, [status, counter](BehaviorContext&) {
|
|
if (counter != nullptr) ++(*counter);
|
|
return status;
|
|
}));
|
|
}
|
|
|
|
std::unique_ptr<BehaviorNode> flagCondition(const char* name, const bool* source) {
|
|
return std::unique_ptr<BehaviorNode>(
|
|
new ConditionNode(name, [source](BehaviorContext&) { return *source; }));
|
|
}
|
|
|
|
// Reports Running for `runningTicks` calls, then Success once, then restarts.
|
|
std::unique_ptr<BehaviorNode> slowAction(const char* name, int runningTicks, int* counter,
|
|
int* progress) {
|
|
return std::unique_ptr<BehaviorNode>(
|
|
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<SequenceNode>(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<SelectorNode>(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<SelectorNode>(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;
|
|
|
|
auto root = std::unique_ptr<SequenceNode>(new SequenceNode("sequence"));
|
|
root->add(constantAction("first", BehaviorStatus::Success, &firstRuns));
|
|
root->add(slowAction("slow", 2, &slowRuns, &slowProgress));
|
|
|
|
BehaviorTree tree(std::move(root));
|
|
BehaviorContext ctx;
|
|
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK_EQ(firstRuns, 1);
|
|
CHECK_EQ(slowRuns, 1);
|
|
|
|
// The whole point: the first child must not run again while the second is
|
|
// still Running.
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK_EQ(firstRuns, 1);
|
|
CHECK_EQ(slowRuns, 2);
|
|
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Success);
|
|
CHECK_EQ(firstRuns, 1);
|
|
CHECK_EQ(slowRuns, 3);
|
|
|
|
// Once settled the sequence starts over from the top.
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK_EQ(firstRuns, 2);
|
|
CHECK_EQ(slowRuns, 4);
|
|
}
|
|
|
|
VF_TEST(selectorResumesTheRunningBranchWithoutRecheckingEarlierOnes) {
|
|
int guardChecks = 0;
|
|
int slowRuns = 0;
|
|
int slowProgress = 0;
|
|
|
|
auto root = std::unique_ptr<SelectorNode>(new SelectorNode("selector"));
|
|
root->add(std::unique_ptr<BehaviorNode>(
|
|
new ConditionNode("guard", [&guardChecks](BehaviorContext&) {
|
|
++guardChecks;
|
|
return false;
|
|
})));
|
|
root->add(slowAction("slow", 3, &slowRuns, &slowProgress));
|
|
|
|
BehaviorTree tree(std::move(root));
|
|
BehaviorContext ctx;
|
|
|
|
tree.tick(ctx);
|
|
CHECK_EQ(guardChecks, 1);
|
|
tree.tick(ctx);
|
|
tree.tick(ctx);
|
|
CHECK_EQ(guardChecks, 1);
|
|
CHECK_EQ(slowRuns, 3);
|
|
CHECK_EQ(tree.root()->childAt(1)->name(), std::string("slow"));
|
|
}
|
|
|
|
VF_TEST(cooldownBlocksForExactlyTheRequestedNumberOfTicks) {
|
|
int runs = 0;
|
|
auto gated = std::unique_ptr<BehaviorNode>(
|
|
new CooldownNode("cooldown", constantAction("fire", BehaviorStatus::Success, &runs), 3));
|
|
BehaviorTree tree(std::move(gated));
|
|
BehaviorContext ctx;
|
|
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Success);
|
|
CHECK_EQ(runs, 1);
|
|
|
|
for (int blocked = 0; blocked < 3; ++blocked) {
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Failure);
|
|
CHECK_EQ(runs, 1);
|
|
}
|
|
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Success);
|
|
CHECK_EQ(runs, 2);
|
|
|
|
// And the block starts again immediately after the second firing.
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Failure);
|
|
CHECK_EQ(runs, 2);
|
|
}
|
|
|
|
VF_TEST(resettingTheTreeClearsALiveCooldown) {
|
|
int runs = 0;
|
|
auto gated = std::unique_ptr<BehaviorNode>(
|
|
new CooldownNode("cooldown", constantAction("fire", BehaviorStatus::Success, &runs), 5));
|
|
BehaviorTree tree(std::move(gated));
|
|
BehaviorContext ctx;
|
|
|
|
tree.tick(ctx);
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Failure);
|
|
tree.reset();
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Success);
|
|
CHECK_EQ(runs, 2);
|
|
}
|
|
|
|
VF_TEST(inverterFlipsSuccessAndFailureButLetsRunningThrough) {
|
|
int progress = 0;
|
|
int runs = 0;
|
|
|
|
BehaviorContext ctx;
|
|
BehaviorTree yes(std::unique_ptr<BehaviorNode>(new InverterNode(
|
|
"invert", constantAction("succeeds", BehaviorStatus::Success, nullptr))));
|
|
CHECK(yes.tick(ctx) == BehaviorStatus::Failure);
|
|
|
|
BehaviorTree no(std::unique_ptr<BehaviorNode>(
|
|
new InverterNode("invert", constantAction("fails", BehaviorStatus::Failure, nullptr))));
|
|
CHECK(no.tick(ctx) == BehaviorStatus::Success);
|
|
|
|
BehaviorTree waiting(std::unique_ptr<BehaviorNode>(
|
|
new InverterNode("invert", slowAction("slow", 2, &runs, &progress))));
|
|
CHECK(waiting.tick(ctx) == BehaviorStatus::Running);
|
|
}
|
|
|
|
VF_TEST(repeaterSucceedsOnlyAfterTheRequestedNumberOfCompletions) {
|
|
int runs = 0;
|
|
auto repeat = std::unique_ptr<BehaviorNode>(new RepeaterNode(
|
|
"repeat", constantAction("beat", BehaviorStatus::Success, &runs), 3));
|
|
BehaviorTree tree(std::move(repeat));
|
|
BehaviorContext ctx;
|
|
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Success);
|
|
CHECK_EQ(runs, 3);
|
|
|
|
int failingRuns = 0;
|
|
auto stopping = std::unique_ptr<BehaviorNode>(new RepeaterNode(
|
|
"repeat", constantAction("beat", BehaviorStatus::Failure, &failingRuns), 3, true));
|
|
BehaviorTree strict(std::move(stopping));
|
|
CHECK(strict.tick(ctx) == BehaviorStatus::Failure);
|
|
CHECK_EQ(failingRuns, 1);
|
|
}
|
|
|
|
VF_TEST(retryUntilSuccessGivesUpAfterTheAttemptBudget) {
|
|
int runs = 0;
|
|
auto retry = std::unique_ptr<BehaviorNode>(new RetryUntilSuccessNode(
|
|
"retry", constantAction("attempt", BehaviorStatus::Failure, &runs), 3));
|
|
BehaviorTree tree(std::move(retry));
|
|
BehaviorContext ctx;
|
|
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Failure);
|
|
CHECK_EQ(runs, 3);
|
|
}
|
|
|
|
VF_TEST(timeLimitCutsOffAChildThatNeverSettles) {
|
|
int runs = 0;
|
|
auto limited = std::unique_ptr<BehaviorNode>(new TimeLimitNode(
|
|
"window", constantAction("stall", BehaviorStatus::Running, &runs), 3));
|
|
BehaviorTree tree(std::move(limited));
|
|
BehaviorContext ctx;
|
|
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Failure);
|
|
CHECK_EQ(runs, 3);
|
|
|
|
// The budget is refilled once the decorator gave its verdict.
|
|
CHECK(tree.tick(ctx) == BehaviorStatus::Running);
|
|
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);
|
|
}
|
|
}
|
|
|
|
CHECK(behaviorHorizontalDistance(agent.position, ctx.playerPosition) <= ctx.tuning.breedRadius);
|
|
CHECK(agent.breedCount >= 2);
|
|
for (std::size_t i = 1; i < breedTicks.size(); ++i) {
|
|
CHECK(breedTicks[i] - breedTicks[i - 1] > ctx.tuning.breedCooldownTicks);
|
|
}
|
|
}
|
|
|
|
VF_TEST(theAnimalTreeGrazesGrassForTheFullChewDuration) {
|
|
World world = grassWorld();
|
|
BehaviorAgent agent = makeAgent(Species::Cow, {0.5f, 1.0f, 0.5f});
|
|
|
|
BehaviorContext ctx = freshContext(&world);
|
|
ctx.agent = &agent;
|
|
ctx.playerPresent = false;
|
|
ctx.matePresent = false;
|
|
|
|
BehaviorTree tree = makeGrazingAnimalTree();
|
|
for (int step = 0; step < ctx.tuning.grazeDurationTicks - 1; ++step) tree.tick(ctx);
|
|
CHECK_EQ(ctx.blackboard.count("meals"), 0);
|
|
CHECK(agent.grazeTicks > 0);
|
|
|
|
tree.tick(ctx);
|
|
CHECK_EQ(ctx.blackboard.count("meals"), 1);
|
|
CHECK_EQ(agent.grazeTicks, 0);
|
|
}
|
|
|
|
VF_TEST(theSunAvoiderWalksIntoShadeAndWaitsOutTheDay) {
|
|
World world = flatWorld();
|
|
for (int x = 3; x <= 6; ++x) {
|
|
for (int z = -1; z <= 1; ++z) world.setBlock(x, 4, z, Block::Stone);
|
|
}
|
|
|
|
BehaviorAgent agent = makeAgent(Species::Skeleton, {0.5f, 1.0f, 0.5f});
|
|
BehaviorContext ctx = freshContext(&world);
|
|
ctx.agent = &agent;
|
|
ctx.daylight = true;
|
|
ctx.skyLight = 15;
|
|
|
|
BehaviorTree tree = makeSunAvoiderTree();
|
|
CHECK(!agent.sheltered);
|
|
for (int step = 0; step < 80; ++step) tree.tick(ctx);
|
|
|
|
CHECK(agent.sheltered);
|
|
CHECK(agent.position.x >= 3.0f);
|
|
CHECK(agent.shelterTicks > 0);
|
|
CHECK(ctx.blackboard.hasCell("shadeCell"));
|
|
}
|
|
|
|
VF_TEST(theSunAvoiderPatrolsInsteadOfHidingAtNight) {
|
|
World world = flatWorld();
|
|
BehaviorAgent agent = makeAgent(Species::Skeleton, {0.5f, 1.0f, 0.5f});
|
|
Vec3 start = agent.position;
|
|
|
|
BehaviorContext ctx = freshContext(&world);
|
|
ctx.agent = &agent;
|
|
ctx.daylight = false;
|
|
ctx.skyLight = 4;
|
|
|
|
BehaviorTree tree = makeSunAvoiderTree();
|
|
for (int step = 0; step < 30; ++step) tree.tick(ctx);
|
|
|
|
CHECK(behaviorHorizontalDistance(agent.position, start) > 1.0f);
|
|
CHECK_EQ(agent.shelterTicks, 0);
|
|
CHECK(!ctx.blackboard.hasCell("shadeCell"));
|
|
}
|
|
|
|
VF_TEST(twoRunsOfTheSameTreeOnTheSameSeedProduceTheSameNodeSequence) {
|
|
World world = grassWorld();
|
|
|
|
auto runOnce = [&world](std::vector<std::string>& trace, Vec3& endPosition) {
|
|
BehaviorAgent agent = makeAgent(Species::Pig, {0.5f, 1.0f, 0.5f});
|
|
agent.rng = 0x1234abcdu;
|
|
BehaviorContext ctx;
|
|
ctx.world = &world;
|
|
ctx.agent = &agent;
|
|
ctx.traceEnabled = true;
|
|
ctx.playerPresent = true;
|
|
ctx.playerPosition = {4.5f, 1.0f, 0.5f};
|
|
ctx.playerHeldItem = ItemId::None;
|
|
|
|
BehaviorTree tree = makeGrazingAnimalTree();
|
|
for (int step = 0; step < 60; ++step) tree.tick(ctx);
|
|
trace = ctx.trace;
|
|
endPosition = agent.position;
|
|
};
|
|
|
|
std::vector<std::string> firstTrace;
|
|
std::vector<std::string> secondTrace;
|
|
Vec3 firstEnd;
|
|
Vec3 secondEnd;
|
|
runOnce(firstTrace, firstEnd);
|
|
runOnce(secondTrace, secondEnd);
|
|
|
|
CHECK(firstTrace.size() > 100u);
|
|
CHECK_EQ(firstTrace.size(), secondTrace.size());
|
|
for (std::size_t i = 0; i < firstTrace.size() && i < secondTrace.size(); ++i) {
|
|
CHECK_EQ(firstTrace[i], secondTrace[i]);
|
|
}
|
|
CHECK_NEAR(firstEnd.x, secondEnd.x, 1e-6f);
|
|
CHECK_NEAR(firstEnd.z, secondEnd.z, 1e-6f);
|
|
}
|
|
|
|
VF_TEST(eachSpeciesMapsToTheTreeThatFitsIt) {
|
|
CHECK(roleForSpecies(Species::Zombie) == BehaviorRole::HostileHunter);
|
|
CHECK(roleForSpecies(Species::Creeper) == BehaviorRole::HostileHunter);
|
|
CHECK(roleForSpecies(Species::Skeleton) == BehaviorRole::SunAvoider);
|
|
CHECK(roleForSpecies(Species::Sheep) == BehaviorRole::GrazingAnimal);
|
|
|
|
CHECK(breedingFoodFor(Species::Cow) == ItemId::Bread);
|
|
CHECK(breedingFoodFor(Species::Pig) == ItemId::Apple);
|
|
CHECK(breedingFoodFor(Species::Zombie) == ItemId::None);
|
|
CHECK(isBreedingFood(Species::Cow, ItemId::Bread));
|
|
CHECK(!isBreedingFood(Species::Cow, ItemId::Apple));
|
|
CHECK(!isBreedingFood(Species::Zombie, ItemId::None));
|
|
|
|
for (int index = 0; index < static_cast<int>(BehaviorRole::Count); ++index) {
|
|
BehaviorRole role = static_cast<BehaviorRole>(index);
|
|
BehaviorTree tree = makeTreeForRole(role);
|
|
CHECK(tree.structureValid());
|
|
CHECK(tree.stats().nodeCount > 4);
|
|
CHECK(std::string(behaviorRoleName(role)) != std::string("unknown"));
|
|
}
|
|
}
|
|
|
|
VF_TEST(theSharedRandomStreamIsReproducibleAndStaysInRange) {
|
|
uint32_t first = 0x2545f491u;
|
|
uint32_t second = 0x2545f491u;
|
|
for (int draw = 0; draw < 64; ++draw) {
|
|
CHECK_EQ(behaviorNextRandom(first), behaviorNextRandom(second));
|
|
}
|
|
|
|
uint32_t stream = 7u;
|
|
for (int draw = 0; draw < 200; ++draw) {
|
|
float unit = behaviorUnitFloat(stream);
|
|
CHECK(unit >= 0.0f);
|
|
CHECK(unit < 1.0f);
|
|
int ranged = behaviorRandomRange(stream, -3, 3);
|
|
CHECK(ranged >= -3);
|
|
CHECK(ranged <= 3);
|
|
}
|
|
CHECK_EQ(behaviorRandomRange(stream, 5, 5), 5);
|
|
|
|
CHECK(behaviorCellOf(Vec3{-0.5f, 1.9f, 3.2f}) == Vec3i(-1, 1, 3));
|
|
CHECK_NEAR(behaviorCellCenter(Vec3i{2, 3, 4}).x, 2.5f, 1e-6f);
|
|
CHECK_NEAR(behaviorCellCenter(Vec3i{2, 3, 4}).y, 3.0f, 1e-6f);
|
|
CHECK(std::string(behaviorStatusName(BehaviorStatus::Running)) == std::string("running"));
|
|
}
|
|
|
|
VF_TEST(routeFollowingWalksCellByCellAndReportsSuccessAtTheEnd) {
|
|
World world = flatWorld();
|
|
BehaviorAgent agent = makeAgent(Species::Cow, {0.5f, 1.0f, 0.5f});
|
|
BehaviorContext ctx = freshContext(&world);
|
|
ctx.agent = &agent;
|
|
|
|
CHECK(planRouteTo(ctx, Vec3i{4, 1, 0}, true));
|
|
CHECK(agent.route.size() >= 5u);
|
|
CHECK_EQ(agent.routeCursor, 1);
|
|
|
|
BehaviorStatus status = BehaviorStatus::Running;
|
|
int guard = 0;
|
|
while (status == BehaviorStatus::Running && guard < 200) {
|
|
status = followRouteStep(ctx);
|
|
++guard;
|
|
}
|
|
CHECK(status == BehaviorStatus::Success);
|
|
CHECK(agent.routeFinished());
|
|
CHECK_NEAR(agent.position.x, 4.5f, 1e-4f);
|
|
CHECK(guard < 200);
|
|
}
|