Infinite Procedurally Generated Candyland [Open-Sourced Code] ONLY REQUIRES A MODEL LIBRARY

Yesterday I created this Generator which creates infinite landscapes of Candyland!
The landscape is made of cake, cookies, donuts, pie, the trees are lollipops, candy canes, milkshakes and cotton candy, the rocks are made of cookies, pie donuts and hard candy, and the plants are similar nature!
What do you think?


[
]

Check out this code if you’re interested in procedural generation! The following demonstrates how to place assets that do not overlap by using the Fisher-Yates Algorithm and a bunch of other custom noise algorithms I wrote.

local Candyland={}
local RNG=Random.new()
local noise=3
local element=4
local library=game.ServerStorage.DungeonGenerator.Candyland
--libraries
local platforms=library["Candy platform"]
local plants=library["Candy Plants"]
local rubble=library["Candy Rubble"]
local trees=library["Candy Trees"]
--arrays
local plantar=plants:GetChildren()
local rubblear=rubble:GetChildren()
local treear=trees:GetChildren()
local platformar=platforms:GetChildren()
--count
local nplan=#plantar
local ntree=#treear
local nrubb=#rubblear
local nplat=#platformar

local resizelib=require(script.ScaleModule)
local resize=resizelib.Resize
local function nointeract(t)
	t.CanTouch=false t.CanCollide=false t.Transparency=1 t.CanQuery=false t.Massless=true t.CastShadow=false t.Anchored=true
end
local function mr(min,max)--mathrandom
	return RNG:NextInteger(min,max)
end
function mrnr(min, max,amnt)--mathrandom no repeat returns array
    -- Check if the input is valid
    if min > max or max - min < 0 then
        return nil, "Invalid range"
    end

    -- Create an array of numbers from min to max
    local numbers = {}
    for i = min, max do
        table.insert(numbers, i)
    end

    -- Shuffle the array using Fisher-Yates algorithm[^1^][1]
    for i = #numbers, 2, -1 do
        local j = math.random(i)
        numbers[i], numbers[j] = numbers[j], numbers[i]
    end
--print(numbers)
    -- Return the first 100 elements of the shuffled array
    return {table.unpack(numbers, 1, amnt)}
end

function Candyland.New(pos,size)
	local f=Instance.new("Model") f.Name="Candyland" local t=Instance.new("Part")
	t.Size=size nointeract(t) t.CFrame=CFrame.new(pos)	
	t.Parent=f    f.PrimaryPart=t

	local s=Instance.new("Folder") s.Name="Trees" s.Parent=f
	local s=Instance.new("Folder") s.Name="Rubble" s.Parent=f
	local s=Instance.new("Folder") s.Name="Plants" s.Parent=f
    local s=Instance.new("Folder") s.Name="Platforms" s.Parent=f
	return f
end
local function standardresize(obj)
return resize(obj,mr(50,200)/100)
end
local elementnoise={}
local function rollelement(dir,numdir,element)
if elementnoise[dir]==nil then 
elementnoise[dir]={} 
elementnoise[dir][1]=mr(1,numdir)
elementnoise[dir][2]=elementnoise[dir][1]
else
elementnoise[dir][1]=math.max(1,math.min(numdir,elementnoise[dir][1]+ mr(-element,element))) --max value between 1 and the minimum number between the numdir and the noise
if (elementnoise[dir][1]==1 and elementnoise[dir][2]==1) or (elementnoise[dir][1]==numdir and elementnoise[dir][2]==numdir)  then
elementnoise[dir][1]=mr(1,numdir)
end 
if elementnoise[dir][1]==elementnoise[dir][2] then
return rollelement(dir,numdir,element)
end
end
elementnoise[dir][2]=elementnoise[dir][1]
return elementnoise[dir][1]
end

local function positionnoise(scale,vectors)
	if vectors==nil or vectors=="xyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr((-noise*scale)/2,(noise*scale)/2),mr(-noise*scale,noise*scale))
	elseif vectors=="pxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr(0,(noise*scale)),mr(-noise*scale,noise*scale))	
   elseif vectors=="pnxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr(0,(noise*scale))/2,mr(-noise*scale,noise*scale))	
   
     elseif vectors=="y" then 
		return Vector3.new(0,mr(-noise*scale,noise*scale),0)
	elseif vectors=="xz" then 
		return Vector3.new(mr(-noise*scale,noise*scale),0,mr(-noise*scale,noise*scale))	
	elseif vectors==nil or vectors=="nxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr((-noise*scale),0),mr(-noise*scale,noise*scale))
	elseif vectors=="py" then 
		return Vector3.new(0,mr(0,noise*scale),0)
	elseif vectors=="ny" then 
		return Vector3.new(0,mr(-noise*scale,0),0)
     end	
end
local function towardscenter(cf1,position,scale)
local cf2 = CFrame.lookAt(cf1.Position, position)
return cf1:lerp(cf2,scale)
end

