RoboCatz.com

JavaScript Triangle Maze

What is Vibe Coding?

Vibe Coding is a cool way to write programs with the help of AI (artificial intelligence). Imagine having a super-smart assistant by your side while coding! The AI can help you in different ways -- it might create an entire program for you, or just give you ideas and help with specific parts, depending on what you need.

To get started, you simply talk to the AI and explain what you want your program to do. The AI will then create a rough version of the program for you. After that, you can ask the AI questions, make suggestions, or request changes to add new features or fix things until the program is just the way you want it.

For this project, I will begin the AI chat with simple request about drawing a single triangle. As the conversation progresses, the requests will get more complex.

Example:I am writing code for a Roblox world using a graphics API. This graphics API is stored in a module script in ReplicatedStorage called genericShapes. In my main script, I include this module in the main script using:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local s = require(ReplicatedStorage.genericShapes)

The shape functions contained in this module are: square, rectangle, circle, panel, disc, cube, tube, sphere, wall, and archway. In creating a shape, you need to include the reference to the replicated storage module. For example, to create a square you would use the square's constructor: s.square(). Optional parameters can be passed into the function such as x, y, z, size, color, opacity, material). A useful way of passing these parameters is through the use of a table. For example:
s.square({ x=0, y=5, z=-15, size=10, thickness=0.5, color="red" })
The table here is encapsulated using curly braces and the named values in the table are separated by commas.
Some of the basic numeric parameters that can be passed to this table include: x, y, z, size, radius, length, width, height, thickness.
You can also pass parameters such as position, rotateBy, and orientation which all require Vector3 values. RotateBy and Orientation are synonymous.
You can pass color as a string (color name) or as a Color3 value. Because of variations in the spelling of color names, you should probably utilize Color3 values, unless the color name is a simple primary color.
Transparency, opacity and reflectance are values that range from 0 to 1.
The parameter "anchored" uses a Boolean value.
Material is a parameter that can be passed as an enumerated value such as: Enum.Material.Foil
Some common materials include:
.Plastic, .Wood, .Slate, .Concrete, .CorrodedMetal, .DiamondPlate, .Foil, .Grass, .Ice, .Marble, .Granite, .Brick, .Pebble, .Sand, .Fabric, .SmoothPlastic, .Metal
Shape objects are created with a default orientation (unless you pass a specific orientation to be used). Circles, squares, discs, rectangles are initially rendered as having a vertical appearance (not laying flat on the ground).

The wall and tube shapes require two sets of coordinates. For example:

s.tube({ x1=-10, y1=4, z1=25, x2=10, y2=4, z2=5, size=10, thickness=2, color=Color3.new(1, 0.243137, 0.054902), material=Enum.Material.Wood })
s.wall({x1=0, y1=0, z1=23, x2=20, y2=2, z2=23, thickness=1, material=Enum.Material.Brick, color=Color3.fromRGB(179, 73, 255)})

For the wall, the height of the wall is determined by the difference between the y1 and y2 values. If y1 is 0 and y2 is 2, then the height of the wall will be 2 units. The thickness of the wall indicates how wide it should be as it traverses from x1,z1 to x2,z2.
The tube function also takes two coordinates. The size parameter indicates the diameter of the tube and the thickness indicates the thickness of the walls of the tube.

The common parameters passed to the s.archway() function include: { x, z, width, height, thickness, material, orientation }
If using "orientation" within the s.archway(), just rotate on the y-axis to turn the archway to a particular degree.
The y-coordinate represents the middle of the archway which will be height/2. If you pass a zero for the y-coordinate, the API will just initialize it to height/2.

The API supports a variety of methods common to all basic shape functions. These methods can be chained. For example:

s.sphere():fill(randomColor()):setMaterial(Enum.Material.CorrodedMetal)
or
s.cube():rotate(45, 45, 0):setColor(randomColor()):setMaterial(Enum.Material.Marble):opacity(0.90)

The list of methods includes:
:anchored(boolean)
:setAnchored(boolean)

:color(Color3)
:setColor(Color3)
:fill(Color3)
:fillColor(Color3)

:material(Enum.Material)
:setMaterial(Enum.Material)

:rotate(Vector3 | number, y: number?, z: number?)
:rotateBy(Vector3 | number, y: number?, z: number?)

:size(number)
:setSize(number)
:reSize(number)

:opacity(number)
:setOpacity(number)

:transparency(number)
:setTransparency(number)

