From 2720f62d0913f2a175c7c2fdc52f0b01ca301eba Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Sun, 3 May 2020 16:19:42 +0100 Subject: [PATCH] //maze: add path_length and path_width support ....now we jujst need to tackle //maze3d --- README.md | 13 ++- worldeditadditions/init.lua | 2 +- worldeditadditions/{maze.lua => maze2d.lua} | 104 ++++++++++-------- worldeditadditions_commands/commands/maze.lua | 42 +++---- worldeditadditions_commands/init.lua | 1 + worldeditadditions_commands/utils/strings.lua | 63 +++++++++++ 6 files changed, 159 insertions(+), 66 deletions(-) rename worldeditadditions/{maze.lua => maze2d.lua} (54%) create mode 100644 worldeditadditions_commands/utils/strings.lua diff --git a/README.md b/README.md index 93247a9..f7f0e0e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ If you can dream of it, it probably belongs here! - [`//hollowellipsoid `](#hollowellipsoid-rx-ry-rz-node_name) - [`//torus `](#torus-major_radius-minor_radius-node_name) - [`//hollowtorus `](#hollowtorus-major_radius-minor_radius-node_name) - - [`//maze []`](#maze-replace_node-seed) + - [`//maze [ [ []]]`](#maze-replace_node-seed) - [`//maze3d []`](#maze3d-replace_node-seed) - [`//multi .....`](#multi-command_a-command_b-command_c-) - [`//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 ``` -### `//maze []` +### `//maze [ [ []]]` 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. +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 stone 1234 +//maze dirt 56789 4 2 ``` ### `//maze3d []` diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index c9086a7..c7d06c4 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -12,5 +12,5 @@ dofile(minetest.get_modpath("worldeditadditions") .. "/floodfill.lua") dofile(minetest.get_modpath("worldeditadditions") .. "/overlay.lua") dofile(minetest.get_modpath("worldeditadditions") .. "/ellipsoid.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") diff --git a/worldeditadditions/maze.lua b/worldeditadditions/maze2d.lua similarity index 54% rename from worldeditadditions/maze.lua rename to worldeditadditions/maze2d.lua index 6b5cde0..3def732 100644 --- a/worldeditadditions/maze.lua +++ b/worldeditadditions/maze2d.lua @@ -2,10 +2,11 @@ -- Algorithm origin: https://starbeamrainbowlabs.com/blog/article.php?article=posts/070-Language-Review-Lua.html -- @module worldeditadditions.maze + ---------------------------------- -- function to print out the world ---------------------------------- -function printspace(space, w, h) +local function printspace(space, w, h) for y = 0, h - 1, 1 do local line = "" for x = 0, w - 1, 1 do @@ -15,7 +16,12 @@ function printspace(space, w, h) 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) math.randomseed(seed) -- seed the random number generator with the system clock @@ -29,7 +35,7 @@ function generate_maze(seed, width, height) world[y][x] = "#" end end - + -- do a random walk to create pathways local nodes = {} -- the nodes left that we haven't investigated local curnode = 1 -- the node we are currently operating on @@ -37,53 +43,63 @@ function generate_maze(seed, width, height) table.insert(nodes, { x = cx, y = cy }) world[cy][cx] = " " 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 - 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" 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" 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" 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" end local shift_attention = math.random(0, 9) - + if #directions > 0 then -- we still have somewhere that we can go + --print("This node is not a dead end yet.") local curdirnum = math.random(1, #directions) local curdir = string.sub(directions, curdirnum, curdirnum) if curdir == "u" then - world[cy - 1][cx] = " " - world[cy - 2][cx] = " " - cy = cy - 2 + for ix = cx,cx+(path_width-1) do + for iy = cy-path_length,cy do + world[iy][ix] = " " + end + end + cy = cy - path_length elseif curdir == "d" then - world[cy + 1][cx] = " " - world[cy + 2][cx] = " " - cy = cy + 2 + for ix = cx,cx+path_width-1 do + for iy = cy,cy+path_length+(path_width-1) do + world[iy][ix] = " " + end + end + cy = cy + path_length elseif curdir == "l" then - world[cy][cx - 1] = " " - world[cy][cx - 2] = " " - cx = cx - 2 + for iy = cy,cy+path_width-1 do + for ix = cx-path_length,cx do + world[iy][ix] = " " + end + end + cx = cx - path_length elseif curdir == "r" then - world[cy][cx + 1] = " " - world[cy][cx + 2] = " " - cx = cx + 2 + for iy = cy,cy+(path_width-1) do + for ix = cx,cx+path_length+(path_width-1) do + world[iy][ix] = " " + end + end + cx = cx + path_length + end + + if #directions > 1 then + table.insert(nodes, { x = cx, y = cy }) end - - table.insert(nodes, { x = cx, y = cy }) else - --print("The node at " .. curnode .. " is a dead end.") table.remove(nodes, curnode) - -- No need to do anything else here, since #directions == 0 will cause a teleport - see below end if #directions == 0 or shift_attention <= 1 then @@ -95,26 +111,27 @@ function generate_maze(seed, width, height) end end end - - return world + + end_time = os.clock() + return world --, (end_time - start_time) * 1000 end -- 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) -- 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 local extent = { - x = (pos2.x - pos1.x) + 1, - y = (pos2.y - pos1.y) + 1, - z = (pos2.z - pos1.z) + 1 + x = (pos2.x - pos1.x) + 1, path_length, path_width, + y = (pos2.y - pos1.y) + 1, -- not a dimension passed to the maze generator itself + z = (pos2.z - pos1.z) + 1, path_length, path_width } -- minetest.log("action", "extent: ("..extent.x..", "..extent.y..", "..extent.z..")") - if extent.x < 3 or extent.y < 3 or extent.z < 1 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") + if extent.x < 3 or extent.y < 1 or extent.z < 3 then + 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 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_target = minetest.get_content_id(target_node) - -- minetest.log("action", "pos1: " .. worldeditadditions.vector.tostring(pos1)) - -- minetest.log("action", "pos2: " .. worldeditadditions.vector.tostring(pos2)) + -- print("pos1: ", worldeditadditions.vector.tostring(pos1)) + -- 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) -- z y x is the preferred loop order, but that isn't really possible here - for z = pos2.z, pos1.z, - 1 do - for x = pos2.x, pos1.x, - 1 do - for y = pos2.y, pos1.y, - 1 do + for z = pos2.z, pos1.z, -1 do + for x = pos2.x, pos1.x, -1 do + for y = pos2.y, pos1.y, -1 do local maze_x = (x - pos1.x) -- - 1 local maze_z = (z - pos1.z) -- - 1 if maze_x < 0 then maze_x = 0 end diff --git a/worldeditadditions_commands/commands/maze.lua b/worldeditadditions_commands/commands/maze.lua index d55a5b8..9e3089a 100644 --- a/worldeditadditions_commands/commands/maze.lua +++ b/worldeditadditions_commands/commands/maze.lua @@ -8,43 +8,45 @@ local we_c = worldeditadditions_commands local function parse_params_maze(params_text) if not params_text then - return 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 + return nil, nil, nil, nil 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) - return replace_node, seed, has_seed + return replace_node, seed, path_length, path_width end minetest.register_chatcommand("/maze", { - params = " []", - 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.", + params = " [ [ []]]", + 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 }, 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 worldedit.player_notify(name, "Error: Invalid node name.") return false 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 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 worldedit.player_notify(name, replaced .. " nodes replaced in " .. time_taken .. "s") diff --git a/worldeditadditions_commands/init.lua b/worldeditadditions_commands/init.lua index ab0473f..deae62a 100644 --- a/worldeditadditions_commands/init.lua +++ b/worldeditadditions_commands/init.lua @@ -15,6 +15,7 @@ dofile(we_c.modpath.."/multi.lua") we_c.safe_region, we_c.check_region, we_c.reset_pending = 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/overlay.lua") dofile(we_c.modpath.."/commands/ellipsoid.lua") diff --git a/worldeditadditions_commands/utils/strings.lua b/worldeditadditions_commands/utils/strings.lua new file mode 100644 index 0000000..9a31e28 --- /dev/null +++ b/worldeditadditions_commands/utils/strings.lua @@ -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