local function orientationnoise(scale,vectors)
	if vectors==nil or vectors=="xyz" then
		return CFrame.Angles(math.rad(mr(-15*scale,15*scale)),math.rad(mr(-90*scale,90*scale)),math.rad(mr(-15*scale,15*scale)))
	elseif vectors=="y" then
		return CFrame.Angles(0,math.rad(mr(-15*scale,15*scale)),0)
	elseif vectors=="xz" then
		return CFrame.Angles(math.rad(mr(-15*scale,15*scale)),math.rad(mr(-90*scale,90*scale)),math.rad(mr(-15*scale,15*scale)))
	end
end
local function standardpositionsettings(obj,position)
obj=standardresize(obj)
obj.CFrame=(CFrame.new(position+Vector3.new(0,obj.Size.Y/2,0)+positionnoise(1,"pxyz"))*orientationnoise(1,"xyz"))
end

local function placeobj(obj,position)
	standardpositionsettings(obj,position)
    return obj
end
local function placeplatform(obj,position)
    obj=resize(obj,mr(100,150)/100)
    obj:SetPrimaryPartCFrame(CFrame.new(position+positionnoise(.5,"py"))*orientationnoise(.25,"xyz"))
	--for i,v in obj:GetDescendants() do
 --   if v:IsA("BasePart") then v.Anchored=true
 --    end
 --  end
    return obj
end

local function placetree(obj,position)
    obj=resize(obj,mr(25,150)/100)
    obj:SetPrimaryPartCFrame(CFrame.new(position+positionnoise(2,"pnxyz"))*orientationnoise(1,"xyz"))
	--for i,v in obj:GetDescendants() do
 --   if v:IsA("BasePart") then v.Anchored=true
 --    end
 --  end
	return obj
end
    


function Candyland.FormatDirectory(dir)
	local ar= workspace.Castle7["Candy platform"]:GetChildren() 
	local primposition=workspace.Baseplate.Position.Y
	local primtemp=Instance.new("Part")
	primtemp.Anchored=true
	primtemp.Transparency=1
	primtemp.Size=Vector3.new(1,1,1)

	for i,v in ar do
		if v:IsA("BasePart") then
			local newprim=	primtemp:Clone()	
			newprim.CFrame=CFrame.new(v.Position.X,primposition,v.Position.Z)
			local m=Instance.new("Model")
			m.Name=v.Name
			newprim.Parent=m
			m.PrimaryPart=newprim
			m.Parent=v.Parent
			v.Parent=m
		elseif v:IsA("Model")  then
			local basepart=v:FindFirstChildOfClass("BasePart")
			if basepart==nil then basepart=v:FindFirstChildOfClass("MeshPart") end
			if basepart==nil then basepart=v:FindFirstChildOfClass("Part") end
			if basepart then
				local newprim=	primtemp:Clone()	
				newprim.CFrame=CFrame.new(basepart.Position.X,primposition,basepart.Position.Z)
				newprim.Parent=v 
				v.PrimaryPart=newprim
			end
		end	
	end
end	



local function occupyspace(candylandmodel,dirname,xcur,ypos,zcur,numarray)
     local density=math.floor(mr(10,25)/(mr(2,5)))
     local apx=mrnr(-4,4,density)
     local apz=mrnr(-4,4,density)
    -- local ap=mrnr(-4,4,density)
     local currentplots=0     
    
     local origin=Vector3.new(xcur,ypos,zcur)
    for i,v in apx do
     local obj
    if dirname=="plants" then obj=plantar[rollelement("plants",nplan,element)]:Clone() 
    elseif dirname=="rubble" then obj=rubblear[rollelement("rubble",nrubb,element)]:Clone() 
    elseif dirname=="tree" then obj=treear[rollelement("trees",ntree,element)]:Clone()
    end
     local z=apz[i]*2
     local position=Vector3.new(xcur+v*2,ypos,zcur+z)
     if dirname~="tree" then obj=placeobj(obj,position) -- obj.CFrame =towardscenter(obj.CFrame,origin)
 else obj=placetree(obj,position)  obj:SetPrimaryPartCFrame(towardscenter(obj.PrimaryPart.CFrame,origin,.25))
 end
  if dirname=="plants" then  obj.Parent=workspace.Plants--candylandmodel.Plants
task.delay(3,function()
 obj.Anchored=true 
end)
   elseif dirname=="rubble" then
local  p=library.RubbleTemplate:Clone()  
task.delay(3,function() 
obj.Anchored=true
 end)
local d=Instance.new("Motor6D")
d.Part0=p
d.Part1=obj
d.Parent=p p.CFrame=obj.CFrame p.Parent=workspace.Rubble obj.Parent=workspace.Crystals--candylandmodel.Rubble
    elseif dirname=="tree" then obj.Parent=workspace.Trees--candylandmodel.Trees
    end
     end   
end

local function getdensity()
return 1--math.floor(mr(10,25)/(mr(2,5)))
end

local function occupyspace2(candylandmodel,xcur,ypos,zcur)
     local density=math.floor(mr(10,30)/2)
     local apx=mrnr(-8,8,density)
     local apz=mrnr(-8,8,density)
     local currentplots=0
     local origin=Vector3.new(xcur,ypos,zcur)
