oneshotlm
Compare all models on this prompt →

Maze generator & solver · seed-2.0-lite

Generate a random maze with the recursive-backtracker algorithm on a canvas, then animate an A-star or BFS solver finding the path from start to finish.

⛨ sandboxed Open in new tab ↗
transcript · 7 messages
user

Generate a random maze with the recursive-backtracker algorithm on a canvas, then animate an A-star or BFS solver finding the path from start to finish.

assistant
thinking
Got it, let's tackle this. The user wants a self-contained index.html that generates a maze with recursive backtracker, then animates a solver (A* probably) to find the path. First, I need to structure the single HTML file with canvas, CSS, and all JS.

First, set up the HTML structure: a canvas element, maybe some controls to regenerate or start solving. Wait, no, make it self-contained, no external stuff. Let's start with the basic HTML boilerplate, then add a style tag to center the canvas, style the page a bit. Then the JS that does maze generation, then A* solving with animation.

First, recursive backtracker: that algorithm works by picking a current cell, marking it as visited, then while there are unvisited neighbors, pick a random one, remove the wall between them, move to that cell, recurse. If stuck, backtrack. That's straightforward. I'll represent the grid as a 2D array of cells, each with walls (top, right, bottom, left), visited, etc.

Then A* algorithm: need a heuristic (Manhattan distance, since it's a grid with 4-directional movement). For each node, track g (cost from start), h (heuristic to end), f = g+h. Open set, closed set, reconstruct path once end is reached. Then animate the solver step by step, drawing the current path, the open/closed sets, then the final solution.

Wait, let's set the grid size: let's make cols and rows, say 20x20, cell size 25px, so canvas is 500x500, that's manageable. Start at (0,0), end at (cols-1, rows-1). The walls between cells: when drawing, for each cell, if it has a top wall, draw a line, etc.

First, in the JS, create a Cell class or object. Let's use an array of arrays, each cell is {x, y, walls: {top, right, bottom, left}, visited: false}. Then recursive backtracker function to generate the maze. Then the A* implementation, with each step animated using requestAnimationFrame or setInterval so you can see it progress.

Wait, let's outline the steps:

1. Create the HTML file with canvas, style it.
2. Initialize the canvas context.
3. Set grid parameters: cols=20, rows=20, cellSize=25, canvas width=cols*cellSize, height=rows*cellSize.
4. Generate the maze using recursive backtracker.
5. Draw the initial maze.
6. Run the A* solver with animation, drawing the current state as it solves, then draw the final path.

Wait, also, need to handle the animation so it's smooth, not too fast. Maybe add a small delay between each A* step, like 30ms, so you can see the exploration.

Let's code this all into one index.html. Let's start writing the file. First, the HTML structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Maze Generator & Solver</title>
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            background: #1a1a2e;
            color: #fff;
            font-family: Arial, sans-serif;
        }
        canvas {
            border: 2px solid #fff;
            margin-top: 20px;
        }
        button {
            margin-top: 15px;
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
            background: #16213e;
            color: #fff;
            border: 1px solid #fff;
            border-radius: 5px;
        }
        button:hover {
            background: #0f3460;
        }
    </style>
