Show code
<think>
Okay, so I need to create an HTML file that animates the A* search algorithm on a canvas. The user specified it should be self-contained, strict limit of 7000 characters, no external libraries, and no comments. Let me start by breaking down the requirements.
First, the HTML structure. It should have a canvas element, and some text elements to display the nodes expanded and path length. Maybe use divs for the text. The canvas needs some styling to be centered and visible.
Then, the JavaScript part. I need to set up the grid, with obstacles, start and goal points. The grid should be randomly generated each time. The A* algorithm needs to be implemented with pathfinding. The animation should use requestAnimationFrame for smoothness. Also, when the path is found or it's impossible, it should restart with a new grid.
Let me think about the grid structure. Each cell can be an object with position, f-score, g-score, h-score, parent, and whether it's an obstacle. The grid can be a 2D array. The obstacles can be set with a certain probability, say 25%, but not on start or goal.
The A* algorithm steps: initialize the open list, start node's g=0, h=heuristic. Then, in a loop, pick the node with the lowest f-score, check if it's the goal. If so, backtrack the path. Otherwise, add neighbors to the open list. Need to track visited nodes (closed list). The neighbors can be up, down, left, right (no diagonals, I think? Probably, since the user didn't specify otherwise).
The animation: each step, draw the grid, color the nodes by their f-score or by state (open, closed, path). The frontier should expand as small squares. Maybe color the current node being processed, then the open nodes with their f-score gradient. The path at the end should be in a distinct color.
Displaying the number of expanded nodes and path length. These can be updated as the algorithm progresses.
Restarting: when the algorithm finishes (either path found or no path), after some delay, generate a new grid, reset all variables, and start again.
So the steps for the code:
1. Set up canvas and ctx.
2. Define grid size, maybe 40x40. The cell size can be computed based on canvas size.
3. Generate grid with random obstacles, start and goal positions (maybe top-left and bottom-right).
4. Implement A* algorithm with functions to handle open list (maybe using a priority queue, but since it's JS, perhaps just an array and find the min each time).
5. Animation loop with requestAnimationFrame. Each frame, process one step of A*, then render.
6. When done, render the path, show numbers, wait, then restart.
Potential issues:
- Performance: with 40x40 grid, it's manageable but need to make sure the animation is smooth. Maybe process one node per frame to prevent it from going too fast.
- Obstacle generation: ensure start and goal are not obstacles and are accessible.
- Pathfinding: correctly calculating the heuristic (manhattan or euclidean?), but Manhattan is easier. Let's use Manhattan distance for h.
- Drawing: Need to fill the cells with appropriate colors. Maybe gradient for f-score, or different colors for open/closed nodes.
- Restarting when no path exists: After a certain number of steps if the open list is empty, then restart.
Implementing the priority queue: Since in each step of A*, we need the node with the lowest f-score, maybe each frame, we find the node with the lowest f in the open list. That's O(n) each time, but for 40x40 grid, that's 1600 nodes. Maybe manageable? Or perhaps optimize with a min-heap later, but first go with the simplest approach.
Let me outline the code structure:
HTML:
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; display: flex; flex-direction: column; align-items: center; padding: 20px; }
canvas { border: 1px solid black; }
div { margin: 5px; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="expanded">Nodes Expanded: 0</div>
<div id="pathLength">Path Length: 0</div>
<script>
// JS code here
</script>
</body>
</html>
Now the JavaScript part.
First, variables:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const gridWidth = 40;
const gridHeight = 40;
let cellSize = 15; // initial, will adjust based on canvas size
But we might need to set canvas size based on grid dimensions. Let's set canvas width and height after grid size is determined.
Wait, maybe better to set canvas to have a fixed size? Like 600x600, which would give cellSize of 15 (600/40). Let me see: 40 cells per side, 15px each. 40*15 = 600. That works. So set canvas to 600x600.
So in HTML:
<canvas id="canvas" width="600" height="600"></canvas>
Then in JS:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const gridWidth = 40, gridHeight = 40;
const cellSize = 15;
Then the grid array:
let grid = Array(gridHeight).fill(0).map(() => Array(gridWidth).fill({})); // need to initialize each cell properly
Wait, better to create each cell with properties:
Each cell will be an object like:
{
x: column, y: row,
f: 0, g: 0, h: 0,
isObstacle: false,
visited: false,
parent: null
}
Wait, but in the grid, maybe a 2D array where each element is such an object. So first, create the grid:
function resetGrid() {
grid = Array(gridHeight).fill().map(() => Array(gridWidth));
// initialize each cell
for (let y=0; y<gridHeight; y++) {
for (let x=0; x<gridWidth; x++) {
grid[y][x] = {
x: x,
y: y,
g: Infinity,
h: 0,
f: Infinity,
isObstacle: Math.random() < 0.25 ? true : false, // 25% chance obstacle
visited: false,
parent: null
};
}
}
// place start and goal
startNode = { x: 0, y: 0 };
goalNode = { x: gridWidth-1, y: gridHeight-1 };
// ensure they are not obstacles
grid[startNode.y][startNode.x].isObstacle = false;
grid[goalNode.y][goalNode.x].isObstacle = false;
}
Wait, but the start and goal positions could be randomized. The user didn't specify, so maybe just place them at the corners? Or better to randomize their positions each time but not on an obstacle.
Alternatively, set startNode to (0,0), goal to (gridWidth-1, gridHeight-1), but make sure they are not obstacles.
But when generating obstacles, they might already be non-obstacle? The line 'isObstacle: Math.random() < 0.25 ? true : false' could have them as obstacles. So after generating, set both start and goal to not be obstacles.
So in resetGrid(), after initializing all cells, set:
grid[startNode.y][startNode.x].isObstacle = false;
Same for goalNode.
Also, maybe the start and goal should be placed randomly but not in obstacle cells.
Alternatively, maybe choose start and goal positions randomly but not on obstacle. Hmm, maybe it's better to set them in corners but ensure they aren't obstacles.
Proceeding with that.
Now, variables needed for A*:
let openList = [];
let closedList = [];
let path = null;
let nodesExpanded = 0;
let pathLength = 0;
let isRunning = false;
Variables for the current step. Maybe in the animation loop, each frame processes one step of the algorithm.
Wait, the A* algorithm is step-based, so each frame, we do one iteration.
Hmm, but the A* algorithm is a loop that continues until the open list is empty or the goal is found. To animate this, we need to break it into steps, so each frame processes part of the loop.
Alternatively, in each frame, process one iteration of the main loop (selecting the best node, expanding it, adding neighbors to open list, etc.)
Wait, the standard A* loop is:
Initialize open list with start node.
While open list is not empty:
current = node in open list with lowest f-score
if current is goal, build path and exit
remove current from open list, add to closed list
for each neighbor:
if neighbor is goal, set parent and exit
if neighbor is obstacle or in closed list, skip
tempG = current.g + 1 (assuming movement cost is 1)
if neighbor not in open list, add to open list, set parent and g and h
else if tempG < neighbor.g, update parent and g
update f and h (h is heuristic)
Wait, the heuristic is the estimated distance from neighbor to goal. Using Manhattan distance: Math.abs(neighbor.x - goal.x) + Math.abs(neighbor.y - goal.y)
Hmm, okay.
So, to make this step-based, perhaps:
Each frame, we process one node from the open list. That way, each frame does a single node's expansion, which can be animated.
So, the main processing would be in a function like stepAStar(), which does one step, and then calls requestAnimationFrame again.
But to structure this, maybe have a function that runs the algorithm step by step.
Now, the animation loop:
function animate() {
if (isRunning) {
stepAStar();
}
render();
requestAnimationFrame(animate);
}
But stepAStar() needs to perform one step of the algorithm.
So, let's structure the algorithm in a way that can be done in steps.
The variables:
let current;
let openList = new MinPriorityQueue(); // but without a library, we can manage as array and find min each time.
Alternatively, we can have an array openList, and each time find the node with lowest f.
Wait, but in each step, we want to process the next node. So, in the step function:
function stepAStar() {
if (openList.length === 0) {
// no path found, wait and restart
isRunning = false;
setTimeout(() => resetGrid(), 2000); // after 2 seconds
return;
}
// get current node (min f-score)
current = getMinFromOpenList();
openList = openList.filter(node => node !== current);
closedList.push(current);
nodesExpanded++;
if (current.x === goalNode.x && current.y === goalNode.y) {
// found path, build path and stop
buildPath(current);
isRunning = false;
setTimeout(() => resetGrid(), 2000); // after showing path for 2 seconds
return;
}
// process neighbors
const neighbors = getNeighbors(current);
for (const neighbor of neighbors) {
if (neighbor.isObstacle || closedList.some(n => n.x === neighbor.x && n.y === neighbor.y)) continue;
const tentativeG = current.g + 1; // movement cost is 1
if (tentativeG < neighbor.g) {
neighbor.parent = current;
neighbor.g = tentativeG;
neighbor.f = neighbor.g + neighbor.h;
if (!openList.some(n => n.x === neighbor.x && n.y === neighbor.y)) {
openList.push(neighbor);
}
}
}
}
Wait, but this is all in one step, which might take too long for one frame. So perhaps split processing each neighbor into steps?
Alternatively, process one neighbor per step? That would make it very slow but animate each move.
Alternatively, process the current node's expansion in one step, but process all neighbors at once. The animation would then need to show the current node being processed, and the neighbors added to the open list.
Hmm, maybe the step function can be structured to process the current node, then the next step processes the next node.
But how to track the state? Perhaps the algorithm is in a state machine. Maybe the step function does one iteration of the loop.
Alternatively, have a flag that indicates whether the algorithm is running, and in each frame step, it does one iteration.
Alternatively, the A* loop can be broken into steps, with each step doing one iteration. Let's try:
In the stepAStar function:
if (openList is empty) { reset }
current = find node with smallest f.
remove from open list, add to closed list.
if current is goal:
build path, schedule reset.
else:
process each neighbor:
but process all neighbors at once, since each step would take too much time otherwise.
Wait, but neighbors can be many. So maybe process one neighbor per step?
That would take a lot of steps, but could make the animation more detailed.
Alternatively, process the current node's expansion (neighbors) all at once, but only do one step of the algorithm per frame.
Wait, perhaps it's better to have the A* algorithm run in steps so that each frame does one expansion step (i.e., selects the next node and processes it), but the neighbor processing is done in full. That way the animation can show the frontier expanding step by step.
Alternatively, the problem is that processing all neighbors in one step would not allow the animation to show them. To have smooth animation, each neighbor addition could be a step, but that would require storing the neighbors to process one by one.
This is getting complicated. Maybe to simplify, in each frame, process one node from the open list, and process all its neighbors in that step. This way, each frame shows the current node being expanded and all its neighbors added. The animation would still be manageable as the grid isn't too big.
Alternatively, in the stepAStar function, process one node and all its neighbors, then the next frame processes the next node.
I think that's manageable.
Now, functions:
function getMinFromOpenList() {
let min = openList[0];
for (let node of openList) {
if (node.f < min.f || (node.f === min.f && node.h < min.h)) {
min = node;
}
}
return min;
}
But since openList is an array, we can loop through it each time to find the minimum.
The neighbors function:
function getNeighbors(node) {
const dirs = [[-1,0], [1,0], [0,-1], [0,1]];
return dirs.map(([dx, dy]) => {
const x = node.x + dx;
const y = node.y + dy;
if (x >=0 && x < gridWidth && y >=0 && y < gridHeight) {
return grid[y][x];
}
return null;
}).filter(n => n !== null);
}
Wait, the grid is as grid[y][x], so need to make sure the indices are correct.
Now, building the path:
function buildPath(endNode) {
path = [];
let current = endNode;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
path.unshift(current); // add start node
pathLength = path.length - 1; // since steps between nodes
// update the display
}
Now, the initial setup:
Reset the grid, set openList to startNode, and start the algorithm.
When starting, after resetting, set openList to startNode, and start the animation loop.
Let me structure the setup:
let startNode, goalNode, grid, openList, closedList, path, pathLength, nodesExpanded, isRunning;
function reset() {
resetGrid();
nodesExpanded = 0;
pathLength = 0;
openList = [];
closedList = [];
const startCell = grid[startNode.y][startNode.x];
startCell.g = 0;
startCell.h = heuristic(startCell);
startCell.f = startCell.g + startCell.h;
openList.push(startCell);
path = null;
isRunning = true;
}
function heuristic(node) {
return Math.abs(node.x - goalNode.x) + Math.abs(node.y - goalNode.y);
}
function startAlgorithm() {
reset();
requestAnimationFrame(animate);
}
So when the page loads, call startAlgorithm().
Wait, but in the script, we need to initialize and start.
Putting all together, in the script section:
(function() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const gridWidth = 40, gridHeight = 40;
const cellSize = 15; // 40*15=600
canvas.width = gridWidth * cellSize;
canvas.height = gridHeight * cellSize;
let grid = [];
let startNode, goalNode;
let openList = [];
let closedList = [];
let path = null;
let nodesExpanded = 0;
let pathLength = 0;
let isRunning = false;
function resetGrid() {
grid = [];
for (let y=0; y<gridHeight; y++) {
grid[y] = [];
for (let x=0; x<gridWidth; x++) {
const isObstacle = Math.random() < 0.25;
grid[y][x] = {
x, y,
g: Infinity,
h: 0,
f: Infinity,
isObstacle: isObstacle,
visited: false,
parent: null
};
}
}
startNode = grid[0][0];
startNode.isObstacle = false;
goalNode = grid[gridHeight-1][gridWidth-1];
goalNode.isObstacle = false;
}
function reset() {
resetGrid();
nodesExpanded = 0;
pathLength = 0;
openList = [];
closedList = [];
const startCell = grid[startNode.y][startNode.x];
startCell.g = 0;
startCell.h = heuristic(startCell);
startCell.f = startCell.g + startCell.h;
openList.push(startCell);
path = null;
isRunning = true;
}
function heuristic(node) {
return Math.abs(node.x - goalNode.x) + Math.abs(node.y - goalNode.y);
}
function getMinFromOpenList() {
let minNode = openList[0];
for (const node of openList) {
if (node.f < minNode.f ||
(node.f === minNode.f && node.h < minNode.h)) {
minNode = node;
}
}
return minNode;
}
function getNeighbors(node) {
const dirs = [[-1,0], [1,0], [0,-1], [0,1]];
return dirs.map(([dx, dy]) => {
const x = node.x + dx;
const y = node.y + dy;
if (x >=0 && x < gridWidth && y >=0 && y < gridHeight) {
return grid[y][x];
}
return null;
}).filter(n => n !== null);
}
function buildPath(endNode) {
path = [];
let current = endNode;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
path.unshift(current);
pathLength = path.length - 1; // steps between nodes
}
function stepAStar() {
if (openList.length === 0) {
isRunning = false;
setTimeout(reset, 2000);
return;
}
const current = getMinFromOpenList();
openList = openList.filter(node => node !== current);
closedList.push(current);
nodesExpanded++;
if (current.x === goalNode.x && current.y === goalNode.y) {
buildPath(current);
isRunning = false;
setTimeout(reset, 2000);
return;
}
const neighbors = getNeighbors(current);
for (const neighbor of neighbors) {
if (neighbor.isObstacle || closedList.some(n => n.x === neighbor.x && n.y === neighbor.y)) continue;
const tentativeG = current.g + 1;
if (tentativeG < neighbor.g) {
neighbor.parent = current;
neighbor.g = tentativeG;
neighbor.h = heuristic(neighbor);
neighbor.f = neighbor.g + neighbor.h;
if (!openList.some(n => n.x === neighbor.x && n.y === neighbor.y)) {
openList.push(neighbor);
}
}
}
}
function animate() {
if (isRunning) stepAStar();
render();
requestAnimationFrame(animate);
}
function render() {
ctx.clearRect(0,0,canvas.width, canvas.height);
for (let y=0; y<gridHeight; y++) {
for (let x=0; x<gridWidth; x++) {
const node = grid[y][x];
let color;
if (node.isObstacle) {
color = '#000';
} else if (closedList.includes(node)) {
color = '#888';
} else if (openList.includes(node)) {
color = '#0f0';
} else {
color = '#fff';
}
ctx.fillStyle = color;
ctx.fillRect(x*cellSize, y*cellSize, cellSize, cellSize);
}
}
if (path) {
ctx.strokeStyle = '#f00';
ctx.lineWidth = 2;
ctx.beginPath();
path.forEach((node, i) => {
if (i === 0) {
ctx.moveTo(node.x*cellSize + cellSize/2, node.y*cellSize + cellSize/2);
} else {
ctx.lineTo(node.x*cellSize + cellSize/2, node.y*cellSize + cellSize/2);
}
});
ctx.stroke();
}
// UI Elements
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
ctx.fillText(`Nodes Expanded: ${nodesExpanded}`, 10, 20);
ctx.fillText(`Path Length: ${pathLength}`, 10, 40);
}
window.addEventListener('load', function() {
reset();
animate();
});
})();
Wait, but in the animate function, we have to make sure that the initial call to animate starts the animation loop. The window.load event listener calls reset() and then animate(). But in the animate function, requestAnimationFrame is called each time, so that should start the loop.
Hmm, but in the code above, the animate function is called once from the load event, then inside it calls requestAnimationFrame(animate), which should keep it going.
Potential issues:
- The reset() function doesn't clear path, but in buildPath it's handled.
- The heuristic is properly calculated.
- The neighbors are correctly fetched from grid.
- The getMinFromOpenList is O(n) each time, which is okay for small grids (40x40 is manageable).
Testing in imagination: when the grid is reset, the open list has the start node. The first step selects it, processes it, and adds neighbors to the open list. The animation shows the start node as closed (gray), and neighbors in open (green).
When the algorithm completes, the path is drawn in red.
Path is built correctly.
Potential optimizations: precompute the goalNode position, etc.
But this should work.
Now, the HTML part:
The user provided just the script, but in the HTML, the canvas must be present. However, since the code is encapsulated in a function, it should work if the canvas is in the page with id 'canvas'.
So, the final code includes a canvas element and the script.
Putting all together:
The full HTML would be something like:
<!DOCTYPE html>
<html>
<head>
<title>A* Animation</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// the code above
</script>
</body>
</html>
But since the user's question is to create the code, I'll present the JavaScript part as the answer.
Wait, the problem says "Create a JavaScript program that..." and the user just needs to provide the code.
So the final answer is the JavaScript code wrapped in a self-invoking function, handling the canvas and animation.
Looking back, I think I have all the necessary parts. The only thing missing is the initial start, but since the window.load event calls reset() and animate(), it should work.
I'll now present the code.