//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)
- [`//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)
- [`//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)
- [`//multi <command_a> <command_b> .....`](#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 <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.
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 <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") .. "/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")

View File

@ -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

View File

@ -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 = "<replace_node> [<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.",
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 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")

View File

@ -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")

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