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.

I am using a 2D graphics program to teach JavaScript. It contains a function called: line(x1,y1,x2,y2). The 2D environment has the origin point in the upper left corner and positive values on the y-axis are descending on the computer screen. The width of the draw area is about 400 pixels and the height is about 200 pixels. Draw a triangle with equal sides of length 40. The bottom side of the triangle should be toward the lower end of the screen (i.e., a higher value of y).
Here's a clean, classroom-friendly way to draw an equilateral triangle with side length 40 in your 2D environment.

Because your coordinate system has:

...it's easiest to place the bottom side somewhere near the lower portion of the screen, then compute the top vertex using the geometry of an equilateral triangle.

Geometry Setup
For an equilateral triangle of side length:

Because positive y goes downward, the top vertex has a smaller y value.

Example Code (ready to run)

This places the bottom side at y = 150, comfortably above the bottom of a 200-pixel canvas.

Example:
// Side length of the triangle
const side = 40;

// Bottom side position
const x1 = 180;      // bottom-left x
const y1 = 150;      // bottom y (toward lower part of screen)
const x2 = x1 + side;

// Height of an equilateral triangle
const h = (Math.sqrt(3) / 2) * side;

// Top vertex
const x3 = x1 + side / 2;
const y3 = y1 - h;

// Draw the three sides
line(x1, y1, x2, y1);   // bottom side
line(x1, y1, x3, y3);   // left side
line(x2, y1, x3, y3);   // right side
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.


Here's a clean, modular way to upgrade your triangle-drawing algorithm so that each side is drawn as two equal line segments instead of one. This is a great next step for students because it reinforces midpoint calculations and vector thinking without overwhelming them.

Step 1 -- Start with the same triangle geometry

We'll reuse the equilateral triangle setup:

Step 2 -- Compute midpoints for each side
For any two points (xa,ya) and (xb,yb), the midpoint is: ((xa+xb)/2, (ya+yb)/2)

We'll compute:
Then draw two lines per side.

Full JavaScript Example
This places the triangle near the lower part of the screen (y = 150), just like before.

Example:
// --- Triangle geometry ---
const side = 40;

// Bottom side position
const x1 = 180;
const y1 = 150;
const x2 = x1 + side;

// Height of equilateral triangle
const h = (Math.sqrt(3) / 2) * side;

// Top vertex
const x3 = x1 + side / 2;
const y3 = y1 - h;

// --- Midpoints for each side ---
const midBottomX = (x1 + x2) / 2;
const midBottomY = y1;

const midLeftX = (x1 + x3) / 2;
const midLeftY = (y1 + y3) / 2;

const midRightX = (x2 + x3) / 2;
const midRightY = (y1 + y3) / 2;

// --- Draw bottom side in two segments ---
line(x1, y1, midBottomX, midBottomY);
line(midBottomX, midBottomY, x2, y1);

// --- Draw left side in two segments ---
line(x1, y1, midLeftX, midLeftY);
line(midLeftX, midLeftY, x3, y3);

// --- Draw right side in two segments ---
line(x2, y1, midRightX, midRightY);
line(midRightX, midRightY, x3, y3);
In this particular graphics program, all global variables need to be declared as "var", not "const". I know this isn't the best practice. However, with the way that the graphics API works, this is necessary. Can you re-write it using "var"?

Since your graphics environment treats global scope differently, switching from to is the right move here.
Below is the same midpoint-based triangle algorithm, rewritten so all global variables use "var".

Example:
// --- Triangle geometry ---
var side = 40;

// Bottom side position
var x1 = 180;
var y1 = 150;
var x2 = x1 + side;

// Height of equilateral triangle
var h = (Math.sqrt(3) / 2) * side;

// Top vertex
var x3 = x1 + side / 2;
var y3 = y1 - h;

// --- Midpoints for each side ---
var midBottomX = (x1 + x2) / 2;
var midBottomY = y1;

var midLeftX = (x1 + x3) / 2;
var midLeftY = (y1 + y3) / 2;

var midRightX = (x2 + x3) / 2;
var midRightY = (y1 + y3) / 2;

// --- Draw bottom side in two segments ---
line(x1, y1, midBottomX, midBottomY);
line(midBottomX, midBottomY, x2, y1);

// --- Draw left side in two segments ---
line(x1, y1, midLeftX, midLeftY);
line(midLeftX, midLeftY, x3, y3);

// --- Draw right side in two segments ---
line(x2, y1, midRightX, midRightY);
line(midRightX, midRightY, x3, y3);
Now draw a slightly larger triangle around this one. The larger triangle needs to have the total length of each side as 60 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.
Let's build this up in a way that stays clean, scalable, and very teachable for your students.