:moveBy(Vector3 | number, y: number?, z: number?)
:moveTo(Vector3 | number, y: number?, z: number?)
:subtract(ShapeObject | Part, destroySubPart: boolean?)
:cut(ShapeObject | Part, destroySubPart: boolean?)
:join(ShapeObject | Part | {ShapeObject | Part})
:merge(ShapeObject | Part | {ShapeObject | Part})
:union(ShapeObject | Part | {ShapeObject | Part})
:intersect(ShapeObject | Part | {ShapeObject | Part})

As you can see from this list of methods, there are often several aliases for the method name. For example: :color(), :setcolor(), :fill(), :fillColor() all perform the same method.


Create an enclosed area using 3 walls. The walls should be of equal length. When looking at it from above, it should appear as a triangle made from the wall function. The wall function takes two sets of coordinates (x1, y1, z1) and (x2, y2, z2). The difference between y1 and y2 represents the height of the wall. The origin point in the world (0,0,0) should be about the middle of the triangle of walls. The length of each wall should be 100 units. The height of the wall should be 10 units. The thickness of the wall should be 3 units. The material should be brick.
Here's a clean, robust implementation of a triangular enclosure made from three 100-unit walls, centered so that (0,0,0) sits roughly in the middle of the triangle.
Everything is expressed using your genericShapes API and follows your conventions for wall height, thickness, and material.


Core Geometry (Equilateral Triangle)
To make all three walls exactly 100 units long, we use an equilateral triangle.
For an equilateral triangle of side length
100:
Height (h) = sqrt(3)/2 * 100 ~~ 86.6

A convenient coordinate layout:

Example Code (ready to run)

Example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local s = require(ReplicatedStorage.genericShapes)

-- Triangle vertices (centered around origin)
local A = { x =   0, z =  57.735 }
local B = { x = -50, z = -28.867 }
local C = { x =  50, z = -28.867 }

-- Common wall parameters
local y1 = 0
local y2 = 10
local thickness = 3
local material = Enum.Material.Brick

-- Wall AB
s.wall({
    x1 = A.x, y1 = y1, z1 = A.z,
    x2 = B.x, y2 = y2, z2 = B.z,
    thickness = thickness,
    material = material
})

-- Wall BC
s.wall({
    x1 = B.x, y1 = y1, z1 = B.z,
    x2 = C.x, y2 = y2, z2 = C.z,
    thickness = thickness,
    material = material
})

-- Wall CA
s.wall({
    x1 = C.x, y1 = y1, z1 = C.z,
    x2 = A.x, y2 = y2, z2 = A.z,
    thickness = thickness,
    material = material
})
Can you add color using the :color() method? Pass a Color3 value that looks reddish.
Absolutely -- here's the same triangular enclosure, now using the :color() method with a reddish Color3 value applied to each wall.

I'll use a warm red tone like:

Example:
Color3.fromRGB(200, 60, 50)
Updated Code with Color3 added (ready to run)

Example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local s = require(ReplicatedStorage.genericShapes)

-- Triangle vertices (centered around origin)
local A = { x =   0, z =  57.735 }
local B = { x = -50, z = -28.867 }
local C = { x =  50, z = -28.867 }

-- Common wall parameters
local y1 = 0
local y2 = 10
local thickness = 3
local material = Enum.Material.Brick
local reddish = Color3.fromRGB(200, 60, 50)

-- Wall AB
s.wall({
    x1 = A.x, y1 = y1, z1 = A.z,
    x2 = B.x, y2 = y2, z2 = B.z,
    thickness = thickness,
    material = material
}):color(reddish)

-- Wall BC
s.wall({
    x1 = B.x, y1 = y1, z1 = B.z,
    x2 = C.x, y2 = y2, z2 = C.z,
    thickness = thickness,
    material = material
}):color(reddish)

-- Wall CA
s.wall({
    x1 = C.x, y1 = y1, z1 = C.z,
    x2 = A.x, y2 = y2, z2 = A.z,
    thickness = thickness,
    material = material
}):color(reddish)
Excellent. Now create a slightly more complex algorithm that divides each side into two equal line segments. So, instead of drawing one line for each side, you will draw two lines--a left half and a right half.


Great -- now we'll upgrade the triangular enclosure so that each wall is drawn as two equal wall segments instead of one long wall. This is a clean geometric extension of what you already built.

Below is a modular, scalable approach that:
This keeps the logic teachable and extensible for future "depth-based" subdivision.


