Puzzle Flow Algorithm

Hi there,

  1. What do you want to achieve? I want to create a puzzle flow algorithm in Lua, which can be used to solve puzzles like flow-based games where you connect colored dots with lines, ensuring that no lines overlap, and all dots are connected.
  2. What is the issue? I have never created such an algorithm before and don’t know where to start. I’m looking for guidance on how to approach this problem in Lua.
  3. What solutions have you tried so far? I have attempted to search for solutions and resources on the internet, including the Developer Hub, but I couldn’t find anything that specifically addresses my case or provides a step-by-step guide on how to create a puzzle flow algorithm in Lua.

Heres the code that I am working with currently, Its a piece of code I found on the internet while looking for this exact problem. Only problem with this code is I do not have any idea how the data they use is generated

Script Found on the Internet

local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Color Palette
local ColorPalette = {
	Index = 1,
	BaseColors = {
		Color3.new(1, 0, 0),
		Color3.fromRGB(0, 120, 255),
		Color3.fromRGB(189, 0, 255),
		Color3.fromRGB(255, 154, 0),
		Color3.fromRGB(1, 255, 31),
		Color3.fromRGB(227, 255, 0)
	},
	Reset = function(self)
		self.Index = 1
	end
}

local RandomGenerator = Random.new()

function ColorPalette.NextColor(self)
	if #self.BaseColors <= self.Index then
		return Color3.fromHSV(RandomGenerator:NextNumber(), 1, 1)
	end
	self.Index = self.Index + 1
	return self.BaseColors[self.Index]
end

-- Flow UI
local FlowUI = {
	IsOpen = false,
	Maid = nil,
	OnFilled = nil
}

local function CreateGrid(width, height)
	local grid = {}
	for y = 1, height do
		grid[y] = {}
		for x = 1, width do
			grid[y][x] = 0
		end
	end
	return grid
end

function FlowUI.SetGrid(self, flowId, gridData)
	self.FlowId = flowId
	self.Grid = gridData
	local gridHeight = #gridData
	local gridWidth = #gridData[1]
	self.GridClean = CreateGrid(gridWidth, gridHeight)
	for y = 1, gridHeight do
		for x = 1, gridWidth do
			self.GridClean[y][x] = gridData[y][x]
		end
	end
	ColorPalette:Reset()
	self.Colors = {}
end

function FlowUI.ResetAlpha(self, value)
	assert(value)
	local gridWidth = #self.Grid[1]
	for y = 1, #self.Grid do
		for x = 1, gridWidth do
			if self.Grid[y][x] == value then
				self.Grid[y][x] = self.GridClean[y][x]
			end
		end
	end
end

function FlowUI.Reset(self)
	if self.Grid == nil then
		return false
	end
	local gridWidth = #self.Grid[1]
	for y = 1, #self.Grid do
		for x = 1, gridWidth do
			self.Grid[y][x] = self.GridClean[y][x]
		end
	end
end

FlowUI.Colors = {}

local FlowGui = script.FlowGui

function FlowUI.Draw(self)
	local gridHeight = #self.Grid
	local gridWidth = #self.Grid[1]
	for y = 1, gridHeight do
		for x = 1, gridWidth do
			local cellId = ("%d.%d"):format(x, y)
			local cell = FlowGui.Board.Inner:FindFirstChild(cellId)
			if cell == nil then
				cell = Instance.new("TextLabel")
				cell.Name = cellId
				cell.BorderSizePixel = 0
				cell.BackgroundTransparency = 0
				cell.TextSize = 20
				cell.TextColor3 = Color3.new(0.1, 0.1, 0.1)
				cell.Size = UDim2.new(1 / gridWidth * 0.5, 0, 1 / gridHeight * 0.5, 0)
				cell.AnchorPoint = Vector2.new(0.5, 0.5)
				cell.Position = UDim2.new((x - 1 + 0.5) / gridWidth, 0, (y - 1 + 0.5) / gridHeight, 0)
				cell.Parent = FlowGui.Board.Inner
			end
			local cellValue = self.Grid[y][x]
			if cellValue >= 0 then
				local cellColor = self.Colors[cellValue]
				if cellColor == nil then
					cellColor = ColorPalette:NextColor()
					self.Colors[cellValue] = cellColor
				end
				cell.BackgroundColor3 = cellColor
			else
				cell.BackgroundColor3 = Color3.new(0.1, 0.1, 0.1)
			end
			local textValue = self.Grid[y][x]
			if textValue >= 0 then
				cell.Text = tostring(textValue)
			else
				cell.Text = ""
			end
		end
	end
