Maze generator & solver · grok-build-0.1
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.
transcript · 12 messages
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.
thinking
The task is: "Build a single-page web app and write it ALL into one self-contained file named index.html in the current directory: CSS in a <style> tag, JS in a <script> tag, inline — do not create se...The task is to build a single-page web app in one index.html file with CSS in <style>, JS in <script>, no externals. It should generate a random maze with recursive-backtracker on a canvas and animate A-star or BFS solver from start to finish.
I need to implement:
1. Maze generation 2D grid, recursive backtracker.
2. Pathfinding A* or BFS.
3. Animate on canvas.
4. UI controls: generate, solve, size, etc.
5. All in one HTML file.
I'll sketch the code structure.
HTML:
<!DOCTYPE html>
<html>
<head>
<title>Maze Solver 2D</title>
<style> /* CSS here */ </style>
</head>
<body>
<canvas id="mazeCanvas" width="500" height="500"></canvas>
<div id="controls">
<button id="generateBtn">Generate New Maze</button>
<button id="solveBtn">Solve Maze</button (A*)
<select id="gridSize">
<option value="10">10x10</option>
<option value="20">20x20</option>
<option value="30">30x30</option>
</select>
<label>Speed: <input type="range" id="speedSlider" min="0" max="100" value="50"></label>
<div id="status">Ready</div>
</div>
<script> /* JS here */ </script>
</body>
</html>
CSS: simple, body {font-family: Arial; text-align:center;} canvas {border:1px solid black; background:white;} controls {margin-top:10px;}
For JS:
const canvas = document.getElementById('mazeCanvas');
const ctx = canvas.getContext('2d');
let cellSize = 20; // base, adjust with size
let rows, cols;
let cells = [];
let start = {x:0 y:0};
let goal = {x:cols-1, y:rows-1};
let mazeGenerated = false;
let solving = false;
let speed = 50; // delay ms
let animationTimeout = null;
let path = [];
let openSet = [];
let cameFrom = new Map();
let gScore = new Map();
let fScore = new Map();
let visited = new Set();
let searchSteps = []; // for animation
// dirs
const dirs = [
{dx:0,dy:-1,wall:0,opp:2}, //N
{dx:1,dy: 0,wall:1,opp:3}, //E
{dx:0,dy:1,wall:2,opp:0}, //S
{dx:-1,dy:0,wall:3,opp:1} //W
];
function initCanvas() {
canvas.width = cols * cellSize;
canvas.height = rows * cellSize;
}
function generateMaze() {
rows = parseInt(document.getElementById('gridSize').value);
cols = rows; // square
cellSize = Math.max(10, 500 / rows); // fit
canvas width = cols * cellSize etc.
cells = [];
for(let y=0; y<rows; y++) {
cells[y] = [];
for(let x=0; x<cols; x++) {
cells[y][x] = {x, y, walls: [true,true,true true], visited:false};
}
}
// recursive backtracker iterative
let stack = [];
let current = cells[0][0];
current.visited = true;
stack.push(current);
while(stack.length >0) {
let neighbors = [];
for(let d of dirs) {
let nx = current.x + d.dx;
let ny = current.y + d.dy;
if(nx>=0 && nx<cols && ny>=0 && ny<rows && !cells[ny][nx].visited) {
neighbors.push({cell: cells[ny][nx], dir: d});
}
}
if(neighbors.length >0) {
let nextInfo = neighbors[Math.floor(Math.random()*neighbors.length)];
removeWall(current, nextInfo.cell, nextInfo.dir);
current.visited = true;
stack.push(current);
current = nextInfo.cell;
} else {
current = stack.pop();
}
}
mazeGenerated = true;
resetSolver();
draw();
document.getElementById('status').innerText = 'Maze generated';
}
function removeWall(a, b, dirInfo) {
let dx = b.x - a.x;
let dy = b .y - a.y;
if(dx ==1) { a.walls[1] =false; b.walls[3]=false; } //E W
else if(dx==-1) { a.walls[3]=false; b.walls[1]=false; }
else if(d y==1) { a.walls[2]=false; b.walls[0]=false; }
else if(dy==-1) { a.walls[0]=false; b.walls[2]=false; }
}
function draw() {
ctx.clearRect(0,0,canvas.width ,canvas.height);
ctx.fillStyle = '#ffffff';
ctx.fillRect(0,0,canvas.width,canvas.height);
// draw walls
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
for(let y=0; y<rows; y++) for(let x=0; x<cols; x++) {
let cell = cells[y][x];
let px = x*cellSize;
let py = y*cellSize;
if(cell.walls[0]) { ctx.beginPath(); ctx.moveTo(px,py); ctx.lineTo(px+cell ize,py); ctx.stroke(); }
if(cell.walls[1]) { ctx.beginPath(); ctx.moveTo(px+cellSize,py); ctx.lineTo(px+cellSize,py+cellSize); ctx.stroke(); }
if(cell.walls[2]) { ctx.beginPath(); ctx.moveTo (px,py+cellSize); ctx.lineTo(px+cellSize,py+cellSize); ctx.stroke(); }
if(cell.walls[3]) { ctx.beginPath(); ctx.moveTo(px,py); ctx.lineTo(px,py+cellSize); ctx.stroke(); }
}
// draw start goal
ctx.fillStyle = '#00ff00';
ctx.beginPath();
ctx.arc(start.x*cellSize + cellSize/2, start.y*cellSize + cellSize/2, cellSize/4,0,Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ff0000';
ctx.beginPath();
ctx.arc(goal.x*cellSize + cellSize/2, goal.y*cellSize + cellSize/2, cellSize/4,0,Math.PI*2);
ctx.fill();
// if solving, draw visited etc
if(s olving) {
for(let step of searchSteps.slice(0, currentStep)) {
ctx.fillStyle = '#ffff00';
ctx.fillRect(step.x*cellSize, step.y*cellSize, cellSize, cellSize);
}
}
}
function resetSolver() {
openSet = [cells[0][0]];
cameFrom = new Map();
gScore = new Map();
fScore = new Map();
visited = new Set();
searchSteps = [];
gScore.set(key(start),0);
fScore.set(key(start), manhattan(start,goal));
came From.set(key(start),null);
}
function key(c) { return c.x + ',' + c.y; }
function manhattan(a,b) { return Math.abs(a.x-b.x) + Math.abs(a.y-b.y); }
function solve() {
if(!mazeGenerated) return;
solving = true ;
currentStep = 0;
searchSteps = [];
resetSolver();
let open = [cells[0][0]];
while(open.length >0) {
let curr = open[0];
for(let i=1; i<open.length; i++) if(fScore.get(key (open[i])) < fScore.get(key(curr))) curr = open[i];
open.splice(open.indexOf(curr),1);
searchSteps.push(curr);
if(curr.x == goal.x && curr.y == goal.y) break;
let neighs = getNeigh(curr);
for(let n of neigh s) {
let tent = gScore.get(key(curr)) +1;
if(!gScore.has(key(n)) || tent < gScore.get(key(n))) {
cameFrom.set(key(n), key(curr));
gScore.set(key(n),tent);
fScore.set(key(n),tent + manh (n,goal));
if(!open.includes(n)) open.push(n);
}
}
}
// reconstruct path
path = [];
let currK = key(goal);
while(currK) {
let [x,y] = currK.split(',').map(Number);
path.unshift ({x,y});
currK = cameFrom.get(currK);
}
solving = false;
draw();
document.getElementById('status').{"command":"ls -la /tmp/ws"}total 0 drwxr-sr-x. 2 node node 6 Aug 1 20:18 . drwxrwsrwx. 3 root node 16 Aug 1 20:18 ..
thinking
The task is: "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."
First, I need to implement a recursive backtracker maze generator in JavaScript that runs on a canvas.
I'll represent the maze as a grid of cells, each with walls.
For the canvas, I'll draw the maze with lines for walls.
Then, for solving: BFS or A* to find path from start to end 0,0 to rows-1,cols-1.
Animate by highlighting cells as visited in order.
Use setTimeout for animation speed.
Make it self-contained: all JS in <script>, CSS in <style>.
Let's sketch the code.
HTML structure:
<!DOCTYPE html>
<html>
<head>
<title>Maze Generator 2</title>
<style>
body { font-family: Arial; text-align: center; }
canvas { border: 1px solid black; }
.controls { margin: 10px; }
button, input { margin: 5px; }
</style>
</head>
<body>
<h1>Maze Generator & 2D Solver</h1>
<div class="controls">
<label>Rows: <input type="number" id="rows" min="5" max="50" value="20"></label>
<label>Cols: <input type="number" id="cols" min="5" max=" 50" value="20"></label>
<label>Solver: <select id="solver">
<option value="bfs">BFS</option>
<option value="astar">A*</option>
</select></label>
<label>Speed: <input type="range" id="speed " min="1" max="10" value="5"></label>
<button id="gen">Generate Maze</button>
<button id="solve">Solve</button>
</div>
<canvas id="mazeCanvas" width="600" height="600"></canvas>
<script>
// JS code here </script>
</body>
</html>
Now, in script:
const canvas = document.getElementById('mazeCanvas');
const ctx = canvas.getContext('2d');
let rows = 20;
let cols = 20;
let cellSize = 600 / Math.max(rows,cols); wait, 600 / rows if square? No, make canvas size based on rows cols.
Better: set canvas width = cols * cellSize, but to fixed, let's calculate cellSize = 25, canvas width = cols*25, height = rows*25, but to make it fit, use fixed 600, cell 600/max.
For simplicity, set cellSize = 25, and set canvas width = cols*25, height = rows*25, but since rows cols variable, need to update canvas size.
In code, when gen, set canvas.width = cols * cellSize; etc.
Let's set cellSize = 25 fixed, and if too big, but for max 50, 1250 too big, so use dynamic cellSize.
let cellSize;
function resizeCanvas() {
cellSize = Math.min(600/rows, 600/cols);
canvas.width = cols * cellSize;
canvas.height = rows * cellSize;
}
Call when change rows cols.
But for simplicity, default 20x20, cellSize=30, canvas 600x600.
I'll fix to 20x20 for now, later add sliders.
No, use inputs, and resize.
Let's write code.
First, class Cell {
constructor(x, y) {
this.x = x;
this.y = y;
this.walls = {
top: true,
right: true,
bottom: true,
left: true
};
this.visited = false;
}
}
let grid = [];
function initGrid() {
grid = [];
for (let y = 0; y < rows; y++) {
let row = [];
for (let x = 0; x < cols; x++) {
row.push(new Cell(x, y));
}
grid.push(row);
}
}
function getUn visitedNeighbors(cell) {
let neighbors = [];
const {x, y} = cell;
const neighborsList = [
{dx:0, dy:-1, wall:'top', opp:'bottom'},
{dx:1, dy:0, wall:'right', opp:'left'},
{dx:0, dy:1, wall:'bottom', opp:'top'},
{dx:-1, dy:0, wall:'left', opp:'right'}
];
for (let dir of neighborsList) {
let nx = x + dir.dx;
let ny = y + dir .dy;
if (nx >=0 && nx <cols && ny >=0 && ny <rows && !grid[ny][nx].visited) {
neighbors.push(grid[ny][nx]);
}
}
return neighbors;
}
function removeWalls(current, next) {
let dx = next.x - current.x;
let dy = next.y - current.y;
if (dx === 1) {
current.walls.right = false;
next.walls.left = false;
} else if (dx === -1) {
current.walls.left = false;
next.walls.right = false;
} else if (dy === 1) {
current.walls.bottom = false;
next.walls.top = false;
} else if (dy === -1) {
current.walls.top = false;
next.walls.bottom = false;
}
}
function recursiveBacktracker() {
initGrid();
let stack = [];
let current = grid[0][0];
current.visited = true;
while (true) {
let neighbors = getUnvisitedNeighbors(current);
if (neighbors.length > 0) {
let next = neighbors[Math.floor(Math.random() * neighbors.length)];
removeWalls(current, next);
stack.push(current);
current = next;
current.visited = true;
} else if (stack.length > 0) {
current = stack.pop();
} else {
break;
}
}
}
function drawMaze() {
ctx .clearRect(0,0,canvas.width,canvas.height);
let cs = cellSize;
// draw cells
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let cell = grid[y ][x];
let px = x * cs;
let py = y * cs;
// draw walls
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.beginPath();
if (cell.walls.top) ctx.moveTo(px, py) , ctx.lineTo (px + cs, py);
if (cell.walls.right) ctx.moveTo(px + cs, py) , ctx.lineTo (px + cs, py + cs);
if (cell.walls.bottom) ctx.moveTo(px, py + cs) , ctx.lineTo (px + cs, py + cs);
if (cell.walls.left) ctx.moveTo(px, py) , ctx.lineTo (px, py + cs);
ctx.stroke();
}
}
// start and end
ctx.fillStyle = 'green';
ctx.fillRect(0,0,cs,cs);
ctx.fillStyle = 'red';
ctx.fillRect((cols-1)*cs, (rows-1)*cs, cs, cs);
}
function animateSolve() {
// first compute path
let path = findPath();
if (!path.length) return;
// animate
let i = 0;
let interval = setInterval(() => {
if (i < path.length) {
let cell = path[i];
let px = cell.x * cellSize;
let py = cell.y * cellSize;
ctx.fillStyle = 'yellow';
ctx.fillRect(px, py, cell Size, cellSize);
i++;
} else {
clearInterval(interval);
}
}, parseInt(document.getElementById('speed').value) * 100);
}
function findPath() {
// implement BFS or A*
// return array of cells in path
// for BFS
let queue = [];
let visited = new Set();
let cameFrom = new Map();
let start = {x:0, y:0};
queue.push(start);
visited.add('0,0');
cameFrom.set('0,0', null);
let goal = {x:cols -1, y:rows-1};
let found = false;
while (queue.length > 0) {
let curr = queue.shift();
if (curr.x === goal.x && curr.y === goal.y) {
found = true;
break;
}
let dirs = [[0 ,-1], [1,0], [0,1], [-1,0]];
for (let [dx, dy] of dirs) {
let nx = curr.x + dx;
let ny = curr.y + dy;
if (nx >=0 && nx <cols && ny >= 0 && ny <rows) {
let cell = grid[ny][curr.y wait curr.y ? grid[ny][nx];
// check if wall
// to check wall, need to see if adjacent wall
// for example if dx=1, if cell.walls.left ? No, from curr to next, if curr.walls.right or next.walls.left
if ( (dx ===1 && curr.walls.right) || (dx === -1 && curr.walls.left) || (dy ===1 && curr.walls.bottom) || (dy === -1 && curr.walls.top) ) continue;
let key = `${nx},${ny}`;
if (!visited.has(key)) {
visited.add(key);
queue.push({x:nx, y:ny});
cameFrom./tmp/ws/index.html (18013 bytes)Successfully wrote 18013 bytes to /tmp/ws/index.html
thinking
The file is created. I need to verify it works correctly by testing it. Since it's a self-contained HTML, I can use bash to check if the file exists and perhaps use a simple way to validate it, but th...
{"command":"ls -la /tmp/ws/index.html && head -c 200 /tmp/ws/index.html && echo -e \"\\n... [file truncated for display]\" && tail -c 100 /t-rw-r--r--. 1 node node 18035 Aug 1 20:21 /tmp/ws/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Maze • Recursive Backtracker + Solver</title> <st ... [file truncated for display] ion.visitedOrder.length}`); } // Boot window.onload = init; </script> </body> </html>
thinking
The file has been successfully created. It's a single self-contained HTML file with embedded CSS and JS. No external dependencies.
{"command":"cd /tmp/ws && wc -l index.html && echo \"---\" && grep -c \"recursive backtracker\\|solveAStar\\|solveBFS\\|getUnvisitedNeighbor624 index.html --- 7
thinking
The file is created successfully. It's a complete single-file HTML app with:
{"command":"test -f /tmp/ws/index.html && echo \"✓ index.html present and ready\" || echo \"✗ missing\""}✓ index.html present and ready
thinking
The file has been created successfully. It's a fully self-contained single HTML file with:
Done! A complete single-file `index.html` has been created at `/tmp/ws/index.html`. **Features:** - **Recursive backtracker** maze generation (standard stack-based implementation) - **A*** (Manhattan heuristic) or **BFS** solver, selectable at runtime - Canvas rendering at 600×600 px (adjustable grid 5×5–40×40) - **Animated** step-by-step pathfinding with controllable speed - **Instant** solution mode (shows full explored set + final path immediately) - Start (green/S) and End (red/E) markers - Visual legend for explored cells vs. final path - Stop button + keyboard shortcuts (G/S/I/Esc) Open `index.html` directly in any browser — no network, no external files.
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
EVALUATION
Recursive backtracker maze generates correctly and A* solver animates blue explored cells, though pointer inputs don't trigger it, keys do.
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.