Below is a 15 × 11 maze. S is the start, the purple corner is the goal. Click any cell to add or remove a wall, then press Step to process one cell at a time or Run to watch the whole search. The yellow cells are the current frontier — exactly the contents of the queue listed under the grid.
<p class="hint">Breadth-first search spreads from the start <b>S</b> like a wave, one layer at a time. Each explored cell shows its distance from S. Click cells to add or remove walls, then press <b>Step</b> or <b>Run</b>. Switch to DFS to see why a stack does not find the shortest path.</p>
<div class="bar">
<label for="mode">Algorithm:</label>
<select id="mode">
<option value="bfs">BFS (queue)</option>
<option value="dfs">DFS (stack)</option>
</select>
</div>
<div id="grid" class="grid" role="grid" aria-label="Maze grid; click a cell to toggle a wall"></div>
<div class="stats">
<span>Explored: <b id="nVisited">0</b></span>
<span id="lblFront">In queue:</span> <b id="nFront">1</b>
<span>Path length: <b id="nPath">–</b></span>
</div>
<div class="status" id="status">Edit the walls, then press Step or Run.</div>
<div class="queue">
<div class="qtitle" id="qtitle">Queue, front → back: (x,y) distance</div>
<div id="qitems" class="qitems"></div>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="run" type="button">Run</button>
<button id="reset" type="button" class="ghost">Reset</button>
<button id="maze" type="button" class="ghost">Random maze</button>
<button id="clear" type="button" class="ghost">Clear walls</button>
</div>
<div class="legend">
<span><i class="sw start"></i>Start</span>
<span><i class="sw goal"></i>Goal</span>
<span><i class="sw wall"></i>Wall</span>
<span><i class="sw front"></i>Frontier</span>
<span><i class="sw path"></i>Path found</span>
</div>
/* Colour palette as variables, with a dark-mode variant. */
:root { --fg: #1f2933; --muted: #52606d; --bg: #ffffff; --cell: #eef2f6; --wall: #2f3e4e;
--accent: #1d3557; --front: #ffd166; --path: #e63946; --start: #2a9d8f; --goal: #7b2cbf; }
@media (prefers-color-scheme: dark) {
:root { --fg: #e4e9ef; --muted: #a7b4c2; --bg: #14181d; --cell: #232a33; --wall: #8a9aac;
--accent: #74b3ce; --front: #c9a227; --path: #ff6b6b; --start: #3cc1b1; --goal: #b47ce8; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; }
.hint { font-size: .88rem; color: var(--muted); margin: 0 0 .5rem; line-height: 1.45; }
.bar { display: flex; align-items: center; gap: .5rem; margin-bottom: .45rem; font-size: .9rem; }
select { font: 600 13px system-ui, sans-serif; padding: .25rem .4rem; border-radius: 6px;
border: 1px solid var(--accent); background: var(--bg); color: var(--fg); }
.grid { display: grid; grid-template-columns: repeat(15, 1fr); gap: 1px; width: 100%; max-width: 465px;
background: var(--muted); border: 1px solid var(--muted); user-select: none; touch-action: manipulation; }
.cell { aspect-ratio: 1 / 1; background: var(--cell); display: flex; align-items: center; justify-content: center;
font: 600 clamp(8px, 2.4vw, 11px) ui-monospace, monospace; color: #1f2933; cursor: pointer; overflow: hidden;
transition: background-color .18s; }
.cell.wall { background: var(--wall); }
.cell.front { background: var(--front) !important; }
.cell.cur { outline: 2px solid var(--accent); outline-offset: -2px; }
.cell.path { background: var(--path) !important; color: #fff; }
.cell.start { background: var(--start) !important; color: #fff; }
.cell.goal { background: var(--goal) !important; color: #fff; }
.stats { display: flex; flex-wrap: wrap; gap: .3rem .9rem; font-size: .88rem; margin: .5rem 0 .2rem; }
.status { font-size: .92rem; font-weight: 600; min-height: 2.6em; margin: .25rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
@media (prefers-color-scheme: dark) { .status.ok { color: #5fd38d; } .status.bad { color: #ff8a8a; } }
.queue { font-size: .8rem; margin-bottom: .5rem; }
.qtitle { color: var(--muted); margin-bottom: .2rem; }
.qitems { display: flex; flex-wrap: wrap; gap: 3px; min-height: 1.4em; }
.qitems span { font: 12px ui-monospace, monospace; padding: 1px 5px; border-radius: 4px; background: var(--front); color: #1f2933; }
.qitems span.more { background: transparent; color: var(--muted); }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .8rem; border: 1px solid var(--accent);
background: var(--accent); color: var(--bg); border-radius: 8px; cursor: pointer; }
button.ghost { background: var(--bg); color: var(--accent); }
.legend { display: flex; flex-wrap: wrap; gap: .3rem .8rem; font-size: .78rem; color: var(--muted); margin-top: .55rem; }
.sw { display: inline-block; width: 11px; height: 11px; border-radius: 2px; margin-right: 4px; vertical-align: -1px; }
.sw.start { background: var(--start); } .sw.goal { background: var(--goal); } .sw.wall { background: var(--wall); }
.sw.front { background: var(--front); } .sw.path { background: var(--path); }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const W = 15, H = 11, N = W * H;
const START = 0, GOAL = N - 1; // start at the top-left corner, goal at the bottom-right
// Small seeded random generator, so the first maze is always the same.
function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
// Carve a perfect maze with a randomized depth-first walk on the even cells,
// then knock out extra walls so the maze has loops (and several possible routes).
function makeMaze(rnd) {
const walls = new Array(N).fill(true);
walls[START] = false;
const stack = [START];
while (stack.length) {
const u = stack[stack.length - 1], r = Math.floor(u / W), c = u % W, opts = [];
for (const [dr, dc] of [[-2, 0], [0, 2], [2, 0], [0, -2]]) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < H && nc >= 0 && nc < W && walls[nr * W + nc]) opts.push([nr, nc]);
}
if (!opts.length) { stack.pop(); continue; }
const [nr, nc] = opts[Math.floor(rnd() * opts.length)];
walls[((r + nr) / 2) * W + (c + nc) / 2] = false;
walls[nr * W + nc] = false;
stack.push(nr * W + nc);
}
// Open some walls between corridors to create cycles.
for (let i = 0; i < N; i++) {
const r = Math.floor(i / W), c = i % W;
if (walls[i] && (r % 2) + (c % 2) === 1 && rnd() < 0.25) walls[i] = false;
}
return walls;
}
// Open neighbours of cell i in the order up, right, down, left.
function neighbors(i, walls) {
const r = Math.floor(i / W), c = i % W, out = [];
if (r > 0) out.push(i - W);
if (c < W - 1) out.push(i + 1);
if (r < H - 1) out.push(i + W);
if (c > 0) out.push(i - 1);
return out.filter((j) => !walls[j]);
}
// Search state: dist = layer number (-1 = unseen), parent pointers, explored flags, frontier.
function newSearch(mode) {
const s = { mode, dist: new Array(N).fill(-1), parent: new Array(N).fill(-1), done: new Array(N).fill(false),
frontier: [START], finished: false, found: false, visited: 0, current: -1 };
s.dist[START] = 0;
return s;
}
// One step: take a cell from the frontier (front of the queue for BFS, top of the stack for DFS),
// mark it explored and add its unseen neighbours with distance + 1.
function step(s, walls) {
if (s.finished) return;
if (s.mode === 'dfs') while (s.frontier.length && s.done[s.frontier[s.frontier.length - 1]]) s.frontier.pop();
if (!s.frontier.length) { s.finished = true; return; }
const u = s.mode === 'bfs' ? s.frontier.shift() : s.frontier.pop();
s.done[u] = true; s.visited++; s.current = u;
if (u === GOAL) { s.finished = true; s.found = true; return; }
const nb = neighbors(u, walls);
if (s.mode === 'dfs') nb.reverse(); // so the stack pops neighbours in the same up/right/down/left order
for (const v of nb) {
if (s.mode === 'bfs' ? s.dist[v] < 0 : !s.done[v]) {
s.dist[v] = s.dist[u] + 1; s.parent[v] = u; s.frontier.push(v);
}
}
}
// Follow parent pointers back from the goal to rebuild the path.
function pathTo(s, t) { const p = []; if (s.dist[t] < 0) return p; for (let v = t; v !== -1; v = s.parent[v]) p.push(v); return p.reverse(); }
// Plain BFS to the end, used to compare the DFS path with the true shortest path.
function shortest(walls) {
const s = newSearch('bfs');
while (!s.finished) step(s, walls);
return s.found ? s.dist[GOAL] : -1;
}
// Drawing and buttons.
const $ = (id) => document.getElementById(id);
const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
let walls = makeMaze(mulberry32(7));
let S = newSearch('bfs');
let timer = null;
const cells = [];
function buildGrid() {
const g = $('grid'); g.innerHTML = '';
for (let i = 0; i < N; i++) {
const d = document.createElement('div');
d.className = 'cell'; d.setAttribute('role', 'gridcell');
d.addEventListener('click', () => {
if (i === START || i === GOAL) return;
walls[i] = !walls[i]; resetSearch();
});
g.appendChild(d); cells.push(d);
}
}
function render() {
const onFront = new Set(S.frontier.filter((v) => !S.done[v]));
const path = new Set(S.found ? pathTo(S, GOAL) : []);
let maxD = 1; for (let i = 0; i < N; i++) if (S.done[i] && S.dist[i] > maxD) maxD = S.dist[i];
for (let i = 0; i < N; i++) {
const d = cells[i]; let cls = 'cell'; let txt = ''; d.style.backgroundColor = ''; d.style.color = '';
if (walls[i]) cls += ' wall';
else {
if (S.dist[i] >= 0 && (S.done[i] || onFront.has(i))) txt = String(S.dist[i]);
if (S.done[i]) {
const hue = 200 - Math.round(170 * S.dist[i] / maxD); // colour shifts from blue to orange as the wave moves outward
d.style.backgroundColor = `hsl(${hue}, 65%, ${dark ? 34 : 80}%)`;
if (dark) d.style.color = '#f2f5f8';
}
if (onFront.has(i)) cls += ' front';
if (path.has(i)) cls += ' path';
if (i === S.current && !S.finished) cls += ' cur';
}
if (i === START) { cls += ' start'; txt = 'S'; }
if (i === GOAL) { cls += ' goal'; txt = S.found ? String(S.dist[GOAL]) : 'G'; }
d.className = cls; d.textContent = txt;
}
$('nVisited').textContent = S.visited;
$('nFront').textContent = onFront.size;
$('nPath').textContent = S.found ? S.dist[GOAL] : '–';
$('lblFront').textContent = S.mode === 'bfs' ? "In queue:" : "On stack:";
$('qtitle').textContent = S.mode === 'bfs' ? "Queue, front → back: (x,y) distance" : "Stack, top → bottom: (x,y) depth";
const items = S.frontier.filter((v) => !S.done[v]);
const ordered = S.mode === 'bfs' ? items : items.slice().reverse();
const q = $('qitems'); q.innerHTML = '';
if (!ordered.length) { const e = document.createElement('span'); e.className = 'more'; e.textContent = "(empty)"; q.appendChild(e); }
ordered.slice(0, 12).forEach((v) => {
const e = document.createElement('span');
e.textContent = `(${v % W},${Math.floor(v / W)}) ${S.dist[v]}`;
q.appendChild(e);
});
if (ordered.length > 12) { const e = document.createElement('span'); e.className = 'more'; e.textContent = i18nFmt("+%n% more", { n: ordered.length - 12 }); q.appendChild(e); }
}
function setStatus(t, cls) { const s = $('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
function finishMessage() {
if (!S.found) { setStatus(i18nFmt("The goal is unreachable: the frontier ran empty after %visited% cells.", { visited: S.visited }), 'bad'); return; }
if (S.mode === 'bfs') setStatus(i18nFmt("Goal reached! Shortest path: %len% steps, after exploring %visited% cells.", { len: S.dist[GOAL], visited: S.visited }), 'ok');
else {
const best = shortest(walls);
setStatus(i18nFmt(S.dist[GOAL] === best ? "DFS reached the goal in %len% steps, which happens to be shortest (%best%) on this maze. Try another one." : "DFS reached the goal by a path of %len% steps, but the shortest one has only %best%.",
{ len: S.dist[GOAL], best, visited: S.visited }), S.dist[GOAL] === best ? 'ok' : 'bad');
}
}
function doStep() {
if (S.finished) { stop(); return; }
step(S, walls); render();
if (S.finished) { stop(); finishMessage(); }
else setStatus(i18nFmt(S.mode === 'bfs' ? "Dequeued (%x%,%y%) at distance %d% and enqueued its unseen neighbours." : "Popped (%x%,%y%) at depth %d% and pushed its unvisited neighbours.",
{ x: S.current % W, y: Math.floor(S.current / W), d: S.dist[S.current] }), '');
}
function stop() { if (timer) { clearInterval(timer); timer = null; } $('run').textContent = "Run"; }
function run() {
if (timer) { stop(); return; }
if (S.finished) resetSearch();
timer = setInterval(doStep, 55); $('run').textContent = "Pause";
}
function resetSearch() { stop(); S = newSearch($('mode').value); render(); setStatus("Edit the walls, then press Step or Run.", ''); }
$('step').addEventListener('click', () => { stop(); doStep(); });
$('run').addEventListener('click', run);
$('reset').addEventListener('click', resetSearch);
$('maze').addEventListener('click', () => { walls = makeMaze(Math.random); resetSearch(); });
$('clear').addEventListener('click', () => { walls = new Array(N).fill(false); resetSearch(); });
$('mode').addEventListener('change', resetSearch);
buildGrid(); render();
<p class="hint">Breadth-first search spreads from the start <b>S</b> like a wave, one layer at a time. Each explored cell shows its distance from S. Click cells to add or remove walls, then press <b>Step</b> or <b>Run</b>. Switch to DFS to see why a stack does not find the shortest path.</p>
<div class="bar">
<label for="mode">Algorithm:</label>
<select id="mode">
<option value="bfs">BFS (queue)</option>
<option value="dfs">DFS (stack)</option>
</select>
</div>
<div id="grid" class="grid" role="grid" aria-label="Maze grid; click a cell to toggle a wall"></div>
<div class="stats">
<span>Explored: <b id="nVisited">0</b></span>
<span id="lblFront">In queue:</span> <b id="nFront">1</b>
<span>Path length: <b id="nPath">–</b></span>
</div>
<div class="status" id="status">Edit the walls, then press Step or Run.</div>
<div class="queue">
<div class="qtitle" id="qtitle">Queue, front → back: (x,y) distance</div>
<div id="qitems" class="qitems"></div>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="run" type="button">Run</button>
<button id="reset" type="button" class="ghost">Reset</button>
<button id="maze" type="button" class="ghost">Random maze</button>
<button id="clear" type="button" class="ghost">Clear walls</button>
</div>
<div class="legend">
<span><i class="sw start"></i>Start</span>
<span><i class="sw goal"></i>Goal</span>
<span><i class="sw wall"></i>Wall</span>
<span><i class="sw front"></i>Frontier</span>
<span><i class="sw path"></i>Path found</span>
</div>
/* Colour palette as variables, with a dark-mode variant. */
:root { --fg: #1f2933; --muted: #52606d; --bg: #ffffff; --cell: #eef2f6; --wall: #2f3e4e;
--accent: #1d3557; --front: #ffd166; --path: #e63946; --start: #2a9d8f; --goal: #7b2cbf; }
@media (prefers-color-scheme: dark) {
:root { --fg: #e4e9ef; --muted: #a7b4c2; --bg: #14181d; --cell: #232a33; --wall: #8a9aac;
--accent: #74b3ce; --front: #c9a227; --path: #ff6b6b; --start: #3cc1b1; --goal: #b47ce8; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; }
.hint { font-size: .88rem; color: var(--muted); margin: 0 0 .5rem; line-height: 1.45; }
.bar { display: flex; align-items: center; gap: .5rem; margin-bottom: .45rem; font-size: .9rem; }
select { font: 600 13px system-ui, sans-serif; padding: .25rem .4rem; border-radius: 6px;
border: 1px solid var(--accent); background: var(--bg); color: var(--fg); }
.grid { display: grid; grid-template-columns: repeat(15, 1fr); gap: 1px; width: 100%; max-width: 465px;
background: var(--muted); border: 1px solid var(--muted); user-select: none; touch-action: manipulation; }
.cell { aspect-ratio: 1 / 1; background: var(--cell); display: flex; align-items: center; justify-content: center;
font: 600 clamp(8px, 2.4vw, 11px) ui-monospace, monospace; color: #1f2933; cursor: pointer; overflow: hidden;
transition: background-color .18s; }
.cell.wall { background: var(--wall); }
.cell.front { background: var(--front) !important; }
.cell.cur { outline: 2px solid var(--accent); outline-offset: -2px; }
.cell.path { background: var(--path) !important; color: #fff; }
.cell.start { background: var(--start) !important; color: #fff; }
.cell.goal { background: var(--goal) !important; color: #fff; }
.stats { display: flex; flex-wrap: wrap; gap: .3rem .9rem; font-size: .88rem; margin: .5rem 0 .2rem; }
.status { font-size: .92rem; font-weight: 600; min-height: 2.6em; margin: .25rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
@media (prefers-color-scheme: dark) { .status.ok { color: #5fd38d; } .status.bad { color: #ff8a8a; } }
.queue { font-size: .8rem; margin-bottom: .5rem; }
.qtitle { color: var(--muted); margin-bottom: .2rem; }
.qitems { display: flex; flex-wrap: wrap; gap: 3px; min-height: 1.4em; }
.qitems span { font: 12px ui-monospace, monospace; padding: 1px 5px; border-radius: 4px; background: var(--front); color: #1f2933; }
.qitems span.more { background: transparent; color: var(--muted); }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .8rem; border: 1px solid var(--accent);
background: var(--accent); color: var(--bg); border-radius: 8px; cursor: pointer; }
button.ghost { background: var(--bg); color: var(--accent); }
.legend { display: flex; flex-wrap: wrap; gap: .3rem .8rem; font-size: .78rem; color: var(--muted); margin-top: .55rem; }
.sw { display: inline-block; width: 11px; height: 11px; border-radius: 2px; margin-right: 4px; vertical-align: -1px; }
.sw.start { background: var(--start); } .sw.goal { background: var(--goal); } .sw.wall { background: var(--wall); }
.sw.front { background: var(--front); } .sw.path { background: var(--path); }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const W = 15, H = 11, N = W * H;
const START = 0, GOAL = N - 1; // start at the top-left corner, goal at the bottom-right
// Small seeded random generator, so the first maze is always the same.
function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
// Carve a perfect maze with a randomized depth-first walk on the even cells,
// then knock out extra walls so the maze has loops (and several possible routes).
function makeMaze(rnd) {
const walls = new Array(N).fill(true);
walls[START] = false;
const stack = [START];
while (stack.length) {
const u = stack[stack.length - 1], r = Math.floor(u / W), c = u % W, opts = [];
for (const [dr, dc] of [[-2, 0], [0, 2], [2, 0], [0, -2]]) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < H && nc >= 0 && nc < W && walls[nr * W + nc]) opts.push([nr, nc]);
}
if (!opts.length) { stack.pop(); continue; }
const [nr, nc] = opts[Math.floor(rnd() * opts.length)];
walls[((r + nr) / 2) * W + (c + nc) / 2] = false;
walls[nr * W + nc] = false;
stack.push(nr * W + nc);
}
// Open some walls between corridors to create cycles.
for (let i = 0; i < N; i++) {
const r = Math.floor(i / W), c = i % W;
if (walls[i] && (r % 2) + (c % 2) === 1 && rnd() < 0.25) walls[i] = false;
}
return walls;
}
// Open neighbours of cell i in the order up, right, down, left.
function neighbors(i, walls) {
const r = Math.floor(i / W), c = i % W, out = [];
if (r > 0) out.push(i - W);
if (c < W - 1) out.push(i + 1);
if (r < H - 1) out.push(i + W);
if (c > 0) out.push(i - 1);
return out.filter((j) => !walls[j]);
}
// Search state: dist = layer number (-1 = unseen), parent pointers, explored flags, frontier.
function newSearch(mode) {
const s = { mode, dist: new Array(N).fill(-1), parent: new Array(N).fill(-1), done: new Array(N).fill(false),
frontier: [START], finished: false, found: false, visited: 0, current: -1 };
s.dist[START] = 0;
return s;
}
// One step: take a cell from the frontier (front of the queue for BFS, top of the stack for DFS),
// mark it explored and add its unseen neighbours with distance + 1.
function step(s, walls) {
if (s.finished) return;
if (s.mode === 'dfs') while (s.frontier.length && s.done[s.frontier[s.frontier.length - 1]]) s.frontier.pop();
if (!s.frontier.length) { s.finished = true; return; }
const u = s.mode === 'bfs' ? s.frontier.shift() : s.frontier.pop();
s.done[u] = true; s.visited++; s.current = u;
if (u === GOAL) { s.finished = true; s.found = true; return; }
const nb = neighbors(u, walls);
if (s.mode === 'dfs') nb.reverse(); // so the stack pops neighbours in the same up/right/down/left order
for (const v of nb) {
if (s.mode === 'bfs' ? s.dist[v] < 0 : !s.done[v]) {
s.dist[v] = s.dist[u] + 1; s.parent[v] = u; s.frontier.push(v);
}
}
}
// Follow parent pointers back from the goal to rebuild the path.
function pathTo(s, t) { const p = []; if (s.dist[t] < 0) return p; for (let v = t; v !== -1; v = s.parent[v]) p.push(v); return p.reverse(); }
// Plain BFS to the end, used to compare the DFS path with the true shortest path.
function shortest(walls) {
const s = newSearch('bfs');
while (!s.finished) step(s, walls);
return s.found ? s.dist[GOAL] : -1;
}
// Drawing and buttons.
const $ = (id) => document.getElementById(id);
const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
let walls = makeMaze(mulberry32(7));
let S = newSearch('bfs');
let timer = null;
const cells = [];
function buildGrid() {
const g = $('grid'); g.innerHTML = '';
for (let i = 0; i < N; i++) {
const d = document.createElement('div');
d.className = 'cell'; d.setAttribute('role', 'gridcell');
d.addEventListener('click', () => {
if (i === START || i === GOAL) return;
walls[i] = !walls[i]; resetSearch();
});
g.appendChild(d); cells.push(d);
}
}
function render() {
const onFront = new Set(S.frontier.filter((v) => !S.done[v]));
const path = new Set(S.found ? pathTo(S, GOAL) : []);
let maxD = 1; for (let i = 0; i < N; i++) if (S.done[i] && S.dist[i] > maxD) maxD = S.dist[i];
for (let i = 0; i < N; i++) {
const d = cells[i]; let cls = 'cell'; let txt = ''; d.style.backgroundColor = ''; d.style.color = '';
if (walls[i]) cls += ' wall';
else {
if (S.dist[i] >= 0 && (S.done[i] || onFront.has(i))) txt = String(S.dist[i]);
if (S.done[i]) {
const hue = 200 - Math.round(170 * S.dist[i] / maxD); // colour shifts from blue to orange as the wave moves outward
d.style.backgroundColor = `hsl(${hue}, 65%, ${dark ? 34 : 80}%)`;
if (dark) d.style.color = '#f2f5f8';
}
if (onFront.has(i)) cls += ' front';
if (path.has(i)) cls += ' path';
if (i === S.current && !S.finished) cls += ' cur';
}
if (i === START) { cls += ' start'; txt = 'S'; }
if (i === GOAL) { cls += ' goal'; txt = S.found ? String(S.dist[GOAL]) : 'G'; }
d.className = cls; d.textContent = txt;
}
$('nVisited').textContent = S.visited;
$('nFront').textContent = onFront.size;
$('nPath').textContent = S.found ? S.dist[GOAL] : '–';
$('lblFront').textContent = S.mode === 'bfs' ? "In queue:" : "On stack:";
$('qtitle').textContent = S.mode === 'bfs' ? "Queue, front → back: (x,y) distance" : "Stack, top → bottom: (x,y) depth";
const items = S.frontier.filter((v) => !S.done[v]);
const ordered = S.mode === 'bfs' ? items : items.slice().reverse();
const q = $('qitems'); q.innerHTML = '';
if (!ordered.length) { const e = document.createElement('span'); e.className = 'more'; e.textContent = "(empty)"; q.appendChild(e); }
ordered.slice(0, 12).forEach((v) => {
const e = document.createElement('span');
e.textContent = `(${v % W},${Math.floor(v / W)}) ${S.dist[v]}`;
q.appendChild(e);
});
if (ordered.length > 12) { const e = document.createElement('span'); e.className = 'more'; e.textContent = i18nFmt("+%n% more", { n: ordered.length - 12 }); q.appendChild(e); }
}
function setStatus(t, cls) { const s = $('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
function finishMessage() {
if (!S.found) { setStatus(i18nFmt("The goal is unreachable: the frontier ran empty after %visited% cells.", { visited: S.visited }), 'bad'); return; }
if (S.mode === 'bfs') setStatus(i18nFmt("Goal reached! Shortest path: %len% steps, after exploring %visited% cells.", { len: S.dist[GOAL], visited: S.visited }), 'ok');
else {
const best = shortest(walls);
setStatus(i18nFmt(S.dist[GOAL] === best ? "DFS reached the goal in %len% steps, which happens to be shortest (%best%) on this maze. Try another one." : "DFS reached the goal by a path of %len% steps, but the shortest one has only %best%.",
{ len: S.dist[GOAL], best, visited: S.visited }), S.dist[GOAL] === best ? 'ok' : 'bad');
}
}
function doStep() {
if (S.finished) { stop(); return; }
step(S, walls); render();
if (S.finished) { stop(); finishMessage(); }
else setStatus(i18nFmt(S.mode === 'bfs' ? "Dequeued (%x%,%y%) at distance %d% and enqueued its unseen neighbours." : "Popped (%x%,%y%) at depth %d% and pushed its unvisited neighbours.",
{ x: S.current % W, y: Math.floor(S.current / W), d: S.dist[S.current] }), '');
}
function stop() { if (timer) { clearInterval(timer); timer = null; } $('run').textContent = "Run"; }
function run() {
if (timer) { stop(); return; }
if (S.finished) resetSearch();
timer = setInterval(doStep, 55); $('run').textContent = "Pause";
}
function resetSearch() { stop(); S = newSearch($('mode').value); render(); setStatus("Edit the walls, then press Step or Run.", ''); }
$('step').addEventListener('click', () => { stop(); doStep(); });
$('run').addEventListener('click', run);
$('reset').addEventListener('click', resetSearch);
$('maze').addEventListener('click', () => { walls = makeMaze(Math.random); resetSearch(); });
$('clear').addEventListener('click', () => { walls = new Array(N).fill(false); resetSearch(); });
$('mode').addEventListener('change', resetSearch);
buildGrid(); render();
Comments
Loading comments...