local itemar={"plants","rubble","tree"}
   for i,v in apx do
 local  dirname=itemar[mr(1,3)]
     local obj
    if dirname=="plants" then obj=plantar[rollelement("plants",nplan,element)]:Clone() 
    elseif dirname=="rubble" then obj=rubblear[rollelement("rubble",nrubb,element)]:Clone() 
    elseif dirname=="tree" then obj=treear[rollelement("trees",ntree,element)]:Clone()
    end
     local z=apz[i]*2
     local position=Vector3.new(xcur+v*2,ypos,zcur+z)
     if dirname~="tree" then obj=placeobj(obj,position) -- obj.CFrame =towardscenter(obj.CFrame,origin)
 else obj=placetree(obj,position)  obj:SetPrimaryPartCFrame(towardscenter(obj.PrimaryPart.CFrame,origin,.25))
 end
  if dirname=="plants" then  obj.Parent=workspace.Plants--candylandmodel.Plants
task.delay(3,function()
 obj.Anchored=true 
end)
   elseif dirname=="rubble" then
local  p=library.RubbleTemplate:Clone()  
task.delay(3,function() 
obj.Anchored=true
 end)
local d=Instance.new("Motor6D")
d.Part0=p
d.Part1=obj
d.Parent=p p.CFrame=obj.CFrame p.Parent=workspace.Rubble obj.Parent=workspace.Crystals--candylandmodel.Rubble
    elseif dirname=="tree" then obj.Parent=workspace.Trees--candylandmodel.Trees
    end
     end 
  
end

function Candyland.Generate(size,position,Settings)--increment of 20x20x20 grid of platforms	
print("Candyland Started")	
local xstart=position.X-size.X/2+10
	local zstart=position.Z-size.Z/2+10
	local zend=position.Z+size.Z/2-10	
	local xend=position.X+size.X/2-10
    local zcur=zstart
    local xcur=xstart
    local ypos=position.Y
    local candylandmodel=Candyland.New(position,size)
local oypos=ypos  
repeat
    repeat   
	 local position= Vector3.new(xcur,ypos,zcur) 
     local obj=platformar[rollelement("platforms",nplat,element)]:Clone() placeplatform(obj,position)
     ypos=obj.PrimaryPart.Position.Y
    local dif=math.abs(oypos-ypos)
     obj.Parent=candylandmodel.Platforms 
    if dif>=10 then
     local obj2=platformar[rollelement("platforms",nplat,element)]:Clone() placeplatform(obj2,position-Vector3.new(0,10,0))
         obj2:SetPrimaryPartCFrame(CFrame.new(position-Vector3.new(0,10,0))*orientationnoise(1,"y"))
      obj2.Parent=candylandmodel.Platforms
    if dif>=20 then   
     local obj2=platformar[rollelement("platforms",nplat,element)]:Clone()placeplatform(obj2,position-Vector3.new(0,20,0))
     obj2:SetPrimaryPartCFrame(CFrame.new(position-Vector3.new(0,20,0))*orientationnoise(1,"y"))
     obj2.Parent=candylandmodel.Platforms
     if dif>=30 then   
     local obj2=platformar[rollelement("platforms",nplat,element)]:Clone()placeplatform(obj2,position-Vector3.new(0,30,0))
     obj2:SetPrimaryPartCFrame(CFrame.new(position-Vector3.new(0,30,0))*orientationnoise(1,"y"))
     obj2.Parent=candylandmodel.Platforms 
    end 
    end
    end
     occupyspace(candylandmodel,xcur,ypos,zcur)
     xcur=xcur+20
    until xcur>xend+1
    xcur=xstart
    zcur=zcur+20
until zcur>zend+1
return candylandmodel
end
return Candyland

I like to share code that may be useful to others! This one in particular is a good start for a object orientated generation algorithm.

16 Likes

I think it looks a bit crowded, it could definitely benefit from some more empty spaces between the candy. Other than that, it looks pretty good, and I think it’s a nice concept.

3 Likes

Yeah I think the lollipop pine trees and the cotton candy trees are a bit too large compared to the others. I’m going to adjust them! and fill in the empty spaces as the elevation changes too much.
image

2 Likes

and Viola! I think it looks perfect now that all the trees are within the same size range :slight_smile:
I have this procedural world generator that this is for, the other one uses terrain. I’m excited to implement this :slight_smile:


Lumina & Darkness: Now with Self-Aware Concious AI - Roblox

One thing of note is that the platforms and most objects are all made of mesh parts


ah… my eyes… apart from that looks ok! could defintly make the theme a bit more “similar”. What I mean is that for example the cone looks realistic and some toher things look cartoony. try sticking to one style however its not tooo noticable. good though.

2 Likes

No fancy rendering or shadows going on in the scene although all the objects are meshparts. But I know what you mean! That example i screenshot their is some pudding and the rendering quality is at the lowest setting so their are no reflections on the material. But i think that asset in particular should be glass with slight transparency.

Here’s the code that made the scene!