Step 1 -- Triangle Geometry (same as before)
We keep the same equilateral triangle coordinates:
A = top
B = bottom-left
C = bottom-right
Each side is 100 units long.

Example:
local A = { x =   0, z =  57.735 }
local B = { x = -50, z = -28.867 }
local C = { x =  50, z = -28.867 }
Step 2 -- Helper function to draw a wall in two equal segments
This function:
Takes two endpoints
Computes the midpoint
Draws two wall segments
Applies color and material

Example:
local function drawSplitWall(p1, p2, y1, y2, thickness, material, color)
    -- midpoint
    local mid = {
        x = (p1.x + p2.x) / 2,
        z = (p1.z + p2.z) / 2
    }

    -- first half
    s.wall({
        x1 = p1.x, y1 = y1, z1 = p1.z,
        x2 = mid.x, y2 = y2, z2 = mid.z,
        thickness = thickness,
        material = material
    }):color(color)

    -- second half
    s.wall({
        x1 = mid.x, y1 = y1, z1 = mid.z,
        x2 = p2.x, y2 = y2, z2 = p2.z,
        thickness = thickness,
        material = material
    }):color(color)
end
Step 3 -- Use the helper to draw all three sides

Example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local s = require(ReplicatedStorage.genericShapes)

-- Triangle vertices
local A = { x =   0, z =  57.735 }
local B = { x = -50, z = -28.867 }
local C = { x =  50, z = -28.867 }

-- Wall parameters
local y1 = 0
local y2 = 10
local thickness = 3
local material = Enum.Material.Brick
local reddish = Color3.fromRGB(200, 60, 50)

-- Helper function (as defined above)
local function drawSplitWall(p1, p2, y1, y2, thickness, material, color)
    local mid = {
        x = (p1.x + p2.x) / 2,
        z = (p1.z + p2.z) / 2
    }

    s.wall({
        x1 = p1.x, y1 = y1, z1 = p1.z,
        x2 = mid.x, y2 = y2, z2 = mid.z,
        thickness = thickness,
        material = material
    }):color(color)

    s.wall({
        x1 = mid.x, y1 = y1, z1 = mid.z,
        x2 = p2.x, y2 = y2, z2 = p2.z,
        thickness = thickness,
        material = material
    }):color(color)
end

-- Draw all three sides as two segments each
drawSplitWall(A, B, y1, y2, thickness, material, reddish)
drawSplitWall(B, C, y1, y2, thickness, material, reddish)
drawSplitWall(C, A, y1, y2, thickness, material, reddish)
Now draw a slightly larger triangle around this one. The larger triangle needs to have the total length of each side as 150 pixels. As for the line segments, this larger triangle should have 3 segments instead of 2. So, it might be better to create a function that performs this line drawing using the segments based on the depth of the triangle. The number of segments will always be the depth of the triangle plus 1. The smallest triangle would be 1+1 and the next larger triangle will be 2+1.
Absolutely, now we'll generalize your triangle-drawing logic into a depth-based segmented wall generator, then use it to draw:

Your original triangle (side = 100, depth = 1 -> 2 segments)

A larger triangle around it (side = 150, depth = 2 -> 3 segments)

This gives you a scalable pattern for any number of triangle layers.


Core Idea
For any triangle:

depth determines the number of segments per side
segments = depth + 1
Each side is subdivided into equal-length wall pieces
Each piece is drawn using s.wall()

This is exactly the pattern you described.

Step 1 -- Utility: subdivide a line into N equal segments

Example:
local function subdivide(p1, p2, segments)
    local pts = {}

    for i = 0, segments do
        local t = i / segments
        pts[i+1] = {
            x = p1.x + (p2.x - p1.x) * t,
            z = p1.z + (p2.z - p1.z) * t
        }
    end

    return pts
end

This returns an array of points including both endpoints.



Step 2 -- Draw a segmented wall side

Example:
local function drawSegmentedWall(p1, p2, depth, y1, y2, thickness, material, color)
    local segments = depth + 1
    local pts = subdivide(p1, p2, segments)

    for i = 1, segments do
        s.wall({
            x1 = pts[i].x, y1 = y1, z1 = pts[i].z,
            x2 = pts[i+1].x, y2 = y2, z2 = pts[i+1].z,
            thickness = thickness,
            material = material
        }):color(color)
    end
end
Step 3 -- Function to draw a full segmented triangle

