//maze: add path_length and path_width support

....now we jujst need to tackle //maze3d
This commit is contained in:
Starbeamrainbowlabs 2020-05-03 16:19:42 +01:00
parent d35a55ea9e
commit 2720f62d09
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
6 changed files with 159 additions and 66 deletions

View file

@ -14,7 +14,7 @@ If you can dream of it, it probably belongs here!
- [`//hollowellipsoid <rx> <ry> <rz> <node_name>`](#hollowellipsoid-rx-ry-rz-node_name) - [`//hollowellipsoid <rx> <ry> <rz> <node_name>`](#hollowellipsoid-rx-ry-rz-node_name)
- [`//torus <major_radius> <minor_radius> <node_name>`](#torus-major_radius-minor_radius-node_name) - [`//torus <major_radius> <minor_radius> <node_name>`](#torus-major_radius-minor_radius-node_name)
- [`//hollowtorus <major_radius> <minor_radius> <node_name>`](#hollowtorus-major_radius-minor_radius-node_name) - [`//hollowtorus <major_radius> <minor_radius> <node_name>`](#hollowtorus-major_radius-minor_radius-node_name)
- [`//maze <replace_node> [<seed>]`](#maze-replace_node-seed) - [`//maze <replace_node> [<path_length> [<path_width> [<seed>]]]`](#maze-replace_node-seed)
- [`//maze3d <replace_node> [<seed>]`](#maze3d-replace_node-seed) - [`//maze3d <replace_node> [<seed>]`](#maze3d-replace_node-seed)
- [`//multi <command_a> <command_b> .....`](#multi-command_a-command_b-command_c-) - [`//multi <command_a> <command_b> .....`](#multi-command_a-command_b-command_c-)
- [`//yy`](#yy) - [`//yy`](#yy)
@ -75,14 +75,23 @@ Creates a hollow torus at position 1 with the radius major and minor radii. Work
//hollowtorus 21 11 stone //hollowtorus 21 11 stone
``` ```
### `//maze <replace_node> [<seed>]` ### `//maze <replace_node> [<path_length> [<path_width> [<seed>]]]`
Generates a maze using replace_node as the walls and air as the paths. Uses [an algorithm of my own devising](https://starbeamrainbowlabs.com/blog/article.php?article=posts/070-Language-Review-Lua.html). It is guaranteed that you can get from every point to every other point in generated mazes, and there are no loops. Generates a maze using replace_node as the walls and air as the paths. Uses [an algorithm of my own devising](https://starbeamrainbowlabs.com/blog/article.php?article=posts/070-Language-Review-Lua.html). It is guaranteed that you can get from every point to every other point in generated mazes, and there are no loops.
Requires the currently selected area to be at least 3x3x3. Requires the currently selected area to be at least 3x3x3.
The optional `path_length` and `path_width` arguments require additional explanation. When generating a maze, a multi-headed random walk is performed. When the generator decides to move forwards from a point, it does so `path_length` nodes at a time.
`path_width` is easier to explain. It's basically the number of nodes wide the path generated is.
Note that `path_width` must always be at least 1 less than the `path_height` in order to operate normally.
The last example below shows how to set the path length and width:
``` ```
//maze ice //maze ice
//maze stone 1234 //maze stone 1234
//maze dirt 56789 4 2
``` ```
### `//maze3d <replace_node> [<seed>]` ### `//maze3d <replace_node> [<seed>]`

View file

@ -12,5 +12,5 @@ dofile(minetest.get_modpath("worldeditadditions") .. "/floodfill.lua")
dofile(minetest.get_modpath("worldeditadditions") .. "/overlay.lua") dofile(minetest.get_modpath("worldeditadditions") .. "/overlay.lua")
dofile(minetest.get_modpath("worldeditadditions") .. "/ellipsoid.lua") dofile(minetest.get_modpath("worldeditadditions") .. "/ellipsoid.lua")
dofile(minetest.get_modpath("worldeditadditions") .. "/torus.lua") dofile(minetest.get_modpath("worldeditadditions") .. "/torus.lua")
dofile(minetest.get_modpath("worldeditadditions") .. "/maze.lua") dofile(minetest.get_modpath("worldeditadditions") .. "/maze2d.lua")
dofile(minetest.get_modpath("worldeditadditions") .. "/maze3d.lua") dofile(minetest.get_modpath("worldeditadditions") .. "/maze3d.lua")

View file

@ -2,10 +2,11 @@
-- Algorithm origin: https://starbeamrainbowlabs.com/blog/article.php?article=posts/070-Language-Review-Lua.html -- Algorithm origin: https://starbeamrainbowlabs.com/blog/article.php?article=posts/070-Language-Review-Lua.html
-- @module worldeditadditions.maze -- @module worldeditadditions.maze
---------------------------------- ----------------------------------
-- function to print out the world -- function to print out the world
---------------------------------- ----------------------------------
function printspace(space, w, h) local function printspace(space, w, h)
for y = 0, h - 1, 1 do for y = 0, h - 1, 1 do
local line = "" local line = ""
for x = 0, w - 1, 1 do for x = 0, w - 1, 1 do
@ -15,7 +16,12 @@ function printspace(space, w, h)
end end
end end
function generate_maze(seed, width, height) local function generate_maze(seed, width, height, path_length, path_width)
start_time = os.clock()
if not path_length then path_length = 2 end
if not path_width then path_width = 1 end
-- minetest.log("action", "width: "..width..", height: "..height) -- minetest.log("action", "width: "..width..", height: "..height)
math.randomseed(seed) -- seed the random number generator with the system clock math.randomseed(seed) -- seed the random number generator with the system clock
@ -37,21 +43,18 @@ function generate_maze(seed, width, height)
table.insert(nodes, { x = cx, y = cy }) table.insert(nodes, { x = cx, y = cy })
world[cy][cx] = " " world[cy][cx] = " "
while #nodes > 0 do while #nodes > 0 do
-- io.write("Nodes left: " .. curnode .. "\r")
--print("Nodes left: " .. #nodes)
--print("Currently at (" .. cx .. ", " .. cy .. ")")
local directions = "" -- the different directions we can move local directions = "" -- the different directions we can move
if cy - 2 > 0 and world[cy - 2][cx] == "#" then if cy - path_length > 0 and world[cy - path_length][cx] == "#" then
directions = directions .. "u" directions = directions .. "u"
end end
if cy + 2 < height and world[cy + 2][cx] == "#" then if cy + path_length < height-path_width and world[cy + path_length][cx] == "#" then
directions = directions .. "d" directions = directions .. "d"
end end
if cx - 2 > 0 and world[cy][cx - 2] == "#" then if cx - path_length > 0 and world[cy][cx - path_length] == "#" then
directions = directions .. "l" directions = directions .. "l"
end end
if cx + 2 < width and world[cy][cx + 2] == "#" then if cx + path_length < width-path_width and world[cy][cx + path_length] == "#" then
directions = directions .. "r" directions = directions .. "r"
end end
@ -59,31 +62,44 @@ function generate_maze(seed, width, height)
if #directions > 0 then if #directions > 0 then
-- we still have somewhere that we can go -- we still have somewhere that we can go
--print("This node is not a dead end yet.")
local curdirnum = math.random(1, #directions) local curdirnum = math.random(1, #directions)
local curdir = string.sub(directions, curdirnum, curdirnum) local curdir = string.sub(directions, curdirnum, curdirnum)
if curdir == "u" then if curdir == "u" then
world[cy - 1][cx] = " " for ix = cx,cx+(path_width-1) do
world[cy - 2][cx] = " " for iy = cy-path_length,cy do
cy = cy - 2 world[iy][ix] = " "
end
end
cy = cy - path_length
elseif curdir == "d" then elseif curdir == "d" then
world[cy + 1][cx] = " " for ix = cx,cx+path_width-1 do
world[cy + 2][cx] = " " for iy = cy,cy+path_length+(path_width-1) do
cy = cy + 2 world[iy][ix] = " "
end
end
cy = cy + path_length
elseif curdir == "l" then elseif curdir == "l" then
world[cy][cx - 1] = " " for iy = cy,cy+path_width-1 do
world[cy][cx - 2] = " " for ix = cx-path_length,cx do
cx = cx - 2 world[iy][ix] = " "
end
end
cx = cx - path_length
elseif curdir == "r" then elseif curdir == "r" then
world[cy][cx + 1] = " " for iy = cy,cy+(path_width-1) do
world[cy][cx + 2] = " " for ix = cx,cx+path_length+(path_width-1) do
cx = cx + 2 world[iy][ix] = " "
end
end
cx = cx + path_length
end end
if #directions > 1 then
table.insert(nodes, { x = cx, y = cy }) table.insert(nodes, { x = cx, y = cy })
end
else else
--print("The node at " .. curnode .. " is a dead end.")
table.remove(nodes, curnode) table.remove(nodes, curnode)
-- No need to do anything else here, since #directions == 0 will cause a teleport - see below
end end
if #directions == 0 or shift_attention <= 1 then if #directions == 0 or shift_attention <= 1 then
@ -96,25 +112,26 @@ function generate_maze(seed, width, height)
end end
end end
return world end_time = os.clock()
return world --, (end_time - start_time) * 1000
end end
-- local world = maze(os.time(), width, height) -- local world = maze(os.time(), width, height)
function worldedit.maze(pos1, pos2, target_node, seed) function worldeditadditions.maze2d(pos1, pos2, target_node, seed, path_length, path_width)
pos1, pos2 = worldedit.sort_pos(pos1, pos2) pos1, pos2 = worldedit.sort_pos(pos1, pos2)
-- pos2 will always have the highest co-ordinates now -- pos2 will always have the highest co-ordinates now
-- getExtent() returns the number of nodes in the VoxelArea, which might be larger than we actually asked for -- getExtent() returns the number of nodes in the VoxelArea, which might be larger than we actually asked for
local extent = { local extent = {
x = (pos2.x - pos1.x) + 1, x = (pos2.x - pos1.x) + 1, path_length, path_width,
y = (pos2.y - pos1.y) + 1, y = (pos2.y - pos1.y) + 1, -- not a dimension passed to the maze generator itself
z = (pos2.z - pos1.z) + 1 z = (pos2.z - pos1.z) + 1, path_length, path_width
} }
-- minetest.log("action", "extent: ("..extent.x..", "..extent.y..", "..extent.z..")") -- minetest.log("action", "extent: ("..extent.x..", "..extent.y..", "..extent.z..")")
if extent.x < 3 or extent.y < 3 or extent.z < 1 then if extent.x < 3 or extent.y < 1 or extent.z < 3 then
minetest.log("info", "[worldeditadditions/maze] error: either x, y of the extent were less than 3, or z of the extent was less than 1") minetest.log("error", "[worldeditadditions/maze] error: either x, y of the extent were less than 3, or z of the extent was less than 1")
return 0 return 0
end end
@ -125,20 +142,21 @@ function worldedit.maze(pos1, pos2, target_node, seed)
local node_id_air = minetest.get_content_id("air") local node_id_air = minetest.get_content_id("air")
local node_id_target = minetest.get_content_id(target_node) local node_id_target = minetest.get_content_id(target_node)
-- minetest.log("action", "pos1: " .. worldeditadditions.vector.tostring(pos1)) -- print("pos1: ", worldeditadditions.vector.tostring(pos1))
-- minetest.log("action", "pos2: " .. worldeditadditions.vector.tostring(pos2)) -- print("pos2: ", worldeditadditions.vector.tostring(pos2))
minetest.log("action", "Generating "..extent.x.."x"..extent.z.." maze (depth "..extent.z..") from pos1 " .. worldeditadditions.vector.tostring(pos1).." to pos2 "..worldeditadditions.vector.tostring(pos2)) -- minetest.log("action", "Generating "..extent.x.."x"..extent.z.." maze (depth "..extent.z..") from pos1 " .. worldeditadditions.vector.tostring(pos1).." to pos2 "..worldeditadditions.vector.tostring(pos2))
-- print("path_width: "..path_width..", path_length: "..path_length)
local maze = generate_maze(seed, extent.z, extent.x) -- x & need to be the opposite way around to how we index it local maze = generate_maze(seed, extent.z, extent.x, path_length, path_width) -- x & need to be the opposite way around to how we index it
-- printspace(maze, extent.z, extent.x) -- printspace(maze, extent.z, extent.x)
-- z y x is the preferred loop order, but that isn't really possible here -- z y x is the preferred loop order, but that isn't really possible here
for z = pos2.z, pos1.z, - 1 do for z = pos2.z, pos1.z, -1 do
for x = pos2.x, pos1.x, - 1 do for x = pos2.x, pos1.x, -1 do
for y = pos2.y, pos1.y, - 1 do for y = pos2.y, pos1.y, -1 do
local maze_x = (x - pos1.x) -- - 1 local maze_x = (x - pos1.x) -- - 1
local maze_z = (z - pos1.z) -- - 1 local maze_z = (z - pos1.z) -- - 1
if maze_x < 0 then maze_x = 0 end if maze_x < 0 then maze_x = 0 end

View file

@ -8,43 +8,45 @@ local we_c = worldeditadditions_commands
local function parse_params_maze(params_text) local function parse_params_maze(params_text)
if not params_text then if not params_text then
return nil, nil, nil return nil, nil, nil, nil
end
local found, _, replace_node, seed_text = params_text:find("([a-z:_\\-]+)%s+([0-9]+)")
local has_seed = true
if found == nil then
has_seed = false
replace_node = params_text
end end
local seed = tonumber(seed_text) local parts = we_c.split(params_text, "%s+", false)
local replace_node = parts[1]
local seed = os.time()
local path_length = 2
local path_width = 1
if #parts >= 2 then
path_length = tonumber(parts[2])
end
if #parts >= 3 then
path_width = tonumber(parts[3])
end
if #parts >= 4 then
seed = tonumber(parts[4])
end
replace_node = worldedit.normalize_nodename(replace_node) replace_node = worldedit.normalize_nodename(replace_node)
return replace_node, seed, has_seed return replace_node, seed, path_length, path_width
end end
minetest.register_chatcommand("/maze", { minetest.register_chatcommand("/maze", {
params = "<replace_node> [<seed>]", params = "<replace_node> [<path_length> [<path_width> [<seed>]]]",
description = "Generates a maze covering the currently selected area (must be at least 3x3 on the x,z axes) with replace_node as the walls. Optionally takes a (integer) seed.", description = "Generates a maze covering the currently selected area (must be at least 3x3 on the x,z axes) with replace_node as the walls. Optionally takes a (integer) seed and the path length and width (see the documentation in the worldeditadditions README for more information).",
privs = { worldedit = true }, privs = { worldedit = true },
func = we_c.safe_region(function(name, params_text) func = we_c.safe_region(function(name, params_text)
local replace_node, seed, has_seed = parse_params_maze(params_text) local replace_node, seed, path_length, path_width = parse_params_maze(params_text)
if not replace_node then if not replace_node then
worldedit.player_notify(name, "Error: Invalid node name.") worldedit.player_notify(name, "Error: Invalid node name.")
return false return false
end end
if not seed and has_seed then
worldedit.player_notify(name, "Error: Invalid seed.")
return false
end
if not seed then seed = os.time() end
local start_time = os.clock() local start_time = os.clock()
local replaced = worldedit.maze(worldedit.pos1[name], worldedit.pos2[name], replace_node, seed) local replaced = worldeditadditions.maze2d(worldedit.pos1[name], worldedit.pos2[name], replace_node, seed, path_length, path_width)
local time_taken = os.clock() - start_time local time_taken = os.clock() - start_time
worldedit.player_notify(name, replaced .. " nodes replaced in " .. time_taken .. "s") worldedit.player_notify(name, replaced .. " nodes replaced in " .. time_taken .. "s")

View file

@ -15,6 +15,7 @@ dofile(we_c.modpath.."/multi.lua")
we_c.safe_region, we_c.check_region, we_c.reset_pending we_c.safe_region, we_c.check_region, we_c.reset_pending
= dofile(we_c.modpath.."/safe.lua") = dofile(we_c.modpath.."/safe.lua")
dofile(we_c.modpath.."/utils/strings.lua")
dofile(we_c.modpath.."/commands/floodfill.lua") dofile(we_c.modpath.."/commands/floodfill.lua")
dofile(we_c.modpath.."/commands/overlay.lua") dofile(we_c.modpath.."/commands/overlay.lua")
dofile(we_c.modpath.."/commands/ellipsoid.lua") dofile(we_c.modpath.."/commands/ellipsoid.lua")

View file

@ -0,0 +1,63 @@
-- Licence: GPLv2 (MPL-2.0 is compatible, so we can use this here)
-- Source: https://stackoverflow.com/a/43582076/1460422
local we_c = worldeditadditions_commands
-- gsplit: iterate over substrings in a string separated by a pattern
--
-- Parameters:
-- text (string) - the string to iterate over
-- pattern (string) - the separator pattern
-- plain (boolean) - if true (or truthy), pattern is interpreted as a plain
-- string, not a Lua pattern
--
-- Returns: iterator
--
-- Usage:
-- for substr in gsplit(text, pattern, plain) do
-- doSomething(substr)
-- end
function we_c.gsplit(text, pattern, plain)
local splitStart, length = 1, #text
return function ()
if splitStart then
local sepStart, sepEnd = string.find(text, pattern, splitStart, plain)
local ret
if not sepStart then
ret = string.sub(text, splitStart)
splitStart = nil
elseif sepEnd < sepStart then
-- Empty separator!
ret = string.sub(text, splitStart, sepStart)
if sepStart < length then
splitStart = sepStart + 1
else
splitStart = nil
end
else
ret = sepStart > splitStart and string.sub(text, splitStart, sepStart - 1) or ''
splitStart = sepEnd + 1
end
return ret
end
end
end
-- split: split a string into substrings separated by a pattern.
--
-- Parameters:
-- text (string) - the string to iterate over
-- pattern (string) - the separator pattern
-- plain (boolean) - if true (or truthy), pattern is interpreted as a plain
-- string, not a Lua pattern
--
-- Returns: table (a sequence table containing the substrings)
function we_c.split(text, pattern, plain)
local ret = {}
for match in we_c.gsplit(text, pattern, plain) do
table.insert(ret, match)
end
return ret
end