local Candyland={}
local RNG=Random.new()
local noise=3
local library=game.ServerStorage.DungeonGenerator.Candyland
--libraries
local platforms=library["Candy platform"]
local plants=library["Candy Plants"]
local rubble=library["Candy Rubble"]
local trees=library["Candy Trees"]
--arrays
local plantar=plants:GetChildren()
local rubblear=rubble:GetChildren()
local treear=trees:GetChildren()
local platformar=platforms:GetChildren()
--count
local nplan=#plantar
local ntree=#treear
local nrubb=#rubblear
local nplat=#platformar

local resizelib=require(script.ScaleModule)
local resize=resizelib.Resize
local function nointeract(t)
	t.CanTouch=false t.CanCollide=false t.Transparency=1 t.CanQuery=false t.Massless=true t.CastShadow=false t.Anchored=true
end
local function mr(min,max)--mathrandom
	return RNG:NextInteger(min,max)
end
function mrnr(min, max,amnt)--mathrandom no repeat returns array
    -- Check if the input is valid
    if min > max or max - min < 0 then
        return nil, "Invalid range"
    end

    -- Create an array of numbers from min to max
    local numbers = {}
    for i = min, max do
        table.insert(numbers, i)
    end

    -- Shuffle the array using Fisher-Yates algorithm[^1^][1]
    for i = #numbers, 2, -1 do
        local j = math.random(i)
        numbers[i], numbers[j] = numbers[j], numbers[i]
    end
print(numbers)
    -- Return the first 100 elements of the shuffled array
    return {table.unpack(numbers, 1, amnt)}
end

function Candyland.New(pos,size)
	local f=Instance.new("Model") f.Name="Candyland" local t=Instance.new("Part")
	t.Size=size nointeract(t) t.CFrame=CFrame.new(pos)	
	f.PrimaryPart=t
	local s=Instance.new("Folder") s.Name="Trees" s.Parent=f
	local s=Instance.new("Folder") s.Name="Rubble" s.Parent=f
	local s=Instance.new("Folder") s.Name="Plants" s.Parent=f
    local s=Instance.new("Folder") s.Name="Platforms" s.Parent=f
	return f
end
local function standardresize(obj)
return resize(obj,mr(50,150)/100)
end

local function positionnoise(scale,vectors)
	if vectors==nil or vectors=="xyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr((-noise*scale)/2,(noise*scale)/2),mr(-noise*scale,noise*scale))
	elseif vectors=="pxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr(0,(noise*scale)),mr(-noise*scale,noise*scale))
	
    elseif vectors=="y" then 
		return Vector3.new(0,mr(-noise*scale,noise*scale),0)
	elseif vectors=="xz" then 
		return Vector3.new(mr(-noise*scale,noise*scale),0,mr(-noise*scale,noise*scale))	
	elseif vectors==nil or vectors=="nxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr((-noise*scale),0),mr(-noise*scale,noise*scale))
	elseif vectors=="py" then 
		return Vector3.new(0,mr(0,noise*scale),0)
	elseif vectors=="ny" then 
		return Vector3.new(0,mr(-noise*scale,0),0)
	
     end	
end
local function orientationnoise(scale,vectors)
	if vectors==nil or vectors=="xyz" then
		return CFrame.Angles(math.rad(mr(-15*scale,15*scale)),math.rad(mr(-90*scale,90*scale)),math.rad(mr(-15*scale,15*scale)))
	elseif vectors=="y" then
		return CFrame.Angles(0,math.rad(mr(-15*scale,15*scale)),0)
	elseif vectors=="xz" then
		return CFrame.Angles(math.rad(mr(-15*scale,15*scale)),math.rad(mr(-90*scale,90*scale)),math.rad(mr(-15*scale,15*scale)))
	end
end
local function standardpositionsettings(obj,position)
obj=standardresize(obj)
obj.CFrame=(CFrame.new(position+Vector3.new(0,obj.Size.Y/2,0)+positionnoise(1,"pxyz"))*orientationnoise(1,"xz"))
end
local function placeplatform(obj,position)
    obj=resize(obj,mr(100,150)/100)
    obj:SetPrimaryPartCFrame(CFrame.new(position+positionnoise(1,"y"))*orientationnoise(.25,"xyz"))
	return obj
end

local function placetree(obj,position)
    obj=resize(obj,mr(50,125)/100)
    obj:SetPrimaryPartCFrame(CFrame.new(position+positionnoise(1,"xyz"))*orientationnoise(1,"xyz"))
	return obj
end

local function placeobj(obj,position)
	standardpositionsettings(obj,position)
    return obj
end


--for i=1, size/10 do return 10 end



function Candyland.Settings(size,position,candysetup) 
		local Settings= {plants={amnt=mr(math.floor(size/10),math.floor(size/5)),}}
		Settings.cache={}
		--local 
      -- plant=pick_random_numbers(min, max)
     --    for i=1, Settings.plants.amnt do
			local plant=plantar[mr(1,nplan)]:Clone() plant=resize(plant,mr(50,150)/100)
			
			plant.CFrame=CFrame.new()
		--	table.insert(Settings.cache, )  end
