Pipe Connection Puzzle

I’ve been trying to make a system where you connect wires to the end and it is randomized every time you attempt to solve.

I have the grid system solved, but I can’t figure out how to generate a path and create the path visually with the connecting working.

Example from FNAF: Glitched Attraction


Please, give snippets of code when you are trying to help or give advice. It would really help me if you could.

Edit: Fixed category SORRY!!

Depends if this is instance-based or using UI elements

Assuming instances:

  • Identify the basic characteristics of each cell. I’d also probably mark them down by name or attribute numbers 1-#cells starting at top right and cycling down to the right corner.
    • it would be wise to simultaneously mark down the type of cell this is, probably a tag (AddTag())
  • Know the starting and end cells. In this case, its cell at the start of the 4th and end of the 4th rows.

Things that MUST be true:

  • All cells must be tagged “Straight” or “Corner”.
  • All cells are instances here, and their name is an integer 1-#rows*15
  • Cell orientation 0,0,0 means a straight cell is vertical [|], and 0,180,0 means a straight cell is horizontal [-].
    • Note this can be adjusted as you wish, but this must be known for my calculations.
  • Cell orientation 0,0,0 means a corner cell is in the shape [┗]. Cell orientation 0,180,0 means corner cell is in the shape [┒]
    • Note this can be adjusted as you wish, but this must be known for my calculations.
local Cells = {}
for _, Cell in something:GetChildren() do
  Cells[tonumber(Cell.Name)] = Cell
end

Great, so now we have all the information we will need. A final reference that will streamline this process is knowing the possible routes of each cell.

  • I.e. straight cells travel straight vertical, corners can hit SideA and the side adjacent to it right or left

Now, it doesn’t really matter what rotation the parts are at when the puzzle starts, because they can be rotated 4x each regardless. To save some time, we will handle vertical lines individually because they only have 2x possible pathways.