You now want:
This is a perfect moment to introduce a reusable helper that:

  1. Takes two endpoints
  2. Divides the line into equal sub-segments
  3. Draws each segment with line()

Then we plug that into a triangle-drawing function.
Below is a clean, fully var-based version that fits your environment.

Step 1 -- A function to draw a subdivided line

Example:
// Draw a line subdivided into (depth + 1) equal segments
function drawSegmentedLine(x1, y1, x2, y2, depth) {
    var segments = depth + 1;

    for (var i = 0; i < segments; i++) {
        var t1 = i / segments;
        var t2 = (i + 1) / segments;

        var sx1 = x1 + (x2 - x1) * t1;
        var sy1 = y1 + (y2 - y1) * t1;

        var sx2 = x1 + (x2 - x1) * t2;
        var sy2 = y1 + (y2 - y1) * t2;

        line(sx1, sy1, sx2, sy2);
    }
}

This uses linear interpolation to compute evenly spaced points along the line.

Step 2 -- A function to draw an equilateral triangle with a given side length and depth

Example:
function drawTriangle(x1, y1, side, depth) {
    // Bottom-left and bottom-right
    var x2 = x1 + side;
    var y2 = y1;

    // Height of equilateral triangle
    var h = (Math.sqrt(3) / 2) * side;

    // Top vertex
    var x3 = x1 + side / 2;
    var y3 = y1 - h;

    // Draw each side using segmented lines
    drawSegmentedLine(x1, y1, x2, y2, depth); // bottom
    drawSegmentedLine(x1, y1, x3, y3, depth); // left
    drawSegmentedLine(x2, y2, x3, y3, depth); // right
}
Step 3 -- Draw the two triangle

Example:
// Small triangle: side = 40, depth = 1 → 2 segments per side
drawTriangle(180, 150, 40, 1);

// Larger triangle: side = 60, depth = 2 → 3 segments per side
drawTriangle(170, 160, 60, 2);
You can adjust the positions so the larger triangle surrounds the smaller one more symmetrically. For example:
Or shift x-values slightly to center them.

After observing the result, I feel that I need to adjust the algorithm slightly.
// Draw a line subdivided into (depth + 1) equal segments 

function drawSegmentedLine(x1, y1, x2, y2, depth) {
  var segments = depth * 2;
  for (var i = 0; i < segments; i++) {
    var t1 = i / segments;
    var t2 = (i + 1) / segments;

    var sx1 = x1 + (x2 - x1) * t1;
    var sy1 = y1 + (y2 - y1) * t1;

    var sx2 = x1 + (x2 - x1) * t2;
    var sy2 = y1 + (y2 - y1) * t2;

    line(sx1, sy1, sx2, sy2);
  } 
}
function drawTriangle(x1, y1, side, depth) {
  // Bottom-left and bottom-right 
  var x2 = x1 + side; 
  var y2 = y1;
  // Height of equilateral triangle
  var h = (Math.sqrt(3) / 2) * side;

  // Top vertex
  var x3 = x1 + side / 2;
  var y3 = y1 - h;

  // Draw each side using segmented lines
  drawSegmentedLine(x1, y1, x2, y2, depth); // bottom
  drawSegmentedLine(x1, y1, x3, y3, depth); // left
  drawSegmentedLine(x2, y2, x3, y3, depth); // right
}
// Small triangle: side = 40, depth = 1 -> 2 segments per side 
drawTriangle(180, 150, 40, 1);
// Larger triangle: side = 60, depth = 2 -> 4 segments per side 
drawTriangle(160, 162, 80, 2);
// Larger triangle: side = 80, depth = 3 -> 6 segments per side 
drawTriangle(140, 174, 120, 3);
// Larger triangle: side = 100, depth = 4 -> 8 segments per side 
drawTriangle(120, 186, 160, 4);
The size of each subsequent triangle increases by 40 pixels. So, we could say the size equals the depth*10. Also the X-coord is 200 - depth * 20 and the Y-coord is 138 + depth * 12
You've got a great pattern emerging here -- you're essentially building a stacked family of equilateral triangles, each one:
Your observation about the pattern is exactly right:
And your new formulas:
This will give you a beautifully scalable system.
Let's rewrite your code so the triangle series is generated automatically from these formulas.

Updated Algorithm With Your New Rules

Example:
// Draw a line subdivided into (depth * 2) equal segments
function drawSegmentedLine(x1, y1, x2, y2, depth) {
    var segments = depth * 2;

    for (var i = 0; i < segments; i++) {
        var t1 = i / segments;
        var t2 = (i + 1) / segments;

        var sx1 = x1 + (x2 - x1) * t1;
        var sy1 = y1 + (y2 - y1) * t1;

        var sx2 = x1 + (x2 - x1) * t2;
        var sy2 = y1 + (y2 - y1) * t2;

        line(sx1, sy1, sx2, sy2);
    }
}