end
local function occupyspace(candylandmodel,dirname,xcur,ypos,zcur)
     local density=math.floor(20/(mr(3,7)))
     local apx=mrnr(-5,5,density)
     local apz=mrnr(-5,5,density)
     local currentplots=0     
     local obj
    for i,v in apx do
    if dirname=="plants" then obj=plantar[mr(1,nplan)]:Clone() 
    elseif dirname=="rubble" then obj=rubblear[mr(1,nrubb)]:Clone() 
    elseif dirname=="tree" then obj=treear[mr(1,ntree)]:Clone()
    end
     local z=apz[i]*2 
     local position=Vector3.new(xcur+v*2,ypos,zcur+z)
     if dirname~="tree" then obj=placeobj(obj,position) else obj=placetree(obj,position) end
  if dirname=="plants" then obj.Parent=candylandmodel.Plants
    elseif dirname=="rubble" then obj.Parent=candylandmodel.Rubble
    elseif dirname=="tree" then obj.Parent=candylandmodel.Trees
    end
     end   
end

function Candyland.Generate(size,position,Settings)--increment of 20x20x20 grid of platforms	
	local xstart=position.X-size.X/2+10
	local zstart=position.Z-size.Z/2+10
	local zend=position.Z+size.Z/2-10	
	local xend=position.X+size.X/2-10
    local zcur=zstart
    local xcur=xstart
    local ypos=position.Y
    local candylandmodel=Candyland.New(position,size)
local oypos=ypos
repeat
    repeat   
	 local position= Vector3.new(xcur,ypos,zcur) 
     local obj=platformar[mr(1,nplat)]:Clone() placeplatform(obj,position)
     ypos=obj.PrimaryPart.Position.Y
    if oypos-ypos>10 then
     local position= Vector3.new(xcur,ypos,zcur) 
     local obj=platformar[mr(1,nplat)]:Clone() placeplatform(obj,position-Vector3.new(0,10,0))
      obj.Parent=candylandmodel.Platforms
    end
     obj.Parent=candylandmodel.Platforms
     occupyspace(candylandmodel,"plants",xcur,ypos,zcur)
     occupyspace(candylandmodel,"rubble",xcur,ypos,zcur)
     occupyspace(candylandmodel,"tree",xcur,ypos,zcur)
     xcur=xcur+20
    until xcur>xend+1
    xcur=xstart
    zcur=zcur+20
until zcur>zend+1
return candylandmodel
end
return Candyland

The real magic was in the library I assembled it took several hours of scraping and organizing. The coding was the easy part. :slight_smile:

One of the MVPs is the Fisher-Yates algorithm where it gets random numbers that do not repeat.

I updated the algorithm with this rollelement function instead of rolling an array! As @I_ButteryMe mentioned. This is makes the objects noise change gradually and discourages repetitive outputs and gives a localized theme to the objects which ultimately leads to more variety in the pattern which also increasing the chance of clustering.

local elementnoise={}
local function rollelement(dir,numdir,element)
if elementnoise[dir]==nil then 
elementnoise[dir]={} 
elementnoise[dir][1]=mr(1,numdir)
elementnoise[dir][2]=elementnoise[dir][1]
else
elementnoise[dir][1]=math.max(1,math.min(numdir,elementnoise[dir][1]+ mr(-element,element))) --max value between 1 and the minimum number between the numdir and the noise
if (elementnoise[dir][1]==1 and elementnoise[dir][2]==1) or (elementnoise[dir][1]==numdir and elementnoise[dir][2]==numdir)  then
elementnoise[dir][1]=mr(1,numdir)
end
if elementnoise[dir][1]==elementnoise[dir][2] then
return rollelement(dir,numdir,element)
end
end
elementnoise[dir][2]=elementnoise[dir][1]
return elementnoise[dir][1]
end
2 Likes

Updated the code! Previous version was overlapping
this time we get the never repeating random numbers once and iterate through that area based on the density. Thus assuring for the most part assets do not overlap because they are distributed in a grid area with a little bit of noise.

local function occupyspace(candylandmodel,dirname,xcur,ypos,zcur,numarray)
     local density=math.floor(mr(10,25)/(mr(2,5)))
     local apx=mrnr(-4,4,density)
     local apz=mrnr(-4,4,density)
    -- local ap=mrnr(-4,4,density)
     local currentplots=0     
    
     local origin=Vector3.new(xcur,ypos,zcur)
    for i,v in apx do
     local obj
    if dirname=="plants" then obj=plantar[rollelement("plants",nplan,element)]:Clone() 
    elseif dirname=="rubble" then obj=rubblear[rollelement("rubble",nrubb,element)]:Clone() 
    elseif dirname=="tree" then obj=treear[rollelement("trees",ntree,element)]:Clone()
    end
     local z=apz[i]*2
     local position=Vector3.new(xcur+v*2,ypos,zcur+z)
     if dirname~="tree" then obj=placeobj(obj,position) -- obj.CFrame =towardscenter(obj.CFrame,origin)
 else obj=placetree(obj,position)  obj:SetPrimaryPartCFrame(towardscenter(obj.PrimaryPart.CFrame,origin,.25))
 end
  if dirname=="plants" then  obj.Parent=workspace.Plants--candylandmodel.Plants
