Floor pattern question

So I found this really cool challenge that MrBeast did in one of his videos and I wanted to replicate this challenge only and make a separate game out of it but I am unsure how I would generate a random new pattern (that would also be represented by green and the wrong blocks would be in red) every round.

Example:

Video: https://youtu.be/PWirijQkH4M?t=251

5 Likes

trye creating it in real life and then seeing how u can script that in studio

2 Likes

algorithm to generate random path in 2D tilemap - Stack Overflow

Linked above is a solution to this question using the A* algorithm. There are many ways to solve this and a quick google search for “random path through 2d tilemap” will get you the results you want

3 Likes

This doesn’t tell me how to make it in lua though

2 Likes

That is a problem for you to solve. It’s not terribly hard to translate from one language to another (keep in mind Lua is not zero-indexed like most other languages)

2 Likes

Wdym non zero indexed. And how do I convert css to lua

2 Likes

Non-zero indexed means arrays start at the value 1 in Lua, as opposed to 0, which is what many other languages use.

image

Anyways…
The first two steps are simple:

Step 1 is just asking to create a blank tilemap.

-- Generate a 2D tilemap filled with zeroes of size size_x by size_y
local tilemap = {}
local size_x = 8
local size_y = 8

for x = 1, size_x do
  tilemap[x] = {}
  for y = 1, size_y do
    tilemap[x][y] = "0"
  end
end

Step 2 is just asking you to add some random obstacles to determine the shape of the path.

local num_obstacles = 4
local rnd = Random.new()

for i = 1, num_obstacles do
  -- Pick num_obstacles tiles at random positions to turn into Xs
  local rx, ry = Random:NextInteger(1, size_x), Random:NextInteger(1, size_y)
  while tilemap[rx][ry] == "X" do
    -- If the chosen tile is already a X, pick a new one
    rx, ry = Random:NextInteger(1, size_x), Random:NextInteger(1, size_y)
  end
  tilemap[rx][ry] = "X"
end

Step 3 is the hard part and requires you to understand how to implement A*. I won’t go into details about it here, but you can use this as a stepping stone. Also, look for clearer solutions if you are struggling with the one I provided.

3 Likes

Yeah this is kinda overly complicated I think I will just create the paths manually (I even used chatgpt to attempt to find the A*)

1 Like

By not making them totally random… Create 30-50 of your own and randomly pick from them.

3 Likes

I made scripts that generate parts based on what you need. It is tested.

My script requires another script in server script service, to make kill blocks. This is that other script.

local CollectionService = game:GetService("CollectionService")

for _, part in CollectionService:GetTagged("KillBlock") do
	part.Touched:Connect(function(hit)
		if hit and hit.Parent and hit.Parent:FindFirstChild("Humanoid") then
			hit.Parent.Humanoid.Health = 0
		end
	end)
end

This script works as a server script in workspace.

