diff --git a/tests/testStructureGen.cpp b/tests/testStructureGen.cpp new file mode 100644 index 0000000..eddbf0a --- /dev/null +++ b/tests/testStructureGen.cpp @@ -0,0 +1,214 @@ +#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); +}