function drawTriangle(x1, y1, side, depth) {
    // Bottom-left and bottom-right
    var x2 = x1 + side;
    var y2 = y1;

    // Height of equilateral triangle
    var h = (Math.sqrt(3) / 2) * side;

    // Top vertex
    var x3 = x1 + side / 2;
    var y3 = y1 - h;

    // Draw each side using segmented lines
    drawSegmentedLine(x1, y1, x2, y2, depth); // bottom
    drawSegmentedLine(x1, y1, x3, y3, depth); // left
    drawSegmentedLine(x2, y2, x3, y3, depth); // right
}

// Draw triangles for depth = 1 through 4
for (var depth = 1; depth <= 4; depth++) {
    var side = depth * 40;
    var x = 200 - depth * 20;
    var y = 138 + depth * 12;

    drawTriangle(x, y, side, depth);
}
What This Version Achieves

Perfectly centered, nested triangles
Each triangle grows outward by 40 pixels per side, and your x/y formulas keep them visually aligned.
Smooth segmentation
Depth 1 -> 2 segments
Depth 2 -> 4 segments
Depth 3 -> 6 segments
Depth 4 -> 8 segments
Fully scalable
You can now draw 10, 20, or 100 triangles by simply increasing the loop.

If you want, we can now:
* Add color gradients
* Animate the triangles growing
* Add rotation
* Turn this into a recursive fractal generator

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 <= 4; 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.
You're building something genuinely fun here -- a geometric maze generator that grows outward in layers, and each layer has exactly one "missing" segment on a specific side (A, B, or C), rotating in sequence. This is a perfect use case for a global counter and a clean Boolean flag passed into your segmented-line function.
Let's build this in a way that's:
* Predictable (A -> B -> C -> A -> etc.)
* Randomized (which segment is missing)
* Scalable (any number of triangle layers)
* Compatible with your -only environment
Below is a polished version of the algorithm you described.

Step 1 -- Update drawSegmentedLine() to support skipping one random segment
We add a parameter skipOne, which tells the function whether this side should have a missing segment.

Example:
function drawSegmentedLine(x1, y1, x2, y2, depth, skipOne) {
    var segments = depth * 2;
    var skipIndex = -1;

    // If this side should skip a segment, choose a random one
    if (skipOne) {
        skipIndex = Math.floor(Math.random() * segments);
    }

    for (var i = 0; i < segments; i++) {

        // Skip this segment if it matches the chosen index
        if (i === skipIndex) {
            continue;
        }

        var t1 = i / segments;
        var t2 = (i + 1) / segments;

        var sx1 = x1 + (x2 - x1) * t1;
        var sy1 = y1 + (y2 - y1) * t1;

        var sx2 = x1 + (x2 - x1) * t2;
        var sy2 = y1 + (y2 - y1) * t2;

        line(sx1, sy1, sx2, sy2);
    }
}
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:
function drawTriangle(x1, y1, side, depth, sideSelector) {
    var x2 = x1 + side;
    var y2 = y1;

    var h = (Math.sqrt(3) / 2) * side;

    var x3 = x1 + side / 2;
    var y3 = y1 - h;

    // Determine which side gets the missing segment
    var skipA = (sideSelector === 1); // bottom
    var skipB = (sideSelector === 2); // left
    var skipC = (sideSelector === 0); // right

    drawSegmentedLine(x1, y1, x2, y2, depth, skipA); // A
    drawSegmentedLine(x1, y1, x3, y3, depth, skipB); // B
    drawSegmentedLine(x2, y2, x3, y3, depth, skipC); // C
}
Step 3 -- Loop through triangle layers

Using your formulas:
* side = depth * 40
* x = 200 - depth * 20
* y = 138 + depth * 12
* sideSelector = depth % 3 (which cycles A -> B -> C)

Example:
for (var depth = 1, sideSelector = 1; depth <= 4; depth++, sideSelector++) {

    var side = depth * 40;
    var x = 200 - depth * 20;
    var y = 138 + depth * 12;

    drawTriangle(x, y, side, depth, sideSelector % 3);
}
What this produces
* Triangle 1 (depth=1)
* Side A (bottom) gets a missing segment
* Triangle 2 (depth=2)
* Side B (left) gets a missing segment
* Triangle 3 (depth=3)
* Side C (right) gets a missing segment
* Triangle 4 (depth=4)
* Back to side A
Each missing segment is chosen randomly, so the maze changes every run.