#include "testHarness.h" #include "vf/LightEngine.h" using namespace vf; namespace { void fillBox(World& world, Vec3i lo, Vec3i hi, Block block) { for (int x = lo.x; x <= hi.x; ++x) for (int y = lo.y; y <= hi.y; ++y) for (int z = lo.z; z <= hi.z; ++z) world.setBlock(x, y, z, block); } // Compares an incrementally maintained engine against a full rebuild of the // same world+box. Equality is the correctness oracle for incremental edits. bool matchesRebuild(const LightEngine& incremental, World& world) { Vec3i origin = incremental.regionOrigin(); Vec3i dims = incremental.regionDims(); LightEngine fresh(world, origin, dims); fresh.rebuild(); for (int lx = 0; lx < dims.x; ++lx) for (int ly = 0; ly < dims.y; ++ly) for (int lz = 0; lz < dims.z; ++lz) { int gx = origin.x + lx, gy = origin.y + ly, gz = origin.z + lz; if (incremental.sunAt(gx, gy, gz) != fresh.sunAt(gx, gy, gz)) return false; if (incremental.blockAt(gx, gy, gz) != fresh.blockAt(gx, gy, gz)) return false; } return true; } } // namespace VF_TEST(lightEngineSkyFillsOpenColumnStraightDown) { World world; fillBox(world, {0, 0, 0}, {7, 0, 7}, Block::Stone); LightEngine engine(world, {0, 0, 0}, {8, 8, 8}); engine.rebuild(); CHECK_EQ(static_cast(engine.sunAt(3, 7, 3)), maxLightLevel); CHECK_EQ(static_cast(engine.sunAt(3, 1, 3)), maxLightLevel); // no vertical loss } VF_TEST(lightEngineBlockEmitterFallsOffRadially) { World world; world.setBlock(5, 5, 5, Block::Glowstone); LightEngine engine(world, {0, 0, 0}, {11, 11, 11}); engine.rebuild(); CHECK_EQ(static_cast(engine.blockAt(5, 5, 5)), maxLightLevel); CHECK_EQ(static_cast(engine.blockAt(6, 5, 5)), maxLightLevel - 1); CHECK_EQ(static_cast(engine.blockAt(8, 5, 5)), maxLightLevel - 3); } VF_TEST(lightEnginePlacingOpaqueDarkensColumnBelow) { World world; fillBox(world, {0, 0, 0}, {4, 11, 4}, Block::Stone); // solid box for (int y = 1; y <= 11; ++y) world.setBlock(2, y, 2, Block::Air); // walled shaft LightEngine engine(world, {0, 0, 0}, {5, 12, 5}); engine.rebuild(); CHECK_EQ(static_cast(engine.sunAt(2, 1, 2)), maxLightLevel); engine.setBlock(2, 6, 2, Block::Stone); // seal the shaft midway CHECK_EQ(static_cast(engine.sunAt(2, 5, 2)), 0); // shadow below the lid CHECK_EQ(static_cast(engine.sunAt(2, 7, 2)), maxLightLevel); // still lit above CHECK(matchesRebuild(engine, world)); } VF_TEST(lightEngineBreakingOpaqueRePropagates) { World world; fillBox(world, {0, 0, 0}, {4, 11, 4}, Block::Stone); for (int y = 1; y <= 11; ++y) world.setBlock(2, y, 2, Block::Air); world.setBlock(2, 6, 2, Block::Stone); // lid already in place LightEngine engine(world, {0, 0, 0}, {5, 12, 5}); engine.rebuild(); CHECK_EQ(static_cast(engine.sunAt(2, 5, 2)), 0); // in shadow engine.setBlock(2, 6, 2, Block::Air); // reopen the shaft CHECK_EQ(static_cast(engine.sunAt(2, 5, 2)), maxLightLevel); // relit straight down CHECK(matchesRebuild(engine, world)); } VF_TEST(lightEngineRemovingEmitterDarkensRoom) { World world; fillBox(world, {0, 0, 0}, {8, 8, 8}, Block::Stone); fillBox(world, {1, 1, 1}, {7, 7, 7}, Block::Air); // sealed room, no sky world.setBlock(4, 4, 4, Block::Glowstone); LightEngine engine(world, {0, 0, 0}, {9, 9, 9}); engine.rebuild(); CHECK(engine.blockAt(5, 4, 4) > 0); engine.setBlock(4, 4, 4, Block::Air); // remove the emitter CHECK_EQ(static_cast(engine.blockAt(5, 4, 4)), 0); CHECK_EQ(static_cast(engine.blockAt(4, 4, 4)), 0); CHECK(matchesRebuild(engine, world)); } VF_TEST(lightEngineEditAcrossChunkBorderMatchesRebuild) { World world; // floor spanning four chunk columns so light crosses chunk borders in the box. fillBox(world, {12, 0, 12}, {35, 0, 35}, Block::Stone); LightEngine engine(world, {12, 0, 12}, {24, 20, 24}); // spans chunks x/z 0..2 engine.rebuild(); world.setBlock(0, 0, 0, Block::Air); engine.setBlock(15, 5, 15, Block::Glowstone); // near a chunk boundary engine.setBlock(16, 5, 16, Block::Stone); engine.setBlock(31, 5, 31, Block::Glowstone); // in a different chunk engine.setBlock(31, 5, 31, Block::Air); CHECK(matchesRebuild(engine, world)); } VF_TEST(lightEngineEditSequenceStaysConsistentWithRebuild) { World world; fillBox(world, {0, 0, 0}, {15, 0, 15}, Block::Stone); LightEngine engine(world, {0, 0, 0}, {16, 16, 16}); engine.rebuild(); const Block palette[4] = {Block::Stone, Block::Air, Block::Glowstone, Block::Water}; uint32_t rng = 0x1234567u; auto nextRng = [&rng]() { rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; return rng; }; for (int i = 0; i < 120; ++i) { int x = static_cast(nextRng() % 16); int y = 1 + static_cast(nextRng() % 12); int z = static_cast(nextRng() % 16); Block b = palette[nextRng() % 4]; engine.setBlock(x, y, z, b); } CHECK(matchesRebuild(engine, world)); }