From 5dbfacf76f808dcf770ec88254eae7d98b39a92e Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 18:22:25 +0200 Subject: [PATCH] test: extend coverage of testSpawning --- tests/testSpawning.cpp | 158 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tests/testSpawning.cpp b/tests/testSpawning.cpp index d976463..382fdd2 100644 --- a/tests/testSpawning.cpp +++ b/tests/testSpawning.cpp @@ -314,3 +314,161 @@ VF_TEST(aRunningCycleStopsSpawningOnceTheCapIsReached) { spawner.runCycle(env, SpawnCategory::Hostile, columns, 1234u + static_cast(cycle)); } + CHECK(env.populationOf(SpawnCategory::Hostile) > 0); + CHECK(env.populationOf(SpawnCategory::Hostile) <= limit + 4); + CHECK(spawner.denialCount(SpawnDenial::CategoryCapReached) > 0); +} + +VF_TEST(aPackArrivesSpreadAroundItsOriginCell) { + World world = plateWorld(Block::Grass); + LightField light = bakeLight(world); + SpawnEnvironment env = nightEnvironment(world, light); + + SpawnRule rule = zombieRule(); + rule.packMin = 4; + rule.packMax = 4; + rule.packSpread = 3; + + Spawner spawner({rule}); + SpawnTrial trial; + trial.cell = originCell(); + trial.species = Species::Zombie; + trial.category = SpawnCategory::Hostile; + trial.ruleFound = true; + trial.cellFound = true; + trial.packSize = 4; + + SpawnResult result = spawner.attemptSpawn(env, rule, trial, 4242u); + CHECK(result.spawned); + CHECK(result.denial == SpawnDenial::None); + CHECK(result.packSize() >= 2); + CHECK(result.packSize() <= 4); + CHECK(result.role == BehaviorRole::HostileHunter); + + for (const Vec3& spot : result.positions) { + CHECK(std::fabs(spot.x - 0.5f) <= static_cast(rule.packSpread)); + CHECK(std::fabs(spot.z - 0.5f) <= static_cast(rule.packSpread)); + CHECK(world.solidAt(static_cast(std::floor(spot.x)), floorY, + static_cast(std::floor(spot.z)))); + } +} + +VF_TEST(aSpawnedPackBecomesAgentsCarryingTheRightBehaviourRole) { + World world = plateWorld(Block::Grass); + LightField light = bakeLight(world); + SpawnEnvironment env = nightEnvironment(world, light); + + SpawnRule rule = zombieRule(); + rule.packMin = 3; + rule.packMax = 3; + Spawner spawner({rule}); + + SpawnTrial trial; + trial.cell = originCell(); + trial.species = Species::Zombie; + trial.ruleFound = true; + trial.cellFound = true; + trial.packSize = 3; + + SpawnResult result = spawner.attemptSpawn(env, rule, trial, 77u); + CHECK(result.spawned); + + std::vector agents = agentsFromSpawn(result, 5u); + CHECK_EQ(agents.size(), result.positions.size()); + const SpeciesStats& stats = speciesStats(Species::Zombie); + for (const BehaviorAgent& agent : agents) { + CHECK(agent.species == Species::Zombie); + CHECK_NEAR(agent.health, stats.maxHealth, 1e-6f); + CHECK_NEAR(agent.maxHealth, stats.maxHealth, 1e-6f); + CHECK(agent.moveStep > 0.0f); + CHECK(agent.rng != 0u); + } + CHECK(makeTreeForRole(result.role).structureValid()); + + SpawnResult refused; + CHECK(agentsFromSpawn(refused, 5u).empty()); +} + +VF_TEST(despawnNeverTouchesAMobStandingNextToThePlayer) { + DespawnPolicy policy; + DespawnCandidate mob; + mob.position = {10.0f, 64.0f, 0.0f}; + Vec3 player{0.0f, 64.0f, 0.0f}; + + CHECK(despawnBandFor(policy, mob.position, player) == DespawnBand::Protected); + uint32_t rng = 12345u; + for (int tick = 0; tick < 2000; ++tick) { + CHECK(!shouldDespawn(policy, mob, player, rng)); + } +} + +VF_TEST(despawnRemovesAMobBeyondTheHardDistanceImmediately) { + DespawnPolicy policy; + DespawnCandidate mob; + mob.position = {200.0f, 64.0f, 0.0f}; + Vec3 player{0.0f, 64.0f, 0.0f}; + + CHECK(despawnBandFor(policy, mob.position, player) == DespawnBand::Immediate); + uint32_t rng = 1u; + CHECK(shouldDespawn(policy, mob, player, rng)); + CHECK_EQ(rng, 1u); +} + +VF_TEST(despawnInTheMiddleBandIsARollThatMatchesItsStatedOdds) { + DespawnPolicy policy; + policy.randomChanceDenominator = 50; + DespawnCandidate mob; + mob.position = {64.0f, 64.0f, 0.0f}; + Vec3 player{0.0f, 64.0f, 0.0f}; + CHECK(despawnBandFor(policy, mob.position, player) == DespawnBand::Random); + + int hits = 0; + uint32_t rng = 0x1337u; + for (int tick = 0; tick < 5000; ++tick) { + if (shouldDespawn(policy, mob, player, rng)) ++hits; + } + CHECK(hits > 40); + CHECK(hits < 180); + + // Same seed, same sequence of rolls. + int replay = 0; + uint32_t again = 0x1337u; + for (int tick = 0; tick < 5000; ++tick) { + if (shouldDespawn(policy, mob, player, again)) ++replay; + } + CHECK_EQ(replay, hits); +} + +VF_TEST(namedAndLeashedMobsSurviveEvenAtTheEdgeOfTheWorld) { + DespawnPolicy policy; + Vec3 player{0.0f, 64.0f, 0.0f}; + uint32_t rng = 9u; + + DespawnCandidate named; + named.position = {500.0f, 64.0f, 0.0f}; + named.named = true; + CHECK(despawnCandidateIsProtected(policy, named)); + CHECK(!shouldDespawn(policy, named, player, rng)); + + DespawnCandidate leashed; + leashed.position = {500.0f, 64.0f, 0.0f}; + leashed.leashed = true; + CHECK(!shouldDespawn(policy, leashed, player, rng)); + + DespawnCandidate persistent; + persistent.position = {500.0f, 64.0f, 0.0f}; + persistent.persistent = true; + CHECK(!shouldDespawn(policy, persistent, player, rng)); + + DespawnPolicy ruthless; + ruthless.despawnNamed = true; + CHECK(!despawnCandidateIsProtected(ruthless, named)); + CHECK(shouldDespawn(ruthless, named, player, rng)); +} + +VF_TEST(theSpawnerBlockCountsDownActivatesAndRespectsItsNearbyLimit) { + World world = plateWorld(Block::Grass); + LightField light = bakeLight(world); + SpawnEnvironment env = nightEnvironment(world, light); + env.playerPosition = {0.5f, static_cast(spawnY), 0.5f}; +