Example:
local function drawTriangle(vertices, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.A, vertices.B, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.B, vertices.C, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.C, vertices.A, depth, y1, y2, thickness, material, color)
end
Step 4 -- Define both triangles
Inner triangle (side = 100, depth = 1 → 2 segments)
Coordinates (same as before):

Example:
local inner = {
    A = { x =   0, z =  57.735 },
    B = { x = -50, z = -28.867 },
    C = { x =  50, z = -28.867 }
}
Outer triangle (side = 150)
For an equilateral triangle:

Example:
local outer = {
    A = { x =   0,   z =  86.602 },
    B = { x = -75,   z = -43.301 },
    C = { x =  75,   z = -43.301 }
}
These points are exactly 150 units apart.


Step 5 -- Final combined script

Example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local s = require(ReplicatedStorage.genericShapes)

-- Utility: subdivide a line into N equal segments
local function subdivide(p1, p2, segments)
    local pts = {}
    for i = 0, segments do
        local t = i / segments
        pts[i+1] = {
            x = p1.x + (p2.x - p1.x) * t,
            z = p1.z + (p2.z - p1.z) * t
        }
    end
    return pts
end

-- Draw a wall side with (depth + 1) segments
local function drawSegmentedWall(p1, p2, depth, y1, y2, thickness, material, color)
    local segments = depth + 1
    local pts = subdivide(p1, p2, segments)

    for i = 1, segments do
        s.wall({
            x1 = pts[i].x, y1 = y1, z1 = pts[i].z,
            x2 = pts[i+1].x, y2 = y2, z2 = pts[i+1].z,
            thickness = thickness,
            material = material
        }):color(color)
    end
end

-- Draw a full segmented triangle
local function drawTriangle(vertices, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.A, vertices.B, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.B, vertices.C, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.C, vertices.A, depth, y1, y2, thickness, material, color)
end

-- Inner triangle (side 100, depth 1 → 2 segments)
local inner = {
    A = { x =   0, z =  57.735 },
    B = { x = -50, z = -28.867 },
    C = { x =  50, z = -28.867 }
}

-- Outer triangle (side 150, depth 2 → 3 segments)
local outer = {
    A = { x =   0,   z =  86.602 },
    B = { x = -75,   z = -43.301 },
    C = { x =  75,   z = -43.301 }
}

-- Shared parameters
local y1 = 0
local y2 = 10
local thickness = 3
local material = Enum.Material.Brick
local reddish = Color3.fromRGB(200, 60, 50)

-- Draw both triangles
drawTriangle(inner, 1, y1, y2, thickness, material, reddish)
drawTriangle(outer, 2, y1, y2, thickness, material, reddish)
Can you create a function so that multiple levels of triangles can be built? The smallest has a side with length of 50. Then the side of each additional level if 50 more than the previous level and the number of segments increases by 1 for each additional level. I would like to get to 7 levels of triangles.
Absolutely, this is the perfect moment to generalize everything into a multi-level triangular framework generator.
You'll get 7 concentric equilateral triangles, each:
Centered around the origin
Increasing in side length by 50 units per level
Increasing in segmentation depth by 1 per level
Using your wall-based segmented drawing system

This becomes a clean, scalable geometric engine.


Step 1 -- Function to compute triangle vertices
All triangles are centered around the origin.

Example:
local function computeTriangle(side)
    local h = (math.sqrt(3)/2) * side

    return {
        A = { x = 0,        z =  h/2 },
        B = { x = -side/2,  z = -h/2 },
        C = { x =  side/2,  z = -h/2 }
    }
end
Step 2 -- Subdivide a line into N equal segments

Example:
local function subdivide(p1, p2, segments)
    local pts = {}
    for i = 0, segments do
        local t = i / segments
        pts[i+1] = {
            x = p1.x + (p2.x - p1.x) * t,
            z = p1.z + (p2.z - p1.z) * t
        }
    end
    return pts
end
Step 3 -- Draw a segmented wall side

Example:
local function drawSegmentedWall(p1, p2, depth, y1, y2, thickness, material, color)
    local segments = depth + 1
    local pts = subdivide(p1, p2, segments)

    for i = 1, segments do
        s.wall({
            x1 = pts[i].x, y1 = y1, z1 = pts[i].z,
            x2 = pts[i+1].x, y2 = y2, z2 = pts[i+1].z,
            thickness = thickness,
            material = material
        }):color(color)
    end
end
Step 4 -- Draw a full segmented triangle

