Files
EVEngine/src/modules/procgen/GridGraphAlgorithms.cpp
T

340 lines
16 KiB
C++

#include "procgen/GridGraphAlgorithms.h"
#include "common/Diagnostic.h"
#include "procgen/Semantic.h"
#include <algorithm>
#include <cmath>
#include <queue>
#include <random>
#include <sstream>
namespace eve::procgen::gridgraph {
namespace {
template <class T>
Result<T> failure(std::string message) {
return Result<T>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, std::move(message), {}, {}, "procgen.gridGraph"));
}
bool occupied(const Grid2D& grid, int x, int y) { return grid.getCell(x, y) != int(Semantic::Empty); }
Grid2D makeGrid(const GenerateSettings& settings) {
Grid2D grid;
grid.resize(settings.width, settings.height);
return grid;
}
void setOccupied(Grid2D& grid, int x, int y, int semantic) {
if (x >= 0 && y >= 0 && x < grid.getWidth() && y < grid.getHeight()) grid.setCell(x, y, semantic);
}
std::vector<std::pair<int, int>> cells(const Grid2D& grid) {
std::vector<std::pair<int, int>> result;
for (int y = 0; y < grid.getHeight(); ++y)
for (int x = 0; x < grid.getWidth(); ++x)
if (occupied(grid, x, y)) result.emplace_back(x, y);
return result;
}
std::vector<std::vector<std::pair<int, int>>> islands(const Grid2D& grid) {
std::vector<std::vector<std::pair<int, int>>> result;
std::vector<unsigned char> visited(std::size_t(grid.getWidth() * grid.getHeight()));
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int y = 0; y < grid.getHeight(); ++y) {
for (int x = 0; x < grid.getWidth(); ++x) {
const int start = y * grid.getWidth() + x;
if (!occupied(grid, x, y) || visited[std::size_t(start)]) continue;
result.emplace_back();
std::queue<std::pair<int, int>> pending;
pending.emplace(x, y);
visited[std::size_t(start)] = 1;
while (!pending.empty()) {
const auto current = pending.front();
pending.pop();
result.back().push_back(current);
for (int direction = 0; direction < 4; ++direction) {
const int nx = current.first + dx[direction];
const int ny = current.second + dy[direction];
if (nx < 0 || ny < 0 || nx >= grid.getWidth() || ny >= grid.getHeight()) continue;
const int index = ny * grid.getWidth() + nx;
if (occupied(grid, nx, ny) && !visited[std::size_t(index)]) {
visited[std::size_t(index)] = 1;
pending.emplace(nx, ny);
}
}
}
}
}
return result;
}
Grid2D cellular(const GenerateSettings& settings) {
Grid2D grid = makeGrid(settings);
std::mt19937_64 rng(settings.seed);
std::uniform_real_distribution<float> probability(0.f, 1.f);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x)
if (probability(rng) < settings.x) grid.setCell(x, y, settings.semantic);
for (int step = 0; step < settings.a; ++step) {
Grid2D next = grid;
for (int y = 0; y < settings.height; ++y) {
for (int x = 0; x < settings.width; ++x) {
int neighbors = 0;
for (int oy = -1; oy <= 1; ++oy)
for (int ox = -1; ox <= 1; ++ox)
if ((ox != 0 || oy != 0) && occupied(grid, x + ox, y + oy)) ++neighbors;
next.setCell(
x, y,
neighbors > 4 ? settings.semantic : (neighbors < 4 ? int(Semantic::Empty) : grid.getCell(x, y)));
}
}
grid = std::move(next);
}
return grid;
}
Grid2D randomWalk(const GenerateSettings& settings) {
Grid2D grid = makeGrid(settings);
std::mt19937_64 rng(settings.seed);
std::uniform_int_distribution<int> startX(0, settings.width - 1);
std::uniform_int_distribution<int> startY(0, settings.height - 1);
std::uniform_int_distribution<int> direction(0, 3);
int x = settings.b != 0 ? std::clamp(settings.b, 0, settings.width - 1) : startX(rng);
int y = settings.c != 0 ? std::clamp(settings.c, 0, settings.height - 1) : startY(rng);
const int iterations = std::max(1, settings.a);
const int length = std::max(1, int(settings.x));
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int iteration = 0; iteration < iterations; ++iteration) {
for (int step = 0; step < length; ++step) {
setOccupied(grid, x, y, settings.semantic);
const int d = direction(rng);
x = std::clamp(x + dx[d], 0, settings.width - 1);
y = std::clamp(y + dy[d], 0, settings.height - 1);
}
if (settings.y > 0.5f) {
x = startX(rng);
y = startY(rng);
}
}
return grid;
}
Grid2D maze(const GenerateSettings& settings) {
Grid2D grid = makeGrid(settings);
if (settings.width < 3 || settings.height < 3) return grid;
std::mt19937_64 rng(settings.seed);
std::vector<std::pair<int, int>> stack{{1, 1}};
setOccupied(grid, 1, 1, settings.semantic);
constexpr int dx[] = {-2, 2, 0, 0};
constexpr int dy[] = {0, 0, -2, 2};
while (!stack.empty()) {
const auto current = stack.back();
std::vector<int> candidates;
for (int d = 0; d < 4; ++d) {
const int nx = current.first + dx[d];
const int ny = current.second + dy[d];
if (nx > 0 && ny > 0 && nx < settings.width - 1 && ny < settings.height - 1 && !occupied(grid, nx, ny))
candidates.push_back(d);
}
if (candidates.empty()) {
stack.pop_back();
continue;
}
std::shuffle(candidates.begin(), candidates.end(), rng);
const int d = candidates.front();
const int nx = current.first + dx[d];
const int ny = current.second + dy[d];
setOccupied(grid, current.first + dx[d] / 2, current.second + dy[d] / 2, settings.semantic);
setOccupied(grid, nx, ny, settings.semantic);
stack.emplace_back(nx, ny);
}
return grid;
}
} // namespace
Result<Grid2D> generate(std::string_view operation, const GenerateSettings& settings) {
if (settings.width <= 0 || settings.height <= 0) return failure<Grid2D>("generator dimensions must be positive");
Grid2D grid = makeGrid(settings);
std::mt19937_64 rng(settings.seed);
if (operation == "generate.fill") {
grid.fill(settings.semantic);
} else if (operation == "generate.random_noise") {
std::uniform_real_distribution<float> probability(0.f, 1.f);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x)
if (probability(rng) < settings.x) grid.setCell(x, y, settings.semantic);
} else if (operation == "generate.checkerboard" || operation == "generate.dot_grid") {
const int spacing = std::max(1, settings.a);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x)
if (operation == "generate.checkerboard" ? ((x / spacing + y / spacing) % 2 == 0)
: (x % spacing == 0 && y % spacing == 0))
grid.setCell(x, y, settings.semantic);
} else if (operation == "generate.shape") {
const int cx = settings.a;
const int cy = settings.b;
const int radius = std::max(1, settings.c);
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x) {
bool inside = settings.x < 0.5f ? ((x - cx) * (x - cx) + (y - cy) * (y - cy) <= radius * radius)
: (std::abs(x - cx) <= radius && std::abs(y - cy) <= radius);
if (inside) grid.setCell(x, y, settings.semantic);
}
} else if (operation == "generate.cellular") {
grid = cellular(settings);
} else if (operation == "generate.random_walk") {
grid = randomWalk(settings);
} else if (operation == "generate.maze") {
grid = maze(settings);
} else if (operation == "generate.poisson") {
const float radius = std::max(1.f, settings.x);
std::vector<std::pair<int, int>> accepted;
std::vector<std::pair<int, int>> candidates;
for (int y = 0; y < settings.height; ++y)
for (int x = 0; x < settings.width; ++x) candidates.emplace_back(x, y);
std::shuffle(candidates.begin(), candidates.end(), rng);
for (const auto& candidate : candidates) {
const bool clear = std::none_of(accepted.begin(), accepted.end(), [&](const auto& point) {
const float dx = float(point.first - candidate.first);
const float dy = float(point.second - candidate.second);
return dx * dx + dy * dy < radius * radius;
});
if (clear) {
accepted.push_back(candidate);
grid.setCell(candidate.first, candidate.second, settings.semantic);
}
}
} else {
return failure<Grid2D>("unsupported generator: " + std::string(operation));
}
return Result<Grid2D>::success(std::move(grid));
}
Result<Grid2D> select(const Grid2D& input, std::string_view operation, int mode, int count, float weight,
std::uint64_t seed, std::string_view rule) {
Grid2D out;
out.resize(input.getWidth(), input.getHeight());
auto active = cells(input);
if (operation == "select.random") {
std::mt19937_64 rng(seed);
std::shuffle(active.begin(), active.end(), rng);
const int amount = count > 0 ? std::min(count, int(active.size()))
: int(std::round(std::clamp(weight, 0.f, 1.f) * float(active.size())));
active.resize(std::size_t(amount));
for (const auto& point : active)
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
} else if (operation == "select.border" || operation == "select.fill") {
for (const auto& point : active) {
bool border = false;
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int d = 0; d < 4; ++d) border = border || !occupied(input, point.first + dx[d], point.second + dy[d]);
if ((operation == "select.border" && border) || (operation == "select.fill" && !border))
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
} else if (operation == "select.neighbors") {
for (const auto& point : active) {
int neighbors = 0;
for (int oy = -1; oy <= 1; ++oy)
for (int ox = -1; ox <= 1; ++ox)
if ((ox != 0 || oy != 0) && occupied(input, point.first + ox, point.second + oy)) ++neighbors;
if ((mode == 0 && neighbors == count) || (mode == 1 && neighbors >= count) ||
(mode == 2 && neighbors <= count))
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
} else if (operation == "select.rule") {
if (rule.size() != 9) return failure<Grid2D>("rule must contain exactly nine characters (0/1/*)");
for (const auto& point : active) {
bool match = true;
for (int oy = -1; oy <= 1 && match; ++oy)
for (int ox = -1; ox <= 1; ++ox) {
const char expected = rule[std::size_t((oy + 1) * 3 + ox + 1)];
if (expected != '*' &&
(occupied(input, point.first + ox, point.second + oy) != (expected == '1'))) {
match = false;
break;
}
}
if (match) out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
} else if (operation == "select.islands" || operation == "select.island_centers") {
for (const auto& island : islands(input)) {
const bool keep = mode == 0 ? int(island.size()) < count
: (mode == 1 ? int(island.size()) > count : int(island.size()) == count);
if (!keep) continue;
if (operation == "select.island_centers") {
long long sx = 0;
long long sy = 0;
for (const auto& point : island) {
sx += point.first;
sy += point.second;
}
const int x = int(std::llround(double(sx) / double(island.size())));
const int y = int(std::llround(double(sy) / double(island.size())));
out.setCell(x, y, input.getCell(island.front().first, island.front().second));
} else {
for (const auto& point : island)
out.setCell(point.first, point.second, input.getCell(point.first, point.second));
}
}
} else {
return failure<Grid2D>("unsupported selector: " + std::string(operation));
}
return Result<Grid2D>::success(std::move(out));
}
Result<Grid2D> findPath(const Grid2D& navigation, const Grid2D& starts, const Grid2D& targets, int semantic) {
if (navigation.getWidth() != starts.getWidth() || navigation.getHeight() != starts.getHeight() ||
navigation.getWidth() != targets.getWidth() || navigation.getHeight() != targets.getHeight())
return failure<Grid2D>("pathfinding grids must have equal dimensions");
const auto startCells = cells(starts);
const auto targetCells = cells(targets);
if (startCells.empty() || targetCells.empty())
return failure<Grid2D>("pathfinding requires start and target cells");
const int width = navigation.getWidth();
const int height = navigation.getHeight();
std::vector<int> previous(std::size_t(width * height), -1);
std::queue<int> pending;
const int start = startCells.front().second * width + startCells.front().first;
pending.push(start);
previous[std::size_t(start)] = start;
int destination = -1;
while (!pending.empty() && destination < 0) {
const int current = pending.front();
pending.pop();
const int x = current % width;
const int y = current / width;
if (occupied(targets, x, y)) {
destination = current;
break;
}
constexpr int dx[] = {-1, 1, 0, 0};
constexpr int dy[] = {0, 0, -1, 1};
for (int d = 0; d < 4; ++d) {
const int nx = x + dx[d];
const int ny = y + dy[d];
if (nx < 0 || ny < 0 || nx >= width || ny >= height || !occupied(navigation, nx, ny)) continue;
const int next = ny * width + nx;
if (previous[std::size_t(next)] < 0) {
previous[std::size_t(next)] = current;
pending.push(next);
}
}
}
if (destination < 0) return failure<Grid2D>("no path found");
Grid2D out;
out.resize(width, height);
for (int current = destination;; current = previous[std::size_t(current)]) {
out.setCell(current % width, current / width, semantic);
if (current == start) break;
}
return Result<Grid2D>::success(std::move(out));
}
} // namespace eve::procgen::gridgraph