Files
EVEngine/test/housegen.cpp
T

223 lines
9.4 KiB
C++

#include "housegen/HouseComponentLibrary.h"
#include "housegen/HouseGenerator.h"
#include "housegen/HouseLayout.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <algorithm>
using namespace eve::housegen;
static const char *kKit = R"({"components":[
{"id":"foundation","model":"fixtures/foundation.glb","category":"foundation"},
{"id":"floor","model":"fixtures/floor.glb","category":"floor"},
{"id":"wall","model":"fixtures/wall.glb","category":"wall","weight":2},
{"id":"wall.alt","model":"fixtures/wall.glb","category":"wall","weight":1},
{"id":"door","model":"fixtures/door.glb","category":"door"},
{"id":"roof","model":"fixtures/roof.glb","category":"roof"}
]})";
TEST_CASE("housegen.library.validatesManifest") {
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(kKit);
REQUIRE(loaded.ok());
CHECK_EQ(lib.count(), 6);
auto failed = lib.loadFromJson(R"([{"id":"broken","category":"wall"}])");
REQUIRE(!failed.ok());
CHECK(failed.status().hasDiagnostics());
}
TEST_CASE("housegen.library.validatesMaterialOverrides") {
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(R"([{
"id":"wall.material","model":"wall.glb","category":"wall",
"material":{"baseColor":[0.82,0.71,0.55,1.0],"baseColorTexture":"wall.png",
"normalTexture":"wall-normal.png","heightTexture":"wall-height.png",
"metallic":0.0,"roughness":0.86,"parallaxScale":0.035,
"parallaxMinLayers":8,"parallaxMaxLayers":24,
"cellBombScale":5,"cellBombStrength":0.2,"cellBombRotation":0.1}
}])");
REQUIRE(loaded.ok());
const auto component = lib.find("wall.material");
REQUIRE(component.has_value());
CHECK(component->get().material.hasBaseColor);
CHECK_EQ(component->get().material.baseColorTexture, std::string("wall.png"));
CHECK_EQ(component->get().material.normalTexture, std::string("wall-normal.png"));
CHECK_EQ(component->get().material.heightTexture, std::string("wall-height.png"));
CHECK(component->get().material.hasMetallic);
CHECK(component->get().material.hasRoughness);
CHECK(component->get().material.parallaxScale > 0.f);
auto failed = lib.loadFromJson(R"([{
"id":"bad.material","model":"wall.glb","category":"wall",
"material":{"roughness":1.2}
}])");
REQUIRE(!failed.ok());
CHECK(failed.status().describe().find("between 0 and 1") != std::string::npos);
}
TEST_CASE("housegen.reproducibleAndSerializable") {
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(kKit);
REQUIRE(loaded.ok());
HouseGenerator generator(lib);
HouseRequest r;
r.seed = 42;
r.width = 5;
r.depth = 4;
r.floors = 2;
HouseLayout a, b;
auto first = generator.generate(r, a);
auto second = generator.generate(r, b);
REQUIRE(first.ok());
REQUIRE(second.ok());
CHECK_EQ(a.toJson(), b.toJson());
CHECK(a.instances.size() > 20);
HouseLayout restored;
auto restoredResult = restored.fromJson(a.toJson());
REQUIRE(restoredResult.ok());
CHECK_EQ(restored.toJson(), a.toJson());
auto validation = restored.validate(lib);
CHECK(validation.ok());
}
TEST_CASE("housegen.requiresStructuralCategories") {
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(R"([{"id":"wall","model":"wall.glb","category":"wall"}])");
REQUIRE(loaded.ok());
HouseGenerator generator(lib);
HouseLayout out;
auto failed = generator.generate(HouseRequest{}, out);
REQUIRE(!failed.ok());
CHECK(failed.status().describe().find("foundation") != std::string::npos);
}
TEST_CASE("housegen.upperFloorsNeverExpandBeyondSupport") {
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(kKit);
REQUIRE(loaded.ok());
HouseGenerator generator(lib);
HouseRequest r;
r.seed = 8675309; r.width = 6; r.depth = 5; r.floors = 3;
HouseLayout layout;
auto generated = generator.generate(r, layout);
REQUIRE(generated.ok());
for (int z = 1; z < r.floors; ++z) {
int lowerMinX = r.width, lowerMaxX = -1, lowerMinY = r.depth, lowerMaxY = -1;
int upperMinX = r.width, upperMaxX = -1, upperMinY = r.depth, upperMaxY = -1;
for (const auto &instance : layout.instances) {
const auto component = lib.find(instance.componentId);
if (!component || component->get().category != "floor") continue;
if (instance.z == z - 1) {
lowerMinX = std::min(lowerMinX, instance.x); lowerMaxX = std::max(lowerMaxX, instance.x);
lowerMinY = std::min(lowerMinY, instance.y); lowerMaxY = std::max(lowerMaxY, instance.y);
} else if (instance.z == z) {
upperMinX = std::min(upperMinX, instance.x); upperMaxX = std::max(upperMaxX, instance.x);
upperMinY = std::min(upperMinY, instance.y); upperMaxY = std::max(upperMaxY, instance.y);
}
}
CHECK(upperMinX >= lowerMinX); CHECK(upperMaxX <= lowerMaxX);
CHECK(upperMinY >= lowerMinY); CHECK(upperMaxY <= lowerMaxY);
}
}
TEST_CASE("housegen.shapeRoofAndEntranceVariants") {
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(kKit);
REQUIRE(loaded.ok());
HouseGenerator generator(lib);
const char *shapes[] = {"rectangle", "l_shape", "t_shape"};
const char *roofs[] = {"gable", "flat", "shed"};
const char *entrances[] = {"north", "east", "west"};
const int rotations[] = {0, 90, 270};
int floorCounts[3] = {};
for (int variant = 0; variant < 3; ++variant) {
HouseRequest r;
r.seed = 100 + variant;
r.width = 7;
r.depth = 6;
r.floors = 2;
r.footprint = shapes[variant]; r.roof = roofs[variant]; r.entrance = entrances[variant];
HouseLayout layout;
auto generated = generator.generate(r, layout);
REQUIRE(generated.ok());
CHECK_EQ(layout.footprintStyle, std::string(shapes[variant]));
CHECK_EQ(layout.roofStyle, std::string(roofs[variant]));
CHECK_EQ(layout.entranceSide, std::string(entrances[variant]));
bool foundDoor = false;
for (const auto &instance : layout.instances) {
const auto component = lib.find(instance.componentId);
if (!component) continue;
if (component->get().category == "floor" && instance.z == 0) ++floorCounts[variant];
if (component->get().category == "door") {
foundDoor = true; CHECK_EQ(instance.rotationDeg, rotations[variant]);
}
}
CHECK(foundDoor);
}
CHECK(floorCounts[0] != floorCounts[1]);
CHECK(floorCounts[1] != floorCounts[2]);
CHECK(floorCounts[0] != floorCounts[2]);
}
TEST_CASE("housegen.everyFloorCellRequiresCoverage") {
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(kKit);
REQUIRE(loaded.ok());
HouseGenerator generator(lib);
HouseRequest r;
r.seed = 8675309; r.width = 7; r.depth = 6; r.floors = 3;
r.footprint = "l_shape";
HouseLayout layout;
auto generated = generator.generate(r, layout);
REQUIRE(generated.ok());
auto valid = layout.validate(lib);
REQUIRE(valid.ok());
auto roof = std::find_if(layout.instances.begin(), layout.instances.end(), [&](const auto &instance) {
const auto component = lib.find(instance.componentId);
return component && component->get().category == "roof" && instance.z == r.floors;
});
REQUIRE(roof != layout.instances.end());
layout.instances.erase(roof);
auto invalid = layout.validate(lib);
REQUIRE(!invalid.ok());
CHECK(invalid.status().describe().find("roof coverage") != std::string::npos);
}
TEST_CASE("housegen.facadeKeepsCornersSolidAndWindowsAligned") {
constexpr char facadeKit[] = R"({"components":[
{"id":"foundation","model":"foundation.glb","category":"foundation"},
{"id":"floor","model":"floor.glb","category":"floor"},
{"id":"wall.solid","model":"wall.glb","category":"wall"},
{"id":"wall.window","model":"window.glb","category":"wall","tags":["window"]},
{"id":"door","model":"door.glb","category":"door"},
{"id":"roof","model":"roof.glb","category":"roof"}
]})";
HouseComponentLibrary lib;
auto loaded = lib.loadFromJson(facadeKit);
REQUIRE(loaded.ok());
HouseRequest r;
r.seed = 44;
r.width = 4;
r.depth = 4;
r.floors = 2;
r.footprint = "rectangle"; r.roof = "flat"; r.entrance = "north";
HouseLayout layout;
HouseGenerator generator(lib);
auto generated = generator.generate(r, layout);
REQUIRE(generated.ok());
std::vector<std::tuple<int, int, int>> groundWindows, upperWindows;
for (const auto &instance : layout.instances) {
if (instance.componentId != "wall.window") continue;
const bool corner = (instance.x == 0 || instance.x == r.width - 1) &&
(instance.y == 0 || instance.y == r.depth - 1);
CHECK(!corner);
auto key = std::make_tuple(instance.x, instance.y, instance.rotationDeg);
(instance.z == 0 ? groundWindows : upperWindows).push_back(key);
}
REQUIRE(!groundWindows.empty());
for (const auto &window : groundWindows)
CHECK(std::find(upperWindows.begin(), upperWindows.end(), window) != upperWindows.end());
}