Example:
local function drawTriangle(vertices, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.A, vertices.B, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.B, vertices.C, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.C, vertices.A, depth, y1, y2, thickness, material, color)
end
Step 5 -- Build N levels of triangles
This is the function you asked for.

Example:
local function buildTriangleLevels(levels)
    local y1 = 0
    local y2 = 10
    local thickness = 3
    local material = Enum.Material.Brick
    local reddish = Color3.fromRGB(200, 60, 50)

    for level = 1, levels do
        local side = 50 * level
        local depth = level

        local verts = computeTriangle(side)
        drawTriangle(verts, depth, y1, y2, thickness, material, reddish)
    end
end
Step 6 -- Build 7 levels

Example:
buildTriangleLevels(7)
Complete Program (ready to run)

Example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local s = require(ReplicatedStorage.genericShapes)

---------------------------------------------------------
-- Compute triangle vertices centered around the origin --
---------------------------------------------------------
local function computeTriangle(side)
    local h = (math.sqrt(3)/2) * side

    return {
        A = { x = 0,        z =  h/2 },
        B = { x = -side/2,  z = -h/2 },
        C = { x =  side/2,  z = -h/2 }
    }
end

---------------------------------------------------------
-- Subdivide a line segment into N equal parts          --
---------------------------------------------------------
local function subdivide(p1, p2, segments)
    local pts = {}
    for i = 0, segments do
        local t = i / segments
        pts[i+1] = {
            x = p1.x + (p2.x - p1.x) * t,
            z = p1.z + (p2.z - p1.z) * t
        }
    end
    return pts
end

---------------------------------------------------------
-- Draw one side of a triangle using segmented walls    --
---------------------------------------------------------
local function drawSegmentedWall(p1, p2, depth, y1, y2, thickness, material, color)
    local segments = depth + 1
    local pts = subdivide(p1, p2, segments)

    for i = 1, segments do
        s.wall({
            x1 = pts[i].x, y1 = y1, z1 = pts[i].z,
            x2 = pts[i+1].x, y2 = y2, z2 = pts[i+1].z,
            thickness = thickness,
            material = material
        }):color(color)
    end
end

---------------------------------------------------------
-- Draw a full segmented triangle                       --
---------------------------------------------------------
local function drawTriangle(vertices, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.A, vertices.B, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.B, vertices.C, depth, y1, y2, thickness, material, color)
    drawSegmentedWall(vertices.C, vertices.A, depth, y1, y2, thickness, material, color)
end

---------------------------------------------------------
-- Build multiple levels of triangles                   --
---------------------------------------------------------
local function buildTriangleLevels(levels)
    local y1 = 0
    local y2 = 10
    local thickness = 3
    local material = Enum.Material.Brick
    local reddish = Color3.fromRGB(200, 60, 50)

    for level = 1, levels do
        local side = 50 * level
        local depth = level

        local verts = computeTriangle(side)
        drawTriangle(verts, depth, y1, y2, thickness, material, reddish)
    end
end

---------------------------------------------------------
-- Build 7 levels                                       --
---------------------------------------------------------
buildTriangleLevels(7)
Now I would like to turn this into a very simple maze. Let's assign each side drawn a letter such that the bottom side will be "A" and the left side will be "B" and the right side will be "C". Now, let's select a random line segment from the "A" side and choose not to draw that one segment. Then for the next level of triangle, lets select a random line segment from the "B" side and choose not to draw that one segment. Then for the next level of triangle, lets select a random line segment from the "C" side and do the same. I think a global variable should help keep track of the side that is going to have the missing segment. Then just increment that global variable each time through the loop. Probably, you could include it in the for() loop initialization and increment. For example: for(var depth = 1, side = 1; depth <= 7; depth++, side++) . Then pass the "side" to the drawTriangle() function and use it in a Boolean expression when passing arguments to the drawSegmentedLine() function. Something like: drawSegmentedLine(x1, y1, x2, y2, depth, side%3==1); If the drawSegmentedLine() function receives a "true" as the last parameter, then calculate a random index for the line segment to be skipped and then add an if() condition just before the actual line(sx1, sy1, sx2, sy3) function call.
This is where your framework really starts to feel like a system - we're now layering in maze logic, driven by a rotating side selector (A -> B -> C -> A -> ...), and skipping exactly one segment on the chosen side for each triangle level.

I'll give you a clean, extensible implementation that plugs directly into your existing multi-level triangle generator.

Below is the full upgraded program, with:

