Minesweeper Optimization Code Review

Hey all! I’ve been working on a multiplayer minesweeper battling game, based on the old computer game. It has been coming across nicely, but there are a few changes I think might need to be implemented in order to prevent issues in the future.

What does my code do?
The section of code I’m sharing below manages the communication of the pieces on the game board. This means that it will check the surrounding tiles for mines, and display a number for how many mines are around it. If there are 0 mines around the tile, it will tell its neighbors to automatically clear, which creates this really cool expanding effect, that also generates a unique board every time:
Roblox VR 2022.03.01 - 00.40.27.10.DVR_Trim

It’s completely random, and has changable values for the amount of mines, allowed time, etc.

The one thing it doesn’t have the capability of, being able to set a certain game board length/width. For example: a 10x10 board, all the way to a huge 100x100 board. This is because of the way I have defined each pieces neighbors.

Example code for piece a1:

local arow = game.Workspace.real.arow

-- arow
local a1ne = {brow.b1, brow.b2, arow.a2} -- the neighbors of a1, referencing game objects

arow.a1:FindFirstChild("isperma"):GetPropertyChangedSignal("Value"):Connect(function() -- check if already defined 
	wait(0.000001) -- this is bad, finding a solution currently 
	if cleartosearch == true then
		if arow.a1:FindFirstChild("isperma").Value == true then 
			if arow.a1:FindFirstChild("status").Value == "0" then -- aka, its not a mine
				print('checking for bombs')
				for index, neigh in pairs(a1ne) do -- the table assigned with a1's neighbors 
					if neigh:FindFirstChild('status').Value == "1" then
						if finalbomba1.Value == false then -- value object within a1
							amountbombsa1.Value = amountbombsa1.Value + 1 -- other value object within a1
							arow.a1.SurfaceGui.TextLabel.Text = tostring(amountbombsa1.Value) -- cosmetic stuff
							arow.a1.SurfaceGui.Enabled = true
						end
					end
				end
				finalbomba1.Value = true
				if amountbombsa1.Value == 0 then -- it has no bombs around it
					for i, v in pairs(a1ne) do
						if v:FindFirstChild('isperma').Value == false then -- make all the neighbors around it cleared
							v.BrickColor = BrickColor.new('Shamrock')
							v.isperm.Value = true
							v.status.Value = "0"
							end
						end
					end
				end
			end
		end
	end
end)

Now this code runs smoothly (other than that gross wait), but it has an issue. It’s a pain to expand the game board, and there is no potential for board customization. In order to implement each neighbor I have to manually write out the tiles surrounding the piece, and then copy and paste this function with each of the pieces! I thought of using script.Parent with an induvial script for each of the pieces, but it doesn’t solve the neighbor problem that way.

End Goal
I want to find some way to make checking the neighbors of each tile a process that can be used even with a changing board. I want to give the player the ability to create board that can be from 5x5 all the way to 128x128. I currently can’t do this because I haven’t found a way to create a table for each pieces neighbors, other than manually defining it. I tried thinking of putting tables into each piece as an object that we can use FindFirstChild to reference, but that doesn’t seem possible.

Any help would be appreciated!

First of all, you should use task.wait(n) instead of wait(n)

He already mentioned its a temp solution so doesnt really matter
For OP:

What are you trying to solve by having that wait?
The min wait time is 0.03, you adding all those 0 doesnt do anything.

If you want the shortest wait possible you should use

game:GetService(“RunService”).RenderStepped:Wait()

Also, you should use an “and” instead of having multiple if statements one after the other

I wrote this after grinding university from 8am to midnight and I am extremely tired so I apologize for any mistake lolol. I also instinctively used the Canadian spelling for “neighbor” so feel free to change that back.

You would probably have to make use of a two-dimensional array to represent the tiles of a board. Something like this:

-- representing a 4x4 board
local tiles = {
    {a1, a2, a3, a4},
    {b1, b2, b3, b4},
    {c1, c2, c3, c4},
    {d1, d2, d3, d4}
}

That is the fundamental concept to making this work. There are multiple ways you can implement this so here’s my attempt at trying to explain the basics as simply as possible in my fatigued state. You might get confused at my messy post if you try to take it in all at once, so just use the advice here as general pointers, since it’ll be up to you to piece all this code together due to your game having its own specifications.

Set up array

Assuming you want to (and can) automatically generate a physical grid of side length n as seen in your post, you’ll need to iterate n times for each row then n times for each column, adding a new reference to each corresponding tile into the array.

The grid model that you just generated should have folders for each row with their names corresponding to their row number (you’ll have to create a function that converts a number into a letter for display, but that’s not within the scope of this post), and each folder contains the tiles in that row, each named after their corresponding column number:

