#include "testHarness.h" #include "vf/BlockTick.h" using namespace vf; namespace { int countBlock(World& world, Block target, int x0, int x1, int y0, int y1, int z0, int z1) { int count = 0; for (int x = x0; x <= x1; ++x) for (int y = y0; y <= y1; ++y) for (int z = z0; z <= z1; ++z) if (world.blockAt(x, y, z) == target) ++count; return count; } } // namespace VF_TEST(scheduledTickFiresAfterExactDelay) { World world; BlockTickScheduler scheduler(1, 0); scheduler.schedule({3, 3, 3}, 4); for (int i = 0; i < 3; ++i) CHECK(scheduler.tick(world).empty()); std::vector fired = scheduler.tick(world); CHECK_EQ(fired.size(), static_cast(1)); CHECK(fired[0] == Vec3i(3, 3, 3)); } VF_TEST(scheduledTicksFireInDueThenInsertionOrder) { World world; BlockTickScheduler scheduler(1, 0); scheduler.schedule({1, 1, 1}, 3); scheduler.schedule({2, 2, 2}, 1); scheduler.schedule({3, 3, 3}, 1); std::vector firstTick = scheduler.tick(world); CHECK_EQ(firstTick.size(), static_cast(2)); CHECK(firstTick[0] == Vec3i(2, 2, 2)); CHECK(firstTick[1] == Vec3i(3, 3, 3)); CHECK(scheduler.tick(world).empty()); std::vector thirdTick = scheduler.tick(world); CHECK_EQ(thirdTick.size(), static_cast(1)); CHECK(thirdTick[0] == Vec3i(1, 1, 1)); } VF_TEST(cancelRemovesAPendingTickBeforeItFires) { World world; BlockTickScheduler scheduler(1, 0); scheduler.schedule({5, 5, 5}, 2); CHECK(scheduler.isScheduled({5, 5, 5})); scheduler.cancel({5, 5, 5}); CHECK(!scheduler.isScheduled({5, 5, 5})); scheduler.tick(world); std::vector fired = scheduler.tick(world); CHECK(fired.empty()); } VF_TEST(sandFallsOneCellPerTickUntilItLands) { World world; world.setBlock(0, 0, 0, Block::Stone); world.setBlock(0, 5, 0, Block::Sand); BlockTickScheduler scheduler(1, 0); scheduler.tick(world); CHECK(world.blockAt(0, 4, 0) == Block::Sand); CHECK(world.blockAt(0, 5, 0) == Block::Air); scheduler.tick(world); CHECK(world.blockAt(0, 3, 0) == Block::Sand); scheduler.tick(world); CHECK(world.blockAt(0, 2, 0) == Block::Sand); scheduler.tick(world); CHECK(world.blockAt(0, 1, 0) == Block::Sand); // landed on the stone scheduler.tick(world); CHECK(world.blockAt(0, 1, 0) == Block::Sand); // stays put once supported CHECK_EQ(countBlock(world, Block::Sand, -2, 2, 0, 6, -2, 2), 1); } VF_TEST(gravelBehavesLikeSandAndSupportedBlocksDoNotFall) { World world; world.setBlock(1, 0, 1, Block::Stone); world.setBlock(1, 1, 1, Block::Gravel); BlockTickScheduler scheduler(1, 0); scheduler.tick(world); scheduler.tick(world); CHECK(world.blockAt(1, 1, 1) == Block::Gravel); // already supported, never moved } VF_TEST(stackedFallingBlocksMaintainRelativeOrderWithoutGaps) { World world; world.setBlock(2, 0, 2, Block::Stone); world.setBlock(2, 5, 2, Block::Sand); world.setBlock(2, 6, 2, Block::Gravel); BlockTickScheduler scheduler(1, 0); for (int i = 0; i < 5; ++i) scheduler.tick(world); CHECK(world.blockAt(2, 1, 2) == Block::Sand); CHECK(world.blockAt(2, 2, 2) == Block::Gravel); CHECK(world.blockAt(2, 3, 2) == Block::Air); } VF_TEST(randomTicksSampleConfiguredCountPerLoadedChunk) { World world; world.setBlock(0, 0, 0, Block::Stone); world.setBlock(20, 0, 0, Block::Stone); // second chunk along x BlockTickScheduler scheduler(42, 5); scheduler.tick(world); CHECK_EQ(scheduler.lastRandomTicks().size(), static_cast(10)); for (const Vec3i& pos : scheduler.lastRandomTicks()) { CHECK(pos.x >= -16 && pos.x < 32); CHECK(pos.y >= 0 && pos.y < 16); CHECK(pos.z >= 0 && pos.z < 16); } } VF_TEST(randomTickSamplingIsDeterministicForAFixedSeed) { World worldA, worldB; worldA.setBlock(0, 0, 0, Block::Stone); worldB.setBlock(0, 0, 0, Block::Stone); BlockTickScheduler a(99, 4); BlockTickScheduler b(99, 4); for (int i = 0; i < 3; ++i) { a.tick(worldA); b.tick(worldB); CHECK(a.lastRandomTicks() == b.lastRandomTicks()); } } VF_TEST(differentSeedsProduceDifferentRandomTickSequences) { World worldA, worldB; worldA.setBlock(0, 0, 0, Block::Stone); worldB.setBlock(0, 0, 0, Block::Stone); BlockTickScheduler a(1, 6); BlockTickScheduler b(2, 6); a.tick(worldA); b.tick(worldB); CHECK(a.lastRandomTicks() != b.lastRandomTicks()); } VF_TEST(growthHookFiresOnRandomTicksAndGrowsExposedDirtToGrass) { World world; world.setBlock(3, 3, 3, Block::Dirt); world.setBlock(3, 3, 4, Block::Dirt); world.setBlock(3, 4, 4, Block::Stone); // covered dirt must not grow BlockTickScheduler scheduler(7, 200); scheduler.setRandomTickHook([](World& w, const Vec3i& pos, Block b) { if (b == Block::Dirt) tryGrowGrass(w, pos); }); bool grewExposed = false; for (int i = 0; i < 100 && !grewExposed; ++i) { scheduler.tick(world); if (world.blockAt(3, 3, 3) == Block::Grass) grewExposed = true; } CHECK(grewExposed); CHECK(world.blockAt(3, 3, 4) == Block::Dirt); // stays dirt, blocked by the stone above } VF_TEST(tryGrowGrassRejectsNonDirtAndCoveredDirt) { World world; world.setBlock(0, 0, 0, Block::Stone); CHECK(!tryGrowGrass(world, {0, 0, 0})); world.setBlock(0, 1, 0, Block::Dirt); world.setBlock(0, 2, 0, Block::Stone); CHECK(!tryGrowGrass(world, {0, 1, 0})); CHECK(world.blockAt(0, 1, 0) == Block::Dirt); world.setBlock(0, 2, 0, Block::Air); CHECK(tryGrowGrass(world, {0, 1, 0})); CHECK(world.blockAt(0, 1, 0) == Block::Grass); } VF_TEST(currentTickAdvancesByOnePerCall) { World world; BlockTickScheduler scheduler(1, 0); CHECK_EQ(scheduler.currentTick(), 0); scheduler.tick(world); scheduler.tick(world); scheduler.tick(world); CHECK_EQ(scheduler.currentTick(), 3); }