Files
EVEngine/test/procgen_grid_graph.cpp

280 lines
12 KiB
C++

#include "procgen/GeneratorRegistry.h"
#include "procgen/GridGraph.h"
#include "procgen/MeshGraph.h"
#include "procgen/Params.h"
#include "procgen/PointGraph.h"
#include "procgen/Semantic.h"
#include "procgen/TilePreset.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
using namespace eve::procgen;
TEST_CASE("procgen.tilePreset.matchesReferenceStandardGridTables") {
const auto isolated = resolveTilePreset(0);
CHECK_EQ(isolated.configuration, 16);
CHECK_EQ(isolated.kind, TilePresetKind::Single);
CHECK_EQ(isolated.rotationDegrees, 0);
CHECK(!isolated.mirrorX);
const auto north = resolveTilePreset(1u << 0u);
CHECK_EQ(north.configuration, 18);
CHECK_EQ(north.kind, TilePresetKind::DeadEnd);
CHECK_EQ(north.rotationDegrees, 90);
const auto east = resolveTilePreset(1u << 1u);
CHECK_EQ(east.configuration, 48);
CHECK_EQ(east.kind, TilePresetKind::DeadEnd);
CHECK_EQ(east.rotationDegrees, 180);
const auto asymmetric = resolveTilePreset(139);
CHECK_EQ(asymmetric.configuration, 59);
CHECK_EQ(asymmetric.kind, TilePresetKind::EdgeCornerFill);
CHECK_EQ(asymmetric.rotationDegrees, 90);
CHECK(asymmetric.mirrorX);
const auto filled = resolveTilePreset(255);
CHECK_EQ(filled.configuration, 511);
CHECK_EQ(filled.kind, TilePresetKind::Fill);
// The source 4.3.5 table omits configuration 350; preserve that compatibility edge case.
const auto unclassified = resolveTilePreset(121);
CHECK_EQ(unclassified.configuration, 350);
CHECK_EQ(unclassified.kind, TilePresetKind::None);
for (int mask = 0; mask <= 255; ++mask) {
const auto variant = resolveTilePreset(std::uint8_t(mask));
CHECK((variant.configuration & 16u) != 0u);
const bool validRotation = variant.rotationDegrees == 0 || variant.rotationDegrees == 90 ||
variant.rotationDegrees == 180 || variant.rotationDegrees == 270;
CHECK(validRotation);
}
}
TEST_CASE("procgen.gridGraph.composesGridAndPointGraphs") {
Grid2D source;
source.resize(5, 5);
source.setCell(2, 2, int(Semantic::Grass));
PointGraph points;
REQUIRE(points.addNode("input", "input"));
REQUIRE(points.addNode("move", "transform"));
REQUIRE(points.connect("input", "move"));
REQUIRE(points.setNodeFloat("move", "y", 3.f));
GridGraph graph;
REQUIRE(graph.addNode("source", "grid.input").ok());
REQUIRE(graph.addNode("grow", "grid.expand").ok());
REQUIRE(graph.addNode("points", "convert.grid_to_points").ok());
REQUIRE(graph.addNode("point_pass", "point.subgraph").ok());
REQUIRE(graph.connect("source", "grow").ok());
REQUIRE(graph.connect("grow", "points").ok());
REQUIRE(graph.connect("points", "point_pass").ok());
REQUIRE(graph.setNodeGrid("source", source).ok());
REQUIRE(graph.setNodeInt("grow", "radius", 1).ok());
REQUIRE(graph.setNodeFloat("points", "cellSize", 2.f).ok());
REQUIRE(graph.setNodePointSubgraph("point_pass", points, "input", "move").ok());
auto result = graph.execute("point_pass");
REQUIRE(result.ok());
const auto* output = std::get_if<PointSet>(&result.value());
REQUIRE(output != nullptr);
CHECK_EQ(output->getCount(), 9);
CHECK_EQ(output->getY(0), 3.f);
CHECK(output->hasIntAttribute(0, "cell_x"));
CHECK(output->hasIntAttribute(0, "semantic"));
auto again = graph.execute("point_pass");
REQUIRE(again.ok());
CHECK_EQ(std::get<PointSet>(again.value()).getCount(), 9);
}
TEST_CASE("procgen.gridGraph.rejectsTypeMismatchCyclesAndDimensionMismatch") {
GridGraph graph;
REQUIRE(graph.addNode("a", "grid.input").ok());
REQUIRE(graph.addNode("b", "grid.input").ok());
REQUIRE(graph.addNode("merge", "grid.union").ok());
REQUIRE(graph.addNode("points", "convert.grid_to_points").ok());
REQUIRE(graph.connect("a", "merge", 0).ok());
REQUIRE(graph.connect("b", "merge", 1).ok());
REQUIRE(graph.connect("merge", "points").ok());
auto badType = graph.connect("points", "merge", 0);
CHECK(!badType.ok());
Grid2D small;
small.resize(2, 2);
Grid2D large;
large.resize(3, 3);
REQUIRE(graph.setNodeGrid("a", small).ok());
REQUIRE(graph.setNodeGrid("b", large).ok());
auto mismatch = graph.execute("merge");
CHECK(!mismatch.ok());
GridGraph cycle;
REQUIRE(cycle.addNode("left", "grid.invert").ok());
REQUIRE(cycle.addNode("right", "grid.invert").ok());
REQUIRE(cycle.connect("left", "right").ok());
REQUIRE(cycle.connect("right", "left").ok());
auto cyclic = cycle.validate("left");
CHECK(!cyclic.ok());
}
TEST_CASE("procgen.gridGraph.coversGeneratorsSelectorsPathAndSerialization") {
GridGraph graph;
REQUIRE(graph.addNode("maze", "generate.maze").ok());
REQUIRE(graph.setNodeInt("maze", "width", 17).ok());
REQUIRE(graph.setNodeInt("maze", "height", 17).ok());
REQUIRE(graph.setNodeInt("maze", "seed", 42).ok());
REQUIRE(graph.addNode("border", "select.border").ok());
REQUIRE(graph.connect("maze", "border").ok());
auto first = graph.execute("border");
REQUIRE(first.ok());
const auto* selected = std::get_if<Grid2D>(&first.value());
REQUIRE(selected != nullptr);
CHECK_EQ(selected->getWidth(), 17);
GridGraph restored;
REQUIRE(restored.deserializeDefinition(graph.serializeDefinition()).ok());
auto second = restored.execute("border");
REQUIRE(second.ok());
CHECK_EQ(std::get<Grid2D>(second.value()).cells(), selected->cells());
Grid2D start;
start.resize(17, 17);
start.setCell(1, 1, int(Semantic::Floor));
Grid2D target;
target.resize(17, 17);
target.setCell(15, 15, int(Semantic::Floor));
REQUIRE(restored.addNode("start", "grid.input").ok());
REQUIRE(restored.addNode("target", "grid.input").ok());
REQUIRE(restored.addNode("path", "grid.path").ok());
REQUIRE(restored.setNodeGrid("start", start).ok());
REQUIRE(restored.setNodeGrid("target", target).ok());
REQUIRE(restored.connect("maze", "path", 0).ok());
REQUIRE(restored.connect("start", "path", 1).ok());
REQUIRE(restored.connect("target", "path", 2).ok());
CHECK(restored.execute("path").ok());
}
TEST_CASE("procgen.gridGraph.reusesRegisteredRoguelikeAndExtractsSemanticLayers") {
GeneratorRegistry::instance().registerBuiltins();
for (const int seed : {7, 42, 436}) {
Params params;
params.setSize(31, 23);
params.setSeed(std::uint32_t(seed));
params.setInt("roomCount", 9);
params.setString("layoutStyle", "clustered");
params.setString("connectionStyle", "nearest");
params.setString("corridorStyle", "l");
Grid2D expected;
std::string error;
REQUIRE(GeneratorRegistry::instance().generate("level.roguelike", params, expected, error));
GridGraph graph;
REQUIRE(graph.addNode("level", "generate.registry").ok());
REQUIRE(graph.setNodeString("level", "algorithm", "level.roguelike").ok());
REQUIRE(graph.setNodeInt("level", "width", 31).ok());
REQUIRE(graph.setNodeInt("level", "height", 23).ok());
REQUIRE(graph.setNodeInt("level", "seed", seed).ok());
REQUIRE(graph.setNodeInt("level", "roomCount", 9).ok());
REQUIRE(graph.setNodeString("level", "layoutStyle", "clustered").ok());
REQUIRE(graph.setNodeString("level", "connectionStyle", "nearest").ok());
REQUIRE(graph.setNodeString("level", "corridorStyle", "l").ok());
REQUIRE(graph.addNode("floor", "select.semantic").ok());
REQUIRE(graph.addNode("corridor", "select.semantic").ok());
REQUIRE(graph.setNodeInt("floor", "semantic", int(Semantic::Floor)).ok());
REQUIRE(graph.setNodeInt("corridor", "semantic", int(Semantic::Corridor)).ok());
REQUIRE(graph.connect("level", "floor").ok());
REQUIRE(graph.connect("level", "corridor").ok());
auto generated = graph.execute("level");
REQUIRE(generated.ok());
const auto& actual = std::get<Grid2D>(generated.value());
CHECK_EQ(actual.cells(), expected.cells());
CHECK_EQ(actual.detail(), expected.detail());
CHECK_EQ(actual.getObjectCount(), expected.getObjectCount());
auto floor = graph.execute("floor");
auto corridor = graph.execute("corridor");
REQUIRE(floor.ok());
REQUIRE(corridor.ok());
int selected = 0;
for (const auto cell : std::get<Grid2D>(floor.value()).cells()) {
const bool valid = cell == Semantic::Empty || cell == Semantic::Floor;
CHECK(valid);
if (cell == Semantic::Floor) ++selected;
}
for (const auto cell : std::get<Grid2D>(corridor.value()).cells()) {
const bool valid = cell == Semantic::Empty || cell == Semantic::Corridor;
CHECK(valid);
if (cell == Semantic::Corridor) ++selected;
}
CHECK(selected > 0);
GridGraph restored;
REQUIRE(restored.deserializeDefinition(graph.serializeDefinition()).ok());
auto restoredLevel = restored.execute("level");
REQUIRE(restoredLevel.ok());
CHECK_EQ(std::get<Grid2D>(restoredLevel.value()).cells(), expected.cells());
}
GridGraph missing;
REQUIRE(missing.addNode("level", "generate.registry").ok());
CHECK(!missing.execute("level").ok());
REQUIRE(missing.setNodeString("level", "algorithm", "missing.generator").ok());
CHECK(!missing.execute("level").ok());
}
TEST_CASE("procgen.meshGraph.composesGridPointAndMeshDomains") {
Grid2D grid;
grid.resize(2, 1);
grid.setCell(0, 0, int(Semantic::Floor));
grid.setCell(1, 0, int(Semantic::Floor));
GridGraph autotile;
REQUIRE(autotile.addNode("grid", "grid.input").ok());
REQUIRE(autotile.addNode("autotile", "grid.autotile").ok());
REQUIRE(autotile.setNodeGrid("grid", grid).ok());
REQUIRE(autotile.connect("grid", "autotile").ok());
auto autotiled = autotile.execute("autotile");
REQUIRE(autotiled.ok());
grid = std::get<Grid2D>(autotiled.value());
MeshGraph tiles;
REQUIRE(tiles.addNode("grid", "grid.input").ok());
REQUIRE(tiles.addNode("tiles", "mesh.grid_tiles").ok());
REQUIRE(tiles.setNodeGrid("grid", grid).ok());
REQUIRE(tiles.connect("grid", "tiles").ok());
auto tiled = tiles.execute("tiles");
REQUIRE(tiled.ok());
CHECK_EQ(tiled.value().getIndexCount(), 48);
CHECK_EQ(tiled.value().getGroupCount(), 2);
CHECK(tiled.value().getGroupName(0).find("/dead_end/r180/normal/48") != std::string::npos);
CHECK(tiled.value().getGroupName(1).find("/dead_end/r0/normal/24") != std::string::npos);
MeshGraph restoredTiles;
REQUIRE(restoredTiles.deserializeDefinition(tiles.serializeDefinition()).ok());
REQUIRE(restoredTiles.setNodeGrid("grid", grid).ok());
auto restoredMesh = restoredTiles.execute("tiles");
REQUIRE(restoredMesh.ok());
CHECK_EQ(restoredMesh.value().getIndexCount(), tiled.value().getIndexCount());
CHECK(!restoredTiles.setNodeFloat("tiles", "misspelled", 1.f).ok());
PointSet points;
points.add(0.f, 0.f, 0.f);
points.add(4.f, 0.f, 0.f);
MeshGraph instances;
REQUIRE(instances.addNode("mesh", "mesh.input").ok());
REQUIRE(instances.addNode("points", "point.input").ok());
REQUIRE(instances.addNode("instances", "mesh.instance_points").ok());
REQUIRE(instances.setNodeMesh("mesh", tiled.value()).ok());
REQUIRE(instances.setNodePoints("points", points).ok());
REQUIRE(instances.connect("mesh", "instances", 0).ok());
REQUIRE(instances.connect("points", "instances", 1).ok());
auto result = instances.execute("instances");
REQUIRE(result.ok());
CHECK_EQ(result.value().getIndexCount(), tiled.value().getIndexCount() * 2);
}