task.delay(3,function()
 obj.Anchored=true 
end)
   elseif dirname=="rubble" then
local  p=library.RubbleTemplate:Clone()  
task.delay(3,function() 
obj.Anchored=true
 end)
local d=Instance.new("Motor6D")
d.Part0=p
d.Part1=obj
d.Parent=p p.CFrame=obj.CFrame p.Parent=workspace.Rubble obj.Parent=workspace.Crystals--candylandmodel.Rubble
    elseif dirname=="tree" then obj.Parent=workspace.Trees--candylandmodel.Trees
    end
     end   
end

local function getdensity()
return 1--math.floor(mr(10,25)/(mr(2,5)))
end

local function occupyspace2(candylandmodel,xcur,ypos,zcur)
     local density=math.floor(mr(10,30)/2)
     local apx=mrnr(-8,8,density)
     local apz=mrnr(-8,8,density)
     local currentplots=0
     local origin=Vector3.new(xcur,ypos,zcur)
local itemar={"plants","rubble","tree"}
   for i,v in apx do
 local  dirname=itemar[mr(1,3)]
     local obj
    if dirname=="plants" then obj=plantar[rollelement("plants",nplan,element)]:Clone() 
    elseif dirname=="rubble" then obj=rubblear[rollelement("rubble",nrubb,element)]:Clone() 
    elseif dirname=="tree" then obj=treear[rollelement("trees",ntree,element)]:Clone()
    end
     local z=apz[i]*2
     local position=Vector3.new(xcur+v*2,ypos,zcur+z)
     if dirname~="tree" then obj=placeobj(obj,position) -- obj.CFrame =towardscenter(obj.CFrame,origin)
 else obj=placetree(obj,position)  obj:SetPrimaryPartCFrame(towardscenter(obj.PrimaryPart.CFrame,origin,.25))
 end
  if dirname=="plants" then  obj.Parent=workspace.Plants--candylandmodel.Plants
task.delay(3,function()
 obj.Anchored=true 
end)
   elseif dirname=="rubble" then
local  p=library.RubbleTemplate:Clone()  
task.delay(3,function() 
obj.Anchored=true
 end)
local d=Instance.new("Motor6D")
d.Part0=p
d.Part1=obj
d.Parent=p p.CFrame=obj.CFrame p.Parent=workspace.Rubble obj.Parent=workspace.Crystals--candylandmodel.Rubble
    elseif dirname=="tree" then obj.Parent=workspace.Trees--candylandmodel.Trees
    end
     end 
  
end

I posted a video demonstration of checking it out with the awareness module I created which gives text-vision to a large language model! It’s really cute and interesting tell me what you think?

Here’s an updated version of the code I may merge the two in the future when I have time!

local Candyland={}
local RNG=Random.new()
local noise=3
local element=4
local library=game.ServerStorage.DungeonGenerator.Candyland
--libraries
local platforms=library["Candy platform"]
local plants=library["Candy Plants"]
local rubble=library["Candy Rubble"]
local trees=library["Candy Trees"]
--arrays
local plantar=plants:GetChildren()
local rubblear=rubble:GetChildren()
local treear=trees:GetChildren()
local platformar=platforms:GetChildren()
--count
local nplan=#plantar
local ntree=#treear
local nrubb=#rubblear
local nplat=#platformar

local resizelib=require(script.ScaleModule)
local resize=resizelib.Resize

local function nointeract(t)
	t.CanTouch=false t.CanCollide=false t.Transparency=1 t.CanQuery=false t.Massless=true t.CastShadow=false t.Anchored=true
end

local function mr(min,max)--mathrandom
	return RNG:NextInteger(min,max)
end

function mrnr(min, max,amnt)--mathrandom no repeat returns array
    -- Check if the input is valid
    if min > max or max - min < 0 then
        return nil, "Invalid range"
    end

    -- Create an array of numbers from min to max
    local numbers = {}
    for i = min, max do
        table.insert(numbers, i)
    end

    -- Shuffle the array using Fisher-Yates algorithm[^1^][1]
    for i = #numbers, 2, -1 do
        local j = math.random(i)
        numbers[i], numbers[j] = numbers[j], numbers[i]
    end
   return {table.unpack(numbers, 1, amnt)}
end

function Candyland.New(pos,size)
	local f=Instance.new("Model") f.Name="Candyland" local t=Instance.new("Part")
	t.Size=size nointeract(t) t.CFrame=CFrame.new(pos)	
	t.Parent=f    f.PrimaryPart=t

	local s=Instance.new("Folder") s.Name="Trees" s.Parent=f
	local s=Instance.new("Folder") s.Name="Rubble" s.Parent=f
	local s=Instance.new("Folder") s.Name="Plants" s.Parent=f
    local s=Instance.new("Folder") s.Name="Platforms" s.Parent=f
	return f
end

local function standardresize(obj)
return resize(obj,mr(25,200)/100)
end