local function createBeastGrid(gridWidth: IntValue, gridHeight: IntValue)
	local grid = {}

	local goDowns = 0
	local x = math.random(2,gridWidth-1)
	local y = 1
	local needle
	local done = false
	local blockCornerConnections = 1 -- 0 is false, 1 is true

	-- Fill the table with empty values
	for i = 1, gridWidth do
		grid[i] = {}
		for j = 1, gridHeight do
			grid[i][j] = 0
		end
	end

	-- generate a path foreward, but keep your distance from your own path
	-- also, always go in one direction (mostly)
	-- 0 is right, 90 is up, 180 is left, 270 is down.
	while done == false do
		local goodDirections = {0,90,180,270}
		-- set the current tile
		grid[x][y]=1

		if y == gridHeight then
			return grid;
		end

		-- dont leave the grid
		if x == 1 then
			needle = table.find(goodDirections,180)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		if x == gridWidth then
			needle = table.find(goodDirections,0)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		if y == 1 then
			needle = table.find(goodDirections,270)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		-- check right edge
		if grid[math.clamp(x+1,1,gridWidth)][y] + grid[math.clamp(x+1,1,gridWidth)][math.clamp(y+1,1,gridHeight)] + grid[math.clamp(x+1,1,gridWidth)][math.clamp(y-1,1,gridHeight)] + (grid[math.clamp(x+2,1,gridWidth)][math.clamp(y-1,1,gridHeight)] * blockCornerConnections)>0 then
			needle = table.find(goodDirections,0)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		-- check left edge
		if grid[math.clamp(x-1,1,gridWidth)][y] + grid[math.clamp(x-1,1,gridWidth)][math.clamp(y+1,1,gridHeight)] + grid[math.clamp(x-1,1,gridWidth)][math.clamp(y-1,1,gridHeight)] + (grid[math.clamp(x-2,1,gridWidth)][math.clamp(y-1,1,gridHeight)] * blockCornerConnections)>0 then 
			needle = table.find(goodDirections,180)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		-- check upper edge
		if grid[x][math.clamp(y+1,1,gridHeight)] + grid[math.clamp(x-1,1,gridWidth)][math.clamp(y+1,1,gridHeight)] + grid[math.clamp(x+1,1,gridWidth)][math.clamp(y+1,1,gridHeight)]>0 then
			needle = table.find(goodDirections,90)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		-- check lower edge
		if grid[x][math.clamp(y-1,1,gridHeight)] + grid[math.clamp(x-1,1,gridWidth)][math.clamp(y-1,1,gridHeight)] + grid[math.clamp(x+1,1,gridWidth)][math.clamp(y-1,1,gridHeight)] + grid[x][math.clamp(y-2,1,gridHeight)] + ((grid[math.clamp(x-1,1,gridWidth)][math.clamp(y-2,1,gridHeight)] + grid[math.clamp(x+1,1,gridWidth)][math.clamp(y-2,1,gridHeight)]) * blockCornerConnections)>0 then
			needle = table.find(goodDirections,270)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		-- limit going down to happen only once
		if goDowns>0 then
			needle = table.find(goodDirections,270)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end

		-- never go down on the edge of the table
		if x <= 2 or x >= gridWidth-1 then
			needle = table.find(goodDirections,270)
			if needle ~= nil then
				table.remove(goodDirections,needle)
			end
		end
		
		

		local chosenDirection
		if #goodDirections > 0 then
			chosenDirection = goodDirections[ math.random(1, #goodDirections ) ]
			-- make left and right more common than up and down
			if math.random(1,2) == 1 and (chosenDirection == 90 or chosenDirection == 270) then
				chosenDirection = goodDirections[ math.random(1, #goodDirections ) ]
			end
		else
			done = true
		end

		if chosenDirection == 270 then
			goDowns += 1
		end
		if chosenDirection == 90 then
			goDowns = math.max(goDowns-1,0)
		end
		
		-- go toward the chosen direction
		if chosenDirection == 0 then
			x += 1
		end
		if chosenDirection == 180 then
			x -= 1
		end
		if chosenDirection == 90 then
			y += 1
		end
		if chosenDirection == 270 then
			y -= 1
		end
	end
	return nil
end

local beastGridWidth = 10
local beastGridHeight = 20
local generationFailed = true
while generationFailed == true do
	local beastGrid = createBeastGrid(beastGridWidth, beastGridHeight)

	-- generate the physical grid do
	if beastGrid ~= nil then
		generationFailed = false
		for x = 1, beastGridWidth do
			for y = 1, beastGridHeight do
				--print('see me')-- seen
				local beastTile = Instance.new("Part")
				beastTile.Size = Vector3.new(9,0.5,9)
				beastTile.Position = Vector3.new(x*10,0.25,y*10)
				beastTile.Color = Color3.new(1, 0.333333, 0)

				if beastGrid[x][y] == 0 then
					beastTile:AddTag("KillBlock")
				else
					beastTile.Color = Color3.new(0.333333, 1, 0.498039)
				end
				beastTile.Anchored = true
				beastTile.Parent = workspace
			end
		end
	end
end

Edit: Grammar fix. Scripts not changed.
Edit 2: Added an extra check to “check lower edge”. (avoids path connects.)
Edit 3: Added a switch to control corner connections.
Edit 4: Somehow, generation can still fail sometimes… The scriot now re-generates the grid if it fails to generate. I’ll keep working on it. I couldn’t get the function to fail. Maybe it’s a roblox bug or something. I do know that the re-generation always works, so I guess it’s done.
Edit 5: goDowns was intended to go down as you go up. It does now. My mistake.
Edit 6: “never go down on the edge of the table” now has a thicker boundary. this will reduce failed generations.

2 Likes

I am gonna be honest you didnt need to write the whole code for me thank you anyway!

1 Like

(You might not need it anymore.) I added a position offset to this. use it to move the grid. I also added tile size controls. You will need to playtest to see it though.

If you want a MeshPart instead of a Part, you could do the last part like this.

local beastGridWidth = 10
local beastGridHeight = 20
local tileSize = 10
local gapSize = 1
local positionOffset = Vector3.new(0,0,0)

local generationFailed = true
while generationFailed == true do
	local beastGrid = createBeastGrid(beastGridWidth, beastGridHeight)

	-- generate the physical grid do
	if beastGrid ~= nil then
		generationFailed = false
		for x = 1, beastGridWidth do
			for y = 1, beastGridHeight do
				--print('see me')-- seen
				local beastTile = Instance.new("MeshPart")
				beastTile.MeshId = "rbxassetid://99999999999" -- safe tile
				beastTile.Size = Vector3.new(tileSize-gapSize,0.5,tileSize-gapSize)
				beastTile.Position = Vector3.new(x*tileSize,0,y*tileSize) + positionOffset

				if beastGrid[x][y] == 0 then
					beastTile:AddTag("KillBlock")
				else
					beastTile.MeshId = "rbxassetid://99999999999" -- kill tile
				end
				beastTile.Anchored = true
				beastTile.Parent = workspace
			end
		end
	end
end
2 Likes

So I set the positionOffset to the position I want to put the whole model to?

1 Like

Yes. The movement point will probably be oriented to one of the corners.
Edit: You might need to tinker with the middle number in beastTile.Size = Vector3.new(tileSize-gapSize,0.5,tileSize-gapSize)

1 Like

Is there anyway I could convert this so the script would pick parts from a model to make the same type of path but in this case there would be a boolvalue in the part called Safe and if its ticked then its safe and vice versa instead of spawning them individually. So pretty much I want to manually make a grid and then use a script to pick random part that connect into a path.

1 Like

If I understand correctly, you want more than one model to represent either type of part (safe and unsafe)? I get the feeling that you will need to set model height scales for each model you use, so I made it so you could do that as well. Just fill the lists where it says
local safeModels = { LIST }
local unsafeModels = { LIST }
local safeModelHeightScale = { LIST }
local unsafeModelHeightScale = { LIST }

local beastGridWidth = 10
local beastGridHeight = 20
local tileSize = 10
local gapSize = 1
local positionOffset = Vector3.new(0,0,0)
local safeModels = {"rbxassetid://99999999994", "rbxassetid://99999999995", "rbxassetid://99999999996"}
local unsafeModels = {"rbxassetid://99999999997", "rbxassetid://99999999998", "rbxassetid://99999999999"}
local safeModelHeightScale = {1,2,1.5}
local unsafeModelHeightScale = {1.75,1.25,1.4}

local generationFailed = true
while generationFailed == true do
	local beastGrid = createBeastGrid(beastGridWidth, beastGridHeight)

	-- generate the physical grid do
	if beastGrid ~= nil then
		generationFailed = false
		for x = 1, beastGridWidth do
			for y = 1, beastGridHeight do
				--print('see me')-- seen
				local beastTile = Instance.new("MeshPart")
				local safeModelIndex = math.random(1, #safeModels )
				local unsafeModelIndex = math.random(1, #unsafeModels )
				beastTile.MeshId = safeModels[safeModelIndex]
				beastTile.Size = Vector3.new(tileSize-gapSize,safeModelHeightScale[safeModelIndex],tileSize-gapSize)
				beastTile.Position = Vector3.new(x*tileSize,0,y*tileSize) + positionOffset

				if beastGrid[x][y] == 0 then
					beastTile:AddTag("KillBlock")
				else
					beastTile.MeshId = unsafeModels[ unsafeModelIndex ]
					beastTile.Size = Vector3.new(tileSize-gapSize,unsafeModelHeightScale[unsafeModelIndex],tileSize-gapSize)
				end
				beastTile.Anchored = true
				beastTile.Parent = workspace
			end
		end
	end
end
1 Like

I want to make one model that contains all the path parts and when a rounds starts (I can code the round system myself) a random path would be generated which would check or uncheck each path part ‘Safe’ value which would create the path.

1 Like

Once you import your model into studio, anchor it, position it, and turn off CanCollide.

Now, put the parts from my script in the same place your model. They will be invisible after you are done.

If you need to see the parts from my script to be able to position them, set beastTile.Transparency to a lower number.

This is the lower part of my script that you need.

local beastGridWidth = 10
local beastGridHeight = 20
local tileSize = 10
local gapSize = 1
local positionOffset = Vector3.new(0,0,0)

local generationFailed = true
while generationFailed == true do
	local beastGrid = createBeastGrid(beastGridWidth, beastGridHeight)

	-- generate the physical grid do
	if beastGrid ~= nil then
		generationFailed = false
		for x = 1, beastGridWidth do
			for y = 1, beastGridHeight do
				local beastTile = Instance.new("Part")
				beastTile.Size = Vector3.new(tileSize-gapSize,0.5,tileSize-gapSize)
				beastTile.Position = Vector3.new(x*tileSize,0,y*tileSize) + positionOffset
				beastTile.Transparency = 1

				if beastGrid[x][y] == 0 then
					beastTile:AddTag("KillBlock")
				end
				beastTile.Anchored = true
				beastTile.Parent = workspace
			end
		end
	end
end
1 Like

But the thing is i dont want the part to be created as I would create the grid manually and then I would get a script to do go through all the parts in the script and choose random ones to make a path.

1 Like

Pretty sure this will work. The procedure is almost the same. There is an optional tag filter, if you think other parts might get in the way during generation.
My tiles are now rays, but you still need something to look at for positioning. That is why the for positioning only! section is there. Those parts will actually get in the way if you remove the optional tag filter (Edit: Because the preview tiles are made after the ray is made, they won’t actually get in the way.), so delete that whole section when you are done (up to and including beastTile.Parent).

local beastGridWidth = 10
local beastGridHeight = 20
local tileSize = 10
local positionOffset = Vector3.new(0,0,0)

-- optional tag filter
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Include
for _, part in pairs(game.Workspace:GetChildren()) do
	if part:HasTag("BeastTile") then
		params:AddToFilter(part)
	end
end

local rayLength = 5
local generationFailed = true
while generationFailed == true do
	local beastGrid = createBeastGrid(beastGridWidth, beastGridHeight)

	-- generate the physical grid do
	if beastGrid ~= nil then
		generationFailed = false
		for x = 1, beastGridWidth do
			for y = 1, beastGridHeight do
				local rayPosition = Vector3.new(x*tileSize,0,y*tileSize) + positionOffset
				-- if you remove the tag filter, remove "params" below as well.
				local raycastResult = workspace:Raycast(rayPosition,Vector3.new(0,-rayLength,0),params)
				if raycastResult ~= nil then
					if beastGrid[x][y] == 0 then
						raycastResult.Instance:AddTag("KillBlock")
					end
				else
					print("A tile was missed. Is the beast grid position correct?")
				end
				
				-- for positioning only!
				local beastTile = Instance.new("Part")
				beastTile.Size = Vector3.new(tileSize-1,0.5,tileSize-1)
				beastTile.Position = rayPosition + Vector3.new(0,-rayLength*0.5,0)
				beastTile.Anchored = true
				beastTile.Parent = workspace
				
			end
		end
	end
end

Edit: missed a minus for beastTile.Position. It was actually the minus at local raycastResult that should have been removed. Now the rays actually point down. Also added a nil check for raycastResult.
Edit 2: Added a message that will allow you to you detect any missed tiles.
Edit 3: Just to clarify.

  1. Once you import your models into studio, anchor them, and position them. you will need one for each tile.
  2. Now, put the parts from my script in the same place your model. Tag your model parts as BeastTile if you are using the optional tag filter.
  3. Erase the for positioning only! section from the script.

Now, just don’t forget the kill block script in script in server script service, and your good.
Edit 4: Negative Y is down. Referance’s to ray length should have been negative. I fixed it now. (it took me a while to realize this.)

2 Likes