#include "testHarness.h" #include "vf/StructureGen.h" #include #include using namespace vf; namespace { bool piecesEqual(const std::vector& a, const std::vector& b) { if (a.size() != b.size()) return false; for (std::size_t i = 0; i < a.size(); ++i) { if (a[i].offset != b[i].offset) return false; if (a[i].block != b[i].block) return false; } return true; } int countBlock(const StructurePlan& plan, Block block) { int count = 0; for (const StructurePiece& piece : plan.pieces) if (piece.block == block) ++count; return count; } } // namespace VF_TEST(structureGenBoundingBoxContainsAndIntersects) { BoundingBoxI box{{0, 0, 0}, {10, 10, 10}}; CHECK(box.contains({5, 5, 5})); CHECK(box.contains({0, 0, 0})); CHECK(box.contains({10, 10, 10})); CHECK(!box.contains({11, 5, 5})); CHECK(!box.contains({5, -1, 5})); BoundingBoxI overlapping{{5, 5, 5}, {15, 15, 15}}; BoundingBoxI disjoint{{20, 20, 20}, {30, 30, 30}}; CHECK(box.intersects(overlapping)); CHECK(!box.intersects(disjoint)); } VF_TEST(structureGenChunkStructureHashIsDeterministicAndVaries) { uint32_t first = chunkStructureHash(1337u, 3, -5, 0x11u); uint32_t second = chunkStructureHash(1337u, 3, -5, 0x11u); CHECK_EQ(first, second); uint32_t differentSalt = chunkStructureHash(1337u, 3, -5, 0x22u); uint32_t differentCoord = chunkStructureHash(1337u, 4, -5, 0x11u); uint32_t differentSeed = chunkStructureHash(7u, 3, -5, 0x11u); CHECK(first != differentSalt); CHECK(first != differentCoord); CHECK(first != differentSeed); } VF_TEST(structureGenDistributionCoversEveryKind) { StructureGen gen; std::array counts{}; for (int cx = -60; cx <= 60; ++cx) { for (int cz = -60; cz <= 60; ++cz) { StructureGenKind kind; if (gen.structureForChunk(cx, cz, kind)) counts[static_cast(kind)] += 1; } } for (int count : counts) CHECK(count > 0); } VF_TEST(structureGenChunkDecisionIsDeterministic) { StructureGen gen; for (int cx = -30; cx <= 30; cx += 3) { for (int cz = -30; cz <= 30; cz += 3) { StructureGenKind a, b; bool hitA = gen.structureForChunk(cx, cz, a); bool hitB = gen.structureForChunk(cx, cz, b); CHECK_EQ(hitA, hitB); if (hitA) CHECK(a == b); } } } VF_TEST(structureGenBuildIsDeterministicForSameInputs) { StructureGen gen; StructurePlan a = gen.buildStructure(StructureGenKind::Village, 3, 4, 40); StructurePlan b = gen.buildStructure(StructureGenKind::Village, 3, 4, 40); CHECK(piecesEqual(a.pieces, b.pieces)); CHECK(a.origin == b.origin); CHECK(a.bounds.min == b.bounds.min); CHECK(a.bounds.max == b.bounds.max); } VF_TEST(structureGenBuildDiffersAcrossChunksAndSeeds) { StructureGen genA(StructureGenParams{11u, 16, 42, 37, 53, 61, 71}); StructureGen genB(StructureGenParams{99u, 16, 42, 37, 53, 61, 71}); StructurePlan village1 = genA.buildStructure(StructureGenKind::Village, 1, 1, 40); StructurePlan village2 = genA.buildStructure(StructureGenKind::Village, 2, 5, 40); StructurePlan village3 = genB.buildStructure(StructureGenKind::Village, 1, 1, 40); CHECK(!piecesEqual(village1.pieces, village2.pieces)); CHECK(!piecesEqual(village1.pieces, village3.pieces)); } VF_TEST(structureGenAllPiecesFallInsideBounds) { StructureGen gen; StructureGenKind kinds[5] = { StructureGenKind::Village, StructureGenKind::Dungeon, StructureGenKind::Mineshaft, StructureGenKind::Ravine, StructureGenKind::DesertTemple, }; for (StructureGenKind kind : kinds) { StructurePlan plan = gen.buildStructure(kind, 5, -7, 50); CHECK(!plan.pieces.empty()); for (const StructurePiece& piece : plan.pieces) { Vec3i world = plan.origin + piece.offset; CHECK(plan.bounds.contains(world)); } } } VF_TEST(structureGenVillageHasHousesAndLanterns) { StructureGen gen; StructurePlan plan = gen.buildStructure(StructureGenKind::Village, 0, 0, 40); CHECK(countBlock(plan, Block::Cobblestone) > 0); CHECK(countBlock(plan, Block::Planks) > 0); CHECK(countBlock(plan, Block::Wood) > 0); CHECK(countBlock(plan, Block::Glowstone) > 0); CHECK(countBlock(plan, Block::Air) > 0); } VF_TEST(structureGenDungeonHasHollowRoomSpawnerAndTwoChests) { StructureGen gen; StructurePlan plan = gen.buildStructure(StructureGenKind::Dungeon, 2, 2, 60); CHECK_EQ(countBlock(plan, Block::Chest), 2); CHECK(countBlock(plan, Block::Glowstone) >= 1); CHECK(countBlock(plan, Block::Cobblestone) > 0); CHECK(countBlock(plan, Block::Air) > 0); CHECK(plan.origin.y < 60); } VF_TEST(structureGenMineshaftHasCorridorsSupportsAndChest) { StructureGen gen; StructurePlan plan = gen.buildStructure(StructureGenKind::Mineshaft, -3, 8, 70); CHECK(countBlock(plan, Block::Planks) > 0); CHECK(countBlock(plan, Block::Wood) > 0); CHECK(countBlock(plan, Block::Air) > 0); CHECK_EQ(countBlock(plan, Block::Chest), 1); } VF_TEST(structureGenRavineIsAPureCarve) { StructureGen gen; StructurePlan plan = gen.buildStructure(StructureGenKind::Ravine, 9, -2, 40); CHECK(!plan.pieces.empty()); for (const StructurePiece& piece : plan.pieces) CHECK(piece.block == Block::Air); CHECK(plan.bounds.max.y - plan.bounds.min.y > 5); } VF_TEST(structureGenDesertTempleHasSandstoneShellAndHiddenChest) { StructureGen gen; StructurePlan plan = gen.buildStructure(StructureGenKind::DesertTemple, 4, 4, 40); CHECK(countBlock(plan, Block::Sandstone) > 20); CHECK_EQ(countBlock(plan, Block::Chest), 1); CHECK(countBlock(plan, Block::Air) > 0); bool hasBelowGround = false; for (const StructurePiece& piece : plan.pieces) if (piece.offset.y < 0) hasBelowGround = true; CHECK(hasBelowGround); } VF_TEST(structureGenStampWritesEveryPieceWithinRange) { StructureGen gen; World world; StructurePlan plan = gen.buildStructure(StructureGenKind::Dungeon, 1, 1, 30); gen.stamp(world, plan, 128); // Later pieces at the same offset overwrite earlier ones (e.g. a chest // stamped over the floor cell it sits on), so the oracle must replay the // writes in order rather than expect every single piece to survive. std::unordered_map oracle; for (const StructurePiece& piece : plan.pieces) oracle[plan.origin + piece.offset] = piece.block; CHECK(!oracle.empty()); for (const auto& entry : oracle) CHECK(world.blockAt(entry.first.x, entry.first.y, entry.first.z) == entry.second); } VF_TEST(structureGenStampClipsPiecesOutsideMaxY) { StructureGen gen; World world; StructurePlan plan = gen.buildStructure(StructureGenKind::DesertTemple, 0, 0, 40); int splitY = (plan.bounds.min.y + plan.bounds.max.y) / 2; gen.stamp(world, plan, splitY); std::unordered_map oracle; std::vector outOfRange; for (const StructurePiece& piece : plan.pieces) { Vec3i worldPos = plan.origin + piece.offset; if (worldPos.y < 0 || worldPos.y >= splitY) { outOfRange.push_back(worldPos); } else { oracle[worldPos] = piece.block; } } CHECK(!oracle.empty()); CHECK(!outOfRange.empty()); for (const auto& entry : oracle) CHECK(world.blockAt(entry.first.x, entry.first.y, entry.first.z) == entry.second); for (const Vec3i& pos : outOfRange) if (oracle.find(pos) == oracle.end()) CHECK(world.blockAt(pos.x, pos.y, pos.z) == Block::Air); }