Made a few compromises to make this a bit easier.
Multiple angle on cylinders is a bit harder than I thought. 
Pipes
--ServerScript in ServerScriptService
--Or LocalScript in a local location
local RESTART = true
local PIPE_COUNT = 8
local GRID_SIZE = 16
local CELL_SIZE = 4
local TURN_PROBABILITY = 0.3
local UPDATE_INTERVAL = 0.05
local RESTART_INTERVAL = 30
local DIRECTIONS = {
Vector3.new(1, 0, 0),
Vector3.new(-1, 0, 0),
Vector3.new(0, 1, 0),
Vector3.new(0, -1, 0),
Vector3.new(0, 0, 1),
Vector3.new(0, 0, -1),
}
local COLORS = {
Color3.fromRGB(31, 119, 180),
Color3.fromRGB(255, 127, 14),
Color3.fromRGB(44, 160, 44),
Color3.fromRGB(214, 39, 40),
Color3.fromRGB(148, 103, 189),
Color3.fromRGB(140, 86, 75),
Color3.fromRGB(227, 119, 194),
Color3.fromRGB(127, 127, 127),
Color3.fromRGB(23, 190, 207),
}
local function makePositionKey(position)
return position.X .. "," .. position.Y .. "," .. position.Z
end
local Pipe = {}
Pipe.__index = Pipe
function Pipe.new(occupiedPositions, color)
local self = setmetatable({}, Pipe)
self.occupied = occupiedPositions
self.color = color
self.parts = {}
self.hasStarted = false
self.isStuck = false
local startPos
local attempts = 0
while true do
startPos = Vector3.new(
math.random(-GRID_SIZE, GRID_SIZE) * CELL_SIZE,
math.random(-GRID_SIZE, GRID_SIZE) * CELL_SIZE,
math.random(-GRID_SIZE, GRID_SIZE) * CELL_SIZE
)
local key = makePositionKey(startPos)
if not occupiedPositions[key] then
occupiedPositions[key] = true
break
end
attempts = attempts + 1
if attempts > 100 then
startPos = Vector3.new(0, 0, 0)
break
end
end
self.position = startPos
self.direction = DIRECTIONS[math.random(1, #DIRECTIONS)]
self:createSphere(startPos)
self.hasStarted = true
return self
end
function Pipe:createSphere(position)
local sphere = Instance.new("Part")
sphere.Name = "PipeSphere"
sphere.Shape = Enum.PartType.Ball
sphere.Size = Vector3.new(1.75, 1.75, 1.75) --2,2,2
sphere.Position = position
sphere.Anchored = true
sphere.CanCollide = false
sphere.Material = Enum.Material.Neon
sphere.Color = self.color
sphere.Parent = workspace
table.insert(self.parts, sphere)
return sphere
end
function Pipe:createBlockSegment(startPos, endPos)
local middle = (startPos + endPos) / 2
local direction = (endPos - startPos).Unit
local length = (startPos - endPos).Magnitude
local segment = Instance.new("Part")
segment.Name = "PipeSegment"
segment.Shape = Enum.PartType.Block
segment.Anchored = true
segment.CanCollide = false
segment.Material = Enum.Material.Neon
segment.Color = self.color
if math.abs(direction.X) > 0.9 then
segment.Size = Vector3.new(length, 1, 1)
elseif math.abs(direction.Y) > 0.9 then
segment.Size = Vector3.new(1, length, 1)
else
segment.Size = Vector3.new(1, 1, length)
end
segment.Position = middle
segment.Parent = workspace
table.insert(self.parts, segment)
return segment
end
function Pipe:findValidDirection()
local directionsToTry = {}
for i = 1, #DIRECTIONS do
directionsToTry[i] = DIRECTIONS[i]
end
for i = #directionsToTry, 2, -1 do
local j = math.random(1, i)
directionsToTry[i], directionsToTry[j] = directionsToTry[j], directionsToTry[i]
end
if math.random() > TURN_PROBABILITY then
for i = 1, #directionsToTry do
if directionsToTry[i] == self.direction then
table.remove(directionsToTry, i)
break
end
end
table.insert(directionsToTry, 1, self.direction)
end
for i = 1, #directionsToTry do
local direction = directionsToTry[i]
local newPos = self.position + (direction * CELL_SIZE)
if math.abs(newPos.X) > GRID_SIZE * CELL_SIZE then
elseif math.abs(newPos.Y) > GRID_SIZE * CELL_SIZE then
elseif math.abs(newPos.Z) > GRID_SIZE * CELL_SIZE then
else
local key = makePositionKey(newPos)
if not self.occupied[key] then
return direction, newPos
end
end
end
return nil, nil
end
function Pipe:update()
if self.isStuck then
return false
end
local direction, newPos = self:findValidDirection()
if direction and newPos then
local oldPos = self.position
local key = makePositionKey(newPos)
self.occupied[key] = true
self:createBlockSegment(oldPos, newPos)
if direction ~= self.direction then
self:createSphere(oldPos)
end
self.position = newPos
self.direction = direction
return true
else
if RESTART then
self:createSphere(self.position)
self:restart()
return true
else
self:createSphere(self.position)
self.isStuck = true
return false
end
end
end
function Pipe:restart()
for i = 1, #self.parts do
if self.parts[i] and self.parts[i].Parent then
self.parts[i]:Destroy()
end
end
self.parts = {}
self.isStuck = false
local newPos
local attempts = 0
while true do
newPos = Vector3.new(
math.random(-GRID_SIZE, GRID_SIZE) * CELL_SIZE,
math.random(-GRID_SIZE, GRID_SIZE) * CELL_SIZE,
math.random(-GRID_SIZE, GRID_SIZE) * CELL_SIZE
)
local key = makePositionKey(newPos)
if not self.occupied[key] then
self.occupied[key] = true
break
end
attempts = attempts + 1
if attempts > 100 then
newPos = Vector3.new(0, 0, 0)
break
end
end
self.position = newPos
self.direction = DIRECTIONS[math.random(1, #DIRECTIONS)]
self:createSphere(newPos)
end
function Pipe:cleanup()
for i = 1, #self.parts do
if self.parts[i] and self.parts[i].Parent then
self.parts[i]:Destroy()
end
end
self.parts = {}
self.isStuck = false
end
local PipeSystem = {}
PipeSystem.__index = PipeSystem
function PipeSystem.new()
local self = setmetatable({}, PipeSystem)
self.occupiedPositions = {}
self.pipes = {}
self.lastUpdate = tick()
self.lastRestart = tick()
self.isRunning = true
self.stuckCount = 0
self.totalPipes = PIPE_COUNT
self.folder = Instance.new("Folder")
self.folder.Name = "PipeSystem"
self.folder.Parent = workspace
for i = 1, PIPE_COUNT do
local colorIndex = ((i - 1) % #COLORS) + 1
local pipe = Pipe.new(self.occupiedPositions, COLORS[colorIndex])
table.insert(self.pipes, pipe)
end
return self
end
function PipeSystem:updateAll()
local activePipes = 0
for i = 1, #self.pipes do
local pipe = self.pipes[i]
if pipe:update() then
activePipes = activePipes + 1
end
end
if not RESTART then
local stuckPipes = 0
for i = 1, #self.pipes do
if self.pipes[i].isStuck then
stuckPipes = stuckPipes + 1
end
end
if stuckPipes == #self.pipes then
self.isRunning = false
end
end
return activePipes
end
function PipeSystem:restartAll()
if not RESTART then
return
end
for i = 1, #self.pipes do
self.pipes[i]:cleanup()
end
self.occupiedPositions = {}
self.pipes = {}
for i = 1, PIPE_COUNT do
local colorIndex = ((i - 1) % #COLORS) + 1
local pipe = Pipe.new(self.occupiedPositions, COLORS[colorIndex])
table.insert(self.pipes, pipe)
end
self.stuckCount = 0
end
local system = PipeSystem.new()
local RunService = game:GetService("RunService")
local connection
connection = RunService.Heartbeat:Connect(function()
local currentTime = tick()
if not system.isRunning and not RESTART then
return
end
if RESTART then
if currentTime - system.lastRestart >= RESTART_INTERVAL then
system.lastRestart = currentTime
system:restartAll()
end
end
if currentTime - system.lastUpdate >= UPDATE_INTERVAL then
system.lastUpdate = currentTime
system:updateAll()
end
end)
Delete the Baseplate.
Drop the script in wherever.