local StartCell = Cells[15*3 + 1] -- just to show where i got the key 45 from, row 4 position 1]
local Solution = {} -- we will store solutions by {IndexOfCellOnBoard, OrientationThatIsTrue}
if StartCell:HasTag("Straight") then
      Solution[1] = {46, Vector3.yAxis*180)} -- must be horizontal, at cell 46. (yAxis = Vector3.new(0,1,0)*180 = our desired orientation
elseif StartCell:HasTag("Corner") then -- could also be else, but for readability
      Solution[1] = {46, math.random(1,2) == 1 and Vector3.yAxis*90 or Vector3.yAxis*180}
end

So in this segment above, we can see that we are making the 1st solution known. After all, this is necessary for us to be able to make the next remaining steps. If the start is a corner, the orientation can be 2 different solutions.

We never addressed the other requirement; an endpoint. We don’t KNOW when the endcell will be reached, but it will be at most rows15 cells away. So, we will assign 10e3 (1000) because we just need a number higher than rows15, and its easy to write/remember

local EndCell = Cells[15*4] -- end cell
if EndCell:HasTag("Straight") then
      Solution[10e3] = {46, Vector3.yAxis*180} -- must be horizontal, at cell 60. (yAxis = Vector3.new(0,1,0)*180 = our desired orientation
elseif EndCell:HasTag("Corner") then -- could also be else, but for readability
      Solution[10e3] = {46, math.random(1,2) == 1 and Vector3.yAxis*-90 or Vector3.zero}
end

Now, the hard part. We can manipulate a path in many ways, but the most important part is knowing when to stop. We already know the endpoint, so we will mark ‘Solved’ when our puzzle is at Cells 59, 45, or 75 [The 3 neighboring cells to cell 60]. However, it can only be solved if the endpoint is aligned with the cell aswell.

First we need a way to find neighbors of a cell. I won’t explain much but its purely mathematical, finding if top/bottom/left/right exist and inserting in a table.

local Solve = function()
 local Batch = 0
 local Neighbors = function(index)
    local neighbors = {}
    local rows = math.ceil(15*7 / 15)

    if (index - 1) % 15~= 0 then -- if index -1 is a different row, its not a neighbor
        table.insert(neighbors, index - 1)
    end

    if index % 15~= 0 and index + 1 <= 15*7 then -- if index +1 is a different row, then its not a neighbor
        table.insert(neighbors, index + 1)
    end

    if index - 15 > 0 then -- also need to make sure that the index Exists in a row below, like at spot 1 there is no spot 0
        table.insert(neighbors, index - 15)
    end

    if index + 15 <= 15*7 then -- same for maximums
        table.insert(neighbors, index + 15)
    end

    return neighbors
end

Okay, so we now have a function to identify neighbors, we will just need some more work to apply it;

    local totalCells = 15 * 7
    local Last = {46, Solution[1][2]} -- index + orientation
    local Solved = false

    repeat
        local lastIndex = Last[1]

        -- check if at the end 
        local nearEnd = Neighbors(60)
        if table.find(nearEnd, lastIndex) then
            Solved = true
            break
        end

        -- search neighbors of Last
        local possibleNeighbors = Neighbors(lastIndex)
        local foundNext = false

        for _, i in possibleNeighbors do
            local cell = Cells[i]
            if not cell then continue end

            -- decide next orientation
            local DesiredOrientation

            if cell:HasTag("Straight") then
                -- continue straight if same column or row
                if i == lastIndex + 15 or i == lastIndex - 15 then
                    DesiredOrientation = Vector3.new(0,0,0) -- vertical
                else
                    DesiredOrientation = Vector3.new(0,180,0) -- horizontal
                end
            elseif cell:HasTag("Corner") then
                -- pick one of two turns arbitrarily for now
                DesiredOrientation = math.random(1,2) == 1 and Vector3.new(0,90,0) or Vector3.new(0,270,0)
            end

            -- store solution
            table.insert(Solution, {i, DesiredOrientation})
            Last = {i, DesiredOrientation}
            foundNext = true
            break
        end

        if not foundNext then
            break
        end

        Batch += 1
        if Batch % 5 == 0 then task.wait() end

    until Solved
end

Okay, so now the solutions table is filled. From here on out, you might want to continuously loop and see if a parts orientation matches a solution, then color it green etc.

May have some issues, let me know [UNTESTED]

Let me know about any questions.

3 Likes

Sorry if this is more work, but how could I go about merging these snippets together? Another thing, I’m aiming for this to be a UI grid.

I forgot to include, the grid system I made, the code for it is:

		local gridSize = 5
		--minigameFrame.Holder.UIGridLayout.CellSize = UDim2.new(0.25 / gridSize, 0, 0.25 / gridSize, 0)
		
		for y=1, gridSize do
			
			for x=1, gridSize do
				
				local frame = Instance.new("Frame", minigameFrame.Holder)
				frame.AnchorPoint = Vector2.new(0, 0)
				frame.Size = UDim2.new(1 / gridSize, 0, 1 / gridSize, 0)
				
				frame.Position = UDim2.new(0 + ((1 / gridSize) * (x - 1)), 0, 0 + ((1 / gridSize) * (y - 1)), 0)
				
			end
			
		end

Again, sorry for more work, it’s just very confusing for me.

This is what the grid looks like, I can change the grid sizing aswell.

Its just continuous, so every part just connects to the next

if its UI, then all rotation is a single integer so all yAxis*num would just become the rotation angle. [any reference to .Rotation must be modified as such, CTRL+F] Other than that, I believe it should work all the same?

would the generation connect the parts together then I would randomize the rotations?

First off, you should randomly apply the images to each UI, I’d probably adjust the rates a bit because you will likely want more corners than lines (since lines have very little possibility for a good path, unless straight)
Then randomize ALL frames to 0, 90, 180, -90. For lines, this will appear as 2 possibilities but for corners this allows the correct motions. If you wanna jump ahead, I’d add a textbutton and when its clicked change the rotation to the next interval: 0->90, 90->180, 180->270, 270->360 (same as 180, -90).

After that, run the script I sent (should work fine, lmk if not)

alr, ill try. i might just need help merging the script snippets u gave.

for the snippet at the top where u create the “cells” table, should I just loop through the tiles already created and add their x:y in the table?

local Cells = {}
for _, Cell in something:GetChildren() do
  Cells[tonumber(Cell.Name)] = Cell -- When you created the cells, add a bit that names them in their order Left->Right
end

local StartCell = Cells[15*3 + 1] -- just to show where i got the key 45 from, row 4 position 1]
local Solution = {} -- we will store solutions by {IndexOfCellOnBoard, OrientationThatIsTrue}
if StartCell:HasTag("Straight") then
      Solution[1] = {46, 180)} -- must be horizontal, at cell 46.