end

function FlowUI.GetCoordFromMouse(self, mousePosition)
	local boardAbsolutePosition = FlowGui.Board.AbsolutePosition
	local boardAbsoluteSize = FlowGui.Board.AbsoluteSize
	local x = math.floor(#self.Grid[1] * (mousePosition.X - boardAbsolutePosition.X) / boardAbsoluteSize.X) + 1
	local y = math.floor(#self.Grid * (mousePosition.Y - boardAbsolutePosition.Y) / boardAbsoluteSize.Y) + 1
	local value = self.Grid[y][x]
	return x, y, value
end

local LocalPlayer = game:GetService("Players").LocalPlayer
local Maid = require(ReplicatedStorage.Maid)
local DirectionX = {0, 1, 0, -1}
local DirectionY = {-1, 0, 1, 0}
local UserInputService = game:GetService("UserInputService")

function FlowUI.Show(flowUI)
	assert(flowUI.IsOpen == false)
	flowUI.IsOpen = true
	FlowGui.Parent = LocalPlayer.PlayerGui
	flowUI.Maid = Maid.new()

	local startX, startY, currentColor, startColor, endColor, drawing = nil, nil, nil, nil, nil, false

	print(flowUI)
	flowUI.Maid:GiveTask(FlowGui.Board.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			local x, y, cellValue = flowUI:GetCoordFromMouse(input.Position)
			local grid = flowUI.Grid
			local valid = x >= 1 and x <= #grid[1] and y >= 1 and y <= #grid and cellValue >= 0

			if not valid then
				return
			end

			startX, startY, currentColor = x, y, cellValue
			startColor, endColor = startX, startY
			flowUI:ResetAlpha(currentColor)
			flowUI:Draw()
			drawing = true
		end
	end))

	local function OnInputEnded(input)
		if not drawing then
			return false
		end
		drawing = false

		if startX == endColor and startY == startColor then
			flowUI:ResetAlpha(currentColor)
		else
			local isValid = false
			for i = 1, 4 do
				local newX, newY = startX + DirectionX[i], startY + DirectionY[i]
				local grid = flowUI.Grid

				local valid = newX >= 1 and newX <= #grid[1] and newY >= 1 and newY <= #grid
				if valid and flowUI.GridClean[newY][newX] == currentColor and (newX ~= endColor or newY ~= startColor) then
					isValid = true
				end
			end

			if not isValid then
				flowUI:ResetAlpha(currentColor)
			else
				if not flowUI.OnConnection then
					error("No OnConnection function set!")
				end
				flowUI.OnConnection()
			end
		end
		flowUI:Draw()
	end

	flowUI.Maid:GiveTask(FlowGui.Board.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			OnInputEnded(input.Position)
		end
	end))

	local function OnInputChanged(input)
		if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
			local x, y, cellValue = flowUI:GetCoordFromMouse(input.Position)
			if x == startX + 1 then
				local valid = y == startY
				if x == startX - 1 then
					valid = valid or y == startY
					if x == startX then
						valid = valid or y == startY + 1
						valid = valid or y == startY - 1
					end
				elseif x == startX then
					valid = valid or y == startY + 1
					valid = valid or y == startY - 1
				end

				if not valid then
					return false
				end

				if x ~= startX or y ~= startY then
					flowUI.Grid[y][x] = currentColor
					startX, startY = x, y
					flowUI:Draw()
				end
			end
		end
	end

	flowUI.Maid:GiveTask(FlowGui.Board.InputChanged:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
			OnInputChanged(input)
		end
	end
	)
	)

	flowUI.Maid:GiveTask(FlowGui.Board.Reset.MouseButton1Down:Connect(function()
		flowUI:Reset()
		flowUI:Draw()
	end
	)
	)

	flowUI.Maid:GiveTask(
		FlowGui.Board.Cancel.MouseButton1Down:Connect(
			function()
				flowUI:Hide()
			end
		)
	)

	FlowUI:Draw()

	if UserInputService:GetLastInputType() == Enum.UserInputType.Gamepad1 then
		local Humanoid = LocalPlayer.Character:WaitForChild("Humanoid")
		flowUI.Maid:GiveTask(
			Humanoid.Changed:connect(
				function()
					Humanoid.Jump = false
				end
			)
		)

		local cursorX, cursorY, cursorLabel = 1, 1, nil

		local function UpdateCursor()
			local gridHeight, gridWidth = #flowUI.Grid, #flowUI.Grid[1]

			if cursorX < 1 then
				cursorX = 1
			end
			if gridWidth < cursorX then
				cursorX = gridWidth
			end

			if cursorY < 1 then
				cursorY = 1
			end
			if gridHeight < cursorY then
				cursorY = gridHeight
			end

			if cursorLabel then
				cursorLabel.Size = UDim2.new(1 / gridWidth * 0.5, 0, 1 / gridHeight * 0.5, 0)
			end

			local cell = FlowGui.Board.Inner:FindFirstChild((("%d.%d"):format(cursorX, cursorY)))
			assert(cell)
			cell.Size = UDim2.new(1 / gridWidth * 0.75, 0, 1 / gridHeight * 0.75, 0)

			OnInputChanged(cell.AbsolutePosition + cell.AbsoluteSize * 0.5)
		end

		flowUI.Maid:GiveTask(
			UserInputService.InputBegan:Connect(
				function(input)
					if input.UserInputType == Enum.UserInputType.Gamepad1 then
						if input.KeyCode == Enum.KeyCode.DPadUp then
							cursorY = cursorY - 1
							UpdateCursor()
							return
						end
						if input.KeyCode == Enum.KeyCode.DPadDown then
							cursorY = cursorY + 1
							UpdateCursor()
							return
						end
						if input.KeyCode == Enum.KeyCode.DPadLeft then
							cursorX = cursorX - 1
							UpdateCursor()
							return
						end
						if input.KeyCode == Enum.KeyCode.DPadRight then
							cursorX = cursorX + 1
							UpdateCursor()
							return
						end
						if input.KeyCode == Enum.KeyCode.ButtonA then
							local cell = FlowGui.Board.Inner:FindFirstChild((("%d.%d"):format(cursorX, cursorY)))
							local x, y, cellValue = flowUI:GetCoordFromMouse(cell.AbsolutePosition + cell.AbsoluteSize * 0.5)
							local grid = flowUI.Grid
							local valid = x >= 1 and x <= #grid[1] and y >= 1 and y <= #grid

							if not valid then
								return
							end

							if cellValue < 0 then
								return
							end

							startX, startY, currentColor = x, y, cellValue
							startColor, endColor = startX, startY
							flowUI:ResetAlpha(currentColor)
							flowUI:Draw()
							drawing = true
						end
					end
				end
			)
		)

		flowUI.Maid:GiveTask(
			UserInputService.InputEnded:Connect(
				function(input)
					if input.UserInputType == Enum.UserInputType.Gamepad1 and input.KeyCode == Enum.KeyCode.ButtonA then
						local cell = FlowGui.Board.Inner:FindFirstChild((("%d.%d"):format(cursorX, cursorY)))
						OnInputEnded(cell.AbsolutePosition + cell.AbsoluteSize * 0.5)
					end
				end
			)
		)

		UpdateCursor()
	end