</head>
<body>
    <h1>Maze Generator & A* Solver</h1>
    <canvas id="mazeCanvas"></canvas>
    <button onclick="reset()">Generate New Maze & Solve</button>

    <script>
        // All JS here
        const canvas = document.getElementById('mazeCanvas');
        const ctx = canvas.getContext('2d');
        const cols = 20;
        const rows = 20;
        const cellSize = 25;
        canvas.width = cols * cellSize;
        canvas.height = rows * cellSize;

        let grid = [];
        let currentCell;
        let stack = [];
        let solving = false;
        let openSet = [];
        let closedSet = [];
        let path = [];
        let start, end;
        let solveInterval;

        // Cell class
        class Cell {
            constructor(x, y) {
                this.x = x;
                this.y = y;
                this.walls = { top: true, right: true, bottom: true, left: true };
                this.visited = false;
                // For A*
                this.g = Infinity;
                this.h = Infinity;
                this.f = Infinity;
                this.previous = undefined;
            }

            draw() {
                const x = this.x * cellSize;
                const y = this.y * cellSize;
                ctx.strokeStyle = '#fff';
                ctx.lineWidth = 2;
                if (this.walls.top) ctx.beginPath(), ctx.moveTo(x,y), ctx.lineTo(x+cellSize,y), ctx.stroke();
                if (this.walls.right) ctx.beginPath(), ctx.moveTo(x+cellSize,y), ctx.lineTo(x+cellSize,y+cellSize), ctx.stroke();
                if (this.walls.bottom) ctx.beginPath(), ctx.moveTo(x+cellSize,y+cellSize), ctx.lineTo(x,y+cellSize), ctx.stroke();
                if (this.walls.left) ctx.beginPath(), ctx.moveTo(x,y+cellSize), ctx.lineTo(x,y), ctx.stroke();

                if (this.visited) {
                    ctx.fillStyle = '#16213e';
                    ctx.fillRect(x+1, y+1, cellSize-2, cellSize-2);
                }
            }

            getNeighbors(grid) {
                const neighbors = [];
                const top = this.y > 0 ? grid[this.x][this.y-1] : undefined;
                const right = this.x < cols-1 ? grid[this.x+1][this.y] : undefined;
                const bottom = this.y < rows-1 ? grid[this.x][this.y+1] : undefined;
                const left = this.x > 0 ? grid[this.x-1][this.y] : undefined;

                if (top && !top.visited) neighbors.push(top);
                if (right && !right.visited) neighbors.push(right);
                if (bottom && !bottom.visited) neighbors.push(bottom);
                if (left && !left.visited) neighbors.push(left);
                return neighbors;
            }

            getAccessibleNeighbors(grid) {
                const neighbors = [];
                const top = this.y > 0 ? grid[this.x][this.y-1] : undefined;
                const right = this.x < cols-1 ? grid[this.x+1][this.y] : undefined;
                const bottom = this.y < rows-1 ? grid[this.x][this.y+1] : undefined;
                const left = this.x > 0 ? grid[this.x-1][this.y] : undefined;

                if (top && !this.walls.top) neighbors.push(top);
                if (right && !this.walls.right) neighbors.push(right);
                if (bottom && !this.walls.bottom) neighbors.push(bottom);
                if (left && !this.walls.left) neighbors.push(left);
                return neighbors;
            }
        }

        function initGrid() {
            grid = [];
            for (let x = 0; x < cols; x++) {
                grid[x] = [];
                for (let y = 0; y < rows; y++) {
                    grid[x][y] = new Cell(x, y);
                }
            }
            currentCell = grid[0][0];
            currentCell.visited = true;
            stack.push(currentCell);
        }

        function removeWalls(a, b) {
            const x = a.x - b.x;
            const y = a.y - b.y;
            if (x === 1) { // a is right of b, remove a's left, b's right
                a.walls.left = false;
                b.walls.right = false;
            } else if (x === -1) { // a is left of b
                a.walls.right = false;
                b.walls.left = false;
            }
            if (y === 1) { // a is below b
                a.walls.top = false;
                b.walls.bottom = false;
            } else if (y === -1) { // a is above b
                a.walls.bottom = false;
                b.walls.top = false;
            }
        }

        function generateMazeStep() {
            if (stack.length > 0) {
                currentCell = stack.pop();
                const neighbors = currentCell.getNeighbors(grid);
                if (neighbors.length > 0) {
                    stack.push(currentCell);
                    const next = neighbors[Math.floor(Math.random() * neighbors.length)];
                    next.visited = true;
                    removeWalls(currentCell, next);
                    stack.push(next);
                }
                drawGrid();
                requestAnimationFrame(generateMazeStep);
            } else {
                // Maze generated, start solving
                startAStar();
            }
        }

        function heuristic(a, b) {
            // Manhattan distance
            return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
        }

        function startAStar() {
            // Reset A* variables
            for (let x = 0; x < cols; x++) {
                for (let y = 0; y < rows; y++) {
                    grid[x][y].g = Infinity;
                    grid[x][y].h = Infinity;
                    grid[x][y].f = Infinity;
                    grid[x][y].previous = undefined;
                }
            }
            start = grid[0][0];
            end = grid[cols-1][rows-1];
            start.g = 0;
            start.h = heuristic(start, end);
            start.f = start.g + start.h;
            openSet = [start];
            closedSet = [];
            solving = true;
            solveInterval = setInterval(solveStep, 30);
        }

        function solveStep() {
            if (openSet.length > 0) {
                // Find node with lowest f in openSet
                let lowestIndex = 0;
                for (let i = 0; i < openSet.length; i++) {
                    if (openSet[i].f < openSet[lowestIndex].f) {
                        lowestIndex = i;
                    }
                }
                let current = openSet[lowestIndex];

                if (current === end) {
                    // Done, reconstruct path
                    clearInterval(solveInterval);
                    path = [];
                    let temp = current;
                    while (temp.previous) {
                        path.push(temp.previous);
                        temp = temp.previous;
                    }
                    solving = false;
                }

                // Remove current from openSet, add to closedSet
                openSet.splice(lowestIndex, 1);
                closedSet.push(current);

                // Check neighbors
                const neighbors = current.getAccessibleNeighbors(grid);
                for (let neighbor of neighbors) {
                    if (!closedSet.includes(neighbor)) {
                        let tempG = current.g + 1;
                        if (openSet.includes(neighbor)) {
                            if (tempG < neighbor.g) {
                                neighbor.g = tempG;
                            }
                        } else {
                            neighbor.g = tempG;
                            openSet.push(neighbor);
                        }
                        neighbor.h = heuristic(neighbor, end);
                        neighbor.f = neighbor.g + neighbor.h;
                        neighbor.previous = current;
                    }
                }

                // Draw everything
                drawGrid();
                // Draw openSet (green)
                ctx.fillStyle = 'rgba(0, 255, 0, 0.5)';
                for (let cell of openSet) {
                    ctx.fillRect(cell.x*cellSize+2, cell.y*cellSize+2, cellSize-4, cellSize-4);
                }
                // Draw closedSet (red)
                ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
                for (let cell of closedSet) {
                    ctx.fillRect(cell.x*cellSize+2, cell.y*cellSize+2, cellSize-4, cellSize-4);
                }
                // Draw current path (blue)
                ctx.fillStyle = 'rgba(0, 0, 255, 0.8)';
                path = [];
                let temp = current;
                while (temp) {
                    path.push(temp);
                    temp = temp.previous;
                }
                for (let cell of path) {
                    ctx.fillRect(cell.x*cellSize+4, cell.y*cellSize+4, cellSize-8, cellSize-8);
                }
                // Mark start and end
                ctx.fillStyle = '#00ff00';
                ctx.fillRect(start.x*cellSize+2, start.y*cellSize+2, cellSize-4, cellSize-4);
                ctx.fillStyle = '#ff00ff';
                ctx.fillRect(end.x*cellSize+2, end.y*cellSize+2, cellSize-4, cellSize-4);
            } else {
                clearInterval(solveInterval);
                alert("No solution found!");
            }
        }

        function drawGrid() {
            ctx.clearRect(0,0,canvas.width,canvas.height);
            for (let x = 0; x < cols; x++) {
                for (let y = 0; y < rows; y++) {
                    grid[x][y].draw();
                }
            }
        }

        function reset() {
            if (solveInterval) clearInterval(solveInterval);
            initGrid();
            generateMazeStep();
        }

        // Initial run
        reset();
    </script>