local elementnoise={}
local function rollelement(dir,numdir,element)
if elementnoise[dir]==nil then 
elementnoise[dir]={} 
elementnoise[dir][1]=mr(1,numdir)
elementnoise[dir][2]=elementnoise[dir][1]
else
elementnoise[dir][1]=math.max(1,math.min(numdir,elementnoise[dir][1]+ mr(-element,element))) --max value between 1 and the minimum number between the numdir and the noise
if (elementnoise[dir][1]==1 and elementnoise[dir][2]==1) or (elementnoise[dir][1]==numdir and elementnoise[dir][2]==numdir)  then
elementnoise[dir][1]=mr(1,numdir)
end 
if elementnoise[dir][1]==elementnoise[dir][2] then
return rollelement(dir,numdir,element)
end
end
elementnoise[dir][2]=elementnoise[dir][1]
return elementnoise[dir][1]
end

local function positionnoise(scale,vectors)
	if vectors==nil or vectors=="xyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr((-noise*scale)/2,(noise*scale)/2),mr(-noise*scale,noise*scale))
	elseif vectors=="pxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr(0,(noise*scale)),mr(-noise*scale,noise*scale))	
   elseif vectors=="pnxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr(0,(noise*scale))/2,mr(-noise*scale,noise*scale))	
   
     elseif vectors=="y" then 
		return Vector3.new(0,mr(-noise*scale,noise*scale),0)
	elseif vectors=="xz" then 
		return Vector3.new(mr(-noise*scale,noise*scale),0,mr(-noise*scale,noise*scale))	
	elseif vectors==nil or vectors=="nxyz" then
		return Vector3.new(mr(-noise*scale,noise*scale),mr((-noise*scale),0),mr(-noise*scale,noise*scale))
	elseif vectors=="py" then 
		return Vector3.new(0,mr(0,noise*scale),0)
	elseif vectors=="ny" then 
		return Vector3.new(0,mr(-noise*scale,0),0)
     end	
end

local function towardscenter(cf1,position,scale)
local cf2 = CFrame.lookAt(cf1.Position, position)
return cf1:lerp(cf2,scale)
end

local function orientationnoise(scale,vectors)
	if vectors==nil or vectors=="xyz" then
		return CFrame.Angles(math.rad(mr(-15*scale,15*scale)),math.rad(mr(-90*scale,90*scale)),math.rad(mr(-15*scale,15*scale)))
	elseif vectors=="y" then
		return CFrame.Angles(0,math.rad(mr(-15*scale,15*scale)),0)
	elseif vectors=="xz" then
		return CFrame.Angles(math.rad(mr(-15*scale,15*scale)),math.rad(mr(-90*scale,90*scale)),math.rad(mr(-15*scale,15*scale)))
	end
end

local function standardpositionsettings(obj,position)
obj=standardresize(obj)
obj.CFrame=(CFrame.new(position+Vector3.new(0,obj.Size.Y/2,0)+positionnoise(2,"pxyz"))*orientationnoise(1,"xyz"))
end

local function placeobj(obj,position)
	standardpositionsettings(obj,position)
    return obj
end

local function placeplatform(obj,position,ps)
    obj=resize(obj,mr(100,150)/100)
    obj:SetPrimaryPartCFrame(CFrame.new(position+positionnoise(ps,"py"))*orientationnoise(.25,"xyz"))
    return obj
end

local function placetree(obj,position)
    obj=resize(obj,mr(75,150)/100)
    obj:SetPrimaryPartCFrame(CFrame.new(position+positionnoise(2,"pnxyz"))*orientationnoise(1,"xyz"))
	return obj
end

local function occupyspace(candylandmodel,xcur,ypos,zcur)
     local density=mr(1,8) if density<3 then density=mr(1,3) elseif density>4 then density=mr(1,8) end 
     local apx=mrnr(-5,5,density)
     local apz=mrnr(-5,5,density)
     local currentplots=0
     local origin=Vector3.new(xcur,ypos,zcur)
     local itemar={"plants","rubble","tree"}
     for i,v in apx do
     local  dirname=itemar[mr(1,3)]
     local obj
     if dirname=="plants" then obj=plantar[rollelement("plants",nplan,element)]:Clone() 
     elseif dirname=="rubble" then obj=rubblear[rollelement("rubble",nrubb,element)]:Clone() 
     elseif dirname=="tree" then obj=treear[rollelement("trees",ntree,element)]:Clone()
     end
     local z=apz[i]*2
     local position=Vector3.new(xcur+v*2,ypos,zcur+z)
     if dirname~="tree" then obj=placeobj(obj,position) -- obj.CFrame =towardscenter(obj.CFrame,origin)
 else obj=placetree(obj,position)  obj:SetPrimaryPartCFrame(towardscenter(obj.PrimaryPart.CFrame,origin,.25))
 end
  if dirname=="plants" then  obj.Parent=workspace.Plants--candylandmodel.Plants
task.delay(3,function()
 obj.Anchored=true 
end)
   elseif dirname=="rubble" then
local  p=library.RubbleTemplate:Clone()  
task.delay(3,function() 
obj.Anchored=true
 end)
