master
rnd1 2016-03-21 13:55:24 +01:00
commit 5d9444dd13
4 changed files with 355 additions and 0 deletions

1
depends.txt Normal file
View File

@ -0,0 +1 @@
default

2
init.lua Normal file
View File

@ -0,0 +1,2 @@
dofile(minetest.get_modpath("basic_layout").."/mazes.lua")
dofile(minetest.get_modpath("basic_layout").."/roads.lua")

203
mazes.lua Normal file
View File

@ -0,0 +1,203 @@
-- Copyright 2016 rnd
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
----------------------------------------------------------------------------------------------------------
-- maze generation
-- http://en.wikipedia.org/wiki/Maze_generation_algorithm#Depth-first_search, recursive backtracker
-- representation of node coordinate (row,coloumn)=(i,j) -> (i-1)*n+j, i=1..n, j=1...m
-- representation of walls: below node k --> k, left of node k --> k+m.n
-- good overview of maze generation algorithms using javascript/html5
-- http://www.jamisbuck.org/presentations/rubyconf2011/index.html#recursive-backtracker
-- helper functions
--stack in lua
local stack={};
function stack.push(s,e) s[#s+1]=e end
function stack.pop(s) local r = s[#s];s[#s]=nil;return r end
--function table2string(s) local r = ""; for i,v in pairs(s) do r = r.. " ["..i.."]=".. v ; end return r end
function maze_deep_first_search(m,n,start,seed) -- returns a table of strings representing line renders
local steps,maxsteps; steps= 0; maxsteps = 999999;
local maze = {}
maze.m = m; maze.n = n;
maze.unvisited = {};maze.stack = {}; maze.walls = {};
maze.free = maze.m*maze.n;
local i,j,k
local nb,wall -- unvisited neighbbors, walls
--init structures
for i=1,maze.m do
for j =1,maze.n do
k=(i-1)*maze.n+j;maze.unvisited[k]=true -- initially all cells unvisited
maze.walls[k]=true;maze.walls[k+maze.n*maze.m]=true; -- walls are there
end
end
math.randomseed(seed)
maze.current = start
maze.unvisited [ maze.current ] = false;
maze.free = maze.free-1; maze.stack[1+#maze.stack] = maze.current
while maze.free>0 and steps<maxsteps do -- main loop
steps=steps+1
-- check current node neighbors
k=maze.current
j = k % maze.n;i=math.ceil(k/maze.n); -- get coords back from index
if j==0 then j = maze.n end
--print("coords current node "..k .. " = " .. i .. " " ..j)
nb={};wall={}-- check unvisited neighbors & wall removals
if i>1 then -- down
k=(i-2)*maze.n+j; if maze.unvisited[k] then wall[#wall+1]=k+maze.n;nb[#nb+1]=k end
end
if i<maze.m then -- up
k=(i)*maze.n+j; if maze.unvisited[k] then wall[#wall+1]=k;nb[#nb+1]=k end
end
if j<maze.n then --right
k=(i-1)*maze.n+j+1; if maze.unvisited[k] then wall[#wall+1]=k+maze.n*maze.m; nb[#nb+1]=k end
end
if j>1 then --left
k=(i-1)*maze.n+j-1; if maze.unvisited[k] then wall[#wall+1]=k+1+maze.n*maze.m;nb[#nb+1]=k end
end
--print(" unvisited neighbors " .. table2string(nb))
if (#nb)>0 then -- if unvisited neighbors, choose random one as next current node
stack.push(maze.stack,maze.current) -- remember previous current node
k=math.random(#nb); -- pick random unvisited neighbor
maze.walls[wall[k]]=false; -- remove wall
--print(" removed wall ".. wall[k])
k=nb[k];
maze.current = k; -- new current cell
maze.unvisited[k]=false; maze.free = maze.free-1 -- one less unvisited
--print("new explore " .. k);
elseif (#maze.stack)~=0 then -- no unvisited neighbors, backtrack using stack
maze.current = stack.pop(maze.stack)
--print("backtrack to "..maze.current)
else -- even stack is empty, just pick random unvisited cell
k = math.random(maze.free); j=1;
for i =1,maze.m*maze.n do
if maze.unvisited[i] then
if j==k then k=i; break end -- pick node
j=j+1
end
end
--print(" stack empty, random pick " ..k)
maze.current=k;maze.unvisited[k]=false; maze.free = maze.free -1;
end
end -- of do
-- render maze with chars, row by row
maze.ret = {};
local hor;local vert;
local wall = "1"
for i=1,maze.m do
hor="";vert="";
k= (i-1)*maze.n;
-- horizontal
for j = 1, maze.n do
k=k+1;
-- if maze.walls[k+maze.n*maze.m] then vert=vert.."X." else vert=vert.. "0." end
-- if maze.walls[k] then hor=hor.."XX" else hor=hor.."X0" end
if maze.walls[k+maze.n*maze.m] then vert=vert..wall.."0" else vert=vert.. "00" end
if maze.walls[k] then hor=hor..wall..wall else hor=hor..wall.."0" end
end
maze.ret[1+#maze.ret]=hor..wall;maze.ret[1+#maze.ret]=vert..wall;
end
maze.ret[1+#maze.ret] = string.rep(wall,2*maze.n+1)
return maze.ret
end
-- RUN PROGRAM
-- local maze=maze_deep_first_search(10,30,1,2015)
--for _,v in pairs(maze) do print(v) end
minetest.register_chatcommand("maze", {
description = "/maze (optional m,n,start,seed) generates maze at players position, directed towards positive x,z axix",
privs = {kick = true},
func = function(name, param)
local m,n,start, seed
if param == "" then m=10;n=10;start=1;seed=1 else
local words = {}; local word
for word in param:gmatch("%w+") do words[1+#words]= word end
if words[1]~=nil then m = tonumber(words[1]) else m = 10 end
if words[2]~=nil then n = tonumber(words[2]) else n = 10 end
if words[3]~=nil then start = tonumber(words[3]) else start = 1 end
if words[4]~=nil then seed = tonumber(words[4]) else seed = 1 end
end
if m*n>1000000 then minetest.chat_send_player(name, "limit maze size to 1000000 cells") return end
local player = minetest.env:get_player_by_name(name); if player==nil then return end
local pos = player:getpos();local p
local maze=maze_deep_first_search(m,n,start,seed) -- m,n,start,seed
local i,j,k;local p = {x=pos.x,y=pos.y,z=pos.z};
--render
for i,v in pairs(maze) do
p.x = pos.x+i
for k = 1,string.len(v) do
p.z=pos.z+k
if string.sub(v,k,k)=="1" then
minetest.set_node(p,{name="basic_layout:stone_maze"})
else minetest.set_node(p,{name="air"})
end
end
end
end,
})
minetest.register_node("basic_layout:glass_maze", {
description = "maze_glass",
drawtype = "glasslike_framed_optional",
tiles = {"maze_glass.png"},
inventory_image = minetest.inventorycube("maze_glass.png"),
paramtype = "light",
sunlight_propagates = true,
is_ground_content = false,
groups = {immortal = 1,disable_jump=1},
sounds = default.node_sound_glass_defaults(),
})
minetest.register_node("basic_layout:stone_maze", {
description = "maze_wall",
tiles = {"default_brick.png"},
is_ground_content = true,
groups = {immortal = 1,disable_jump=1},
legacy_mineral = true,
sounds = default.node_sound_stone_defaults(),
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
local name = player:get_player_name(); if name == nil then return end
if minetest.is_protected(pos, name) then
return
end
local player_inv = player:get_inventory()
if player_inv:room_for_item("main", {"basic_layout:stone_maze"}) then
player_inv:add_item("main", "basic_layout:stone_maze")
minetest.remove_node(pos) -- remove bones
end
end
})

149
roads.lua Normal file
View File

@ -0,0 +1,149 @@
-- ROAD LAYOUT GENERATOR by rnd
-- Copyright 2016 rnd
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
local layout = {};
layout.cells = {}; -- list of cells, each cell will be {x1,z1,x2,z2,mark}, mark = true means cell is to be removed
layout.roads = {}; -- road list, each road {x1,z1,x2,z2}
layout.pos = {x=0,y=0,z=0}; -- where to put it
-- SETTINGS
layout.height = 200; -- dimension of outer box
layout.width = 200;
layout.minheight = 5; -- smallest dimension of cell
layout.minheightupper = 100;
layout.minwidth = 5;
layout.minwidthupper = 100;
layout.excludeprobability = 0.8;
-- END OF SETTINGS
layout.subdivide = function ( cell_index )
local cell = layout.cells[cell_index];
local x1 = cell[1]; local z1 = cell[2]; local x2 = cell[3]; local z2 = cell[4];
local nz, nx,vertically,horizontaly;
vertically=false;horizontaly=false;
local minheight = layout.minheight+(layout.minheightupper-layout.minheight)*math.random();
local minwidth = layout.minwidth+(layout.minwidthupper-layout.minwidth)*math.random();
if z2-z1<layout.height*0.25 and z2-z1>0.1*layout.height then -- chunks of certains size get discarded
if math.random()<layout.excludeprobability then return false end -- discard
end
if z2-z1>2*minheight then -- can subdivide vertically
--z1+layout.minheight,z2-layout.minheight
nz = z1+minheight+math.random()*(z2-z1-2*minheight);
vertically=true;
table.insert(layout.roads,{x1,nz,x2,nz});
end
if x2-x1>2*minwidth then -- can subdivide horizontaly
nx = x1+minwidth+math.random()*(x2-x1-2*minwidth);
horizontaly=true;
table.insert(layout.roads,{nx,z1,nx,z2});
end
if horizontaly then -- add new cells
if vertically then -- horizontally and vertically
table.insert(layout.cells,{x1,z1,nx,nz,false});
table.insert(layout.cells,{nx,z1,x2,nz,false});
table.insert(layout.cells,{x1,nz,nx,z2,false});
table.insert(layout.cells,{nx,nz,x2,z2,false});
else -- horizontally
table.insert(layout.cells,{x1,z1,nx,z2,false});
table.insert(layout.cells,{nx,z1,x2,z2,false});
end
else
if vertically then -- vertically
table.insert(layout.cells,{x1,z1,x2,nz,false});
table.insert(layout.cells,{x1,nz,x2,z2,false});
else
return false; -- no subdivide possible
end
end
layout.cells[cell_index][5] = true; -- mark cell for removal
return true;
end
layout.iterate = function ()
local not_finished = false;
for i = 1, #layout.cells do -- try subdivide all cells in cell list
if layout.subdivide(i) then not_finished=true end -- at least one remains to be subdivided
end
-- remove cells scheduled for removal
for i = #layout.cells, 1, -1 do -- remove from back to start to prevent idx messup after removal
if layout.cells[i][5] then table.remove(layout.cells,i) end
end
return not_finished; -- if false then its finished
end
layout.generate = function ()
layout.cells = {{0,0,layout.width,layout.height}};
layout.roads = {};
local not_finished = true;
while not_finished==true do
not_finished=layout.iterate();
end
end
-- JUST FOR TEST RENDERING IN THE WORLD
minetest.register_chatcommand("road", {
description = "/road seed, will generate and render layout of roads starting at player position",
privs = {
interact = true
},
func = function(name, param)
local player = minetest.get_player_by_name(name);
local pos = player:getpos();
layout.pos = pos;
math.randomseed(tonumber(param) or 1);
local t = os.clock();
layout.generate();
local t = os.clock()-t;
local road,length,dir;
minetest.chat_send_all("#ROAD LAYOUT GENERATOR: number of roads " .. #layout.roads .. " generation time " .. t .. " seconds ");
-- rendering
for i = 1,#layout.roads do
road = layout.roads[i];
--minetest.chat_send_all("road " .. i .. " : " .. road[1] .. " " .. road[2] .. " " .. road[3] .. " " .. road[4])
length = road[3]-road[1] + road[4]-road[2];
dir = {x=(road[3]-road[1])/length,y=0,z=(road[4]-road[2])/length};
for j=1,length do
minetest.set_node({x=pos.x+road[1]+dir.x*j,y=pos.y+dir.y*j,z=road[2]+pos.z+dir.z*j},{name = "default:diamondblock"});
end
end
end
});
minetest.register_chatcommand("roadc", {
description = "/roadc, will clear render of previously made layout ",
privs = {
interact = true
},
func = function(name, param)
local pos = layout.pos;
for i = 1,#layout.roads do
road = layout.roads[i];
--minetest.chat_send_all("road " .. i .. " : " .. road[1] .. " " .. road[2] .. " " .. road[3] .. " " .. road[4])
length = road[3]-road[1] + road[4]-road[2];
dir = {x=(road[3]-road[1])/length,y=0,z=(road[4]-road[2])/length};
for j=1,length do
minetest.set_node({x=pos.x+road[1]+dir.x*j,y=pos.y+dir.y*j,z=road[2]+pos.z+dir.z*j},{name = "air"});
end
end
end
});