</body>
</html>

Wait, let's check if this works. Let's see: when reset is called, it initializes the grid, runs the recursive backtracker step by step, draws each cell, then once maze is generated, starts A*. The A* step runs every 30ms, draws open set in green, closed in red, current path in blue, start is green, end is magenta. The removeWalls function correctly removes the walls between cells. The getAccessibleNeighbors for A* only allows moving through cells that don't have a wall between them, which is correct. The heuristic is Manhattan, which is right for grid movement.

Wait, in the solveStep, when we draw the path, we reconstruct it each time, that's okay. When the end is reached, it stops the interval, and the final path is drawn. The button to reset generates a new maze, which is good. All code is in one file, no external resources, canvas is used, all CSS is in the style tag, JS in script tag. Let's write this to index.html in the current directory /tmp/ws.
write /tmp/ws/index.html (9171 bytes)
toolResult

Successfully wrote 9171 bytes to /tmp/ws/index.html

assistant
assistant
assistant
assistant
thinking
Great! The file is written successfully. Let's verify that it's all there. The index.html contains everything needed: the recursive backtracker maze generation, A* solver with animation, all CSS and JS in one file, no external dependencies. The user can open this file in a browser and it will generate a maze, animate the A* solver solving it, and have a button to generate a new maze. That meets all the requirements. I can now confirm the task is complete.

The single-file maze generator and solver is complete! The `/tmp/ws/index.html` file contains: - Recursive-backtracker maze generation algorithm that animates maze creation - A* pathfinding algorithm that animates the solving process with visual cues: - Green cells: Active open set (nodes being evaluated) - Red cells: Closed set (nodes already processed) - Blue cells: Current explored path - Green start cell, magenta end cell - A "Generate New Maze & Solve" button to create and solve a new random maze - All code is self-contained with no external dependencies, runs fully locally in any modern browser.

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modelseed-2.0-lite
vendorByteDance · Seed
harnesspi-agent
generated2026-08-01

EVALUATION

4/5 working

Maze generator with recursive backtracker animates, A* solver visualizes explored (red/blue) cells and path to green/magenta endpoints on click/wheel/keys.

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion0.2%
on click3.9%
on drag1.5%
on wheel2.3%
on enter + space5.8%
on arrow keys0.5%
on w/a/s/d0.0%
frame spread24.6 / 255
console errors0
js errors none

METRICS

runtime86.0s
tokens in9.6k
tokens out6.3k
cached1.3k
cost / run$0.0147