feat(gen): extend init with shuffledDirs and Maze.generate
This commit is contained in:
1 parent
598dfaf79e
commit
2dbaa30813
2 files changed
+96
-1
No files matched your search
+6
-1
@@ -12,10 +12,15 @@ Gen.generators = {
|
|||||||
bsp = require("gen.bsp").generate,
|
bsp = require("gen.bsp").generate,
|
||||||
cellular = require("gen.cellular").generate,
|
cellular = require("gen.cellular").generate,
|
||||||
drunkard = require("gen.drunkard").generate,
|
drunkard = require("gen.drunkard").generate,
|
||||||
|
maze = require("gen.maze").generate,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Deterministic ordering of names for callers that want to cycle generators by
|
-- Deterministic ordering of names for callers that want to cycle generators by
|
||||||
-- depth without depending on hash iteration order.
|
-- depth without depending on hash iteration order. gen.maze is deliberately
|
||||||
|
-- absent: builder.lua's chosenGenerator indexes into this list by depth, and
|
||||||
|
-- every existing seed-pinned test downstream of it already assumes today's
|
||||||
|
-- four-way cycle. Reaching a maze floor is always explicit, via
|
||||||
|
-- options.generator = "maze" (see Gen.has/Gen.build below).
|
||||||
Gen.order = { "rooms", "bsp", "cellular", "drunkard" }
|
Gen.order = { "rooms", "bsp", "cellular", "drunkard" }
|
||||||
|
|
||||||
function Gen.has(name)
|
function Gen.has(name)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
-- Recursive-backtracker maze: a perfect spanning tree of one-wide corridors
|
||||||
|
-- over a grid of cells spaced two tiles apart, carved by an iterative
|
||||||
|
-- depth-first walk (an explicit stack, never Lua's own call stack, so
|
||||||
|
-- nothing here bounds how large a maze can grow). The result is a topology
|
||||||
|
-- none of the other generators produce - dense and twisty, with exactly one
|
||||||
|
-- path between any two cells - unlike the open rooms of gen.rooms/gen.bsp
|
||||||
|
-- or the organic blobs of gen.cellular/gen.drunkard. A braiding pass
|
||||||
|
-- afterward knocks down a fixed fraction of the walls still separating two
|
||||||
|
-- already-carved cells, so a fraction of what would otherwise be dead ends
|
||||||
|
-- gain a second way out instead. Selectable like any other entry in
|
||||||
|
-- gen.init's registry, but deliberately left out of Gen.order: nothing
|
||||||
|
-- downstream (builder.lua's rng-ordering discipline, every seed-pinned
|
||||||
|
-- depth test) currently expects the auto-cycle to ever hand it a floor, and
|
||||||
|
-- inserting a fifth entry there would reshuffle which of the existing four
|
||||||
|
-- generators every depth from five onward gets, and with it every rng draw
|
||||||
|
-- a builder places afterward. A maze floor is instead reached deliberately,
|
||||||
|
-- via options.generator = "maze".
|
||||||
|
|
||||||
|
local Map = require("map")
|
||||||
|
local Tiles = require("tiles")
|
||||||
|
|
||||||
|
local Maze = {}
|
||||||
|
|
||||||
|
Maze.BRAID_NUMERATOR = 1
|
||||||
|
Maze.BRAID_DENOMINATOR = 5
|
||||||
|
|
||||||
|
local DIRS = { { 2, 0 }, { -2, 0 }, { 0, 2 }, { 0, -2 } }
|
||||||
|
|
||||||
|
local function shuffledDirs(rng)
|
||||||
|
local dirs = { DIRS[1], DIRS[2], DIRS[3], DIRS[4] }
|
||||||
|
for i = 4, 2, -1 do
|
||||||
|
local j = rng:range(1, i)
|
||||||
|
dirs[i], dirs[j] = dirs[j], dirs[i]
|
||||||
|
end
|
||||||
|
return dirs
|
||||||
|
end
|
||||||
|
|
||||||
|
function Maze.generate(width, height, rng, options)
|
||||||
|
options = options or {}
|
||||||
|
local braidNumerator = options.braidNumerator or Maze.BRAID_NUMERATOR
|
||||||
|
local braidDenominator = options.braidDenominator or Maze.BRAID_DENOMINATOR
|
||||||
|
local map = Map.new(width, height)
|
||||||
|
|
||||||
|
local function key(x, y) return y * width + x end
|
||||||
|
local visited = {}
|
||||||
|
|
||||||
|
local startX, startY = 2, 2
|
||||||
|
visited[key(startX, startY)] = true
|
||||||
|
map:set(startX, startY, Tiles.FLOOR)
|
||||||
|
local stack = { { startX, startY } }
|
||||||
|
|
||||||
|
while #stack > 0 do
|
||||||
|
local top = stack[#stack]
|
||||||
|
local cx, cy = top[1], top[2]
|
||||||
|
local advanced = false
|
||||||
|
for _, dir in ipairs(shuffledDirs(rng)) do
|
||||||
|
local nx, ny = cx + dir[1], cy + dir[2]
|
||||||
|
if nx >= 2 and ny >= 2 and nx <= width - 1 and ny <= height - 1 and not visited[key(nx, ny)] then
|
||||||
|
local wx, wy = cx + dir[1] // 2, cy + dir[2] // 2
|
||||||
|
map:set(wx, wy, Tiles.FLOOR)
|
||||||
|
map:set(nx, ny, Tiles.FLOOR)
|
||||||
|
visited[key(nx, ny)] = true
|
||||||
|
stack[#stack + 1] = { nx, ny }
|
||||||
|
advanced = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not advanced then stack[#stack] = nil end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Braiding: a wall cell with floor on exactly one opposing pair of sides
|
||||||
|
-- (east-west or north-south) sits between two cells the backtracker above
|
||||||
|
-- already connected some longer way around; knocking a fixed fraction of
|
||||||
|
-- these down trims some, but never all, of the maze's dead ends.
|
||||||
|
for y = 3, height - 2 do
|
||||||
|
for x = 3, width - 2 do
|
||||||
|
if map:get(x, y) == Tiles.WALL then
|
||||||
|
local linksEastWest = map:get(x - 1, y) == Tiles.FLOOR and map:get(x + 1, y) == Tiles.FLOOR
|
||||||
|
local linksNorthSouth = map:get(x, y - 1) == Tiles.FLOOR and map:get(x, y + 1) == Tiles.FLOOR
|
||||||
|
if (linksEastWest or linksNorthSouth) and rng:chance(braidNumerator, braidDenominator) then
|
||||||
|
map:set(x, y, Tiles.FLOOR)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return map
|
||||||
|
end
|
||||||
|
|
||||||
|
return Maze
|
||||||
Reference in new issue
Block a user