local grid = makeGrid() -- generate a grid model as described above
local tiles = {}
for r = 1, n do
    tiles[r] = {} -- new row
    for c = 1, n do
        tiles[r][c] = grid[r][c] -- this works because you numbered each folder (tostring is not needed) 
    end
end

Great! We now have a physical board and a backend representation of this board. However, the tiles are left without knowing what their neighbouring tiles are. Luckily, our new, fancy array can help us.

Find neighbours

We want a function that returns an array with all neighbours surrounding a point on the grid:

function findNeighbours(...)
    -- "..." in this case is the same as typing "tiles, row, column"; I just use it here to avoid repetition
    local left = getLeftNeighbour(...)
    local right = getRightNeighbour(...)
    local up  = getUpNeighbour(...)
    local down = getDownNeighbour(...)
    return removeNil({left, right, up, down})
end

Cool, but now we have more functions we need to implement. I won’t show you how to implement removeNil, since you can do that yourself. Let’s get to it:

function getLeftNeighbour(tiles, row, column)
    if column ~= 1 then -- check if there is a tile to the left
        return tiles[row][column - 1]
    end
    return nil -- tile is left-most tile, so it has no neighbour to the left
end

function getRightNeighbour(tiles, row, column)
    if column ~= #tile[1] then -- if it's a square then using just #tile would be fine
        return tiles[row][column + 1]
    end
    return nil
end

function getUpNeighbour(tiles, row, column)
    if row ~= 1 then
        return tiles[row - 1][column]
    end
    return nil
end

function getDownNeighbour(tiles, row, column)
    if row ~= #tile then
        return tiles[row + 1][column]
    end
    return nil
end

Voilà! Those return nil statements aren’t actually necessary in Lua, but I left them in so you can more clearly see what happens when the condition above fails.

Assign neighbours to each tile

We can finally go back to our for loops. We need to re-iterate over the entire tiles array now that it’s complete and assign each tile its neighbours:

local grid = makeGrid() -- generate a grid model as described above
local tiles = {}
for r = 1, n do
    tiles[r] = {} -- new row
    for c = 1, n do
        tiles[r][c] = grid[r][c] -- this works because you numbered each folder (tostring is not needed) 
    end
end

for r, row in ipairs(tiles) do
    for c, tile in ipairs(row) do
        setNeighbours(tile, findNeighbours(tiles, r, c))
    end
end

Lets write setNeighbours:

function setNeighbours(tile, neighbours)
    tile:FindFirstChild("isperma"):GetPropertyChangedSignal("Value"):Connect(function()
        -- all the code that you wrote in your post, with "neighbours" replacing "a1ne"
    end)
end

Edits: a few minor changes and added tiles[r] = {} -- new row which was missing in the for loops

2 Likes

So far your work has been very helpful! I’ve been able to use 2d arrays to create a function that returns neighbors, no matter how big I make the generated board!

One thing that I am still struggling with is how to automatically clear the pieces. By this I mean I want to have my game check if the clicked on tile’s neighbors don’t have bombs, and if they don’t then automatically clear them, and continuing to clear up until we start to hit edges with mines (as show with in my previous gif).

I previously completed this by changing a value within the piece when it got cleared, which would automatically fire a function that would also check its neighbors, causing that chain reaction.

This is what I’ve got so far. In my input function I can check if the neighbors are clear around the selected piece using this code:

local rowneighbors = findNeighbours(tiles, rowselected, column) -- returns table with neighbors sweet
		if finishedcheck == false then
			for i,v in pairs(rowneighbors) do
				if v.status.Value == true then -- if there is a bomb around this piece
					print('bomb around')
					finishedcheck = true
				else
					if v.isperm.Value == false then -- there is no bomb around this piece
						v.BrickColor = BrickColor.new("Cyan")
						v.isperm.Value = true
						v.status.Value = false -- make it cleared
					end
				end
			end
		end
		print('done')

Here is an image of what that does:

Now this works well, but I’m not quite sure what approach I should take to having this loop for EACH of the neighbors that ALSO have no mines around. Basically I’ve been able to check the selected tiles neighbors and clear them, but I want to check those neighbors, neighbors. That is a huge mouthful. If you have any idea how I could complete this, lemme know. Appreciate your response!

2 Likes

Coincidently enough I just recently made a minesweeper game while messing around.

In short for the issue you are currently dealing with I just have a value within the 2d array that lets me know whether an item has been identified.
With that I just begin a recursive search through the table of the clicked on tile’s neighbors. If that neighbor has not been identified then we run the same search through its neighbors and mark them all identified as we pass through them. We also don’t look through the neighbors of tiles neighboring bombs, instead we just identify them.

If you wanna check it out I’ll leave this here. Little self plug.

1 Like