Side selector cycling through A, B, C

Each triangle level skipping one random segment on the chosen side

Depth-based segmentation still intact

All triangles centered around the origin

Fully compatible with your genericShapes API

Let's walk through the logic, then I'll show the complete code.

Side Assignment
You defined:
A = bottom side
B = left side
C = right side

We'll map these to the triangle edges:

Example:
A -> BC
B -> CA
C -> AB
This matches the orientation of your coordinate system.


Side Selector Logic
We use:

Example:
for level = 1, levels do
    local sideToSkip = (level % 3)  -- 1=A, 2=B, 0=C
end
Step 2 -- Update to accept the "side selector"

We pass in a number that determines which side gets the missing segment:
* 1 -> A the bottom
* 2 -> B the left
* 0 -> C the right because side % 3 cycles as 1, 2, 0, 1, 2, 0 etc.

Example:
local skipA = (sideToSkip == 1)
local skipB = (sideToSkip == 2)
local skipC = (sideToSkip == 0)
Each side gets exactly one skipped segment.


Skipping a Random Segment
Inside drawSegmentedWall, we add:

A skipOne boolean

If skipOne == true, choose a random segment index

Skip drawing that segment


FULL PROGRAM -- With Maze Logic

Example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local s = require(ReplicatedStorage.genericShapes)

---------------------------------------------------------
-- Compute triangle vertices centered around the origin --
---------------------------------------------------------
local function computeTriangle(side)
    local h = (math.sqrt(3)/2) * side

    return {
        A = { x = 0,        z =  h/2 },
        B = { x = -side/2,  z = -h/2 },
        C = { x =  side/2,  z = -h/2 }
    }
end

---------------------------------------------------------
-- Subdivide a line segment into N equal parts          --
---------------------------------------------------------
local function subdivide(p1, p2, segments)
    local pts = {}
    for i = 0, segments do
        local t = i / segments
        pts[i+1] = {
            x = p1.x + (p2.x - p1.x) * t,
            z = p1.z + (p2.z - p1.z) * t
        }
    end
    return pts
end

---------------------------------------------------------
-- Draw one side of a triangle using segmented walls    --
-- skipOne = true means skip exactly one random segment --
---------------------------------------------------------
local function drawSegmentedWall(p1, p2, depth, y1, y2, thickness, material, color, skipOne)
    local segments = depth + 1
    local pts = subdivide(p1, p2, segments)

    local skipIndex = nil
    if skipOne then
        skipIndex = math.random(1, segments)
    end

    for i = 1, segments do
        if i ~= skipIndex then
            s.wall({
                x1 = pts[i].x, y1 = y1, z1 = pts[i].z,
                x2 = pts[i+1].x, y2 = y2, z2 = pts[i+1].z,
                thickness = thickness,
                material = material
            }):color(color)
        end
    end
end

---------------------------------------------------------
-- Draw a full segmented triangle with maze logic       --
---------------------------------------------------------
local function drawTriangle(vertices, depth, y1, y2, thickness, material, color, sideToSkip)
    -- sideToSkip: 1 = A, 2 = B, 0 = C

    local skipA = (sideToSkip == 1)
    local skipB = (sideToSkip == 2)
    local skipC = (sideToSkip == 0)

    -- A = bottom = B → C
    drawSegmentedWall(vertices.B, vertices.C, depth, y1, y2, thickness, material, color, skipA)

    -- B = left = C → A
    drawSegmentedWall(vertices.C, vertices.A, depth, y1, y2, thickness, material, color, skipB)

    -- C = right = A → B
    drawSegmentedWall(vertices.A, vertices.B, depth, y1, y2, thickness, material, color, skipC)
end

---------------------------------------------------------
-- Build multiple levels of triangles with maze gaps    --
---------------------------------------------------------
local function buildTriangleLevels(levels)
    local y1 = 0
    local y2 = 10
    local thickness = 3
    local material = Enum.Material.Brick
    local reddish = Color3.fromRGB(200, 60, 50)

    for level = 1, levels do
        local side = 50 * level
        local depth = level

        local verts = computeTriangle(side)

        -- rotating side selector: A → B → C → A → ...
        local sideToSkip = level % 3  -- 1=A, 2=B, 0=C

        drawTriangle(verts, depth, y1, y2, thickness, material, reddish, sideToSkip)
    end
end

---------------------------------------------------------
-- Build 7 levels                                       --
---------------------------------------------------------
buildTriangleLevels(7)