end

local eventHandler = nil
local eventManager = nil

return {
	Init = function(params)
		eventHandler = params.Event
		eventManager = params.em

		function FlowUI.OnConnection()
			print("Connection established.")
			eventHandler:FireServer("m5z4m5of", FlowUI.FlowId, FlowUI.Grid)
		end

		function eventManager.ShowGrid(p24, p25)
			FlowUI:SetGrid(p24, p25)
			FlowUI:Show(FlowUI)
		end

		function eventManager.HideGrid(p26)
			if FlowUI.IsOpen and (p26 == nil or FlowUI.FlowId == p26) then
				FlowUI:Reset()
				FlowUI:Hide()
			end
		end

		return eventManager
	end
}

Maid Script (REQUIRED)


local Maid = {}  -- Create a table named 'Maid'
local TaskManager  -- Declare a local variable 'TaskManager' to be defined later

do
	-- Define a set of utility functions in a table
	TaskManager = {
		GiveTask = function(maid, task)
			local taskCount = #maid.Tasks + 1  -- Calculate the index for the new task
			maid.Tasks[taskCount] = task  -- Add the task to the Maid's list
			return taskCount  -- Return the index of the task
		end,
		DoCleaning = function(maid)
			local tasks = maid.Tasks
			for index, task in pairs(tasks) do
				if type(task) == "function" then
					task()  -- Execute the task if it's a function
				elseif typeof(task) == "RBXScriptConnection" then
					task:disconnect()  -- Disconnect the connection if it's an RBXScriptConnection
				else
					task:Destroy()  -- Destroy the task object if it's neither a function nor RBXScriptConnection
				end
				tasks[index] = nil  -- Remove the task from the list
			end
		end
	}

	-- Alias Destroy function as DoCleaning
	TaskManager.Destroy = TaskManager.DoCleaning

	-- Create a metatable for the Maid
	local MaidMetatable = {
		__index = function(maid, key)
			if TaskManager[key] then
				return TaskManager[key]  -- Use TaskManager functions if available
			else
				return maid.Tasks[key]  -- Otherwise, access tasks by key
			end
		end,
		__newindex = function(maid, key, value)
			local tasks = maid.Tasks
			if value == nil then
				if type(tasks[key]) ~= "function" and tasks[key] then
					if typeof(tasks[key]) == "RBXScriptConnection" then
						tasks[key]:disconnect()  -- Disconnect an RBXScriptConnection if it exists
					else
						tasks[key]:Destroy()  -- Destroy the task object if it's not a function or RBXScriptConnection
					end
				end
			else
				tasks[key] = value  -- Assign a new task to the specified key
			end
		end
	}

	-- Define a function to create a new Maid object
	function TaskManager.new()
		return setmetatable({
			Tasks = {},  -- Initialize an empty table for tasks
			Instances = {}  -- Initialize an empty table for instances (not used in the provided code)
		}, MaidMetatable)
	end
end

-- Alias MakeMaid and new functions to Maid
Maid.MakeMaid = TaskManager.new
Maid.new = TaskManager.new

return Maid  -- Return the Maid table as the module

How it should look, again this is my failed attempt at trying to do this

The GUI Structure is looking like this screenshot below, if u are a scripter u can guess how the UI is looking after the Puzzle Flow Initialized

image

Model of the UI

puzzle.rbxm (13.9 KB)

Additional Details: I’m specifically interested in understanding the logic and steps required to implement a puzzle flow algorithm in Lua. I’m not asking for a complete script or system design but rather seeking guidance on how to approach this problem and where to start my coding process.

1 Like

This is just a bump reply because I am really desperate

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.