载入中...
搜索中...
未找到
RoguelikeGenerator.cpp
浏览该文件的文档.
1// RoguelikeGenerator �?a configurable, seed-driven room-and-corridor level
2// generator ("RoguelikeGenerator Pro" style). Unlike dungeon.bsp it layers
3// extra data on top of the wall/floor grid:
4// * `detail` (Grid2D) stores per-cell wall-autotile direction masks, floor
5// pattern variants and scattered decor tiles (see k* constants below).
6// * `objects` carry placed props (pillars / chests / grass tufts) and the
7// spawn / stairs markers.
8// Everything is driven by Params so scripts can re-roll with a different seed
9// or tweak generation rules without touching code. Generated cells use the
10// standard Semantic ids, so the result still applies to a TileLayer through
11// the usual palette pipeline.
12//
13// Register id: "level.roguelike"
15
17#include "procgen/Grid2D.h"
18#include "procgen/Params.h"
19#include "procgen/Semantic.h"
20
21#include <algorithm>
22#include <cmath>
23#include <cstdint>
24#include <random>
25#include <string>
26#include <vector>
27
28namespace eve::procgen {
29namespace {
30
31// ---- detail-layer encoding (documented; the script example consumes these) ----
32// Wall cells: detail = 8-bit neighbour mask (which directions are walkable).
33// Floor cells: detail = floor-pattern variant (1..kFloorMaxVariant), or
34// >= kDecorBase to mark a scattered decor tile.
35constexpr int kDecorBase = 100; // floor detail >= this => decor tile
36constexpr int kFloorMaxVariant = 32; // floor pattern variant cap
37
38// Neighbour direction bits used by the wall mask.
39constexpr int kE = 1 << 0; // +x
40constexpr int kS = 1 << 1; // +y
41constexpr int kW = 1 << 2; // -x
42constexpr int kN = 1 << 3; // -y
43constexpr int kSE = 1 << 4;
44constexpr int kSW = 1 << 5;
45constexpr int kNW = 1 << 6;
46constexpr int kNE = 1 << 7;
47
48bool isWalkable(uint32_t c) {
49 return c == Semantic::Floor || c == Semantic::Corridor;
50}
51
52struct Rect {
53 int x = 0;
54 int y = 0;
55 int w = 0;
56 int h = 0;
57};
58
59int clampInt(int v, int lo, int hi) { return std::max(lo, std::min(hi, v)); }
60
62void autotileWalls(Grid2D &out) {
63 const int w = out.getWidth();
64 const int h = out.getHeight();
65 for (int y = 0; y < h; ++y) {
66 for (int x = 0; x < w; ++x) {
67 if (uint32_t(out.getCell(x, y)) != Semantic::Wall) continue;
68 int mask = 0;
69 if (x + 1 < w && isWalkable(uint32_t(out.getCell(x + 1, y)))) mask |= kE;
70 if (y + 1 < h && isWalkable(uint32_t(out.getCell(x, y + 1)))) mask |= kS;
71 if (x - 1 >= 0 && isWalkable(uint32_t(out.getCell(x - 1, y)))) mask |= kW;
72 if (y - 1 >= 0 && isWalkable(uint32_t(out.getCell(x, y - 1)))) mask |= kN;
73 if (x + 1 < w && y + 1 < h && isWalkable(uint32_t(out.getCell(x + 1, y + 1))))
74 mask |= kSE;
75 if (x - 1 >= 0 && y + 1 < h && isWalkable(uint32_t(out.getCell(x - 1, y + 1))))
76 mask |= kSW;
77 if (x - 1 >= 0 && y - 1 >= 0 && isWalkable(uint32_t(out.getCell(x - 1, y - 1))))
78 mask |= kNW;
79 if (x + 1 < w && y - 1 >= 0 && isWalkable(uint32_t(out.getCell(x + 1, y - 1))))
80 mask |= kNE;
81 out.setDetail(x, y, mask);
82 }
83 }
84}
85
87int floorVariant(int x, int y, const std::string &pattern, int variants, uint32_t salt) {
88 const int v = std::max(1, variants);
89 switch (pattern[0]) {
90 case 'c': // checker
91 return 1 + ((x + y) % v);
92 case 'p': // plank
93 return 1 + (y % v);
94 default: // brick / plain / cobble -> pseudo-random but stable per seed
95 return 1 + (int((x * 73856093u + y * 19349663u + salt) & 0x7FFFFFFFu) % v);
96 }
97}
98
99void carveRect(Grid2D &out, const Rect &r, uint32_t semantic) {
100 for (int y = r.y; y < r.y + r.h; ++y)
101 for (int x = r.x; x < r.x + r.w; ++x) out.setCell(x, y, int(semantic));
102}
103
104bool rectsOverlap(const Rect &a, const Rect &b, int pad) {
105 return a.x < b.x + b.w + pad && b.x < a.x + a.w + pad && a.y < b.y + b.h + pad &&
106 b.y < a.y + a.h + pad;
107}
108
109void carveCorridor(Grid2D &out, int ax, int ay, int bx, int by, int width,
110 const std::string &style, std::mt19937 &rng) {
111 if (style == "diagonal") {
112 int x = ax;
113 int y = ay;
114 while (x != bx || y != by) {
115 for (int oy = 0; oy < width; ++oy)
116 for (int ox = 0; ox < width; ++ox)
117 out.setCell(x + ox, y + oy, int(Semantic::Corridor));
118 if (x != bx && y != by && (rng() & 1u)) {
119 x += (bx > x) ? 1 : -1;
120 } else if (x != bx) {
121 x += (bx > x) ? 1 : -1;
122 } else {
123 y += (by > y) ? 1 : -1;
124 }
125 }
126 for (int oy = 0; oy < width; ++oy)
127 for (int ox = 0; ox < width; ++ox) out.setCell(x + ox, y + oy, int(Semantic::Corridor));
128 return;
129 }
130
131 // L-shaped: horizontal then vertical (random bend for variety on "l").
132 int cornerX = bx;
133 int cornerY = ay;
134 if (style == "straight") {
135 // Force a straight line when aligned, otherwise keep the L.
136 } else if (rng() & 1u) {
137 cornerX = ax;
138 cornerY = by;
139 }
140
141 const int x0 = std::min(ax, cornerX);
142 const int x1 = std::max(ax, cornerX);
143 for (int x = x0; x <= x1; ++x)
144 for (int oy = 0; oy < width; ++oy)
145 out.setCell(x, cornerY + oy, int(Semantic::Corridor));
146
147 const int y0 = std::min(cornerY, by);
148 const int y1 = std::max(cornerY, by);
149 for (int y = y0; y <= y1; ++y)
150 for (int ox = 0; ox < width; ++ox)
151 out.setCell(cornerX + ox, y, int(Semantic::Corridor));
152}
153
154bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
155 const int w = params.getWidth();
156 const int h = params.getHeight();
157 if (w < 9 || h < 9) {
158 error = "level.roguelike: size must be at least 9x9";
159 return false;
160 }
161
162 const uint32_t seed = params.getSeed();
163 const int roomCount = clampInt(params.getInt("roomCount", 9), 1, 256);
164 const int roomMin = clampInt(params.getInt("roomMin", 4), 2, w);
165 const int roomMax = clampInt(params.getInt("roomMax", 8), roomMin, w);
166 const int padding = clampInt(params.getInt("padding", 1), 0, 4);
167 const int spacing = clampInt(params.getInt("spacing", 2), 0, 8);
168 const int corridorW = clampInt(params.getInt("corridorWidth", 1), 1, 3);
169 const std::string style = params.getString("corridorStyle", "l");
170 const std::string pattern = params.getString("floorPattern", "brick");
171 const int variants = clampInt(params.getInt("floorVariants", 4), 1, kFloorMaxVariant);
172 const float decorDensity = std::clamp(params.getFloat("decorDensity", 0.05f), 0.f, 1.f);
173 const std::string decorSet = params.getString("decorSet", "mixed");
174 const bool doAutotile = params.getInt("autotile", 1) != 0;
175
176 out.resize(w, h);
177 out.fill(Semantic::Wall);
178
179 std::mt19937 rng(seed);
180
181 // 1) Place rooms on a coarse grid partition so they stay spread out, but
182 // allow jitter within each slot. Reject a slot if its room would overlap
183 // an earlier one (with spacing) or push outside the map.
184 const int cols = std::max(1, int(std::sqrt(double(roomCount))));
185 const int rows = std::max(1, (roomCount + cols - 1) / cols);
186 std::vector<Rect> rooms;
187 std::uniform_int_distribution<int> dim(roomMin, roomMax);
188 std::uniform_int_distribution<int> jx(0, 0), jy(0, 0);
189
190 for (int r = 0; r < rows; ++r) {
191 for (int c = 0; c < cols; ++c) {
192 if (int(rooms.size()) >= roomCount) break;
193 // Compute the slot's usable area (leave `padding` on each side).
194 const int sx = padding + int((long(c) * (w - 2 * padding)) / cols);
195 const int ex = padding + int((long(c + 1) * (w - 2 * padding)) / cols);
196 const int sy = padding + int((long(r) * (h - 2 * padding)) / rows);
197 const int ey = padding + int((long(r + 1) * (h - 2 * padding)) / rows);
198 const int slotW = std::max(roomMin, ex - sx);
199 const int slotH = std::max(roomMin, ey - sy);
200 if (slotW < roomMin || slotH < roomMin) continue;
201
202 // Try a few candidate sizes/offsets inside the slot before giving up.
203 for (int attempt = 0; attempt < 24; ++attempt) {
204 const int rw = std::min(dim(rng), slotW);
205 const int rh = std::min(dim(rng), slotH);
206 const int maxX = sx + (slotW - rw);
207 const int maxY = sy + (slotH - rh);
208 std::uniform_int_distribution<int> ox(sx, maxX);
209 std::uniform_int_distribution<int> oy(sy, maxY);
210 const Rect cand{ox(rng), oy(rng), rw, rh};
211
212 bool ok = true;
213 for (const Rect &other : rooms) {
214 if (rectsOverlap(cand, other, spacing)) { ok = false; break; }
215 }
216 if (ok) {
217 rooms.push_back(cand);
218 break;
219 }
220 }
221 }
222 }
223 if (rooms.empty()) {
224 // Last resort: one central room.
225 const int rw = std::max(roomMin, w / 3);
226 const int rh = std::max(roomMin, h / 3);
227 rooms.push_back({(w - rw) / 2, (h - rh) / 2, rw, rh});
228 }
229
230 // 2) Carve room floors.
231 for (const Rect &r : rooms) carveRect(out, r, Semantic::Floor);
232
233 // 3) Connect consecutive rooms.
234 for (size_t i = 1; i < rooms.size(); ++i) {
235 const int ax = rooms[i - 1].x + rooms[i - 1].w / 2;
236 const int ay = rooms[i - 1].y + rooms[i - 1].h / 2;
237 const int bx = rooms[i].x + rooms[i].w / 2;
238 const int by = rooms[i].y + rooms[i].h / 2;
239 carveCorridor(out, ax, ay, bx, by, corridorW, style, rng);
240 }
241
242 // 4) Wall autotile direction masks.
243 if (doAutotile) autotileWalls(out);
244
245 // 5) Floor pattern variants.
246 const uint32_t salt = seed * 2654435761u;
247 for (int y = 0; y < h; ++y) {
248 for (int x = 0; x < w; ++x) {
249 const uint32_t c = uint32_t(out.getCell(x, y));
250 if (c == Semantic::Floor || c == Semantic::Corridor) {
251 out.setDetail(x, y, floorVariant(x, y, pattern, variants, salt));
252 }
253 }
254 }
255
256 // 6) Scatter decor (ground tiles + placed props), avoiding the walkway
257 // through doorways (cells with a wall on two opposite sides) and the
258 // outer border so spawn/stairs stay reachable.
259 std::vector<std::pair<int, int>> floorCells;
260 floorCells.reserve(size_t(w * h));
261 for (int y = 1; y < h - 1; ++y)
262 for (int x = 1; x < w - 1; ++x)
263 if (isWalkable(uint32_t(out.getCell(x, y)))) floorCells.emplace_back(x, y);
264
265 std::vector<std::pair<int, int>> decorTiles;
266 if (!floorCells.empty() && decorDensity > 0.f) {
267 const size_t count = size_t(float(floorCells.size()) * decorDensity);
268 std::uniform_int_distribution<size_t> pick(0, floorCells.size() - 1);
269 std::uniform_int_distribution<int> kind(0, 2);
270 for (size_t i = 0; i < count; ++i) {
271 const auto [dx, dy] = floorCells[pick(rng)];
272 if (out.getDetail(dx, dy) >= kDecorBase) continue;
273 // Keep a clear 1-tile ring around spawn/stairs for reachability.
274 bool nearMarker = false;
275 for (int k = 0; k < out.getObjectCount(); ++k) {
276 if (int(out.getObjectX(k)) == dx && int(out.getObjectY(k)) == dy) {
277 nearMarker = true;
278 break;
279 }
280 }
281 if (nearMarker) continue;
282 out.setDetail(dx, dy, kDecorBase + kind(rng));
283 decorTiles.emplace_back(dx, dy);
284 }
285 }
286
287 // 7) Props (pillars / chests) from the chosen decor set.
288 auto propCount = [&](int base) {
289 return decorSet == "none" ? 0 : std::max(0, int(float(base) * 0.35f));
290 };
291 std::uniform_int_distribution<int> yroll(0, 2);
292 if (decorSet == "pillars" || decorSet == "mixed") {
293 const int n = propCount(roomCount);
294 for (int i = 0; i < n; ++i) {
295 if (rooms.empty()) break;
296 const Rect &r = rooms[size_t(i) % rooms.size()];
297 if (r.w < 4 || r.h < 4) continue;
298 const int px = r.x + r.w / 2 + (yroll(rng) - 1);
299 const int py = r.y + r.h / 2 + (yroll(rng) - 1);
300 out.addObject("pillar" + std::to_string(i), "pillar", float(px), float(py), 1.f, 1.f,
301 0);
302 }
303 }
304 if (decorSet == "treasure" || decorSet == "mixed") {
305 const int n = propCount(roomCount);
306 for (int i = 0; i < n; ++i) {
307 if (rooms.empty()) break;
308 const Rect &r = rooms[size_t(i) % rooms.size()];
309 const int px = r.x + r.w / 2 + (yroll(rng) - 1);
310 const int py = r.y + r.h / 2 + (yroll(rng) - 1);
311 out.addObject("chest" + std::to_string(i), "chest", float(px), float(py), 1.f, 1.f, 0);
312 }
313 }
314
315 // 8) Spawn + stairs on walkable cells (props from step 7 stay in place).
316 std::vector<std::pair<int, int>> walkable;
317 for (int y = 0; y < h; ++y)
318 for (int x = 0; x < w; ++x)
319 if (isWalkable(uint32_t(out.getCell(x, y)))) walkable.emplace_back(x, y);
320 if (!walkable.empty()) {
321 auto pick = [&](uint32_t s) {
322 return walkable[(seed * 1664525u + s * 1013904223u) % walkable.size()];
323 };
324 auto a = pick(1);
325 auto b = pick(7);
326 if (a == b && walkable.size() > 1) b = walkable[(size_t(seed) + 1) % walkable.size()];
327 out.addObjectAt("spawn", "spawn", float(a.first), float(a.second));
328 out.addObjectAt("stairs", "stairs", float(b.first), float(b.second));
329 }
330
331 // 9) Metadata.
332 out.setMeta("algorithm", "level.roguelike");
333 out.setMeta("seed", std::to_string(seed));
334 out.setMeta("rooms", std::to_string(rooms.size()));
335 out.setMeta("floorPattern", pattern);
336 out.setMeta("decorTiles", std::to_string(decorTiles.size()));
337 out.setMeta("corridorStyle", style);
338 return true;
339}
340
341} // namespace
342
344 registry.registerAlgorithm("level.roguelike", genRoguelike);
345}
346
348 autotileWalls(grid);
349 return grid.getWidth() > 0 && grid.getHeight() > 0;
350}
351
352uint32_t randomSeedValue() {
353 std::random_device rd;
354 const uint32_t v = rd();
355 return v == 0 ? 1u : v;
356}
357
358} // namespace eve::procgen
uint32_t seed
Tok kind
int y
Definition Grass.cpp:135
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
std::vector< Colorf > px
std::string error
uint32_t a
uint32_t b
uint32_t c
int width
int v
int spacing
uint32_t s
Definition Weather.cpp:28
uint32_t semantic
Definition WfcSimple.cpp:15
void registerAlgorithm(const std::string &id, GeneratorFn fn)
Intermediate 2D generation result. cells store semantic ids (see Semantic.h), not tile GIDs — convert...
Definition Grid2D.h:16
int getWidth() const
Definition Grid2D.cpp:20
int getHeight() const
Definition Grid2D.cpp:21
constexpr uint32_t Floor
Definition Semantic.h:12
constexpr uint32_t Wall
Definition Semantic.h:11
constexpr uint32_t Corridor
Definition Semantic.h:13
uint32_t randomSeedValue()
Produce a fresh seed suitable for regenerating a level (never 0).
void registerRoguelikeGenerator(GeneratorRegistry &registry)
Register the "level.roguelike" algorithm (idempotent).
bool autotileGridInPlace(Grid2D &grid)
Post-process any generated Grid2D: fill each wall cell's detail with an 8-bit neighbour mask describi...