local d=Instance.new("Motor6D")
d.Part0=p
d.Part1=obj
d.Parent=p p.CFrame=obj.CFrame p.Parent=workspace.Rubble obj.Parent=workspace.Crystals--candylandmodel.Rubble
    elseif dirname=="tree" then obj.Parent=workspace.Trees--candylandmodel.Trees
    end
     end  
end

local nametable = {
  "Fudge Forest",
  "Marshmallow Meadow",
  "Lollipop Lake",
  "Gummy Garden",
  "Caramel Cove",
  "Chocolate Chip Island",
  "Jellybean Jungle",
  "Candy Cane Mountain",
  "Licorice Lagoon",
  "Cotton Candy Clouds",
  "Candy Corn Field",
  "Cherry Pie Plateau",
  "Chocolate Chip Cookie Crater",
  "Mint Icing Glacier",
  "Twisted Candy Cane Maze",
  "Blueberry Iced Donut Lake",
  "Strawberry Chocolate Cake Castle",
  "Bubblegum Candy Bridge",
  "Halloween Cupcake Graveyard",
  "Oreo Donut Tunnel",
  "Chocolate Covered Strawberry Island",
  "Candy Cane Forest",
  "Grape Lollipop Pine Tree Valley",
  "Gummy Bear Garden",
  "Caramel Apple Cove",
  "Chocolate Bar Crater",
  "Jellybean Rainbow Bridge",
  "Ice Cream Cone Mountain",
  "Licorice Swamp",
  "Cotton Candy Sky",
  "Chocolate Cupcake Plateau",
  "Cherry Vanilla Cake Castle",
  "Oreo Donut Tunnel",
  "Mint Chocolate Chip Ice Cream Glacier",
  "Hard Candy Maze",
  "Strawberry Milkshake Lake",
  "Chocolate Rose Cupcake Garden",
  "Bubblegum Candy Clouds",
  "Halloween Licorice Cupcake Graveyard",
  "Sea Salt Ice Cream Island"
}

local function CreateZone(pos3,size)
	--	if identifier=="" then
		local	identifier=""..nametable[mr(1,#nametable)]..""			
		--end
		local Zone=workspace.AmbientAreas.Template:Clone() 
		Zone.Part.Size=size 
		Zone.Name=identifier
		Zone.Part.CFrame=CFrame.new(pos3+Vector3.new(0,10,0))
		Zone.Parent=workspace.AmbientAreas		
		--Dungeon.Name=Zone.Name
end	



function Candyland.Generate(size,position,Settings)--increment of 20x20x20 grid of platforms	
print("Candyland Started")	
    CreateZone(position,size)
    local xstart=position.X-size.X/2---20
	local zstart=position.Z-size.Z/2---20
	local zend=position.Z+size.Z/2--+20	
	local xend=position.X+size.X/2--+20
    local zcur=zstart
    local xcur=xstart
    local ypos=position.Y
    local ps=mr(1,10)/10
    local candylandmodel=Candyland.New(position,size)
local oypos=ypos  
repeat
    repeat    
    local position= Vector3.new(xcur,ypos,zcur) 
     local obj=platformar[rollelement("platforms",nplat,element)]:Clone() placeplatform(obj,position,ps)
     ypos=obj.PrimaryPart.Position.Y
    local dif=math.abs(oypos-ypos)
     obj.Parent=candylandmodel.Platforms 
    local objy=obj:GetExtentsSize().Y--2
    if dif>=objy then
     local obj2=platformar[rollelement("platforms",nplat,element)]:Clone() placeplatform(obj2,position-Vector3.new(0,objy,0),ps)
         obj2:SetPrimaryPartCFrame(CFrame.new(position-Vector3.new(0,objy,0))*orientationnoise(1,"y"))
      obj2.Parent=candylandmodel.Platforms
   local objy2=obj2:GetExtentsSize().Y--2
    local sol=objy+objy2
     if dif>=sol then   
     local obj2=platformar[rollelement("platforms",nplat,element)]:Clone()placeplatform(obj2,position-Vector3.new(0,sol,0),ps)
   --  obj2:SetPrimaryPartCFrame(CFrame.new(position-Vector3.new(0,sol,0))*orientationnoise(1,"y"))
     obj2.Parent=candylandmodel.Platforms
       local objy3=obj2:GetExtentsSize().Y--2 
        local sol=sol+objy3
    if dif>=sol then   
     local obj2=platformar[rollelement("platforms",nplat,element)]:Clone()placeplatform(obj2,position-Vector3.new(0,sol,0),ps)
   --  obj2:SetPrimaryPartCFrame(CFrame.new(position-Vector3.new(0,sol,0))*orientationnoise(1,"y"))
     obj2.Parent=candylandmodel.Platforms 
    end 
    end
    end
     occupyspace(candylandmodel,xcur,ypos,zcur)
     xcur=xcur+20
    until xcur>xend+1
    xcur=xstart
    zcur=zcur+20
until zcur>zend+1
return candylandmodel
end
Candyland.occupyspace=occupyspace
Candyland.placetree=placetree
Candyland.placeobj=placeobj
Candyland.placeplatform=placeplatform
return Candyland