elseif StartCell:HasTag("Corner") then -- could also be else, but for readability
      Solution[1] = {46, math.random(1,2) == 1 and 90 or 180}
end

local EndCell = Cells[15*4] -- end cell
if EndCell:HasTag("Straight") then
      Solution[10e3] = {46, 180} -- must be horizontal, at cell 60. 
elseif EndCell:HasTag("Corner") then -- could also be else, but for readability
      Solution[10e3] = {46, math.random(1,2) == 1 and -90 or 0}
end

local Solve = function()
 local Batch = 0
 local Neighbors = function(index)
    local neighbors = {}
    local rows = math.ceil(15*7 / 15)

    if (index - 1) % 15~= 0 then -- if index -1 is a different row, its not a neighbor
        table.insert(neighbors, index - 1)
    end

    if index % 15~= 0 and index + 1 <= 15*7 then -- if index +1 is a different row, then its not a neighbor
        table.insert(neighbors, index + 1)
    end

    if index - 15 > 0 then -- also need to make sure that the index Exists in a row below, like at spot 1 there is no spot 0
        table.insert(neighbors, index - 15)
    end

    if index + 15 <= 15*7 then -- same for maximums
        table.insert(neighbors, index + 15)
    end

    return neighbors
end
local totalCells = 15 * 7
    local Last = {46, Solution[1][2]} -- index + orientation
    local Solved = false

    repeat
        local lastIndex = Last[1]

        -- check if at the end 
        local nearEnd = Neighbors(60)
        if table.find(nearEnd, lastIndex) then
            Solved = true
            break
        end

        -- search neighbors of Last
        local possibleNeighbors = Neighbors(lastIndex)
        local foundNext = false

        for _, i in possibleNeighbors do
            local cell = Cells[i]
            if not cell then continue end

            -- decide next orientation
            local DesiredOrientation

            if cell:HasTag("Straight") then
                -- continue straight if same column or row
                if i == lastIndex + 15 or i == lastIndex - 15 then
                    DesiredOrientation = 0 -- vertical
                else
                    DesiredOrientation = 180 -- horizontal
                end
            elseif cell:HasTag("Corner") then
                -- pick one of two turns arbitrarily for now
                DesiredOrientation = math.random(1,2) == 1 and 90 or 270
            end

            -- store solution
            table.insert(Solution, {i, DesiredOrientation})
            Last = {i, DesiredOrientation}
            foundNext = true
            break
        end

        if not foundNext then
            break
        end

        Batch += 1
        if Batch % 5 == 0 then task.wait() end

    until Solved
end

Solve()

The table must be formatted {[Cell Name as an integer] = the Cell object}

should I put an image label indicating if its a straight or corner piece here?

When you create the grid, add the imagelabels there. Then, perform

Cell:AddTag("Straight") 

or

Cell:AddTag("Corner")

Depending on the image added. This lets the script know later which cells are corners and which are not.

alright, ill try and get the rest done. ill notify if it works or doesn’t!!

Should the grid be generated row by row, or column by column?

Also, how did u get the cell 46 and cell 60 in the calculations? How can I calculate those grid stuff based on the grid size?

I managed to calculate the start cell and end cell, but I don’t get how u got the 15 values and multiply values like here:[quote=“Squid, post:12, topic:3989244, username:GenericYellowSquid”]
local rows = math.ceil(15*7 / 15)
[/quote]

i got this working:


but i dont know how it solves the puzzle through the function..

Hi,
15, 60, 45, etc were all calculated based on the reference image you sent. At the time, I thought you were mirroring that design.

Replace 15 with ROW LENGTH
Replace 60 with the end cell
Replace 45 with the start cell

Create by rows

The function generates a solution, but it doesn’t show it. Add a print (solution) at the end to view it. The result might not be correct so let me know if it is through testing

1 Like